mirror of
https://github.com/jazzband/django-categories.git
synced 2026-03-16 22:30:24 +00:00
Adds doc strings for lots of functions.
This commit is contained in:
parent
f9a46848b2
commit
076debb44d
26 changed files with 380 additions and 155 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""Admin interface classes."""
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
@ -15,10 +16,9 @@ class NullTreeNodeChoiceField(forms.ModelChoiceField):
|
||||||
self.level_indicator = level_indicator
|
self.level_indicator = level_indicator
|
||||||
super(NullTreeNodeChoiceField, self).__init__(*args, **kwargs)
|
super(NullTreeNodeChoiceField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def label_from_instance(self, obj):
|
def label_from_instance(self, obj) -> str:
|
||||||
"""
|
"""
|
||||||
Creates labels which represent the tree level of each node when
|
Creates labels which represent the tree level of each node when generating option labels.
|
||||||
generating option labels.
|
|
||||||
"""
|
"""
|
||||||
return "%s %s" % (self.level_indicator * getattr(obj, obj._mptt_meta.level_attr), obj)
|
return "%s %s" % (self.level_indicator * getattr(obj, obj._mptt_meta.level_attr), obj)
|
||||||
|
|
||||||
|
|
@ -27,15 +27,20 @@ if RELATION_MODELS:
|
||||||
from .models import CategoryRelation
|
from .models import CategoryRelation
|
||||||
|
|
||||||
class InlineCategoryRelation(GenericCollectionTabularInline):
|
class InlineCategoryRelation(GenericCollectionTabularInline):
|
||||||
|
"""The inline admin panel for category relations."""
|
||||||
|
|
||||||
model = CategoryRelation
|
model = CategoryRelation
|
||||||
|
|
||||||
|
|
||||||
class CategoryAdminForm(CategoryBaseAdminForm):
|
class CategoryAdminForm(CategoryBaseAdminForm):
|
||||||
|
"""The form for a category in the admin."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Category
|
model = Category
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
def clean_alternate_title(self):
|
def clean_alternate_title(self) -> str:
|
||||||
|
"""Return either the name or alternate title for the category."""
|
||||||
if self.instance is None or not self.cleaned_data["alternate_title"]:
|
if self.instance is None or not self.cleaned_data["alternate_title"]:
|
||||||
return self.cleaned_data["name"]
|
return self.cleaned_data["name"]
|
||||||
else:
|
else:
|
||||||
|
|
@ -43,6 +48,8 @@ class CategoryAdminForm(CategoryBaseAdminForm):
|
||||||
|
|
||||||
|
|
||||||
class CategoryAdmin(CategoryBaseAdmin):
|
class CategoryAdmin(CategoryBaseAdmin):
|
||||||
|
"""Admin for categories."""
|
||||||
|
|
||||||
form = CategoryAdminForm
|
form = CategoryAdminForm
|
||||||
list_display = ("name", "alternate_title", "active")
|
list_display = ("name", "alternate_title", "active")
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
|
"""Django application setup."""
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class CategoriesConfig(AppConfig):
|
class CategoriesConfig(AppConfig):
|
||||||
|
"""Application configuration for categories."""
|
||||||
|
|
||||||
name = "categories"
|
name = "categories"
|
||||||
verbose_name = "Categories"
|
verbose_name = "Categories"
|
||||||
|
|
||||||
|
|
@ -12,6 +15,7 @@ class CategoriesConfig(AppConfig):
|
||||||
class_prepared.connect(handle_class_prepared)
|
class_prepared.connect(handle_class_prepared)
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
"""Migrate the app after it is ready."""
|
||||||
from django.db.models.signals import post_migrate
|
from django.db.models.signals import post_migrate
|
||||||
|
|
||||||
from .migration import migrate_app
|
from .migration import migrate_app
|
||||||
|
|
@ -21,7 +25,7 @@ class CategoriesConfig(AppConfig):
|
||||||
|
|
||||||
def handle_class_prepared(sender, **kwargs):
|
def handle_class_prepared(sender, **kwargs):
|
||||||
"""
|
"""
|
||||||
See if this class needs registering of fields
|
See if this class needs registering of fields.
|
||||||
"""
|
"""
|
||||||
from .registration import registry
|
from .registration import registry
|
||||||
from .settings import FK_REGISTRY, M2M_REGISTRY
|
from .settings import FK_REGISTRY, M2M_REGISTRY
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
"""
|
"""
|
||||||
This is the base class on which to build a hierarchical category-like model
|
This is the base class on which to build a hierarchical category-like model.
|
||||||
with customizable metadata and its own name space.
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
It provides customizable metadata and its own name space.
|
||||||
|
"""
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
@ -17,31 +16,24 @@ from slugify import slugify
|
||||||
from .editor.tree_editor import TreeEditor
|
from .editor.tree_editor import TreeEditor
|
||||||
from .settings import ALLOW_SLUG_CHANGE, SLUG_TRANSLITERATOR
|
from .settings import ALLOW_SLUG_CHANGE, SLUG_TRANSLITERATOR
|
||||||
|
|
||||||
if sys.version_info[0] < 3: # Remove this after dropping support of Python 2
|
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
|
||||||
else:
|
|
||||||
|
|
||||||
def python_2_unicode_compatible(x):
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryManager(models.Manager):
|
class CategoryManager(models.Manager):
|
||||||
"""
|
"""
|
||||||
A manager that adds an "active()" method for all active categories
|
A manager that adds an "active()" method for all active categories.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def active(self):
|
def active(self):
|
||||||
"""
|
"""
|
||||||
Only categories that are active
|
Only categories that are active.
|
||||||
"""
|
"""
|
||||||
return self.get_queryset().filter(active=True)
|
return self.get_queryset().filter(active=True)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class CategoryBase(MPTTModel):
|
class CategoryBase(MPTTModel):
|
||||||
"""
|
"""
|
||||||
This base model includes the absolute bare bones fields and methods. One
|
This base model includes the absolute bare-bones fields and methods.
|
||||||
could simply subclass this model and do nothing else and it should work.
|
|
||||||
|
One could simply subclass this model, do nothing else, and it should work.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parent = TreeForeignKey(
|
parent = TreeForeignKey(
|
||||||
|
|
@ -61,9 +53,15 @@ class CategoryBase(MPTTModel):
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
Save the category.
|
||||||
|
|
||||||
While you can activate an item without activating its descendants,
|
While you can activate an item without activating its descendants,
|
||||||
It doesn't make sense that you can deactivate an item and have its
|
It doesn't make sense that you can deactivate an item and have its
|
||||||
decendants remain active.
|
decendants remain active.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: generic args
|
||||||
|
kwargs: generic keyword arguments
|
||||||
"""
|
"""
|
||||||
if not self.slug:
|
if not self.slug:
|
||||||
self.slug = slugify(SLUG_TRANSLITERATOR(self.name))[:50]
|
self.slug = slugify(SLUG_TRANSLITERATOR(self.name))[:50]
|
||||||
|
|
@ -95,14 +93,16 @@ class CategoryBase(MPTTModel):
|
||||||
|
|
||||||
|
|
||||||
class CategoryBaseAdminForm(forms.ModelForm):
|
class CategoryBaseAdminForm(forms.ModelForm):
|
||||||
|
"""Base admin form for categories."""
|
||||||
|
|
||||||
def clean_slug(self):
|
def clean_slug(self):
|
||||||
if not self.cleaned_data.get("slug", None):
|
"""Prune and transliterate the slug."""
|
||||||
if self.instance is None or not ALLOW_SLUG_CHANGE:
|
if not self.cleaned_data.get("slug", None) and (self.instance is None or not ALLOW_SLUG_CHANGE):
|
||||||
self.cleaned_data["slug"] = slugify(SLUG_TRANSLITERATOR(self.cleaned_data["name"]))
|
self.cleaned_data["slug"] = slugify(SLUG_TRANSLITERATOR(self.cleaned_data["name"]))
|
||||||
return self.cleaned_data["slug"][:50]
|
return self.cleaned_data["slug"][:50]
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
"""Clean the data passed from the admin interface."""
|
||||||
super(CategoryBaseAdminForm, self).clean()
|
super(CategoryBaseAdminForm, self).clean()
|
||||||
|
|
||||||
if not self.is_valid():
|
if not self.is_valid():
|
||||||
|
|
@ -141,6 +141,8 @@ class CategoryBaseAdminForm(forms.ModelForm):
|
||||||
|
|
||||||
|
|
||||||
class CategoryBaseAdmin(TreeEditor, admin.ModelAdmin):
|
class CategoryBaseAdmin(TreeEditor, admin.ModelAdmin):
|
||||||
|
"""Base admin class for categories."""
|
||||||
|
|
||||||
form = CategoryBaseAdminForm
|
form = CategoryBaseAdminForm
|
||||||
list_display = ("name", "active")
|
list_display = ("name", "active")
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
|
|
@ -149,14 +151,15 @@ class CategoryBaseAdmin(TreeEditor, admin.ModelAdmin):
|
||||||
actions = ["activate", "deactivate"]
|
actions = ["activate", "deactivate"]
|
||||||
|
|
||||||
def get_actions(self, request):
|
def get_actions(self, request):
|
||||||
|
"""Get available actions for the admin interface."""
|
||||||
actions = super(CategoryBaseAdmin, self).get_actions(request)
|
actions = super(CategoryBaseAdmin, self).get_actions(request)
|
||||||
if "delete_selected" in actions:
|
if "delete_selected" in actions:
|
||||||
del actions["delete_selected"]
|
del actions["delete_selected"]
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
def deactivate(self, request, queryset):
|
def deactivate(self, request, queryset): # NOQA: queryset is not used.
|
||||||
"""
|
"""
|
||||||
Set active to False for selected items
|
Set active to False for selected items.
|
||||||
"""
|
"""
|
||||||
selected_cats = self.model.objects.filter(pk__in=[int(x) for x in request.POST.getlist("_selected_action")])
|
selected_cats = self.model.objects.filter(pk__in=[int(x) for x in request.POST.getlist("_selected_action")])
|
||||||
|
|
||||||
|
|
@ -168,9 +171,9 @@ class CategoryBaseAdmin(TreeEditor, admin.ModelAdmin):
|
||||||
|
|
||||||
deactivate.short_description = _("Deactivate selected categories and their children")
|
deactivate.short_description = _("Deactivate selected categories and their children")
|
||||||
|
|
||||||
def activate(self, request, queryset):
|
def activate(self, request, queryset): # NOQA: queryset is not used.
|
||||||
"""
|
"""
|
||||||
Set active to True for selected items
|
Set active to True for selected items.
|
||||||
"""
|
"""
|
||||||
selected_cats = self.model.objects.filter(pk__in=[int(x) for x in request.POST.getlist("_selected_action")])
|
selected_cats = self.model.objects.filter(pk__in=[int(x) for x in request.POST.getlist("_selected_action")])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
# Placeholder for Django
|
"""Placeholder for Django."""
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""Settings management for the editor."""
|
||||||
import django
|
import django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""Template tags used to render the tree editor."""
|
||||||
import django
|
import django
|
||||||
from django.contrib.admin.templatetags.admin_list import _boolean_icon, result_headers
|
from django.contrib.admin.templatetags.admin_list import _boolean_icon, result_headers
|
||||||
from django.contrib.admin.utils import lookup_field
|
from django.contrib.admin.utils import lookup_field
|
||||||
|
|
@ -19,13 +20,14 @@ if settings.IS_GRAPPELLI_INSTALLED:
|
||||||
|
|
||||||
|
|
||||||
def get_empty_value_display(cl):
|
def get_empty_value_display(cl):
|
||||||
|
"""Get the value to display when empty."""
|
||||||
if hasattr(cl.model_admin, "get_empty_value_display"):
|
if hasattr(cl.model_admin, "get_empty_value_display"):
|
||||||
return cl.model_admin.get_empty_value_display()
|
return cl.model_admin.get_empty_value_display()
|
||||||
else:
|
|
||||||
# Django < 1.9
|
|
||||||
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
|
|
||||||
|
|
||||||
return EMPTY_CHANGELIST_VALUE
|
# Django < 1.9
|
||||||
|
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
|
||||||
|
|
||||||
|
return EMPTY_CHANGELIST_VALUE
|
||||||
|
|
||||||
|
|
||||||
def items_for_tree_result(cl, result, form):
|
def items_for_tree_result(cl, result, form):
|
||||||
|
|
@ -130,10 +132,13 @@ def items_for_tree_result(cl, result, form):
|
||||||
|
|
||||||
|
|
||||||
class TreeList(list):
|
class TreeList(list):
|
||||||
|
"""A list subclass for tree result."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def tree_results(cl):
|
def tree_results(cl):
|
||||||
|
"""Generates a list of results for the tree."""
|
||||||
if cl.formset:
|
if cl.formset:
|
||||||
for res, form in zip(cl.result_list, cl.formset.forms):
|
for res, form in zip(cl.result_list, cl.formset.forms):
|
||||||
result = TreeList(items_for_tree_result(cl, res, form))
|
result = TreeList(items_for_tree_result(cl, res, form))
|
||||||
|
|
@ -158,7 +163,7 @@ def tree_results(cl):
|
||||||
|
|
||||||
def result_tree_list(cl):
|
def result_tree_list(cl):
|
||||||
"""
|
"""
|
||||||
Displays the headers and data list together
|
Displays the headers and data list together.
|
||||||
"""
|
"""
|
||||||
import django
|
import django
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
"""Classes for representing tree structures in Django's admin."""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import django
|
import django
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin.options import IncorrectLookupParameters
|
from django.contrib.admin.options import IncorrectLookupParameters
|
||||||
|
|
@ -12,9 +15,9 @@ from . import settings
|
||||||
|
|
||||||
class TreeEditorQuerySet(QuerySet):
|
class TreeEditorQuerySet(QuerySet):
|
||||||
"""
|
"""
|
||||||
The TreeEditorQuerySet is a special query set used only in the TreeEditor
|
A special query set used only in the TreeEditor ChangeList page.
|
||||||
ChangeList page. The only difference to a regular QuerySet is that it
|
|
||||||
will enforce:
|
The only difference to a regular QuerySet is that it will enforce:
|
||||||
|
|
||||||
(a) The result is ordered in correct tree order so that
|
(a) The result is ordered in correct tree order so that
|
||||||
the TreeAdmin works all right.
|
the TreeAdmin works all right.
|
||||||
|
|
@ -25,6 +28,7 @@ class TreeEditorQuerySet(QuerySet):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def iterator(self):
|
def iterator(self):
|
||||||
|
"""Iterates through the items in thee query set."""
|
||||||
qs = self
|
qs = self
|
||||||
# Reaching into the bowels of query sets to find out whether the qs is
|
# Reaching into the bowels of query sets to find out whether the qs is
|
||||||
# actually filtered and we need to do the INCLUDE_ANCESTORS dance at all.
|
# actually filtered and we need to do the INCLUDE_ANCESTORS dance at all.
|
||||||
|
|
@ -54,18 +58,27 @@ class TreeEditorQuerySet(QuerySet):
|
||||||
# def __getitem__(self, index):
|
# def __getitem__(self, index):
|
||||||
# return self # Don't even try to slice
|
# return self # Don't even try to slice
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs) -> Any:
|
||||||
"""
|
"""
|
||||||
Quick and dirty hack to fix change_view and delete_view; they use
|
Quick and dirty hack to fix change_view and delete_view.
|
||||||
self.queryset(request).get(...) to get the object they should work
|
|
||||||
with. Our modifications to the queryset when INCLUDE_ANCESTORS is
|
They use ``self.queryset(request).get(...)`` to get the object they should work
|
||||||
enabled make get() fail often with a MultipleObjectsReturned
|
with. Our modifications to the queryset when ``INCLUDE_ANCESTORS`` is enabled make ``get()``
|
||||||
exception.
|
fail often with a ``MultipleObjectsReturned`` exception.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: generic arguments
|
||||||
|
kwargs: generic keyword arguments
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The object they should work with.
|
||||||
"""
|
"""
|
||||||
return self.model._default_manager.get(*args, **kwargs)
|
return self.model._default_manager.get(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class TreeChangeList(ChangeList):
|
class TreeChangeList(ChangeList):
|
||||||
|
"""A change list for a tree."""
|
||||||
|
|
||||||
def _get_default_ordering(self):
|
def _get_default_ordering(self):
|
||||||
if django.VERSION[0] == 1 and django.VERSION[1] < 4:
|
if django.VERSION[0] == 1 and django.VERSION[1] < 4:
|
||||||
return "", "" # ('tree_id', 'lft')
|
return "", "" # ('tree_id', 'lft')
|
||||||
|
|
@ -73,17 +86,31 @@ class TreeChangeList(ChangeList):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_ordering(self, request=None, queryset=None):
|
def get_ordering(self, request=None, queryset=None):
|
||||||
|
"""
|
||||||
|
Return ordering information for the change list.
|
||||||
|
|
||||||
|
Always returns empty/default ordering.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The incoming request.
|
||||||
|
queryset: The current queryset
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Either a tuple of empty strings or an empty list.
|
||||||
|
"""
|
||||||
if django.VERSION[0] == 1 and django.VERSION[1] < 4:
|
if django.VERSION[0] == 1 and django.VERSION[1] < 4:
|
||||||
return "", "" # ('tree_id', 'lft')
|
return "", "" # ('tree_id', 'lft')
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_queryset(self, *args, **kwargs):
|
def get_queryset(self, *args, **kwargs):
|
||||||
qs = super(TreeChangeList, self).get_queryset(*args, **kwargs).order_by("tree_id", "lft")
|
"""Return a queryset."""
|
||||||
return qs
|
return super(TreeChangeList, self).get_queryset(*args, **kwargs).order_by("tree_id", "lft")
|
||||||
|
|
||||||
|
|
||||||
class TreeEditor(admin.ModelAdmin):
|
class TreeEditor(admin.ModelAdmin):
|
||||||
|
"""A tree editor view for Django's admin."""
|
||||||
|
|
||||||
list_per_page = 999999999 # We can't have pagination
|
list_per_page = 999999999 # We can't have pagination
|
||||||
list_max_show_all = 200 # new in django 1.4
|
list_max_show_all = 200 # new in django 1.4
|
||||||
|
|
||||||
|
|
@ -120,7 +147,7 @@ class TreeEditor(admin.ModelAdmin):
|
||||||
return TreeChangeList
|
return TreeChangeList
|
||||||
|
|
||||||
def old_changelist_view(self, request, extra_context=None):
|
def old_changelist_view(self, request, extra_context=None):
|
||||||
"The 'change list' admin view for this model."
|
"""The 'change list' admin view for this model."""
|
||||||
from django.contrib.admin.views.main import ERROR_FLAG
|
from django.contrib.admin.views.main import ERROR_FLAG
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
|
|
@ -302,8 +329,7 @@ class TreeEditor(admin.ModelAdmin):
|
||||||
|
|
||||||
def changelist_view(self, request, extra_context=None, *args, **kwargs):
|
def changelist_view(self, request, extra_context=None, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Handle the changelist view, the django view for the model instances
|
Handle the changelist view, the django view for the model instances change list/actions page.
|
||||||
change list/actions page.
|
|
||||||
"""
|
"""
|
||||||
extra_context = extra_context or {}
|
extra_context = extra_context or {}
|
||||||
extra_context["EDITOR_MEDIA_PATH"] = settings.MEDIA_PATH
|
extra_context["EDITOR_MEDIA_PATH"] = settings.MEDIA_PATH
|
||||||
|
|
@ -312,10 +338,17 @@ class TreeEditor(admin.ModelAdmin):
|
||||||
# FIXME
|
# FIXME
|
||||||
return self.old_changelist_view(request, extra_context)
|
return self.old_changelist_view(request, extra_context)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request) -> TreeEditorQuerySet:
|
||||||
"""
|
"""
|
||||||
Returns a QuerySet of all model instances that can be edited by the
|
Returns a QuerySet of all model instances that can be edited by the admin site.
|
||||||
admin site. This is used by changelist_view.
|
|
||||||
|
This is used by changelist_view.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: the incoming request.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A QuerySet of editable model instances
|
||||||
"""
|
"""
|
||||||
qs = self.model._default_manager.get_queryset()
|
qs = self.model._default_manager.get_queryset()
|
||||||
qs.__class__ = TreeEditorQuerySet
|
qs.__class__ = TreeEditorQuerySet
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
"""
|
"""
|
||||||
Provides compatibility with Django 1.8
|
Provides compatibility with Django 1.8.
|
||||||
"""
|
"""
|
||||||
from django.contrib.admin.utils import display_for_field as _display_for_field
|
from django.contrib.admin.utils import display_for_field as _display_for_field
|
||||||
|
|
||||||
|
|
||||||
def display_for_field(value, field, empty_value_display=None):
|
def display_for_field(value, field, empty_value_display=None):
|
||||||
|
"""Compatility for displaying a field in Django 1.8."""
|
||||||
try:
|
try:
|
||||||
return _display_for_field(value, field, empty_value_display)
|
return _display_for_field(value, field, empty_value_display)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
|
"""Custom category fields for other models."""
|
||||||
from django.db.models import ForeignKey, ManyToManyField
|
from django.db.models import ForeignKey, ManyToManyField
|
||||||
|
|
||||||
|
|
||||||
class CategoryM2MField(ManyToManyField):
|
class CategoryM2MField(ManyToManyField):
|
||||||
|
"""A many to many field to a Category model."""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
from .models import Category
|
from .models import Category
|
||||||
|
|
||||||
|
|
@ -11,18 +14,11 @@ class CategoryM2MField(ManyToManyField):
|
||||||
|
|
||||||
|
|
||||||
class CategoryFKField(ForeignKey):
|
class CategoryFKField(ForeignKey):
|
||||||
|
"""A foreign key to the Category model."""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
from .models import Category
|
from .models import Category
|
||||||
|
|
||||||
if "to" in kwargs:
|
if "to" in kwargs:
|
||||||
kwargs.pop("to")
|
kwargs.pop("to")
|
||||||
super(CategoryFKField, self).__init__(to=Category, **kwargs)
|
super(CategoryFKField, self).__init__(to=Category, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from south.modelsinspector import add_introspection_rules
|
|
||||||
|
|
||||||
add_introspection_rules([], [r"^categories\.fields\.CategoryFKField"])
|
|
||||||
add_introspection_rules([], [r"^categories\.fields\.CategoryM2MField"])
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""Special helpers for generic collections."""
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
@ -6,10 +7,13 @@ from django.urls import NoReverseMatch, reverse
|
||||||
|
|
||||||
|
|
||||||
class GenericCollectionInlineModelAdmin(admin.options.InlineModelAdmin):
|
class GenericCollectionInlineModelAdmin(admin.options.InlineModelAdmin):
|
||||||
|
"""Inline admin for generic model collections."""
|
||||||
|
|
||||||
ct_field = "content_type"
|
ct_field = "content_type"
|
||||||
ct_fk_field = "object_id"
|
ct_fk_field = "object_id"
|
||||||
|
|
||||||
def get_content_types(self):
|
def get_content_types(self):
|
||||||
|
"""Get the content types supported by this collection."""
|
||||||
ctypes = ContentType.objects.all().order_by("id").values_list("id", "app_label", "model")
|
ctypes = ContentType.objects.all().order_by("id").values_list("id", "app_label", "model")
|
||||||
elements = {}
|
elements = {}
|
||||||
for x, y, z in ctypes:
|
for x, y, z in ctypes:
|
||||||
|
|
@ -20,6 +24,7 @@ class GenericCollectionInlineModelAdmin(admin.options.InlineModelAdmin):
|
||||||
return json.dumps(elements)
|
return json.dumps(elements)
|
||||||
|
|
||||||
def get_formset(self, request, obj=None, **kwargs):
|
def get_formset(self, request, obj=None, **kwargs):
|
||||||
|
"""Get the formset for the generic collection."""
|
||||||
result = super(GenericCollectionInlineModelAdmin, self).get_formset(request, obj, **kwargs)
|
result = super(GenericCollectionInlineModelAdmin, self).get_formset(request, obj, **kwargs)
|
||||||
result.content_types = self.get_content_types()
|
result.content_types = self.get_content_types()
|
||||||
result.ct_fk_field = self.ct_fk_field
|
result.ct_fk_field = self.ct_fk_field
|
||||||
|
|
@ -30,8 +35,12 @@ class GenericCollectionInlineModelAdmin(admin.options.InlineModelAdmin):
|
||||||
|
|
||||||
|
|
||||||
class GenericCollectionTabularInline(GenericCollectionInlineModelAdmin):
|
class GenericCollectionTabularInline(GenericCollectionInlineModelAdmin):
|
||||||
|
"""Tabular model admin for a generic collection."""
|
||||||
|
|
||||||
template = "admin/edit_inline/gen_coll_tabular.html"
|
template = "admin/edit_inline/gen_coll_tabular.html"
|
||||||
|
|
||||||
|
|
||||||
class GenericCollectionStackedInline(GenericCollectionInlineModelAdmin):
|
class GenericCollectionStackedInline(GenericCollectionInlineModelAdmin):
|
||||||
|
"""Stacked model admin for a generic collection."""
|
||||||
|
|
||||||
template = "admin/edit_inline/gen_coll_stacked.html"
|
template = "admin/edit_inline/gen_coll_stacked.html"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
"""The add_category_fields command."""
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""
|
"""
|
||||||
Alter one or more models' tables with the registered attributes
|
Alter one or more models' tables with the registered attributes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
help = "Alter the tables for all registered models, or just specified models"
|
help = "Alter the tables for all registered models, or just specified models"
|
||||||
|
|
@ -12,13 +13,13 @@ class Command(BaseCommand):
|
||||||
requires_system_checks = False
|
requires_system_checks = False
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
|
"""Add app_names argument to the command."""
|
||||||
parser.add_argument("app_names", nargs="*")
|
parser.add_argument("app_names", nargs="*")
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
"""
|
"""
|
||||||
Alter the tables
|
Alter the tables.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from categories.migration import migrate_app
|
from categories.migration import migrate_app
|
||||||
from categories.settings import MODEL_REGISTRY
|
from categories.settings import MODEL_REGISTRY
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
"""Alter one or more models' tables with the registered attributes."""
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""
|
"""
|
||||||
Alter one or more models' tables with the registered attributes
|
Alter one or more models' tables with the registered attributes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
help = "Drop the given field from the given model's table"
|
help = "Drop the given field from the given model's table"
|
||||||
|
|
@ -12,13 +13,14 @@ class Command(BaseCommand):
|
||||||
requires_system_checks = False
|
requires_system_checks = False
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
|
"""Add app_name, model_name, and field_name arguments to the command."""
|
||||||
parser.add_argument("app_name")
|
parser.add_argument("app_name")
|
||||||
parser.add_argument("model_name")
|
parser.add_argument("model_name")
|
||||||
parser.add_argument("field_name")
|
parser.add_argument("field_name")
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
"""
|
"""
|
||||||
Alter the tables
|
Alter the tables.
|
||||||
"""
|
"""
|
||||||
from categories.migration import drop_field
|
from categories.migration import drop_field
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
"""Import category trees from a file."""
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
|
@ -16,7 +18,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
def get_indent(self, string):
|
def get_indent(self, string):
|
||||||
"""
|
"""
|
||||||
Look through the string and count the spaces
|
Look through the string and count the spaces.
|
||||||
"""
|
"""
|
||||||
indent_amt = 0
|
indent_amt = 0
|
||||||
|
|
||||||
|
|
@ -31,7 +33,7 @@ class Command(BaseCommand):
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def make_category(self, string, parent=None, order=1):
|
def make_category(self, string, parent=None, order=1):
|
||||||
"""
|
"""
|
||||||
Make and save a category object from a string
|
Make and save a category object from a string.
|
||||||
"""
|
"""
|
||||||
cat = Category(
|
cat = Category(
|
||||||
name=string.strip(),
|
name=string.strip(),
|
||||||
|
|
@ -48,12 +50,12 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
def parse_lines(self, lines):
|
def parse_lines(self, lines):
|
||||||
"""
|
"""
|
||||||
Do the work of parsing each line
|
Do the work of parsing each line.
|
||||||
"""
|
"""
|
||||||
indent = ""
|
indent = ""
|
||||||
level = 0
|
level = 0
|
||||||
|
|
||||||
if lines[0][0] == " " or lines[0][0] == "\t":
|
if lines[0][0] in [" ", "\t"]:
|
||||||
raise CommandError("The first line in the file cannot start with a space or tab.")
|
raise CommandError("The first line in the file cannot start with a space or tab.")
|
||||||
|
|
||||||
# This keeps track of the current parents at a given level
|
# This keeps track of the current parents at a given level
|
||||||
|
|
@ -62,10 +64,10 @@ class Command(BaseCommand):
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if len(line) == 0:
|
if len(line) == 0:
|
||||||
continue
|
continue
|
||||||
if line[0] == " " or line[0] == "\t":
|
if line[0] in [" ", "\t"]:
|
||||||
if indent == "":
|
if indent == "":
|
||||||
indent = self.get_indent(line)
|
indent = self.get_indent(line)
|
||||||
elif not line[0] in indent:
|
elif line[0] not in indent:
|
||||||
raise CommandError("You can't mix spaces and tabs for indents")
|
raise CommandError("You can't mix spaces and tabs for indents")
|
||||||
level = line.count(indent)
|
level = line.count(indent)
|
||||||
current_parents[level] = self.make_category(line, parent=current_parents[level - 1])
|
current_parents[level] = self.make_category(line, parent=current_parents[level - 1])
|
||||||
|
|
@ -76,7 +78,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
def handle(self, *file_paths, **options):
|
def handle(self, *file_paths, **options):
|
||||||
"""
|
"""
|
||||||
Handle the basic import
|
Handle the basic import.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
@ -84,8 +86,6 @@ class Command(BaseCommand):
|
||||||
if not os.path.isfile(file_path):
|
if not os.path.isfile(file_path):
|
||||||
print("File %s not found." % file_path)
|
print("File %s not found." % file_path)
|
||||||
continue
|
continue
|
||||||
f = open(file_path, "r")
|
with open(file_path, "r") as f:
|
||||||
data = f.readlines()
|
data = f.readlines()
|
||||||
f.close()
|
|
||||||
|
|
||||||
self.parse_lines(data)
|
self.parse_lines(data)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""Adds and removes category relations on the database."""
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import connection, transaction
|
from django.db import connection, transaction
|
||||||
from django.db.utils import ProgrammingError
|
from django.db.utils import ProgrammingError
|
||||||
|
|
@ -5,7 +6,7 @@ from django.db.utils import ProgrammingError
|
||||||
|
|
||||||
def table_exists(table_name):
|
def table_exists(table_name):
|
||||||
"""
|
"""
|
||||||
Check if a table exists in the database
|
Check if a table exists in the database.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -33,7 +34,7 @@ def field_exists(app_name, model_name, field_name):
|
||||||
|
|
||||||
def drop_field(app_name, model_name, field_name):
|
def drop_field(app_name, model_name, field_name):
|
||||||
"""
|
"""
|
||||||
Drop the given field from the app's model
|
Drop the given field from the app's model.
|
||||||
"""
|
"""
|
||||||
app_config = apps.get_app_config(app_name)
|
app_config = apps.get_app_config(app_name)
|
||||||
model = app_config.get_model(model_name)
|
model = app_config.get_model(model_name)
|
||||||
|
|
@ -44,7 +45,7 @@ def drop_field(app_name, model_name, field_name):
|
||||||
|
|
||||||
def migrate_app(sender, *args, **kwargs):
|
def migrate_app(sender, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Migrate all models of this app registered
|
Migrate all models of this app registered.
|
||||||
"""
|
"""
|
||||||
from .registration import registry
|
from .registration import registry
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""Category models."""
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
@ -26,6 +27,8 @@ STORAGE = get_storage_class(THUMBNAIL_STORAGE)
|
||||||
|
|
||||||
|
|
||||||
class Category(CategoryBase):
|
class Category(CategoryBase):
|
||||||
|
"""A basic category model."""
|
||||||
|
|
||||||
thumbnail = models.FileField(
|
thumbnail = models.FileField(
|
||||||
upload_to=THUMBNAIL_UPLOAD_PATH,
|
upload_to=THUMBNAIL_UPLOAD_PATH,
|
||||||
null=True,
|
null=True,
|
||||||
|
|
@ -53,10 +56,11 @@ class Category(CategoryBase):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def short_title(self):
|
def short_title(self):
|
||||||
|
"""Return the name."""
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
"""Return a path"""
|
"""Return a path."""
|
||||||
from django.urls import NoReverseMatch
|
from django.urls import NoReverseMatch
|
||||||
|
|
||||||
if self.alternate_url:
|
if self.alternate_url:
|
||||||
|
|
@ -74,17 +78,18 @@ class Category(CategoryBase):
|
||||||
|
|
||||||
def get_related_content_type(self, content_type):
|
def get_related_content_type(self, content_type):
|
||||||
"""
|
"""
|
||||||
Get all related items of the specified content type
|
Get all related items of the specified content type.
|
||||||
"""
|
"""
|
||||||
return self.categoryrelation_set.filter(content_type__name=content_type)
|
return self.categoryrelation_set.filter(content_type__name=content_type)
|
||||||
|
|
||||||
def get_relation_type(self, relation_type):
|
def get_relation_type(self, relation_type):
|
||||||
"""
|
"""
|
||||||
Get all relations of the specified relation type
|
Get all relations of the specified relation type.
|
||||||
"""
|
"""
|
||||||
return self.categoryrelation_set.filter(relation_type=relation_type)
|
return self.categoryrelation_set.filter(relation_type=relation_type)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
"""Save the category."""
|
||||||
if self.thumbnail:
|
if self.thumbnail:
|
||||||
width, height = get_image_dimensions(self.thumbnail.file)
|
width, height = get_image_dimensions(self.thumbnail.file)
|
||||||
else:
|
else:
|
||||||
|
|
@ -110,6 +115,8 @@ else:
|
||||||
|
|
||||||
|
|
||||||
class CategoryRelationManager(models.Manager):
|
class CategoryRelationManager(models.Manager):
|
||||||
|
"""Custom access functions for category relations."""
|
||||||
|
|
||||||
def get_content_type(self, content_type):
|
def get_content_type(self, content_type):
|
||||||
"""
|
"""
|
||||||
Get all the items of the given content type related to this item.
|
Get all the items of the given content type related to this item.
|
||||||
|
|
@ -126,7 +133,7 @@ class CategoryRelationManager(models.Manager):
|
||||||
|
|
||||||
|
|
||||||
class CategoryRelation(models.Model):
|
class CategoryRelation(models.Model):
|
||||||
"""Related category item"""
|
"""Related category item."""
|
||||||
|
|
||||||
category = models.ForeignKey(Category, verbose_name=_("category"), on_delete=models.CASCADE)
|
category = models.ForeignKey(Category, verbose_name=_("category"), on_delete=models.CASCADE)
|
||||||
content_type = models.ForeignKey(
|
content_type = models.ForeignKey(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
"""
|
"""
|
||||||
These functions handle the adding of fields to other models
|
These functions handle the adding of fields to other models.
|
||||||
"""
|
"""
|
||||||
|
from typing import Optional, Type, Union
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
|
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
|
||||||
from django.db.models import CASCADE, ForeignKey, ManyToManyField
|
from django.db.models import CASCADE, ForeignKey, ManyToManyField
|
||||||
|
|
||||||
|
|
@ -16,17 +20,26 @@ FIELD_TYPES = {
|
||||||
|
|
||||||
|
|
||||||
class Registry(object):
|
class Registry(object):
|
||||||
|
"""Keeps track of fields and models registered."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._field_registry = {}
|
self._field_registry = {}
|
||||||
self._model_registry = {}
|
self._model_registry = {}
|
||||||
|
|
||||||
def register_model(self, app, model_name, field_type, field_definitions):
|
def register_model(
|
||||||
|
self, app: str, model_name, field_type: str, field_definitions: Union[str, collections.Iterable]
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Process for Django 1.7 +
|
Registration process for Django 1.7+.
|
||||||
app: app name/label
|
|
||||||
model_name: name of the model
|
Args:
|
||||||
field_definitions: a string, tuple or list of field configurations
|
app: app name/label
|
||||||
field_type: either 'ForeignKey' or 'ManyToManyField'
|
model_name: name of the model
|
||||||
|
field_definitions: a string, tuple or list of field configurations
|
||||||
|
field_type: either 'ForeignKey' or 'ManyToManyField'
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ImproperlyConfigured: For incorrect parameter types or missing model.
|
||||||
"""
|
"""
|
||||||
import collections
|
import collections
|
||||||
|
|
||||||
|
|
@ -93,14 +106,26 @@ class Registry(object):
|
||||||
self._field_registry[registry_name] = FIELD_TYPES[field_type](**extra_params)
|
self._field_registry[registry_name] = FIELD_TYPES[field_type](**extra_params)
|
||||||
self._field_registry[registry_name].contribute_to_class(model, field_name)
|
self._field_registry[registry_name].contribute_to_class(model, field_name)
|
||||||
|
|
||||||
def register_m2m(self, model, field_name="categories", extra_params={}):
|
def register_m2m(self, model, field_name: str = "categories", extra_params: Optional[dict] = None):
|
||||||
|
"""Register a field name to the model as a many to many field."""
|
||||||
|
extra_params = extra_params or {}
|
||||||
return self._register(model, field_name, extra_params, fields.CategoryM2MField)
|
return self._register(model, field_name, extra_params, fields.CategoryM2MField)
|
||||||
|
|
||||||
def register_fk(self, model, field_name="category", extra_params={}):
|
def register_fk(self, model, field_name: str = "category", extra_params: Optional[dict] = None):
|
||||||
|
"""Register a field name to the model as a foreign key."""
|
||||||
|
extra_params = extra_params or {}
|
||||||
return self._register(model, field_name, extra_params, fields.CategoryFKField)
|
return self._register(model, field_name, extra_params, fields.CategoryFKField)
|
||||||
|
|
||||||
def _register(self, model, field_name, extra_params={}, field=fields.CategoryFKField):
|
def _register(
|
||||||
|
self,
|
||||||
|
model,
|
||||||
|
field_name: str,
|
||||||
|
extra_params: Optional[dict] = None,
|
||||||
|
field: Type = fields.CategoryFKField,
|
||||||
|
):
|
||||||
|
"""Does the heavy lifting for registering a field to a model."""
|
||||||
app_label = model._meta.app_label
|
app_label = model._meta.app_label
|
||||||
|
extra_params = extra_params or {}
|
||||||
registry_name = ".".join((app_label, model.__name__, field_name)).lower()
|
registry_name = ".".join((app_label, model.__name__, field_name)).lower()
|
||||||
|
|
||||||
if registry_name in self._field_registry:
|
if registry_name in self._field_registry:
|
||||||
|
|
@ -122,7 +147,7 @@ registry = Registry()
|
||||||
|
|
||||||
def _process_registry(registry, call_func):
|
def _process_registry(registry, call_func):
|
||||||
"""
|
"""
|
||||||
Given a dictionary, and a registration function, process the registry
|
Given a dictionary, and a registration function, process the registry.
|
||||||
"""
|
"""
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""Manages settings for the categories application."""
|
||||||
import collections
|
import collections
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
"""Template tags for categories."""
|
||||||
|
from typing import Any, Type, Union
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.template import Node, TemplateSyntaxError, VariableDoesNotExist
|
from django.template import Node, TemplateSyntaxError, VariableDoesNotExist
|
||||||
|
|
@ -9,7 +12,6 @@ from mptt.templatetags.mptt_tags import (
|
||||||
tree_path,
|
tree_path,
|
||||||
)
|
)
|
||||||
from mptt.utils import drilldown_tree_for_node
|
from mptt.utils import drilldown_tree_for_node
|
||||||
from six import string_types
|
|
||||||
|
|
||||||
from categories.base import CategoryBase
|
from categories.base import CategoryBase
|
||||||
from categories.models import Category
|
from categories.models import Category
|
||||||
|
|
@ -21,7 +23,8 @@ register.filter(tree_info)
|
||||||
register.tag("full_tree_for_category", full_tree_for_model)
|
register.tag("full_tree_for_category", full_tree_for_model)
|
||||||
|
|
||||||
|
|
||||||
def resolve(var, context):
|
def resolve(var: Any, context: dict) -> Any:
|
||||||
|
"""Aggressively resolve a variable."""
|
||||||
try:
|
try:
|
||||||
return var.resolve(context)
|
return var.resolve(context)
|
||||||
except VariableDoesNotExist:
|
except VariableDoesNotExist:
|
||||||
|
|
@ -33,10 +36,11 @@ def resolve(var, context):
|
||||||
|
|
||||||
def get_cat_model(model):
|
def get_cat_model(model):
|
||||||
"""
|
"""
|
||||||
Return a class from a string or class
|
Return a class from a string or class.
|
||||||
"""
|
"""
|
||||||
|
model_class = None
|
||||||
try:
|
try:
|
||||||
if isinstance(model, string_types):
|
if isinstance(model, str):
|
||||||
model_class = apps.get_model(*model.split("."))
|
model_class = apps.get_model(*model.split("."))
|
||||||
elif issubclass(model, CategoryBase):
|
elif issubclass(model, CategoryBase):
|
||||||
model_class = model
|
model_class = model
|
||||||
|
|
@ -47,9 +51,16 @@ def get_cat_model(model):
|
||||||
return model_class
|
return model_class
|
||||||
|
|
||||||
|
|
||||||
def get_category(category_string, model=Category):
|
def get_category(category_string, model: Union[str, Type] = Category) -> Any:
|
||||||
"""
|
"""
|
||||||
Convert a string, including a path, and return the Category object
|
Convert a string, including a path, and return the Category object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
category_string: The name or path of the category
|
||||||
|
model: The name of or Category model to search in
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The found category object or None if no category was found
|
||||||
"""
|
"""
|
||||||
model_class = get_cat_model(model)
|
model_class = get_cat_model(model)
|
||||||
category = str(category_string).strip("'\"")
|
category = str(category_string).strip("'\"")
|
||||||
|
|
@ -66,31 +77,31 @@ def get_category(category_string, model=Category):
|
||||||
# if the parent matches the parent passed in the string
|
# if the parent matches the parent passed in the string
|
||||||
if len(categories) == 1:
|
if len(categories) == 1:
|
||||||
return categories[0]
|
return categories[0]
|
||||||
else:
|
|
||||||
for item in categories:
|
for item in categories:
|
||||||
if item.parent.name == cat_list[-2]:
|
if item.parent.name == cat_list[-2]:
|
||||||
return item
|
return item
|
||||||
except model_class.DoesNotExist:
|
except model_class.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class CategoryDrillDownNode(template.Node):
|
class CategoryDrillDownNode(template.Node):
|
||||||
|
"""A category drill down template node."""
|
||||||
|
|
||||||
def __init__(self, category, varname, model):
|
def __init__(self, category, varname, model):
|
||||||
self.category = category
|
self.category = category
|
||||||
self.varname = varname
|
self.varname = varname
|
||||||
self.model = model
|
self.model = model
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
|
"""Render this node."""
|
||||||
category = resolve(self.category, context)
|
category = resolve(self.category, context)
|
||||||
if isinstance(category, CategoryBase):
|
if isinstance(category, CategoryBase):
|
||||||
cat = category
|
cat = category
|
||||||
else:
|
else:
|
||||||
cat = get_category(category, self.model)
|
cat = get_category(category, self.model)
|
||||||
try:
|
try:
|
||||||
if cat is not None:
|
context[self.varname] = drilldown_tree_for_node(cat) if cat is not None else []
|
||||||
context[self.varname] = drilldown_tree_for_node(cat)
|
|
||||||
else:
|
|
||||||
context[self.varname] = []
|
|
||||||
except Exception:
|
except Exception:
|
||||||
context[self.varname] = []
|
context[self.varname] = []
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -99,8 +110,7 @@ class CategoryDrillDownNode(template.Node):
|
||||||
@register.tag
|
@register.tag
|
||||||
def get_category_drilldown(parser, token):
|
def get_category_drilldown(parser, token):
|
||||||
"""
|
"""
|
||||||
Retrieves the specified category, its ancestors and its immediate children
|
Retrieves the specified category, its ancestors and its immediate children as an Iterable.
|
||||||
as an iterable.
|
|
||||||
|
|
||||||
Syntax::
|
Syntax::
|
||||||
|
|
||||||
|
|
@ -117,6 +127,16 @@ def get_category_drilldown(parser, token):
|
||||||
Sets family to::
|
Sets family to::
|
||||||
|
|
||||||
Grandparent, Parent, Child 1, Child 2, Child n
|
Grandparent, Parent, Child 1, Child 2, Child n
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parser: The Django template parser.
|
||||||
|
token: The tag contents
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The recursive tree node.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TemplateSyntaxError: If the tag is malformed.
|
||||||
"""
|
"""
|
||||||
bits = token.split_contents()
|
bits = token.split_contents()
|
||||||
error_str = (
|
error_str = (
|
||||||
|
|
@ -124,13 +144,14 @@ def get_category_drilldown(parser, token):
|
||||||
'"category name" [using "app.Model"] as varname %%} or '
|
'"category name" [using "app.Model"] as varname %%} or '
|
||||||
"{%% %(tagname)s category_obj as varname %%}."
|
"{%% %(tagname)s category_obj as varname %%}."
|
||||||
)
|
)
|
||||||
|
varname = model = ""
|
||||||
if len(bits) == 4:
|
if len(bits) == 4:
|
||||||
if bits[2] != "as":
|
if bits[2] != "as":
|
||||||
raise template.TemplateSyntaxError(error_str % {"tagname": bits[0]})
|
raise template.TemplateSyntaxError(error_str % {"tagname": bits[0]})
|
||||||
if bits[2] == "as":
|
if bits[2] == "as":
|
||||||
varname = bits[3].strip("'\"")
|
varname = bits[3].strip("'\"")
|
||||||
model = "categories.category"
|
model = "categories.category"
|
||||||
if len(bits) == 6:
|
elif len(bits) == 6:
|
||||||
if bits[2] not in ("using", "as") or bits[4] not in ("using", "as"):
|
if bits[2] not in ("using", "as") or bits[4] not in ("using", "as"):
|
||||||
raise template.TemplateSyntaxError(error_str % {"tagname": bits[0]})
|
raise template.TemplateSyntaxError(error_str % {"tagname": bits[0]})
|
||||||
if bits[2] == "as":
|
if bits[2] == "as":
|
||||||
|
|
@ -139,6 +160,8 @@ def get_category_drilldown(parser, token):
|
||||||
if bits[2] == "using":
|
if bits[2] == "using":
|
||||||
varname = bits[5].strip("'\"")
|
varname = bits[5].strip("'\"")
|
||||||
model = bits[3].strip("'\"")
|
model = bits[3].strip("'\"")
|
||||||
|
else:
|
||||||
|
raise template.TemplateSyntaxError(error_str % {"tagname": bits[0]})
|
||||||
category = FilterExpression(bits[1], parser)
|
category = FilterExpression(bits[1], parser)
|
||||||
return CategoryDrillDownNode(category, varname, model)
|
return CategoryDrillDownNode(category, varname, model)
|
||||||
|
|
||||||
|
|
@ -146,10 +169,17 @@ def get_category_drilldown(parser, token):
|
||||||
@register.inclusion_tag("categories/breadcrumbs.html")
|
@register.inclusion_tag("categories/breadcrumbs.html")
|
||||||
def breadcrumbs(category_string, separator=" > ", using="categories.category"):
|
def breadcrumbs(category_string, separator=" > ", using="categories.category"):
|
||||||
"""
|
"""
|
||||||
|
Render breadcrumbs, using the ``categories/breadcrumbs.html`` template.
|
||||||
|
|
||||||
{% breadcrumbs category separator="::" using="categories.category" %}
|
{% breadcrumbs category separator="::" using="categories.category" %}
|
||||||
|
|
||||||
Render breadcrumbs, using the ``categories/breadcrumbs.html`` template,
|
Args:
|
||||||
using the optional ``separator`` argument.
|
category_string: A variable reference to or the name of the category to display
|
||||||
|
separator: The string to separate the categories
|
||||||
|
using: A variable reference to or the name of the category model to search for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The inclusion template
|
||||||
"""
|
"""
|
||||||
cat = get_category(category_string, using)
|
cat = get_category(category_string, using)
|
||||||
|
|
||||||
|
|
@ -159,8 +189,7 @@ def breadcrumbs(category_string, separator=" > ", using="categories.category"):
|
||||||
@register.inclusion_tag("categories/ul_tree.html")
|
@register.inclusion_tag("categories/ul_tree.html")
|
||||||
def display_drilldown_as_ul(category, using="categories.Category"):
|
def display_drilldown_as_ul(category, using="categories.Category"):
|
||||||
"""
|
"""
|
||||||
Render the category with ancestors and children using the
|
Render the category with ancestors and children using the ``categories/ul_tree.html`` template.
|
||||||
``categories/ul_tree.html`` template.
|
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
|
|
@ -189,6 +218,13 @@ def display_drilldown_as_ul(category, using="categories.Category"):
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
Args:
|
||||||
|
category: A variable reference to or the name of the category to display
|
||||||
|
using: A variable reference to or the name of the category model to search for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The inclusion template
|
||||||
"""
|
"""
|
||||||
cat = get_category(category, using)
|
cat = get_category(category, using)
|
||||||
if cat is None:
|
if cat is None:
|
||||||
|
|
@ -200,19 +236,18 @@ def display_drilldown_as_ul(category, using="categories.Category"):
|
||||||
@register.inclusion_tag("categories/ul_tree.html")
|
@register.inclusion_tag("categories/ul_tree.html")
|
||||||
def display_path_as_ul(category, using="categories.Category"):
|
def display_path_as_ul(category, using="categories.Category"):
|
||||||
"""
|
"""
|
||||||
Render the category with ancestors, but no children using the
|
Render the category with ancestors, but no children using the ``categories/ul_tree.html`` template.
|
||||||
``categories/ul_tree.html`` template.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```
|
||||||
{% display_path_as_ul "/Grandparent/Parent" %}
|
{% display_path_as_ul "/Grandparent/Parent" %}
|
||||||
|
```
|
||||||
or ::
|
```
|
||||||
|
|
||||||
{% display_path_as_ul category_obj %}
|
{% display_path_as_ul category_obj %}
|
||||||
|
```
|
||||||
|
|
||||||
Returns::
|
Output:
|
||||||
|
```
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/categories/">Top</a>
|
<li><a href="/categories/">Top</a>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -220,21 +255,32 @@ def display_path_as_ul(category, using="categories.Category"):
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
```
|
||||||
|
|
||||||
|
Args:
|
||||||
|
category: A variable reference to or the name of the category to display
|
||||||
|
using: A variable reference to or the name of the category model to search for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The inclusion template
|
||||||
"""
|
"""
|
||||||
if isinstance(category, CategoryBase):
|
if isinstance(category, CategoryBase):
|
||||||
cat = category
|
cat = category
|
||||||
else:
|
else:
|
||||||
cat = get_category(category)
|
cat = get_category(category, using)
|
||||||
|
|
||||||
return {"category": cat, "path": cat.get_ancestors() or []}
|
return {"category": cat, "path": cat.get_ancestors() or []}
|
||||||
|
|
||||||
|
|
||||||
class TopLevelCategoriesNode(template.Node):
|
class TopLevelCategoriesNode(template.Node):
|
||||||
|
"""Template node for the top level categories."""
|
||||||
|
|
||||||
def __init__(self, varname, model):
|
def __init__(self, varname, model):
|
||||||
self.varname = varname
|
self.varname = varname
|
||||||
self.model = model
|
self.model = model
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
|
"""Render this node."""
|
||||||
model = get_cat_model(self.model)
|
model = get_cat_model(self.model)
|
||||||
context[self.varname] = model.objects.filter(parent=None).order_by("name")
|
context[self.varname] = model.objects.filter(parent=None).order_by("name")
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -245,14 +291,25 @@ def get_top_level_categories(parser, token):
|
||||||
"""
|
"""
|
||||||
Retrieves an alphabetical list of all the categories that have no parents.
|
Retrieves an alphabetical list of all the categories that have no parents.
|
||||||
|
|
||||||
Syntax::
|
Usage:
|
||||||
|
|
||||||
|
```
|
||||||
{% get_top_level_categories [using "app.Model"] as categories %}
|
{% get_top_level_categories [using "app.Model"] as categories %}
|
||||||
|
```
|
||||||
|
|
||||||
Returns an list of categories [<category>, <category>, <category, ...]
|
Args:
|
||||||
|
parser: The Django template parser.
|
||||||
|
token: The tag contents
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Returns an list of categories [<category>, <category>, <category, ...]
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TemplateSyntaxError: If a queryset isn't provided
|
||||||
"""
|
"""
|
||||||
bits = token.split_contents()
|
bits = token.split_contents()
|
||||||
usage = 'Usage: {%% %s [using "app.Model"] as <variable> %%}' % bits[0]
|
usage = 'Usage: {%% %s [using "app.Model"] as <variable> %%}' % bits[0]
|
||||||
|
|
||||||
if len(bits) == 3:
|
if len(bits) == 3:
|
||||||
if bits[1] != "as":
|
if bits[1] != "as":
|
||||||
raise template.TemplateSyntaxError(usage)
|
raise template.TemplateSyntaxError(usage)
|
||||||
|
|
@ -267,11 +324,14 @@ def get_top_level_categories(parser, token):
|
||||||
else:
|
else:
|
||||||
model = bits[4].strip("'\"")
|
model = bits[4].strip("'\"")
|
||||||
varname = bits[2].strip("'\"")
|
varname = bits[2].strip("'\"")
|
||||||
|
else:
|
||||||
|
raise template.TemplateSyntaxError(usage)
|
||||||
|
|
||||||
return TopLevelCategoriesNode(varname, model)
|
return TopLevelCategoriesNode(varname, model)
|
||||||
|
|
||||||
|
|
||||||
def get_latest_objects_by_category(category, app_label, model_name, set_name, date_field="pub_date", num=15):
|
def get_latest_objects_by_category(category, app_label, model_name, set_name, date_field="pub_date", num=15):
|
||||||
|
"""Return a queryset of the latest objects of ``app_label.model_name`` in given category."""
|
||||||
m = apps.get_model(app_label, model_name)
|
m = apps.get_model(app_label, model_name)
|
||||||
if not isinstance(category, CategoryBase):
|
if not isinstance(category, CategoryBase):
|
||||||
category = Category.objects.get(slug=str(category))
|
category = Category.objects.get(slug=str(category))
|
||||||
|
|
@ -285,10 +345,11 @@ def get_latest_objects_by_category(category, app_label, model_name, set_name, da
|
||||||
|
|
||||||
|
|
||||||
class LatestObjectsNode(Node):
|
class LatestObjectsNode(Node):
|
||||||
|
"""
|
||||||
|
Get latest objects of app_label.model_name.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, var_name, category, app_label, model_name, set_name, date_field="pub_date", num=15):
|
def __init__(self, var_name, category, app_label, model_name, set_name, date_field="pub_date", num=15):
|
||||||
"""
|
|
||||||
Get latest objects of app_label.model_name
|
|
||||||
"""
|
|
||||||
self.category = category
|
self.category = category
|
||||||
self.app_label = app_label
|
self.app_label = app_label
|
||||||
self.model_name = model_name
|
self.model_name = model_name
|
||||||
|
|
@ -299,7 +360,7 @@ class LatestObjectsNode(Node):
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
"""
|
"""
|
||||||
Render this sucker
|
Render this sucker.
|
||||||
"""
|
"""
|
||||||
category = resolve(self.category, context)
|
category = resolve(self.category, context)
|
||||||
app_label = resolve(self.app_label, context)
|
app_label = resolve(self.app_label, context)
|
||||||
|
|
@ -316,11 +377,27 @@ class LatestObjectsNode(Node):
|
||||||
|
|
||||||
def do_get_latest_objects_by_category(parser, token):
|
def do_get_latest_objects_by_category(parser, token):
|
||||||
"""
|
"""
|
||||||
Get the latest objects by category
|
Get the latest objects by category.
|
||||||
|
|
||||||
{% get_latest_objects_by_category category app_name model_name set_name [date_field] [number] as [var_name] %}
|
Usage:
|
||||||
|
```
|
||||||
|
{% get_latest_objects_by_category category app_name model_name set_name [date_field] [number] as [var_name] %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parser: The Django template parser.
|
||||||
|
token: The tag contents
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The latet objects node.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TemplateSyntaxError: If the tag is malformed
|
||||||
"""
|
"""
|
||||||
proper_form = "{% get_latest_objects_by_category category app_name model_name set_name [date_field] [number] as [var_name] %}"
|
proper_form = (
|
||||||
|
"{% get_latest_objects_by_category category app_name model_name set_name "
|
||||||
|
"[date_field] [number] as [var_name] %}"
|
||||||
|
)
|
||||||
bits = token.split_contents()
|
bits = token.split_contents()
|
||||||
|
|
||||||
if bits[-2] != "as":
|
if bits[-2] != "as":
|
||||||
|
|
@ -351,8 +428,15 @@ register.tag("get_latest_objects_by_category", do_get_latest_objects_by_category
|
||||||
@register.filter
|
@register.filter
|
||||||
def tree_queryset(value):
|
def tree_queryset(value):
|
||||||
"""
|
"""
|
||||||
Converts a normal queryset from an MPTT model to include all the ancestors
|
Converts a normal queryset from an MPTT model to include all the ancestors.
|
||||||
so a filtered subset of items can be formatted correctly
|
|
||||||
|
Allows a filtered subset of items to be formatted correctly
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The queryset to convert
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The converted queryset
|
||||||
"""
|
"""
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
|
|
@ -389,22 +473,35 @@ def tree_queryset(value):
|
||||||
def recursetree(parser, token):
|
def recursetree(parser, token):
|
||||||
"""
|
"""
|
||||||
Iterates over the nodes in the tree, and renders the contained block for each node.
|
Iterates over the nodes in the tree, and renders the contained block for each node.
|
||||||
|
|
||||||
This tag will recursively render children into the template variable {{ children }}.
|
This tag will recursively render children into the template variable {{ children }}.
|
||||||
Only one database query is required (children are cached for the whole tree)
|
Only one database query is required (children are cached for the whole tree)
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
<ul>
|
```
|
||||||
{% recursetree nodes %}
|
<ul>
|
||||||
<li>
|
{% recursetree nodes %}
|
||||||
{{ node.name }}
|
<li>
|
||||||
{% if not node.is_leaf_node %}
|
{{ node.name }}
|
||||||
<ul>
|
{% if not node.is_leaf_node %}
|
||||||
{{ children }}
|
<ul>
|
||||||
</ul>
|
{{ children }}
|
||||||
{% endif %}
|
</ul>
|
||||||
</li>
|
{% endif %}
|
||||||
{% endrecursetree %}
|
</li>
|
||||||
</ul>
|
{% endrecursetree %}
|
||||||
|
</ul>
|
||||||
|
```
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parser: The Django template parser.
|
||||||
|
token: The tag contents
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The recursive tree node.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TemplateSyntaxError: If a queryset isn't provided.
|
||||||
"""
|
"""
|
||||||
bits = token.contents.split()
|
bits = token.contents.split()
|
||||||
if len(bits) != 2:
|
if len(bits) != 2:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""URL patterns for the categories app."""
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
"""View functions for categories."""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.template.loader import select_template
|
from django.template.loader import select_template
|
||||||
|
|
@ -7,7 +10,11 @@ from django.views.generic import DetailView, ListView
|
||||||
from .models import Category
|
from .models import Category
|
||||||
|
|
||||||
|
|
||||||
def category_detail(request, path, template_name="categories/category_detail.html", extra_context={}):
|
def category_detail(
|
||||||
|
request, path, template_name="categories/category_detail.html", extra_context: Optional[dict] = None
|
||||||
|
):
|
||||||
|
"""Render the detail page for a category."""
|
||||||
|
extra_context = extra_context or {}
|
||||||
path_items = path.strip("/").split("/")
|
path_items = path.strip("/").split("/")
|
||||||
if len(path_items) >= 2:
|
if len(path_items) >= 2:
|
||||||
category = get_object_or_404(
|
category = get_object_or_404(
|
||||||
|
|
@ -29,6 +36,7 @@ def category_detail(request, path, template_name="categories/category_detail.htm
|
||||||
|
|
||||||
|
|
||||||
def get_category_for_path(path, queryset=Category.objects.all()):
|
def get_category_for_path(path, queryset=Category.objects.all()):
|
||||||
|
"""Return the category for a path."""
|
||||||
path_items = path.strip("/").split("/")
|
path_items = path.strip("/").split("/")
|
||||||
if len(path_items) >= 2:
|
if len(path_items) >= 2:
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
|
|
@ -40,10 +48,13 @@ def get_category_for_path(path, queryset=Category.objects.all()):
|
||||||
|
|
||||||
|
|
||||||
class CategoryDetailView(DetailView):
|
class CategoryDetailView(DetailView):
|
||||||
|
"""Detail view for a category."""
|
||||||
|
|
||||||
model = Category
|
model = Category
|
||||||
path_field = "path"
|
path_field = "path"
|
||||||
|
|
||||||
def get_object(self, **kwargs):
|
def get_object(self, **kwargs):
|
||||||
|
"""Get the category."""
|
||||||
if self.path_field not in self.kwargs:
|
if self.path_field not in self.kwargs:
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
"Category detail view %s must be called with " "a %s." % (self.__class__.__name__, self.path_field)
|
"Category detail view %s must be called with " "a %s." % (self.__class__.__name__, self.path_field)
|
||||||
|
|
@ -58,6 +69,7 @@ class CategoryDetailView(DetailView):
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_template_names(self):
|
def get_template_names(self):
|
||||||
|
"""Get the potential template names."""
|
||||||
names = []
|
names = []
|
||||||
path_items = self.kwargs[self.path_field].strip("/").split("/")
|
path_items = self.kwargs[self.path_field].strip("/").split("/")
|
||||||
while path_items:
|
while path_items:
|
||||||
|
|
@ -68,10 +80,13 @@ class CategoryDetailView(DetailView):
|
||||||
|
|
||||||
|
|
||||||
class CategoryRelatedDetail(DetailView):
|
class CategoryRelatedDetail(DetailView):
|
||||||
|
"""Detailed view for a category relation."""
|
||||||
|
|
||||||
path_field = "category_path"
|
path_field = "category_path"
|
||||||
object_name_field = None
|
object_name_field = None
|
||||||
|
|
||||||
def get_object(self, **kwargs):
|
def get_object(self, **kwargs):
|
||||||
|
"""Get the object to render."""
|
||||||
if self.path_field not in self.kwargs:
|
if self.path_field not in self.kwargs:
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
"Category detail view %s must be called with " "a %s." % (self.__class__.__name__, self.path_field)
|
"Category detail view %s must be called with " "a %s." % (self.__class__.__name__, self.path_field)
|
||||||
|
|
@ -86,6 +101,7 @@ class CategoryRelatedDetail(DetailView):
|
||||||
return queryset.get(category=category)
|
return queryset.get(category=category)
|
||||||
|
|
||||||
def get_template_names(self):
|
def get_template_names(self):
|
||||||
|
"""Get all template names."""
|
||||||
names = []
|
names = []
|
||||||
opts = self.object._meta
|
opts = self.object._meta
|
||||||
path_items = self.kwargs[self.path_field].strip("/").split("/")
|
path_items = self.kwargs[self.path_field].strip("/").split("/")
|
||||||
|
|
@ -103,9 +119,12 @@ class CategoryRelatedDetail(DetailView):
|
||||||
|
|
||||||
|
|
||||||
class CategoryRelatedList(ListView):
|
class CategoryRelatedList(ListView):
|
||||||
|
"""List related category items."""
|
||||||
|
|
||||||
path_field = "category_path"
|
path_field = "category_path"
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
"""Get the list of items."""
|
||||||
if self.path_field not in self.kwargs:
|
if self.path_field not in self.kwargs:
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
"Category detail view %s must be called with " "a %s." % (self.__class__.__name__, self.path_field)
|
"Category detail view %s must be called with " "a %s." % (self.__class__.__name__, self.path_field)
|
||||||
|
|
@ -115,6 +134,7 @@ class CategoryRelatedList(ListView):
|
||||||
return queryset.filter(category=category)
|
return queryset.filter(category=category)
|
||||||
|
|
||||||
def get_template_names(self):
|
def get_template_names(self):
|
||||||
|
"""Get the template names."""
|
||||||
names = []
|
names = []
|
||||||
if hasattr(self.object_list, "model"):
|
if hasattr(self.object_list, "model"):
|
||||||
opts = self.object_list.model._meta
|
opts = self.object_list.model._meta
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
"""Entrypoint for custom django functions."""
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Django settings for sample project.
|
"""Django settings for sample project."""
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""Admin interface for simple text."""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from categories.admin import CategoryBaseAdmin, CategoryBaseAdminForm
|
from categories.admin import CategoryBaseAdmin, CategoryBaseAdminForm
|
||||||
|
|
@ -6,6 +7,8 @@ from .models import SimpleCategory, SimpleText
|
||||||
|
|
||||||
|
|
||||||
class SimpleTextAdmin(admin.ModelAdmin):
|
class SimpleTextAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin for simple text model."""
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
(
|
||||||
None,
|
None,
|
||||||
|
|
@ -20,12 +23,16 @@ class SimpleTextAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
|
||||||
class SimpleCategoryAdminForm(CategoryBaseAdminForm):
|
class SimpleCategoryAdminForm(CategoryBaseAdminForm):
|
||||||
|
"""Admin form for simple category."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SimpleCategory
|
model = SimpleCategory
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class SimpleCategoryAdmin(CategoryBaseAdmin):
|
class SimpleCategoryAdmin(CategoryBaseAdmin):
|
||||||
|
"""Admin for simple category."""
|
||||||
|
|
||||||
form = SimpleCategoryAdminForm
|
form = SimpleCategoryAdminForm
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
6
example/simpletext/models.py
Executable file → Normal file
6
example/simpletext/models.py
Executable file → Normal file
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""Example model."""
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from categories.base import CategoryBase
|
from categories.base import CategoryBase
|
||||||
|
|
@ -5,7 +6,7 @@ from categories.base import CategoryBase
|
||||||
|
|
||||||
class SimpleText(models.Model):
|
class SimpleText(models.Model):
|
||||||
"""
|
"""
|
||||||
(SimpleText description)
|
(SimpleText description).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
|
|
@ -22,6 +23,7 @@ class SimpleText(models.Model):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
|
"""Get the absolute URL for this object."""
|
||||||
try:
|
try:
|
||||||
from django.db.models import permalink
|
from django.db.models import permalink
|
||||||
|
|
||||||
|
|
@ -33,7 +35,7 @@ class SimpleText(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class SimpleCategory(CategoryBase):
|
class SimpleCategory(CategoryBase):
|
||||||
"""A Test of catgorizing"""
|
"""A Test of catgorizing."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = "simple categories"
|
verbose_name_plural = "simple categories"
|
||||||
|
|
|
||||||
2
example/simpletext/views.py
Executable file → Normal file
2
example/simpletext/views.py
Executable file → Normal file
|
|
@ -1 +1 @@
|
||||||
# Create your views here.
|
"""Create your views here."""
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""URL patterns for the example project."""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue