├── example
├── __init__.py
├── simpletext
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ ├── 0003_auto_20200306_0928.py
│ │ ├── 0002_auto_20171204_0721.py
│ │ └── 0001_initial.py
│ ├── views.py
│ ├── tests.py
│ ├── admin.py
│ └── models.py
├── static
│ ├── editor
│ │ ├── toggle-expand-dark.png
│ │ ├── toggle-collapse-dark.png
│ │ ├── toggle-collapse-light.png
│ │ ├── toggle-expand-light.png
│ │ └── jquery.treeTable.css
│ └── js
│ │ └── genericcollections.js
├── test_storages.py
├── manage.py
├── templates
│ └── base.html
├── urls.py
├── settings.py
└── settings-testing.py
├── categories
├── api
│ └── __init__.py
├── tests
│ ├── __init__.py
│ ├── test_image.jpg
│ ├── test_manager.py
│ ├── test_utils.py
│ ├── test_mgmt_commands.py
│ ├── test_models.py
│ ├── test_migrations.py
│ ├── test_registration.py
│ ├── test_category_import.py
│ ├── test_admin.py
│ ├── test_views.py
│ └── test_templatetags.py
├── editor
│ ├── __init__.py
│ ├── templatetags
│ │ ├── __init__.py
│ │ └── admin_tree_list_tags.py
│ ├── models.py
│ ├── locale
│ │ └── de
│ │ │ └── LC_MESSAGES
│ │ │ ├── django.mo
│ │ │ └── django.po
│ ├── static
│ │ └── editor
│ │ │ ├── toggle-expand-dark.png
│ │ │ ├── toggle-collapse-dark.png
│ │ │ ├── toggle-collapse-light.png
│ │ │ ├── toggle-expand-light.png
│ │ │ └── jquery.treeTable.css
│ ├── utils.py
│ ├── settings.py
│ └── templates
│ │ └── admin
│ │ └── editor
│ │ ├── grappelli_tree_list_results.html
│ │ ├── tree_list_results.html
│ │ ├── tree_editor.html
│ │ └── grappelli_tree_editor.html
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ ├── add_category_fields.py
│ │ ├── drop_category_field.py
│ │ └── import_categories.py
├── migrations
│ ├── __init__.py
│ ├── 0004_auto_20200517_1832.py
│ ├── 0002_auto_20170217_1111.py
│ ├── 0005_unique_category_slug.py
│ ├── 0003_auto_20200306_1050.py
│ └── 0001_initial.py
├── templatetags
│ └── __init__.py
├── templates
│ ├── categories
│ │ ├── base.html
│ │ ├── breadcrumbs.html
│ │ ├── category_list.html
│ │ ├── ancestors_ul.html
│ │ ├── ul_tree.html
│ │ └── category_detail.html
│ └── admin
│ │ └── edit_inline
│ │ └── gen_coll_tabular.html
├── locale
│ ├── de
│ │ └── LC_MESSAGES
│ │ │ ├── django.mo
│ │ │ └── django.po
│ ├── fr
│ │ └── LC_MESSAGES
│ │ │ ├── django.mo
│ │ │ └── django.po
│ └── it
│ │ └── LC_MESSAGES
│ │ ├── django.mo
│ │ └── django.po
├── __init__.py
├── fixtures
│ ├── test_category_tabs.txt
│ ├── test_category_spaces.txt
│ ├── categories.json
│ └── genres.txt
├── utils.py
├── urls.py
├── fields.py
├── static
│ └── js
│ │ └── genericcollections.js
├── apps.py
├── genericcollection.py
├── settings.py
├── migration.py
├── admin.py
├── models.py
└── views.py
├── requirements
├── prod.txt
├── dev.txt
└── test.txt
├── requirements.txt
├── doc_src
├── changelog.md
├── requirements.txt
├── api
│ └── index.rst
├── user_guide
│ ├── index.md
│ ├── code_examples
│ │ ├── custom_categories5.py
│ │ ├── custom_categories1.py
│ │ ├── custom_categories2.py
│ │ ├── custom_categories6.py
│ │ ├── custom_categories4.py
│ │ ├── custom_categories7.py
│ │ └── custom_categories3.py
│ ├── usage_example_template.html
│ ├── adding_the_fields.rst
│ ├── admin_settings.rst
│ ├── usage.rst
│ ├── custom_categories.rst
│ └── registering_models.rst
├── reference
│ ├── index.rst
│ ├── management_commands.rst
│ ├── models.rst
│ └── settings.rst
├── _templates
│ └── autosummary
│ │ ├── base.rst
│ │ ├── class.rst
│ │ └── module.rst
├── _static
│ └── css
│ │ └── custom.css
├── index.rst
├── installation.rst
├── conf.py
├── getting_started.rst
├── Makefile
└── make.bat
├── .github
├── changelog_templates
│ ├── version_heading.md.jinja
│ └── commit.md.jinja
└── workflows
│ ├── publish-docs.yml
│ ├── publish-package.yml
│ └── run-tests.yaml
├── CONTRIBUTING.md
├── NOTICES.txt
├── .editorconfig
├── .readthedocs.yaml
├── CREDITS.md
├── MANIFEST.in
├── setup.py
├── tox.ini
├── setup.cfg
├── .pre-commit-config.yaml
├── pyproject.toml
├── README.md
├── .gitignore
├── Makefile
└── .changelog-config.yaml
/example/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/categories/api/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/categories/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/categories/editor/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/categories/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/categories/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/simpletext/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/categories/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements/prod.txt:
--------------------------------------------------------------------------------
1 | django-mptt
2 |
--------------------------------------------------------------------------------
/categories/editor/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/categories/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/simpletext/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | -r requirements/prod.txt
2 |
--------------------------------------------------------------------------------
/categories/editor/models.py:
--------------------------------------------------------------------------------
1 | """Placeholder for Django."""
2 |
--------------------------------------------------------------------------------
/doc_src/changelog.md:
--------------------------------------------------------------------------------
1 | ```{include} ../CHANGELOG.md
2 | ```
3 |
--------------------------------------------------------------------------------
/example/simpletext/views.py:
--------------------------------------------------------------------------------
1 | """Create your views here."""
2 |
--------------------------------------------------------------------------------
/doc_src/requirements.txt:
--------------------------------------------------------------------------------
1 | # Add any extensions for Sphinx right here
2 |
--------------------------------------------------------------------------------
/categories/templates/categories/base.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block content %}
3 | {% endblock %}
4 |
--------------------------------------------------------------------------------
/doc_src/api/index.rst:
--------------------------------------------------------------------------------
1 | API
2 | ===
3 |
4 | .. autosummary::
5 | :toctree:
6 | :recursive:
7 |
8 | categories
9 |
--------------------------------------------------------------------------------
/categories/tests/test_image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-categories/master/categories/tests/test_image.jpg
--------------------------------------------------------------------------------
/requirements/dev.txt:
--------------------------------------------------------------------------------
1 | -r test.txt
2 |
3 | bump2version>=1.0.1
4 | generate-changelog
5 | git-fame>=1.12.2
6 | pre-commit
7 |
--------------------------------------------------------------------------------
/categories/locale/de/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-categories/master/categories/locale/de/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/categories/locale/fr/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-categories/master/categories/locale/fr/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/categories/locale/it/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-categories/master/categories/locale/it/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/categories/__init__.py:
--------------------------------------------------------------------------------
1 | """Django categories."""
2 |
3 | __version__ = "2.0.0"
4 |
5 |
6 | default_app_config = "categories.apps.CategoriesConfig"
7 |
--------------------------------------------------------------------------------
/example/static/editor/toggle-expand-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-categories/master/example/static/editor/toggle-expand-dark.png
--------------------------------------------------------------------------------
/example/static/editor/toggle-collapse-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-categories/master/example/static/editor/toggle-collapse-dark.png
--------------------------------------------------------------------------------
/example/static/editor/toggle-collapse-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-categories/master/example/static/editor/toggle-collapse-light.png
--------------------------------------------------------------------------------
/example/static/editor/toggle-expand-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-categories/master/example/static/editor/toggle-expand-light.png
--------------------------------------------------------------------------------
/categories/editor/locale/de/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-categories/master/categories/editor/locale/de/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/categories/editor/static/editor/toggle-expand-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-categories/master/categories/editor/static/editor/toggle-expand-dark.png
--------------------------------------------------------------------------------
/categories/editor/static/editor/toggle-collapse-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-categories/master/categories/editor/static/editor/toggle-collapse-dark.png
--------------------------------------------------------------------------------
/categories/editor/static/editor/toggle-collapse-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-categories/master/categories/editor/static/editor/toggle-collapse-light.png
--------------------------------------------------------------------------------
/categories/editor/static/editor/toggle-expand-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-categories/master/categories/editor/static/editor/toggle-expand-light.png
--------------------------------------------------------------------------------
/categories/fixtures/test_category_tabs.txt:
--------------------------------------------------------------------------------
1 | Category 1
2 | Category 1-1
3 | Category 1-2
4 | Category 2
5 | Category 2-1
6 | Category 2-1-1
7 | Category 3
8 | Category 3-1
9 |
--------------------------------------------------------------------------------
/categories/fixtures/test_category_spaces.txt:
--------------------------------------------------------------------------------
1 | Category 1
2 | Category 1-1
3 | Category 1-2
4 | Category 2
5 | Category 2-1
6 | Category 2-1-1
7 | Category 3
8 | Category 3-1
9 |
--------------------------------------------------------------------------------
/requirements/test.txt:
--------------------------------------------------------------------------------
1 | -r prod.txt
2 | django<4.0.0
3 | ghp-import
4 | linkify-it-py
5 | myst-parser
6 | pydata-sphinx-theme
7 | Sphinx>=4.3.0
8 | sphinx-autodoc-typehints
9 | sphinxcontrib-django2
10 |
--------------------------------------------------------------------------------
/doc_src/user_guide/index.md:
--------------------------------------------------------------------------------
1 | # User Guide
2 |
3 | ```{toctree}
4 | ---
5 | maxdepth: 2
6 | ---
7 | usage
8 | registering_models
9 | adding_the_fields
10 | admin_settings
11 | custom_categories
12 | ```
13 |
--------------------------------------------------------------------------------
/doc_src/reference/index.rst:
--------------------------------------------------------------------------------
1 | =========
2 | Reference
3 | =========
4 |
5 | .. toctree::
6 | :maxdepth: 2
7 | :glob:
8 |
9 | management_commands
10 | models
11 | settings
12 | templatetags
13 |
--------------------------------------------------------------------------------
/categories/templates/categories/breadcrumbs.html:
--------------------------------------------------------------------------------
1 | {% spaceless %}{% for item in category.get_ancestors %}
2 | {{ item.name }}{{ separator }}{% endfor %}{{ category.name }}
3 | {% endspaceless %}
4 |
--------------------------------------------------------------------------------
/doc_src/user_guide/code_examples/custom_categories5.py:
--------------------------------------------------------------------------------
1 | from categories.base import CategoryBase
2 |
3 |
4 | class Meta(CategoryBase.Meta):
5 | verbose_name_plural = "categories"
6 |
7 |
8 | class MPTTMeta:
9 | order_insertion_by = ("order", "name")
10 |
--------------------------------------------------------------------------------
/.github/changelog_templates/version_heading.md.jinja:
--------------------------------------------------------------------------------
1 | ## {{ version.label }} ({{ version.date_time.strftime("%Y-%m-%d") }})
2 | {% if version.previous_tag %}
3 | [Compare the full difference.]({{ repo_url }}/compare/{{ version.previous_tag }}...{{ version.tag }})
4 | {% endif %}
5 |
--------------------------------------------------------------------------------
/categories/templates/categories/category_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'categories/base.html' %}
2 | {% block content %}
3 |
Categories
4 |
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/doc_src/_templates/autosummary/base.rst:
--------------------------------------------------------------------------------
1 | .. rst-class:: h4 text-secondary
2 |
3 | {{ fullname }}
4 |
5 | {{ objname | escape | underline}}
6 |
7 | .. currentmodule:: {{ module }}
8 | {%- if not objname.startswith("test") %}
9 | .. auto{{ objtype }}:: {{ objname }}
10 | {%- endif %}
11 |
--------------------------------------------------------------------------------
/doc_src/user_guide/code_examples/custom_categories1.py:
--------------------------------------------------------------------------------
1 | from categories.models import CategoryBase
2 |
3 |
4 | class SimpleCategory(CategoryBase):
5 | """
6 | A simple of catgorizing example
7 | """
8 |
9 | class Meta:
10 | verbose_name_plural = "simple categories"
11 |
--------------------------------------------------------------------------------
/example/test_storages.py:
--------------------------------------------------------------------------------
1 | # Storages used for testing THUMBNAIL_STORAGE and THUMBNAIL_STORAGE_ALIAS
2 | from django.core.files.storage import FileSystemStorage
3 |
4 |
5 | class MyTestStorage(FileSystemStorage):
6 | pass
7 |
8 |
9 | class MyTestStorageAlias(FileSystemStorage):
10 | pass
11 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | [](https://jazzband.co/)
2 |
3 | This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines).
4 |
--------------------------------------------------------------------------------
/NOTICES.txt:
--------------------------------------------------------------------------------
1 | Django Categories
2 | Copyright 2009 The Washington Times
3 |
4 | This product includes software developed at The Washington Times.
5 |
6 | Portions of this software were developed by matthiask for feincms
7 | (http://github.com/matthiask/feincms/tree/master). Specifically
8 | the Django admin modifications and templates.
9 |
--------------------------------------------------------------------------------
/doc_src/user_guide/code_examples/custom_categories2.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from categories.admin import CategoryBaseAdmin
4 |
5 | from .models import SimpleCategory
6 |
7 |
8 | class SimpleCategoryAdmin(CategoryBaseAdmin):
9 | pass
10 |
11 |
12 | admin.site.register(SimpleCategory, SimpleCategoryAdmin)
13 |
--------------------------------------------------------------------------------
/example/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Entrypoint for custom django functions."""
3 | import os
4 | import sys
5 |
6 | if __name__ == "__main__":
7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
8 |
9 | from django.core.management import execute_from_command_line
10 |
11 | execute_from_command_line(sys.argv)
12 |
--------------------------------------------------------------------------------
/categories/utils.py:
--------------------------------------------------------------------------------
1 | """This module contains utility functions that are used across the project."""
2 |
3 | from django.utils.text import slugify as django_slugify
4 |
5 |
6 | def slugify(text):
7 | """Slugify a string. This function is a wrapper to unify the slugify function across the project."""
8 | return django_slugify(text, allow_unicode=True)
9 |
--------------------------------------------------------------------------------
/.github/changelog_templates/commit.md.jinja:
--------------------------------------------------------------------------------
1 | - {{ commit.summary }} [{{ commit.short_sha }}]({{ repo_url }}/commit/{{ commit.sha }})
2 | {{ commit.body|indent(2, first=True) }}
3 | {% for key, val in commit.metadata["trailers"].items() %}
4 | {% if key not in VALID_AUTHOR_TOKENS %}
5 | **{{ key }}:** {{ val|join(", ") }}
6 |
7 | {% endif %}
8 | {% endfor %}
9 |
--------------------------------------------------------------------------------
/example/templates/base.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 | Django Categories
7 |
8 |
9 | {% block content %}
10 | {% endblock %}
11 |
12 |
13 |
--------------------------------------------------------------------------------
/categories/fixtures/categories.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "fields": {
4 | "level": 0,
5 | "lft": 1,
6 | "name": "Category 1",
7 | "order": 1,
8 | "parent": null,
9 | "rght": 10,
10 | "slug": "category-1",
11 | "tree_id": 1
12 | },
13 | "model": "categories.category",
14 | "pk": 1
15 | }
16 | ]
17 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{py,rst,ini}]
12 | indent_style = space
13 | indent_size = 4
14 |
15 | [*.{html,css,scss,json,yml}]
16 | indent_style = space
17 | indent_size = 2
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
22 | [Makefile]
23 | indent_style = tab
24 |
--------------------------------------------------------------------------------
/categories/editor/utils.py:
--------------------------------------------------------------------------------
1 | """
2 | Provides compatibility with Django 1.8.
3 | """
4 |
5 | from django.contrib.admin.utils import display_for_field as _display_for_field
6 |
7 |
8 | def display_for_field(value, field, empty_value_display=None):
9 | """Compatility for displaying a field in Django 1.8."""
10 | try:
11 | return _display_for_field(value, field, empty_value_display)
12 | except TypeError:
13 | return _display_for_field(value, field)
14 |
--------------------------------------------------------------------------------
/categories/editor/settings.py:
--------------------------------------------------------------------------------
1 | """Settings management for the editor."""
2 |
3 | from django.conf import settings
4 |
5 | STATIC_URL = getattr(settings, "STATIC_URL", settings.MEDIA_URL)
6 | if STATIC_URL is None:
7 | STATIC_URL = settings.MEDIA_URL
8 | MEDIA_PATH = getattr(settings, "EDITOR_MEDIA_PATH", "%seditor/" % STATIC_URL)
9 |
10 | TREE_INITIAL_STATE = getattr(settings, "EDITOR_TREE_INITIAL_STATE", "collapsed")
11 |
12 | IS_GRAPPELLI_INSTALLED = "grappelli" in settings.INSTALLED_APPS
13 |
--------------------------------------------------------------------------------
/categories/urls.py:
--------------------------------------------------------------------------------
1 | """URL patterns for the categories app."""
2 |
3 | from django.urls import path, re_path
4 | from django.views.generic import ListView
5 |
6 | from . import views
7 | from .models import Category
8 |
9 | categorytree_dict = {"queryset": Category.objects.filter(level=0)}
10 |
11 | urlpatterns = (path("", ListView.as_view(**categorytree_dict), name="categories_tree_list"),)
12 |
13 | urlpatterns += (re_path(r"^(?P.+)/$", views.category_detail, name="categories_category"),)
14 |
--------------------------------------------------------------------------------
/categories/templates/categories/ancestors_ul.html:
--------------------------------------------------------------------------------
1 | {% load category_tags %}
2 | - Top
3 | {% for node,structure in path|tree_info %}
4 | {% if structure.new_level %}
-
5 | {% else %}
-
6 | {% endif %}
7 | {% if node == category %}{{ node.name }}
8 | {% else %}{{ node.name }}
9 | {% endif %}
10 | {% for level in structure.closed_levels %}
{% endfor %}
11 | {% endfor %}
12 |
--------------------------------------------------------------------------------
/doc_src/user_guide/code_examples/custom_categories6.py:
--------------------------------------------------------------------------------
1 | from categories.base import CategoryBaseAdminForm
2 | from categories.models import Category
3 |
4 |
5 | class CategoryAdminForm(CategoryBaseAdminForm):
6 | class Meta:
7 | model = Category
8 |
9 | def clean_alternate_title(self):
10 | if self.instance is None or not self.cleaned_data["alternate_title"]:
11 | return self.cleaned_data["name"]
12 | else:
13 | return self.cleaned_data["alternate_title"]
14 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # Read the Docs configuration file for Sphinx projects
2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3 |
4 | # Required
5 | version: 2
6 |
7 | # Set the OS, Python version and other tools you might need
8 | build:
9 | os: ubuntu-22.04
10 | tools:
11 | python: "3.12"
12 |
13 | # Build documentation in the "doc_src/" directory with Sphinx
14 | sphinx:
15 | configuration: doc_src/conf.py
16 |
17 | python:
18 | install:
19 | - requirements: requirements/test.txt
20 |
--------------------------------------------------------------------------------
/categories/templates/categories/ul_tree.html:
--------------------------------------------------------------------------------
1 | {% load category_tags %}{% spaceless %}
2 | - Top
3 | {% for node,structure in path|tree_info %}
4 | {% if structure.new_level %}
-
5 | {% else %}
-
6 | {% endif %}
7 | {% if node == category %}{{ node.name }}
8 | {% else %}{{ node.name }}
9 | {% endif %}
10 | {% for level in structure.closed_levels %}
{% endfor %}
11 | {% endfor %}
{% endspaceless %}
12 |
--------------------------------------------------------------------------------
/example/simpletext/tests.py:
--------------------------------------------------------------------------------
1 | """
2 | This file demonstrates two different styles of tests (one doctest and one
3 | unittest). These will both pass when you run "manage.py test".
4 |
5 | Replace these with more appropriate tests for your application.
6 | """
7 |
8 | from django.test import TestCase
9 |
10 |
11 | class SimpleTest(TestCase):
12 | def test_basic_addition(self):
13 | """
14 | Tests that 1 + 1 always equals 2.
15 | """
16 | self.assertEqual(1 + 1, 2)
17 |
18 |
19 | __test__ = {
20 | "doctest": """
21 | Another way to test that 1 + 1 is equal to 2.
22 |
23 | >>> 1 + 1 == 2
24 | True
25 | """
26 | }
27 |
--------------------------------------------------------------------------------
/CREDITS.md:
--------------------------------------------------------------------------------
1 | Contributors
2 | ============
3 |
4 | Corey Oordt github.com/coordt
5 | Erik Simmler github.com/tgecho
6 | Martin Ogden githun.com/martinogden
7 | Ramiro Morales github.com/ramiro
8 | Evan Culver github.com/eculver
9 | Andrzej Herok github.com/aherok
10 | Jonathan Hensley github.com/jhensley
11 | Justin Quick github.com/justquick
12 | Josh Ourisman github.com/joshourisman
13 | Jose Soares github.com/jsoa
14 | David Charbonnier github.com/oxys
15 | Brad Jasper github.com/bradjasper
16 | Martin Matusiak github.com/numerodix
17 | Iacopo Spalletti github.com/yakky
18 | Brent O'Connor github.com/epicserve
19 |
--------------------------------------------------------------------------------
/doc_src/user_guide/code_examples/custom_categories4.py:
--------------------------------------------------------------------------------
1 | from categories.models import Category
2 |
3 |
4 | def save(self, *args, **kwargs):
5 | if self.thumbnail:
6 | import django
7 | from django.core.files.images import get_image_dimensions
8 |
9 | if django.VERSION[1] < 2:
10 | width, height = get_image_dimensions(self.thumbnail.file)
11 | else:
12 | width, height = get_image_dimensions(self.thumbnail.file, close=True)
13 | else:
14 | width, height = None, None
15 |
16 | self.thumbnail_width = width
17 | self.thumbnail_height = height
18 |
19 | super(Category, self).save(*args, **kwargs)
20 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include requirements.txt
2 | include NOTICES.txt
3 | include README.txt
4 | include CREDITS.txt
5 | include LICENSE.txt
6 |
7 | recursive-include categories *.html *.txt *.json *.html
8 | recursive-include categories/static *.html *.gif *.png *.css *.txt *.js
9 | recursive-include categories/editor *.html *.gif *.png *.css *.js
10 |
11 | recursive-include doc_src *.rst *.txt *.png *.css *.html *.js
12 | recursive-exclude doc_src/_build *.*
13 | include doc_src/Makefile
14 | include doc_src/make.bat
15 |
16 | recursive-include categories/locale *.mo *.po
17 | recursive-include categories/editor/locale *.mo *.po
18 | recursive-include requirements *.txt
19 | prune example
20 |
--------------------------------------------------------------------------------
/doc_src/_static/css/custom.css:
--------------------------------------------------------------------------------
1 | .sig-prename.descclassname {
2 | display: none;
3 | }
4 | dl.attribute {
5 | margin-bottom: 30px;
6 | }
7 | dl.field-list {
8 | display: block;
9 | }
10 | dl.field-list > dt {
11 | padding-left: 0;
12 | }
13 | dl.field-list > dd {
14 | padding-left: 1em;
15 | border-left: 0;
16 | }
17 | dl.field-list > dd > ul.simple {
18 | list-style-type: none;
19 | padding-left: 0;
20 | }
21 | dl.field-list > dd + dt {
22 | margin-top: 0.5em;
23 | }
24 | dd {
25 | margin-left: 0;
26 | padding-left: 30px;
27 | border-left: 1px solid #c9c9c9;
28 | }
29 | .table.autosummary td {
30 | border-top: 0;
31 | border-bottom: 1px solid #dee2e6;
32 | }
33 |
--------------------------------------------------------------------------------
/categories/fields.py:
--------------------------------------------------------------------------------
1 | """Custom category fields for other models."""
2 |
3 | from django.db.models import ForeignKey, ManyToManyField
4 |
5 |
6 | class CategoryM2MField(ManyToManyField):
7 | """A many to many field to a Category model."""
8 |
9 | def __init__(self, **kwargs):
10 | if "to" in kwargs:
11 | kwargs.pop("to")
12 | super(CategoryM2MField, self).__init__(to="categories.Category", **kwargs)
13 |
14 |
15 | class CategoryFKField(ForeignKey):
16 | """A foreign key to the Category model."""
17 |
18 | def __init__(self, **kwargs):
19 | if "to" in kwargs:
20 | kwargs.pop("to")
21 | super(CategoryFKField, self).__init__(to="categories.Category", **kwargs)
22 |
--------------------------------------------------------------------------------
/categories/migrations/0004_auto_20200517_1832.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.6 on 2020-05-17 18:32
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 | ("categories", "0003_auto_20200306_1050"),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name="categoryrelation",
16 | name="content_type",
17 | field=models.ForeignKey(
18 | on_delete=django.db.models.deletion.CASCADE, to="contenttypes.ContentType", verbose_name="content type"
19 | ),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/categories/tests/test_manager.py:
--------------------------------------------------------------------------------
1 | # test active returns only active items
2 | from django.test import TestCase
3 |
4 | from categories.models import Category
5 |
6 |
7 | class CategoryManagerTest(TestCase):
8 | fixtures = ["categories.json"]
9 |
10 | def setUp(self):
11 | pass
12 |
13 | def testActive(self):
14 | """
15 | Should raise an exception.
16 | """
17 | all_count = Category.objects.all().count()
18 | self.assertEqual(Category.objects.active().count(), all_count)
19 |
20 | cat1 = Category.objects.get(name="Category 1")
21 | cat1.active = False
22 | cat1.save()
23 |
24 | active_count = all_count - cat1.get_descendants(True).count()
25 | self.assertEqual(Category.objects.active().count(), active_count)
26 |
--------------------------------------------------------------------------------
/categories/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | from ..utils import slugify
4 |
5 |
6 | class TestSlugify(TestCase):
7 | def test_slugify(self):
8 | string_dict = {
9 | "naïve café": "naïve-café",
10 | "spaced out": "spaced-out",
11 | "user@domain.com": "userdomaincom",
12 | "100% natural": "100-natural",
13 | "über-cool": "über-cool",
14 | "façade élégant": "façade-élégant",
15 | "北京大学": "北京大学",
16 | "Толстой": "толстой",
17 | "ñoño": "ñoño",
18 | "سلام": "سلام",
19 | "Αθήνα": "αθήνα",
20 | "こんにちは": "こんにちは",
21 | "˚č$'\\*>%ˇ'!/": "čˇ",
22 | }
23 | for key, value in string_dict.items():
24 | self.assertEqual(slugify(key), value)
25 |
--------------------------------------------------------------------------------
/doc_src/_templates/autosummary/class.rst:
--------------------------------------------------------------------------------
1 | .. rst-class:: h4 text-secondary
2 |
3 | {{ fullname }}
4 |
5 | {{ objname | escape | underline}}
6 |
7 | .. currentmodule:: {{ module }}
8 |
9 | .. autoclass:: {{ objname }}
10 |
11 | {% block methods %}
12 | {% if methods %}
13 | .. rubric:: {{ _('Methods') }}
14 |
15 | .. autosummary::
16 | {% for item in methods %}
17 | {%- if "__" not in item %}
18 | ~{{ name }}.{{ item }}
19 | {% endif -%}
20 | {%- endfor %}
21 | {% endif %}
22 | {% endblock %}
23 |
24 | {% block attributes %}
25 | {% if attributes %}
26 | .. rubric:: {{ _('Attributes') }}
27 |
28 | .. autosummary::
29 | {% for item in attributes %}
30 | {%- if "__" not in item %}
31 | ~{{ name }}.{{ item }}{% endif %}
32 | {% endfor -%}
33 | {% endif %}
34 | {% endblock %}
35 |
--------------------------------------------------------------------------------
/doc_src/user_guide/code_examples/custom_categories7.py:
--------------------------------------------------------------------------------
1 | from categories.admin import CategoryAdminForm
2 | from categories.base import CategoryBaseAdmin
3 |
4 |
5 | class CategoryAdmin(CategoryBaseAdmin):
6 | form = CategoryAdminForm
7 | list_display = ("name", "alternate_title", "active")
8 | fieldsets = (
9 | (None, {"fields": ("parent", "name", "thumbnail", "active")}),
10 | (
11 | "Meta Data",
12 | {
13 | "fields": ("alternate_title", "alternate_url", "description", "meta_keywords", "meta_extra"),
14 | "classes": ("collapse",),
15 | },
16 | ),
17 | (
18 | "Advanced",
19 | {
20 | "fields": ("order", "slug"),
21 | "classes": ("collapse",),
22 | },
23 | ),
24 | )
25 |
--------------------------------------------------------------------------------
/doc_src/index.rst:
--------------------------------------------------------------------------------
1 | =============================
2 | Django Categories v |version|
3 | =============================
4 |
5 | Django Categories grew out of our need to provide a basic hierarchical taxonomy management system that multiple applications could use independently or in concert.
6 |
7 | As a news site, our stories, photos, and other content get divided into "sections" and we wanted all the apps to use the same set of sections. As our needs grew, the Django Categories grew in the functionality it gave to category handling within web pages.
8 |
9 |
10 | Contents
11 | ========
12 |
13 | .. toctree::
14 | :maxdepth: 2
15 | :glob:
16 |
17 | installation
18 | getting_started
19 | user_guide/index
20 | reference/index
21 | api/index
22 | changelog
23 |
24 | Indices and tables
25 | ==================
26 |
27 | * :ref:`genindex`
28 | * :ref:`modindex`
29 | * :ref:`search`
30 |
--------------------------------------------------------------------------------
/example/simpletext/migrations/0003_auto_20200306_0928.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-03-06 09:28
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("simpletext", "0002_auto_20171204_0721"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="simplecategory",
14 | name="level",
15 | field=models.PositiveIntegerField(editable=False),
16 | ),
17 | migrations.AlterField(
18 | model_name="simplecategory",
19 | name="lft",
20 | field=models.PositiveIntegerField(editable=False),
21 | ),
22 | migrations.AlterField(
23 | model_name="simplecategory",
24 | name="rght",
25 | field=models.PositiveIntegerField(editable=False),
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/example/static/js/genericcollections.js:
--------------------------------------------------------------------------------
1 | function showGenericRelatedObjectLookupPopup(triggeringLink, ctArray) {
2 | var realName = triggeringLink.id.replace(/^lookup_/, '');
3 | var name = id_to_windowname(realName);
4 | realName = realName.replace(/object_id/, 'content_type');
5 | var select = document.getElementById(realName);
6 | if (select.selectedIndex === 0) {
7 | alert("Select a content type first.");
8 | return false;
9 | }
10 | var selectedItem = select.item(select.selectedIndex).value;
11 | var href = triggeringLink.href.replace(/#/,'../../../'+ctArray[selectedItem]+"/?t=id");
12 | if (href.search(/\?/) >= 0) {
13 | href = href + '&pop=1';
14 | } else {
15 | href = href + '?pop=1';
16 | }
17 | var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
18 | win.focus();
19 | return false;
20 | }
21 |
--------------------------------------------------------------------------------
/categories/static/js/genericcollections.js:
--------------------------------------------------------------------------------
1 | function showGenericRelatedObjectLookupPopup(triggeringLink, ctArray) {
2 | var realName = triggeringLink.id.replace(/^lookup_/, '');
3 | var name = id_to_windowname(realName);
4 | realName = realName.replace(/object_id/, 'content_type');
5 | var select = document.getElementById(realName);
6 | if (select.selectedIndex === 0) {
7 | alert("Select a content type first.");
8 | return false;
9 | }
10 | var selectedItem = select.item(select.selectedIndex).value;
11 | var href = triggeringLink.href.replace(/#/,'../../../'+ctArray[selectedItem]+"/?t=id");
12 | if (href.search(/\?/) >= 0) {
13 | href = href + '&pop=1';
14 | } else {
15 | href = href + '?pop=1';
16 | }
17 | var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
18 | win.focus();
19 | return false;
20 | }
21 |
--------------------------------------------------------------------------------
/doc_src/user_guide/usage_example_template.html:
--------------------------------------------------------------------------------
1 | {% extends 'categories/base.html' %}
2 | {% block content %}
3 | {{ category }}
4 | {% if category.parent %}
5 |
9 | {% endif %}
10 | {% if category.description %}{{ category.description }}
{% endif %}
11 | {% if category.children.count %}
12 | Subcategories
13 |
14 | {% for child in category.children.all %}
15 | - {{ child }}
16 | {% endfor %}
17 |
18 | {% endif %}
19 | Entries
20 | {% if category.entries_set.all %}
21 | {% for entry in category.entries_set.all %}
22 | {{ entry.headline }}
23 | {% endfor %}
24 | {% else %}
25 | No entries for {{ category }}
26 | {% endif %}
27 |
28 | {% endblock %}
29 |
--------------------------------------------------------------------------------
/categories/migrations/0002_auto_20170217_1111.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 1.10.5 on 2017-02-17 11:11
2 | from __future__ import unicode_literals
3 |
4 | import django.db.models.manager
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | ("categories", "0001_initial"),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelManagers(
15 | name="category",
16 | managers=[
17 | ("tree", django.db.models.manager.Manager()),
18 | ],
19 | ),
20 | migrations.AlterField(
21 | model_name="categoryrelation",
22 | name="relation_type",
23 | field=models.CharField(
24 | blank=True,
25 | help_text="A generic text field to tag a relation, like 'leadphoto'.",
26 | max_length=200,
27 | null=True,
28 | verbose_name="relation type",
29 | ),
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/categories/templates/categories/category_detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'categories/base.html' %}
2 | {% load category_tags %}
3 | {% block content %}
4 | breadcrumbs category result:
5 | {% breadcrumbs category %}
6 | breadcrumbs category ">" result:
7 | {% breadcrumbs category ">" %}
8 | display_path_as_ul category result:
9 | {% display_path_as_ul category %}
10 | display_drilldown_as_ul category result:
11 | {% display_drilldown_as_ul category %}
12 | category.get_ancestors|category_path filter result: {{ category.get_ancestors|category_path }}
13 | {{ category }}
14 | {% if category.parent %}{% endif %}
15 | {% if category.description %}{{ category.description }}
{% endif %}
16 | {% if category.children.count %}Subcategories
{% for child in category.children.all %}- {{ child }}
{% endfor %}
{% endif %}
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/example/simpletext/admin.py:
--------------------------------------------------------------------------------
1 | """Admin interface for simple text."""
2 |
3 | from django.contrib import admin
4 |
5 | from categories.admin import CategoryBaseAdmin, CategoryBaseAdminForm
6 |
7 | from .models import SimpleCategory, SimpleText
8 |
9 |
10 | class SimpleTextAdmin(admin.ModelAdmin):
11 | """Admin for simple text model."""
12 |
13 | fieldsets = (
14 | (
15 | None,
16 | {
17 | "fields": (
18 | "name",
19 | "description",
20 | )
21 | },
22 | ),
23 | )
24 |
25 |
26 | class SimpleCategoryAdminForm(CategoryBaseAdminForm):
27 | """Admin form for simple category."""
28 |
29 | class Meta:
30 | model = SimpleCategory
31 | fields = "__all__"
32 |
33 |
34 | class SimpleCategoryAdmin(CategoryBaseAdmin):
35 | """Admin for simple category."""
36 |
37 | form = SimpleCategoryAdminForm
38 |
39 |
40 | admin.site.register(SimpleText, SimpleTextAdmin)
41 | admin.site.register(SimpleCategory, SimpleCategoryAdmin)
42 |
--------------------------------------------------------------------------------
/.github/workflows/publish-docs.yml:
--------------------------------------------------------------------------------
1 | name: Build and publish documentation
2 |
3 | on:
4 | push:
5 | tags: [ "*" ]
6 |
7 | permissions:
8 | contents: write
9 | pull-requests: write
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | matrix:
16 | python-version: ["3.10"]
17 | fail-fast: false
18 |
19 | steps:
20 | - uses: actions/checkout@v2
21 |
22 | - name: Set up Python ${{ matrix.python-version }}
23 | uses: actions/setup-python@v2
24 | with:
25 | python-version: ${{ matrix.python-version }}
26 |
27 | - name: Install dependencies
28 | run: |
29 | python -m pip install --upgrade pip
30 | pip install -r requirements/dev.txt
31 |
32 | - name: Build documentation
33 | run: |
34 | make docs
35 |
36 | - name: github pages deploy
37 | uses: peaceiris/actions-gh-pages@v3.8.0
38 | with:
39 | github_token: ${{ secrets.GITHUB_TOKEN }}
40 | publish_branch: gh-pages
41 | publish_dir: docs
42 | force_orphan: true
43 |
--------------------------------------------------------------------------------
/categories/management/commands/add_category_fields.py:
--------------------------------------------------------------------------------
1 | """The add_category_fields command."""
2 |
3 | from django.core.management.base import BaseCommand
4 |
5 |
6 | class Command(BaseCommand):
7 | """
8 | Alter one or more models' tables with the registered attributes.
9 | """
10 |
11 | help = "Alter the tables for all registered models, or just specified models"
12 | args = "[appname ...]"
13 | can_import_settings = True
14 | requires_system_checks = []
15 |
16 | def add_arguments(self, parser):
17 | """Add app_names argument to the command."""
18 | parser.add_argument("app_names", nargs="*")
19 |
20 | def handle(self, *args, **options):
21 | """
22 | Alter the tables.
23 | """
24 | from categories.migration import migrate_app
25 | from categories.settings import MODEL_REGISTRY
26 |
27 | if options["app_names"]:
28 | for app in options["app_names"]:
29 | migrate_app(None, app)
30 | else:
31 | for app in MODEL_REGISTRY:
32 | migrate_app(None, app)
33 |
--------------------------------------------------------------------------------
/categories/tests/test_mgmt_commands.py:
--------------------------------------------------------------------------------
1 | from django.core import management
2 | from django.core.management.base import CommandError
3 | from django.db import connection
4 | from django.test import TestCase
5 |
6 |
7 | class TestMgmtCommands(TestCase):
8 | @classmethod
9 | def setUpClass(cls):
10 | connection.disable_constraint_checking()
11 | super(TestMgmtCommands, cls).setUpClass()
12 |
13 | @classmethod
14 | def tearDownClass(cls):
15 | super(TestMgmtCommands, cls).tearDownClass()
16 | connection.enable_constraint_checking()
17 |
18 | def test_add_category_fields(self):
19 | management.call_command("add_category_fields", verbosity=0)
20 |
21 | def test_add_category_fields_app(self):
22 | management.call_command("add_category_fields", "flatpages", verbosity=0)
23 |
24 | def test_drop_category_field(self):
25 | management.call_command("drop_category_field", "flatpages", "flatpage", "category", verbosity=0)
26 |
27 | def test_drop_category_field_error(self):
28 | self.assertRaises(CommandError, management.call_command, "drop_category_field", verbosity=0)
29 |
--------------------------------------------------------------------------------
/categories/tests/test_models.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import django
4 | from django.core.files import File
5 | from django.core.files.uploadedfile import UploadedFile
6 | from django.test import TestCase
7 |
8 | from categories.models import Category
9 | from example.test_storages import MyTestStorage, MyTestStorageAlias
10 |
11 |
12 | class TestCategoryThumbnail(TestCase):
13 | def test_thumbnail(self):
14 | file_name = "test_image.jpg"
15 | with open(os.path.join(os.path.dirname(__file__), file_name), "rb") as f:
16 | test_image = UploadedFile(File(f), content_type="image/jpeg")
17 | category = Category.objects.create(name="Test Category", slug="test-category", thumbnail=test_image)
18 |
19 | self.assertEqual(category.pk, 1)
20 | self.assertEqual(category.thumbnail_width, 640)
21 | self.assertEqual(category.thumbnail_height, 480)
22 |
23 | if django.VERSION >= (4, 2):
24 | self.assertEqual(category.thumbnail.storage.__class__, MyTestStorageAlias)
25 | else:
26 | self.assertEqual(category.thumbnail.storage.__class__, MyTestStorage)
27 |
--------------------------------------------------------------------------------
/doc_src/reference/management_commands.rst:
--------------------------------------------------------------------------------
1 | .. _management-commands:
2 |
3 | ===================
4 | Management Commands
5 | ===================
6 |
7 | .. _import_categories:
8 |
9 | import_categories
10 | =================
11 |
12 | **Usage:** ``./manage.py import_categories /path/to/file.txt [/path/to/file2.txt]``
13 |
14 | Imports category tree(s) from a file. Sub categories must be indented by the same multiple of spaces or tabs. The first line in the file cannot start with a space or tab and you can't mix spaces and tabs.
15 |
16 |
17 | .. _add_category_fields:
18 |
19 | add_category_fields
20 | ===================
21 |
22 | **Usage:** ``./manage.py add_category_fields [app1 app2 ...]``
23 |
24 | Add missing registered category fields to the database table of a specified application or all registered applications.
25 |
26 | Requires Django South.
27 |
28 |
29 | .. _drop_category_field:
30 |
31 | drop_category_field
32 | ===================
33 |
34 | **Usage:** ``./manage.py drop_category_field app_name model_name field_name``
35 |
36 | Drop the ``field_name`` field from the ``app_name_model_name`` table, if the field is currently registered in ``CATEGORIES_SETTINGS``\ .
37 |
38 | Requires Django South.
39 |
--------------------------------------------------------------------------------
/categories/management/commands/drop_category_field.py:
--------------------------------------------------------------------------------
1 | """Alter one or more models' tables with the registered attributes."""
2 |
3 | from django.core.management.base import BaseCommand, CommandError
4 |
5 |
6 | class Command(BaseCommand):
7 | """
8 | Alter one or more models' tables with the registered attributes.
9 | """
10 |
11 | help = "Drop the given field from the given model's table"
12 | args = "appname modelname fieldname"
13 | can_import_settings = True
14 | requires_system_checks = []
15 |
16 | def add_arguments(self, parser):
17 | """Add app_name, model_name, and field_name arguments to the command."""
18 | parser.add_argument("app_name")
19 | parser.add_argument("model_name")
20 | parser.add_argument("field_name")
21 |
22 | def handle(self, *args, **options):
23 | """
24 | Alter the tables.
25 | """
26 | from categories.migration import drop_field
27 |
28 | if "app_name" not in options or "model_name" not in options or "field_name" not in options:
29 | raise CommandError("You must specify an Application name, a Model name and a Field name")
30 |
31 | drop_field(options["app_name"], options["model_name"], options["field_name"])
32 |
--------------------------------------------------------------------------------
/doc_src/installation.rst:
--------------------------------------------------------------------------------
1 | ============
2 | Installation
3 | ============
4 |
5 | To use the Category model
6 | =========================
7 |
8 | 1. Install django-categories::
9 |
10 | pip install django-categories
11 |
12 | 2. Add ``"categories"`` and ``"categories.editor"`` to your ``INSTALLED_APPS`` list in your project's ``settings.py`` file.
13 |
14 | .. code-block:: python
15 |
16 | INSTALLED_APPS = [
17 | # ...
18 | "categories",
19 | "categories.editor",
20 | ]
21 |
22 | 3. Run ``./manage.py syncdb`` (or ``./manage.py migrate categories`` if you are using `South `_)
23 |
24 |
25 | To only subclass CategoryBase
26 | =============================
27 |
28 | If you are going to create your own models using :py:class:`CategoryBase`, (see :ref:`creating_custom_categories`) you'll need a few different steps.
29 |
30 | 1. Install django-categories::
31 |
32 | pip install django-categories
33 |
34 | 2. Add ``"categories.editor"`` to your ``INSTALLED_APPS`` list in your project's ``settings.py`` file.
35 |
36 | .. code-block:: python
37 |
38 | INSTALLED_APPS = [
39 | # ...
40 | "categories.editor",
41 | ]
42 |
43 | 3. Create your own models.
44 |
--------------------------------------------------------------------------------
/example/urls.py:
--------------------------------------------------------------------------------
1 | """URL patterns for the example project."""
2 |
3 | import os
4 |
5 | from django.conf.urls import include
6 | from django.contrib import admin
7 | from django.urls import path, re_path
8 | from django.views.static import serve
9 |
10 | admin.autodiscover()
11 |
12 |
13 | ROOT_PATH = os.path.dirname(os.path.dirname(__file__))
14 |
15 | urlpatterns = (
16 | # Example:
17 | # (r'^sample/', include('sample.foo.urls')),
18 | # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
19 | # to INSTALLED_APPS to enable admin documentation:
20 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')),
21 | # Uncomment the next line to enable the admin:
22 | path("admin/", admin.site.urls),
23 | path("categories/", include("categories.urls")),
24 | # r'^cats/', include('categories.urls')),
25 | re_path(
26 | r"^static/categories/(?P.*)$", serve, {"document_root": ROOT_PATH + "/categories/media/categories/"}
27 | ),
28 | # (r'^static/editor/(?P.*)$', 'django.views.static.serve',
29 | # {'document_root': ROOT_PATH + '/editor/media/editor/',
30 | # 'show_indexes':True}),
31 | re_path(r"^static/(?P.*)$", serve, {"document_root": os.path.join(ROOT_PATH, "example", "static")}),
32 | )
33 |
--------------------------------------------------------------------------------
/categories/editor/locale/de/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # German translation of django-categories
2 | # Copyright (C) 2012 winniehell
3 | # This file is distributed under the same license as django-categories.
4 | # winniehell , 2012.
5 | msgid ""
6 | msgstr ""
7 | "Project-Id-Version: django-categories 1.1.4\n"
8 | "Report-Msgid-Bugs-To: \n"
9 | "POT-Creation-Date: 2012-09-02 12:39+0200\n"
10 | "PO-Revision-Date: 2012-09-01 02:38+0200\n"
11 | "Last-Translator: winniehell \n"
12 | "Language-Team: winniehell \n"
13 | "Language: \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 | #: tree_editor.py:167
20 | msgid "Database error"
21 | msgstr ""
22 |
23 | #: tree_editor.py:206
24 | #, python-format
25 | msgid "%(count)s %(name)s was changed successfully."
26 | msgid_plural "%(count)s %(name)s were changed successfully."
27 | msgstr[0] ""
28 | msgstr[1] ""
29 |
30 | #: tree_editor.py:247
31 | #, python-format
32 | msgid "%(total_count)s selected"
33 | msgid_plural "All %(total_count)s selected"
34 | msgstr[0] ""
35 | msgstr[1] ""
36 |
37 | #: tree_editor.py:252
38 | #, python-format
39 | msgid "0 of %(cnt)s selected"
40 | msgstr ""
41 |
--------------------------------------------------------------------------------
/categories/editor/templates/admin/editor/grappelli_tree_list_results.html:
--------------------------------------------------------------------------------
1 | {% if result_hidden_fields %}
2 | {# DIV for HTML validation #}
3 | {% for item in result_hidden_fields %}{{ item }}{% endfor %}
4 |
5 | {% endif %}
6 |
7 | {% if results %}
8 |
28 | {% endif %}
29 |
--------------------------------------------------------------------------------
/doc_src/user_guide/code_examples/custom_categories3.py:
--------------------------------------------------------------------------------
1 | from categories import models, settings
2 | from categories.base import CategoryBase
3 |
4 |
5 | class Category(CategoryBase):
6 | thumbnail = models.FileField(
7 | upload_to=settings.THUMBNAIL_UPLOAD_PATH,
8 | null=True,
9 | blank=True,
10 | storage=settings.THUMBNAIL_STORAGE,
11 | )
12 | thumbnail_width = models.IntegerField(blank=True, null=True)
13 | thumbnail_height = models.IntegerField(blank=True, null=True)
14 | order = models.IntegerField(default=0)
15 | alternate_title = models.CharField(
16 | blank=True, default="", max_length=100, help_text="An alternative title to use on pages with this category."
17 | )
18 | alternate_url = models.CharField(
19 | blank=True,
20 | max_length=200,
21 | help_text="An alternative URL to use instead of the one derived from " "the category hierarchy.",
22 | )
23 | description = models.TextField(blank=True, null=True)
24 | meta_keywords = models.CharField(
25 | blank=True, default="", max_length=255, help_text="Comma-separated keywords for search engines."
26 | )
27 | meta_extra = models.TextField(
28 | blank=True, default="", help_text="(Advanced) Any additional HTML to be placed verbatim " "in the <head>"
29 | )
30 |
--------------------------------------------------------------------------------
/categories/migrations/0005_unique_category_slug.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.9 on 2018-10-05 13:59
2 |
3 | from django.db import migrations, models
4 |
5 | from categories.models import Category
6 |
7 |
8 | def make_slugs_unique(apps, schema_editor):
9 | duplicates = Category.tree.values("slug").annotate(slug_count=models.Count("slug")).filter(slug_count__gt=1)
10 | category_objs = []
11 | for duplicate in duplicates:
12 | slug = duplicate["slug"]
13 | categories = Category.tree.filter(slug=slug)
14 | count = categories.count()
15 | i = 0
16 | for category in categories.all():
17 | if i != 0:
18 | category.slug = "{}-{}".format(slug, str(i).zfill(len(str(count))))
19 | category_objs.append(category)
20 | i += 1
21 | Category.objects.bulk_update(category_objs, ["slug"])
22 |
23 |
24 | class Migration(migrations.Migration):
25 | dependencies = [
26 | ("categories", "0004_auto_20200517_1832"),
27 | ]
28 |
29 | operations = [
30 | migrations.RunPython(make_slugs_unique, reverse_code=migrations.RunPython.noop),
31 | migrations.AlterField(
32 | model_name="category",
33 | name="slug",
34 | field=models.SlugField(unique=True, verbose_name="slug"),
35 | ),
36 | ]
37 |
--------------------------------------------------------------------------------
/.github/workflows/publish-package.yml:
--------------------------------------------------------------------------------
1 | name: Release package on PyPI
2 |
3 | on:
4 | push:
5 | tags: [ "*" ]
6 |
7 | # Allows you to run this workflow manually from the Actions tab
8 | workflow_dispatch:
9 |
10 | jobs:
11 | build-n-publish:
12 | name: Build package and publish to TestPyPI and PyPI
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 |
18 | - uses: actions/setup-python@v2
19 | name: Use Python 3.9
20 | with:
21 | python-version: 3.9
22 |
23 | - name: Install pypa/build
24 | run: >-
25 | python -m
26 | pip install
27 | build
28 | --user
29 |
30 | - name: Build a binary wheel and a source tarball
31 | run: >-
32 | python -m
33 | build
34 | --sdist
35 | --wheel
36 | --outdir dist/
37 |
38 | - name: Publish package to Test PyPI
39 | uses: pypa/gh-action-pypi-publish@master
40 | with:
41 | password: ${{ secrets.TEST_PYPI_API_TOKEN }}
42 | repository_url: https://test.pypi.org/legacy/
43 |
44 | - name: Publish package
45 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
46 | uses: pypa/gh-action-pypi-publish@v1.5.1
47 | with:
48 | password: ${{ secrets.PYPI_API_TOKEN }}
49 |
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yaml:
--------------------------------------------------------------------------------
1 | name: Run Tox tests
2 | on:
3 | - push
4 | - pull_request
5 |
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | python-version: [ 3.7, 3.8, 3.9, '3.10', '3.11', '3.12' ]
12 | fail-fast: false
13 |
14 | steps:
15 | - uses: actions/checkout@v1
16 |
17 | - name: Set up Python ${{ matrix.python-version }}
18 | uses: actions/setup-python@v2
19 | with:
20 | python-version: ${{ matrix.python-version }}
21 |
22 |
23 | - name: Get pip cache dir
24 | id: pip-cache
25 | run: |
26 | echo "::set-output name=dir::$(pip cache dir)"
27 |
28 | - name: Cache
29 | uses: actions/cache@v2
30 | with:
31 | path: ${{ steps.pip-cache.outputs.dir }}
32 | key:
33 | ${{ matrix.python-version }}-v1-${{ hashFiles('**/requirements.txt') }}-${{ hashFiles('**/tox.ini') }}
34 | restore-keys: |
35 | ${{ matrix.python-version }}-v1-
36 |
37 | - name: Install dependencies
38 | run: |
39 | python -m pip install --upgrade pip
40 | pip install tox tox-gh-actions
41 |
42 | - name: Test with tox
43 | run: tox
44 |
45 | - name: Upload coverage
46 | uses: codecov/codecov-action@v1
47 | with:
48 | name: Python ${{ matrix.python-version }}
49 |
--------------------------------------------------------------------------------
/example/simpletext/models.py:
--------------------------------------------------------------------------------
1 | """Example model."""
2 |
3 | from django.db import models
4 |
5 | from categories.base import CategoryBase
6 |
7 |
8 | class SimpleText(models.Model):
9 | """
10 | (SimpleText description).
11 | """
12 |
13 | name = models.CharField(max_length=255)
14 | description = models.TextField(blank=True)
15 | created = models.DateTimeField(auto_now_add=True)
16 | updated = models.DateTimeField(auto_now=True)
17 |
18 | class Meta:
19 | verbose_name_plural = "Simple Text"
20 | ordering = ("-created",)
21 | get_latest_by = "updated"
22 |
23 | def __unicode__(self):
24 | return self.name
25 |
26 | def get_absolute_url(self):
27 | """Get the absolute URL for this object."""
28 | try:
29 | from django.db.models import permalink
30 |
31 | return permalink("simpletext_detail_view_name", [str(self.id)])
32 | except ImportError:
33 | from django.urls import reverse
34 |
35 | return reverse("simpletext_detail_view_name", args=[str(self.id)])
36 |
37 |
38 | class SimpleCategory(CategoryBase):
39 | """A Test of catgorizing."""
40 |
41 | class Meta:
42 | verbose_name_plural = "simple categories"
43 |
44 |
45 | # mport categories
46 |
47 | # ategories.register_fk(SimpleText, 'primary_category', {'related_name':'simpletext_primary_set'})
48 | # ategories.register_m2m(SimpleText, 'cats', )
49 |
--------------------------------------------------------------------------------
/categories/apps.py:
--------------------------------------------------------------------------------
1 | """Django application setup."""
2 |
3 | from django.apps import AppConfig
4 |
5 |
6 | class CategoriesConfig(AppConfig):
7 | """Application configuration for categories."""
8 |
9 | name = "categories"
10 | verbose_name = "Categories"
11 |
12 | def __init__(self, *args, **kwargs):
13 | super(CategoriesConfig, self).__init__(*args, **kwargs)
14 | from django.db.models.signals import class_prepared
15 |
16 | class_prepared.connect(handle_class_prepared)
17 |
18 | def ready(self):
19 | """Migrate the app after it is ready."""
20 | from django.db.models.signals import post_migrate
21 |
22 | from .migration import migrate_app
23 |
24 | post_migrate.connect(migrate_app)
25 |
26 |
27 | def handle_class_prepared(sender, **kwargs):
28 | """
29 | See if this class needs registering of fields.
30 | """
31 | from .registration import registry
32 | from .settings import FK_REGISTRY, M2M_REGISTRY
33 |
34 | sender_app = sender._meta.app_label
35 | sender_name = sender._meta.model_name
36 |
37 | for key, val in list(FK_REGISTRY.items()):
38 | app_name, model_name = key.split(".")
39 | if app_name == sender_app and sender_name == model_name:
40 | registry.register_model(app_name, sender, "ForeignKey", val)
41 |
42 | for key, val in list(M2M_REGISTRY.items()):
43 | app_name, model_name = key.split(".")
44 | if app_name == sender_app and sender_name == model_name:
45 | registry.register_model(app_name, sender, "ManyToManyField", val)
46 |
--------------------------------------------------------------------------------
/categories/migrations/0003_auto_20200306_1050.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.4 on 2020-03-06 10:50
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 | ("categories", "0002_auto_20170217_1111"),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name="category",
16 | name="level",
17 | field=models.PositiveIntegerField(editable=False),
18 | ),
19 | migrations.AlterField(
20 | model_name="category",
21 | name="lft",
22 | field=models.PositiveIntegerField(editable=False),
23 | ),
24 | migrations.AlterField(
25 | model_name="category",
26 | name="rght",
27 | field=models.PositiveIntegerField(editable=False),
28 | ),
29 | migrations.AlterField(
30 | model_name="categoryrelation",
31 | name="content_type",
32 | field=models.ForeignKey(
33 | limit_choices_to=models.Q(
34 | models.Q(("app_label", "simpletext"), ("model", "simpletext")),
35 | models.Q(("app_label", "flatpages"), ("model", "flatpage")),
36 | _connector="OR",
37 | ),
38 | on_delete=django.db.models.deletion.CASCADE,
39 | to="contenttypes.ContentType",
40 | verbose_name="content type",
41 | ),
42 | ),
43 | ]
44 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """The setup script."""
2 |
3 | from pathlib import Path
4 |
5 | from setuptools import setup
6 |
7 |
8 | def parse_reqs(filepath: str) -> list:
9 | """
10 | Parse a file path containing requirements and return a list of requirements.
11 |
12 | Will properly follow ``-r`` and ``--requirements`` links like ``pip``. This
13 | means nested requirements will be returned as one list.
14 |
15 | Other ``pip``-specific lines are excluded.
16 |
17 | Args:
18 | filepath: The path to the requirements file
19 |
20 | Returns:
21 | All the requirements as a list.
22 | """
23 | path = Path(filepath)
24 | reqstr = path.read_text()
25 | reqs = []
26 | for line in reqstr.splitlines():
27 | line = line.strip()
28 | if line == "":
29 | continue
30 | elif not line or line.startswith("#"):
31 | # comments are lines that start with # only
32 | continue
33 | elif line.startswith("-r") or line.startswith("--requirement"):
34 | _, new_filename = line.split()
35 | new_file_path = path.parent / new_filename
36 | reqs.extend(parse_reqs(new_file_path))
37 | elif line.startswith("-f") or line.startswith("-i") or line.startswith("--"):
38 | continue
39 | elif line.startswith("-Z") or line.startswith("--always-unzip"):
40 | continue
41 | else:
42 | reqs.append(line)
43 | return reqs
44 |
45 |
46 | requirements = parse_reqs("requirements.txt")
47 |
48 | setup(
49 | install_requires=requirements,
50 | py_modules=[],
51 | )
52 |
--------------------------------------------------------------------------------
/example/simpletext/migrations/0002_auto_20171204_0721.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 1.10.5 on 2017-12-04 07:21
2 | from __future__ import unicode_literals
3 |
4 | import django.db.models.deletion
5 | import django.db.models.manager
6 | from django.db import migrations, models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("categories", "0002_auto_20170217_1111"),
12 | ("simpletext", "0001_initial"),
13 | ]
14 |
15 | operations = [
16 | migrations.AlterModelManagers(
17 | name="simplecategory",
18 | managers=[
19 | ("tree", django.db.models.manager.Manager()),
20 | ],
21 | ),
22 | migrations.AddField(
23 | model_name="simpletext",
24 | name="categories",
25 | field=models.ManyToManyField(blank=True, related_name="m2mcats", to="categories.Category"),
26 | ),
27 | migrations.AddField(
28 | model_name="simpletext",
29 | name="primary_category",
30 | field=models.ForeignKey(
31 | blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="categories.Category"
32 | ),
33 | ),
34 | migrations.AddField(
35 | model_name="simpletext",
36 | name="secondary_category",
37 | field=models.ForeignKey(
38 | blank=True,
39 | null=True,
40 | on_delete=django.db.models.deletion.CASCADE,
41 | related_name="simpletext_sec_cat",
42 | to="categories.Category",
43 | ),
44 | ),
45 | ]
46 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist =
3 | begin
4 | py37-lint
5 | py{37,38,39}-django{22,3,31}
6 | py{37,38,39,310}-django{32}
7 | py{38,39,310}-django{40}
8 | py{38,39,310,311}-django{41}
9 | py{310,311,312}-django{42,50,51}
10 | coverage-report
11 |
12 | [gh-actions]
13 | python =
14 | 3.7: py37
15 | 3.8: py38
16 | 3.9: py39
17 | 3.10: py310
18 | 3.11: py311
19 |
20 | [testenv]
21 | passenv = GITHUB_*
22 |
23 | deps=
24 | django22: Django>=2.2,<2.3
25 | django3: Django>=3.0,<3.1
26 | django31: Django>=3.1,<3.2
27 | django32: Django>=3.2,<4.0
28 | django40: Django>=4.0,<4.1
29 | django41: Django>=4.1,<4.2
30 | django42: Django>=4.2,<5.0
31 | django50: Django>=5.0,<5.1
32 | django51: Django>=5.1a1,<5.2
33 | coverage[toml]
34 | pillow
35 | ipdb
36 | codecov
37 | django-test-migrations
38 | django21: django-test-migrations<=1.2.0
39 | django22: django-test-migrations<=1.2.0
40 | django3: django-test-migrations<=1.2.0
41 | django31: django-test-migrations<=1.2.0
42 | -r{toxinidir}/requirements.txt
43 |
44 | commands=
45 | coverage erase
46 | coverage run \
47 | --source=categories \
48 | --omit='.tox/*,example/*,*/tests/*' \
49 | {toxinidir}/example/manage.py \
50 | test \
51 | --settings='settings-testing' \
52 | categories{posargs}
53 | coverage report -m
54 | coverage xml
55 |
56 | [testenv:begin]
57 | commands = coverage erase
58 |
59 | [testenv:py36-lint]
60 | deps=
61 | flake8
62 |
63 | commands=
64 | flake8
65 |
66 | [testenv:coverage-report]
67 | commands =
68 | coverage report -m
69 | coverage xml
70 | codecov
71 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 1.9.2
3 | commit = True
4 | commit_args = --no-verify
5 | tag = True
6 | tag_name = {new_version}
7 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\+\w+-(?P\d+))?
8 | serialize =
9 | {major}.{minor}.{patch}+{$BRANCH_NAME}-{dev}
10 | {major}.{minor}.{patch}
11 | message = Version updated from {current_version} to {new_version}
12 |
13 | [metadata]
14 | name = django-categories
15 | version = attr:categories.__version__
16 | description = A way to handle one or more hierarchical category trees in django.
17 | long_description = file:README.md
18 | long_description_content_type = text/markdown
19 | author = Corey Oordt
20 | author_email = coreyoordt@gmail.com
21 | url = http://github.com/jazzband/django-categories
22 | classifiers =
23 | Framework :: Django
24 |
25 | [options]
26 | zip_safe = False
27 | include_package_data = True
28 | packages = find:
29 |
30 | [options.packages.find]
31 | exclude =
32 | example*
33 | docs
34 | build
35 | include =
36 | categories
37 | categories.*
38 |
39 | [flake8]
40 | ignore = D203,W503,E501
41 | exclude =
42 | .git
43 | .tox
44 | docs
45 | build
46 | dist
47 | doc_src
48 | max-line-length = 119
49 |
50 | [darglint]
51 | ignore = DAR402
52 |
53 | [bdist_wheel]
54 | universal = 1
55 |
56 | [bumpversion:part:dev]
57 |
58 | [bumpversion:file:setup.cfg]
59 |
60 | [bumpversion:file:categories/__init__.py]
61 |
62 | [bumpversion:file(version heading):CHANGELOG.md]
63 | search = Unreleased
64 |
65 | [bumpversion:file(diff link):CHANGELOG.md]
66 | search = {current_version}...HEAD
67 | replace = {current_version}...{new_version}
68 |
69 | [zest.releaser]
70 | python-file-with-version = categories/__init__.py
71 |
--------------------------------------------------------------------------------
/doc_src/_templates/autosummary/module.rst:
--------------------------------------------------------------------------------
1 | .. rst-class:: h4 text-secondary
2 |
3 | {{ fullname }}
4 |
5 | {{ objname | escape | underline}}
6 | .. currentmodule:: {{ fullname }}
7 |
8 |
9 | .. automodule:: {{ fullname }}
10 |
11 | {% block modules -%}
12 | {% if modules %}
13 |
14 | .. rubric:: Submodules
15 |
16 | .. autosummary::
17 | :toctree:
18 | :recursive:
19 | {% for item in modules %}
20 | {% if "migrations" not in item and "tests" not in item%}{{ item }}{% endif %}
21 | {%- endfor %}
22 | {% endif %}
23 | {% endblock %}
24 | {% block attributes -%}
25 | {%- if attributes -%}
26 | .. rubric:: {{ _('Module Attributes') }}
27 |
28 | .. autosummary::
29 | :toctree:
30 | {% for item in attributes %}
31 | {{ item }}
32 | {%- endfor -%}
33 | {%- endif -%}
34 | {% endblock attributes -%}
35 | {%- block functions -%}
36 | {%- if functions %}
37 |
38 | .. rubric:: {{ _('Functions') }}
39 |
40 | .. autosummary::
41 | :toctree:
42 | {% for item in functions %}
43 | {{ item }}
44 | {%- endfor -%}
45 | {%- endif -%}
46 | {% endblock functions -%}
47 | {% block classes -%}
48 | {% if classes %}
49 |
50 | .. rubric:: {{ _('Classes') }}
51 |
52 | .. autosummary::
53 | :toctree:
54 | {% for item in classes %}
55 | {{ item }}
56 | {%- endfor -%}
57 | {%- endif -%}
58 | {% endblock classes -%}
59 | {% block exceptions -%}
60 | {% if exceptions %}
61 |
62 | .. rubric:: {{ _('Exceptions') }}
63 |
64 | .. autosummary::
65 | :toctree:
66 | {% for item in exceptions %}
67 | {{ item }}
68 | {%- endfor -%}
69 | {%- endif -%}
70 | {% endblock exceptions -%}
71 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/PyCQA/isort
3 | rev: 5.13.2
4 | hooks:
5 | - id: isort
6 | additional_dependencies: [toml]
7 | - repo: https://github.com/psf/black
8 | rev: 24.4.2
9 | hooks:
10 | - id: black
11 | - repo: https://github.com/pre-commit/pre-commit-hooks
12 | rev: v4.6.0
13 | hooks:
14 | - id: check-added-large-files
15 | - id: check-case-conflict
16 | - id: check-executables-have-shebangs
17 | - id: check-json
18 | - id: check-merge-conflict
19 | - id: check-shebang-scripts-are-executable
20 | - id: check-symlinks
21 | - id: check-toml
22 | - id: check-yaml
23 | - id: debug-statements
24 | - id: end-of-file-fixer
25 | exclude: "^tests/resources/"
26 | - id: fix-byte-order-marker
27 | - id: fix-encoding-pragma
28 | args: ["--remove"]
29 | - id: requirements-txt-fixer
30 | - repo: https://github.com/PyCQA/flake8
31 | rev: 7.0.0
32 | hooks:
33 | - id: flake8
34 | - repo: https://github.com/pycqa/pydocstyle
35 | rev: 6.3.0
36 | hooks:
37 | - id: pydocstyle
38 | exclude: test.*|custom_.*|\d\d\d\d_.*
39 | additional_dependencies: [toml]
40 | - repo: https://github.com/terrencepreilly/darglint
41 | rev: v1.8.1
42 | hooks:
43 | - id: darglint
44 | args:
45 | - -v 2
46 | - "--message-template={path}:{line} in `{obj}`:\n {msg_id}: {msg}"
47 | - --strictness=short
48 | exclude: test.*|custom_.*|\d\d\d\d_.*
49 |
50 | - repo: https://github.com/econchick/interrogate
51 | rev: 1.7.0 # or master if you're bold
52 | hooks:
53 | - id: interrogate
54 |
--------------------------------------------------------------------------------
/categories/editor/templates/admin/editor/tree_list_results.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% if result_hidden_fields %}
3 | {# DIV for HTML validation #}
4 | {% for item in result_hidden_fields %}{{ item }}{% endfor %}
5 |
6 | {% endif %}
7 | {% if results %}
8 |
9 |
10 |
11 |
12 | {% for header in result_headers %}
13 | {% if header.sortable %}
14 | {% if header.sort_priority > 0 %}
15 |
16 |
17 | {% if num_sorted_fields > 1 %} {{ header.sort_priority }}{% endif %}
18 |
19 |
20 | {% endif %}
21 | {% endif %}
22 | {% if header.sortable %} {{ header.text|capfirst }}{% else %} {{ header.text|capfirst }}{% endif %}
23 |
24 | | {% endfor %}
25 |
26 |
27 |
28 | {% for result in results %}
29 | {% for item in result %}{{ item }}{% endfor %}
30 | {% endfor %}
31 |
32 |
33 |
34 | {% endif %}
35 |
--------------------------------------------------------------------------------
/categories/tests/test_migrations.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | if sys.version_info >= (3, 0):
4 | from django_test_migrations.contrib.unittest_case import MigratorTestCase
5 |
6 | class TestMigrations(MigratorTestCase):
7 | migrate_from = ("categories", "0004_auto_20200517_1832")
8 | migrate_to = ("categories", "0005_unique_category_slug")
9 |
10 | def prepare(self):
11 | Category = self.old_state.apps.get_model("categories", "Category")
12 | Category.tree.create(slug="foo", lft=0, rght=0, tree_id=0, level=0)
13 | Category.tree.create(slug="foo", lft=0, rght=0, tree_id=0, level=0)
14 | Category.tree.create(slug="foo", lft=0, rght=0, tree_id=0, level=0)
15 | for i in range(1, 12):
16 | Category.tree.create(slug="bar", lft=0, rght=0, tree_id=0, level=0)
17 | Category.tree.create(slug="baz", lft=0, rght=0, tree_id=0, level=0)
18 | assert Category.tree.count() == 15
19 |
20 | def test_unique_slug_migration(self):
21 | Category = self.new_state.apps.get_model("categories", "Category")
22 |
23 | self.assertListEqual(
24 | list(Category.tree.values_list("slug", flat=True)),
25 | [
26 | "foo",
27 | "foo-1",
28 | "foo-2",
29 | "bar",
30 | "bar-01",
31 | "bar-02",
32 | "bar-03",
33 | "bar-04",
34 | "bar-05",
35 | "bar-06",
36 | "bar-07",
37 | "bar-08",
38 | "bar-09",
39 | "bar-10",
40 | "baz",
41 | ],
42 | )
43 |
--------------------------------------------------------------------------------
/doc_src/user_guide/adding_the_fields.rst:
--------------------------------------------------------------------------------
1 | .. _adding_the_fields:
2 |
3 | =================================
4 | Adding the fields to the database
5 | =================================
6 |
7 | While Django will create the appropriate columns and tables if you configure Django Categories first, many times that is not possible. You could also reset the table, but you would loose all data in it. There is another way.
8 |
9 | Enter South
10 | ***********
11 |
12 | `South `_ is a Django application for managing database schema changes. South's default behavior is for managing permanent changes to a model's structure. In the case of dynamically adding a field or fields to a model, this doesn't work because you are not making the change permanent. And probably don't want to.
13 |
14 | Django Categories has a management command to create any missing fields. It requires South because it uses the South's API for making changes to databases. The management command is simple: ``python manage.py add_category_fields [app]``\ . If you do not include an app name, it will attempt to do all applications configured.
15 |
16 | Running this command several times will not hurt any data or cause any errors.
17 |
18 | Reconfiguring Fields
19 | ********************
20 |
21 | You can make changes to the field configurations as long as they do not change the underlying database structure. For example, adding a ``related_name`` (see :ref:`registering_a_m2one_relationship`\ ) because it only affects Django code. Changing the name of the field, however, is a different matter.
22 |
23 | Django Categories provides a complementary management command to drop a field from the database (the field must still be in the configuration to do so): ``python manage.py drop_category_field app_name model_name field_name``
24 |
--------------------------------------------------------------------------------
/categories/genericcollection.py:
--------------------------------------------------------------------------------
1 | """Special helpers for generic collections."""
2 |
3 | import json
4 |
5 | from django.contrib import admin
6 | from django.contrib.contenttypes.models import ContentType
7 | from django.urls import NoReverseMatch, reverse
8 |
9 |
10 | class GenericCollectionInlineModelAdmin(admin.options.InlineModelAdmin):
11 | """Inline admin for generic model collections."""
12 |
13 | ct_field = "content_type"
14 | ct_fk_field = "object_id"
15 |
16 | def get_content_types(self):
17 | """Get the content types supported by this collection."""
18 | ctypes = ContentType.objects.all().order_by("id").values_list("id", "app_label", "model")
19 | elements = {}
20 | for x, y, z in ctypes:
21 | try:
22 | elements[x] = reverse("admin:%s_%s_changelist" % (y, z))
23 | except NoReverseMatch:
24 | continue
25 | return json.dumps(elements)
26 |
27 | def get_formset(self, request, obj=None, **kwargs):
28 | """Get the formset for the generic collection."""
29 | result = super(GenericCollectionInlineModelAdmin, self).get_formset(request, obj, **kwargs)
30 | result.content_types = self.get_content_types()
31 | result.ct_fk_field = self.ct_fk_field
32 | return result
33 |
34 | class Media:
35 | js = ("contentrelations/js/genericlookup.js",)
36 |
37 |
38 | class GenericCollectionTabularInline(GenericCollectionInlineModelAdmin):
39 | """Tabular model admin for a generic collection."""
40 |
41 | template = "admin/edit_inline/gen_coll_tabular.html"
42 |
43 |
44 | class GenericCollectionStackedInline(GenericCollectionInlineModelAdmin):
45 | """Stacked model admin for a generic collection."""
46 |
47 | template = "admin/edit_inline/gen_coll_stacked.html"
48 |
--------------------------------------------------------------------------------
/categories/editor/templates/admin/editor/tree_editor.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/change_list.html" %}
2 | {% block extrahead %}
3 | {{block.super}}
4 |
33 | {% endblock %}
34 | {% block result_list %}
35 | {% load admin_list i18n admin_tree_list_tags %}
36 | {% if action_form and actions_on_top and cl.full_result_count %}{% admin_actions %}{% endif %}
37 | {% result_tree_list cl %}
38 | {% if action_form and actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %}
39 | {% endblock %}
40 |
--------------------------------------------------------------------------------
/doc_src/user_guide/admin_settings.rst:
--------------------------------------------------------------------------------
1 | .. _admin_settings:
2 |
3 | ==============================
4 | Adding the fields to the Admin
5 | ==============================
6 |
7 | By default, Django Categories adds the fields you configure to the model's Admin class. If your ModelAdmin class does not use the ``fieldsets`` attribute, the configured category fields are simply appended to the bottom the fields. If your ModelAdmin uses the ``fieldsets`` attribute, a new fieldset named ``Categories``, containing all the configured fields is appended to the fieldsets. You can override or alter this behavior with the :ref:`ADMIN_FIELDSETS` setting.
8 |
9 | ADMIN_FIELDSETS allows you to:
10 |
11 | * Prevent Django Categories from adding the fields or fieldsets to a model's ModelAdmin class.
12 | * Change the name of the fieldset (from the default: "Categories")
13 | * Change the placement of the fieldset (from the default: bottom)
14 |
15 | Preventing fields in the admin class
16 | ====================================
17 |
18 | If you don't want Django Categories to add any fields to the admin class, simply use the following format::
19 |
20 | CATEGORIES_SETTINGS = {
21 | 'ADMIN_FIELDSETS': [
22 | 'app.model': None,
23 | ]
24 | }
25 |
26 | Changing the name of the field set
27 | ==================================
28 |
29 | To rename the field set, use the following format::
30 |
31 | CATEGORIES_SETTINGS = {
32 | 'ADMIN_FIELDSETS': [
33 | 'app.model': 'Taxonomy',
34 | ]
35 | }
36 |
37 | Putting the field set exactly where you want it
38 | ===============================================
39 |
40 | For complete control over the field set, use the following format::
41 |
42 | CATEGORIES_SETTINGS = {
43 | 'ADMIN_FIELDSETS': [
44 | 'app.model': {
45 | 'name': 'Categories',
46 | 'index': 0
47 | },
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = [
3 | "setuptools >= 40.9.0",
4 | "wheel",
5 | ]
6 | build-backend = "setuptools.build_meta"
7 |
8 | [tool.coverage.run]
9 | branch = true
10 | omit = ["**/test_*.py"]
11 |
12 | [tool.coverage.report]
13 | omit = [
14 | "*site-packages*",
15 | "*tests*",
16 | "*.tox*",
17 | ]
18 | show_missing = true
19 | exclude_lines = [
20 | "raise NotImplementedError",
21 | "pragma: no-coverage",
22 | ]
23 |
24 | [tool.interrogate]
25 | ignore-init-method = true
26 | ignore-init-module = false
27 | ignore-magic = true
28 | ignore-semiprivate = false
29 | ignore-private = false
30 | ignore-property-decorators = false
31 | ignore-module = false
32 | ignore-nested-functions = true
33 | ignore-nested-classes = true
34 | ignore-setters = false
35 | fail-under = 60
36 | exclude = ["setup.py", "docs", "build", "test"]
37 | ignore-regex = ["^get$", "^mock_.*", ".*BaseClass.*"]
38 | verbose = 0
39 | quiet = false
40 | whitelist-regex = []
41 | color = true
42 |
43 | [tool.isort]
44 | py_version = "38"
45 | force_grid_wrap = 0
46 | use_parentheses = true
47 | line_length = 88
48 | known_typing = ["typing", "types", "typing_extensions", "mypy", "mypy_extensions"]
49 | sections = ["FUTURE", "TYPING", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
50 | include_trailing_comma = true
51 | profile = "black"
52 | multi_line_output = 3
53 | indent = 4
54 | color_output = true
55 |
56 | [tool.pydocstyle]
57 | convention = "google"
58 | add-ignore = ["D104", "D105", "D106", "D107", "D200", "D212"]
59 | match = "(?!test_).*\\.py"
60 |
61 | [tool.black]
62 | line-length = 119
63 | target-version = ['py38', 'py39']
64 | include = '\.pyi?$'
65 | exclude = '''
66 | /(
67 | \.eggs
68 | | \.git
69 | | \.hg
70 | | \.mypy_cache
71 | | \.tox
72 | | \.venv
73 | | _build
74 | | buck-out
75 | | build
76 | | dist
77 | # The following are specific to Black, you probably don't want those.
78 | | blib2to3
79 | | tests/data
80 | | profiling
81 | )/
82 | '''
83 |
--------------------------------------------------------------------------------
/example/static/editor/jquery.treeTable.css:
--------------------------------------------------------------------------------
1 | /* jQuery TreeTable Core 2.0 stylesheet
2 | *
3 | * This file contains styles that are used to display the tree table. Each tree
4 | * table is assigned the +treeTable+ class.
5 | * ========================================================================= */
6 |
7 | /* jquery.treeTable.collapsible
8 | * ------------------------------------------------------------------------- */
9 | .treeTable tr td .expander {
10 | background-position: left center;
11 | background-repeat: no-repeat;
12 | cursor: pointer;
13 | padding: 0;
14 | zoom: 1; /* IE7 Hack */
15 | }
16 |
17 | .treeTable tr.collapsed td .expander {
18 | background-image: url(toggle-expand-dark.png);
19 | }
20 |
21 | .treeTable tr.expanded td .expander {
22 | background-image: url(toggle-collapse-dark.png);
23 | }
24 |
25 | #changelist table.treeTable td.disclosure {
26 | font-size: 12px;
27 | text-align: left;
28 | font-weight: bold;
29 | padding-left: 19px;
30 | }
31 | #changelist table.treeTable td.disclosure input[type="checkbox"] {
32 | margin-right: 10px;
33 | }
34 |
35 | /* jquery.treeTable.sortable
36 | * ------------------------------------------------------------------------- */
37 | .treeTable tr.selected, .treeTable tr.accept {
38 | background-color: #3875d7;
39 | color: #fff;
40 | }
41 |
42 | .treeTable .ui-draggable-dragging {
43 | border-width: 0;
44 | }
45 |
46 | .treeTable tr.selected, .treeTable tr.ghost_row td {
47 | padding : 5px;
48 | background-color : #ccc;
49 | border : dotted 1px #eee;
50 | font-weight : bold;
51 | text-align : center;
52 | }
53 |
54 | .treeTable tr.collapsed.selected td .expander, .treeTable tr.collapsed.accept td .expander {
55 | background-image: url(toggle-expand-light.png);
56 | }
57 |
58 | .treeTable tr.expanded.selected td .expander, .treeTable tr.expanded.accept td .expander {
59 | background-image: url(toggle-collapse-light.png);
60 | }
61 |
62 | .treeTable .ui-draggable-dragging {
63 | color: #000;
64 | z-index: 1;
65 | }
66 |
--------------------------------------------------------------------------------
/categories/editor/static/editor/jquery.treeTable.css:
--------------------------------------------------------------------------------
1 | /* jQuery TreeTable Core 2.0 stylesheet
2 | *
3 | * This file contains styles that are used to display the tree table. Each tree
4 | * table is assigned the +treeTable+ class.
5 | * ========================================================================= */
6 |
7 | /* jquery.treeTable.collapsible
8 | * ------------------------------------------------------------------------- */
9 | .treeTable tr td .expander {
10 | background-position: left center;
11 | background-repeat: no-repeat;
12 | cursor: pointer;
13 | padding: 0;
14 | zoom: 1; /* IE7 Hack */
15 | }
16 |
17 |
18 | .treeTable td.action-checkbox, .treeTable th.action-checkbox-column {
19 | padding-left: 20px;
20 | }
21 | .treeTable tr.collapsed td .expander {
22 | background-image: url(toggle-expand-dark.png);
23 | }
24 |
25 | .treeTable tr.expanded td .expander {
26 | background-image: url(toggle-collapse-dark.png);
27 | }
28 |
29 | #changelist table.treeTable td.disclosure {
30 | font-size: 12px;
31 | text-align: left;
32 | font-weight: bold;
33 | padding-left: 19px;
34 | }
35 | #changelist table.treeTable td.disclosure input[type="checkbox"] {
36 | margin-right: 10px;
37 | }
38 |
39 | /* jquery.treeTable.sortable
40 | * ------------------------------------------------------------------------- */
41 | .treeTable tr.selected, .treeTable tr.accept {
42 | background-color: #3875d7;
43 | color: #fff;
44 | }
45 |
46 | .treeTable .ui-draggable-dragging {
47 | border-width: 0;
48 | }
49 |
50 | .treeTable tr.selected, .treeTable tr.ghost_row td {
51 | padding : 5px;
52 | background-color : #ccc;
53 | border : dotted 1px #eee;
54 | font-weight : bold;
55 | text-align : center;
56 | }
57 |
58 | .treeTable tr.collapsed.selected td .expander, .treeTable tr.collapsed.accept td .expander {
59 | background-image: url(toggle-expand-light.png);
60 | }
61 |
62 | .treeTable tr.expanded.selected td .expander, .treeTable tr.expanded.accept td .expander {
63 | background-image: url(toggle-collapse-light.png);
64 | }
65 |
66 | .treeTable .ui-draggable-dragging {
67 | color: #000;
68 | z-index: 1;
69 | }
70 |
--------------------------------------------------------------------------------
/categories/editor/templates/admin/editor/grappelli_tree_editor.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/change_list.html" %}
2 | {% load admin_list i18n admin_tree_list_tags %}
3 | {% block extrahead %}
4 | {{ block.super }}
5 |
34 |
44 | {% endblock %}
45 | {% block result_list %}
46 | {% result_tree_list cl %}
47 | {% if action_form and actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %}
48 | {% endblock %}
49 |
--------------------------------------------------------------------------------
/categories/tests/test_registration.py:
--------------------------------------------------------------------------------
1 | # Test adding 1 fk string
2 | # Test adding 1 fk dict
3 | # test adding many-to-many
4 | # test adding 1 fk, 1 m2m
5 | import django
6 | from django.test import TestCase
7 |
8 | from categories.registration import _process_registry, registry
9 |
10 |
11 | class CategoryRegistrationTest(TestCase):
12 | """
13 | Test various aspects of adding fields to a model.
14 | """
15 |
16 | def test_foreignkey_string(self):
17 | FK_REGISTRY = {"flatpages.flatpage": "category"}
18 | _process_registry(FK_REGISTRY, registry.register_fk)
19 | from django.contrib.flatpages.models import FlatPage
20 |
21 | self.assertTrue("category" in [f.name for f in FlatPage()._meta.get_fields()])
22 |
23 | def test_foreignkey_dict(self):
24 | FK_REGISTRY = {"flatpages.flatpage": {"name": "category"}}
25 | _process_registry(FK_REGISTRY, registry.register_fk)
26 | from django.contrib.flatpages.models import FlatPage
27 |
28 | self.assertTrue("category" in [f.name for f in FlatPage()._meta.get_fields()])
29 |
30 | def test_foreignkey_list(self):
31 | FK_REGISTRY = {"flatpages.flatpage": ({"name": "category", "related_name": "cats"},)}
32 | _process_registry(FK_REGISTRY, registry.register_fk)
33 | from django.contrib.flatpages.models import FlatPage
34 |
35 | self.assertTrue("category" in [f.name for f in FlatPage()._meta.get_fields()])
36 |
37 | if django.VERSION[1] >= 7:
38 |
39 | def test_new_foreignkey_string(self):
40 | registry.register_model("flatpages", "flatpage", "ForeignKey", "category")
41 | from django.contrib.flatpages.models import FlatPage
42 |
43 | self.assertTrue("category" in [f.name for f in FlatPage()._meta.get_fields()])
44 |
45 |
46 | class Categorym2mTest(TestCase):
47 | def test_m2m_string(self):
48 | M2M_REGISTRY = {"flatpages.flatpage": "categories"}
49 | _process_registry(M2M_REGISTRY, registry.register_m2m)
50 | from django.contrib.flatpages.models import FlatPage
51 |
52 | self.assertTrue("category" in [f.name for f in FlatPage()._meta.get_fields()])
53 |
--------------------------------------------------------------------------------
/categories/settings.py:
--------------------------------------------------------------------------------
1 | """Manages settings for the categories application."""
2 |
3 | import collections
4 |
5 | from django.conf import settings
6 | from django.db.models import Q
7 | from django.utils.translation import gettext_lazy as _
8 |
9 | DEFAULT_SETTINGS = {
10 | "ALLOW_SLUG_CHANGE": False,
11 | "M2M_REGISTRY": {},
12 | "FK_REGISTRY": {},
13 | "THUMBNAIL_UPLOAD_PATH": "uploads/categories/thumbnails",
14 | "THUMBNAIL_STORAGE_ALIAS": "default",
15 | "JAVASCRIPT_URL": getattr(settings, "STATIC_URL", settings.MEDIA_URL) + "js/",
16 | "SLUG_TRANSLITERATOR": "",
17 | "REGISTER_ADMIN": True,
18 | "RELATION_MODELS": [],
19 | }
20 |
21 | if hasattr(settings, "DEFAULT_FILE_STORAGE"):
22 | DEFAULT_SETTINGS["THUMBNAIL_STORAGE"] = settings.DEFAULT_FILE_STORAGE
23 |
24 | DEFAULT_SETTINGS.update(getattr(settings, "CATEGORIES_SETTINGS", {}))
25 |
26 | if DEFAULT_SETTINGS["SLUG_TRANSLITERATOR"]:
27 | if isinstance(DEFAULT_SETTINGS["SLUG_TRANSLITERATOR"], collections.Callable):
28 | pass
29 | elif isinstance(DEFAULT_SETTINGS["SLUG_TRANSLITERATOR"], str):
30 | from django.utils.importlib import import_module
31 |
32 | bits = DEFAULT_SETTINGS["SLUG_TRANSLITERATOR"].split(".")
33 | module = import_module(".".join(bits[:-1]))
34 | DEFAULT_SETTINGS["SLUG_TRANSLITERATOR"] = getattr(module, bits[-1])
35 | else:
36 | from django.core.exceptions import ImproperlyConfigured
37 |
38 | raise ImproperlyConfigured(
39 | _("%(transliterator) must be a callable or a string.") % {"transliterator": "SLUG_TRANSLITERATOR"}
40 | )
41 | else:
42 | DEFAULT_SETTINGS["SLUG_TRANSLITERATOR"] = lambda x: x
43 |
44 |
45 | # Add all the keys/values to the module's namespace
46 | globals().update(DEFAULT_SETTINGS)
47 |
48 | RELATIONS = [Q(app_label=al, model=m) for al, m in [x.split(".") for x in DEFAULT_SETTINGS["RELATION_MODELS"]]]
49 |
50 | # The field registry keeps track of the individual fields created.
51 | # {'app.model.field': Field(**extra_params)}
52 | # Useful for doing a schema migration
53 | FIELD_REGISTRY = {}
54 |
55 | # The model registry keeps track of which models have one or more fields
56 | # registered.
57 | # {'app': [model1, model2]}
58 | # Useful for admin alteration
59 | MODEL_REGISTRY = {}
60 |
--------------------------------------------------------------------------------
/doc_src/conf.py:
--------------------------------------------------------------------------------
1 | """
2 | Sphinx configuration.
3 | """
4 |
5 | import os
6 | import sys
7 | from datetime import date
8 | from pathlib import Path
9 |
10 | project_root = Path("..").resolve()
11 | sys.path.insert(0, str(project_root / "example"))
12 | sys.path.insert(0, str(project_root))
13 | os.environ["DJANGO_SETTINGS_MODULE"] = "example.settings"
14 |
15 | # Setup Django
16 | import django # NOQA
17 |
18 | django.setup()
19 |
20 | import categories # noqa
21 | import categories.urls # noqa
22 |
23 | project = "Django Categories"
24 | copyright = f"2010-{date.today():%Y}, Corey Oordt"
25 |
26 | version = categories.__version__
27 | release = categories.__version__
28 |
29 | # -- General configuration -----------------------------------------------------
30 |
31 | extensions = [
32 | "myst_parser",
33 | "sphinx.ext.autodoc",
34 | "sphinx.ext.viewcode",
35 | "sphinx.ext.autosummary",
36 | "sphinx.ext.intersphinx",
37 | "sphinx.ext.autosectionlabel",
38 | "sphinx.ext.napoleon",
39 | "sphinx_autodoc_typehints",
40 | "sphinx.ext.coverage",
41 | "sphinx.ext.githubpages",
42 | "sphinxcontrib_django2",
43 | ]
44 | autosectionlabel_prefix_document = True
45 | autosectionlabel_maxdepth = 2
46 | autosummary_generate = True
47 | napoleon_attr_annotations = True
48 | napoleon_include_special_with_doc = False
49 | napoleon_include_private_with_doc = True
50 | napoleon_include_init_with_doc = True
51 | myst_enable_extensions = [
52 | "amsmath",
53 | "colon_fence",
54 | "deflist",
55 | "dollarmath",
56 | "linkify",
57 | "replacements",
58 | "smartquotes",
59 | "substitution",
60 | "tasklist",
61 | ]
62 | intersphinx_mapping = {
63 | "python": ("https://docs.python.org/3", None),
64 | "django": (
65 | "https://docs.djangoproject.com/en/stable",
66 | "https://docs.djangoproject.com/en/stable/_objects",
67 | ),
68 | }
69 |
70 | templates_path = ["_templates"]
71 | source_suffix = [".rst", ".md"]
72 | master_doc = "index"
73 |
74 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
75 | pygments_style = "sphinx"
76 | todo_include_todos = False
77 |
78 |
79 | # -- Options for HTML output ---------------------------------------------------
80 |
81 | html_theme = "pydata_sphinx_theme"
82 | html_static_path = ["_static"]
83 | html_css_files = [
84 | "css/custom.css",
85 | ]
86 |
--------------------------------------------------------------------------------
/doc_src/user_guide/usage.rst:
--------------------------------------------------------------------------------
1 | =============================
2 | Using categories in templates
3 | =============================
4 |
5 |
6 | Getting all items within a category
7 | ===================================
8 |
9 | The :py:class:`Category` model automatically gets `reverse relationships `_ with all other models related to it.
10 |
11 | This allows you access to the related objects from the template without altering any views. For example, if you only had ``Entry`` models related to :py:class:`Category`, your ``categories/category_detail.html`` template could look like
12 |
13 | .. literalinclude:: usage_example_template.html
14 | :language: django
15 | :linenos:
16 |
17 |
18 | If you have ``related_name`` parameters to the configuration (see :ref:`registering_models`), then you would use ``category.related_name.all`` instead of ``category.relatedmodel_set.all``\ .
19 |
20 |
21 | Template Tags
22 | =============
23 |
24 | To use the template tags::
25 |
26 | {% import category_tags %}
27 |
28 |
29 | ``tree_info``
30 | -------------
31 |
32 | Given a list of categories, iterates over the list, generating
33 | two-tuples of the current tree item and a ``dict`` containing
34 | information about the tree structure around the item, with the following
35 | keys:
36 |
37 | ``'new_level'``
38 | ``True`` if the current item is the start of a new level in
39 | the tree, ``False`` otherwise.
40 |
41 | ``'closed_levels'``
42 | A list of levels which end after the current item. This will
43 | be an empty list if the next item's level is the same as or
44 | greater than the level of the current item.
45 |
46 | An optional argument can be provided to specify extra details about the
47 | structure which should appear in the ``dict``. This should be a
48 | comma-separated list of feature names. The valid feature names are:
49 |
50 | ancestors
51 | Adds a list of unicode representations of the ancestors of the
52 | current node, in descending order (root node first, immediate
53 | parent last), under the key ``'ancestors'``.
54 |
55 | For example: given the sample tree below, the contents of the list
56 | which would be available under the ``'ancestors'`` key are given
57 | on the right::
58 |
59 | Books -> []
60 | Sci-fi -> [u'Books']
61 | Dystopian Futures -> [u'Books', u'Sci-fi']
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Django Categories
2 |
3 | [](https://jazzband.co/)
4 | [](https://codecov.io/gh/jazzband/django-categories)
5 | [](https://pypi.org/project/django-categories/#history)
6 |
7 |
8 | Django Categories grew out of our need to provide a basic hierarchical taxonomy management system that multiple applications could use independently or in concert.
9 |
10 | As a news site, our stories, photos, and other content get divided into "sections" and we wanted all the apps to use the same set of sections. As our needs grew, the Django Categories grew in the functionality it gave to category handling within web pages.
11 |
12 | ## Features of the project
13 |
14 | **Multiple trees, or a single tree.**
15 | You can treat all the records as a single tree, shared by all the applications. You can also treat each of the top level records as individual trees, for different apps or uses.
16 |
17 | **Easy handling of hierarchical data.**
18 | We use [Django MPTT](http://pypi.python.org/pypi/django-mptt) to manage the data efficiently and provide the extra access functions.
19 |
20 | **Easy importation of data.**
21 | Import a tree or trees of space- or tab-indented data with a Django management command.
22 |
23 | **Metadata for better SEO on web pages**
24 | Include all the metadata you want for easy inclusion on web pages.
25 |
26 | **Link uncategorized objects to a category**
27 | Attach any number of objects to a category, even if the objects themselves aren't categorized.
28 |
29 | **Hierarchical Admin**
30 | Shows the data in typical tree form with disclosure triangles
31 |
32 | **Template Helpers**
33 | Easy ways for displaying the tree data in templates:
34 |
35 | - **Show one level of a tree.** All root categories or just children of a specified category
36 |
37 | - **Show multiple levels.** Ancestors of category, category and all children of category or a category and its children
38 |
39 | ## Categories API
40 |
41 | Support for categories API can be added by third party application [django-categories-api](https://github.com/PetrDlouhy/django-categories-api).
42 |
43 | **Optional Thumbnail field.**
44 | Have a thumbnail for each category!
45 |
46 | **"Categorize" models in settings.**
47 | Now you don't have to modify the model to add a `Category` relationship. Use the new settings to "wire" categories to different models.
48 |
--------------------------------------------------------------------------------
/example/simpletext/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 1.9.13 on 2017-10-12 20:27
2 | from __future__ import unicode_literals
3 |
4 | import django.db.models.deletion
5 | import mptt.fields
6 | from django.db import migrations, models
7 |
8 |
9 | class Migration(migrations.Migration):
10 | initial = True
11 |
12 | dependencies = []
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name="SimpleCategory",
17 | fields=[
18 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
19 | ("name", models.CharField(max_length=100, verbose_name="name")),
20 | ("slug", models.SlugField(verbose_name="slug")),
21 | ("active", models.BooleanField(default=True, verbose_name="active")),
22 | ("lft", models.PositiveIntegerField(db_index=True, editable=False)),
23 | ("rght", models.PositiveIntegerField(db_index=True, editable=False)),
24 | ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)),
25 | ("level", models.PositiveIntegerField(db_index=True, editable=False)),
26 | (
27 | "parent",
28 | mptt.fields.TreeForeignKey(
29 | blank=True,
30 | null=True,
31 | on_delete=django.db.models.deletion.CASCADE,
32 | related_name="children",
33 | to="simpletext.SimpleCategory",
34 | verbose_name="parent",
35 | ),
36 | ),
37 | ],
38 | options={
39 | "verbose_name_plural": "simple categories",
40 | },
41 | ),
42 | migrations.CreateModel(
43 | name="SimpleText",
44 | fields=[
45 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
46 | ("name", models.CharField(max_length=255)),
47 | ("description", models.TextField(blank=True)),
48 | ("created", models.DateTimeField(auto_now_add=True)),
49 | ("updated", models.DateTimeField(auto_now=True)),
50 | ],
51 | options={
52 | "ordering": ("-created",),
53 | "get_latest_by": "updated",
54 | "verbose_name_plural": "Simple Text",
55 | },
56 | ),
57 | ]
58 |
--------------------------------------------------------------------------------
/categories/tests/test_category_import.py:
--------------------------------------------------------------------------------
1 | # test spaces in hierarchy
2 | # test tabs in hierarchy
3 | # test mixed
4 | import os
5 |
6 | from django.conf import settings
7 | from django.core.management.base import CommandError
8 | from django.test import TestCase, override_settings
9 |
10 | from categories.management.commands.import_categories import Command
11 | from categories.models import Category
12 |
13 |
14 | @override_settings(INSTALLED_APPS=(app for app in settings.INSTALLED_APPS if app != "django.contrib.flatpages"))
15 | class CategoryImportTest(TestCase):
16 | def setUp(self):
17 | pass
18 |
19 | def _import_file(self, filename):
20 | root_cats = ["Category 1", "Category 2", "Category 3"]
21 | testfile = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures", filename))
22 | cmd = Command()
23 | cmd.handle(testfile)
24 | roots = Category.tree.root_nodes()
25 |
26 | self.assertEqual(len(roots), 3)
27 | for item in roots:
28 | assert item.name in root_cats
29 |
30 | cat2 = Category.objects.get(name="Category 2")
31 | cat21 = cat2.children.all()[0]
32 | self.assertEqual(cat21.name, "Category 2-1")
33 | cat211 = cat21.children.all()[0]
34 | self.assertEqual(cat211.name, "Category 2-1-1")
35 |
36 | def testImportSpaceDelimited(self):
37 | Category.objects.all().delete()
38 | self._import_file("test_category_spaces.txt")
39 |
40 | items = Category.objects.all()
41 |
42 | self.assertEqual(items[0].name, "Category 1")
43 | self.assertEqual(items[1].name, "Category 1-1")
44 | self.assertEqual(items[2].name, "Category 1-2")
45 |
46 | def testImportTabDelimited(self):
47 | Category.objects.all().delete()
48 | self._import_file("test_category_tabs.txt")
49 |
50 | items = Category.objects.all()
51 |
52 | self.assertEqual(items[0].name, "Category 1")
53 | self.assertEqual(items[1].name, "Category 1-1")
54 | self.assertEqual(items[2].name, "Category 1-2")
55 |
56 | def testMixingTabsSpaces(self):
57 | """
58 | Should raise an exception.
59 | """
60 | string1 = ["cat1", " cat1-1", "\tcat1-2-FAIL!", ""]
61 | string2 = ["cat1a", "\tcat1-1a", " cat1-2-FAIL!", ""]
62 | cmd = Command()
63 |
64 | # raise Exception
65 | self.assertRaises(CommandError, cmd.parse_lines, string1)
66 | self.assertRaises(CommandError, cmd.parse_lines, string2)
67 |
--------------------------------------------------------------------------------
/doc_src/user_guide/custom_categories.rst:
--------------------------------------------------------------------------------
1 | .. _creating_custom_categories:
2 |
3 | ==========================
4 | Creating Custom Categories
5 | ==========================
6 |
7 | Django Categories isn't just for using a single category model. It allows you to create your own custom category-like models with as little or much customization as you need.
8 |
9 | Name only
10 | =========
11 |
12 | For many cases, you want a simple user-managed lookup table. You can do this with just a little bit of code. The resulting model will include name, slug and active fields and a hierarchical admin.
13 |
14 | #. Create a model that subclasses :py:class:`CategoryBase`
15 |
16 | .. literalinclude:: code_examples/custom_categories1.py
17 | :linenos:
18 |
19 | #. Create a subclass of CategoryBaseAdmin.
20 |
21 | .. literalinclude:: code_examples/custom_categories2.py
22 | :linenos:
23 |
24 | #. Register your model and custom model admin class.
25 |
26 | Name and other data
27 | ===================
28 |
29 | Sometimes you need more functionality, such as extra metadata and custom functions. The :py:class:`Category` model in this package does this.
30 |
31 | #. Create a model that subclasses :py:class:`CategoryBase` as above.
32 |
33 | #. Add new fields to the model. The :py:class:`Category` model adds these extra fields.
34 |
35 | .. literalinclude:: code_examples/custom_categories3.py
36 | :linenos:
37 |
38 | #. Add new methods to the model. For example, the :py:class:`Category` model adds several new methods, including overriding the :py:meth:`save` method.
39 |
40 | .. literalinclude:: code_examples/custom_categories4.py
41 | :linenos:
42 |
43 | #. Alter :py:class:`Meta` or :py:class:`MPTTMeta` class. Either of these inner classes can be overridden, however your :py:class:`Meta` class should inherit :py:class:`CategoryBase.Meta`. Options for :py:class:`Meta` are in the `Django-MPTT docs `_.
44 |
45 | .. literalinclude:: code_examples/custom_categories5.py
46 | :linenos:
47 |
48 | #. For the admin, you must create a form that subclasses :py:class:`CategoryBaseAdminForm` and at least sets the ``Meta.model`` attribute. You can also alter the form fields and cleaning methods, as :py:class:`Category` does.
49 |
50 | .. literalinclude:: code_examples/custom_categories6.py
51 | :linenos:
52 |
53 | #. Next you must subclass :py:class:`CategoryBaseAdmin` and assign the ``form`` attribute the form class created above. You can alter any other attributes as necessary.
54 |
55 | .. literalinclude:: code_examples/custom_categories7.py
56 | :linenos:
57 |
--------------------------------------------------------------------------------
/doc_src/getting_started.rst:
--------------------------------------------------------------------------------
1 | ===============
2 | Getting Started
3 | ===============
4 |
5 | You can use Django Categories in two ways:
6 |
7 | 1. As storage for one tree of categories, using the :py:class:`Category` model::
8 |
9 | Top Category 1
10 | Subcategory 1-1
11 | Subcategory 1-2
12 | subcategory 1-2-1
13 | Top Category 2
14 | Subcategory 2-1
15 |
16 | 2. As the basis for several custom categories (see :ref:`creating_custom_categories`), e.g. a **MusicGenre** model
17 |
18 | ::
19 |
20 | MusicGenre 1
21 | MusicSubGenre 1-1
22 | MusicSubGenre 1-2
23 | MusicSubGenre 1-2-1
24 | MusicGenre 2
25 | MusicSubGenre 2-1
26 |
27 | and a **Subject** model
28 |
29 | ::
30 |
31 | Subject 1
32 | Discipline 1-1
33 | Discipline 1-2
34 | SubDiscipline 1-2-1
35 | Subject 2
36 | Discipline 2-1
37 |
38 |
39 |
40 | Connecting your model with Django-Categories
41 | ============================================
42 |
43 | There are two ways to add Category fields to an application. If you are in control of the code (it's your application) or you are willing to take control of the code (fork someone else's app) you can make a :ref:`hard_coded_connection`\ .
44 |
45 | For 3rd-party apps or even your own apps that you don't wish to add Django-Categories as a dependency, you can configure a :ref:`lazy_connection`\ .
46 |
47 | .. _hard_coded_connection:
48 |
49 | Hard Coded Connection
50 | ---------------------
51 |
52 | Hard coded connections are done in the exact same way you handle any other foreign key or many-to-many relations to a model.
53 |
54 | .. code-block:: python
55 |
56 | from django.db import models
57 |
58 | class MyModel(models.Model):
59 | name = models.CharField(max_length=100)
60 | category = models.ForeignKey('categories.Category')
61 |
62 | Don't forget to add the field or fields to your ``ModelAdmin`` class as well.
63 |
64 |
65 | .. _lazy_connection:
66 |
67 | Lazy Connection
68 | ---------------
69 |
70 | Lazy connections are done through configuring Django Categories in the project's ``settings.py`` file. When the project starts up, the configured fields are dynamically added to the configured models and admin.
71 |
72 | If you do this before you have created the database (before you ran ``manage.py syncdb``), the fields will also be in the tables. If you do this after you have already created all the tables, you can run ``manage.py add_category_fields`` to create the fields (this requires Django South to be installed).
73 |
74 | You add a many-to-one or many-to-many relationship with Django Categories using the :ref:`FK_REGISTRY` and :ref:`M2M_REGISTRY` settings respectively. For more information see :ref:`registering_models`\ .
75 |
--------------------------------------------------------------------------------
/categories/migration.py:
--------------------------------------------------------------------------------
1 | """Adds and removes category relations on the database."""
2 |
3 | from django.apps import apps
4 | from django.db import DatabaseError, connection, transaction
5 | from django.db.utils import OperationalError, ProgrammingError
6 |
7 |
8 | def table_exists(table_name):
9 | """
10 | Check if a table exists in the database.
11 | """
12 | pass
13 |
14 |
15 | def field_exists(app_name, model_name, field_name):
16 | """
17 | Does the FK or M2M table exist in the database already?
18 | """
19 | model = apps.get_model(app_name, model_name)
20 | table_name = model._meta.db_table
21 | cursor = connection.cursor()
22 | field_info = connection.introspection.get_table_description(cursor, table_name)
23 | field_names = [f.name for f in field_info]
24 |
25 | # Return True if the many to many table exists
26 | field = model._meta.get_field(field_name)
27 | if hasattr(field, "m2m_db_table"):
28 | m2m_table_name = field.m2m_db_table()
29 | try:
30 | m2m_field_info = connection.introspection.get_table_description(cursor, m2m_table_name)
31 | except DatabaseError: # Django >= 4.1 throws DatabaseError
32 | m2m_field_info = []
33 | if m2m_field_info:
34 | return True
35 |
36 | return field_name in field_names
37 |
38 |
39 | def drop_field(app_name, model_name, field_name):
40 | """
41 | Drop the given field from the app's model.
42 | """
43 | app_config = apps.get_app_config(app_name)
44 | model = app_config.get_model(model_name)
45 | field = model._meta.get_field(field_name)
46 | with connection.schema_editor() as schema_editor:
47 | schema_editor.remove_field(model, field)
48 |
49 |
50 | def migrate_app(sender, *args, **kwargs):
51 | """
52 | Migrate all models of this app registered.
53 | """
54 | from .registration import registry
55 |
56 | if "app_config" not in kwargs:
57 | return
58 | app_config = kwargs["app_config"]
59 |
60 | app_name = app_config.label
61 |
62 | fields = [fld for fld in list(registry._field_registry.keys()) if fld.startswith(app_name)]
63 |
64 | sid = transaction.savepoint()
65 | for fld in fields:
66 | model_name, field_name = fld.split(".")[1:]
67 | if field_exists(app_name, model_name, field_name):
68 | continue
69 | model = app_config.get_model(model_name)
70 | try:
71 | with connection.schema_editor() as schema_editor:
72 | schema_editor.add_field(model, registry._field_registry[fld])
73 | if sid:
74 | transaction.savepoint_commit(sid)
75 | # Django 4.1 with sqlite3 has for some reason started throwing OperationalError
76 | # instead of ProgrammingError, so we need to catch both.
77 | except (ProgrammingError, OperationalError):
78 | if sid:
79 | transaction.savepoint_rollback(sid)
80 | continue
81 |
--------------------------------------------------------------------------------
/categories/management/commands/import_categories.py:
--------------------------------------------------------------------------------
1 | """Import category trees from a file."""
2 |
3 | from django.core.management.base import BaseCommand, CommandError
4 | from django.db import transaction
5 |
6 | from categories.models import Category
7 | from categories.settings import SLUG_TRANSLITERATOR
8 |
9 | from ...utils import slugify
10 |
11 |
12 | class Command(BaseCommand):
13 | """Import category trees from a file."""
14 |
15 | help = (
16 | "Imports category tree(s) from a file. Sub categories must be indented by the same multiple of spaces or tabs."
17 | )
18 | args = "file_path [file_path ...]"
19 |
20 | def get_indent(self, string):
21 | """
22 | Look through the string and count the spaces.
23 | """
24 | indent_amt = 0
25 |
26 | if string[0] == "\t":
27 | return "\t"
28 | for char in string:
29 | if char == " ":
30 | indent_amt += 1
31 | else:
32 | return " " * indent_amt
33 |
34 | @transaction.atomic
35 | def make_category(self, string, parent=None, order=1):
36 | """
37 | Make and save a category object from a string.
38 | """
39 | cat = Category(
40 | name=string.strip(),
41 | slug=slugify(SLUG_TRANSLITERATOR(string.strip()))[:49],
42 | # arent=parent,
43 | order=order,
44 | )
45 | cat._tree_manager.insert_node(cat, parent, "last-child", True)
46 | cat.save()
47 | if parent:
48 | parent.rght = cat.rght + 1
49 | parent.save()
50 | return cat
51 |
52 | def parse_lines(self, lines):
53 | """
54 | Do the work of parsing each line.
55 | """
56 | indent = ""
57 | level = 0
58 |
59 | if lines[0][0] in [" ", "\t"]:
60 | raise CommandError("The first line in the file cannot start with a space or tab.")
61 |
62 | # This keeps track of the current parents at a given level
63 | current_parents = {0: None}
64 |
65 | for line in lines:
66 | if len(line) == 0:
67 | continue
68 | if line[0] in [" ", "\t"]:
69 | if indent == "":
70 | indent = self.get_indent(line)
71 | elif line[0] not in indent:
72 | raise CommandError("You can't mix spaces and tabs for indents")
73 | level = line.count(indent)
74 | current_parents[level] = self.make_category(line, parent=current_parents[level - 1])
75 | else:
76 | # We are back to a zero level, so reset the whole thing
77 | current_parents = {0: self.make_category(line)}
78 | current_parents[0]._tree_manager.rebuild()
79 |
80 | def handle(self, *file_paths, **options):
81 | """
82 | Handle the basic import.
83 | """
84 | import os
85 |
86 | for file_path in file_paths:
87 | if not os.path.isfile(file_path):
88 | print("File %s not found." % file_path)
89 | continue
90 | with open(file_path, "r") as f:
91 | data = f.readlines()
92 | self.parse_lines(data)
93 |
--------------------------------------------------------------------------------
/categories/templates/admin/edit_inline/gen_coll_tabular.html:
--------------------------------------------------------------------------------
1 | {% load i18n static %}
2 |
69 |
--------------------------------------------------------------------------------
/categories/tests/test_admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from django.test import Client, TestCase
3 | from django.test.utils import override_settings
4 | from django.urls import reverse
5 | from django.utils.encoding import smart_str
6 |
7 | from categories.models import Category
8 |
9 |
10 | class TestCategoryAdmin(TestCase):
11 | def setUp(self):
12 | self.client = Client()
13 | self.superuser = User.objects.create_superuser("testuser", "testuser@example.com", "password")
14 |
15 | def test_adding_parent_and_child(self):
16 | self.client.login(username="testuser", password="password")
17 | url = reverse("admin:categories_category_add")
18 | data = {
19 | "parent": "",
20 | "name": smart_str("Parent Catégory"),
21 | "thumbnail": "",
22 | "filename": "",
23 | "active": "on",
24 | "alternate_title": "",
25 | "alternate_url": "",
26 | "description": "",
27 | "meta_keywords": "",
28 | "meta_extra": "",
29 | "order": 0,
30 | "slug": "parent",
31 | "_save": "_save",
32 | "categoryrelation_set-TOTAL_FORMS": "0",
33 | "categoryrelation_set-INITIAL_FORMS": "0",
34 | "categoryrelation_set-MIN_NUM_FORMS": "1000",
35 | "categoryrelation_set-MAX_NUM_FORMS": "1000",
36 | }
37 | resp = self.client.post(url, data=data)
38 | self.assertEqual(resp.status_code, 302)
39 | self.assertEqual(1, Category.objects.count())
40 |
41 | # update parent
42 | data.update({"name": smart_str("Parent Catégory (Changed)")})
43 | resp = self.client.post(reverse("admin:categories_category_change", args=(1,)), data=data)
44 | self.assertEqual(resp.status_code, 302)
45 | self.assertEqual(1, Category.objects.count())
46 |
47 | # add a child
48 | data.update(
49 | {
50 | "parent": "1",
51 | "name": smart_str("Child Catégory"),
52 | "slug": smart_str("child-category"),
53 | }
54 | )
55 | resp = self.client.post(url, data=data)
56 | self.assertEqual(resp.status_code, 302)
57 | self.assertEqual(2, Category.objects.count())
58 |
59 | # update child
60 | data.update({"name": "Child (Changed)"})
61 | resp = self.client.post(reverse("admin:categories_category_change", args=(2,)), data=data)
62 | self.assertEqual(resp.status_code, 302)
63 | self.assertEqual(2, Category.objects.count())
64 |
65 | # test the admin list view
66 | url = reverse("admin:categories_category_changelist")
67 | resp = self.client.get(url)
68 | self.assertEqual(resp.status_code, 200)
69 |
70 | @override_settings(RELATION_MODELS=True)
71 | def test_addview_get(self):
72 | self.client.force_login(self.superuser)
73 | url = reverse("admin:categories_category_add")
74 | resp = self.client.get(url)
75 | self.assertEqual(resp.status_code, 200)
76 | self.assertContains(resp, '')
77 | self.assertContains(
78 | resp,
79 | '',
81 | html=True,
82 | )
83 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### macOS ###
2 | # General
3 | .DS_Store
4 | .AppleDouble
5 | .LSOverride
6 |
7 | ### Python ###
8 | # Byte-compiled / optimized / DLL files
9 | __pycache__/
10 | *.py[cod]
11 | *$py.class
12 |
13 | # C extensions
14 | *.so
15 |
16 | # Distribution / packaging
17 | .Python
18 | build/
19 | develop-eggs/
20 | dist/
21 | downloads/
22 | eggs/
23 | .eggs/
24 | lib/
25 | lib64/
26 | parts/
27 | sdist/
28 | var/
29 | wheels/
30 | share/python-wheels/
31 | *.egg-info/
32 | .installed.cfg
33 | *.egg
34 | MANIFEST
35 |
36 | # PyInstaller
37 | # Usually these files are written by a python script from a template
38 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
39 | *.manifest
40 | *.spec
41 |
42 | # Installer logs
43 | pip-log.txt
44 | pip-delete-this-directory.txt
45 |
46 | # Unit test / coverage reports
47 | htmlcov/
48 | .tox/
49 | .nox/
50 | .coverage
51 | .coverage.*
52 | .cache
53 | nosetests.xml
54 | coverage.xml
55 | *.cover
56 | *.py,cover
57 | .hypothesis/
58 | .pytest_cache/
59 | cover/
60 |
61 | # Translations
62 | *.mo
63 | *.pot
64 |
65 | # Django stuff:
66 | *.log
67 | local_settings.py
68 | db.sqlite3
69 | db.sqlite3-journal
70 | dev.db
71 |
72 | # Flask stuff:
73 | instance/
74 | .webassets-cache
75 |
76 | # Scrapy stuff:
77 | .scrapy
78 |
79 | # Sphinx documentation
80 | docs/_build/
81 |
82 | # PyBuilder
83 | .pybuilder/
84 | target/
85 |
86 | # Jupyter Notebook
87 | .ipynb_checkpoints
88 | */.ipynb_checkpoints/*
89 |
90 | # IPython
91 | profile_default/
92 | ipython_config.py
93 |
94 | # pyenv
95 | # For a library or package, you might want to ignore these files since the code is
96 | # intended to run in multiple environments; otherwise, check them in:
97 | # .python-version
98 |
99 | # pipenv
100 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
101 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
102 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
103 | # install all needed dependencies.
104 | #Pipfile.lock
105 |
106 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
107 | __pypackages__/
108 |
109 | # celery stuff
110 | celerybeat-schedule
111 | celerybeat.pid
112 |
113 | # SageMath parsed files
114 | *.sage.py
115 |
116 | # Environments
117 | .env
118 | .venv
119 | env/
120 | venv/
121 | ENV/
122 | env.bak/
123 | venv.bak/
124 |
125 | # Spyder project settings
126 | .spyderproject
127 | .spyproject
128 |
129 | # Rope project settings
130 | .ropeproject
131 |
132 | # mkdocs documentation
133 | /site
134 |
135 | # mypy
136 | .mypy_cache/
137 | .dmypy.json
138 | dmypy.json
139 |
140 | # Pyre type checker
141 | .pyre/
142 |
143 | # pytype static type analyzer
144 | .pytype/
145 |
146 | # Cython debug symbols
147 | cython_debug/
148 |
149 | # Mr Developer
150 | .mr.developer.cfg
151 | .project
152 | .pydevproject
153 |
154 | # Pycharm/Intellij
155 | .idea
156 |
157 | # VSCode
158 | .vscode
159 |
160 | # Docker
161 | docker-compose*
162 | **/docker-compose*
163 |
164 | # Complexity
165 | output/*.html
166 | output/*/index.html
167 |
168 | # Testing artifacts
169 | junit-*.xml
170 | flake8-errors.txt
171 | example/media/
172 |
173 | # Documentation building
174 | _build
175 | doc_src/api/categories*.rst
176 | docs
177 |
178 | RELEASE.txt
179 | site-packages
180 | reports
181 | test-reports/*
182 |
--------------------------------------------------------------------------------
/doc_src/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS = -a
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 | DESTDIR = _build
10 |
11 | # Internal variables.
12 | PAPEROPT_a4 = -D latex_paper_size=a4
13 | PAPEROPT_letter = -D latex_paper_size=letter
14 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
15 |
16 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
17 |
18 | help:
19 | @echo "Please use \`make ' where is one of"
20 | @echo " html to make standalone HTML files"
21 | @echo " dirhtml to make HTML files named index.html in directories"
22 | @echo " pickle to make pickle files"
23 | @echo " json to make JSON files"
24 | @echo " htmlhelp to make HTML files and a HTML help project"
25 | @echo " qthelp to make HTML files and a qthelp project"
26 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
27 | @echo " changes to make an overview of all changed/added/deprecated items"
28 | @echo " linkcheck to check all external links for integrity"
29 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
30 |
31 | clean:
32 | -rm -rf $(BUILDDIR)/*
33 |
34 | html:
35 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(DESTDIR)/html
36 | @echo
37 | @echo "Build finished. The HTML pages are in $(DESTDIR)/html."
38 |
39 | dirhtml:
40 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
41 | @echo
42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
43 |
44 | pickle:
45 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
46 | @echo
47 | @echo "Build finished; now you can process the pickle files."
48 |
49 | json:
50 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
51 | @echo
52 | @echo "Build finished; now you can process the JSON files."
53 |
54 | htmlhelp:
55 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
56 | @echo
57 | @echo "Build finished; now you can run HTML Help Workshop with the" \
58 | ".hhp project file in $(BUILDDIR)/htmlhelp."
59 |
60 | qthelp:
61 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
62 | @echo
63 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
64 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
65 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoCategories.qhcp"
66 | @echo "To view the help file:"
67 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoCategories.qhc"
68 |
69 | latex:
70 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
71 | @echo
72 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
73 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
74 | "run these through (pdf)latex."
75 |
76 | changes:
77 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
78 | @echo
79 | @echo "The overview file is in $(BUILDDIR)/changes."
80 |
81 | linkcheck:
82 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
83 | @echo
84 | @echo "Link check complete; look for any errors in the above output " \
85 | "or in $(BUILDDIR)/linkcheck/output.txt."
86 |
87 | doctest:
88 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
89 | @echo "Testing of doctests in the sources finished, look at the " \
90 | "results in $(BUILDDIR)/doctest/output.txt."
91 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean clean-test clean-pyc clean-build docs help
2 | .DEFAULT_GOAL := help
3 |
4 | RELEASE_KIND := patch
5 | SOURCE_DIR := categories
6 |
7 | BRANCH_NAME := $(shell echo $$(git rev-parse --abbrev-ref HEAD))
8 | SHORT_BRANCH_NAME := $(shell echo $(BRANCH_NAME) | cut -c 1-20)
9 | PRIMARY_BRANCH_NAME := master
10 | BUMPVERSION_OPTS :=
11 |
12 | EDIT_CHANGELOG_IF_EDITOR_SET := @bash -c "$(shell if [[ -n $$EDITOR ]] ; then echo "$$EDITOR CHANGELOG.md" ; else echo "" ; fi)"
13 |
14 | help:
15 | @grep '^[a-zA-Z]' $(MAKEFILE_LIST) | sort | awk -F ':.*?## ' 'NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}'
16 |
17 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
18 |
19 | clean-build: ## remove build artifacts
20 | rm -fr build/
21 | rm -fr dist/
22 | rm -fr .eggs/
23 | find . -name '*.egg-info' -exec rm -fr {} +
24 | find . -name '*.egg' -exec rm -f {} +
25 |
26 | clean-pyc: ## remove Python file artifacts
27 | find . -name '*.pyc' -exec rm -f {} +
28 | find . -name '*.pyo' -exec rm -f {} +
29 | find . -name '*~' -exec rm -f {} +
30 | find . -name '__pycache__' -exec rm -fr {} +
31 |
32 | clean-test: ## remove test and coverage artifacts
33 | rm -fr .tox/
34 | rm -f .coverage
35 | rm -fr htmlcov/
36 | rm -fr .pytest_cache
37 |
38 | deps: ## Install development dependencies
39 | pip install -r requirements.txt
40 | pip install tox sphinx sphinx-autobuild twine
41 |
42 | test: ## Run tests
43 | tox
44 |
45 | publish: ## Publish a release to PyPi (requires permissions)
46 | rm -fr build dist
47 | python setup.py sdist bdist_wheel
48 | twine upload dist/*
49 |
50 | docs: ## generate Sphinx HTML documentation, including API docs
51 | mkdir -p docs
52 | rm -f doc_src/api/$(SOURCE_DIR)*.rst
53 | ls -A1 docs | xargs -I {} rm -rf docs/{}
54 | $(MAKE) -C doc_src clean html
55 | cp -a doc_src/_build/html/. docs
56 |
57 | pubdocs: docs ## Publish the documentation to GitHub
58 | ghp-import -op docs
59 |
60 | release-dev: RELEASE_KIND := dev
61 | release-dev: do-release ## Release a new development version: 1.1.1 -> 1.1.1+branchname-1
62 |
63 | release-patch: RELEASE_KIND := patch
64 | release-patch: do-release ## Release a new patch version: 1.1.1 -> 1.1.2
65 |
66 | release-minor: RELEASE_KIND := minor
67 | release-minor: do-release ## Release a new minor version: 1.1.1 -> 1.2.0
68 |
69 | release-major: RELEASE_KIND := major
70 | release-major: do-release ## Release a new major version: 1.1.1 -> 2.0.0
71 |
72 | release-version: get-version do-release ## Release a specific version: release-version 1.2.3
73 |
74 | #
75 | # Helper targets. Not meant to use directly
76 | #
77 |
78 | do-release:
79 | @if [[ "$(BRANCH_NAME)" == "$(PRIMARY_BRANCH_NAME)" ]]; then \
80 | if [[ "$(RELEASE_KIND)" == "dev" ]]; then \
81 | echo "Error! Can't bump $(RELEASE_KIND) while on the $(PRIMARY_BRANCH_NAME) branch."; \
82 | exit; \
83 | fi; \
84 | elif [[ "$(RELEASE_KIND)" != "dev" ]]; then \
85 | echo "Error! Must be on the $(PRIMARY_BRANCH_NAME) branch to bump $(RELEASE_KIND)."; \
86 | exit; \
87 | fi; \
88 | git fetch -p --all; \
89 | generate-changelog; \
90 | export BRANCH_NAME=$(SHORT_BRANCH_NAME);bumpversion $(BUMPVERSION_OPTS) $(RELEASE_KIND) --allow-dirty; \
91 | git push origin $(BRANCH_NAME); \
92 | git push --tags;
93 |
94 | get-version: # Sets the value after release-version to the VERSION
95 | $(eval VERSION := $(filter-out release-version,$(MAKECMDGOALS)))
96 | $(eval BUMPVERSION_OPTS := --new-version=$(VERSION))
97 |
98 | %: # NO-OP for unrecognized rules
99 | @:
100 |
--------------------------------------------------------------------------------
/categories/tests/test_views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import AnonymousUser
2 | from django.http import Http404
3 | from django.test import Client, RequestFactory, TestCase
4 |
5 | from categories import views
6 | from categories.models import Category, CategoryRelation
7 |
8 |
9 | class MyCategoryRelationView(views.CategoryRelatedDetail):
10 | model = CategoryRelation
11 |
12 |
13 | class TestCategoryViews(TestCase):
14 | fixtures = [
15 | "musicgenres.json",
16 | ]
17 |
18 | def setUp(self):
19 | self.client = Client()
20 | self.factory = RequestFactory()
21 |
22 | def test_category_detail(self):
23 | cat0 = Category.objects.get(slug="country", level=0)
24 | cat1 = cat0.children.get(slug="country-pop")
25 | cat2 = Category.objects.get(slug="urban-cowboy")
26 | url = cat0.get_absolute_url()
27 | response = self.client.get(url)
28 | self.assertEqual(response.status_code, 200)
29 | url = cat1.get_absolute_url()
30 | response = self.client.get(url)
31 | self.assertEqual(response.status_code, 200)
32 | url = cat2.get_absolute_url()
33 | response = self.client.get(url)
34 | self.assertEqual(response.status_code, 200)
35 | response = self.client.get("%sfoo/" % url)
36 | self.assertEqual(response.status_code, 404)
37 |
38 | def test_get_category_for_path(self):
39 | cat0 = Category.objects.get(slug="country", level=0)
40 | cat1 = cat0.children.get(slug="country-pop")
41 | cat2 = Category.objects.get(slug="urban-cowboy")
42 |
43 | result = views.get_category_for_path("/country/country-pop/urban-cowboy/")
44 | self.assertEqual(result, cat2)
45 | result = views.get_category_for_path("/country/country-pop/")
46 | self.assertEqual(result, cat1)
47 | result = views.get_category_for_path("/country/")
48 | self.assertEqual(result, cat0)
49 |
50 | def test_categorydetailview(self):
51 | request = self.factory.get("")
52 | request.user = AnonymousUser()
53 | self.assertRaises(AttributeError, views.CategoryDetailView.as_view(), request)
54 |
55 | request = self.factory.get("")
56 | request.user = AnonymousUser()
57 | response = views.CategoryDetailView.as_view()(request, path="/country/country-pop/urban-cowboy/")
58 | self.assertEqual(response.status_code, 200)
59 |
60 | request = self.factory.get("")
61 | request.user = AnonymousUser()
62 | self.assertRaises(Http404, views.CategoryDetailView.as_view(), request, path="/country/country-pop/foo/")
63 |
64 | def test_categoryrelateddetailview(self):
65 | from simpletext.models import SimpleText
66 |
67 | stext = SimpleText.objects.create(name="Test", description="test description")
68 | cat = Category.objects.get(slug="urban-cowboy")
69 | cat_rel = CategoryRelation.objects.create(category=cat, content_object=stext) # NOQA
70 | request = self.factory.get("")
71 | request.user = AnonymousUser()
72 | self.assertRaises(AttributeError, MyCategoryRelationView.as_view(), request)
73 |
74 | request = self.factory.get("")
75 | request.user = AnonymousUser()
76 | response = MyCategoryRelationView.as_view()(request, category_path="/country/country-pop/urban-cowboy/")
77 | self.assertEqual(response.status_code, 200)
78 |
79 | request = self.factory.get("")
80 | request.user = AnonymousUser()
81 | self.assertRaises(
82 | Http404, MyCategoryRelationView.as_view(), request, category_path="/country/country-pop/foo/"
83 | )
84 |
--------------------------------------------------------------------------------
/doc_src/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | set SPHINXBUILD=sphinx-build
6 | set BUILDDIR=_build
7 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
8 | if NOT "%PAPER%" == "" (
9 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
10 | )
11 |
12 | if "%1" == "" goto help
13 |
14 | if "%1" == "help" (
15 | :help
16 | echo.Please use `make ^` where ^ is one of
17 | echo. html to make standalone HTML files
18 | echo. dirhtml to make HTML files named index.html in directories
19 | echo. pickle to make pickle files
20 | echo. json to make JSON files
21 | echo. htmlhelp to make HTML files and a HTML help project
22 | echo. qthelp to make HTML files and a qthelp project
23 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
24 | echo. changes to make an overview over all changed/added/deprecated items
25 | echo. linkcheck to check all external links for integrity
26 | echo. doctest to run all doctests embedded in the documentation if enabled
27 | goto end
28 | )
29 |
30 | if "%1" == "clean" (
31 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
32 | del /q /s %BUILDDIR%\*
33 | goto end
34 | )
35 |
36 | if "%1" == "html" (
37 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
38 | echo.
39 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
40 | goto end
41 | )
42 |
43 | if "%1" == "dirhtml" (
44 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
45 | echo.
46 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
47 | goto end
48 | )
49 |
50 | if "%1" == "pickle" (
51 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
52 | echo.
53 | echo.Build finished; now you can process the pickle files.
54 | goto end
55 | )
56 |
57 | if "%1" == "json" (
58 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
59 | echo.
60 | echo.Build finished; now you can process the JSON files.
61 | goto end
62 | )
63 |
64 | if "%1" == "htmlhelp" (
65 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
66 | echo.
67 | echo.Build finished; now you can run HTML Help Workshop with the ^
68 | .hhp project file in %BUILDDIR%/htmlhelp.
69 | goto end
70 | )
71 |
72 | if "%1" == "qthelp" (
73 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
74 | echo.
75 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
76 | .qhcp project file in %BUILDDIR%/qthelp, like this:
77 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DjangoCategories.qhcp
78 | echo.To view the help file:
79 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DjangoCategories.ghc
80 | goto end
81 | )
82 |
83 | if "%1" == "latex" (
84 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
85 | echo.
86 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
87 | goto end
88 | )
89 |
90 | if "%1" == "changes" (
91 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
92 | echo.
93 | echo.The overview file is in %BUILDDIR%/changes.
94 | goto end
95 | )
96 |
97 | if "%1" == "linkcheck" (
98 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
99 | echo.
100 | echo.Link check complete; look for any errors in the above output ^
101 | or in %BUILDDIR%/linkcheck/output.txt.
102 | goto end
103 | )
104 |
105 | if "%1" == "doctest" (
106 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
107 | echo.
108 | echo.Testing of doctests in the sources finished, look at the ^
109 | results in %BUILDDIR%/doctest/output.txt.
110 | goto end
111 | )
112 |
113 | :end
114 |
--------------------------------------------------------------------------------
/categories/tests/test_templatetags.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from django import template
4 | from django.test import TestCase
5 |
6 | from categories.models import Category
7 |
8 |
9 | class CategoryTagsTest(TestCase):
10 | fixtures = ["musicgenres.json"]
11 |
12 | def render_template(self, template_string, context={}):
13 | """
14 | Return the rendered string or raise an exception.
15 | """
16 | tpl = template.Template(template_string)
17 | ctxt = template.Context(context)
18 | return tpl.render(ctxt)
19 |
20 | def testTooFewArguments(self):
21 | """
22 | Ensure that get_category raises an exception if there aren't enough arguments.
23 | """
24 | self.assertRaises(
25 | template.TemplateSyntaxError, self.render_template, "{% load category_tags %}{% get_category %}"
26 | )
27 |
28 | def testBasicUsage(self):
29 | """
30 | Test that we can properly retrieve a category.
31 | """
32 | # display_path_as_ul
33 | rock_resp = ''
34 | resp = self.render_template('{% load category_tags %}{% display_path_as_ul "/Rock" %}')
35 | resp = re.sub(r"\n$", "", resp)
36 | self.assertEqual(resp, rock_resp)
37 |
38 | # display_drilldown_as_ul
39 | expected_resp = ''
40 | resp = self.render_template(
41 | "{% load category_tags %}" '{% display_drilldown_as_ul "/World/Worldbeat" "categories.category" %}'
42 | )
43 | resp = re.sub(r"\n$", "", resp)
44 | self.assertEqual(resp, expected_resp)
45 |
46 | # breadcrumbs
47 | expected_resp = 'World > Worldbeat\n'
48 | resp = self.render_template(
49 | "{% load category_tags %}" '{% breadcrumbs "/World/Worldbeat" " > " "categories.category" %}'
50 | )
51 | self.assertEqual(resp, expected_resp)
52 |
53 | # get_top_level_categories
54 | expected_resp = "Avant-garde|Blues|Country|Easy listening|Electronic|Hip hop/Rap music|Jazz|Latin|Modern folk|Pop|Reggae|Rhythm and blues|Rock|World|"
55 | resp = self.render_template(
56 | "{% load category_tags %}"
57 | '{% get_top_level_categories using "categories.category" as varname %}'
58 | "{% for item in varname %}{{ item }}|{% endfor %}"
59 | )
60 | self.assertEqual(resp, expected_resp)
61 |
62 | # get_category_drilldown
63 | expected_resp = "World|World > Worldbeat|"
64 | resp = self.render_template(
65 | "{% load category_tags %}"
66 | '{% get_category_drilldown "/World" using "categories.category" as var %}'
67 | "{% for item in var %}{{ item }}|{% endfor %}"
68 | )
69 | self.assertEqual(resp, expected_resp)
70 |
71 | # recursetree
72 | expected_resp = ""
73 | ctxt = {"nodes": Category.objects.filter(name__in=("Worldbeat", "Urban Cowboy"))}
74 | resp = self.render_template(
75 | "{% load category_tags %}"
76 | "{% recursetree nodes|tree_queryset %}- {{ node.name }}"
77 | "{% if not node.is_leaf_node %}{% endif %}
{% endrecursetree %}
",
79 | ctxt,
80 | )
81 | self.assertEqual(resp, expected_resp)
82 |
--------------------------------------------------------------------------------
/doc_src/reference/models.rst:
--------------------------------------------------------------------------------
1 | ======
2 | Models
3 | ======
4 |
5 | CategoryBase
6 | ============
7 |
8 | .. py:class:: CategoryBase
9 |
10 | .. py:attribute:: parent
11 |
12 | :py:class:`TreeForeignKey` ``(self)``
13 |
14 | The category's parent category. Leave this blank for an root category.
15 |
16 | .. py:attribute:: name
17 |
18 | **Required** ``CharField(100)``
19 |
20 | The name of the category.
21 |
22 | .. py:attribute:: slug
23 |
24 | **Required** ``SlugField``
25 |
26 | URL-friendly title. It is automatically generated from the title.
27 |
28 | .. py:attribute:: active
29 |
30 | **Required** ``BooleanField`` *default:* ``True``
31 |
32 | Is this item active. If it is inactive, all children are set to inactive as well.
33 |
34 | .. py:attribute:: objects
35 |
36 | ``CategoryManager``
37 |
38 | An object manager that adds an ``active`` method for only selecting items whose ``active`` attribute is ``True``.
39 |
40 | .. py:attribute:: tree
41 |
42 | ``TreeManager``
43 |
44 | A Django-MPTT `TreeManager `_ instance.
45 |
46 | Category
47 | ========
48 |
49 | .. py:class:: Category
50 |
51 | Category is a subclass of :py:class:`CategoryBase` and includes all its attributes.
52 |
53 | .. py:attribute:: thumbnail
54 |
55 | ``FileField``
56 |
57 | An optional thumbnail, that is uploaded to :ref:`thumbnail_upload_path` via :ref:`THUMBNAIL_STORAGE`.
58 |
59 | .. note:: Why isn't this an ``ImageField``?
60 |
61 | For ``ImageField``\ s, Django checks the file system for the existance of the files to handle the height and width. In many cases this can lead to problems and impact performance.
62 |
63 | For these reasons, a ``FileField`` that manually manages the width and height was chosen.
64 |
65 | .. py:attribute:: thumbnail_width
66 |
67 | ``IntegerField``
68 |
69 | The thumbnail width. Automatically set on save if a thumbnail is uploaded.
70 |
71 | .. py:attribute:: thumbnail_height
72 |
73 | ``IntegerField``
74 |
75 | The thumbnail height. Automatically set on save if a thumbnail is uploaded.
76 |
77 | .. py:attribute:: order
78 |
79 | **Required** ``IntegerField`` *default:* 0
80 |
81 | A manually-managed order of this category in the listing. Items with the same order are sorted alphabetically.
82 |
83 | .. py:attribute:: alternate_title
84 |
85 | ``CharField(100)``
86 |
87 | An alternative title to use on pages with this category.
88 |
89 | .. py:attribute:: alternate_url
90 |
91 | ``CharField(200)``
92 |
93 | An alternative URL to use instead of the one derived from the category hierarchy.
94 |
95 | .. note:: Why isn't this a ``URLField``?
96 |
97 | For ``URLField``\ s, Django checks that the URL includes ``http://`` and the site name. This makes it impossible to use relative URLs in that field.
98 |
99 | .. py:attribute:: description
100 |
101 | ``TextField``
102 |
103 | An optional longer description of the category. Very useful on category landing pages.
104 |
105 | .. py:attribute:: meta_keywords
106 |
107 | ``CharField(255)``
108 |
109 | Comma-separated keywords for search engines.
110 |
111 | .. py:attribute:: meta_extra
112 |
113 | ``TextField``
114 |
115 | (Advanced) Any additional HTML to be placed verbatim in the ```` of the page.
116 |
--------------------------------------------------------------------------------
/.changelog-config.yaml:
--------------------------------------------------------------------------------
1 | # For more configuration information, please see https://coordt.github.io/generate-changelog/
2 |
3 | # User variables for reference in other parts of the configuration.
4 | variables:
5 | repo_url: https://github.com/jazzband/django-categories
6 | changelog_filename: CHANGELOG.md
7 |
8 | # Pipeline to find the most recent tag for incremental changelog generation.
9 | # Leave empty to always start at first commit.
10 | starting_tag_pipeline:
11 | - action: ReadFile
12 | kwargs:
13 | filename: '{{ changelog_filename }}'
14 | - action: FirstRegExMatch
15 | kwargs:
16 | pattern: (?im)^## (?P\d+\.\d+(?:\.\d+)?)\s+\(\d+-\d{2}-\d{2}\)$
17 | named_subgroup: rev
18 |
19 | # Used as the version title of the changes since the last valid tag.
20 | unreleased_label: Unreleased
21 |
22 | # Process the commit's first line for use in the changelog.
23 | summary_pipeline:
24 | - action: strip_spaces
25 | - action: Strip
26 | comment: Get rid of any periods so we don't get double periods
27 | kwargs:
28 | chars: .
29 | - action: SetDefault
30 | args:
31 | - no commit message
32 | - action: capitalize
33 | - action: append_dot
34 |
35 | # Process the commit's body for use in the changelog.
36 | body_pipeline:
37 | - action: ParseTrailers
38 | comment: Parse the trailers into metadata.
39 | kwargs:
40 | commit_metadata: save_commit_metadata
41 |
42 | # Process and store the full or partial changelog.
43 | output_pipeline:
44 | - action: IncrementalFileInsert
45 | kwargs:
46 | filename: '{{ changelog_filename }}'
47 | last_heading_pattern: (?im)^## \d+\.\d+(?:\.\d+)?\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)$
48 |
49 | # Full or relative paths to look for output generation templates.
50 | template_dirs:
51 | - .github/changelog_templates/
52 |
53 | # Group the commits within a version by these commit attributes.
54 | group_by:
55 | - metadata.category
56 |
57 | # Only tags matching this regular expression are used for the changelog.
58 | tag_pattern: ^[0-9]+\.[0-9]+(?:\.[0-9]+)?$
59 |
60 | # Tells ``git-log`` whether to include merge commits in the log.
61 | include_merges: false
62 |
63 | # Ignore commits whose summary line matches any of these regular expression patterns.
64 | ignore_patterns:
65 | - '[@!]minor'
66 | - '[@!]cosmetic'
67 | - '[@!]refactor'
68 | - '[@!]wip'
69 | - ^$
70 | - ^Merge branch
71 | - ^Merge pull
72 |
73 | # Set the commit's category metadata to the first classifier that returns ``True``.
74 | commit_classifiers:
75 | - action: SummaryRegexMatch
76 | category: New
77 | kwargs:
78 | pattern: (?i)^(?:new|add)[^\n]*$
79 | - action: SummaryRegexMatch
80 | category: Updates
81 | kwargs:
82 | pattern: (?i)^(?:update|change|rename|remove|delete|improve|refactor|chg|modif)[^\n]*$
83 | - action: SummaryRegexMatch
84 | category: Fixes
85 | kwargs:
86 | pattern: (?i)^(?:fix)[^\n]*$
87 | - action:
88 | category: Other
89 |
90 | # Tokens in git commit trailers that indicate authorship.
91 | valid_author_tokens:
92 | - author
93 | - based-on-a-patch-by
94 | - based-on-patch-by
95 | - co-authored-by
96 | - co-committed-by
97 | - contributions-by
98 | - from
99 | - helped-by
100 | - improved-by
101 | - original-patch-by
102 |
103 | # Rules applied to commits to determine the type of release to suggest.
104 | release_hint_rules:
105 | - match_result: patch
106 | no_match_result: no-release
107 | grouping: Other
108 | - match_result: patch
109 | no_match_result: no-release
110 | grouping: Fixes
111 | - match_result: minor
112 | no_match_result: no-release
113 | grouping: Updates
114 | - match_result: minor
115 | no_match_result:
116 | grouping: New
117 | - match_result: major
118 | no_match_result:
119 | grouping: Breaking Changes
120 |
--------------------------------------------------------------------------------
/categories/admin.py:
--------------------------------------------------------------------------------
1 | """Admin interface classes."""
2 |
3 | from django import forms
4 | from django.contrib import admin
5 | from django.utils.translation import gettext_lazy as _
6 |
7 | from .base import CategoryBaseAdmin, CategoryBaseAdminForm
8 | from .genericcollection import GenericCollectionTabularInline
9 | from .models import Category
10 | from .settings import JAVASCRIPT_URL, MODEL_REGISTRY, REGISTER_ADMIN, RELATION_MODELS
11 |
12 |
13 | class NullTreeNodeChoiceField(forms.ModelChoiceField):
14 | """A ModelChoiceField for tree nodes."""
15 |
16 | def __init__(self, level_indicator="---", *args, **kwargs):
17 | self.level_indicator = level_indicator
18 | super(NullTreeNodeChoiceField, self).__init__(*args, **kwargs)
19 |
20 | def label_from_instance(self, obj) -> str:
21 | """
22 | Creates labels which represent the tree level of each node when generating option labels.
23 | """
24 | return "%s %s" % (self.level_indicator * getattr(obj, obj._mptt_meta.level_attr), obj)
25 |
26 |
27 | if RELATION_MODELS:
28 | from .models import CategoryRelation
29 |
30 | class InlineCategoryRelation(GenericCollectionTabularInline):
31 | """The inline admin panel for category relations."""
32 |
33 | model = CategoryRelation
34 |
35 |
36 | class CategoryAdminForm(CategoryBaseAdminForm):
37 | """The form for a category in the admin."""
38 |
39 | class Meta:
40 | model = Category
41 | fields = "__all__"
42 |
43 | def clean_alternate_title(self) -> str:
44 | """Return either the name or alternate title for the category."""
45 | if self.instance is None or not self.cleaned_data["alternate_title"]:
46 | return self.cleaned_data["name"]
47 | else:
48 | return self.cleaned_data["alternate_title"]
49 |
50 |
51 | class CategoryAdmin(CategoryBaseAdmin):
52 | """Admin for categories."""
53 |
54 | form = CategoryAdminForm
55 | list_display = ("name", "alternate_title", "active")
56 | fieldsets = (
57 | (None, {"fields": ("parent", "name", "thumbnail", "active")}),
58 | (
59 | _("Meta Data"),
60 | {
61 | "fields": ("alternate_title", "alternate_url", "description", "meta_keywords", "meta_extra"),
62 | "classes": ("collapse",),
63 | },
64 | ),
65 | (
66 | _("Advanced"),
67 | {
68 | "fields": ("order", "slug"),
69 | "classes": ("collapse",),
70 | },
71 | ),
72 | )
73 | autocomplete_fields = ("parent",)
74 |
75 | if RELATION_MODELS:
76 | inlines = [
77 | InlineCategoryRelation,
78 | ]
79 |
80 | class Media:
81 | js = (JAVASCRIPT_URL + "genericcollections.js",)
82 |
83 |
84 | if REGISTER_ADMIN:
85 | admin.site.register(Category, CategoryAdmin)
86 |
87 | for model, modeladmin in list(admin.site._registry.items()):
88 | if model in list(MODEL_REGISTRY.values()) and modeladmin.fieldsets:
89 | fieldsets = getattr(modeladmin, "fieldsets", ())
90 | fields = [cat.split(".")[2] for cat in MODEL_REGISTRY if MODEL_REGISTRY[cat] == model]
91 | # check each field to see if already defined
92 | for cat in fields:
93 | for k, v in fieldsets:
94 | if cat in v["fields"]:
95 | fields.remove(cat)
96 | # if there are any fields left, add them under the categories fieldset
97 | if len(fields) > 0:
98 | admin.site.unregister(model)
99 | admin.site.register(
100 | model,
101 | type(
102 | "newadmin",
103 | (modeladmin.__class__,),
104 | {"fieldsets": fieldsets + (("Categories", {"fields": fields}),)},
105 | ),
106 | )
107 |
--------------------------------------------------------------------------------
/example/settings.py:
--------------------------------------------------------------------------------
1 | """Django settings for sample project."""
2 |
3 | import os
4 | import sys
5 |
6 | import django
7 | from django.db import models
8 |
9 | APP = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
10 | PROJ_ROOT = os.path.abspath(os.path.dirname(__file__))
11 | sys.path.insert(0, APP)
12 | DEBUG = True
13 |
14 | ADMINS = (
15 | # ('Your Name', 'your_email@domain.com'),
16 | )
17 |
18 | MANAGERS = ADMINS
19 |
20 | DATABASES = {
21 | "default": {
22 | "ENGINE": "django.db.backends.sqlite3",
23 | "NAME": "dev.db",
24 | "USER": "",
25 | "PASSWORD": "",
26 | "HOST": "",
27 | "PORT": "",
28 | }
29 | }
30 |
31 | INSTALLED_APPS = (
32 | "django.contrib.admin",
33 | "django.contrib.auth",
34 | "django.contrib.contenttypes",
35 | "django.contrib.sessions",
36 | "django.contrib.sites",
37 | "django.contrib.messages",
38 | "django.contrib.staticfiles",
39 | "django.contrib.flatpages",
40 | "categories",
41 | "categories.editor",
42 | "mptt",
43 | "simpletext",
44 | )
45 |
46 | TIME_ZONE = "America/Chicago"
47 |
48 | LANGUAGE_CODE = "en-us"
49 |
50 | SITE_ID = 1
51 |
52 | USE_I18N = True
53 |
54 | MEDIA_ROOT = os.path.abspath(os.path.join(PROJ_ROOT, "media", "uploads"))
55 |
56 | MEDIA_URL = "/uploads/"
57 |
58 | STATIC_ROOT = os.path.abspath(os.path.join(PROJ_ROOT, "media", "static"))
59 |
60 | STATIC_URL = "/static/"
61 |
62 | STATICFILES_DIRS = ()
63 |
64 | STATICFILES_FINDERS = (
65 | "django.contrib.staticfiles.finders.FileSystemFinder",
66 | "django.contrib.staticfiles.finders.AppDirectoriesFinder",
67 | )
68 |
69 | SECRET_KEY = "bwq#m)-zsey-fs)0#4*o=2z(v5g!ei=zytl9t-1hesh4b&-u^d"
70 |
71 | MIDDLEWARE = (
72 | "django.middleware.security.SecurityMiddleware",
73 | "django.contrib.sessions.middleware.SessionMiddleware",
74 | "django.middleware.common.CommonMiddleware",
75 | "django.middleware.csrf.CsrfViewMiddleware",
76 | "django.contrib.auth.middleware.AuthenticationMiddleware",
77 | "django.contrib.messages.middleware.MessageMiddleware",
78 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
79 | )
80 |
81 | ROOT_URLCONF = "urls"
82 |
83 | TEMPLATES = [
84 | {
85 | "BACKEND": "django.template.backends.django.DjangoTemplates",
86 | "APP_DIRS": True,
87 | "DIRS": [os.path.abspath(os.path.join(os.path.dirname(__file__), "templates"))],
88 | "OPTIONS": {
89 | "debug": DEBUG,
90 | "context_processors": [
91 | "django.contrib.auth.context_processors.auth",
92 | "django.template.context_processors.debug",
93 | "django.template.context_processors.i18n",
94 | "django.template.context_processors.media",
95 | "django.template.context_processors.static",
96 | "django.template.context_processors.tz",
97 | "django.contrib.messages.context_processors.messages",
98 | ],
99 | },
100 | }
101 | ]
102 |
103 |
104 | CATEGORIES_SETTINGS = {
105 | "ALLOW_SLUG_CHANGE": True,
106 | "RELATION_MODELS": ["simpletext.simpletext", "flatpages.flatpage"],
107 | "FK_REGISTRY": {
108 | "flatpages.flatpage": ("category", {"on_delete": models.CASCADE}),
109 | "simpletext.simpletext": (
110 | "primary_category",
111 | {"name": "secondary_category", "related_name": "simpletext_sec_cat"},
112 | ),
113 | },
114 | "M2M_REGISTRY": {
115 | "simpletext.simpletext": {"name": "categories", "related_name": "m2mcats"},
116 | "flatpages.flatpage": (
117 | {"name": "other_categories", "related_name": "other_cats"},
118 | {"name": "more_categories", "related_name": "more_cats"},
119 | ),
120 | },
121 | }
122 |
123 | if django.VERSION[1] > 5:
124 | TEST_RUNNER = "django.test.runner.DiscoverRunner"
125 |
--------------------------------------------------------------------------------
/doc_src/reference/settings.rst:
--------------------------------------------------------------------------------
1 | .. _reference_settings:
2 |
3 | ========
4 | Settings
5 | ========
6 |
7 | The ``CATEGORIES_SETTINGS`` dictionary is where you can override the default settings. You don't have to include all the settings; only the ones which you want to override.
8 |
9 | .. contents::
10 | :local:
11 |
12 |
13 | The default settings are:
14 |
15 | .. code-block:: python
16 |
17 | CATEGORIES_SETTINGS = {
18 | 'ALLOW_SLUG_CHANGE': False,
19 | 'CACHE_VIEW_LENGTH': 0,
20 | 'RELATION_MODELS': [],
21 | 'M2M_REGISTRY': {},
22 | 'FK_REGISTRY': {},
23 | 'THUMBNAIL_UPLOAD_PATH': 'uploads/categories/thumbnails',
24 | 'THUMBNAIL_STORAGE': settings.DEFAULT_FILE_STORAGE,
25 | 'THUMBNAIL_STORAGE_ALIAS': 'default',
26 | 'SLUG_TRANSLITERATOR': lambda x: x,
27 | 'ADMIN_FIELDSETS': {}
28 | }
29 |
30 |
31 | .. _ALLOW_SLUG_CHANGE:
32 |
33 | ALLOW_SLUG_CHANGE
34 | =================
35 |
36 | **Default:** ``False``
37 |
38 | **Description:** Changing the slug for a category can have serious consequences if it is used as part of a URL. Setting this to ``True`` will allow users to change the slug of a category.
39 |
40 | .. _SLUG_TRANSLITERATOR:
41 |
42 | SLUG_TRANSLITERATOR
43 | ===================
44 |
45 | **Default:** ``lambda x: x``
46 |
47 | **Description:** Allows the specification of a function to convert non-ASCII characters in the potential slug to ASCII characters. Allows specifying a ``callable()`` or a string in the form of ``'path.to.module.function'``.
48 |
49 | A great tool for this is `Unidecode `_. Use it by setting ``SLUG_TRANSLITERATOR`` to ``'unidecode.unidecode``.
50 |
51 |
52 | .. _CACHE_VIEW_LENGTH:
53 |
54 | CACHE_VIEW_LENGTH
55 | =================
56 |
57 | **Default:** ``0``
58 |
59 | **Description:** This setting will be deprecated soon, but in the mean time, it allows you to specify the amount of time each view result is cached.
60 |
61 | .. _RELATION_MODELS:
62 |
63 | RELATION_MODELS
64 | ===============
65 |
66 | **Default:** ``[]``
67 |
68 | **Description:** Relation models is a set of models that a user can associate with this category. You specify models using ``'app_name.modelname'`` syntax.
69 |
70 | .. _M2M_REGISTRY:
71 |
72 | M2M_REGISTRY
73 | ============
74 |
75 | **Default:** {}
76 |
77 | **Description:** A dictionary where the keys are in ``'app_name.model_name'`` syntax, and the values are a string, dict, or tuple of dicts. See :ref:`registering_models`\ .
78 |
79 | .. _FK_REGISTRY:
80 |
81 | FK_REGISTRY
82 | ============
83 |
84 | **Default:** {}
85 |
86 | **Description:** A dictionary where the keys are in ``'app_name.model_name'`` syntax, and the values are a string, dict, or tuple of dicts. See :ref:`registering_models`\ .
87 |
88 | .. _THUMBNAIL_UPLOAD_PATH:
89 |
90 | .. _REGISTER_ADMIN:
91 |
92 | REGISTER_ADMIN
93 | ==============
94 |
95 | **Default:** ``True``
96 |
97 | **Description:** If you write your own category class by subclassing ``CategoryBase`` then you probably have no use for registering the default ``Category`` class in the admin.
98 |
99 |
100 | THUMBNAIL_UPLOAD_PATH
101 | =====================
102 |
103 | **Default:** ``'uploads/categories/thumbnails'``
104 |
105 | **Description:** Where thumbnails for the categories will be saved.
106 |
107 | .. _THUMBNAIL_STORAGE:
108 |
109 | THUMBNAIL_STORAGE
110 | =================
111 |
112 | **Default:** ``settings.DEFAULT_FILE_STORAGE``
113 |
114 | **Description:** How to store the thumbnails. Allows for external storage engines like S3.
115 |
116 | .. _THUMBNAIL_STORAGE:
117 |
118 | THUMBNAIL_STORAGE_ALIAS
119 | =======================
120 |
121 | **Default:** ``default``
122 |
123 | **Description:** If new STORAGES settings from Django 4.2+ is used, use storage with this alias.
124 |
125 | .. _JAVASCRIPT_URL:
126 |
127 | JAVASCRIPT_URL
128 | ==============
129 |
130 | **Default:** ``STATIC_URL or MEDIA_URL + 'js/'``
131 |
132 | **Description:** Allows for customization of javascript placement.
133 |
134 | .. _ADMIN_FIELDSETS:
135 |
136 | ADMIN_FIELDSETS
137 | ===============
138 |
139 | **Default:** ``{}``
140 |
141 | **Description:** Allows for selective customization of the default behavior of adding the fields to the admin class. See :ref:`admin_settings` for more information.
142 |
--------------------------------------------------------------------------------
/example/settings-testing.py:
--------------------------------------------------------------------------------
1 | # Django settings for sample project.
2 | import os
3 | import sys
4 |
5 | from django.db import models
6 |
7 | APP = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
8 | PROJ_ROOT = os.path.abspath(os.path.dirname(__file__))
9 | sys.path.insert(0, APP)
10 | DEBUG = True
11 |
12 | ADMINS = (
13 | # ('Your Name', 'your_email@domain.com'),
14 | )
15 |
16 | MANAGERS = ADMINS
17 |
18 | DATABASES = {
19 | "default": {
20 | "ENGINE": "django.db.backends.sqlite3",
21 | "NAME": "dev.db",
22 | "USER": "",
23 | "PASSWORD": "",
24 | "HOST": "",
25 | "PORT": "",
26 | }
27 | }
28 |
29 | INSTALLED_APPS = (
30 | "django.contrib.admin",
31 | "django.contrib.auth",
32 | "django.contrib.contenttypes",
33 | "django.contrib.sessions",
34 | "django.contrib.sites",
35 | "django.contrib.messages",
36 | "django.contrib.staticfiles",
37 | "django.contrib.flatpages",
38 | "categories",
39 | "categories.editor",
40 | "mptt",
41 | "simpletext",
42 | )
43 |
44 | TIME_ZONE = "America/Chicago"
45 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
46 | LANGUAGE_CODE = "en-us"
47 |
48 | SITE_ID = 1
49 |
50 | USE_I18N = True
51 |
52 | MEDIA_ROOT = os.path.abspath(os.path.join(PROJ_ROOT, "media", "uploads"))
53 |
54 | MEDIA_URL = "/uploads/"
55 |
56 | STATIC_ROOT = os.path.abspath(os.path.join(PROJ_ROOT, "media", "static"))
57 |
58 | STATIC_URL = "/static/"
59 |
60 | STATICFILES_DIRS = ()
61 |
62 | STATICFILES_FINDERS = (
63 | "django.contrib.staticfiles.finders.FileSystemFinder",
64 | "django.contrib.staticfiles.finders.AppDirectoriesFinder",
65 | )
66 |
67 | SECRET_KEY = "bwq#m)-zsey-fs)0#4*o=2z(v5g!ei=zytl9t-1hesh4b&-u^d"
68 |
69 | MIDDLEWARE = (
70 | "django.middleware.security.SecurityMiddleware",
71 | "django.contrib.sessions.middleware.SessionMiddleware",
72 | "django.middleware.common.CommonMiddleware",
73 | "django.middleware.csrf.CsrfViewMiddleware",
74 | "django.contrib.auth.middleware.AuthenticationMiddleware",
75 | "django.contrib.messages.middleware.MessageMiddleware",
76 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
77 | )
78 |
79 | ROOT_URLCONF = "urls"
80 |
81 | TEMPLATES = [
82 | {
83 | "BACKEND": "django.template.backends.django.DjangoTemplates",
84 | "APP_DIRS": True,
85 | "DIRS": [os.path.abspath(os.path.join(os.path.dirname(__file__), "templates"))],
86 | "OPTIONS": {
87 | "debug": DEBUG,
88 | "context_processors": [
89 | "django.contrib.auth.context_processors.auth",
90 | "django.template.context_processors.debug",
91 | "django.template.context_processors.i18n",
92 | "django.template.context_processors.media",
93 | "django.template.context_processors.static",
94 | "django.template.context_processors.tz",
95 | "django.contrib.messages.context_processors.messages",
96 | ],
97 | },
98 | },
99 | ]
100 |
101 |
102 | STORAGES = {
103 | "default": {
104 | "BACKEND": "django.core.files.storage.FileSystemStorage",
105 | },
106 | "staticfiles": {
107 | "BACKEND": "django.core.files.storage.FileSystemStorage",
108 | },
109 | "thumbnails": {
110 | "BACKEND": "example.test_storages.MyTestStorageAlias",
111 | },
112 | }
113 |
114 |
115 | CATEGORIES_SETTINGS = {
116 | "ALLOW_SLUG_CHANGE": True,
117 | "RELATION_MODELS": ["simpletext.simpletext", "flatpages.flatpage"],
118 | "FK_REGISTRY": {
119 | "flatpages.flatpage": ("category", {"on_delete": models.CASCADE}),
120 | "simpletext.simpletext": (
121 | "primary_category",
122 | {"name": "secondary_category", "related_name": "simpletext_sec_cat"},
123 | ),
124 | },
125 | "M2M_REGISTRY": {
126 | "simpletext.simpletext": {"name": "categories", "related_name": "m2mcats"},
127 | "flatpages.flatpage": (
128 | {"name": "other_categories", "related_name": "other_cats"},
129 | {"name": "more_categories", "related_name": "more_cats"},
130 | ),
131 | },
132 | "THUMBNAIL_STORAGE": "example.test_storages.MyTestStorage",
133 | "THUMBNAIL_STORAGE_ALIAS": "thumbnails",
134 | }
135 |
136 | TEST_RUNNER = "django.test.runner.DiscoverRunner"
137 |
--------------------------------------------------------------------------------
/categories/locale/de/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # German translation of django-categories
2 | # Copyright (C) 2012 winniehell
3 | # This file is distributed under the same license as django-categories.
4 | # winniehell , 2012.
5 | msgid ""
6 | msgstr ""
7 | "Project-Id-Version: django-categories 1.1.4\n"
8 | "Report-Msgid-Bugs-To: \n"
9 | "POT-Creation-Date: 2012-09-02 15:59+0200\n"
10 | "PO-Revision-Date: 2012-09-01 02:38+0200\n"
11 | "Last-Translator: winniehell \n"
12 | "Language-Team: winniehell \n"
13 | "Language: \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 | #: admin.py:50
20 | msgid "Meta Data"
21 | msgstr "Meta-Daten"
22 |
23 | #: admin.py:55
24 | msgid "Advanced"
25 | msgstr "Erweitert"
26 |
27 | #: base.py:41
28 | msgid "parent"
29 | msgstr "oberkategorie"
30 |
31 | #: base.py:42
32 | msgid "name"
33 | msgstr "name"
34 |
35 | #: base.py:43
36 | msgid "slug"
37 | msgstr "slug"
38 |
39 | #: base.py:44
40 | msgid "active"
41 | msgstr "aktiv"
42 |
43 | #: base.py:99
44 | msgid "The slug must be unique among the items at its level."
45 | msgstr "Der Slug muss eindeutig innerhalb der Kategorien einer Ebene sein."
46 |
47 | #: base.py:109
48 | msgid "You can't set the parent of the item to itself."
49 | msgstr "Eine Kategorie kann nicht Oberkategorie von sich selbst sein."
50 |
51 | #: base.py:112
52 | msgid "You can't set the parent of the item to a descendant."
53 | msgstr "Die Oberkategorie kann keine untergeordnete Kategorie sein."
54 |
55 | #: base.py:143
56 | msgid "Deactivate selected categories and their children"
57 | msgstr "Markierte Kategorien und ihre Unterkategorien deaktivieren"
58 |
59 | #: base.py:156
60 | msgid "Activate selected categories and their children"
61 | msgstr "Markierte Kategorien und ihre Unterkategorien aktivieren"
62 |
63 | #: migration.py:21 migration.py:91
64 | msgid "%(dependency) must be installed for this command to work"
65 | msgstr ""
66 | "%(dependency) muss installiert sein, damit dieses Kommando funktioniert"
67 |
68 | #: migration.py:43
69 | msgid "Added ForeignKey %(field_name) to %(model_name)"
70 | msgstr "Fremdschlüssel %(field_name) zu %(model_name) hinzugefügt"
71 |
72 | #: migration.py:49
73 | msgid "ForeignKey %(field_name) to %(model_name) already exists"
74 | msgstr "Fremdschlüssel %(field_name) zu %(model_name) existiert bereits"
75 |
76 | #: migration.py:66
77 | msgid "Added Many2Many table between %(model_name) and %(category_table)"
78 | msgstr "M:N-Beziehung zwischen %(model_name) und %(category_table) hinzugefügt"
79 |
80 | #: migration.py:72
81 | msgid ""
82 | "Many2Many table between %(model_name) and %(category_table) already exists"
83 | msgstr ""
84 | "M:N-Beziehung zwischen %(model_name) und %(category_table) existiert bereits"
85 |
86 | #: migration.py:98
87 | msgid "Dropping ForeignKey %(field_name) from %(model_name)"
88 | msgstr "Entferne Fremdschlüssel %(field_name) zu %(model_name)"
89 |
90 | #: migration.py:109
91 | msgid "Dropping Many2Many table between %(model_name) and %(category_table)"
92 | msgstr "Entferne M:N-Beziehung zwischen %(model_name) und %(category_table)"
93 |
94 | #: models.py:91 models.py:122
95 | msgid "category"
96 | msgstr "kategorie"
97 |
98 | #: models.py:92
99 | msgid "categories"
100 | msgstr "kategorien"
101 |
102 | #: models.py:124
103 | msgid "content type"
104 | msgstr "content type"
105 |
106 | #: models.py:125
107 | msgid "object id"
108 | msgstr "objekt-ID"
109 |
110 | #: models.py:127
111 | msgid "relation type"
112 | msgstr "relationstyp"
113 |
114 | #: models.py:131
115 | msgid "A generic text field to tag a relation, like 'leadphoto'."
116 | msgstr ""
117 | "Ein generisches Textfeld um eine Relation zu bezeichnen, z.B. 'leadphoto'"
118 |
119 | #: registration.py:45
120 | #, python-format
121 | msgid "%(key) is not a model"
122 | msgstr "%(key) ist kein Model"
123 |
124 | #: registration.py:54 registration.py:62
125 | msgid "%(settings) doesn't recognize the value of %(key)"
126 | msgstr "%(settings) erkennt den Wert von %(key) nicht"
127 |
128 | #: settings.py:33
129 | msgid "%(transliterator) must be a callable or a string."
130 | msgstr "%(transliterator) muss callable oder string sein"
131 |
132 | #: settings.py:39
133 | #, python-format
134 | msgid "%(deprecated_setting) is deprecated; use %(replacement)s instead."
135 | msgstr ""
136 | "%(deprecated_setting) ist veraltet und wurde durch %(replacement)s ersetzt."
137 |
138 | #: views.py:73
139 | msgid "Category detail view %(view) must be called with a %(path_field)."
140 | msgstr ""
141 | "Kategorie-Detailansicht %(view) muss ein %(path_field) übergeben bekommen."
142 |
143 | #: views.py:80
144 | #, python-format
145 | msgid "No %(verbose_name)s found matching the query"
146 | msgstr "Es wurde kein passendes Objekt für %(verbose_name)s gefunden"
147 |
148 | #: templates/admin/edit_inline/gen_coll_tabular.html:13
149 | msgid "Delete?"
150 | msgstr "Löschen?"
151 |
152 | #: templates/admin/edit_inline/gen_coll_tabular.html:24
153 | msgid "View on site"
154 | msgstr "Anzeigen"
155 |
--------------------------------------------------------------------------------
/doc_src/user_guide/registering_models.rst:
--------------------------------------------------------------------------------
1 | .. _registering_models:
2 |
3 | ==================
4 | Registering Models
5 | ==================
6 |
7 |
8 | Registering models in settings.py
9 | =================================
10 |
11 | It is nice to not have to modify the code of applications to add a relation to categories. You can therefore do all the registering in ``settings.py``\ . For example:
12 |
13 | .. code-block:: python
14 |
15 | CATEGORIES_SETTINGS = {
16 | 'FK_REGISTRY': {
17 | 'app.AModel': 'category',
18 | 'app.MyModel': (
19 | 'primary_category',
20 | {'name': 'secondary_category', 'related_name': 'mymodel_sec_cat'}, )
21 | },
22 | 'M2M_REGISTRY': {
23 | 'app.BModel': 'categories',
24 | 'app.MyModel': ('other_categories', 'more_categories', ),
25 | }
26 | }
27 |
28 | The ``FK_REGISTRY`` is a dictionary whose keys are the model to which to add the new field(s). The value is a string or tuple of strings or dictionaries specifying the necessary information for each field.
29 |
30 | The ``M2M_REGISTRY`` is a dictionary whose keys are the model to which to add the new field(s). The value is a string or tuple of strings specifying the necessary information for each field.
31 |
32 |
33 | Registering one Category field to model
34 | ***************************************
35 |
36 | The simplest way is to specify the name of the field, such as:
37 |
38 | .. code-block:: python
39 |
40 | CATEGORIES_SETTINGS = {
41 | 'FK_REGISTRY': {
42 | 'app.AModel': 'category'
43 | }
44 | }
45 |
46 | This is equivalent to adding the following the ``AModel`` of ``app``\ :
47 |
48 | .. code-block:: python
49 |
50 | category = models.ForeignKey(Category)
51 |
52 |
53 | If you want more control over the new field, you can use a dictionary and pass other ``ForeignKey`` options. The ``name`` key is required:
54 |
55 | .. code-block:: python
56 |
57 | CATEGORIES_SETTINGS = {
58 | 'FK_REGISTRY': {
59 | 'app.AModel': {'name': 'category', 'related_name': 'amodel_cats'}
60 | }
61 | }
62 |
63 | This is equivalent to adding the following the ``AModel`` of ``app``\ :
64 |
65 | .. code-block:: python
66 |
67 | category = models.ForeignKey(Category, related_name='amodel_cats')
68 |
69 | Registering two or more Category fields to a Model
70 | **************************************************
71 |
72 | When you want more than one relation to ``Category``\ , all but one of the fields must specify a ``related_name`` to avoid conflicts, like so:
73 |
74 | .. code-block:: python
75 |
76 | CATEGORIES_SETTINGS = {
77 | 'FK_REGISTRY': {
78 | 'app.MyModel': (
79 | 'primary_category',
80 | {'name': 'secondary_category', 'related_name': 'mymodel_sec_cat'}, )
81 | },
82 |
83 | Registering one or more Many-to-Many Category fields to a Model
84 | ***************************************************************
85 |
86 | .. code-block:: python
87 |
88 | CATEGORIES_SETTINGS = {
89 | 'M2M_REGISTRY': {
90 | 'app.AModel': 'categories',
91 | 'app.MyModel': (
92 | {'name': 'other_categories', 'related_name': 'other_cats'},
93 | {'name': 'more_categories', 'related_name': 'more_cats'},
94 | ),
95 | }
96 | }
97 |
98 | .. _registering_a_m2one_relationship:
99 |
100 | Registering a many-to-one relationship
101 | ======================================
102 |
103 | To create a many-to-one relationship (foreign key) between a model and Django Categories, you register your model with the ``register_fk`` function.
104 |
105 | .. py:function:: register_fk(model, field_name='category', extra_params={}])
106 |
107 | :param model: The Django Model to link to Django Categories
108 | :param field_name: Optional name for the field **default:** category
109 | :param extra_params: Optional dictionary of extra parameters passed to the ``ForeignKey`` class.
110 |
111 | Example, in your ``models.py``::
112 |
113 | import categories
114 | categories.register_fk(MyModel)
115 |
116 | If you want more than one field on a model you have to have some extra arguments::
117 |
118 | import categories
119 | categories.register_fk(MyModel, 'primary_category')
120 | categories.register_fk(MyModel, 'secondary_category', {'related_name':'mymodel_sec_cat'})
121 |
122 | The ``extra_args`` allows you to specify the related_name of one of the fields so it doesn't clash.
123 |
124 |
125 | Registering a many-to-many relationship
126 | =======================================
127 |
128 | To create a many-to-many relationship between a model and Django Categories, you register your model with the ``register_m2m`` function.
129 |
130 | .. py:function:: register_m2m(model, field_name='categories', extra_params={}])
131 |
132 | :param model: The Django Model to link to Django Categories
133 | :param field_name: Optional name for the field **default:** categories
134 | :param extra_params: Optional dictionary of extra parameters passed to the ``ManyToManyField`` class.
135 |
136 | Example, in your ``models.py``::
137 |
138 | import categories
139 | categories.register_m2m(MyModel)
140 |
--------------------------------------------------------------------------------
/categories/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | import django.core.files.storage
2 | import mptt.fields
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("contenttypes", "0001_initial"),
9 | ]
10 |
11 | operations = [
12 | migrations.CreateModel(
13 | name="Category",
14 | fields=[
15 | ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
16 | ("name", models.CharField(max_length=100, verbose_name="name")),
17 | ("slug", models.SlugField(verbose_name="slug")),
18 | ("active", models.BooleanField(default=True, verbose_name="active")),
19 | (
20 | "thumbnail",
21 | models.FileField(
22 | storage=django.core.files.storage.FileSystemStorage(),
23 | null=True,
24 | upload_to="uploads/categories/thumbnails",
25 | blank=True,
26 | ),
27 | ),
28 | ("thumbnail_width", models.IntegerField(null=True, blank=True)),
29 | ("thumbnail_height", models.IntegerField(null=True, blank=True)),
30 | ("order", models.IntegerField(default=0)),
31 | (
32 | "alternate_title",
33 | models.CharField(
34 | default="",
35 | help_text="An alternative title to use on pages with this category.",
36 | max_length=100,
37 | blank=True,
38 | ),
39 | ),
40 | (
41 | "alternate_url",
42 | models.CharField(
43 | help_text="An alternative URL to use instead of the one derived from the category hierarchy.",
44 | max_length=200,
45 | blank=True,
46 | ),
47 | ),
48 | ("description", models.TextField(null=True, blank=True)),
49 | (
50 | "meta_keywords",
51 | models.CharField(
52 | default="",
53 | help_text="Comma-separated keywords for search engines.",
54 | max_length=255,
55 | blank=True,
56 | ),
57 | ),
58 | (
59 | "meta_extra",
60 | models.TextField(
61 | default="",
62 | help_text="(Advanced) Any additional HTML to be placed verbatim in the <head>",
63 | blank=True,
64 | ),
65 | ),
66 | ("lft", models.PositiveIntegerField(editable=False, db_index=True)),
67 | ("rght", models.PositiveIntegerField(editable=False, db_index=True)),
68 | ("tree_id", models.PositiveIntegerField(editable=False, db_index=True)),
69 | ("level", models.PositiveIntegerField(editable=False, db_index=True)),
70 | (
71 | "parent",
72 | mptt.fields.TreeForeignKey(
73 | related_name="children",
74 | verbose_name="parent",
75 | blank=True,
76 | to="categories.Category",
77 | on_delete=models.CASCADE,
78 | null=True,
79 | ),
80 | ),
81 | ],
82 | options={
83 | "ordering": ("tree_id", "lft"),
84 | "abstract": False,
85 | "verbose_name": "category",
86 | "verbose_name_plural": "categories",
87 | },
88 | ),
89 | migrations.CreateModel(
90 | name="CategoryRelation",
91 | fields=[
92 | ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
93 | ("object_id", models.PositiveIntegerField(verbose_name="object id")),
94 | (
95 | "relation_type",
96 | models.CharField(
97 | help_text="A generic text field to tag a relation, like 'leadphoto'.",
98 | max_length="200",
99 | null=True,
100 | verbose_name="relation type",
101 | blank=True,
102 | ),
103 | ),
104 | (
105 | "category",
106 | models.ForeignKey(verbose_name="category", to="categories.Category", on_delete=models.CASCADE),
107 | ),
108 | (
109 | "content_type",
110 | models.ForeignKey(
111 | verbose_name="content type", to="contenttypes.ContentType", on_delete=models.CASCADE
112 | ),
113 | ),
114 | ],
115 | ),
116 | migrations.AlterUniqueTogether(
117 | name="category",
118 | unique_together=set([("parent", "name")]),
119 | ),
120 | ]
121 |
--------------------------------------------------------------------------------
/categories/models.py:
--------------------------------------------------------------------------------
1 | """Category models."""
2 |
3 | from functools import reduce
4 |
5 | from django.contrib.contenttypes.fields import GenericForeignKey
6 | from django.contrib.contenttypes.models import ContentType
7 | from django.core.files.images import get_image_dimensions
8 | from django.db import models
9 | from django.urls import reverse
10 | from django.utils.encoding import force_str
11 | from django.utils.translation import gettext_lazy as _
12 |
13 | from .base import CategoryBase
14 | from .settings import (
15 | RELATION_MODELS,
16 | RELATIONS,
17 | THUMBNAIL_STORAGE_ALIAS,
18 | THUMBNAIL_UPLOAD_PATH,
19 | )
20 |
21 | # Determine storage method based on Django version
22 | try: # Django 4.2+
23 | from django.core.files.storage import storages
24 |
25 | STORAGE = storages[THUMBNAIL_STORAGE_ALIAS]
26 | except ImportError:
27 | from django.core.files.storage import get_storage_class
28 |
29 | from .settings import THUMBNAIL_STORAGE
30 |
31 | STORAGE = get_storage_class(THUMBNAIL_STORAGE)()
32 |
33 |
34 | class Category(CategoryBase):
35 | """A basic category model."""
36 |
37 | thumbnail = models.FileField(
38 | upload_to=THUMBNAIL_UPLOAD_PATH,
39 | null=True,
40 | blank=True,
41 | storage=STORAGE,
42 | )
43 | thumbnail_width = models.IntegerField(blank=True, null=True)
44 | thumbnail_height = models.IntegerField(blank=True, null=True)
45 | order = models.IntegerField(default=0)
46 | alternate_title = models.CharField(
47 | blank=True, default="", max_length=100, help_text="An alternative title to use on pages with this category."
48 | )
49 | alternate_url = models.CharField(
50 | blank=True,
51 | max_length=200,
52 | help_text="An alternative URL to use instead of the one derived from " "the category hierarchy.",
53 | )
54 | description = models.TextField(blank=True, null=True)
55 | meta_keywords = models.CharField(
56 | blank=True, default="", max_length=255, help_text="Comma-separated keywords for search engines."
57 | )
58 | meta_extra = models.TextField(
59 | blank=True, default="", help_text="(Advanced) Any additional HTML to be placed verbatim " "in the <head>"
60 | )
61 |
62 | @property
63 | def short_title(self):
64 | """Return the name."""
65 | return self.name
66 |
67 | def get_absolute_url(self):
68 | """Return a path."""
69 | from django.urls import NoReverseMatch
70 |
71 | if self.alternate_url:
72 | return self.alternate_url
73 | try:
74 | prefix = reverse("categories_tree_list")
75 | except NoReverseMatch:
76 | prefix = "/"
77 | ancestors = list(self.get_ancestors()) + [
78 | self,
79 | ]
80 | return prefix + "/".join([force_str(i.slug) for i in ancestors]) + "/"
81 |
82 | if RELATION_MODELS:
83 |
84 | def get_related_content_type(self, content_type):
85 | """
86 | Get all related items of the specified content type.
87 | """
88 | return self.categoryrelation_set.filter(content_type__name=content_type)
89 |
90 | def get_relation_type(self, relation_type):
91 | """
92 | Get all relations of the specified relation type.
93 | """
94 | return self.categoryrelation_set.filter(relation_type=relation_type)
95 |
96 | def save(self, *args, **kwargs):
97 | """Save the category."""
98 | if self.thumbnail:
99 | width, height = get_image_dimensions(self.thumbnail.file)
100 | else:
101 | width, height = None, None
102 |
103 | self.thumbnail_width = width
104 | self.thumbnail_height = height
105 |
106 | super(Category, self).save(*args, **kwargs)
107 |
108 | class Meta(CategoryBase.Meta):
109 | verbose_name = _("category")
110 | verbose_name_plural = _("categories")
111 |
112 | class MPTTMeta:
113 | order_insertion_by = ("order", "name")
114 |
115 |
116 | if RELATIONS:
117 | CATEGORY_RELATION_LIMITS = reduce(lambda x, y: x | y, RELATIONS)
118 | else:
119 | CATEGORY_RELATION_LIMITS = []
120 |
121 |
122 | class CategoryRelationManager(models.Manager):
123 | """Custom access functions for category relations."""
124 |
125 | def get_content_type(self, content_type):
126 | """
127 | Get all the items of the given content type related to this item.
128 | """
129 | qs = self.get_queryset()
130 | return qs.filter(content_type__name=content_type)
131 |
132 | def get_relation_type(self, relation_type):
133 | """
134 | Get all the items of the given relationship type related to this item.
135 | """
136 | qs = self.get_queryset()
137 | return qs.filter(relation_type=relation_type)
138 |
139 |
140 | class CategoryRelation(models.Model):
141 | """Related category item."""
142 |
143 | category = models.ForeignKey(Category, verbose_name=_("category"), on_delete=models.CASCADE)
144 | content_type = models.ForeignKey(
145 | ContentType,
146 | on_delete=models.CASCADE,
147 | limit_choices_to=CATEGORY_RELATION_LIMITS,
148 | verbose_name=_("content type"),
149 | )
150 | object_id = models.PositiveIntegerField(verbose_name=_("object id"))
151 | content_object = GenericForeignKey("content_type", "object_id")
152 | relation_type = models.CharField(
153 | verbose_name=_("relation type"),
154 | max_length=200,
155 | blank=True,
156 | null=True,
157 | help_text=_("A generic text field to tag a relation, like 'leadphoto'."),
158 | )
159 |
160 | objects = CategoryRelationManager()
161 |
162 | def __unicode__(self):
163 | return "CategoryRelation"
164 |
--------------------------------------------------------------------------------
/categories/locale/it/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 | # This file is distributed under the same license as the PACKAGE package.
3 | #
4 | # Iacopo Spalletti, 2013.
5 | msgid ""
6 | msgstr ""
7 | "Project-Id-Version: \n"
8 | "Report-Msgid-Bugs-To: \n"
9 | "POT-Creation-Date: 2013-06-05 12:22+0200\n"
10 | "PO-Revision-Date: 2013-06-05 12:48+0200\n"
11 | "Last-Translator: Iacopo Spalletti\n"
12 | "Language-Team: Italian \n"
13 | "Language: it\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 | "X-Generator: Lokalize 1.5\n"
19 |
20 | #: admin.py:50
21 | msgid "Meta Data"
22 | msgstr "Metadati"
23 |
24 | #: admin.py:55
25 | msgid "Advanced"
26 | msgstr "Avanzate"
27 |
28 | #: base.py:41
29 | msgid "parent"
30 | msgstr "livello superiore"
31 |
32 | #: base.py:42
33 | msgid "name"
34 | msgstr "nome"
35 |
36 | #: base.py:43
37 | msgid "slug"
38 | msgstr "slug"
39 |
40 | #: base.py:44
41 | msgid "active"
42 | msgstr "attivo"
43 |
44 | #: base.py:104
45 | msgid "The slug must be unique among the items at its level."
46 | msgstr "Lo slug deve essere unico fra gli elementi allo stesso livello."
47 |
48 | #: base.py:114
49 | msgid "You can't set the parent of the item to itself."
50 | msgstr ""
51 | "Non è possibile selezionare come livello superiore di un elemento l'elemento "
52 | "stesso."
53 |
54 | #: base.py:117
55 | msgid "You can't set the parent of the item to a descendant."
56 | msgstr ""
57 | "Non è possibile selezionare come livello superiore di un elemento uno dei "
58 | "suoi discendenti."
59 |
60 | #: base.py:148
61 | msgid "Deactivate selected categories and their children"
62 | msgstr "Disattiva le categorie selezionate e i discendenti"
63 |
64 | #: base.py:161
65 | msgid "Activate selected categories and their children"
66 | msgstr "Attiva le categorie selezionate e i discendenti"
67 |
68 | #: migration.py:21 migration.py:94
69 | msgid "%(dependency) must be installed for this command to work"
70 | msgstr "%(dependency) deve essere installato per far funzionare il comando"
71 |
72 | #: migration.py:46
73 | msgid "Added ForeignKey %(field_name) to %(model_name)"
74 | msgstr "Aggiunta ForeignKey %(field_name) a %(model_name)"
75 |
76 | #: migration.py:52
77 | msgid "ForeignKey %(field_name) to %(model_name) already exists"
78 | msgstr "La ForeignKey %(field_name) a %(model_name) esiste già"
79 |
80 | #: migration.py:69
81 | msgid "Added Many2Many table between %(model_name) and %(category_table)"
82 | msgstr "Aggiunta tabella Many2Many fra %(model_name) e %(category_table)"
83 |
84 | #: migration.py:75
85 | msgid ""
86 | "Many2Many table between %(model_name) and %(category_table) already exists"
87 | msgstr "La tabella Many2Many fra %(model_name) e %(category_table) esiste già"
88 |
89 | #: migration.py:101
90 | msgid "Dropping ForeignKey %(field_name) from %(model_name)"
91 | msgstr "Eliminazione ForeignKey %(field_name) da %(model_name)"
92 |
93 | #: migration.py:112
94 | msgid "Dropping Many2Many table between %(model_name) and %(category_table)"
95 | msgstr ""
96 | "Eliminazione della tabella Many2Many fra %(model_name) e %(category_table)"
97 |
98 | #: models.py:91 models.py:122
99 | msgid "category"
100 | msgstr "categoria"
101 |
102 | #: models.py:92
103 | msgid "categories"
104 | msgstr "categorie"
105 |
106 | #: models.py:124
107 | msgid "content type"
108 | msgstr "content type"
109 |
110 | #: models.py:125
111 | msgid "object id"
112 | msgstr "object id"
113 |
114 | #: models.py:127
115 | msgid "relation type"
116 | msgstr "tipo di relazione"
117 |
118 | #: models.py:131
119 | msgid "A generic text field to tag a relation, like 'leadphoto'."
120 | msgstr ""
121 | "Un campo di testo generico per marcare la relazione, ad es: 'leadphoto'."
122 |
123 | #: registration.py:45
124 | #, python-format
125 | msgid "%(key)s is not a model"
126 | msgstr "%(key)s non è un model"
127 |
128 | #: registration.py:54 registration.py:62
129 | msgid "%(settings)s doesn't recognize the value of %(key)"
130 | msgstr "%(settings)s non riconosce il valore di %(key)"
131 |
132 | #: settings.py:30
133 | msgid "%(transliterator)s must be a callable or a string."
134 | msgstr "%(transliterator)s deve essere un callable o una stringa."
135 |
136 | #: views.py:73
137 | #, python-format
138 | msgid "No %(verbose_name)s found matching the query"
139 | msgstr "Nessun %(verbose_name)s corrispondente alla ricerca"
140 |
141 | #: editor/tree_editor.py:167
142 | msgid "Database error"
143 | msgstr "Errore di database"
144 |
145 | #: editor/tree_editor.py:206
146 | #, python-format
147 | msgid "%(count)s %(name)s was changed successfully."
148 | msgid_plural "%(count)s %(name)s were changed successfully."
149 | msgstr[0] "%(count)s %(name)s modificati."
150 | msgstr[1] ""
151 |
152 | #: editor/tree_editor.py:247
153 | #, python-format
154 | msgid "%(total_count)s selected"
155 | msgid_plural "All %(total_count)s selected"
156 | msgstr[0] "%(total_count)s selezionati"
157 | msgstr[1] ""
158 |
159 | #: editor/tree_editor.py:252
160 | #, python-format
161 | msgid "0 of %(cnt)s selected"
162 | msgstr "selezionati 0 su %(cnt)s"
163 |
164 | #: editor/templates/admin/editor/tree_list_results.html:16
165 | msgid "Remove from sorting"
166 | msgstr "Rimuovi dall'ordinamento"
167 |
168 | #: editor/templates/admin/editor/tree_list_results.html:17
169 | #, python-format
170 | msgid "Sorting priority: %(priority_number)s"
171 | msgstr "Priorità di ordinamento: %(priority_number)s"
172 |
173 | #: editor/templates/admin/editor/tree_list_results.html:18
174 | msgid "Toggle sorting"
175 | msgstr "Inverti ordinamento"
176 |
177 | #: templates/admin/edit_inline/gen_coll_tabular.html:13
178 | msgid "Delete?"
179 | msgstr "Cancella?"
180 |
181 | #: templates/admin/edit_inline/gen_coll_tabular.html:24
182 | msgid "View on site"
183 | msgstr "Vedi sul sito"
184 |
--------------------------------------------------------------------------------
/categories/locale/fr/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 | # Olivier Le Brouster, 2015.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2015-05-19 16:53+0200\n"
12 | "PO-Revision-Date: 2015-05-19 16:59+0200\n"
13 | "Last-Translator: FULL NAME \n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: fr\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 | #: admin.py:50
22 | msgid "Meta Data"
23 | msgstr "Meta donnée"
24 |
25 | #: admin.py:55
26 | msgid "Advanced"
27 | msgstr "Avancé"
28 |
29 | #: base.py:41
30 | msgid "parent"
31 | msgstr "parent"
32 |
33 | #: base.py:42
34 | msgid "name"
35 | msgstr "nom"
36 |
37 | #: base.py:43
38 | msgid "slug"
39 | msgstr "slug"
40 |
41 | #: base.py:44
42 | msgid "active"
43 | msgstr "actif"
44 |
45 | #: base.py:105
46 | msgid "The slug must be unique among the items at its level."
47 | msgstr "Le slug doit être unique parmi les items d'un même niveau."
48 |
49 | #: base.py:115
50 | msgid "You can't set the parent of the item to itself."
51 | msgstr "Il n'est pas possible choisir l'item lui-même comme parent."
52 |
53 | #: base.py:118
54 | msgid "You can't set the parent of the item to a descendant."
55 | msgstr "Il n'est pas possible de choisir le parent de l'item pour un de ses descendants."
56 |
57 | #: base.py:149
58 | msgid "Deactivate selected categories and their children"
59 | msgstr "Désactiver les catégories sélectionnées et leurs descendants."
60 |
61 | #: base.py:162
62 | msgid "Activate selected categories and their children"
63 | msgstr "Activer les catégories sélectionnées et leurs descendants."
64 |
65 | #: migration.py:21 migration.py:94
66 | msgid "%(dependency) must be installed for this command to work"
67 | msgstr "%(dependency) doit être installée pour que cela fonctionne"
68 |
69 | #: migration.py:46
70 | msgid "Added ForeignKey %(field_name) to %(model_name)"
71 | msgstr "ForeignKey %(field_name) ajoutée à %(model_name)"
72 |
73 | #: migration.py:52
74 | msgid "ForeignKey %(field_name) to %(model_name) already exists"
75 | msgstr "La ForeignKey %(field_name) de %(model_name) existe déjà"
76 |
77 | #: migration.py:69
78 | msgid "Added Many2Many table between %(model_name) and %(category_table)"
79 | msgstr "La table Many2Many entre %(model_name) et %(category_table) a été ajoutée"
80 |
81 | #: migration.py:75
82 | msgid ""
83 | "Many2Many table between %(model_name) and %(category_table) already exists"
84 | msgstr "La table Many2Many entre %(model_name) et %(category_table) existe déjà"
85 |
86 | #: migration.py:101
87 | msgid "Dropping ForeignKey %(field_name) from %(model_name)"
88 | msgstr "Suppression de la ForeignKey %(field_name) de %(model_name)"
89 |
90 | #: migration.py:112
91 | msgid "Dropping Many2Many table between %(model_name) and %(category_table)"
92 | msgstr "Suppression de la table Many2Many entre %(model_name) et %(category_table)"
93 |
94 | #: models.py:91 models.py:122
95 | msgid "category"
96 | msgstr "catégorie"
97 |
98 | #: models.py:92
99 | msgid "categories"
100 | msgstr "catégories"
101 |
102 | #: models.py:124
103 | msgid "content type"
104 | msgstr "type de contenu"
105 |
106 | #: models.py:125
107 | msgid "object id"
108 | msgstr "object id"
109 |
110 | #: models.py:127
111 | msgid "relation type"
112 | msgstr "type de relation"
113 |
114 | #: models.py:131
115 | msgid "A generic text field to tag a relation, like 'leadphoto'."
116 | msgstr "Un champ texte générique pour marquer la relation, comme 'leadphoto'."
117 |
118 | #: registration.py:46
119 | #, python-format
120 | msgid "%(key)s is not a model"
121 | msgstr "%(key)s n'est pas un modèle"
122 |
123 | #: registration.py:55 registration.py:63
124 | #, python-format
125 | msgid "%(settings)s doesn't recognize the value of %(key)s"
126 | msgstr "%(settings)s n'accepte pas la valeure %(key)s"
127 |
128 | #: settings.py:30
129 | msgid "%(transliterator) must be a callable or a string."
130 | msgstr "%(transliterator) doit être un exécutable ou une chaîne."
131 |
132 | #: views.py:73
133 | #, python-format
134 | msgid "No %(verbose_name)s found matching the query"
135 | msgstr "Aucun(e) %(verbose_name)s ne correspond à la requête"
136 |
137 | #: editor/tree_editor.py:167
138 | msgid "Database error"
139 | msgstr "Erreur de base données"
140 |
141 | #: editor/tree_editor.py:206
142 | #, python-format
143 | msgid "%(count)s %(name)s was changed successfully."
144 | msgid_plural "%(count)s %(name)s were changed successfully."
145 | msgstr[0] "%(count)s %(name)s modifié·e avec succès."
146 | msgstr[1] "%(count)s %(name)s modifié·e·s avec succès."
147 |
148 | #: editor/tree_editor.py:247
149 | #, python-format
150 | msgid "%(total_count)s selected"
151 | msgid_plural "All %(total_count)s selected"
152 | msgstr[0] "%(total_count)s sélectionné·e"
153 | msgstr[1] "%(total_count)s sélectionné·e·s"
154 |
155 | #: editor/tree_editor.py:252
156 | #, python-format
157 | msgid "0 of %(cnt)s selected"
158 | msgstr "0 sur %(cnt)s sélectionné·e·s"
159 |
160 | #: editor/templates/admin/editor/tree_list_results.html:16
161 | msgid "Remove from sorting"
162 | msgstr "Enlever du tri"
163 |
164 | #: editor/templates/admin/editor/tree_list_results.html:17
165 | #, python-format
166 | msgid "Sorting priority: %(priority_number)s"
167 | msgstr "Priorité de tri : %(priority_number)s"
168 |
169 | #: editor/templates/admin/editor/tree_list_results.html:18
170 | msgid "Toggle sorting"
171 | msgstr "Inverser le tri"
172 |
173 | #: templates/admin/edit_inline/gen_coll_tabular.html:13
174 | msgid "Delete?"
175 | msgstr "Supprimer ?"
176 |
177 | #: templates/admin/edit_inline/gen_coll_tabular.html:24
178 | msgid "View on site"
179 | msgstr "Voir sur le site"
180 |
--------------------------------------------------------------------------------
/categories/views.py:
--------------------------------------------------------------------------------
1 | """View functions for categories."""
2 |
3 | from typing import Optional
4 |
5 | from django.http import Http404, HttpResponse
6 | from django.shortcuts import get_object_or_404
7 | from django.template.loader import select_template
8 | from django.utils.translation import gettext_lazy as _
9 | from django.views.generic import DetailView, ListView
10 |
11 | from .models import Category
12 |
13 |
14 | def category_detail(
15 | request, path, template_name="categories/category_detail.html", extra_context: Optional[dict] = None
16 | ):
17 | """Render the detail page for a category."""
18 | extra_context = extra_context or {}
19 | path_items = path.strip("/").split("/")
20 | if len(path_items) >= 2:
21 | category = get_object_or_404(
22 | Category, slug__iexact=path_items[-1], level=len(path_items) - 1, parent__slug__iexact=path_items[-2]
23 | )
24 | else:
25 | category = get_object_or_404(Category, slug__iexact=path_items[-1], level=len(path_items) - 1)
26 |
27 | templates = []
28 | while path_items:
29 | templates.append("categories/%s.html" % "_".join(path_items))
30 | path_items.pop()
31 | templates.append(template_name)
32 |
33 | context = {"category": category}
34 | if extra_context:
35 | context.update(extra_context)
36 | return HttpResponse(select_template(templates).render(context))
37 |
38 |
39 | def get_category_for_path(path, queryset=Category.objects.all()):
40 | """Return the category for a path."""
41 | path_items = path.strip("/").split("/")
42 | if len(path_items) >= 2:
43 | queryset = queryset.filter(
44 | slug__iexact=path_items[-1], level=len(path_items) - 1, parent__slug__iexact=path_items[-2]
45 | )
46 | else:
47 | queryset = queryset.filter(slug__iexact=path_items[-1], level=len(path_items) - 1)
48 | return queryset.get()
49 |
50 |
51 | class CategoryDetailView(DetailView):
52 | """Detail view for a category."""
53 |
54 | model = Category
55 | path_field = "path"
56 |
57 | def get_object(self, **kwargs):
58 | """Get the category."""
59 | if self.path_field not in self.kwargs:
60 | raise AttributeError(
61 | "Category detail view %s must be called with " "a %s." % (self.__class__.__name__, self.path_field)
62 | )
63 | if self.queryset is None:
64 | queryset = self.get_queryset()
65 | try:
66 | return get_category_for_path(self.kwargs[self.path_field], self.model.objects.all())
67 | except Category.DoesNotExist:
68 | raise Http404(
69 | _("No %(verbose_name)s found matching the query") % {"verbose_name": queryset.model._meta.verbose_name}
70 | )
71 |
72 | def get_template_names(self):
73 | """Get the potential template names."""
74 | names = []
75 | path_items = self.kwargs[self.path_field].strip("/").split("/")
76 | while path_items:
77 | names.append("categories/%s.html" % "_".join(path_items))
78 | path_items.pop()
79 | names.extend(super(CategoryDetailView, self).get_template_names())
80 | return names
81 |
82 |
83 | class CategoryRelatedDetail(DetailView):
84 | """Detailed view for a category relation."""
85 |
86 | path_field = "category_path"
87 | object_name_field = None
88 |
89 | def get_object(self, **kwargs):
90 | """Get the object to render."""
91 | if self.path_field not in self.kwargs:
92 | raise AttributeError(
93 | "Category detail view %s must be called with " "a %s." % (self.__class__.__name__, self.path_field)
94 | )
95 | queryset = super(CategoryRelatedDetail, self).get_queryset()
96 | try:
97 | category = get_category_for_path(self.kwargs[self.path_field])
98 | except Category.DoesNotExist:
99 | raise Http404(
100 | _("No %(verbose_name)s found matching the query") % {"verbose_name": queryset.model._meta.verbose_name}
101 | )
102 | return queryset.get(category=category)
103 |
104 | def get_template_names(self):
105 | """Get all template names."""
106 | names = []
107 | opts = self.object._meta
108 | path_items = self.kwargs[self.path_field].strip("/").split("/")
109 | if self.object_name_field:
110 | path_items.append(getattr(self.object, self.object_name_field))
111 | while path_items:
112 | names.append(
113 | "%s/category_%s_%s%s.html"
114 | % (opts.app_label, "_".join(path_items), opts.object_name.lower(), self.template_name_suffix)
115 | )
116 | path_items.pop()
117 | names.append("%s/category_%s%s.html" % (opts.app_label, opts.object_name.lower(), self.template_name_suffix))
118 | names.extend(super(CategoryRelatedDetail, self).get_template_names())
119 | return names
120 |
121 |
122 | class CategoryRelatedList(ListView):
123 | """List related category items."""
124 |
125 | path_field = "category_path"
126 |
127 | def get_queryset(self):
128 | """Get the list of items."""
129 | if self.path_field not in self.kwargs:
130 | raise AttributeError(
131 | "Category detail view %s must be called with " "a %s." % (self.__class__.__name__, self.path_field)
132 | )
133 | queryset = super(CategoryRelatedList, self).get_queryset()
134 | category = get_category_for_path(self.kwargs[self.path_field])
135 | return queryset.filter(category=category)
136 |
137 | def get_template_names(self):
138 | """Get the template names."""
139 | names = []
140 | if hasattr(self.object_list, "model"):
141 | opts = self.object_list.model._meta
142 | path_items = self.kwargs[self.path_field].strip("/").split("/")
143 | while path_items:
144 | names.append(
145 | "%s/category_%s_%s%s.html"
146 | % (opts.app_label, "_".join(path_items), opts.object_name.lower(), self.template_name_suffix)
147 | )
148 | path_items.pop()
149 | names.append(
150 | "%s/category_%s%s.html" % (opts.app_label, opts.object_name.lower(), self.template_name_suffix)
151 | )
152 | names.extend(super(CategoryRelatedList, self).get_template_names())
153 | return names
154 |
--------------------------------------------------------------------------------
/categories/fixtures/genres.txt:
--------------------------------------------------------------------------------
1 | Avant-garde
2 | Experimental music
3 | Minimalist music
4 | Lo-fi
5 | Country
6 | Alternative country
7 | Americana
8 | Country pop
9 | Nashville sound/Countrypolitan
10 | Urban Cowboy
11 | Country rock
12 | Honky tonk
13 | Bakersfield Sound
14 | Progressive country
15 | Neotraditional country
16 | Outlaw country
17 | Rockabilly
18 | Traditional country
19 | Bluegrass
20 | Progressive bluegrass
21 | Traditional bluegrass
22 | Close harmony
23 | String band
24 | Western
25 | Western swing
26 | Jazz
27 | Acid jazz
28 | Asian American jazz
29 | Avant-garde jazz
30 | Bebop
31 | Big band
32 | Crossover jazz
33 | Dixieland
34 | Calypso jazz
35 | Chamber jazz
36 | Cool jazz
37 | Free jazz
38 | Gypsy jazz
39 | Hard bop
40 | Jazz blues
41 | Jazz-funk
42 | Jazz fusion
43 | Jazz rap
44 | Latin jazz
45 | Mainstream jazz
46 | Mini-jazz
47 | Modal jazz
48 | M-Base
49 | Nu jazz
50 | Smooth jazz
51 | Soul jazz
52 | Swing
53 | Trad jazz
54 | West Coast jazz
55 | Blues
56 | Boogie-woogie
57 | Country blues
58 | Delta blues
59 | Electric blues
60 | Jump blues
61 | Easy listening
62 | Background music
63 | Beautiful music
64 | Elevator music
65 | Furniture music
66 | Lounge music
67 | Middle of the road
68 | Electronic
69 | Ambient
70 | Dark ambient
71 | Breakbeat
72 | Dance
73 | Big beat
74 | Dubstep
75 | Drum and bass
76 | Darkcore
77 | Electroclash
78 | Eurodance
79 | Gabba
80 | Garage
81 | Goa trance
82 | Psychedelic trance
83 | Happy hardcore
84 | Hardcore techno
85 | Hi-NRG
86 | House
87 | Acid house
88 | Ambient house
89 | Italo house
90 | Kwaito
91 | IDM
92 | Spacesynth
93 | Trance
94 | Acid
95 | Classic
96 | Euro
97 | Hard
98 | Hardstyle
99 | Progressive trance
100 | Tech
101 | Uplifting
102 | Vocal
103 | Electro
104 | Electronica
105 | Folktronica
106 | Rave
107 | Techno
108 | Acid breaks
109 | Trip hop
110 | Downtempo
111 | Glitch
112 | Industrial music
113 | Progressive electronic
114 | Hip hop/Rap music
115 | Australian hip hop
116 | Canadian hip hop
117 | Crunk
118 | Dirty rap/Pornocore
119 | East Coast hip hop
120 | Gangsta rap
121 | G-funk
122 | Grime
123 | Latin rap
124 | Chicano rap
125 | Miami bass
126 | Midwest hip hop
127 | Political hip hop
128 | Southern hip hop
129 | Turntablism
130 | West Coast hip hop
131 | Latin
132 | Bossa Nova
133 | Mambo
134 | Merengue
135 | Música Popular Brasileira
136 | Reggaeton
137 | Salsa
138 | Samba
139 | Tejano
140 | Tropicalismo
141 | Zouk
142 | Pop
143 | Adult contemporary music
144 | Adult oriented pop music
145 | Afropop
146 | Arab pop
147 | Austropop
148 | Baroque pop
149 | Brazilian pop
150 | Bubblegum pop
151 | Chinese pop
152 | Contemporary Christian
153 | Country pop
154 | Dance-pop
155 | Disco
156 | Disco polo
157 | Electropop/Technopop
158 | Eurobeat
159 | Euro disco
160 | Europop
161 | French pop
162 | Greek Laïkó pop
163 | Hindi pop
164 | Hong Kong and Cantonese pop
165 | Hong Kong English pop
166 | House-pop
167 | Indonesian pop
168 | Italo dance
169 | Italo disco
170 | Jangle pop
171 | Japanese pop
172 | Korean pop
173 | Latin pop
174 | Levenslied
175 | Louisiana swamp pop
176 | Mandarin pop
177 | Manufactured pop
178 | Mexican pop
179 | Nederpop
180 | New romantic
181 | Noise pop
182 | Operatic pop
183 | Persian pop
184 | Pop folk
185 | Pop rap
186 | Psychedelic pop
187 | Russian pop
188 | Schlager
189 | Sophisti-pop
190 | Space age pop
191 | Sunshine pop
192 | Surf pop
193 | Synthpop
194 | Taiwanese pop
195 | Teen pop
196 | Thai pop
197 | Traditional pop music
198 | Turkish pop
199 | US pop
200 | Vispop
201 | Wonky Pop
202 | Yugoslav pop
203 | Modern folk
204 | Indie folk
205 | Neofolk
206 | Progressive folk
207 | Reggae
208 | 2 tone
209 | Dancehall
210 | Dub
211 | Lovers rock
212 | Ragga
213 | Reggaefusion
214 | Ska
215 | Rhythm and blues
216 | Contemporary R&B
217 | Doo wop
218 | Funk
219 | Deep Funk
220 | Go-go
221 | P-Funk
222 | New jack swing
223 | Soul
224 | Hip hop soul
225 | Northern soul
226 | Neo soul
227 | Urban contemporary
228 | Rock
229 | Alternative rock
230 | Britpop
231 | Dream pop
232 | Emo
233 | Gothic rock
234 | Grunge
235 | Post-grunge
236 | Indie rock
237 | C86
238 | Industrial rock
239 | Indie pop
240 | Madchester
241 | Post-rock
242 | Shoegazing
243 | Blues-rock
244 | C-Rock
245 | Dark cabaret
246 | Desert rock
247 | Folk rock
248 | Garage rock
249 | Glam rock
250 | Hard rock
251 | Heavy metal
252 | Black metal
253 | Death metal
254 | Doom metal
255 | Folk metal
256 | Glam metal
257 | Gothic metal
258 | Grindcore
259 | Groove metal
260 | Industrial metal
261 | Metalcore
262 | Nu metal
263 | Power metal
264 | Progressive metal
265 | Rap metal
266 | Sludge metal
267 | Speed metal
268 | Symphonic metal
269 | Thrash metal
270 | J-Rock
271 | New Wave
272 | Paisley Underground
273 | Power pop
274 | Progressive rock
275 | Psychedelic rock
276 | Acid rock
277 | Punk rock
278 | Anarcho punk
279 | Crust punk
280 | Deathrock
281 | Hardcore punk
282 | Post-hardcore
283 | Pop punk
284 | Post-punk
285 | Psychobilly
286 | Rap rock
287 | Rapcore
288 | Rock and roll
289 | Soft rock
290 | Southern rock
291 | Surf rock
292 | World
293 | Worldbeat
294 | Afrobeat
295 |
--------------------------------------------------------------------------------
/categories/editor/templatetags/admin_tree_list_tags.py:
--------------------------------------------------------------------------------
1 | """Template tags used to render the tree editor."""
2 |
3 | from django.contrib.admin.templatetags.admin_list import _boolean_icon, result_headers
4 | from django.contrib.admin.utils import lookup_field
5 | from django.core.exceptions import ObjectDoesNotExist
6 | from django.db import models
7 | from django.template import Library
8 | from django.utils.encoding import force_str, smart_str
9 | from django.utils.html import conditional_escape, escape, escapejs, format_html
10 | from django.utils.safestring import mark_safe
11 |
12 | from categories.editor import settings
13 | from categories.editor.utils import display_for_field
14 |
15 | register = Library()
16 |
17 | TREE_LIST_RESULTS_TEMPLATE = "admin/editor/tree_list_results.html"
18 | if settings.IS_GRAPPELLI_INSTALLED:
19 | TREE_LIST_RESULTS_TEMPLATE = "admin/editor/grappelli_tree_list_results.html"
20 |
21 |
22 | def get_empty_value_display(cl):
23 | """Get the value to display when empty."""
24 | if hasattr(cl.model_admin, "get_empty_value_display"):
25 | return cl.model_admin.get_empty_value_display()
26 |
27 | # Django < 1.9
28 | from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
29 |
30 | return EMPTY_CHANGELIST_VALUE
31 |
32 |
33 | def items_for_tree_result(cl, result, form):
34 | """
35 | Generates the actual list of data.
36 | """
37 | first = True
38 | pk = cl.lookup_opts.pk.attname
39 | for field_name in cl.list_display:
40 | row_class = ""
41 | try:
42 | f, attr, value = lookup_field(field_name, result, cl.model_admin)
43 | except (AttributeError, ObjectDoesNotExist):
44 | result_repr = get_empty_value_display(cl)
45 | else:
46 | if f is None:
47 | allow_tags = getattr(attr, "allow_tags", False)
48 | boolean = getattr(attr, "boolean", False)
49 | if boolean:
50 | allow_tags = True
51 | result_repr = _boolean_icon(value)
52 | else:
53 | result_repr = smart_str(value)
54 | # Strip HTML tags in the resulting text, except if the
55 | # function has an "allow_tags" attribute set to True.
56 | if not allow_tags:
57 | result_repr = escape(result_repr)
58 | else:
59 | result_repr = mark_safe(result_repr)
60 | else:
61 | if value is None:
62 | result_repr = get_empty_value_display(cl)
63 | if hasattr(f, "rel") and isinstance(f.rel, models.ManyToOneRel):
64 | result_repr = escape(getattr(result, f.name))
65 | else:
66 | result_repr = display_for_field(value, f, "")
67 | if isinstance(f, models.DateField) or isinstance(f, models.TimeField):
68 | row_class = ' class="nowrap"'
69 |
70 | if force_str(result_repr) == "":
71 | result_repr = mark_safe(" ")
72 | # If list_display_links not defined, add the link tag to the first field
73 | if (first and not cl.list_display_links) or field_name in cl.list_display_links:
74 | table_tag = {True: "th", False: "td"}[first]
75 |
76 | url = cl.url_for_result(result)
77 | # Convert the pk to something that can be used in Javascript.
78 | # Problem cases are long ints (23L) and non-ASCII strings.
79 | if cl.to_field:
80 | attr = str(cl.to_field)
81 | else:
82 | attr = pk
83 | value = result.serializable_value(attr)
84 | result_id = repr(force_str(value))[1:]
85 | first = False
86 | result_id = escapejs(value)
87 | yield mark_safe(
88 | format_html(
89 | smart_str('<{}{}>{}{}>'),
90 | table_tag,
91 | row_class,
92 | url,
93 | (
94 | format_html(
95 | ' onclick="opener.dismissRelatedLookupPopup(window, ' ''{}'); return false;"',
96 | result_id,
97 | )
98 | if cl.is_popup
99 | else ""
100 | ),
101 | result_repr,
102 | table_tag,
103 | )
104 | )
105 |
106 | else:
107 | # By default the fields come from ModelAdmin.list_editable, but if we pull
108 | # the fields out of the form instead of list_editable custom admins
109 | # can provide fields on a per request basis
110 | if form and field_name in form.fields:
111 | bf = form[field_name]
112 | result_repr = mark_safe(force_str(bf.errors) + force_str(bf))
113 | else:
114 | result_repr = conditional_escape(result_repr)
115 | yield mark_safe(smart_str("%s | " % (row_class, result_repr)))
116 | if form and not form[cl.model._meta.pk.name].is_hidden:
117 | yield mark_safe(smart_str("%s | " % force_str(form[cl.model._meta.pk.name])))
118 |
119 |
120 | class TreeList(list):
121 | """A list subclass for tree result."""
122 |
123 | pass
124 |
125 |
126 | def tree_results(cl):
127 | """Generates a list of results for the tree."""
128 | if cl.formset:
129 | for res, form in zip(cl.result_list, cl.formset.forms):
130 | result = TreeList(items_for_tree_result(cl, res, form))
131 | if hasattr(res, "pk"):
132 | result.pk = res.pk
133 | if res.parent:
134 | result.parent_pk = res.parent.pk
135 | else:
136 | res.parent_pk = None
137 | yield result
138 | else:
139 | for res in cl.result_list:
140 | result = TreeList(items_for_tree_result(cl, res, None))
141 | if hasattr(res, "pk"):
142 | result.pk = res.pk
143 | if res.parent:
144 | result.parent_pk = res.parent.pk
145 | else:
146 | res.parent_pk = None
147 | yield result
148 |
149 |
150 | def result_tree_list(cl):
151 | """
152 | Displays the headers and data list together.
153 | """
154 | result = {"cl": cl, "result_headers": list(result_headers(cl)), "results": list(tree_results(cl))}
155 | return result
156 |
157 |
158 | result_tree_list = register.inclusion_tag(TREE_LIST_RESULTS_TEMPLATE)(result_tree_list)
159 |
--------------------------------------------------------------------------------