├── .coveragerc ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── AUTHORS ├── CHANGELOG.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── admin.rst ├── api.rst ├── changelog.rst ├── conf.py ├── contributing.rst ├── custom_tagging.rst ├── external_apps.rst ├── faq.rst ├── forms.rst ├── getting_started.rst ├── index.rst ├── serializers.rst └── testing.rst ├── requirements ├── docs.txt └── test.txt ├── sample_taggit ├── __init__.py ├── asgi.py ├── fixtures │ ├── 0001_users.json │ ├── 0002_author.json │ ├── 0003_book_type.json │ ├── 0004_books.json │ ├── 0005_tags.json │ ├── 0006_condition_tags.json │ ├── 0007_tags.json │ └── 0008_condition_tagged_item.json ├── library_management │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── library_management │ │ │ ├── author_detail.html │ │ │ ├── author_form.html │ │ │ ├── author_list.html │ │ │ ├── base.html │ │ │ ├── book_detail.html │ │ │ ├── book_form.html │ │ │ ├── book_list.html │ │ │ ├── home_page.html │ │ │ ├── magazine_list.html │ │ │ └── physical_copy_form.html │ ├── templatetags │ │ ├── __init__.py │ │ └── custom_filters.py │ ├── urls.py │ └── views.py ├── make.bat ├── makefile ├── manage.py ├── settings.py ├── urls.py └── wsgi.py ├── setup.cfg ├── setup.py ├── taggit ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── locale │ ├── ar │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── cs │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── da │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── el │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── eo │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fa │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fi │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── he │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── it │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ja │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── nb │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── nl │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pt_BR │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ru │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── tr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── uk │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── zh_Hans │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── management │ └── commands │ │ ├── deduplicate_tags.py │ │ └── remove_orphaned_tags.py ├── managers.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20150616_2121.py │ ├── 0003_taggeditem_add_unique_index.py │ ├── 0004_alter_taggeditem_content_type_alter_taggeditem_tag.py │ ├── 0005_auto_20220424_2025.py │ ├── 0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx.py │ └── __init__.py ├── models.py ├── serializers.py ├── templates │ └── admin │ │ └── taggit │ │ └── merge_tags_form.html ├── utils.py └── views.py ├── tests ├── __init__.py ├── admin.py ├── custom_parser.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200214_1129.py │ ├── 0003_auto_20210310_0918.py │ ├── 0004_auto_20210619_0826.py │ ├── 0005_auto_20210713_2301.py │ ├── 0006_auto_20230622_0834.py │ ├── 0007_alter_multiinheritancelazyresolutionfoodtag_tag_and_more.py │ └── __init__.py ├── models.py ├── serializers.py ├── settings.py ├── templates │ └── tests │ │ └── food_tag_list.html ├── test_admin.py ├── test_deduplicate_tags.py ├── test_forms.py ├── test_models.py ├── test_remove_orphaned_tags.py ├── test_serializers.py ├── test_utils.py ├── tests.py ├── urls.py └── views.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = taggit 4 | 5 | [report] 6 | exclude_lines = 7 | if self.debug: 8 | pragma: no cover 9 | raise NotImplementedError 10 | if __name__ == .__main__.: 11 | ignore_errors = True 12 | omit = 13 | tests/* 14 | taggit/migrations/* 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | if: github.repository == 'jazzband/django-taggit' 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: 3.9 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install -U pip 26 | python -m pip install -U setuptools twine wheel 27 | 28 | - name: Build package 29 | run: | 30 | python setup.py --version 31 | python setup.py sdist --format=gztar bdist_wheel 32 | twine check dist/* 33 | 34 | - name: Upload packages to Jazzband 35 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 36 | uses: pypa/gh-action-pypi-publish@master 37 | with: 38 | user: jazzband 39 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 40 | repository_url: https://jazzband.co/projects/django-taggit/upload 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Python ${{ matrix.python-version }} 8 | runs-on: ubuntu-latest 9 | 10 | # The maximum number of minutes to let a workflow run 11 | # before GitHub automatically cancels it. Default: 360 12 | timeout-minutes: 30 13 | 14 | strategy: 15 | # When set to true, GitHub cancels 16 | # all in-progress jobs if any matrix job fails. 17 | fail-fast: false 18 | 19 | max-parallel: 5 20 | 21 | matrix: 22 | python-version: 23 | - "3.9" 24 | - "3.10" 25 | - "3.11" 26 | - "3.12" 27 | 28 | steps: 29 | - uses: actions/checkout@v2 30 | 31 | - name: Set up Python ${{ matrix.python-version }} 32 | uses: actions/setup-python@v2 33 | with: 34 | python-version: ${{ matrix.python-version }} 35 | 36 | - name: Get pip cache dir 37 | id: pip-cache 38 | run: echo "::set-output name=dir::$(pip cache dir)" 39 | 40 | - name: Cache 41 | uses: actions/cache@v2 42 | with: 43 | path: ${{ steps.pip-cache.outputs.dir }} 44 | key: ${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}-${{ hashFiles('**/tox.ini') }} 45 | restore-keys: | 46 | ${{ matrix.python-version }}-v1- 47 | 48 | - name: Install Python dependencies 49 | run: | 50 | python -m pip install --upgrade pip 51 | python -m pip install --upgrade tox tox-gh-actions 52 | 53 | - name: Tox tests 54 | run: tox -v 55 | 56 | - name: Upload coverage 57 | uses: codecov/codecov-action@v1 58 | with: 59 | name: Python ${{ matrix.python-version }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | db.sqlite3 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # Environments 84 | .env 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | env.bak/ 90 | venv.bak/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | 105 | # PyCharm files 106 | .idea/ 107 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.6.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/psf/black 9 | rev: 24.8.0 10 | hooks: 11 | - id: black 12 | - repo: https://github.com/PyCQA/flake8 13 | rev: 7.1.1 14 | hooks: 15 | - id: flake8 16 | - repo: https://github.com/asottile/pyupgrade 17 | rev: v3.17.0 18 | hooks: 19 | - id: pyupgrade 20 | args: [--py37-plus] 21 | - repo: https://github.com/adamchainz/django-upgrade 22 | rev: 1.21.0 23 | hooks: 24 | - id: django-upgrade 25 | args: [--target-version, "3.2"] 26 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: "3.9" 6 | 7 | python: 8 | install: 9 | - method: pip 10 | path: . 11 | 12 | sphinx: 13 | configuration: docs/conf.py 14 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | django-taggit was originally created by Alex Gaynor. 2 | 3 | django-taggit-serializer was originally created by Paul Oostenrijk. 4 | 5 | The following is a list of much appreciated contributors: 6 | 7 | Nathan Borror 8 | fakeempire 9 | Ben Firshman 10 | Alex Gaynor 11 | Rob Hudson 12 | Carl Meyer 13 | Frank Wiles 14 | Jonathan Buchanan 15 | idle sign 16 | Charles Leifer 17 | Florian Apolloner 18 | Andrew Pryde 19 | John Whitlock 20 | Jon Dufresne 21 | Pablo Olmedo Dorado 22 | Steve Reciao 23 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As contributors and maintainers of the Jazzband projects, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating documentation, 6 | submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in the Jazzband a harassment-free experience 9 | for everyone, regardless of the level of experience, gender, gender identity and 10 | expression, sexual orientation, disability, personal appearance, body size, race, 11 | ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | - The use of sexualized language or imagery 16 | - Personal attacks 17 | - Trolling or insulting/derogatory comments 18 | - Public or private harassment 19 | - Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | - Other unethical or unprofessional conduct 22 | 23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are not 25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing the jazzband 30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently 31 | removed from the Jazzband roadies. 32 | 33 | This code of conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and 38 | investigated and will result in a response that is deemed necessary and appropriate to 39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the 40 | reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/3/0/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing to django-taggit 2 | ============================= 3 | 4 | .. image:: https://jazzband.co/static/img/jazzband.svg 5 | :target: https://jazzband.co/ 6 | :alt: Jazzband 7 | 8 | This is a `Jazzband `_ project. By contributing you agree 9 | to abide by the `Contributor Code of Conduct 10 | `_ and follow the `guidelines 11 | `_. 12 | 13 | Thank you for taking the time to contribute to django-taggit. 14 | 15 | Follow these guidelines to speed up the process. 16 | 17 | Reach out before you start 18 | -------------------------- 19 | 20 | Before opening a new issue, look if somebody else has already started working 21 | on the same issue in the `GitHub issues 22 | `_ and `pull requests 23 | `_. 24 | 25 | Fork the repository 26 | ------------------- 27 | 28 | Once you have forked this repository to your own GitHub account, install your 29 | own fork in your development environment: 30 | 31 | .. code-block:: console 32 | 33 | git clone git@github.com:/django-taggit.git 34 | cd django-taggit 35 | python setup.py develop 36 | 37 | Running tests 38 | ------------- 39 | 40 | django-taggit uses `tox `_ to run tests: 41 | 42 | .. code-block:: console 43 | 44 | tox 45 | 46 | Running the sample application 47 | ------------------------------ 48 | 49 | There is a sample application in ``sample_taggit``. You can run it by doing the following: 50 | 51 | 52 | **Prepare the Database** 53 | ~~~~~~~~~~~~~~~~~~~~~~~~ 54 | 55 | Use the `reset-db` command to prepare your database. This will remove any existing data, run migrations, and load fixtures, including creating a default admin user. 56 | 57 | **On Windows:** 58 | 59 | .. code-block:: console 60 | 61 | cd sample_taggit 62 | call make.bat reset-db 63 | 64 | **On Mac/Linux:** 65 | 66 | .. code-block:: console 67 | 68 | cd sample_taggit 69 | make reset-db 70 | 71 | **Launch the Sample Project** 72 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 73 | 74 | Launch the sample project itself with: 75 | 76 | .. code-block:: console 77 | 78 | sample_taggit/manage.py runserver 79 | 80 | **Default Admin User Login:** 81 | 82 | Username: taggit 83 | 84 | Password: admin 85 | 86 | Follow style conventions (black, flake8, isort) 87 | ----------------------------------------------- 88 | 89 | Check that your changes are not breaking the style conventions with: 90 | 91 | .. code-block:: console 92 | 93 | tox -e black,flake8,isort 94 | 95 | Update the documentation 96 | ------------------------ 97 | 98 | If you introduce new features or change existing documented behavior, please 99 | remember to update the documentation. 100 | 101 | The documentation is located in the ``docs`` directory of the repository. 102 | 103 | To do work on the docs, proceed with the following steps: 104 | 105 | .. code-block:: console 106 | 107 | pip install sphinx 108 | sphinx-build -n -W docs docs/_build 109 | 110 | Add a changelog line 111 | -------------------- 112 | 113 | Even when the change is minor, a changelog line is helpful to both describe 114 | the intent of the change, and to give a heads up to people upgrading. You can 115 | add a line in the ``(Unreleased)`` section of ``CHANGELOG.rst``, along with 116 | any more detailed explanations for more complicated changes. 117 | 118 | Send pull request 119 | ----------------- 120 | 121 | It is now time to push your changes to GitHub and open a `pull request 122 | `_! 123 | 124 | 125 | Release Checklist 126 | ----------------- 127 | 128 | These steps need to happen by a release maintainer. 129 | 130 | To make a release, the following needs to happen: 131 | 132 | - Make sure that ``setup.cfg`` is set up properly w/r/t Python and Django requirements 133 | - Make sure the documentation (``docs/index.rst``) also describes the right Python/Django versions 134 | - Bump the version number in ``taggit/__init__.py`` 135 | - Update the changelog (making sure to add the (Unreleased) section to the top) 136 | - Get those changes onto the ``master`` branch 137 | - Tag the commit with the version number 138 | - CI should then upload a release to be verified through Jazzband 139 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Alex Gaynor, Paul Oostenrijk, and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of django-taggit nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include CHANGELOG.rst 3 | include LICENSE 4 | include README.rst 5 | include setup.cfg 6 | include setup.py 7 | recursive-include taggit *.py 8 | recursive-include taggit/templates *.html 9 | recursive-include taggit/locale *.mo *.po 10 | prune tests 11 | prune sample_taggit 12 | prune docs 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-taggit 2 | ============= 3 | 4 | .. image:: https://jazzband.co/static/img/badge.svg 5 | :target: https://jazzband.co/ 6 | :alt: Jazzband 7 | 8 | .. image:: https://img.shields.io/pypi/pyversions/django-taggit.svg 9 | :target: https://pypi.org/project/django-taggit/ 10 | :alt: Supported Python versions 11 | 12 | .. image:: https://img.shields.io/pypi/djversions/django-taggit.svg 13 | :target: https://pypi.org/project/django-taggit/ 14 | :alt: Supported Django versions 15 | 16 | .. image:: https://github.com/jazzband/django-taggit/workflows/Test/badge.svg 17 | :target: https://github.com/jazzband/django-taggit/actions 18 | :alt: GitHub Actions 19 | 20 | .. image:: https://codecov.io/gh/jazzband/django-taggit/coverage.svg?branch=master 21 | :target: https://codecov.io/gh/jazzband/django-taggit?branch=master 22 | 23 | This is a `Jazzband `_ project. By contributing you agree 24 | to abide by the `Contributor Code of Conduct 25 | `_ and follow the `guidelines 26 | `_. 27 | 28 | ``django-taggit`` a simpler approach to tagging with Django. Add ``"taggit"`` to your 29 | ``INSTALLED_APPS`` then just add a TaggableManager to your model and go: 30 | 31 | .. code:: python 32 | 33 | from django.db import models 34 | 35 | from taggit.managers import TaggableManager 36 | 37 | 38 | class Food(models.Model): 39 | # ... fields here 40 | 41 | tags = TaggableManager() 42 | 43 | 44 | Then you can use the API like so: 45 | 46 | .. code:: pycon 47 | 48 | >>> apple = Food.objects.create(name="apple") 49 | >>> apple.tags.add("red", "green", "delicious") 50 | >>> apple.tags.all() 51 | [, , ] 52 | >>> apple.tags.remove("green") 53 | >>> apple.tags.all() 54 | [, ] 55 | >>> Food.objects.filter(tags__name__in=["red"]) 56 | [, ] 57 | 58 | Tags will show up for you automatically in forms and the admin. 59 | 60 | ``django-taggit`` requires Django 3.2 or greater. 61 | 62 | For more info check out the `documentation 63 | `_. And for questions about usage or 64 | development you can create an issue on Github (if your question is about 65 | usage please add the `question` tag). 66 | -------------------------------------------------------------------------------- /docs/admin.rst: -------------------------------------------------------------------------------- 1 | Using tags in the admin 2 | ======================= 3 | 4 | By default if you have a :class:`TaggableManager` on your model it will show up 5 | in the admin, just as it will in any other form. 6 | 7 | If you are specifying :attr:`ModelAdmin.fieldsets 8 | `, include the name of the 9 | :class:`TaggableManager` as a field:: 10 | 11 | fieldsets = ( 12 | (None, {'fields': ('tags',)}), 13 | ) 14 | 15 | Including tags in ``ModelAdmin.list_display`` 16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 17 | 18 | One important thing to note is that you *cannot* include a 19 | :class:`TaggableManager` in :attr:`ModelAdmin.list_display 20 | `. If you do you'll see an 21 | exception that looks like:: 22 | 23 | AttributeError: '_TaggableManager' object has no attribute 'name' 24 | 25 | This is for the same reason that you cannot include a 26 | :class:`~django.db.models.ManyToManyField`: it would result in an unreasonable 27 | number of queries being executed. 28 | 29 | If you want to show tags in :attr:`ModelAdmin.list_display 30 | `, you can add a custom display 31 | method to the :class:`~django.contrib.admin.ModelAdmin`, using 32 | :meth:`~django.db.models.query.QuerySet.prefetch_related` to minimize queries:: 33 | 34 | class MyModelAdmin(admin.ModelAdmin): 35 | list_display = ['tag_list'] 36 | 37 | def get_queryset(self, request): 38 | return super().get_queryset(request).prefetch_related('tags') 39 | 40 | def tag_list(self, obj): 41 | return u", ".join(o.name for o in obj.tags.all()) 42 | 43 | 44 | Merging tags in the admin 45 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 46 | 47 | Functionality has been added to the admin app to allow for tag "merging". Multiple tags can be selected, and all of their usages will be replaced by the tag that you choose to use. 48 | 49 | To merge your tags follow these steps: 50 | 51 | 1. Navigate to the Tags page inside of the Taggit app 52 | 2. Select the tags that you want to merge 53 | 3. Use the dropdown action list and select `Merge selected tags` and then click `Go` 54 | 4. This will redirect you onto a new page where you can insert the new tag name. 55 | 5. Click `Merge Tags` 56 | 6. This will redirect you back to the tag list 57 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | The API 2 | ======= 3 | 4 | After you've got your ``TaggableManager`` added to your model you can start 5 | playing around with the API. 6 | 7 | .. class:: TaggableManager([verbose_name="Tags", help_text="A comma-separated list of tags.", through=None, blank=False]) 8 | 9 | :param verbose_name: The verbose_name for this field. 10 | :param help_text: The help_text to be used in forms (including the admin). 11 | :param through: The through model, see :doc:`custom_tagging` for more 12 | information. 13 | :param blank: Controls whether this field is required. 14 | 15 | .. method:: add(*tags, through_defaults=None, tag_kwargs=None) 16 | 17 | This adds tags to an object. The tags can be either ``Tag`` instances, or 18 | strings:: 19 | 20 | >>> apple.tags.all() 21 | [] 22 | >>> apple.tags.add("red", "green", "fruit") 23 | 24 | Use the ``through_defaults`` argument to specify values for your custom 25 | ``through`` model, if needed. 26 | 27 | The ``tag_kwargs`` argument allows one to specify parameters for the tags 28 | themselves. 29 | 30 | .. method:: remove(*tags) 31 | 32 | Removes a tag from an object. No exception is raised if the object 33 | doesn't have that tag. 34 | 35 | .. method:: clear() 36 | 37 | Removes all tags from an object. 38 | 39 | .. method:: set(tags, *, through_defaults=None, clear=False) 40 | 41 | If ``clear = True`` removes all the current tags and then adds the 42 | specified tags to the object. Otherwise sets the object's tags to those 43 | specified, removing only the missing tags and adding only the new tags. 44 | 45 | Use the ``through_defaults`` argument to specify values for your custom 46 | ``through`` model, if needed. 47 | 48 | .. method: most_common() 49 | 50 | Returns a ``QuerySet`` of all tags, annotated with the number of times 51 | they appear, available as the ``num_times`` attribute on each tag. The 52 | ``QuerySet``is ordered by ``num_times``, descending. The ``QuerySet`` 53 | is lazily evaluated, and can be sliced efficiently. 54 | 55 | :param min_count: Specify a min count to limit the returned queryset 56 | 57 | .. method:: similar_objects() 58 | 59 | Returns a list (not a lazy ``QuerySet``) of other objects tagged 60 | similarly to this one, ordered with most similar first. Each object in 61 | the list is decorated with a ``similar_tags`` attribute, the number of 62 | tags it shares with this object. 63 | 64 | If the model is using generic tagging (the default), this method 65 | searches tagged objects from all classes. If you are querying on a 66 | model with its own tagging through table, only other instances of the 67 | same model will be returned. 68 | 69 | .. method:: names() 70 | 71 | Convenience method, returning a ``ValuesListQuerySet`` (basically 72 | just an iterable) containing the name of each tag as a string:: 73 | 74 | >>> apple.tags.names() 75 | ["green and juicy", "red"] 76 | 77 | .. method:: slugs() 78 | 79 | Convenience method, returning a ``ValuesListQuerySet`` (basically 80 | just an iterable) containing the slug of each tag as a string:: 81 | 82 | >>> apple.tags.slugs() 83 | ["green-and-juicy", "red"] 84 | 85 | .. hint:: 86 | 87 | You can subclass ``_TaggableManager`` (note the underscore) to add 88 | methods or functionality. ``TaggableManager`` takes an optional 89 | manager keyword argument for your custom class, like this:: 90 | 91 | class Food(models.Model): 92 | # ... fields here 93 | tags = TaggableManager(manager=_CustomTaggableManager) 94 | 95 | Filtering 96 | ~~~~~~~~~ 97 | 98 | To find all of a model with a specific tags you can filter, using the normal 99 | Django ORM API. For example if you had a ``Food`` model, whose 100 | ``TaggableManager`` was named ``tags``, you could find all the delicious fruit 101 | like so:: 102 | 103 | >>> Food.objects.filter(tags__name__in=["delicious"]) 104 | [, , ] 105 | 106 | 107 | If you're filtering on multiple tags, it's very common to get duplicate 108 | results, because of the way relational databases work. Often you'll want to 109 | make use of the ``distinct()`` method on ``QuerySets``:: 110 | 111 | >>> Food.objects.filter(tags__name__in=["delicious", "red"]) 112 | [, ] 113 | >>> Food.objects.filter(tags__name__in=["delicious", "red"]).distinct() 114 | [] 115 | 116 | You can also filter by the slug on tags. If you're using a custom ``Tag`` 117 | model you can use this API to filter on any fields it has. 118 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import taggit 2 | 3 | extensions = ["sphinx.ext.intersphinx"] 4 | 5 | master_doc = "index" 6 | 7 | project = "django-taggit" 8 | copyright = "Alex Gaynor and individual contributors." 9 | 10 | # The short X.Y version. 11 | version = taggit.__version__ 12 | # The full version, including alpha/beta/rc tags. 13 | release = taggit.__version__ 14 | 15 | intersphinx_mapping = { 16 | "django": ( 17 | "https://docs.djangoproject.com/en/stable", 18 | "https://docs.djangoproject.com/en/stable/_objects/", 19 | ), 20 | "python": ("https://docs.python.org/3", None), 21 | } 22 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/external_apps.rst: -------------------------------------------------------------------------------- 1 | External Applications 2 | ===================== 3 | 4 | In addition to the features included in ``django-taggit`` directly, there are a 5 | number of external applications which provide additional features that may be 6 | of interest. 7 | 8 | .. note:: 9 | 10 | Despite their mention here, the following applications are in no way 11 | official, nor have they in any way been reviewed or tested. 12 | 13 | If you have an application that you'd like to see listed here, simply fork 14 | `django-taggit on github `_, 15 | add it to this list, and send a pull request. 16 | 17 | * `django-taggit-anywhere `_: 18 | Simpler approach to tagging with ``taggit``. Additionally this 19 | project provides easy-to-use integration with ``django-taggit-helpers`` and 20 | ``django-taggit-labels``. 21 | * `django-taggit-helpers `_: 22 | Makes it easier to work with admin pages of models 23 | associated with ``taggit`` tags by adding helper classes: ``TaggitCounter``, 24 | ``TaggitListFilter``, ``TaggitStackedInline``, ``TaggitTabularInline``. 25 | * `django-taggit-labels `_: 26 | Provides a clickable label widget for the 27 | Django admin for user friendly selection from managed tag sets. 28 | * `django-taggit-serializer `_: 29 | Adds functionality for using ``taggit`` with 30 | ``django-rest-framework``. 31 | * `django-taggit-suggest `_: 32 | Provides support for defining keyword and regular 33 | expression rules for suggesting new tags for content. This used to be 34 | available at ``taggit.contrib.suggest``. 35 | * `django-taggit-templatetags `_: 36 | Provides several templatetags, including one 37 | for tag clouds, to expose various ``taggit`` APIs directly to templates. 38 | * `django-taggit-bulk `_: 39 | An admin action for the bulk tagging from the model admin instance list view. 40 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | Frequently Asked Questions 2 | ========================== 3 | 4 | - How can I get all my tags? 5 | 6 | If you are using just an out-of-the-box setup, your tags are stored in the `Tag` model (found in `taggit.models`). If this is a custom model (for example you have your own models derived from `ItemBase`), then you'll need to query that one instead. 7 | 8 | So if you are using the standard setup, ``Tag.objects.all()`` will give you the tags. 9 | 10 | - How can I use this with factory_boy? 11 | 12 | Since these are all built off of many-to-many relationships, you can check out `factory_boy's documentation on this topic `_ and get some ideas on how to deal with tags. 13 | 14 | 15 | One way to handle this is with post-generation hooks:: 16 | 17 | class ProductFactory(DjangoModelFactory): 18 | # Rest of the stuff 19 | 20 | @post_generation 21 | def tags(self, create, extracted, **kwargs): 22 | if not create: 23 | return 24 | 25 | if extracted: 26 | self.tags.add(*extracted) 27 | -------------------------------------------------------------------------------- /docs/forms.rst: -------------------------------------------------------------------------------- 1 | .. _tags-in-forms: 2 | 3 | Tags in forms 4 | ============= 5 | 6 | The ``TaggableManager`` will show up automatically as a field in a 7 | ``ModelForm`` or in the admin. Tags input via the form field are parsed 8 | as follows: 9 | 10 | * If the input doesn't contain any commas or double quotes, it is simply 11 | treated as a space-delimited list of tag names. 12 | 13 | * If the input does contain either of these characters: 14 | 15 | * Groups of characters which appear between double quotes take 16 | precedence as multi-word tags (so double quoted tag names may 17 | contain commas). An unclosed double quote will be ignored. 18 | 19 | * Otherwise, if there are any unquoted commas in the input, it will 20 | be treated as comma-delimited. If not, it will be treated as 21 | space-delimited. 22 | 23 | Examples: 24 | 25 | ====================== ================================= ================================================ 26 | Tag input string Resulting tags Notes 27 | ====================== ================================= ================================================ 28 | apple ball cat ``["apple", "ball", "cat"]`` No commas, so space delimited 29 | apple, ball cat ``["apple", "ball cat"]`` Comma present, so comma delimited 30 | "apple, ball" cat dog ``["apple, ball", "cat", "dog"]`` All commas are quoted, so space delimited 31 | "apple, ball", cat dog ``["apple, ball", "cat dog"]`` Contains an unquoted comma, so comma delimited 32 | apple "ball cat" dog ``["apple", "ball cat", "dog"]`` No commas, so space delimited 33 | "apple" "ball dog ``["apple", "ball", "dog"]`` Unclosed double quote is ignored 34 | ====================== ================================= ================================================ 35 | 36 | 37 | ``commit=False`` 38 | ~~~~~~~~~~~~~~~~ 39 | 40 | If, when saving a form, you use the ``commit=False`` option you'll need to call 41 | ``save_m2m()`` on the form after you save the object, just as you would for a 42 | form with normal many to many fields on it:: 43 | 44 | if request.method == "POST": 45 | form = MyFormClass(request.POST) 46 | if form.is_valid(): 47 | obj = form.save(commit=False) 48 | obj.user = request.user 49 | obj.save() 50 | # Without this next line the tags won't be saved. 51 | form.save_m2m() 52 | 53 | You can check the details over in the `Django documentation on form saving `_. 54 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | To get started using ``django-taggit`` simply install it with 5 | ``pip``:: 6 | 7 | $ pip install django-taggit 8 | 9 | 10 | Add ``"taggit"`` to your project's ``INSTALLED_APPS`` setting. 11 | 12 | Run `./manage.py migrate`. 13 | 14 | And then to any model you want tagging on do the following:: 15 | 16 | from django.db import models 17 | 18 | from taggit.managers import TaggableManager 19 | 20 | class Food(models.Model): 21 | # ... fields here 22 | 23 | tags = TaggableManager() 24 | 25 | .. note:: 26 | 27 | If you want ``django-taggit`` to be **CASE-INSENSITIVE** when looking up existing tags, you'll have to set ``TAGGIT_CASE_INSENSITIVE`` (in ``settings.py`` or wherever you have your Django settings) to ``True`` (``False`` by default):: 28 | 29 | TAGGIT_CASE_INSENSITIVE = True 30 | 31 | 32 | Settings 33 | -------- 34 | 35 | The following Django-level settings affect the behavior of the library 36 | 37 | * ``TAGGIT_CASE_INSENSITIVE`` 38 | 39 | When set to ``True``, tag lookups will be case insensitive. This defaults to ``False``. 40 | 41 | * ``TAGGIT_STRIP_UNICODE_WHEN_SLUGIFYING`` 42 | When this is set to ``True``, tag slugs will be limited to ASCII characters. In this case, if you also have ``unidecode`` installed, 43 | then tag sluggification will transform a tag like ``あい うえお`` to ``ai-ueo``. 44 | If you do not have ``unidecode`` installed, then you will usually be outright stripping unicode, meaning that something like ``helloあい`` will be slugified as ``hello``. 45 | 46 | This value defaults to ``False``, meaning that unicode is preserved in slugification. 47 | 48 | Because the behavior when ``True`` is set leads to situations where 49 | slugs can be entirely stripped to an empty string, we recommend not activating this. 50 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to django-taggit's documentation! 2 | ========================================= 3 | 4 | ``django-taggit`` is a reusable Django application designed to make adding 5 | tagging to your project easy and fun. 6 | 7 | ``django-taggit`` works with Django 4.1+ and Python 3.9+. 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | getting_started 13 | forms 14 | admin 15 | serializers 16 | testing 17 | api 18 | faq 19 | custom_tagging 20 | contributing 21 | external_apps 22 | changelog 23 | 24 | Indices and tables 25 | ================== 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | -------------------------------------------------------------------------------- /docs/serializers.rst: -------------------------------------------------------------------------------- 1 | Usage With Django Rest Framework 2 | ================================ 3 | 4 | Because the tags in `django-taggit` need to be added into a `TaggableManager()` we cannot use the usual `Serializer` that we get from Django REST Framework. Because this is trying to save the tags into a `list`, which will throw an exception. 5 | 6 | To accept tags through a `REST` API call we need to add the following to our `Serializer`:: 7 | 8 | 9 | from taggit.serializers import (TagListSerializerField, 10 | TaggitSerializer) 11 | 12 | 13 | class YourSerializer(TaggitSerializer, serializers.ModelSerializer): 14 | 15 | tags = TagListSerializerField() 16 | 17 | class Meta: 18 | model = YourModel 19 | fields = '__all__' 20 | 21 | And you're done, so now you can add tags to your model. 22 | -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | Testing 2 | ======= 3 | 4 | Natural Key Support 5 | ------------------- 6 | We have added `natural key support `_ to the Tag model in the Django taggit library. This allows you to identify objects by human-readable identifiers rather than by their database ID:: 7 | 8 | python manage.py dumpdata taggit.Tag --natural-foreign --natural-primary > tags.json 9 | 10 | python manage.py loaddata tags.json 11 | 12 | By default tags use the name field as the natural key. 13 | 14 | You can customize this in your own custom tag model by setting the ``natural_key_fields`` property on your model the required fields. 15 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | Sphinx 2 | docutils<0.18 3 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | Django>=3.2 2 | coverage 3 | -------------------------------------------------------------------------------- /sample_taggit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/sample_taggit/__init__.py -------------------------------------------------------------------------------- /sample_taggit/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for sample_taggit project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample_taggit.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /sample_taggit/fixtures/0001_users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "auth.user", 4 | "pk": 1, 5 | "fields": { 6 | "password": "pbkdf2_sha256$720000$KnQUl5xpmqQF2aWaANeHIG$kO4C7iRjRpDfUNwWgvXyCtmfoCQqOHUTHCO2IvrH58U=", 7 | "last_login": "2024-07-28T13:26:52.011Z", 8 | "is_superuser": true, 9 | "username": "taggit", 10 | "first_name": "", 11 | "last_name": "", 12 | "email": "", 13 | "is_staff": true, 14 | "is_active": true, 15 | "date_joined": "2024-07-27T17:02:40.676Z", 16 | "groups": [], 17 | "user_permissions": [] 18 | } 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /sample_taggit/fixtures/0003_book_type.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "library_management.booktype", 4 | "pk": 1, 5 | "fields": { 6 | "slug": "hardcover", 7 | "name": "Hardcover" 8 | } 9 | }, 10 | { 11 | "model": "library_management.booktype", 12 | "pk": 2, 13 | "fields": { 14 | "slug": "paperback", 15 | "name": "Paperback" 16 | } 17 | }, 18 | { 19 | "model": "library_management.booktype", 20 | "pk": 3, 21 | "fields": { 22 | "slug": "e-book", 23 | "name": "E-book" 24 | } 25 | }, 26 | { 27 | "model": "library_management.booktype", 28 | "pk": 4, 29 | "fields": { 30 | "slug": "audiobook", 31 | "name": "Audiobook" 32 | } 33 | }, 34 | { 35 | "model": "library_management.booktype", 36 | "pk": 5, 37 | "fields": { 38 | "slug": "magazine", 39 | "name": "Magazine" 40 | } 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /sample_taggit/fixtures/0006_condition_tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "library_management.conditiontag", 4 | "pk": 1, 5 | "fields": { 6 | "name": "ripped", 7 | "slug": "ripped" 8 | } 9 | }, 10 | { 11 | "model": "library_management.conditiontag", 12 | "pk": 2, 13 | "fields": { 14 | "name": "signed", 15 | "slug": "signed" 16 | } 17 | }, 18 | { 19 | "model": "library_management.conditiontag", 20 | "pk": 3, 21 | "fields": { 22 | "name": "burnt", 23 | "slug": "burnt" 24 | } 25 | }, 26 | { 27 | "model": "library_management.conditiontag", 28 | "pk": 4, 29 | "fields": { 30 | "name": "Burned", 31 | "slug": "burned" 32 | } 33 | }, 34 | { 35 | "model": "library_management.conditiontag", 36 | "pk": 5, 37 | "fields": { 38 | "name": "Dog-eared", 39 | "slug": "dog-eared" 40 | } 41 | }, 42 | { 43 | "model": "library_management.conditiontag", 44 | "pk": 6, 45 | "fields": { 46 | "name": "Fair", 47 | "slug": "fair" 48 | } 49 | }, 50 | { 51 | "model": "library_management.conditiontag", 52 | "pk": 7, 53 | "fields": { 54 | "name": "Good", 55 | "slug": "good" 56 | } 57 | }, 58 | { 59 | "model": "library_management.conditiontag", 60 | "pk": 8, 61 | "fields": { 62 | "name": "Highlighted", 63 | "slug": "highlighted" 64 | } 65 | }, 66 | { 67 | "model": "library_management.conditiontag", 68 | "pk": 9, 69 | "fields": { 70 | "name": "Like New", 71 | "slug": "like-new" 72 | } 73 | }, 74 | { 75 | "model": "library_management.conditiontag", 76 | "pk": 10, 77 | "fields": { 78 | "name": "Loose Binding", 79 | "slug": "loose-binding" 80 | } 81 | }, 82 | { 83 | "model": "library_management.conditiontag", 84 | "pk": 11, 85 | "fields": { 86 | "name": "Missing Pages", 87 | "slug": "missing-pages" 88 | } 89 | }, 90 | { 91 | "model": "library_management.conditiontag", 92 | "pk": 12, 93 | "fields": { 94 | "name": "New", 95 | "slug": "new" 96 | } 97 | }, 98 | { 99 | "model": "library_management.conditiontag", 100 | "pk": 13, 101 | "fields": { 102 | "name": "Poor", 103 | "slug": "poor" 104 | } 105 | }, 106 | { 107 | "model": "library_management.conditiontag", 108 | "pk": 14, 109 | "fields": { 110 | "name": "Repaired", 111 | "slug": "repaired" 112 | } 113 | }, 114 | { 115 | "model": "library_management.conditiontag", 116 | "pk": 15, 117 | "fields": { 118 | "name": "Signed", 119 | "slug": "signed_1" 120 | } 121 | }, 122 | { 123 | "model": "library_management.conditiontag", 124 | "pk": 16, 125 | "fields": { 126 | "name": "Stained", 127 | "slug": "stained" 128 | } 129 | }, 130 | { 131 | "model": "library_management.conditiontag", 132 | "pk": 17, 133 | "fields": { 134 | "name": "Torn", 135 | "slug": "torn" 136 | } 137 | }, 138 | { 139 | "model": "library_management.conditiontag", 140 | "pk": 18, 141 | "fields": { 142 | "name": "Water Damaged", 143 | "slug": "water-damaged" 144 | } 145 | }, 146 | { 147 | "model": "library_management.conditiontag", 148 | "pk": 20, 149 | "fields": { 150 | "name": "First Edition", 151 | "slug": "first-edition" 152 | } 153 | } 154 | ] 155 | -------------------------------------------------------------------------------- /sample_taggit/library_management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/sample_taggit/library_management/__init__.py -------------------------------------------------------------------------------- /sample_taggit/library_management/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from library_management.models import ( 3 | Author, 4 | Book, 5 | BookType, 6 | ConditionTag, 7 | Magazine, 8 | PhysicalCopy, 9 | ) 10 | 11 | admin.site.register(Book) 12 | admin.site.register(Author) 13 | admin.site.register(BookType) 14 | admin.site.register(Magazine) 15 | admin.site.register(PhysicalCopy) 16 | admin.site.register(ConditionTag) 17 | -------------------------------------------------------------------------------- /sample_taggit/library_management/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class LibraryManagementConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "library_management" 7 | -------------------------------------------------------------------------------- /sample_taggit/library_management/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from taggit.models import Tag 4 | 5 | from .models import Author, Book, ConditionTag, PhysicalCopy 6 | 7 | 8 | class PhysicalCopyForm(forms.ModelForm): 9 | condition_tags = forms.ModelMultipleChoiceField( 10 | queryset=ConditionTag.objects.all(), 11 | widget=forms.CheckboxSelectMultiple, 12 | required=False, 13 | ) 14 | 15 | class Meta: 16 | model = PhysicalCopy 17 | fields = ["condition_tags"] 18 | 19 | 20 | class AuthorForm(forms.ModelForm): 21 | tags = forms.ModelMultipleChoiceField( 22 | queryset=Tag.objects.all(), widget=forms.CheckboxSelectMultiple, required=False 23 | ) 24 | 25 | def __init__(self, *args, **kwargs): 26 | super().__init__(*args, **kwargs) 27 | self.fields["tags"].queryset = self.fields["tags"].queryset.order_by("name") 28 | 29 | class Meta: 30 | model = Author 31 | fields = [ 32 | "first_name", 33 | "last_name", 34 | "middle_name", 35 | "birth_date", 36 | "biography", 37 | "tags", 38 | ] 39 | 40 | 41 | class BookForm(forms.ModelForm): 42 | tags = forms.ModelMultipleChoiceField( 43 | queryset=Tag.objects.all(), widget=forms.CheckboxSelectMultiple, required=False 44 | ) 45 | 46 | def __init__(self, *args, **kwargs): 47 | super().__init__(*args, **kwargs) 48 | self.fields["tags"].queryset = self.fields["tags"].queryset.order_by("name") 49 | 50 | class Meta: 51 | model = Book 52 | fields = ["name", "author", "published_date", "isbn", "summary", "tags"] 53 | -------------------------------------------------------------------------------- /sample_taggit/library_management/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/sample_taggit/library_management/migrations/__init__.py -------------------------------------------------------------------------------- /sample_taggit/library_management/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.db import models 4 | from django.urls import reverse 5 | 6 | from taggit.managers import TaggableManager 7 | from taggit.models import TagBase, TaggedItemBase 8 | 9 | 10 | class BookTypeChoices(models.TextChoices): 11 | HARDCOVER = "HC", "Hardcover" 12 | PAPERBACK = "PB", "Paperback" 13 | EBOOK = "EB", "E-book" 14 | AUDIOBOOK = "AB", "Audiobook" 15 | MAGAZINE = "MG", "Magazine" 16 | 17 | 18 | class BookType(TagBase): 19 | name = models.CharField(max_length=255, unique=True) 20 | 21 | class Meta: 22 | verbose_name = "Book Type" 23 | verbose_name_plural = "Book Types" 24 | 25 | 26 | class Book(models.Model): 27 | name = models.CharField(max_length=255) 28 | author = models.ForeignKey("Author", on_delete=models.CASCADE) 29 | published_date = models.DateField(null=True) 30 | isbn = models.CharField(max_length=17, unique=True) 31 | summary = models.TextField() 32 | tags = TaggableManager() 33 | 34 | @property 35 | def title(self): 36 | return self.name 37 | 38 | def __str__(self): 39 | return self.title 40 | 41 | class Meta: 42 | verbose_name = "Book" 43 | verbose_name_plural = "Books" 44 | 45 | 46 | class MagazineManager(models.Manager): 47 | def get_queryset(self): 48 | return ( 49 | super() 50 | .get_queryset() 51 | .filter( 52 | physical_copies__book_type__name="Magazine", 53 | physical_copies__isnull=False, 54 | ) 55 | .distinct() 56 | ) 57 | 58 | 59 | class Magazine(Book): 60 | objects = MagazineManager() 61 | 62 | class Meta: 63 | proxy = True 64 | 65 | @property 66 | def title(self): 67 | return f"{self.name} Edition: {self.published_date.strftime('%B %Y')}" 68 | 69 | 70 | class Author(models.Model): 71 | first_name = models.CharField(max_length=255, blank=True) 72 | last_name = models.CharField(max_length=255, blank=True) 73 | middle_name = models.CharField(max_length=255, blank=True) 74 | birth_date = models.DateField() 75 | biography = models.TextField() 76 | tags = TaggableManager() 77 | 78 | @property 79 | def full_name(self): 80 | return f"{self.first_name} {self.last_name}" 81 | 82 | def get_absolute_url(self): 83 | return reverse("author-detail", kwargs={"pk": self.pk}) 84 | 85 | def __str__(self): 86 | return self.full_name 87 | 88 | 89 | class ConditionTag(TagBase): 90 | class Meta: 91 | verbose_name = "Condition Tag" 92 | verbose_name_plural = "Condition Tags" 93 | ordering = ["name"] 94 | 95 | 96 | class ConditionTaggedItem(TaggedItemBase): 97 | tag = models.ForeignKey( 98 | ConditionTag, related_name="tagged_items", on_delete=models.CASCADE 99 | ) 100 | content_object = models.ForeignKey("PhysicalCopy", on_delete=models.CASCADE) 101 | 102 | class Meta: 103 | verbose_name = "Condition Tagged Item" 104 | verbose_name_plural = "Condition Tagged Items" 105 | 106 | 107 | class PhysicalCopy(models.Model): 108 | book = models.ForeignKey( 109 | Book, on_delete=models.CASCADE, related_name="physical_copies" 110 | ) 111 | barcode = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) 112 | book_type = models.ForeignKey(BookType, on_delete=models.CASCADE) 113 | condition_tags = TaggableManager(through=ConditionTaggedItem, blank=True) 114 | 115 | def __str__(self): 116 | return f"{self.book.name} - {self.barcode}" 117 | 118 | class Meta: 119 | verbose_name = "Physical Copy" 120 | verbose_name_plural = "Physical Copies" 121 | -------------------------------------------------------------------------------- /sample_taggit/library_management/templates/library_management/author_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "library_management/base.html" %} 2 | {% load custom_filters %} 3 | 4 | {% block title %}Author Detail{% endblock %} 5 | 6 | {% block content %} 7 |

{{ author.full_name }}

8 |

Birth Date: {{ author.birth_date }}

9 |

{{ author.biography }}

10 |

Tags

11 |
    12 | {% for tag in author.tags.all|order_tags %} 13 |
  • {{ tag.name }}
  • 14 | {% endfor %} 15 |
16 |

Books

17 |
    18 | {% for book in author.book_set.all %} 19 |
  • {{ book.name }}
  • 20 | {% endfor %} 21 |
22 | Back to list 23 | Edit Author 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /sample_taggit/library_management/templates/library_management/author_form.html: -------------------------------------------------------------------------------- 1 | {% extends "library_management/base.html" %} 2 | 3 | {% block title %}Edit Author{% endblock %} 4 | 5 | {% block content %} 6 |

Edit Author

7 |
8 | {% csrf_token %} 9 | {{ form.as_p }} 10 | 11 | Cancel 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /sample_taggit/library_management/templates/library_management/author_list.html: -------------------------------------------------------------------------------- 1 | {% extends "library_management/base.html" %} 2 | 3 | {% block title %}Authors{% endblock %} 4 | 5 | {% block content %} 6 |

Authors

7 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /sample_taggit/library_management/templates/library_management/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Library Management{% endblock %} 6 | 7 | {% block extra_css %}{% endblock %} 8 | 19 | 20 | 21 | 22 | 23 | 42 |
43 | {% block content %}{% endblock %} 44 |
45 | {% block extra_js %}{% endblock %} 46 | 47 | 48 | -------------------------------------------------------------------------------- /sample_taggit/library_management/templates/library_management/book_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "library_management/base.html" %} 2 | {% load custom_filters %} 3 | 4 | 5 | {% block title %}Book Detail{% endblock %} 6 | 7 | {% block content %} 8 |

{{ book }}

9 |

Author: {{ book.author.full_name }}

10 |

Published Date: {{ book.published_date }}

11 |

ISBN: {{ book.isbn }}

12 |

Summary: {{ book.summary }}

13 |

Tags: {{ book.tags.all|join:", " }}

14 | 15 |

Physical Copies

16 |
    17 | {% for copy in book.physical_copies.all %} 18 |
  • 19 | Type: {{ copy.book_type.name }}
    20 | Condition: {{ copy.condition_tags.all|join:", " }}
    21 | Update Condition 22 |
  • 23 | {% endfor %} 24 |
25 | 26 | Back to list 27 | {% if book|classname == 'Book' %} 28 | Edit 29 | {% elif book|classname == 'Magazine' %} 30 | Edit 31 | {% endif %} 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /sample_taggit/library_management/templates/library_management/book_form.html: -------------------------------------------------------------------------------- 1 | {% extends "library_management/base.html" %} 2 | 3 | {% block title %}Edit Book{% endblock %} 4 | 5 | {% block content %} 6 |

Edit Book

7 |
8 | {% csrf_token %} 9 | {{ form.as_p }} 10 | 11 | Cancel 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /sample_taggit/library_management/templates/library_management/book_list.html: -------------------------------------------------------------------------------- 1 | {% extends "library_management/base.html" %} 2 | 3 | {% block title %}Book List{% endblock %} 4 | 5 | {% block content %} 6 |

Book List

7 |
    8 | {% for book in page_obj %} 9 |
  • 10 | {{ book.name }} by {{ book.author }} 11 |
  • 12 | {% endfor %} 13 |
14 | 15 | 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /sample_taggit/library_management/templates/library_management/home_page.html: -------------------------------------------------------------------------------- 1 | {% extends 'library_management/base.html' %} 2 | 3 | {% block content %} 4 |

Library Management Home

5 | 6 |
7 |
8 |
9 |
10 |
Number of Books
11 |

{{ num_books }}

12 |
13 |
14 |
15 |
16 |
17 |
18 |
Genres Represented
19 |
    20 | {% for genre in genres %} 21 |
  • {{ genre.name }}: {{ genre.count }}
  • 22 | {% endfor %} 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
Condition of Physical Books
31 |
    32 | {% for condition in condition_stats %} 33 |
  • {{ condition.condition_tags__name }}: {{ condition.count }}
  • 34 | {% endfor %} 35 |
36 |
37 |
38 |
39 |
40 | 41 | 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /sample_taggit/library_management/templates/library_management/magazine_list.html: -------------------------------------------------------------------------------- 1 | {% extends "library_management/base.html" %} 2 | 3 | {% block title %}Magazine List{% endblock %} 4 | 5 | {% block content %} 6 |

Magazines

7 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /sample_taggit/library_management/templates/library_management/physical_copy_form.html: -------------------------------------------------------------------------------- 1 | {% extends "library_management/base.html" %} 2 | 3 | {% block title %}Update Physical Copy Condition{% endblock %} 4 | 5 | {% block content %} 6 |

Update Physical Copy Condition

7 |
8 | {% csrf_token %} 9 | {{ form.as_p }} 10 |
11 | 12 | Cancel 13 |
14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /sample_taggit/library_management/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/sample_taggit/library_management/templatetags/__init__.py -------------------------------------------------------------------------------- /sample_taggit/library_management/templatetags/custom_filters.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.filter 7 | def classname(obj): 8 | return obj.__class__.__name__ 9 | 10 | 11 | @register.filter 12 | def order_tags(tags): 13 | return tags.order_by("name") 14 | -------------------------------------------------------------------------------- /sample_taggit/library_management/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import ( 4 | AuthorDetailView, 5 | AuthorListView, 6 | AuthorUpdateView, 7 | BookCreateView, 8 | BookDetailView, 9 | BookListView, 10 | BookUpdateView, 11 | MagazineCreateView, 12 | MagazineDetailView, 13 | MagazineListView, 14 | MagazineUpdateView, 15 | PhysicalCopyUpdateView, 16 | home_page, 17 | ) 18 | 19 | urlpatterns = [ 20 | path("", home_page, name="home_page"), 21 | path("book_list", BookListView.as_view(), name="book-list"), 22 | path("book//", BookDetailView.as_view(), name="book-detail"), 23 | path("book/new/", BookCreateView.as_view(), name="book-create"), 24 | path("book//edit/", BookUpdateView.as_view(), name="book-update"), 25 | path("authors/", AuthorListView.as_view(), name="author-list"), 26 | path("authors//", AuthorDetailView.as_view(), name="author-detail"), 27 | path("authors//edit/", AuthorUpdateView.as_view(), name="author-update"), 28 | path("magazines/", MagazineListView.as_view(), name="magazine-list"), 29 | path("magazine//", MagazineDetailView.as_view(), name="magazine-detail"), 30 | path("magazine/new/", MagazineCreateView.as_view(), name="magazine-create"), 31 | path( 32 | "magazine//edit/", MagazineUpdateView.as_view(), name="magazine-update" 33 | ), 34 | path( 35 | "physical_copy//edit/", 36 | PhysicalCopyUpdateView.as_view(), 37 | name="physical-copy-update", 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /sample_taggit/library_management/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Count 2 | from django.db.models.functions import Lower 3 | from django.shortcuts import render 4 | from django.urls import reverse, reverse_lazy 5 | from django.views.generic import CreateView, DetailView, ListView, UpdateView 6 | 7 | from .forms import AuthorForm, BookForm, PhysicalCopyForm 8 | from .models import Author, Book, Magazine, PhysicalCopy 9 | 10 | 11 | def home_page(request): 12 | # Number of books 13 | num_books = Book.objects.count() 14 | 15 | # Different genres represented 16 | genres = Book.tags.values("name").annotate(count=Count("name")).order_by("-count") 17 | 18 | # Condition of physical books 19 | condition_stats = ( 20 | PhysicalCopy.objects.values("condition_tags__name") 21 | .annotate(count=Count("condition_tags")) 22 | .order_by("-count") 23 | ) 24 | 25 | context = { 26 | "num_books": num_books, 27 | "genres": genres, 28 | "condition_stats": condition_stats, 29 | } 30 | 31 | return render(request, "library_management/home_page.html", context) 32 | 33 | 34 | class BookListView(ListView): 35 | model = Book 36 | template_name = "library_management/book_list.html" 37 | context_object_name = "books" 38 | paginate_by = 20 39 | 40 | def get_ordering(self): 41 | ordering = self.request.GET.get("ordering", "name") 42 | if ordering == "name": 43 | return [Lower("name")] 44 | return [ordering] 45 | 46 | 47 | class BookDetailView(DetailView): 48 | model = Book 49 | template_name = "library_management/book_detail.html" 50 | context_object_name = "book" 51 | 52 | 53 | class BookCreateView(CreateView): 54 | model = Book 55 | template_name = "library_management/book_form.html" 56 | fields = ["name", "author", "published_date", "isbn", "summary", "tags"] 57 | success_url = reverse_lazy("book-list") 58 | 59 | 60 | class BookUpdateView(UpdateView): 61 | model = Book 62 | form_class = BookForm 63 | template_name = "library_management/book_form.html" 64 | 65 | def get_success_url(self): 66 | return reverse("book-detail", kwargs={"pk": self.object.pk}) 67 | 68 | 69 | class AuthorListView(ListView): 70 | model = Author 71 | template_name = "library_management/author_list.html" 72 | context_object_name = "authors" 73 | ordering = ["last_name", "first_name"] 74 | 75 | 76 | class AuthorDetailView(DetailView): 77 | model = Author 78 | template_name = "library_management/author_detail.html" 79 | context_object_name = "author" 80 | 81 | 82 | class AuthorUpdateView(UpdateView): 83 | model = Author 84 | form_class = AuthorForm 85 | template_name = "library_management/author_form.html" 86 | context_object_name = "author" 87 | 88 | 89 | class MagazineListView(BookListView): 90 | model = Magazine 91 | template_name = "library_management/magazine_list.html" 92 | context_object_name = "magazines" 93 | 94 | 95 | class MagazineDetailView(BookDetailView): 96 | model = Magazine 97 | template_name = "library_management/book_detail.html" 98 | context_object_name = "book" 99 | 100 | 101 | class MagazineCreateView(BookCreateView): 102 | model = Magazine 103 | template_name = "library_management/book_form.html" 104 | success_url = reverse_lazy("magazine-list") 105 | 106 | 107 | class MagazineUpdateView(BookUpdateView): 108 | model = Magazine 109 | form_class = BookForm 110 | template_name = "library_management/book_form.html" 111 | context_object_name = "book" 112 | 113 | def get_success_url(self): 114 | return reverse("magazine-detail", kwargs={"pk": self.object.pk}) 115 | 116 | 117 | class PhysicalCopyUpdateView(UpdateView): 118 | model = PhysicalCopy 119 | form_class = PhysicalCopyForm 120 | template_name = "library_management/physical_copy_form.html" 121 | 122 | def get_success_url(self): 123 | return reverse("book-detail", kwargs={"pk": self.object.book.pk}) 124 | -------------------------------------------------------------------------------- /sample_taggit/make.bat: -------------------------------------------------------------------------------- 1 | :: make.bat 2 | 3 | @echo off 4 | 5 | set PYTHON=python 6 | if not "%2" == "" ( 7 | set PYTHON=%2 8 | ) 9 | 10 | if "%1" == "reset-db" ( 11 | %PYTHON% manage.py migrate library_management zero 12 | %PYTHON% manage.py migrate taggit zero 13 | %PYTHON% manage.py migrate 14 | for %%f in (fixtures\*.json) do ( 15 | %PYTHON% manage.py loaddata %%f 16 | ) 17 | ) else if "%1" == "export-data" ( 18 | %PYTHON% manage.py dumpdata --indent 2 auth.User --output=fixtures\0001_users.json 19 | %PYTHON% manage.py dumpdata --indent 2 library_management.Author --output=fixtures\0002_author.json 20 | %PYTHON% manage.py dumpdata --indent 2 library_management.BookType --output=fixtures\0003_book_type.json 21 | %PYTHON% manage.py dumpdata --indent 2 library_management.Book --output=fixtures\0004_books.json 22 | %PYTHON% manage.py dumpdata --indent 2 taggit.Tag --output=fixtures\0005_tags.json 23 | %PYTHON% manage.py dumpdata --indent 2 library_management.ConditionTag --output=fixtures\0006_condition_tags.json 24 | %PYTHON% manage.py dumpdata --indent 2 library_management.PhysicalCopy --output=fixtures\0007_tags.json 25 | %PYTHON% manage.py dumpdata --indent 2 library_management.ConditionTaggedItem --output=fixtures\0008_condition_tagged_item.json 26 | ) else if "%1" == "loaddata" ( 27 | for %%f in (fixtures\*.json) do ( 28 | %PYTHON% manage.py loaddata %%f 29 | ) 30 | ) else ( 31 | echo Invalid command. Use 'reset-db', 'export', or 'loaddata'. 32 | ) 33 | -------------------------------------------------------------------------------- /sample_taggit/makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | 3 | PYTHON ?= python3 4 | 5 | reset-db: 6 | $(PYTHON) manage.py migrate library_management zero; 7 | $(PYTHON) manage.py migrate taggit zero; 8 | $(PYTHON) manage.py migrate; 9 | $(PYTHON) manage.py loaddata fixtures/*.json 10 | 11 | export-data: 12 | $(PYTHON) manage.py dumpdata --indent 2 auth.User --output=sample_taggit/fixtures/0001_users.json; 13 | $(PYTHON) manage.py dumpdata --indent 2 library_management.Author --output=sample_taggit/fixtures/0002_author.json; 14 | $(PYTHON) manage.py dumpdata --indent 2 library_management.BookType --output=sample_taggit/fixtures/0003_book_type.json; 15 | $(PYTHON) manage.py dumpdata --indent 2 library_management.Book --output=sample_taggit/fixtures/0004_books.json; 16 | $(PYTHON) manage.py dumpdata --indent 2 taggit.Tag --output=sample_taggit/fixtures/0005_tags.json; 17 | $(PYTHON) manage.py dumpdata --indent 2 library_management.ConditionTag --output=sample_taggit/fixtures/0006_condition_tags.json; 18 | $(PYTHON) manage.py dumpdata --indent 2 library_management.PhysicalCopy --output=sample_taggit/fixtures/0007_tags.json; 19 | $(PYTHON) manage.py dumpdata --indent 2 library_management.ConditionTaggedItem --output=sample_taggit/fixtures/0008_condition_tagged_item.json; 20 | 21 | loaddata: 22 | $(PYTHON) manage.py loaddata fixtures/*.json 23 | -------------------------------------------------------------------------------- /sample_taggit/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | from pathlib import Path 6 | 7 | 8 | def main(): 9 | """Run administrative tasks.""" 10 | # add this so that we can import from sample_taggit 11 | sys.path.append(str(Path(__file__).resolve().parent.parent)) 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample_taggit.settings") 13 | try: 14 | from django.core.management import execute_from_command_line 15 | except ImportError as exc: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) from exc 21 | execute_from_command_line(sys.argv) 22 | 23 | 24 | if __name__ == "__main__": 25 | main() 26 | -------------------------------------------------------------------------------- /sample_taggit/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for sample_taggit project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.0.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/5.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | from pathlib import Path 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = "django-insecure-1$#3+wir_0n0&d#_f$35%b-fb_!f(8vzh8*a2x%ih*+j6*gih_" 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | "django.contrib.admin", 36 | "django.contrib.auth", 37 | "django.contrib.contenttypes", 38 | "django.contrib.sessions", 39 | "django.contrib.messages", 40 | "django.contrib.staticfiles", 41 | "taggit", 42 | "library_management", 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | "django.middleware.security.SecurityMiddleware", 47 | "django.contrib.sessions.middleware.SessionMiddleware", 48 | "django.middleware.common.CommonMiddleware", 49 | "django.middleware.csrf.CsrfViewMiddleware", 50 | "django.contrib.auth.middleware.AuthenticationMiddleware", 51 | "django.contrib.messages.middleware.MessageMiddleware", 52 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 53 | ] 54 | 55 | ROOT_URLCONF = "sample_taggit.urls" 56 | 57 | TEMPLATES = [ 58 | { 59 | "BACKEND": "django.template.backends.django.DjangoTemplates", 60 | "DIRS": [os.path.join(BASE_DIR, "templates")], 61 | "APP_DIRS": True, 62 | "OPTIONS": { 63 | "context_processors": [ 64 | "django.template.context_processors.debug", 65 | "django.template.context_processors.request", 66 | "django.contrib.auth.context_processors.auth", 67 | "django.contrib.messages.context_processors.messages", 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = "sample_taggit.wsgi.application" 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/5.0/ref/settings/#databases 78 | 79 | DATABASES = { 80 | "default": { 81 | "ENGINE": "django.db.backends.sqlite3", 82 | "NAME": BASE_DIR / "db.sqlite3", 83 | } 84 | } 85 | 86 | 87 | # Password validation 88 | # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators 89 | 90 | AUTH_PASSWORD_VALIDATORS = [ 91 | { 92 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 93 | }, 94 | { 95 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 96 | }, 97 | { 98 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 99 | }, 100 | { 101 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 102 | }, 103 | ] 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/5.0/topics/i18n/ 108 | 109 | LANGUAGE_CODE = "en-us" 110 | 111 | TIME_ZONE = "UTC" 112 | 113 | USE_I18N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/5.0/howto/static-files/ 120 | 121 | STATIC_URL = "static/" 122 | 123 | # Default primary key field type 124 | # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field 125 | 126 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 127 | -------------------------------------------------------------------------------- /sample_taggit/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for sample_taggit project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/5.0/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | 18 | from django.contrib import admin 19 | from django.urls import include, path 20 | from library_management import urls as library_management_urls 21 | 22 | urlpatterns = [ 23 | path("admin/", admin.site.urls), 24 | path("", include(library_management_urls)), 25 | ] 26 | -------------------------------------------------------------------------------- /sample_taggit/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for sample_taggit project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample_taggit.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = django-taggit 3 | version = attr: taggit.__version__ 4 | description = django-taggit is a reusable Django application for simple tagging. 5 | long_description = file: README.rst 6 | author = Alex Gaynor 7 | author_email = alex.gaynor@gmail.com 8 | url = https://github.com/jazzband/django-taggit 9 | license = BSD 10 | classifiers = 11 | Development Status :: 5 - Production/Stable 12 | Environment :: Web Environment 13 | Framework :: Django 14 | Framework :: Django :: 4.1 15 | Framework :: Django :: 4.2 16 | Framework :: Django :: 5.0 17 | Intended Audience :: Developers 18 | License :: OSI Approved :: BSD License 19 | Operating System :: OS Independent 20 | Programming Language :: Python 21 | Programming Language :: Python :: 3 22 | Programming Language :: Python :: 3 :: Only 23 | Programming Language :: Python :: 3.9 24 | Programming Language :: Python :: 3.10 25 | Programming Language :: Python :: 3.11 26 | Programming Language :: Python :: 3.12 27 | project_urls = 28 | Documentation = https://django-taggit.readthedocs.io 29 | Source = https://github.com/jazzband/django-taggit 30 | Tracker = https://github.com/jazzband/django-taggit/issues 31 | 32 | [options] 33 | python_requires = >=3.9 34 | packages = find: 35 | install_requires = Django>=4.1 36 | include_package_data = true 37 | zip_safe = false 38 | 39 | [options.packages.find] 40 | exclude = tests* 41 | 42 | [flake8] 43 | # E501: line too long 44 | ignore = E501 45 | exclude = .venv,.git,.tox,.direnv 46 | 47 | [isort] 48 | profile = black 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /taggit/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (6, 1, 0) 2 | __version__ = ".".join(str(i) for i in VERSION) 3 | -------------------------------------------------------------------------------- /taggit/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.db import transaction 3 | from django.shortcuts import redirect, render 4 | from django.urls import path 5 | 6 | from taggit.models import Tag, TaggedItem 7 | 8 | from .forms import MergeTagsForm 9 | 10 | 11 | class TaggedItemInline(admin.StackedInline): 12 | model = TaggedItem 13 | 14 | 15 | @admin.register(Tag) 16 | class TagAdmin(admin.ModelAdmin): 17 | inlines = [TaggedItemInline] 18 | list_display = ["name", "slug"] 19 | ordering = ["name", "slug"] 20 | search_fields = ["name"] 21 | prepopulated_fields = {"slug": ["name"]} 22 | actions = ["render_tag_form", "remove_orphaned_tags_action"] 23 | 24 | def get_urls(self): 25 | urls = super().get_urls() 26 | custom_urls = [ 27 | path( 28 | "merge-tags/", 29 | self.admin_site.admin_view(self.merge_tags_view), 30 | name="taggit_tag_merge_tags", 31 | ), 32 | ] 33 | return custom_urls + urls 34 | 35 | @admin.action(description="Merge selected tags") 36 | def render_tag_form(self, request, queryset): 37 | selected = request.POST.getlist(admin.helpers.ACTION_CHECKBOX_NAME) 38 | if not selected: 39 | self.message_user(request, "Please select at least one tag.") 40 | return redirect(request.get_full_path()) 41 | 42 | # set the selected tags into the session, to be used later 43 | selected_tag_ids = ",".join(selected) 44 | request.session["selected_tag_ids"] = selected_tag_ids 45 | 46 | return redirect("admin:taggit_tag_merge_tags") 47 | 48 | def merge_tags_view(self, request): 49 | selected_tag_ids = request.session.get("selected_tag_ids", "").split(",") 50 | if request.method == "POST": 51 | form = MergeTagsForm(request.POST) 52 | if form.is_valid(): 53 | new_tag_name = form.cleaned_data["new_tag_name"] 54 | new_tag, created = Tag.objects.get_or_create(name=new_tag_name) 55 | with transaction.atomic(): 56 | for tag_id in selected_tag_ids: 57 | tag = Tag.objects.get(id=tag_id) 58 | tagged_items = TaggedItem.objects.filter(tag=tag) 59 | for tagged_item in tagged_items: 60 | if TaggedItem.objects.filter( 61 | tag=new_tag, 62 | content_type=tagged_item.content_type, 63 | object_id=tagged_item.object_id, 64 | ).exists(): 65 | # we have the new tag as well, so we can just 66 | # remove the tag association 67 | tagged_item.delete() 68 | else: 69 | # point this taggedItem to the new one 70 | tagged_item.tag = new_tag 71 | tagged_item.save() 72 | 73 | self.message_user(request, "Tags have been merged", level="success") 74 | # clear the selected_tag_ids from session after merge is complete 75 | request.session.pop("selected_tag_ids", None) 76 | 77 | return redirect("..") 78 | else: 79 | self.message_user(request, "Form is invalid.", level="error") 80 | 81 | context = { 82 | "form": MergeTagsForm(), 83 | "selected_tag_ids": selected_tag_ids, 84 | } 85 | return render(request, "admin/taggit/merge_tags_form.html", context) 86 | 87 | @admin.action(description="Remove orphaned tags") 88 | def remove_orphaned_tags_action(self, request, queryset): 89 | try: 90 | orphaned_tags = queryset.objects.orphaned() 91 | count, _ = orphaned_tags.delete() 92 | self.message_user( 93 | request, f"Successfully removed {count} orphaned tags.", level="success" 94 | ) 95 | except Exception as e: 96 | self.message_user(request, f"An error occurred: {e}", level="error") 97 | -------------------------------------------------------------------------------- /taggit/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig as BaseConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class TaggitAppConfig(BaseConfig): 6 | name = "taggit" 7 | verbose_name = _("Taggit") 8 | default_auto_field = "django.db.models.AutoField" 9 | -------------------------------------------------------------------------------- /taggit/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.translation import gettext as _ 3 | 4 | from taggit.utils import edit_string_for_tags, parse_tags 5 | 6 | 7 | class TagWidgetMixin: 8 | def format_value(self, value): 9 | if value is not None and not isinstance(value, str): 10 | value = edit_string_for_tags(value) 11 | return super().format_value(value) 12 | 13 | 14 | class TagWidget(TagWidgetMixin, forms.TextInput): 15 | pass 16 | 17 | 18 | class TextareaTagWidget(TagWidgetMixin, forms.Textarea): 19 | pass 20 | 21 | 22 | class TagField(forms.CharField): 23 | widget = TagWidget 24 | 25 | def clean(self, value): 26 | value = super().clean(value) 27 | try: 28 | return parse_tags(value) 29 | except ValueError: 30 | raise forms.ValidationError( 31 | _("Please provide a comma-separated list of tags.") 32 | ) 33 | 34 | def has_changed(self, initial_value, data_value): 35 | # Always return False if the field is disabled since self.bound_data 36 | # always uses the initial value in this case. 37 | if self.disabled: 38 | return False 39 | 40 | try: 41 | data_value = self.clean(data_value) 42 | except forms.ValidationError: 43 | pass 44 | 45 | # normalize "empty values" 46 | if not data_value: 47 | data_value = [] 48 | if not initial_value: 49 | initial_value = [] 50 | 51 | initial_value = [tag.name for tag in initial_value] 52 | initial_value.sort() 53 | 54 | return initial_value != data_value 55 | 56 | 57 | class MergeTagsForm(forms.Form): 58 | new_tag_name = forms.CharField( 59 | label="New Tag Name", 60 | max_length=100, 61 | widget=forms.TextInput(attrs={"id": "id_new_tag_name"}), 62 | help_text="Enter new or existing tag name", 63 | ) 64 | -------------------------------------------------------------------------------- /taggit/locale/ar/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/ar/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/ar/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2021-11-14 11:27+0900\n" 11 | "PO-Revision-Date: 2021-04-19 23:30+0100\n" 12 | "Last-Translator: Soufyane Hedidi \n" 13 | "Language-Team: \n" 14 | "Language: ar_DZ\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 2.3\n" 19 | "Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " 20 | "&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n" 21 | 22 | #: taggit/apps.py:7 23 | msgid "Taggit" 24 | msgstr "أوْسِمه" 25 | 26 | #: taggit/forms.py:31 27 | msgid "Please provide a comma-separated list of tags." 28 | msgstr "يرجى توفير قائمة وسوم مفصولة بفاصلة." 29 | 30 | #: taggit/managers.py:432 31 | msgid "Tags" 32 | msgstr "الوسوم" 33 | 34 | #: taggit/managers.py:433 35 | msgid "A comma-separated list of tags." 36 | msgstr "قائمة وسوم مفصولة بفاصلة." 37 | 38 | #: taggit/models.py:19 39 | msgctxt "A tag name" 40 | msgid "name" 41 | msgstr "الإسم" 42 | 43 | #: taggit/models.py:22 44 | msgctxt "A tag slug" 45 | msgid "slug" 46 | msgstr "سبيكة الوسم" 47 | 48 | #: taggit/models.py:82 49 | msgid "tag" 50 | msgstr "الوسم" 51 | 52 | #: taggit/models.py:83 53 | msgid "tags" 54 | msgstr "الوسوم" 55 | 56 | #: taggit/models.py:89 57 | #, python-format 58 | msgid "%(object)s tagged with %(tag)s" 59 | msgstr "%(object)s الموسوم بـ %(tag)s" 60 | 61 | #: taggit/models.py:134 62 | msgid "content type" 63 | msgstr "نوع المحتوى" 64 | 65 | #: taggit/models.py:165 taggit/models.py:172 66 | msgid "object ID" 67 | msgstr "معرِّف الكائن" 68 | 69 | #: taggit/models.py:180 70 | msgid "tagged item" 71 | msgstr "العنصر الموسوم" 72 | 73 | #: taggit/models.py:181 74 | msgid "tagged items" 75 | msgstr "العناصر الموسومة" 76 | 77 | #: taggit/serializers.py:40 78 | #, python-brace-format 79 | msgid "Expected a list of items but got type \"{input_type}\"." 80 | msgstr "" 81 | 82 | #: taggit/serializers.py:43 83 | msgid "" 84 | "Invalid json list. A tag list submitted in string form must be valid json." 85 | msgstr "" 86 | 87 | #: taggit/serializers.py:46 88 | msgid "All list items must be of string type." 89 | msgstr "" 90 | -------------------------------------------------------------------------------- /taggit/locale/cs/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/cs/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/cs/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2021-11-14 11:28+0900\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" 20 | 21 | #: taggit/apps.py:7 22 | msgid "Taggit" 23 | msgstr "" 24 | 25 | #: taggit/forms.py:31 26 | msgid "Please provide a comma-separated list of tags." 27 | msgstr "Vložte čárkami oddělený seznam tagů" 28 | 29 | #: taggit/managers.py:432 30 | msgid "Tags" 31 | msgstr "Tagy" 32 | 33 | #: taggit/managers.py:433 34 | msgid "A comma-separated list of tags." 35 | msgstr "Čárkami oddělený seznam tagů" 36 | 37 | #: taggit/models.py:19 38 | msgctxt "A tag name" 39 | msgid "name" 40 | msgstr "Jméno" 41 | 42 | #: taggit/models.py:22 43 | msgctxt "A tag slug" 44 | msgid "slug" 45 | msgstr "Slug" 46 | 47 | #: taggit/models.py:82 48 | msgid "tag" 49 | msgstr "Tag" 50 | 51 | #: taggit/models.py:83 52 | msgid "tags" 53 | msgstr "" 54 | 55 | #: taggit/models.py:89 56 | #, python-format 57 | msgid "%(object)s tagged with %(tag)s" 58 | msgstr "%(object)s označen tagem %(tag)s" 59 | 60 | #: taggit/models.py:134 61 | msgid "content type" 62 | msgstr "Typ obsahu" 63 | 64 | #: taggit/models.py:165 taggit/models.py:172 65 | msgid "object ID" 66 | msgstr "ID objektu" 67 | 68 | #: taggit/models.py:180 69 | msgid "tagged item" 70 | msgstr "Tagem označená položka" 71 | 72 | #: taggit/models.py:181 73 | msgid "tagged items" 74 | msgstr "Tagy označené položky" 75 | 76 | #: taggit/serializers.py:40 77 | #, python-brace-format 78 | msgid "Expected a list of items but got type \"{input_type}\"." 79 | msgstr "" 80 | 81 | #: taggit/serializers.py:43 82 | msgid "" 83 | "Invalid json list. A tag list submitted in string form must be valid json." 84 | msgstr "" 85 | 86 | #: taggit/serializers.py:46 87 | msgid "All list items must be of string type." 88 | msgstr "" 89 | -------------------------------------------------------------------------------- /taggit/locale/da/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/da/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/da/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , 2020. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2021-11-14 11:29+0900\n" 11 | "PO-Revision-Date: 2020-10-25 14:29+0100\n" 12 | "Last-Translator: \n" 13 | "Language-Team: \n" 14 | "Language: da\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | "X-Generator: Poedit 2.4.1\n" 20 | 21 | #: taggit/apps.py:7 22 | msgid "Taggit" 23 | msgstr "Taggit" 24 | 25 | #: taggit/forms.py:31 26 | msgid "Please provide a comma-separated list of tags." 27 | msgstr "Venligts angiv mærker adskilt af et komma." 28 | 29 | #: taggit/managers.py:432 30 | msgid "Tags" 31 | msgstr "Mærker" 32 | 33 | #: taggit/managers.py:433 34 | msgid "A comma-separated list of tags." 35 | msgstr "Adskil mærker med et komma." 36 | 37 | #: taggit/models.py:19 38 | msgctxt "A tag name" 39 | msgid "name" 40 | msgstr "navn" 41 | 42 | #: taggit/models.py:22 43 | msgctxt "A tag slug" 44 | msgid "slug" 45 | msgstr "slug" 46 | 47 | #: taggit/models.py:82 48 | msgid "tag" 49 | msgstr "mærke" 50 | 51 | #: taggit/models.py:83 52 | msgid "tags" 53 | msgstr "mærker" 54 | 55 | #: taggit/models.py:89 56 | #, python-format 57 | msgid "%(object)s tagged with %(tag)s" 58 | msgstr "%(object)s mærket med %(tag)s" 59 | 60 | #: taggit/models.py:134 61 | msgid "content type" 62 | msgstr "indholdstype" 63 | 64 | #: taggit/models.py:165 taggit/models.py:172 65 | msgid "object ID" 66 | msgstr "objekt ID" 67 | 68 | #: taggit/models.py:180 69 | msgid "tagged item" 70 | msgstr "mærket element" 71 | 72 | #: taggit/models.py:181 73 | msgid "tagged items" 74 | msgstr "mærkede elementer" 75 | 76 | #: taggit/serializers.py:40 77 | #, python-brace-format 78 | msgid "Expected a list of items but got type \"{input_type}\"." 79 | msgstr "" 80 | 81 | #: taggit/serializers.py:43 82 | msgid "" 83 | "Invalid json list. A tag list submitted in string form must be valid json." 84 | msgstr "" 85 | 86 | #: taggit/serializers.py:46 87 | msgid "All list items must be of string type." 88 | msgstr "" 89 | -------------------------------------------------------------------------------- /taggit/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | #, fuzzy 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: django-taggit\n" 5 | "Report-Msgid-Bugs-To: \n" 6 | "POT-Creation-Date: 2021-11-14 11:29+0900\n" 7 | "PO-Revision-Date: 2010-09-07 09:26-0700\n" 8 | "Last-Translator: Jannis Leidel \n" 9 | "Language-Team: German \n" 10 | "Language: de\n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=UTF-8\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 15 | 16 | #: taggit/apps.py:7 17 | msgid "Taggit" 18 | msgstr "" 19 | 20 | #: taggit/forms.py:31 21 | msgid "Please provide a comma-separated list of tags." 22 | msgstr "Bitte eine durch Komma getrennte Schlagwortliste eingeben." 23 | 24 | #: taggit/managers.py:432 25 | msgid "Tags" 26 | msgstr "Schlagwörter" 27 | 28 | #: taggit/managers.py:433 29 | msgid "A comma-separated list of tags." 30 | msgstr "Eine durch Komma getrennte Schlagwortliste." 31 | 32 | #: taggit/models.py:19 33 | msgctxt "A tag name" 34 | msgid "name" 35 | msgstr "Name" 36 | 37 | #: taggit/models.py:22 38 | msgctxt "A tag slug" 39 | msgid "slug" 40 | msgstr "Kürzel" 41 | 42 | #: taggit/models.py:82 43 | msgid "tag" 44 | msgstr "Schlagwort" 45 | 46 | #: taggit/models.py:83 47 | msgid "tags" 48 | msgstr "" 49 | 50 | #: taggit/models.py:89 51 | #, python-format 52 | msgid "%(object)s tagged with %(tag)s" 53 | msgstr "%(object)s verschlagwortet mit %(tag)s" 54 | 55 | #: taggit/models.py:134 56 | msgid "content type" 57 | msgstr "Inhaltstyp" 58 | 59 | #: taggit/models.py:165 taggit/models.py:172 60 | msgid "object ID" 61 | msgstr "Objekt-ID" 62 | 63 | #: taggit/models.py:180 64 | msgid "tagged item" 65 | msgstr "Verschlagwortetes Objekt" 66 | 67 | #: taggit/models.py:181 68 | msgid "tagged items" 69 | msgstr "Verschlagwortete Objekte" 70 | 71 | #: taggit/serializers.py:40 72 | #, python-brace-format 73 | msgid "Expected a list of items but got type \"{input_type}\"." 74 | msgstr "" 75 | 76 | #: taggit/serializers.py:43 77 | msgid "" 78 | "Invalid json list. A tag list submitted in string form must be valid json." 79 | msgstr "" 80 | 81 | #: taggit/serializers.py:46 82 | msgid "All list items must be of string type." 83 | msgstr "" 84 | -------------------------------------------------------------------------------- /taggit/locale/el/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/el/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/el/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | #, fuzzy 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: django-taggit\n" 5 | "Report-Msgid-Bugs-To: \n" 6 | "POT-Creation-Date: 2021-11-14 11:30+0900\n" 7 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 8 | "Last-Translator: Serafeim Papastefanos \n" 9 | "Language-Team: Greek \n" 10 | "Language: el\n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=UTF-8\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 15 | 16 | #: taggit/apps.py:7 17 | msgid "Taggit" 18 | msgstr "" 19 | 20 | #: taggit/forms.py:31 21 | msgid "Please provide a comma-separated list of tags." 22 | msgstr "Παρακαλούμε συπληρώστε μια λίστα από ετικέτες χωρισμένη με κόμματα" 23 | 24 | #: taggit/managers.py:432 25 | msgid "Tags" 26 | msgstr "Ετικέτες" 27 | 28 | #: taggit/managers.py:433 29 | msgid "A comma-separated list of tags." 30 | msgstr "Μια χωρισμένη με κόμματα λίστα από ετικέτες" 31 | 32 | #: taggit/models.py:19 33 | msgctxt "A tag name" 34 | msgid "name" 35 | msgstr "Όνομα" 36 | 37 | #: taggit/models.py:22 38 | msgctxt "A tag slug" 39 | msgid "slug" 40 | msgstr "Sluig" 41 | 42 | #: taggit/models.py:82 43 | msgid "tag" 44 | msgstr "Ετικέτα" 45 | 46 | #: taggit/models.py:83 47 | msgid "tags" 48 | msgstr "" 49 | 50 | #: taggit/models.py:89 51 | #, python-format 52 | msgid "%(object)s tagged with %(tag)s" 53 | msgstr "%(object)s μαρκαρισμένα με %(tag)s" 54 | 55 | #: taggit/models.py:134 56 | #, fuzzy 57 | #| msgid "Content type" 58 | msgid "content type" 59 | msgstr "Είδος περιεχομένου" 60 | 61 | #: taggit/models.py:165 taggit/models.py:172 62 | #, fuzzy 63 | #| msgid "Object id" 64 | msgid "object ID" 65 | msgstr "Κωδικός αντικειμένου" 66 | 67 | #: taggit/models.py:180 68 | #, fuzzy 69 | #| msgid "Tagged Item" 70 | msgid "tagged item" 71 | msgstr "Αντικείμενο με ετικέτα" 72 | 73 | #: taggit/models.py:181 74 | #, fuzzy 75 | #| msgid "Tagged Items" 76 | msgid "tagged items" 77 | msgstr "Αντικείμενα με ετικέτα" 78 | 79 | #: taggit/serializers.py:40 80 | #, python-brace-format 81 | msgid "Expected a list of items but got type \"{input_type}\"." 82 | msgstr "" 83 | 84 | #: taggit/serializers.py:43 85 | msgid "" 86 | "Invalid json list. A tag list submitted in string form must be valid json." 87 | msgstr "" 88 | 89 | #: taggit/serializers.py:46 90 | msgid "All list items must be of string type." 91 | msgstr "" 92 | -------------------------------------------------------------------------------- /taggit/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2021-11-14 11:31+0900\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: taggit/apps.py:7 21 | msgid "Taggit" 22 | msgstr "" 23 | 24 | #: taggit/forms.py:31 25 | msgid "Please provide a comma-separated list of tags." 26 | msgstr "" 27 | 28 | #: taggit/managers.py:432 29 | msgid "Tags" 30 | msgstr "" 31 | 32 | #: taggit/managers.py:433 33 | msgid "A comma-separated list of tags." 34 | msgstr "" 35 | 36 | #: taggit/models.py:19 37 | msgctxt "A tag name" 38 | msgid "name" 39 | msgstr "" 40 | 41 | #: taggit/models.py:22 42 | msgctxt "A tag slug" 43 | msgid "slug" 44 | msgstr "" 45 | 46 | #: taggit/models.py:82 47 | msgid "tag" 48 | msgstr "" 49 | 50 | #: taggit/models.py:83 51 | msgid "tags" 52 | msgstr "" 53 | 54 | #: taggit/models.py:89 55 | #, python-format 56 | msgid "%(object)s tagged with %(tag)s" 57 | msgstr "" 58 | 59 | #: taggit/models.py:134 60 | msgid "content type" 61 | msgstr "" 62 | 63 | #: taggit/models.py:165 taggit/models.py:172 64 | msgid "object ID" 65 | msgstr "" 66 | 67 | #: taggit/models.py:180 68 | msgid "tagged item" 69 | msgstr "" 70 | 71 | #: taggit/models.py:181 72 | msgid "tagged items" 73 | msgstr "" 74 | 75 | #: taggit/serializers.py:40 76 | #, python-brace-format 77 | msgid "Expected a list of items but got type \"{input_type}\"." 78 | msgstr "" 79 | 80 | #: taggit/serializers.py:43 81 | msgid "" 82 | "Invalid json list. A tag list submitted in string form must be valid json." 83 | msgstr "" 84 | 85 | #: taggit/serializers.py:46 86 | msgid "All list items must be of string type." 87 | msgstr "" 88 | -------------------------------------------------------------------------------- /taggit/locale/eo/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/eo/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/eo/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: django-taggit\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2021-11-14 11:31+0900\n" 6 | "PO-Revision-Date: 2014-03-29 18:57+0100\n" 7 | "Last-Translator: Baptiste Darthenay \n" 8 | "Language-Team: Esperanto \n" 9 | "Language: eo\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | "X-Generator: Poedit 1.5.4\n" 15 | 16 | #: taggit/apps.py:7 17 | msgid "Taggit" 18 | msgstr "Etikedoj" 19 | 20 | #: taggit/forms.py:31 21 | msgid "Please provide a comma-separated list of tags." 22 | msgstr "Bonvolu enmeti liston da etikedoj apartitaj per komoj." 23 | 24 | #: taggit/managers.py:432 25 | msgid "Tags" 26 | msgstr "Etikedoj" 27 | 28 | #: taggit/managers.py:433 29 | msgid "A comma-separated list of tags." 30 | msgstr "Listo da etikedoj apartitaj per komoj." 31 | 32 | #: taggit/models.py:19 33 | msgctxt "A tag name" 34 | msgid "name" 35 | msgstr "Nomo" 36 | 37 | #: taggit/models.py:22 38 | msgctxt "A tag slug" 39 | msgid "slug" 40 | msgstr "Ĵetonvorto" 41 | 42 | #: taggit/models.py:82 43 | msgid "tag" 44 | msgstr "Etikedo" 45 | 46 | #: taggit/models.py:83 47 | msgid "tags" 48 | msgstr "" 49 | 50 | #: taggit/models.py:89 51 | #, python-format 52 | msgid "%(object)s tagged with %(tag)s" 53 | msgstr "%(object)s etikedita %(tag)s" 54 | 55 | #: taggit/models.py:134 56 | msgid "content type" 57 | msgstr "Enhavtipo" 58 | 59 | #: taggit/models.py:165 taggit/models.py:172 60 | msgid "object ID" 61 | msgstr "Objekto ID" 62 | 63 | #: taggit/models.py:180 64 | msgid "tagged item" 65 | msgstr "Etikedita elemento" 66 | 67 | #: taggit/models.py:181 68 | msgid "tagged items" 69 | msgstr "Etikeditaj elementoj" 70 | 71 | #: taggit/serializers.py:40 72 | #, python-brace-format 73 | msgid "Expected a list of items but got type \"{input_type}\"." 74 | msgstr "" 75 | 76 | #: taggit/serializers.py:43 77 | msgid "" 78 | "Invalid json list. A tag list submitted in string form must be valid json." 79 | msgstr "" 80 | 81 | #: taggit/serializers.py:46 82 | msgid "All list items must be of string type." 83 | msgstr "" 84 | -------------------------------------------------------------------------------- /taggit/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/es/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2021-11-14 11:32+0900\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: taggit/apps.py:7 22 | msgid "Taggit" 23 | msgstr "" 24 | 25 | #: taggit/forms.py:31 26 | msgid "Please provide a comma-separated list of tags." 27 | msgstr "Por favor introduzca una lista de etiquetas separadas por coma." 28 | 29 | #: taggit/managers.py:432 30 | msgid "Tags" 31 | msgstr "Etiquetas" 32 | 33 | #: taggit/managers.py:433 34 | msgid "A comma-separated list of tags." 35 | msgstr "Una lista de etiquetas separadas por coma." 36 | 37 | #: taggit/models.py:19 38 | msgctxt "A tag name" 39 | msgid "name" 40 | msgstr "Nombre" 41 | 42 | #: taggit/models.py:22 43 | msgctxt "A tag slug" 44 | msgid "slug" 45 | msgstr "Slug" 46 | 47 | #: taggit/models.py:82 48 | msgid "tag" 49 | msgstr "Etiqueta" 50 | 51 | #: taggit/models.py:83 52 | msgid "tags" 53 | msgstr "" 54 | 55 | #: taggit/models.py:89 56 | #, python-format 57 | msgid "%(object)s tagged with %(tag)s" 58 | msgstr "%(object)s etiquetados con %(tag)s" 59 | 60 | #: taggit/models.py:134 61 | msgid "content type" 62 | msgstr "Tipo de contenido" 63 | 64 | #: taggit/models.py:165 taggit/models.py:172 65 | msgid "object ID" 66 | msgstr "Id del objeto" 67 | 68 | #: taggit/models.py:180 69 | msgid "tagged item" 70 | msgstr "Elemento etiquetado" 71 | 72 | #: taggit/models.py:181 73 | msgid "tagged items" 74 | msgstr "Elementos etiquetados" 75 | 76 | #: taggit/serializers.py:40 77 | #, python-brace-format 78 | msgid "Expected a list of items but got type \"{input_type}\"." 79 | msgstr "" 80 | 81 | #: taggit/serializers.py:43 82 | msgid "" 83 | "Invalid json list. A tag list submitted in string form must be valid json." 84 | msgstr "" 85 | 86 | #: taggit/serializers.py:46 87 | msgid "All list items must be of string type." 88 | msgstr "" 89 | -------------------------------------------------------------------------------- /taggit/locale/fa/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/fa/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/fa/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # Mohammad Hossein Yazdani , 2021. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2021-11-14 11:32+0900\n" 12 | "PO-Revision-Date: 2021-07-27 23:15+0430\n" 13 | "Last-Translator: Mohammad Hossein Yazdani \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: fa\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: taggit/apps.py:7 21 | msgid "Taggit" 22 | msgstr "" 23 | 24 | #: taggit/forms.py:31 25 | msgid "Please provide a comma-separated list of tags." 26 | msgstr "لطفا لیستی از برچسب های جدا شده توسط کاما بسازید" 27 | 28 | #: taggit/managers.py:432 29 | msgid "Tags" 30 | msgstr "برچسب ها" 31 | 32 | #: taggit/managers.py:433 33 | msgid "A comma-separated list of tags." 34 | msgstr "یک لیست از برچسب های جدا شده توسط کاما " 35 | 36 | #: taggit/models.py:19 37 | msgctxt "A tag name" 38 | msgid "name" 39 | msgstr "نام" 40 | 41 | #: taggit/models.py:22 42 | msgctxt "A tag slug" 43 | msgid "slug" 44 | msgstr "نامک" 45 | 46 | #: taggit/models.py:82 47 | msgid "tag" 48 | msgstr "برچسب" 49 | 50 | #: taggit/models.py:83 51 | msgid "tags" 52 | msgstr "برچسب ها" 53 | 54 | #: taggit/models.py:89 55 | #, python-format 56 | msgid "%(object)s tagged with %(tag)s" 57 | msgstr "%(object)s برچسب گزاری شده با %(tag)s" 58 | 59 | #: taggit/models.py:134 60 | msgid "content type" 61 | msgstr "نوع محتوا" 62 | 63 | #: taggit/models.py:165 taggit/models.py:172 64 | msgid "object ID" 65 | msgstr "شناسه شی" 66 | 67 | #: taggit/models.py:180 68 | msgid "tagged item" 69 | msgstr "آیتم برچسب گزاری شده" 70 | 71 | #: taggit/models.py:181 72 | msgid "tagged items" 73 | msgstr "آیتم های برچسب گزاری شده" 74 | 75 | #: taggit/serializers.py:40 76 | #, python-brace-format 77 | msgid "Expected a list of items but got type \"{input_type}\"." 78 | msgstr "" 79 | 80 | #: taggit/serializers.py:43 81 | msgid "" 82 | "Invalid json list. A tag list submitted in string form must be valid json." 83 | msgstr "" 84 | 85 | #: taggit/serializers.py:46 86 | msgid "All list items must be of string type." 87 | msgstr "" 88 | -------------------------------------------------------------------------------- /taggit/locale/fi/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/fi/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/fi/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under the same license as the django-taggit package. 2 | # 3 | # Translators: 4 | # Nikolay Korotkiy , 2018 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-taggit\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2021-11-14 11:32+0900\n" 11 | "PO-Revision-Date: 2018-01-06 17:27-0600\n" 12 | "Last-Translator: Nikolay Korotkiy \n" 13 | "Language-Team: Finnish\n" 14 | "Language: fi\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: taggit/apps.py:7 21 | msgid "Taggit" 22 | msgstr "Tagit" 23 | 24 | #: taggit/forms.py:31 25 | msgid "Please provide a comma-separated list of tags." 26 | msgstr "Ole hyvä ja anna pilkulla erotettu lista tageista." 27 | 28 | #: taggit/managers.py:432 29 | msgid "Tags" 30 | msgstr "Tagit" 31 | 32 | #: taggit/managers.py:433 33 | msgid "A comma-separated list of tags." 34 | msgstr "Pilkulla erotettu lista tageista." 35 | 36 | #: taggit/models.py:19 37 | msgctxt "A tag name" 38 | msgid "name" 39 | msgstr "Nimi" 40 | 41 | #: taggit/models.py:22 42 | msgctxt "A tag slug" 43 | msgid "slug" 44 | msgstr "Lyhytnimi" 45 | 46 | #: taggit/models.py:82 47 | msgid "tag" 48 | msgstr "Tagi" 49 | 50 | #: taggit/models.py:83 51 | msgid "tags" 52 | msgstr "" 53 | 54 | #: taggit/models.py:89 55 | #, python-format 56 | msgid "%(object)s tagged with %(tag)s" 57 | msgstr "%(object)s on merkitty %(tag)s:lla" 58 | 59 | #: taggit/models.py:134 60 | msgid "content type" 61 | msgstr "Sisältötyyppi" 62 | 63 | #: taggit/models.py:165 taggit/models.py:172 64 | msgid "object ID" 65 | msgstr "Kohteen ID" 66 | 67 | #: taggit/models.py:180 68 | msgid "tagged item" 69 | msgstr "Merkitty kohde" 70 | 71 | #: taggit/models.py:181 72 | msgid "tagged items" 73 | msgstr "Merkittyjä kohteita" 74 | 75 | #: taggit/serializers.py:40 76 | #, python-brace-format 77 | msgid "Expected a list of items but got type \"{input_type}\"." 78 | msgstr "" 79 | 80 | #: taggit/serializers.py:43 81 | msgid "" 82 | "Invalid json list. A tag list submitted in string form must be valid json." 83 | msgstr "" 84 | 85 | #: taggit/serializers.py:46 86 | msgid "All list items must be of string type." 87 | msgstr "" 88 | -------------------------------------------------------------------------------- /taggit/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # French translation for django-taggit 2 | # Copyright (C) Jazz Band 3 | # This file is distributed under the same license as the taggit package. 4 | # Guillaume Bernard , 2019 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2021-11-14 11:33+0900\n" 11 | "PO-Revision-Date: 2019-04-09 10:57+0200\n" 12 | "Last-Translator: Guillaume Bernard \n" 13 | "Language-Team: \n" 14 | "Language: fr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | "X-Generator: Poedit 2.2.1\n" 20 | 21 | #: taggit/apps.py:7 22 | msgid "Taggit" 23 | msgstr "" 24 | 25 | #: taggit/forms.py:31 26 | msgid "Please provide a comma-separated list of tags." 27 | msgstr "Renseignez une liste d’étiquettes séparées par des virgules." 28 | 29 | #: taggit/managers.py:432 30 | msgid "Tags" 31 | msgstr "Étiquettes" 32 | 33 | #: taggit/managers.py:433 34 | msgid "A comma-separated list of tags." 35 | msgstr "Une liste d’étiquettes séparées par des virgules." 36 | 37 | #: taggit/models.py:19 38 | msgctxt "A tag name" 39 | msgid "name" 40 | msgstr "nom" 41 | 42 | #: taggit/models.py:22 43 | msgctxt "A tag slug" 44 | msgid "slug" 45 | msgstr "slug" 46 | 47 | #: taggit/models.py:82 48 | msgid "tag" 49 | msgstr "Étiquette" 50 | 51 | #: taggit/models.py:83 52 | msgid "tags" 53 | msgstr "" 54 | 55 | #: taggit/models.py:89 56 | #, python-format 57 | msgid "%(object)s tagged with %(tag)s" 58 | msgstr "%(object)s étiquetés avec %(tag)s" 59 | 60 | #: taggit/models.py:134 61 | msgid "content type" 62 | msgstr "Type de contenu" 63 | 64 | #: taggit/models.py:165 taggit/models.py:172 65 | msgid "object ID" 66 | msgstr "Identifiant de l’objet" 67 | 68 | #: taggit/models.py:180 69 | msgid "tagged item" 70 | msgstr "Élément étiqueté" 71 | 72 | #: taggit/models.py:181 73 | msgid "tagged items" 74 | msgstr "Éléments étiquetés" 75 | 76 | #: taggit/serializers.py:40 77 | #, python-brace-format 78 | msgid "Expected a list of items but got type \"{input_type}\"." 79 | msgstr "" 80 | 81 | #: taggit/serializers.py:43 82 | msgid "" 83 | "Invalid json list. A tag list submitted in string form must be valid json." 84 | msgstr "" 85 | 86 | #: taggit/serializers.py:46 87 | msgid "All list items must be of string type." 88 | msgstr "" 89 | -------------------------------------------------------------------------------- /taggit/locale/he/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/he/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/he/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Django Taggit\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2021-11-14 11:34+0900\n" 11 | "PO-Revision-Date: 2010-06-26 12:54-0600\n" 12 | "Last-Translator: Alex \n" 13 | "Language-Team: LANGUAGE \n" 14 | "Language: \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: taggit/apps.py:7 21 | msgid "Taggit" 22 | msgstr "" 23 | 24 | #: taggit/forms.py:31 25 | msgid "Please provide a comma-separated list of tags." 26 | msgstr "נא לספק רשימה של תגים מופרדת עם פסיקים." 27 | 28 | #: taggit/managers.py:432 29 | msgid "Tags" 30 | msgstr "תגיות" 31 | 32 | #: taggit/managers.py:433 33 | msgid "A comma-separated list of tags." 34 | msgstr "רשימה של תגים מופרדת עם פסיקים." 35 | 36 | #: taggit/models.py:19 37 | msgctxt "A tag name" 38 | msgid "name" 39 | msgstr "שם" 40 | 41 | #: taggit/models.py:22 42 | msgctxt "A tag slug" 43 | msgid "slug" 44 | msgstr "" 45 | 46 | #: taggit/models.py:82 47 | msgid "tag" 48 | msgstr "תג" 49 | 50 | #: taggit/models.py:83 51 | msgid "tags" 52 | msgstr "" 53 | 54 | #: taggit/models.py:89 55 | #, python-format 56 | msgid "%(object)s tagged with %(tag)s" 57 | msgstr "%(object)s מתויג עם %(tag)s" 58 | 59 | #: taggit/models.py:134 60 | msgid "content type" 61 | msgstr "" 62 | 63 | #: taggit/models.py:165 taggit/models.py:172 64 | msgid "object ID" 65 | msgstr "" 66 | 67 | #: taggit/models.py:180 68 | msgid "tagged item" 69 | msgstr "" 70 | 71 | #: taggit/models.py:181 72 | msgid "tagged items" 73 | msgstr "" 74 | 75 | #: taggit/serializers.py:40 76 | #, python-brace-format 77 | msgid "Expected a list of items but got type \"{input_type}\"." 78 | msgstr "" 79 | 80 | #: taggit/serializers.py:43 81 | msgid "" 82 | "Invalid json list. A tag list submitted in string form must be valid json." 83 | msgstr "" 84 | 85 | #: taggit/serializers.py:46 86 | msgid "All list items must be of string type." 87 | msgstr "" 88 | -------------------------------------------------------------------------------- /taggit/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/it/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2021-11-14 11:35+0900\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: taggit/apps.py:7 21 | msgid "Taggit" 22 | msgstr "" 23 | 24 | #: taggit/forms.py:31 25 | msgid "Please provide a comma-separated list of tags." 26 | msgstr "Fornire una lista di tag separati da virgola." 27 | 28 | #: taggit/managers.py:432 29 | msgid "Tags" 30 | msgstr "Tag" 31 | 32 | #: taggit/managers.py:433 33 | msgid "A comma-separated list of tags." 34 | msgstr "Una lista di tag separati da virgola." 35 | 36 | #: taggit/models.py:19 37 | msgctxt "A tag name" 38 | msgid "name" 39 | msgstr "Nome" 40 | 41 | #: taggit/models.py:22 42 | msgctxt "A tag slug" 43 | msgid "slug" 44 | msgstr "Slug" 45 | 46 | #: taggit/models.py:82 47 | msgid "tag" 48 | msgstr "Tag" 49 | 50 | #: taggit/models.py:83 51 | msgid "tags" 52 | msgstr "" 53 | 54 | #: taggit/models.py:89 55 | #, python-format 56 | msgid "%(object)s tagged with %(tag)s" 57 | msgstr "%(object)s con tag %(tag)s" 58 | 59 | #: taggit/models.py:134 60 | msgid "content type" 61 | msgstr "Tipo" 62 | 63 | #: taggit/models.py:165 taggit/models.py:172 64 | msgid "object ID" 65 | msgstr "Id Oggetto" 66 | 67 | #: taggit/models.py:180 68 | msgid "tagged item" 69 | msgstr "Oggetto con tag" 70 | 71 | #: taggit/models.py:181 72 | msgid "tagged items" 73 | msgstr "Oggetti con tag" 74 | 75 | #: taggit/serializers.py:40 76 | #, python-brace-format 77 | msgid "Expected a list of items but got type \"{input_type}\"." 78 | msgstr "" 79 | 80 | #: taggit/serializers.py:43 81 | msgid "" 82 | "Invalid json list. A tag list submitted in string form must be valid json." 83 | msgstr "" 84 | 85 | #: taggit/serializers.py:46 86 | msgid "All list items must be of string type." 87 | msgstr "" 88 | -------------------------------------------------------------------------------- /taggit/locale/ja/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/ja/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/ja/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: django-taggit\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2021-11-14 11:39+0900\n" 6 | "PO-Revision-Date: 2014-04-23 08:05+0900\n" 7 | "Last-Translator: Tatsuo Ikeda \n" 8 | "Language-Team: \n" 9 | "Language: ja\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 14 | "X-Generator: Poedit 1.6.4\n" 15 | 16 | #: taggit/apps.py:7 17 | msgid "Taggit" 18 | msgstr "" 19 | 20 | #: taggit/forms.py:31 21 | msgid "Please provide a comma-separated list of tags." 22 | msgstr "複数タグはカンマ区切りのリストを入れてください。" 23 | 24 | #: taggit/managers.py:432 25 | msgid "Tags" 26 | msgstr "タグ一覧" 27 | 28 | #: taggit/managers.py:433 29 | msgid "A comma-separated list of tags." 30 | msgstr "複数タグはカンマ区切りのリスト。" 31 | 32 | #: taggit/models.py:19 33 | msgctxt "A tag name" 34 | msgid "name" 35 | msgstr "名称" 36 | 37 | #: taggit/models.py:22 38 | msgctxt "A tag slug" 39 | msgid "slug" 40 | msgstr "スラッグ" 41 | 42 | #: taggit/models.py:82 43 | msgid "tag" 44 | msgstr "タグ" 45 | 46 | #: taggit/models.py:83 47 | msgid "tags" 48 | msgstr "" 49 | 50 | #: taggit/models.py:89 51 | #, python-format 52 | msgid "%(object)s tagged with %(tag)s" 53 | msgstr "%(object)s tagged with %(tag)s" 54 | 55 | #: taggit/models.py:134 56 | msgid "content type" 57 | msgstr "コンテンツタイプ" 58 | 59 | #: taggit/models.py:165 taggit/models.py:172 60 | msgid "object ID" 61 | msgstr "オブジェクト ID" 62 | 63 | #: taggit/models.py:180 64 | msgid "tagged item" 65 | msgstr "タグ付け済みのアイテム" 66 | 67 | #: taggit/models.py:181 68 | msgid "tagged items" 69 | msgstr "タグ付け済みのアイテム一覧" 70 | 71 | #: taggit/serializers.py:40 72 | #, python-brace-format 73 | msgid "Expected a list of items but got type \"{input_type}\"." 74 | msgstr "" 75 | 76 | #: taggit/serializers.py:43 77 | msgid "" 78 | "Invalid json list. A tag list submitted in string form must be valid json." 79 | msgstr "" 80 | 81 | #: taggit/serializers.py:46 82 | msgid "All list items must be of string type." 83 | msgstr "" 84 | -------------------------------------------------------------------------------- /taggit/locale/nb/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/nb/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/nb/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: 0.9.3\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2021-11-14 11:41+0900\n" 11 | "PO-Revision-Date: 2012-12-08 14:42+0100\n" 12 | "Last-Translator: Bjørn Pettersen \n" 13 | "Language-Team: Norwegian \n" 14 | "Language: Norwegian\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 1.5.4\n" 19 | 20 | #: taggit/apps.py:7 21 | msgid "Taggit" 22 | msgstr "" 23 | 24 | #: taggit/forms.py:31 25 | msgid "Please provide a comma-separated list of tags." 26 | msgstr "Vennligst oppgi en kommaseparert tagg-liste." 27 | 28 | #: taggit/managers.py:432 29 | msgid "Tags" 30 | msgstr "Tagger" 31 | 32 | #: taggit/managers.py:433 33 | msgid "A comma-separated list of tags." 34 | msgstr "En kommaseparert tagg-liste." 35 | 36 | #: taggit/models.py:19 37 | msgctxt "A tag name" 38 | msgid "name" 39 | msgstr "Navn" 40 | 41 | #: taggit/models.py:22 42 | msgctxt "A tag slug" 43 | msgid "slug" 44 | msgstr "Slug" 45 | 46 | #: taggit/models.py:82 47 | msgid "tag" 48 | msgstr "Tagg" 49 | 50 | #: taggit/models.py:83 51 | msgid "tags" 52 | msgstr "" 53 | 54 | #: taggit/models.py:89 55 | #, python-format 56 | msgid "%(object)s tagged with %(tag)s" 57 | msgstr "%(object)s tagget med %(tag)s" 58 | 59 | #: taggit/models.py:134 60 | msgid "content type" 61 | msgstr "Innholdstype" 62 | 63 | #: taggit/models.py:165 taggit/models.py:172 64 | msgid "object ID" 65 | msgstr "Objekt-id" 66 | 67 | #: taggit/models.py:180 68 | msgid "tagged item" 69 | msgstr "Tagget Element" 70 | 71 | #: taggit/models.py:181 72 | msgid "tagged items" 73 | msgstr "Taggede Elementer" 74 | 75 | #: taggit/serializers.py:40 76 | #, python-brace-format 77 | msgid "Expected a list of items but got type \"{input_type}\"." 78 | msgstr "" 79 | 80 | #: taggit/serializers.py:43 81 | msgid "" 82 | "Invalid json list. A tag list submitted in string form must be valid json." 83 | msgstr "" 84 | 85 | #: taggit/serializers.py:46 86 | msgid "All list items must be of string type." 87 | msgstr "" 88 | -------------------------------------------------------------------------------- /taggit/locale/nl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/nl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/nl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: django-taggit\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2021-11-14 11:41+0900\n" 6 | "PO-Revision-Date: 2010-09-07 23:04+0100\n" 7 | "Last-Translator: Jeffrey Gelens \n" 8 | "Language-Team: Dutch\n" 9 | "Language: \n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | 14 | #: taggit/apps.py:7 15 | msgid "Taggit" 16 | msgstr "" 17 | 18 | #: taggit/forms.py:31 19 | msgid "Please provide a comma-separated list of tags." 20 | msgstr "Geef een door komma gescheiden lijst van tags." 21 | 22 | #: taggit/managers.py:432 23 | msgid "Tags" 24 | msgstr "Tags" 25 | 26 | #: taggit/managers.py:433 27 | msgid "A comma-separated list of tags." 28 | msgstr "Een door komma gescheiden lijst van tags." 29 | 30 | #: taggit/models.py:19 31 | msgctxt "A tag name" 32 | msgid "name" 33 | msgstr "Naam" 34 | 35 | #: taggit/models.py:22 36 | msgctxt "A tag slug" 37 | msgid "slug" 38 | msgstr "Slug" 39 | 40 | #: taggit/models.py:82 41 | #, fuzzy 42 | #| msgid "Tag" 43 | msgid "tag" 44 | msgstr "Tag" 45 | 46 | #: taggit/models.py:83 47 | msgid "tags" 48 | msgstr "" 49 | 50 | #: taggit/models.py:89 51 | #, python-format 52 | msgid "%(object)s tagged with %(tag)s" 53 | msgstr "%(object)s getagged met %(tag)s" 54 | 55 | #: taggit/models.py:134 56 | msgid "content type" 57 | msgstr "Inhoudstype" 58 | 59 | #: taggit/models.py:165 taggit/models.py:172 60 | msgid "object ID" 61 | msgstr "Object-id" 62 | 63 | #: taggit/models.py:180 64 | msgid "tagged item" 65 | msgstr "Object getagged" 66 | 67 | #: taggit/models.py:181 68 | msgid "tagged items" 69 | msgstr "Objecten getagged" 70 | 71 | #: taggit/serializers.py:40 72 | #, python-brace-format 73 | msgid "Expected a list of items but got type \"{input_type}\"." 74 | msgstr "" 75 | 76 | #: taggit/serializers.py:43 77 | msgid "" 78 | "Invalid json list. A tag list submitted in string form must be valid json." 79 | msgstr "" 80 | 81 | #: taggit/serializers.py:46 82 | msgid "All list items must be of string type." 83 | msgstr "" 84 | -------------------------------------------------------------------------------- /taggit/locale/pt_BR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/pt_BR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/pt_BR/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under WTFPL license. 2 | # 3 | # Translators: 4 | # RPB , 2013. 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: django-taggit\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2021-11-14 11:42+0900\n" 10 | "PO-Revision-Date: 2013-01-12 18:11-0200\n" 11 | "Last-Translator: RPB \n" 12 | "Language-Team: Portuguese (Brazil) \n" 13 | "Language: pt_BR\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=2; plural=(n > 1)\n" 18 | 19 | #: taggit/apps.py:7 20 | msgid "Taggit" 21 | msgstr "" 22 | 23 | #: taggit/forms.py:31 24 | msgid "Please provide a comma-separated list of tags." 25 | msgstr "Favor fornecer uma lista de marcadores separados por vírgula." 26 | 27 | #: taggit/managers.py:432 28 | msgid "Tags" 29 | msgstr "Marcadores" 30 | 31 | #: taggit/managers.py:433 32 | msgid "A comma-separated list of tags." 33 | msgstr "Uma lista de marcadores separados por vírgula." 34 | 35 | #: taggit/models.py:19 36 | msgctxt "A tag name" 37 | msgid "name" 38 | msgstr "Nome" 39 | 40 | #: taggit/models.py:22 41 | msgctxt "A tag slug" 42 | msgid "slug" 43 | msgstr "Slug" 44 | 45 | #: taggit/models.py:82 46 | msgid "tag" 47 | msgstr "Marcador" 48 | 49 | #: taggit/models.py:83 50 | msgid "tags" 51 | msgstr "" 52 | 53 | #: taggit/models.py:89 54 | #, python-format 55 | msgid "%(object)s tagged with %(tag)s" 56 | msgstr "%(object)s marcados com %(tag)s" 57 | 58 | #: taggit/models.py:134 59 | msgid "content type" 60 | msgstr "Tipo de conteúdo" 61 | 62 | #: taggit/models.py:165 taggit/models.py:172 63 | msgid "object ID" 64 | msgstr "Id do objeto" 65 | 66 | #: taggit/models.py:180 67 | msgid "tagged item" 68 | msgstr "Item marcado" 69 | 70 | #: taggit/models.py:181 71 | msgid "tagged items" 72 | msgstr "Itens marcados" 73 | 74 | #: taggit/serializers.py:40 75 | #, python-brace-format 76 | msgid "Expected a list of items but got type \"{input_type}\"." 77 | msgstr "" 78 | 79 | #: taggit/serializers.py:43 80 | msgid "" 81 | "Invalid json list. A tag list submitted in string form must be valid json." 82 | msgstr "" 83 | 84 | #: taggit/serializers.py:46 85 | msgid "All list items must be of string type." 86 | msgstr "" 87 | -------------------------------------------------------------------------------- /taggit/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Django Taggit\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2021-11-14 11:43+0900\n" 11 | "PO-Revision-Date: 2021-09-26 00:51+0300\n" 12 | "Last-Translator: Serghei Iakovlev \n" 13 | "Language-Team: \n" 14 | "Language: ru\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 19 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" 20 | 21 | #: taggit/apps.py:7 22 | msgid "Taggit" 23 | msgstr "Теги" 24 | 25 | #: taggit/forms.py:31 26 | msgid "Please provide a comma-separated list of tags." 27 | msgstr "Укажите теги через запятую." 28 | 29 | #: taggit/managers.py:432 30 | msgid "Tags" 31 | msgstr "Теги" 32 | 33 | #: taggit/managers.py:433 34 | msgid "A comma-separated list of tags." 35 | msgstr "Список тегов через запятую." 36 | 37 | #: taggit/models.py:19 38 | msgctxt "A tag name" 39 | msgid "name" 40 | msgstr "название" 41 | 42 | #: taggit/models.py:22 43 | msgctxt "A tag slug" 44 | msgid "slug" 45 | msgstr "слаг" 46 | 47 | #: taggit/models.py:82 48 | msgid "tag" 49 | msgstr "тег" 50 | 51 | #: taggit/models.py:83 52 | msgid "tags" 53 | msgstr "теги" 54 | 55 | #: taggit/models.py:89 56 | #, python-format 57 | msgid "%(object)s tagged with %(tag)s" 58 | msgstr "элемент «%(object)s» с тегом «%(tag)s»" 59 | 60 | #: taggit/models.py:134 61 | msgid "content type" 62 | msgstr "тип содержимого" 63 | 64 | #: taggit/models.py:165 taggit/models.py:172 65 | msgid "object ID" 66 | msgstr "идентификатор объекта" 67 | 68 | #: taggit/models.py:180 69 | msgid "tagged item" 70 | msgstr "элемент с меткой" 71 | 72 | #: taggit/models.py:181 73 | msgid "tagged items" 74 | msgstr "элементы с тегом" 75 | 76 | #: taggit/serializers.py:40 77 | #, python-brace-format 78 | msgid "Expected a list of items but got type \"{input_type}\"." 79 | msgstr "Ожидался список элементов, но получен тип «{input_type}»." 80 | 81 | #: taggit/serializers.py:43 82 | msgid "" 83 | "Invalid json list. A tag list submitted in string form must be valid json." 84 | msgstr "" 85 | "Неверный список json. Список тегов, представленный в строковой форме, должен " 86 | "быть корректным json." 87 | 88 | #: taggit/serializers.py:46 89 | msgid "All list items must be of string type." 90 | msgstr "Все элементы списка должны быть строкового типа." 91 | -------------------------------------------------------------------------------- /taggit/locale/tr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/tr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/tr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2021-11-14 11:44+0900\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: taggit/apps.py:7 22 | msgid "Taggit" 23 | msgstr "" 24 | 25 | #: taggit/forms.py:31 26 | msgid "Please provide a comma-separated list of tags." 27 | msgstr "Etiketleri bir virgülle ayrılmış listesini veriniz." 28 | 29 | #: taggit/managers.py:432 30 | msgid "Tags" 31 | msgstr "Etiketler" 32 | 33 | #: taggit/managers.py:433 34 | msgid "A comma-separated list of tags." 35 | msgstr "Etiketlerin virgülle ayrılmış listesi." 36 | 37 | #: taggit/models.py:19 38 | msgctxt "A tag name" 39 | msgid "name" 40 | msgstr "Adı" 41 | 42 | #: taggit/models.py:22 43 | msgctxt "A tag slug" 44 | msgid "slug" 45 | msgstr "Kısaltma" 46 | 47 | #: taggit/models.py:82 48 | msgid "tag" 49 | msgstr "Etiketler" 50 | 51 | #: taggit/models.py:83 52 | msgid "tags" 53 | msgstr "" 54 | 55 | #: taggit/models.py:89 56 | #, python-format 57 | msgid "%(object)s tagged with %(tag)s" 58 | msgstr "%(object)s %(tag)s ile etiketlendi" 59 | 60 | #: taggit/models.py:134 61 | msgid "content type" 62 | msgstr "İçerik türü" 63 | 64 | #: taggit/models.py:165 taggit/models.py:172 65 | msgid "object ID" 66 | msgstr "Nesne kimliği" 67 | 68 | #: taggit/models.py:180 69 | msgid "tagged item" 70 | msgstr "Takip edilen Öğe" 71 | 72 | #: taggit/models.py:181 73 | msgid "tagged items" 74 | msgstr "Takip edilen Öğeler" 75 | 76 | #: taggit/serializers.py:40 77 | #, python-brace-format 78 | msgid "Expected a list of items but got type \"{input_type}\"." 79 | msgstr "" 80 | 81 | #: taggit/serializers.py:43 82 | msgid "" 83 | "Invalid json list. A tag list submitted in string form must be valid json." 84 | msgstr "" 85 | 86 | #: taggit/serializers.py:46 87 | msgid "All list items must be of string type." 88 | msgstr "" 89 | -------------------------------------------------------------------------------- /taggit/locale/uk/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/uk/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/uk/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Django Taggit\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2021-11-14 11:44+0900\n" 11 | "PO-Revision-Date: 2010-06-11 11:30+0700\n" 12 | "Last-Translator: Igor 'idle sign' Starikov \n" 13 | "Language-Team: \n" 14 | "Language: uk\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 19 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" 20 | 21 | #: taggit/apps.py:7 22 | msgid "Taggit" 23 | msgstr "Мітка" 24 | 25 | #: taggit/forms.py:31 26 | msgid "Please provide a comma-separated list of tags." 27 | msgstr "Вкажіть мітки через кому." 28 | 29 | #: taggit/managers.py:432 30 | msgid "Tags" 31 | msgstr "Мітка" 32 | 33 | #: taggit/managers.py:433 34 | msgid "A comma-separated list of tags." 35 | msgstr "Список міток через кому." 36 | 37 | #: taggit/models.py:19 38 | msgctxt "A tag name" 39 | msgid "name" 40 | msgstr "Назва" 41 | 42 | #: taggit/models.py:22 43 | msgctxt "A tag slug" 44 | msgid "slug" 45 | msgstr "Слаг" 46 | 47 | #: taggit/models.py:82 48 | msgid "tag" 49 | msgstr "Мітка" 50 | 51 | #: taggit/models.py:83 52 | msgid "tags" 53 | msgstr "" 54 | 55 | #: taggit/models.py:89 56 | #, python-format 57 | msgid "%(object)s tagged with %(tag)s" 58 | msgstr "елемент «%(object)s» з міткою «%(tag)s»" 59 | 60 | #: taggit/models.py:134 61 | msgid "content type" 62 | msgstr "Тип вмісту" 63 | 64 | #: taggit/models.py:165 taggit/models.py:172 65 | msgid "object ID" 66 | msgstr "ID об'єкта" 67 | 68 | #: taggit/models.py:180 69 | msgid "tagged item" 70 | msgstr "Елемент з міткою" 71 | 72 | #: taggit/models.py:181 73 | msgid "tagged items" 74 | msgstr "Елементи з міткою" 75 | 76 | #: taggit/serializers.py:40 77 | #, python-brace-format 78 | msgid "Expected a list of items but got type \"{input_type}\"." 79 | msgstr "" 80 | 81 | #: taggit/serializers.py:43 82 | msgid "" 83 | "Invalid json list. A tag list submitted in string form must be valid json." 84 | msgstr "" 85 | 86 | #: taggit/serializers.py:46 87 | msgid "All list items must be of string type." 88 | msgstr "" 89 | -------------------------------------------------------------------------------- /taggit/locale/zh_Hans/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/locale/zh_Hans/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /taggit/locale/zh_Hans/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-01-30 16:06+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=1; plural=0;\n" 20 | 21 | #: taggit/apps.py:7 22 | msgid "Taggit" 23 | msgstr "标签项" 24 | 25 | #: taggit/forms.py:31 26 | msgid "Please provide a comma-separated list of tags." 27 | msgstr "请提供逗号分隔的标签列表。" 28 | 29 | #: taggit/managers.py:442 30 | msgid "Tags" 31 | msgstr "标签" 32 | 33 | #: taggit/managers.py:443 34 | msgid "A comma-separated list of tags." 35 | msgstr "逗号分隔的标签列表。" 36 | 37 | #: taggit/models.py:20 38 | msgctxt "A tag name" 39 | msgid "name" 40 | msgstr "名称" 41 | 42 | #: taggit/models.py:23 43 | msgctxt "A tag slug" 44 | msgid "slug" 45 | msgstr "唯一标识" 46 | 47 | #: taggit/models.py:89 48 | msgid "tag" 49 | msgstr "标签" 50 | 51 | #: taggit/models.py:90 52 | msgid "tags" 53 | msgstr "标签" 54 | 55 | #: taggit/models.py:96 56 | #, python-format 57 | msgid "%(object)s tagged with %(tag)s" 58 | msgstr "%(object)s 使用了标签 %(tag)s" 59 | 60 | #: taggit/models.py:141 61 | msgid "content type" 62 | msgstr "内容类型" 63 | 64 | #: taggit/models.py:172 taggit/models.py:179 65 | msgid "object ID" 66 | msgstr "对象ID" 67 | 68 | #: taggit/models.py:187 69 | msgid "tagged item" 70 | msgstr "标签项" 71 | 72 | #: taggit/models.py:188 73 | msgid "tagged items" 74 | msgstr "标签项" 75 | 76 | #: taggit/serializers.py:53 77 | #, python-brace-format 78 | msgid "Expected a list of items but got type \"{input_type}\"." 79 | msgstr "期望得到一个项目列表,但得到的是类型 “{input_type}”。" 80 | 81 | #: taggit/serializers.py:56 82 | msgid "" 83 | "Invalid json list. A tag list submitted in string form must be valid json." 84 | msgstr "无效的 JSON 列表。以字符串形式提交的标签列表必须是有效的 JSON。" 85 | 86 | #: taggit/serializers.py:59 87 | msgid "All list items must be of string type." 88 | msgstr "所有列表项必须是字符串类型。" 89 | -------------------------------------------------------------------------------- /taggit/management/commands/deduplicate_tags.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.management.base import BaseCommand 3 | from django.db import transaction 4 | 5 | from taggit.models import Tag, TaggedItem 6 | 7 | 8 | class Command(BaseCommand): 9 | help = "Identify and remove duplicate tags based on case insensitivity" 10 | 11 | def handle(self, *args, **kwargs): 12 | if not getattr(settings, "TAGGIT_CASE_INSENSITIVE", False): 13 | self.stdout.write( 14 | self.style.ERROR("TAGGIT_CASE_INSENSITIVE is not enabled.") 15 | ) 16 | return 17 | 18 | tags = Tag.objects.all() 19 | tag_dict = {} 20 | 21 | for tag in tags: 22 | lower_name = tag.name.lower() 23 | if lower_name in tag_dict: 24 | existing_tag = tag_dict[lower_name] 25 | self._deduplicate_tags(existing_tag=existing_tag, tag_to_remove=tag) 26 | else: 27 | tag_dict[lower_name] = tag 28 | 29 | self.stdout.write(self.style.SUCCESS("Tag deduplication complete.")) 30 | 31 | @transaction.atomic 32 | def _deduplicate_tags(self, existing_tag, tag_to_remove): 33 | """ 34 | Remove a tag by merging it into an existing tag 35 | """ 36 | # If this ends up very slow for you, please file a ticket! 37 | # This isn't trying to be performant, in order to keep the code simple. 38 | for item in TaggedItem.objects.filter(tag=tag_to_remove): 39 | # if we already have the same association on the model 40 | # (via the existing tag), then we can just remove the 41 | # tagged item. 42 | tag_exists_other = TaggedItem.objects.filter( 43 | tag=existing_tag, 44 | content_type_id=item.content_type_id, 45 | object_id=item.object_id, 46 | ).exists() 47 | if tag_exists_other: 48 | item.delete() 49 | else: 50 | item.tag = existing_tag 51 | item.save() 52 | 53 | # this should never trigger, but can never be too sure 54 | assert not TaggedItem.objects.filter( 55 | tag=tag_to_remove 56 | ).exists(), "Tags were not all cleaned up!" 57 | 58 | tag_to_remove.delete() 59 | 60 | def _collect_tagged_items(self, tag, existing_tag, tagged_items_to_update): 61 | for item in TaggedItem.objects.filter(tag=tag): 62 | tagged_items_to_update[(item.content_type_id, item.object_id)].append( 63 | existing_tag.id 64 | ) 65 | 66 | def _remove_duplicates_and_update(self, tagged_items_to_update): 67 | with transaction.atomic(): 68 | for (content_type_id, object_id), tag_ids in tagged_items_to_update.items(): 69 | unique_tag_ids = set(tag_ids) 70 | if len(unique_tag_ids) > 1: 71 | first_tag_id = unique_tag_ids.pop() 72 | for duplicate_tag_id in unique_tag_ids: 73 | TaggedItem.objects.filter( 74 | content_type_id=content_type_id, 75 | object_id=object_id, 76 | tag_id=duplicate_tag_id, 77 | ).delete() 78 | 79 | TaggedItem.objects.filter( 80 | content_type_id=content_type_id, 81 | object_id=object_id, 82 | tag_id=first_tag_id, 83 | ).update(tag_id=first_tag_id) 84 | -------------------------------------------------------------------------------- /taggit/management/commands/remove_orphaned_tags.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from taggit.models import Tag 4 | 5 | 6 | class Command(BaseCommand): 7 | help = "Remove orphaned tags" 8 | 9 | def handle(self, *args, **options): 10 | orphaned_tags = Tag.objects.orphaned() 11 | count = orphaned_tags.delete() 12 | self.stdout.write(f"Successfully removed {count} orphaned tags") 13 | -------------------------------------------------------------------------------- /taggit/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | dependencies = [("contenttypes", "0001_initial")] 6 | 7 | operations = [ 8 | migrations.CreateModel( 9 | name="Tag", 10 | fields=[ 11 | ( 12 | "id", 13 | models.AutoField( 14 | auto_created=True, 15 | primary_key=True, 16 | serialize=False, 17 | help_text="", 18 | verbose_name="ID", 19 | ), 20 | ), 21 | ( 22 | "name", 23 | models.CharField( 24 | help_text="", unique=True, max_length=100, verbose_name="name" 25 | ), 26 | ), 27 | ( 28 | "slug", 29 | models.SlugField( 30 | help_text="", unique=True, max_length=100, verbose_name="slug" 31 | ), 32 | ), 33 | ], 34 | options={"verbose_name": "tag", "verbose_name_plural": "tags"}, 35 | bases=(models.Model,), 36 | ), 37 | migrations.CreateModel( 38 | name="TaggedItem", 39 | fields=[ 40 | ( 41 | "id", 42 | models.AutoField( 43 | auto_created=True, 44 | primary_key=True, 45 | serialize=False, 46 | help_text="", 47 | verbose_name="ID", 48 | ), 49 | ), 50 | ( 51 | "object_id", 52 | models.IntegerField( 53 | help_text="", verbose_name="object ID", db_index=True 54 | ), 55 | ), 56 | ( 57 | "content_type", 58 | models.ForeignKey( 59 | related_name="taggit_taggeditem_tagged_items", 60 | verbose_name="content type", 61 | to="contenttypes.ContentType", 62 | help_text="", 63 | on_delete=models.CASCADE, 64 | ), 65 | ), 66 | ( 67 | "tag", 68 | models.ForeignKey( 69 | related_name="taggit_taggeditem_items", 70 | to="taggit.Tag", 71 | help_text="", 72 | on_delete=models.CASCADE, 73 | ), 74 | ), 75 | ], 76 | options={ 77 | "verbose_name": "tagged item", 78 | "verbose_name_plural": "tagged items", 79 | }, 80 | bases=(models.Model,), 81 | ), 82 | ] 83 | -------------------------------------------------------------------------------- /taggit/migrations/0002_auto_20150616_2121.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | dependencies = [("taggit", "0001_initial")] 6 | 7 | operations = [ 8 | # this migration was modified from previously being 9 | # a ModifyIndexTogether operation. 10 | # 11 | # If you are a long-enough user of this library, the name 12 | # of the index does not match what is written here. Please 13 | # query the DB itself to find out what the name originally was. 14 | migrations.AddIndex( 15 | "taggeditem", 16 | models.Index( 17 | fields=("content_type", "object_id"), 18 | # this is not the name of the index in previous version, 19 | # but this is necessary to deal with index_together issues. 20 | name="taggit_tagg_content_8fc721_idx", 21 | ), 22 | ) 23 | ] 24 | -------------------------------------------------------------------------------- /taggit/migrations/0003_taggeditem_add_unique_index.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | dependencies = [ 6 | ("contenttypes", "0002_remove_content_type_name"), 7 | ("taggit", "0002_auto_20150616_2121"), 8 | ] 9 | 10 | operations = [ 11 | # this migration was modified to declare a uniqueness constraint differently 12 | # this change was written on 2023-09-20, if any issues occurred from this please report it upstream 13 | migrations.AddConstraint( 14 | model_name="taggeditem", 15 | constraint=models.UniqueConstraint( 16 | fields=("content_type", "object_id", "tag"), 17 | name="taggit_taggeditem_content_type_id_object_id_tag_id_4bb97a8e_uniq", 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /taggit/migrations/0004_alter_taggeditem_content_type_alter_taggeditem_tag.py: -------------------------------------------------------------------------------- 1 | # This migration has no effect in practice. It exists to stop 2 | # Django from autodetecting migrations in taggit when users 3 | # update to Django 4.0. 4 | # See https://docs.djangoproject.com/en/stable/releases/4.0/#migrations-autodetector-changes 5 | import django.db.models.deletion 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | dependencies = [ 11 | ("contenttypes", "0002_remove_content_type_name"), 12 | ("taggit", "0003_taggeditem_add_unique_index"), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name="taggeditem", 18 | name="content_type", 19 | field=models.ForeignKey( 20 | on_delete=django.db.models.deletion.CASCADE, 21 | related_name="%(app_label)s_%(class)s_tagged_items", 22 | to="contenttypes.contenttype", 23 | verbose_name="content type", 24 | ), 25 | ), 26 | migrations.AlterField( 27 | model_name="taggeditem", 28 | name="tag", 29 | field=models.ForeignKey( 30 | on_delete=django.db.models.deletion.CASCADE, 31 | related_name="%(app_label)s_%(class)s_items", 32 | to="taggit.tag", 33 | ), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /taggit/migrations/0005_auto_20220424_2025.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.26 on 2022-04-24 20:25 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("taggit", "0004_alter_taggeditem_content_type_alter_taggeditem_tag"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="tag", 14 | name="slug", 15 | field=models.SlugField( 16 | allow_unicode=True, max_length=100, unique=True, verbose_name="slug" 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /taggit/migrations/0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-09-19 23:16 2 | from django.db import migrations 3 | 4 | 5 | class Migration(migrations.Migration): 6 | dependencies = [ 7 | ("taggit", "0005_auto_20220424_2025"), 8 | ] 9 | 10 | operations = [ 11 | migrations.RenameIndex( 12 | model_name="taggeditem", 13 | new_name="taggit_tagg_content_8fc721_idx", 14 | old_fields=("content_type", "object_id"), 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /taggit/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/taggit/migrations/__init__.py -------------------------------------------------------------------------------- /taggit/serializers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django-taggit serializer support 3 | 4 | Originally vendored from https://github.com/glemmaPaul/django-taggit-serializer 5 | """ 6 | 7 | import json 8 | 9 | # Third party 10 | from django.utils.translation import gettext_lazy 11 | from rest_framework import serializers 12 | 13 | 14 | class TagList(list): 15 | """ 16 | This tag list subclass adds pretty printing support to the tag list 17 | serializer 18 | """ 19 | 20 | def __init__(self, *args, **kwargs): 21 | pretty_print = kwargs.pop("pretty_print", True) 22 | super().__init__(*args, **kwargs) 23 | self.pretty_print = pretty_print 24 | 25 | def __add__(self, rhs): 26 | return TagList(super().__add__(rhs)) 27 | 28 | def __getitem__(self, item): 29 | result = super().__getitem__(item) 30 | try: 31 | return TagList(result) 32 | except TypeError: 33 | return result 34 | 35 | def __str__(self): 36 | if self.pretty_print: 37 | return json.dumps(self, sort_keys=True, indent=4, separators=(",", ": ")) 38 | else: 39 | return json.dumps(self) 40 | 41 | 42 | class TagListSerializerField(serializers.ListField): 43 | """ 44 | A serializer field that can write out a tag list 45 | 46 | This serializer field has some odd qualities compared to just using a ListField. 47 | If this field poses problems, we should introduce a new field that is a simpler 48 | ListField implementation with less features. 49 | """ 50 | 51 | child = serializers.CharField() 52 | default_error_messages = { 53 | "not_a_list": gettext_lazy( 54 | 'Expected a list of items but got type "{input_type}".' 55 | ), 56 | "invalid_json": gettext_lazy( 57 | "Invalid json list. A tag list submitted in string" 58 | " form must be valid json." 59 | ), 60 | "not_a_str": gettext_lazy("All list items must be of string type."), 61 | } 62 | order_by = None 63 | 64 | def __init__(self, **kwargs): 65 | pretty_print = kwargs.pop("pretty_print", True) 66 | 67 | style = kwargs.pop("style", {}) 68 | kwargs["style"] = {"base_template": "textarea.html"} 69 | kwargs["style"].update(style) 70 | 71 | super().__init__(**kwargs) 72 | 73 | self.pretty_print = pretty_print 74 | 75 | def to_internal_value(self, value): 76 | # note to future maintainers: this field used to not be a ListField 77 | # and has extra behavior to support string-based input. 78 | # 79 | # In the future we should look at removing this feature so we can 80 | # make this a simple ListField (if feasible) 81 | if isinstance(value, str): 82 | if not value: 83 | value = "[]" 84 | try: 85 | value = json.loads(value) 86 | except ValueError: 87 | self.fail("invalid_json") 88 | 89 | if not isinstance(value, list): 90 | self.fail("not_a_list", input_type=type(value).__name__) 91 | 92 | for s in value: 93 | if not isinstance(s, str): 94 | self.fail("not_a_str") 95 | 96 | self.child.run_validation(s) 97 | 98 | return value 99 | 100 | def to_representation(self, value): 101 | if not isinstance(value, TagList): 102 | if not isinstance(value, list): 103 | if self.order_by: 104 | tags = value.all().order_by(*self.order_by) 105 | else: 106 | tags = value.all() 107 | value = [tag.name for tag in tags] 108 | value = TagList(value, pretty_print=self.pretty_print) 109 | 110 | return value 111 | 112 | 113 | class TaggitSerializer(serializers.Serializer): 114 | def create(self, validated_data): 115 | to_be_tagged, validated_data = self._pop_tags(validated_data) 116 | 117 | tag_object = super().create(validated_data) 118 | 119 | return self._save_tags(tag_object, to_be_tagged) 120 | 121 | def update(self, instance, validated_data): 122 | to_be_tagged, validated_data = self._pop_tags(validated_data) 123 | 124 | tag_object = super().update(instance, validated_data) 125 | 126 | return self._save_tags(tag_object, to_be_tagged) 127 | 128 | def _save_tags(self, tag_object, tags): 129 | for key in tags.keys(): 130 | tag_values = tags.get(key) 131 | getattr(tag_object, key).set(tag_values) 132 | 133 | return tag_object 134 | 135 | def _pop_tags(self, validated_data): 136 | to_be_tagged = {} 137 | 138 | for key in self.fields.keys(): 139 | field = self.fields[key] 140 | if isinstance(field, TagListSerializerField): 141 | if key in validated_data: 142 | to_be_tagged[key] = validated_data.pop(key) 143 | 144 | return (to_be_tagged, validated_data) 145 | -------------------------------------------------------------------------------- /taggit/templates/admin/taggit/merge_tags_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} {% block content %} 2 |
3 |
4 |
5 |
6 |
11 | {% csrf_token %} {% for field in form %} 12 |
13 | {{ field.label_tag }} {{ field }} {% if field.errors %} 14 |
    15 | {% for error in field.errors %} 16 |
  • {{ error }}
  • 17 | {% endfor %} 18 |
19 | {% endif %} 20 |
21 | {% endfor %} 22 |

Enter new or existing tag name

23 |
24 | 25 |
26 |
27 |
28 |
29 |
30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /taggit/utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.functional import wraps 3 | from django.utils.module_loading import import_string 4 | 5 | 6 | def _parse_tags(tagstring): 7 | """ 8 | Parses tag input, with multiple word input being activated and 9 | delineated by commas and double quotes. Quotes take precedence, so 10 | they may contain commas. 11 | 12 | Returns a sorted list of unique tag names. 13 | 14 | Ported from Jonathan Buchanan's `django-tagging 15 | `_ 16 | """ 17 | if not tagstring: 18 | return [] 19 | 20 | # Special case - if there are no commas or double quotes in the 21 | # input, we don't *do* a recall... I mean, we know we only need to 22 | # split on spaces. 23 | if "," not in tagstring and '"' not in tagstring: 24 | words = list(set(split_strip(tagstring, " "))) 25 | words.sort() 26 | return words 27 | 28 | words = [] 29 | buffer = [] 30 | # Defer splitting of non-quoted sections until we know if there are 31 | # any unquoted commas. 32 | to_be_split = [] 33 | saw_loose_comma = False 34 | open_quote = False 35 | i = iter(tagstring) 36 | try: 37 | while True: 38 | c = next(i) 39 | if c == '"': 40 | if buffer: 41 | to_be_split.append("".join(buffer)) 42 | buffer = [] 43 | # Find the matching quote 44 | open_quote = True 45 | c = next(i) 46 | while c != '"': 47 | buffer.append(c) 48 | c = next(i) 49 | if buffer: 50 | word = "".join(buffer).strip() 51 | if word: 52 | words.append(word) 53 | buffer = [] 54 | open_quote = False 55 | else: 56 | if not saw_loose_comma and c == ",": 57 | saw_loose_comma = True 58 | buffer.append(c) 59 | except StopIteration: 60 | # If we were parsing an open quote which was never closed treat 61 | # the buffer as unquoted. 62 | if buffer: 63 | if open_quote and "," in buffer: 64 | saw_loose_comma = True 65 | to_be_split.append("".join(buffer)) 66 | if to_be_split: 67 | if saw_loose_comma: 68 | delimiter = "," 69 | else: 70 | delimiter = " " 71 | for chunk in to_be_split: 72 | words.extend(split_strip(chunk, delimiter)) 73 | words = list(set(words)) 74 | words.sort() 75 | return words 76 | 77 | 78 | def split_strip(string, delimiter=","): 79 | """ 80 | Splits ``string`` on ``delimiter``, stripping each resulting string 81 | and returning a list of non-empty strings. 82 | 83 | Ported from Jonathan Buchanan's `django-tagging 84 | `_ 85 | """ 86 | if not string: 87 | return [] 88 | 89 | words = [w.strip() for w in string.split(delimiter)] 90 | return [w for w in words if w] 91 | 92 | 93 | def _edit_string_for_tags(tags): 94 | """ 95 | Given list of ``Tag`` instances, creates a string representation of 96 | the list suitable for editing by the user, such that submitting the 97 | given string representation back without changing it will give the 98 | same list of tags. 99 | 100 | Tag names which contain commas will be double quoted. 101 | 102 | If any tag name which isn't being quoted contains whitespace, the 103 | resulting string of tag names will be comma-delimited, otherwise 104 | it will be space-delimited. 105 | 106 | Ported from Jonathan Buchanan's `django-tagging 107 | `_ 108 | """ 109 | names = [] 110 | for tag in tags: 111 | name = tag.name 112 | if "," in name or " " in name: 113 | names.append('"%s"' % name) 114 | else: 115 | names.append(name) 116 | return ", ".join(sorted(names)) 117 | 118 | 119 | def require_instance_manager(func): 120 | @wraps(func) 121 | def inner(self, *args, **kwargs): 122 | if self.instance is None: 123 | raise TypeError("Can't call %s with a non-instance manager" % func.__name__) 124 | return func(self, *args, **kwargs) 125 | 126 | return inner 127 | 128 | 129 | def get_func(key, default): 130 | func_path = getattr(settings, key, None) 131 | return default if func_path is None else import_string(func_path) 132 | 133 | 134 | def parse_tags(tagstring): 135 | func = get_func("TAGGIT_TAGS_FROM_STRING", _parse_tags) 136 | return func(tagstring) 137 | 138 | 139 | def edit_string_for_tags(tags): 140 | func = get_func("TAGGIT_STRING_FROM_TAGS", _edit_string_for_tags) 141 | return func(tags) 142 | -------------------------------------------------------------------------------- /taggit/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.models import ContentType 2 | from django.shortcuts import get_object_or_404 3 | from django.views.generic.list import ListView 4 | 5 | from taggit.models import Tag, TaggedItem 6 | 7 | 8 | def tagged_object_list(request, slug, queryset, **kwargs): 9 | if callable(queryset): 10 | queryset = queryset() 11 | kwargs["slug"] = slug 12 | tag_list_view = type( 13 | "TagListView", 14 | (TagListMixin, ListView), 15 | {"model": queryset.model, "queryset": queryset}, 16 | ) 17 | return tag_list_view.as_view()(request, **kwargs) 18 | 19 | 20 | class TagListMixin: 21 | tag_suffix = "_tag" 22 | 23 | def dispatch(self, request, *args, **kwargs): 24 | slug = kwargs.pop("slug") 25 | self.tag = get_object_or_404(Tag, slug=slug) 26 | return super().dispatch(request, *args, **kwargs) 27 | 28 | def get_queryset(self, **kwargs): 29 | qs = super().get_queryset(**kwargs) 30 | return qs.filter( 31 | pk__in=TaggedItem.objects.filter( 32 | tag=self.tag, content_type=ContentType.objects.get_for_model(qs.model) 33 | ).values_list("object_id", flat=True) 34 | ) 35 | 36 | def get_template_names(self): 37 | if self.tag_suffix: 38 | self.template_name_suffix = self.tag_suffix + self.template_name_suffix 39 | return super().get_template_names() 40 | 41 | def get_context_data(self, **kwargs): 42 | context = super().get_context_data(**kwargs) 43 | if "extra_context" not in context: 44 | context["extra_context"] = {} 45 | context["extra_context"]["tag"] = self.tag 46 | return context 47 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/tests/__init__.py -------------------------------------------------------------------------------- /tests/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from . import models 4 | 5 | admin.site.register(models.Food) 6 | -------------------------------------------------------------------------------- /tests/custom_parser.py: -------------------------------------------------------------------------------- 1 | def comma_splitter(tag_string): 2 | return [t.strip() for t in tag_string.split(",") if t.strip()] 3 | 4 | 5 | def comma_joiner(tags): 6 | return ", ".join(t.name for t in tags) 7 | -------------------------------------------------------------------------------- /tests/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import ( 4 | BlankTagModel, 5 | CustomPKFood, 6 | DirectCustomPKFood, 7 | DirectFood, 8 | Food, 9 | OfficialFood, 10 | ) 11 | 12 | 13 | class BlankTagForm(forms.ModelForm): 14 | class Meta: 15 | model = BlankTagModel 16 | fields = "__all__" 17 | 18 | 19 | class FoodForm(forms.ModelForm): 20 | class Meta: 21 | model = Food 22 | fields = "__all__" 23 | 24 | 25 | class DirectFoodForm(forms.ModelForm): 26 | class Meta: 27 | model = DirectFood 28 | fields = "__all__" 29 | 30 | 31 | class DirectCustomPKFoodForm(forms.ModelForm): 32 | class Meta: 33 | model = DirectCustomPKFood 34 | fields = "__all__" 35 | 36 | 37 | class CustomPKFoodForm(forms.ModelForm): 38 | class Meta: 39 | model = CustomPKFood 40 | fields = "__all__" 41 | 42 | 43 | class OfficialFoodForm(forms.ModelForm): 44 | class Meta: 45 | model = OfficialFood 46 | fields = "__all__" 47 | -------------------------------------------------------------------------------- /tests/migrations/0002_auto_20200214_1129.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.3 on 2020-02-14 11:29 2 | 3 | import uuid 4 | 5 | import django.utils.timezone 6 | from django.db import migrations, models 7 | 8 | import taggit.managers 9 | 10 | 11 | class Migration(migrations.Migration): 12 | dependencies = [ 13 | ("taggit", "0003_taggeditem_add_unique_index"), 14 | ("contenttypes", "0002_remove_content_type_name"), 15 | ("tests", "0001_initial"), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name="UUIDPet", 21 | fields=[ 22 | ( 23 | "id", 24 | models.UUIDField( 25 | default=uuid.uuid4, 26 | editable=False, 27 | primary_key=True, 28 | serialize=False, 29 | ), 30 | ), 31 | ("name", models.CharField(max_length=50)), 32 | ("created_at", models.DateTimeField(auto_now_add=True)), 33 | ], 34 | options={"ordering": ["created_at"]}, 35 | ), 36 | migrations.AlterModelOptions( 37 | name="uuidfood", options={"ordering": ["created_at"]} 38 | ), 39 | migrations.AddField( 40 | model_name="blanktagmodel", 41 | name="tags", 42 | field=taggit.managers.TaggableManager( 43 | blank=True, 44 | help_text="A comma-separated list of tags.", 45 | through="taggit.TaggedItem", 46 | to="taggit.Tag", 47 | verbose_name="Tags", 48 | ), 49 | ), 50 | migrations.AddField( 51 | model_name="uuidfood", 52 | name="created_at", 53 | field=models.DateTimeField( 54 | auto_now_add=True, default=django.utils.timezone.now 55 | ), 56 | preserve_default=False, 57 | ), 58 | migrations.AlterField( 59 | model_name="taggedtrackedfood", 60 | name="tag", 61 | field=models.ForeignKey( 62 | on_delete=django.db.models.deletion.CASCADE, 63 | related_name="taggedtrackedfood_items", 64 | to="tests.TrackedTag", 65 | ), 66 | ), 67 | migrations.AlterField( 68 | model_name="taggedtrackedpet", 69 | name="tag", 70 | field=models.ForeignKey( 71 | on_delete=django.db.models.deletion.CASCADE, 72 | related_name="taggedtrackedpet_items", 73 | to="tests.TrackedTag", 74 | ), 75 | ), 76 | migrations.AlterUniqueTogether( 77 | name="uuidtaggeditem", 78 | unique_together={("content_type", "object_id", "tag")}, 79 | ), 80 | migrations.CreateModel( 81 | name="UUIDHousePet", 82 | fields=[ 83 | ( 84 | "uuidpet_ptr", 85 | models.OneToOneField( 86 | auto_created=True, 87 | on_delete=django.db.models.deletion.CASCADE, 88 | parent_link=True, 89 | primary_key=True, 90 | serialize=False, 91 | to="tests.UUIDPet", 92 | ), 93 | ), 94 | ("trained", models.BooleanField(default=False)), 95 | ], 96 | bases=("tests.uuidpet",), 97 | ), 98 | migrations.AddField( 99 | model_name="uuidpet", 100 | name="tags", 101 | field=taggit.managers.TaggableManager( 102 | help_text="A comma-separated list of tags.", 103 | through="tests.UUIDTaggedItem", 104 | to="tests.UUIDTag", 105 | verbose_name="Tags", 106 | ), 107 | ), 108 | ] 109 | -------------------------------------------------------------------------------- /tests/migrations/0003_auto_20210310_0918.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-03-10 09:18 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | import taggit.managers 7 | 8 | 9 | class Migration(migrations.Migration): 10 | dependencies = [ 11 | ("taggit", "0003_taggeditem_add_unique_index"), 12 | ("contenttypes", "0002_remove_content_type_name"), 13 | ("tests", "0002_auto_20200214_1129"), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="BaseFood", 19 | fields=[ 20 | ( 21 | "id", 22 | models.AutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ("name", models.CharField(max_length=50)), 30 | ], 31 | ), 32 | migrations.CreateModel( 33 | name="MultiInheritanceFood", 34 | fields=[ 35 | ( 36 | "basefood_ptr", 37 | models.OneToOneField( 38 | auto_created=True, 39 | on_delete=django.db.models.deletion.CASCADE, 40 | parent_link=True, 41 | primary_key=True, 42 | serialize=False, 43 | to="tests.basefood", 44 | ), 45 | ), 46 | ], 47 | bases=("tests.basefood",), 48 | ), 49 | migrations.CreateModel( 50 | name="MultiInheritanceLazyResolutionFoodTag", 51 | fields=[ 52 | ( 53 | "id", 54 | models.AutoField( 55 | auto_created=True, 56 | primary_key=True, 57 | serialize=False, 58 | verbose_name="ID", 59 | ), 60 | ), 61 | ( 62 | "tag", 63 | models.ForeignKey( 64 | on_delete=django.db.models.deletion.CASCADE, 65 | related_name="tests_multiinheritancelazyresolutionfoodtag_items", 66 | to="taggit.tag", 67 | ), 68 | ), 69 | ( 70 | "content_object", 71 | models.ForeignKey( 72 | on_delete=django.db.models.deletion.CASCADE, 73 | related_name="tagged_items", 74 | to="tests.multiinheritancefood", 75 | ), 76 | ), 77 | ], 78 | options={ 79 | "unique_together": {("content_object", "tag")}, 80 | }, 81 | ), 82 | migrations.AddField( 83 | model_name="multiinheritancefood", 84 | name="tags", 85 | field=taggit.managers.TaggableManager( 86 | help_text="A comma-separated list of tags.", 87 | through="tests.MultiInheritanceLazyResolutionFoodTag", 88 | to="taggit.Tag", 89 | verbose_name="Tags", 90 | ), 91 | ), 92 | ] 93 | -------------------------------------------------------------------------------- /tests/migrations/0004_auto_20210619_0826.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2021-06-19 08:26 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | import taggit.managers 7 | 8 | 9 | class Migration(migrations.Migration): 10 | dependencies = [ 11 | ("contenttypes", "0002_remove_content_type_name"), 12 | ("taggit", "0003_taggeditem_add_unique_index"), 13 | ("tests", "0003_auto_20210310_0918"), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name="officialtag", 19 | name="name", 20 | field=models.CharField(max_length=100, unique=True, verbose_name="name"), 21 | ), 22 | migrations.AlterField( 23 | model_name="officialtag", 24 | name="slug", 25 | field=models.SlugField(max_length=100, unique=True, verbose_name="slug"), 26 | ), 27 | migrations.AlterField( 28 | model_name="officialthroughmodel", 29 | name="content_type", 30 | field=models.ForeignKey( 31 | on_delete=django.db.models.deletion.CASCADE, 32 | related_name="tests_officialthroughmodel_tagged_items", 33 | to="contenttypes.contenttype", 34 | verbose_name="content type", 35 | ), 36 | ), 37 | migrations.AlterField( 38 | model_name="officialthroughmodel", 39 | name="object_id", 40 | field=models.IntegerField(db_index=True, verbose_name="object ID"), 41 | ), 42 | migrations.AlterField( 43 | model_name="taggedcustompk", 44 | name="content_type", 45 | field=models.ForeignKey( 46 | on_delete=django.db.models.deletion.CASCADE, 47 | related_name="tests_taggedcustompk_tagged_items", 48 | to="contenttypes.contenttype", 49 | verbose_name="content type", 50 | ), 51 | ), 52 | migrations.AlterField( 53 | model_name="throughgfk", 54 | name="content_type", 55 | field=models.ForeignKey( 56 | on_delete=django.db.models.deletion.CASCADE, 57 | related_name="tests_throughgfk_tagged_items", 58 | to="contenttypes.contenttype", 59 | verbose_name="content type", 60 | ), 61 | ), 62 | migrations.AlterField( 63 | model_name="throughgfk", 64 | name="object_id", 65 | field=models.IntegerField(db_index=True, verbose_name="object ID"), 66 | ), 67 | migrations.AlterField( 68 | model_name="trackedtag", 69 | name="name", 70 | field=models.CharField(max_length=100, unique=True, verbose_name="name"), 71 | ), 72 | migrations.AlterField( 73 | model_name="trackedtag", 74 | name="slug", 75 | field=models.SlugField(max_length=100, unique=True, verbose_name="slug"), 76 | ), 77 | migrations.AlterField( 78 | model_name="uuidtag", 79 | name="name", 80 | field=models.CharField(max_length=100, unique=True, verbose_name="name"), 81 | ), 82 | migrations.AlterField( 83 | model_name="uuidtag", 84 | name="slug", 85 | field=models.SlugField(max_length=100, unique=True, verbose_name="slug"), 86 | ), 87 | migrations.AlterField( 88 | model_name="uuidtaggeditem", 89 | name="content_type", 90 | field=models.ForeignKey( 91 | on_delete=django.db.models.deletion.CASCADE, 92 | related_name="tests_uuidtaggeditem_tagged_items", 93 | to="contenttypes.contenttype", 94 | verbose_name="content type", 95 | ), 96 | ), 97 | migrations.AlterField( 98 | model_name="uuidtaggeditem", 99 | name="object_id", 100 | field=models.UUIDField(db_index=True, verbose_name="object ID"), 101 | ), 102 | migrations.CreateModel( 103 | name="TestModel", 104 | fields=[ 105 | ( 106 | "id", 107 | models.AutoField( 108 | auto_created=True, 109 | primary_key=True, 110 | serialize=False, 111 | verbose_name="ID", 112 | ), 113 | ), 114 | ( 115 | "tags", 116 | taggit.managers.TaggableManager( 117 | help_text="A comma-separated list of tags.", 118 | through="taggit.TaggedItem", 119 | to="taggit.Tag", 120 | verbose_name="Tags", 121 | ), 122 | ), 123 | ], 124 | ), 125 | ] 126 | -------------------------------------------------------------------------------- /tests/migrations/0005_auto_20210713_2301.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2021-06-19 08:26 2 | 3 | from django.db import migrations, models 4 | 5 | import taggit.managers 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("tests", "0004_auto_20210619_0826"), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="OrderedModel", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ( 27 | "tags", 28 | taggit.managers.TaggableManager( 29 | help_text="A comma-separated list of tags.", 30 | through="taggit.TaggedItem", 31 | to="taggit.Tag", 32 | verbose_name="Tags", 33 | ), 34 | ), 35 | ], 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /tests/migrations/0006_auto_20230622_0834.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.19 on 2023-06-22 08:34 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | import taggit.managers 7 | 8 | 9 | class Migration(migrations.Migration): 10 | dependencies = [ 11 | ("contenttypes", "0002_remove_content_type_name"), 12 | ("tests", "0005_auto_20210713_2301"), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="TenantTag", 18 | fields=[ 19 | ( 20 | "id", 21 | models.AutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ("name", models.CharField(max_length=100)), 29 | ("slug", models.SlugField(allow_unicode=True, max_length=100)), 30 | ("tenant_id", models.IntegerField()), 31 | ], 32 | options={ 33 | "unique_together": {("name", "tenant_id"), ("slug", "tenant_id")}, 34 | }, 35 | ), 36 | migrations.AlterField( 37 | model_name="officialtag", 38 | name="slug", 39 | field=models.SlugField( 40 | allow_unicode=True, max_length=100, unique=True, verbose_name="slug" 41 | ), 42 | ), 43 | migrations.AlterField( 44 | model_name="trackedtag", 45 | name="slug", 46 | field=models.SlugField( 47 | allow_unicode=True, max_length=100, unique=True, verbose_name="slug" 48 | ), 49 | ), 50 | migrations.AlterField( 51 | model_name="uuidtag", 52 | name="slug", 53 | field=models.SlugField( 54 | allow_unicode=True, max_length=100, unique=True, verbose_name="slug" 55 | ), 56 | ), 57 | migrations.CreateModel( 58 | name="TenantTaggedItem", 59 | fields=[ 60 | ( 61 | "id", 62 | models.AutoField( 63 | auto_created=True, 64 | primary_key=True, 65 | serialize=False, 66 | verbose_name="ID", 67 | ), 68 | ), 69 | ( 70 | "object_id", 71 | models.IntegerField(db_index=True, verbose_name="object ID"), 72 | ), 73 | ( 74 | "content_type", 75 | models.ForeignKey( 76 | on_delete=django.db.models.deletion.CASCADE, 77 | related_name="tests_tenanttaggeditem_tagged_items", 78 | to="contenttypes.contenttype", 79 | verbose_name="content type", 80 | ), 81 | ), 82 | ( 83 | "tag", 84 | models.ForeignKey( 85 | on_delete=django.db.models.deletion.CASCADE, 86 | related_name="tests_tenanttaggeditem_items", 87 | to="tests.tenanttag", 88 | ), 89 | ), 90 | ], 91 | options={ 92 | "abstract": False, 93 | }, 94 | ), 95 | migrations.CreateModel( 96 | name="TenantModel", 97 | fields=[ 98 | ( 99 | "id", 100 | models.AutoField( 101 | auto_created=True, 102 | primary_key=True, 103 | serialize=False, 104 | verbose_name="ID", 105 | ), 106 | ), 107 | ("name", models.CharField(max_length=50)), 108 | ( 109 | "tags", 110 | taggit.managers.TaggableManager( 111 | help_text="A comma-separated list of tags.", 112 | through="tests.TenantTaggedItem", 113 | to="tests.TenantTag", 114 | verbose_name="Tags", 115 | ), 116 | ), 117 | ], 118 | ), 119 | ] 120 | -------------------------------------------------------------------------------- /tests/migrations/0007_alter_multiinheritancelazyresolutionfoodtag_tag_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2 on 2023-07-24 00:05 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("contenttypes", "0002_remove_content_type_name"), 10 | ("taggit", "0005_auto_20220424_2025"), 11 | ("tests", "0006_auto_20230622_0834"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="multiinheritancelazyresolutionfoodtag", 17 | name="tag", 18 | field=models.ForeignKey( 19 | on_delete=django.db.models.deletion.CASCADE, 20 | related_name="%(app_label)s_%(class)s_items", 21 | to="taggit.tag", 22 | ), 23 | ), 24 | migrations.AlterField( 25 | model_name="officialthroughmodel", 26 | name="content_type", 27 | field=models.ForeignKey( 28 | on_delete=django.db.models.deletion.CASCADE, 29 | related_name="%(app_label)s_%(class)s_tagged_items", 30 | to="contenttypes.contenttype", 31 | verbose_name="content type", 32 | ), 33 | ), 34 | migrations.AlterField( 35 | model_name="taggedcustompk", 36 | name="content_type", 37 | field=models.ForeignKey( 38 | on_delete=django.db.models.deletion.CASCADE, 39 | related_name="%(app_label)s_%(class)s_tagged_items", 40 | to="contenttypes.contenttype", 41 | verbose_name="content type", 42 | ), 43 | ), 44 | migrations.AlterField( 45 | model_name="taggedcustompk", 46 | name="tag", 47 | field=models.ForeignKey( 48 | on_delete=django.db.models.deletion.CASCADE, 49 | related_name="%(app_label)s_%(class)s_items", 50 | to="taggit.tag", 51 | ), 52 | ), 53 | migrations.AlterField( 54 | model_name="taggedcustompkfood", 55 | name="tag", 56 | field=models.ForeignKey( 57 | on_delete=django.db.models.deletion.CASCADE, 58 | related_name="%(app_label)s_%(class)s_items", 59 | to="taggit.tag", 60 | ), 61 | ), 62 | migrations.AlterField( 63 | model_name="taggedcustompkpet", 64 | name="tag", 65 | field=models.ForeignKey( 66 | on_delete=django.db.models.deletion.CASCADE, 67 | related_name="%(app_label)s_%(class)s_items", 68 | to="taggit.tag", 69 | ), 70 | ), 71 | migrations.AlterField( 72 | model_name="taggedfood", 73 | name="tag", 74 | field=models.ForeignKey( 75 | on_delete=django.db.models.deletion.CASCADE, 76 | related_name="%(app_label)s_%(class)s_items", 77 | to="taggit.tag", 78 | ), 79 | ), 80 | migrations.AlterField( 81 | model_name="taggedpet", 82 | name="tag", 83 | field=models.ForeignKey( 84 | on_delete=django.db.models.deletion.CASCADE, 85 | related_name="%(app_label)s_%(class)s_items", 86 | to="taggit.tag", 87 | ), 88 | ), 89 | migrations.AlterField( 90 | model_name="taggedtrackedfood", 91 | name="tag", 92 | field=models.ForeignKey( 93 | on_delete=django.db.models.deletion.CASCADE, 94 | related_name="%(class)s_items", 95 | to="tests.trackedtag", 96 | ), 97 | ), 98 | migrations.AlterField( 99 | model_name="taggedtrackedpet", 100 | name="tag", 101 | field=models.ForeignKey( 102 | on_delete=django.db.models.deletion.CASCADE, 103 | related_name="%(class)s_items", 104 | to="tests.trackedtag", 105 | ), 106 | ), 107 | migrations.AlterField( 108 | model_name="tenanttaggeditem", 109 | name="content_type", 110 | field=models.ForeignKey( 111 | on_delete=django.db.models.deletion.CASCADE, 112 | related_name="%(app_label)s_%(class)s_tagged_items", 113 | to="contenttypes.contenttype", 114 | verbose_name="content type", 115 | ), 116 | ), 117 | migrations.AlterField( 118 | model_name="tenanttaggeditem", 119 | name="tag", 120 | field=models.ForeignKey( 121 | on_delete=django.db.models.deletion.CASCADE, 122 | related_name="%(app_label)s_%(class)s_items", 123 | to="tests.tenanttag", 124 | ), 125 | ), 126 | migrations.AlterField( 127 | model_name="through1", 128 | name="tag", 129 | field=models.ForeignKey( 130 | on_delete=django.db.models.deletion.CASCADE, 131 | related_name="%(app_label)s_%(class)s_items", 132 | to="taggit.tag", 133 | ), 134 | ), 135 | migrations.AlterField( 136 | model_name="through2", 137 | name="tag", 138 | field=models.ForeignKey( 139 | on_delete=django.db.models.deletion.CASCADE, 140 | related_name="%(app_label)s_%(class)s_items", 141 | to="taggit.tag", 142 | ), 143 | ), 144 | migrations.AlterField( 145 | model_name="throughgfk", 146 | name="content_type", 147 | field=models.ForeignKey( 148 | on_delete=django.db.models.deletion.CASCADE, 149 | related_name="%(app_label)s_%(class)s_tagged_items", 150 | to="contenttypes.contenttype", 151 | verbose_name="content type", 152 | ), 153 | ), 154 | migrations.AlterField( 155 | model_name="uuidtaggeditem", 156 | name="content_type", 157 | field=models.ForeignKey( 158 | on_delete=django.db.models.deletion.CASCADE, 159 | related_name="%(app_label)s_%(class)s_tagged_items", 160 | to="contenttypes.contenttype", 161 | verbose_name="content type", 162 | ), 163 | ), 164 | migrations.AlterField( 165 | model_name="uuidtaggeditem", 166 | name="tag", 167 | field=models.ForeignKey( 168 | on_delete=django.db.models.deletion.CASCADE, 169 | related_name="%(app_label)s_%(class)s_items", 170 | to="tests.uuidtag", 171 | ), 172 | ), 173 | ] 174 | -------------------------------------------------------------------------------- /tests/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-taggit/7af1e7415225ef00c801a7e687137a2a0eb9f323/tests/migrations/__init__.py -------------------------------------------------------------------------------- /tests/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from taggit.serializers import TaggitSerializer, TagListSerializerField 4 | 5 | from .models import TestModel 6 | 7 | 8 | class TestModelSerializer(TaggitSerializer, serializers.ModelSerializer): 9 | tags = TagListSerializerField() 10 | 11 | class Meta: 12 | model = TestModel 13 | fields = "__all__" 14 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | SECRET_KEY = "secretkey" 2 | 3 | INSTALLED_APPS = [ 4 | "django.contrib.admin", 5 | "django.contrib.auth", 6 | "django.contrib.contenttypes", 7 | "django.contrib.sessions", 8 | "django.contrib.messages", 9 | "django.contrib.staticfiles", 10 | "taggit", 11 | "tests", 12 | ] 13 | 14 | MIDDLEWARE = [ 15 | "django.middleware.security.SecurityMiddleware", 16 | "django.contrib.sessions.middleware.SessionMiddleware", 17 | "django.middleware.common.CommonMiddleware", 18 | "django.middleware.csrf.CsrfViewMiddleware", 19 | "django.contrib.auth.middleware.AuthenticationMiddleware", 20 | "django.contrib.messages.middleware.MessageMiddleware", 21 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 22 | ] 23 | 24 | ROOT_URLCONF = "tests.urls" 25 | 26 | TEMPLATES = [ 27 | { 28 | "BACKEND": "django.template.backends.django.DjangoTemplates", 29 | "APP_DIRS": True, 30 | "OPTIONS": { 31 | "context_processors": [ 32 | "django.template.context_processors.debug", 33 | "django.template.context_processors.request", 34 | "django.contrib.auth.context_processors.auth", 35 | "django.contrib.messages.context_processors.messages", 36 | ] 37 | }, 38 | } 39 | ] 40 | 41 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} 42 | 43 | STATIC_URL = "/static/" 44 | 45 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 46 | 47 | USE_TZ = True 48 | -------------------------------------------------------------------------------- /tests/templates/tests/food_tag_list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/test_admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.test import TestCase 3 | from django.urls import reverse 4 | 5 | from taggit.models import Tag 6 | 7 | from .models import Food 8 | 9 | 10 | class AdminTest(TestCase): 11 | def setUp(self): 12 | super().setUp() 13 | self.apple = Food.objects.create(name="apple") 14 | self.apple.tags.add("Red", "red") 15 | self.pear = Food.objects.create(name="pear") 16 | self.pear.tags.add("red", "RED") 17 | self.peach = Food.objects.create(name="peach") 18 | self.peach.tags.add("red", "Yellow") 19 | 20 | user = User.objects.create_superuser( 21 | username="admin", email="admin@mailinator.com", password="password" 22 | ) 23 | self.client.force_login(user) 24 | 25 | def test_get_changelist(self): 26 | response = self.client.get(reverse("admin:tests_food_changelist")) 27 | self.assertEqual(response.status_code, 200) 28 | 29 | def test_get_add(self): 30 | response = self.client.get(reverse("admin:tests_food_add")) 31 | self.assertEqual(response.status_code, 200) 32 | 33 | def test_get_history(self): 34 | response = self.client.get( 35 | reverse("admin:tests_food_history", args=(self.apple.pk,)) 36 | ) 37 | self.assertEqual(response.status_code, 200) 38 | 39 | def test_get_delete(self): 40 | response = self.client.get( 41 | reverse("admin:tests_food_delete", args=(self.apple.pk,)) 42 | ) 43 | self.assertEqual(response.status_code, 200) 44 | 45 | def test_get_change(self): 46 | response = self.client.get( 47 | reverse("admin:tests_food_change", args=(self.apple.pk,)) 48 | ) 49 | self.assertEqual(response.status_code, 200) 50 | 51 | def test_tag_merging(self): 52 | response = self.client.get(reverse("admin:taggit_tag_changelist")) 53 | 54 | # merging red and RED into Red 55 | pks_to_select = [Tag.objects.get(name="red").pk, Tag.objects.get(name="RED").pk] 56 | response = self.client.post( 57 | reverse("admin:taggit_tag_changelist"), 58 | data={"action": "render_tag_form", "_selected_action": pks_to_select}, 59 | ) 60 | # we're redirecting 61 | self.assertEqual(response.status_code, 302) 62 | self.assertEqual(response.url, reverse("admin:taggit_tag_merge_tags")) 63 | # make sure what we expected got into the session keys 64 | assert "selected_tag_ids" in self.client.session.keys() 65 | self.assertEqual( 66 | self.client.session["selected_tag_ids"], ",".join(map(str, pks_to_select)) 67 | ) 68 | 69 | # let's do the actual merge operation 70 | response = self.client.post( 71 | reverse("admin:taggit_tag_merge_tags"), {"new_tag_name": "Red"} 72 | ) 73 | self.assertEqual(response.status_code, 302) 74 | 75 | # time to check the result of the merges 76 | self.assertSetEqual({tag.name for tag in self.apple.tags.all()}, {"Red"}) 77 | self.assertSetEqual({tag.name for tag in self.pear.tags.all()}, {"Red"}) 78 | self.assertSetEqual( 79 | {tag.name for tag in self.peach.tags.all()}, {"Yellow", "Red"} 80 | ) 81 | -------------------------------------------------------------------------------- /tests/test_deduplicate_tags.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | 3 | from django.conf import settings 4 | from django.core.management import call_command 5 | from django.test import TestCase 6 | 7 | from taggit.models import Tag, TaggedItem 8 | from tests.models import Food, HousePet 9 | 10 | 11 | class DeduplicateTagsTests(TestCase): 12 | def setUp(self): 13 | settings.TAGGIT_CASE_INSENSITIVE = True 14 | 15 | self.tag1 = Tag.objects.create(name="Python") 16 | self.tag2 = Tag.objects.create(name="python") 17 | self.tag3 = Tag.objects.create(name="PYTHON") 18 | 19 | self.food_item = Food.objects.create(name="Apple") 20 | self.pet_item = HousePet.objects.create(name="Fido") 21 | 22 | self.food_item.tags.add(self.tag1) 23 | self.pet_item.tags.add(self.tag2) 24 | self.pet_item.tags.add(self.tag3) 25 | 26 | def test_deduplicate_tags(self): 27 | self.assertEqual(Tag.objects.count(), 3) 28 | self.assertEqual(TaggedItem.objects.count(), 3) 29 | 30 | out = StringIO() 31 | call_command("deduplicate_tags", stdout=out) 32 | 33 | self.assertEqual(Tag.objects.count(), 1) 34 | self.assertEqual(TaggedItem.objects.count(), 2) 35 | 36 | self.assertTrue(Tag.objects.filter(name__iexact="python").exists()) 37 | self.assertEqual( 38 | TaggedItem.objects.filter(tag__name__iexact="python").count(), 2 39 | ) 40 | 41 | self.assertIn("Tag deduplication complete.", out.getvalue()) 42 | 43 | def test_no_duplicates(self): 44 | self.assertEqual(Tag.objects.count(), 3) 45 | self.assertEqual(TaggedItem.objects.count(), 3) 46 | 47 | out = StringIO() 48 | call_command("deduplicate_tags", stdout=out) 49 | 50 | self.assertEqual(Tag.objects.count(), 1) 51 | self.assertEqual(TaggedItem.objects.count(), 2) 52 | 53 | self.assertTrue(Tag.objects.filter(name__iexact="python").exists()) 54 | self.assertEqual( 55 | TaggedItem.objects.filter(tag__name__iexact="python").count(), 2 56 | ) 57 | 58 | self.assertIn("Tag deduplication complete.", out.getvalue()) 59 | 60 | def test_taggit_case_insensitive_not_enabled(self): 61 | settings.TAGGIT_CASE_INSENSITIVE = False 62 | 63 | out = StringIO() 64 | call_command("deduplicate_tags", stdout=out) 65 | 66 | self.assertIn("TAGGIT_CASE_INSENSITIVE is not enabled.", out.getvalue()) 67 | 68 | self.assertEqual(Tag.objects.count(), 3) 69 | self.assertEqual(TaggedItem.objects.count(), 3) 70 | -------------------------------------------------------------------------------- /tests/test_forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.test import TestCase 3 | from django.test.utils import override_settings 4 | 5 | from taggit.forms import TagField 6 | from taggit.models import Tag 7 | 8 | 9 | def _test_parse_tags(tagstring): 10 | if "," in tagstring: 11 | return tagstring.split(",") 12 | else: 13 | raise ValueError() 14 | 15 | 16 | @override_settings(TAGGIT_TAGS_FROM_STRING="tests.test_forms._test_parse_tags") 17 | class TagFieldTests(TestCase): 18 | def test_should_return_error_on_clean_if_not_comma_separated(self): 19 | class TestForm(forms.Form): 20 | tag = TagField() 21 | 22 | excpected_error = "Please provide a comma-separated list of tags." 23 | 24 | form = TestForm({"tag": "not-comma-separated"}) 25 | 26 | self.assertFalse(form.is_valid()) 27 | self.assertIn(excpected_error, form.errors["tag"]) 28 | 29 | def test_should_always_return_False_on_has_change_if_disabled(self): 30 | class TestForm(forms.Form): 31 | tag = TagField(disabled=True) 32 | 33 | form = TestForm(initial={"tag": "foo,bar"}, data={"tag": ["a,b,c"]}) 34 | 35 | self.assertTrue(form.is_valid()) 36 | self.assertFalse(form.has_changed()) 37 | 38 | def test_should_return_True_if_form_has_changed(self): 39 | class TestForm(forms.Form): 40 | tag = TagField() 41 | 42 | form = TestForm(initial={"tag": [Tag(name="a")]}, data={"tag": ["b"]}) 43 | 44 | self.assertTrue(form.has_changed()) 45 | 46 | def test_should_return_False_if_form_has_not_changed(self): 47 | class TestForm(forms.Form): 48 | tag = TagField() 49 | 50 | form = TestForm( 51 | initial={"tag": [Tag(name="foo-bar")]}, data={"tag": ["foo-bar"]} 52 | ) 53 | 54 | self.assertFalse(form.has_changed()) 55 | 56 | def test_should_return_False_if_not_provided(self): 57 | class TestForm(forms.Form): 58 | tag = TagField() 59 | 60 | form = TestForm() 61 | 62 | self.assertFalse(form.has_changed()) 63 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from unittest import skipIf 3 | 4 | from django.test import TestCase, override_settings 5 | 6 | from taggit import models as taggit_models 7 | from tests.models import TestModel 8 | 9 | 10 | @contextmanager 11 | def disable_unidecode(): 12 | """ 13 | Disable unidecode temporarily 14 | """ 15 | old_installed_value = taggit_models.unidecode_installed 16 | taggit_models.unidecode_installed = False 17 | try: 18 | yield 19 | finally: 20 | taggit_models.unidecode_installed = old_installed_value 21 | 22 | 23 | class TestTaggableManager(TestCase): 24 | def test_duplicates(self): 25 | sample_obj = TestModel.objects.create() 26 | sample_obj.tags.set(["green", "green"]) 27 | desired_result = ["green"] 28 | self.assertEqual(desired_result, [tag.name for tag in sample_obj.tags.all()]) 29 | 30 | 31 | class TestSlugification(TestCase): 32 | def test_unicode_slugs(self): 33 | """ 34 | Confirm the preservation of unicode in slugification by default 35 | """ 36 | sample_obj = TestModel.objects.create() 37 | # a unicode tag will be slugified for space reasons but 38 | # unicode-ness will be kept by default 39 | sample_obj.tags.add("あい うえお") 40 | self.assertEqual([tag.slug for tag in sample_obj.tags.all()], ["あい-うえお"]) 41 | 42 | def test_old_slugs_wo_unidecode(self): 43 | """ 44 | Test that the setting that gives us the old slugification behavior 45 | is in place 46 | """ 47 | with ( 48 | disable_unidecode(), 49 | override_settings(TAGGIT_STRIP_UNICODE_WHEN_SLUGIFYING=True), 50 | ): 51 | sample_obj = TestModel.objects.create() 52 | sample_obj.tags.add("aあい うえおb") 53 | # when unidecode is not installed, the unicode ends up being passed directly 54 | # to slugify, and will get "wiped" 55 | self.assertEqual([tag.slug for tag in sample_obj.tags.all()], ["a-b"]) 56 | 57 | @skipIf( 58 | not taggit_models.unidecode_installed, 59 | "This test requires unidecode to be installed", 60 | ) 61 | def test_old_slugs_with_unidecode(self): 62 | """ 63 | Test that the setting that gives us the old slugification behavior 64 | is in place 65 | """ 66 | with override_settings(TAGGIT_STRIP_UNICODE_WHEN_SLUGIFYING=True): 67 | sample_obj = TestModel.objects.create() 68 | # unidecode will transform the tag on top of slugification 69 | sample_obj.tags.add("あい うえお") 70 | self.assertEqual([tag.slug for tag in sample_obj.tags.all()], ["ai-ueo"]) 71 | 72 | 73 | class TestPrefetchCache(TestCase): 74 | def setUp(self) -> None: 75 | sample_obj = TestModel.objects.create() 76 | sample_obj.tags.set(["1", "2", "3"]) 77 | 78 | def test_cache_clears_on_add(self): 79 | """ 80 | Test that the prefetch cache gets cleared on tag addition 81 | """ 82 | sample_obj = TestModel.objects.prefetch_related("tags").get() 83 | self.assertTrue(sample_obj.tags.is_cached(sample_obj)) 84 | 85 | sample_obj.tags.add("4") 86 | self.assertFalse(sample_obj.tags.is_cached(sample_obj)) 87 | 88 | def test_cache_clears_on_remove(self): 89 | """ 90 | Test that the prefetch cache gets cleared on tag removal 91 | """ 92 | sample_obj = TestModel.objects.prefetch_related("tags").get() 93 | self.assertTrue(sample_obj.tags.is_cached(sample_obj)) 94 | 95 | sample_obj.tags.remove("3") 96 | self.assertFalse(sample_obj.tags.is_cached(sample_obj)) 97 | 98 | def test_cache_clears_on_clear(self): 99 | """ 100 | Test that the prefetch cache gets cleared when tags are cleared 101 | """ 102 | sample_obj = TestModel.objects.prefetch_related("tags").get() 103 | self.assertTrue(sample_obj.tags.is_cached(sample_obj)) 104 | 105 | sample_obj.tags.clear() 106 | self.assertFalse(sample_obj.tags.is_cached(sample_obj)) 107 | -------------------------------------------------------------------------------- /tests/test_remove_orphaned_tags.py: -------------------------------------------------------------------------------- 1 | from django.core.management import call_command 2 | from django.test import TestCase 3 | 4 | from taggit.models import Tag 5 | from tests.models import Food, HousePet 6 | 7 | 8 | class RemoveOrphanedTagsTests(TestCase): 9 | def setUp(self): 10 | # Create some tags, some orphaned and some not 11 | self.orphan_tag1 = Tag.objects.create(name="Orphan1") 12 | self.orphan_tag2 = Tag.objects.create(name="Orphan2") 13 | self.used_tag = Tag.objects.create(name="Used") 14 | 15 | # Create instances of Food and HousePet and tag them 16 | self.food_item = Food.objects.create(name="Apple") 17 | self.pet_item = HousePet.objects.create(name="Fido") 18 | 19 | self.food_item.tags.add(self.used_tag) 20 | self.pet_item.tags.add(self.used_tag) 21 | 22 | def test_remove_orphaned_tags(self): 23 | # Ensure the setup is correct 24 | self.assertEqual(Tag.objects.count(), 3) 25 | self.assertEqual(Tag.objects.filter(taggit_taggeditem_items=None).count(), 2) 26 | 27 | # Call the management command to remove orphaned tags 28 | call_command("remove_orphaned_tags") 29 | 30 | # Check the count of tags after running the command 31 | self.assertEqual(Tag.objects.count(), 1) 32 | self.assertEqual(Tag.objects.filter(taggit_taggeditem_items=None).count(), 0) 33 | 34 | # Ensure that the used tag still exists 35 | self.assertTrue(Tag.objects.filter(name="Used").exists()) 36 | self.assertFalse(Tag.objects.filter(name="Orphan1").exists()) 37 | self.assertFalse(Tag.objects.filter(name="Orphan2").exists()) 38 | 39 | def test_no_orphaned_tags(self): 40 | # Ensure the setup is correct 41 | self.assertEqual(Tag.objects.count(), 3) 42 | self.assertEqual(Tag.objects.filter(taggit_taggeditem_items=None).count(), 2) 43 | 44 | # Add used_tag to food_item to make no tags orphaned 45 | self.food_item.tags.add(self.orphan_tag1) 46 | self.food_item.tags.add(self.orphan_tag2) 47 | 48 | # Call the management command to remove orphaned tags 49 | call_command("remove_orphaned_tags") 50 | 51 | # Check the count of tags after running the command 52 | self.assertEqual(Tag.objects.count(), 3) 53 | self.assertEqual(Tag.objects.filter(taggit_taggeditem_items=None).count(), 0) 54 | 55 | # Ensure all tags still exist 56 | self.assertTrue(Tag.objects.filter(name="Used").exists()) 57 | self.assertTrue(Tag.objects.filter(name="Orphan1").exists()) 58 | self.assertTrue(Tag.objects.filter(name="Orphan2").exists()) 59 | -------------------------------------------------------------------------------- /tests/test_serializers.py: -------------------------------------------------------------------------------- 1 | """ 2 | test_django-taggit-serializer 3 | ------------ 4 | 5 | Tests for `django-taggit-serializer` models module. 6 | """ 7 | 8 | from django.test import TestCase 9 | from rest_framework.exceptions import ValidationError 10 | 11 | from taggit import serializers 12 | 13 | from .models import TestModel 14 | from .serializers import TestModelSerializer 15 | 16 | 17 | class TestTaggit_serializer(TestCase): 18 | def test_taggit_serializer_field(self): 19 | correct_value = ["1", "2", "3"] 20 | serializer_field = serializers.TagListSerializerField() 21 | 22 | correct_value = serializer_field.to_internal_value(correct_value) 23 | 24 | assert type(correct_value) is list 25 | 26 | incorrect_value = "123" 27 | 28 | with self.assertRaises(ValidationError): 29 | incorrect_value = serializer_field.to_internal_value(incorrect_value) 30 | 31 | representation = serializer_field.to_representation(correct_value) 32 | self.assertIsInstance(representation, serializers.TagList) 33 | 34 | def test_taggit_serializer_update(self): 35 | """Test if serializer class is working properly on updating object""" 36 | request_data = {"tags": ["1", "2", "3"]} 37 | 38 | test_model = TestModel.objects.create() 39 | 40 | serializer = TestModelSerializer(test_model, data=request_data) 41 | serializer.is_valid() 42 | serializer.save() 43 | 44 | assert len(test_model.tags.all()) == len(request_data.get("tags")) 45 | 46 | def test_taggit_serializer_create(self): 47 | """ 48 | Test if serializer class is working 49 | properly on creating a object 50 | """ 51 | request_data = {"tags": ["1", "2", "3"]} 52 | 53 | serializer = TestModelSerializer(data=request_data) 54 | assert serializer.is_valid() 55 | test_model = serializer.save() 56 | 57 | assert len(test_model.tags.all()) == len(request_data.get("tags")) 58 | 59 | def test_taggit_serializer_create_with_string(self): 60 | """ 61 | Test that we can pass in a string instead of an array for 62 | a tag list without issues 63 | """ 64 | request_data = {"tags": '["1", "2", "3"]'} 65 | 66 | serializer = TestModelSerializer(data=request_data) 67 | assert serializer.is_valid(), serializer.errors 68 | test_model = serializer.save() 69 | 70 | assert {tag.name for tag in test_model.tags.all()} == {"1", "2", "3"} 71 | 72 | def test_taggit_removes_tags(self): 73 | """ 74 | Test if the old assigned tags are removed 75 | """ 76 | test_model = TestModel.objects.create() 77 | test_model.tags.add("1") 78 | 79 | request_data = {"tags": ["2", "3"]} 80 | 81 | serializer = TestModelSerializer(test_model, data=request_data) 82 | serializer.is_valid() 83 | test_model = serializer.save() 84 | 85 | assert TestModel.objects.filter(tags__name__in=["1"]).count() == 0 86 | assert TestModel.objects.filter(tags__name__in=["1", "2"]).count() == 1 87 | 88 | def test_returns_new_data_after_update(self): 89 | """ 90 | Test if the serializer uses fresh data after updating prefetched fields 91 | """ 92 | TestModel.objects.create().tags.add("1") 93 | 94 | test_model = TestModel.objects.prefetch_related("tags").get() 95 | 96 | assert TestModelSerializer(test_model).data["tags"] == ["1"] 97 | 98 | request_data = {"tags": ["2", "3"]} 99 | serializer = TestModelSerializer(test_model, data=request_data) 100 | serializer.is_valid() 101 | test_model = serializer.save() 102 | 103 | assert set(serializer.data["tags"]) == {"2", "3"} 104 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | 4 | from django.test import TestCase 5 | from django.utils import translation 6 | 7 | from taggit.utils import split_strip 8 | 9 | 10 | class SplitStripTests(TestCase): 11 | def test_should_return_empty_list_if_not_string(self): 12 | result = split_strip(None) 13 | 14 | self.assertListEqual(result, []) 15 | 16 | def test_should_return_list_of_non_empty_words(self): 17 | expected_result = ["foo", "bar"] 18 | 19 | result = split_strip("foo|bar||", delimiter="|") 20 | 21 | self.assertListEqual(result, expected_result) 22 | 23 | 24 | class TestLanguages(TestCase): 25 | maxDiff = None 26 | 27 | def get_locale_dir(self): 28 | return os.path.join(os.path.dirname(__file__), "..", "taggit", "locale") 29 | 30 | def test_language_file_integrity(self): 31 | locale_dir = self.get_locale_dir() 32 | for locale in os.listdir(locale_dir): 33 | # attempt translation activation to confirm that the language files are working 34 | with translation.override(locale): 35 | pass 36 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import re_path 3 | 4 | from .views import FoodTagListView 5 | 6 | urlpatterns = [ 7 | re_path( 8 | r"^food/tags/(?P[a-z0-9_-]+)/$", 9 | FoodTagListView.as_view(), 10 | name="food-tag-list", 11 | ), 12 | re_path(r"^admin/", admin.site.urls), 13 | ] 14 | -------------------------------------------------------------------------------- /tests/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic.list import ListView 2 | 3 | from taggit.views import TagListMixin 4 | 5 | from .models import Food 6 | 7 | 8 | class FoodTagListView(TagListMixin, ListView): 9 | model = Food 10 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 1.9 3 | envlist = 4 | black 5 | flake8 6 | isort 7 | py{39,310,311,312}-dj{41,42} 8 | py{310,311,312}-dj{50} 9 | py{310,311,312}-djmain 10 | docs 11 | 12 | [gh-actions] 13 | python = 14 | 3.9: py39, black, flake8, isort 15 | 3.10: py310 16 | 3.11: py311 17 | 3.12: py312 18 | 19 | [testenv] 20 | deps = 21 | dj41: Django>=4.1,<4.2 22 | dj42: Django>=4.2,<5.0 23 | dj50: Django>=5.0,<5.1 24 | djmain: https://github.com/django/django/archive/main.tar.gz 25 | coverage 26 | djangorestframework 27 | unidecode 28 | setenv = 29 | PYTHONWARNINGS=all 30 | commands = 31 | coverage run -m django test --settings=tests.settings {posargs} 32 | coverage report 33 | coverage xml 34 | ignore_outcome = 35 | djmain: True 36 | ignore_errors = 37 | djmain: True 38 | 39 | [testenv:black] 40 | basepython = python3 41 | skip_install = true 42 | deps = black 43 | commands = black --target-version=py38 --check --diff . 44 | 45 | [testenv:flake8] 46 | basepython = python3 47 | skip_install = true 48 | deps = flake8 49 | commands = flake8 50 | 51 | [testenv:isort] 52 | basepython = python3 53 | skip_install = true 54 | deps = isort>=5.0.2 55 | commands = isort --check-only --diff . 56 | 57 | [testenv:docs] 58 | deps = sphinx 59 | commands = sphinx-build -n -W docs docs/_build 60 | --------------------------------------------------------------------------------