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.contrib import admin
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
|
@ -15,10 +16,9 @@ class NullTreeNodeChoiceField(forms.ModelChoiceField):
|
|||
self.level_indicator = level_indicator
|
||||
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
|
||||
generating option labels.
|
||||
Creates labels which represent the tree level of each node when generating option labels.
|
||||
"""
|
||||
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
|
||||
|
||||
class InlineCategoryRelation(GenericCollectionTabularInline):
|
||||
"""The inline admin panel for category relations."""
|
||||
|
||||
model = CategoryRelation
|
||||
|
||||
|
||||
class CategoryAdminForm(CategoryBaseAdminForm):
|
||||
"""The form for a category in the admin."""
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
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"]:
|
||||
return self.cleaned_data["name"]
|
||||
else:
|
||||
|
|
@ -43,6 +48,8 @@ class CategoryAdminForm(CategoryBaseAdminForm):
|
|||
|
||||
|
||||
class CategoryAdmin(CategoryBaseAdmin):
|
||||
"""Admin for categories."""
|
||||
|
||||
form = CategoryAdminForm
|
||||
list_display = ("name", "alternate_title", "active")
|
||||
fieldsets = (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
"""Django application setup."""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CategoriesConfig(AppConfig):
|
||||
"""Application configuration for categories."""
|
||||
|
||||
name = "categories"
|
||||
verbose_name = "Categories"
|
||||
|
||||
|
|
@ -12,6 +15,7 @@ class CategoriesConfig(AppConfig):
|
|||
class_prepared.connect(handle_class_prepared)
|
||||
|
||||
def ready(self):
|
||||
"""Migrate the app after it is ready."""
|
||||
from django.db.models.signals import post_migrate
|
||||
|
||||
from .migration import migrate_app
|
||||
|
|
@ -21,7 +25,7 @@ class CategoriesConfig(AppConfig):
|
|||
|
||||
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 .settings import FK_REGISTRY, M2M_REGISTRY
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
"""
|
||||
This is the base class on which to build a hierarchical category-like model
|
||||
with customizable metadata and its own name space.
|
||||
"""
|
||||
import sys
|
||||
This is the base class on which to build a hierarchical category-like model.
|
||||
|
||||
It provides customizable metadata and its own name space.
|
||||
"""
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.db import models
|
||||
|
|
@ -17,31 +16,24 @@ from slugify import slugify
|
|||
from .editor.tree_editor import TreeEditor
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Only categories that are active
|
||||
Only categories that are active.
|
||||
"""
|
||||
return self.get_queryset().filter(active=True)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CategoryBase(MPTTModel):
|
||||
"""
|
||||
This base model includes the absolute bare bones fields and methods. One
|
||||
could simply subclass this model and do nothing else and it should work.
|
||||
This base model includes the absolute bare-bones fields and methods.
|
||||
|
||||
One could simply subclass this model, do nothing else, and it should work.
|
||||
"""
|
||||
|
||||
parent = TreeForeignKey(
|
||||
|
|
@ -61,9 +53,15 @@ class CategoryBase(MPTTModel):
|
|||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Save the category.
|
||||
|
||||
While you can activate an item without activating its descendants,
|
||||
It doesn't make sense that you can deactivate an item and have its
|
||||
decendants remain active.
|
||||
|
||||
Args:
|
||||
args: generic args
|
||||
kwargs: generic keyword arguments
|
||||
"""
|
||||
if not self.slug:
|
||||
self.slug = slugify(SLUG_TRANSLITERATOR(self.name))[:50]
|
||||
|
|
@ -95,14 +93,16 @@ class CategoryBase(MPTTModel):
|
|||
|
||||
|
||||
class CategoryBaseAdminForm(forms.ModelForm):
|
||||
"""Base admin form for categories."""
|
||||
|
||||
def clean_slug(self):
|
||||
if not self.cleaned_data.get("slug", None):
|
||||
if self.instance is None or not ALLOW_SLUG_CHANGE:
|
||||
self.cleaned_data["slug"] = slugify(SLUG_TRANSLITERATOR(self.cleaned_data["name"]))
|
||||
"""Prune and transliterate the slug."""
|
||||
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"]))
|
||||
return self.cleaned_data["slug"][:50]
|
||||
|
||||
def clean(self):
|
||||
|
||||
"""Clean the data passed from the admin interface."""
|
||||
super(CategoryBaseAdminForm, self).clean()
|
||||
|
||||
if not self.is_valid():
|
||||
|
|
@ -141,6 +141,8 @@ class CategoryBaseAdminForm(forms.ModelForm):
|
|||
|
||||
|
||||
class CategoryBaseAdmin(TreeEditor, admin.ModelAdmin):
|
||||
"""Base admin class for categories."""
|
||||
|
||||
form = CategoryBaseAdminForm
|
||||
list_display = ("name", "active")
|
||||
search_fields = ("name",)
|
||||
|
|
@ -149,14 +151,15 @@ class CategoryBaseAdmin(TreeEditor, admin.ModelAdmin):
|
|||
actions = ["activate", "deactivate"]
|
||||
|
||||
def get_actions(self, request):
|
||||
"""Get available actions for the admin interface."""
|
||||
actions = super(CategoryBaseAdmin, self).get_actions(request)
|
||||
if "delete_selected" in actions:
|
||||
del actions["delete_selected"]
|
||||
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")])
|
||||
|
||||
|
|
@ -168,9 +171,9 @@ class CategoryBaseAdmin(TreeEditor, admin.ModelAdmin):
|
|||
|
||||
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")])
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
# Placeholder for Django
|
||||
"""Placeholder for Django."""
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
"""Settings management for the editor."""
|
||||
import django
|
||||
from django.conf import settings
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
"""Template tags used to render the tree editor."""
|
||||
import django
|
||||
from django.contrib.admin.templatetags.admin_list import _boolean_icon, result_headers
|
||||
from django.contrib.admin.utils import lookup_field
|
||||
|
|
@ -19,13 +20,14 @@ if settings.IS_GRAPPELLI_INSTALLED:
|
|||
|
||||
|
||||
def get_empty_value_display(cl):
|
||||
"""Get the value to display when empty."""
|
||||
if hasattr(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):
|
||||
|
|
@ -130,10 +132,13 @@ def items_for_tree_result(cl, result, form):
|
|||
|
||||
|
||||
class TreeList(list):
|
||||
"""A list subclass for tree result."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def tree_results(cl):
|
||||
"""Generates a list of results for the tree."""
|
||||
if cl.formset:
|
||||
for res, form in zip(cl.result_list, cl.formset.forms):
|
||||
result = TreeList(items_for_tree_result(cl, res, form))
|
||||
|
|
@ -158,7 +163,7 @@ def tree_results(cl):
|
|||
|
||||
def result_tree_list(cl):
|
||||
"""
|
||||
Displays the headers and data list together
|
||||
Displays the headers and data list together.
|
||||
"""
|
||||
import django
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
"""Classes for representing tree structures in Django's admin."""
|
||||
from typing import Any
|
||||
|
||||
import django
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.options import IncorrectLookupParameters
|
||||
|
|
@ -12,9 +15,9 @@ from . import settings
|
|||
|
||||
class TreeEditorQuerySet(QuerySet):
|
||||
"""
|
||||
The TreeEditorQuerySet is a special query set used only in the TreeEditor
|
||||
ChangeList page. The only difference to a regular QuerySet is that it
|
||||
will enforce:
|
||||
A special query set used only in the TreeEditor ChangeList page.
|
||||
|
||||
The only difference to a regular QuerySet is that it will enforce:
|
||||
|
||||
(a) The result is ordered in correct tree order so that
|
||||
the TreeAdmin works all right.
|
||||
|
|
@ -25,6 +28,7 @@ class TreeEditorQuerySet(QuerySet):
|
|||
"""
|
||||
|
||||
def iterator(self):
|
||||
"""Iterates through the items in thee query set."""
|
||||
qs = self
|
||||
# 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.
|
||||
|
|
@ -54,18 +58,27 @@ class TreeEditorQuerySet(QuerySet):
|
|||
# def __getitem__(self, index):
|
||||
# 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
|
||||
self.queryset(request).get(...) to get the object they should work
|
||||
with. Our modifications to the queryset when INCLUDE_ANCESTORS is
|
||||
enabled make get() fail often with a MultipleObjectsReturned
|
||||
exception.
|
||||
Quick and dirty hack to fix change_view and delete_view.
|
||||
|
||||
They use ``self.queryset(request).get(...)`` to get the object they should work
|
||||
with. Our modifications to the queryset when ``INCLUDE_ANCESTORS`` is enabled make ``get()``
|
||||
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)
|
||||
|
||||
|
||||
class TreeChangeList(ChangeList):
|
||||
"""A change list for a tree."""
|
||||
|
||||
def _get_default_ordering(self):
|
||||
if django.VERSION[0] == 1 and django.VERSION[1] < 4:
|
||||
return "", "" # ('tree_id', 'lft')
|
||||
|
|
@ -73,17 +86,31 @@ class TreeChangeList(ChangeList):
|
|||
return []
|
||||
|
||||
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:
|
||||
return "", "" # ('tree_id', 'lft')
|
||||
else:
|
||||
return []
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
qs = super(TreeChangeList, self).get_queryset(*args, **kwargs).order_by("tree_id", "lft")
|
||||
return qs
|
||||
"""Return a queryset."""
|
||||
return super(TreeChangeList, self).get_queryset(*args, **kwargs).order_by("tree_id", "lft")
|
||||
|
||||
|
||||
class TreeEditor(admin.ModelAdmin):
|
||||
"""A tree editor view for Django's admin."""
|
||||
|
||||
list_per_page = 999999999 # We can't have pagination
|
||||
list_max_show_all = 200 # new in django 1.4
|
||||
|
||||
|
|
@ -120,7 +147,7 @@ class TreeEditor(admin.ModelAdmin):
|
|||
return TreeChangeList
|
||||
|
||||
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.core.exceptions import PermissionDenied
|
||||
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):
|
||||
"""
|
||||
Handle the changelist view, the django view for the model instances
|
||||
change list/actions page.
|
||||
Handle the changelist view, the django view for the model instances change list/actions page.
|
||||
"""
|
||||
extra_context = extra_context or {}
|
||||
extra_context["EDITOR_MEDIA_PATH"] = settings.MEDIA_PATH
|
||||
|
|
@ -312,10 +338,17 @@ class TreeEditor(admin.ModelAdmin):
|
|||
# FIXME
|
||||
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
|
||||
admin site. This is used by changelist_view.
|
||||
Returns a QuerySet of all model instances that can be edited by the admin site.
|
||||
|
||||
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.__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
|
||||
|
||||
|
||||
def display_for_field(value, field, empty_value_display=None):
|
||||
"""Compatility for displaying a field in Django 1.8."""
|
||||
try:
|
||||
return _display_for_field(value, field, empty_value_display)
|
||||
except TypeError:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
"""Custom category fields for other models."""
|
||||
from django.db.models import ForeignKey, ManyToManyField
|
||||
|
||||
|
||||
class CategoryM2MField(ManyToManyField):
|
||||
"""A many to many field to a Category model."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
from .models import Category
|
||||
|
||||
|
|
@ -11,18 +14,11 @@ class CategoryM2MField(ManyToManyField):
|
|||
|
||||
|
||||
class CategoryFKField(ForeignKey):
|
||||
"""A foreign key to the Category model."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
from .models import Category
|
||||
|
||||
if "to" in kwargs:
|
||||
kwargs.pop("to")
|
||||
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
|
||||
|
||||
from django.contrib import admin
|
||||
|
|
@ -6,10 +7,13 @@ from django.urls import NoReverseMatch, reverse
|
|||
|
||||
|
||||
class GenericCollectionInlineModelAdmin(admin.options.InlineModelAdmin):
|
||||
"""Inline admin for generic model collections."""
|
||||
|
||||
ct_field = "content_type"
|
||||
ct_fk_field = "object_id"
|
||||
|
||||
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")
|
||||
elements = {}
|
||||
for x, y, z in ctypes:
|
||||
|
|
@ -20,6 +24,7 @@ class GenericCollectionInlineModelAdmin(admin.options.InlineModelAdmin):
|
|||
return json.dumps(elements)
|
||||
|
||||
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.content_types = self.get_content_types()
|
||||
result.ct_fk_field = self.ct_fk_field
|
||||
|
|
@ -30,8 +35,12 @@ class GenericCollectionInlineModelAdmin(admin.options.InlineModelAdmin):
|
|||
|
||||
|
||||
class GenericCollectionTabularInline(GenericCollectionInlineModelAdmin):
|
||||
"""Tabular model admin for a generic collection."""
|
||||
|
||||
template = "admin/edit_inline/gen_coll_tabular.html"
|
||||
|
||||
|
||||
class GenericCollectionStackedInline(GenericCollectionInlineModelAdmin):
|
||||
"""Stacked model admin for a generic collection."""
|
||||
|
||||
template = "admin/edit_inline/gen_coll_stacked.html"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
"""The add_category_fields command."""
|
||||
from django.core.management.base import 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"
|
||||
|
|
@ -12,13 +13,13 @@ class Command(BaseCommand):
|
|||
requires_system_checks = False
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add app_names argument to the command."""
|
||||
parser.add_argument("app_names", nargs="*")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
Alter the tables
|
||||
Alter the tables.
|
||||
"""
|
||||
|
||||
from categories.migration import migrate_app
|
||||
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
|
||||
|
||||
|
||||
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"
|
||||
|
|
@ -12,13 +13,14 @@ class Command(BaseCommand):
|
|||
requires_system_checks = False
|
||||
|
||||
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("model_name")
|
||||
parser.add_argument("field_name")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
Alter the tables
|
||||
Alter the tables.
|
||||
"""
|
||||
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.db import transaction
|
||||
from slugify import slugify
|
||||
|
|
@ -16,7 +18,7 @@ class Command(BaseCommand):
|
|||
|
||||
def get_indent(self, string):
|
||||
"""
|
||||
Look through the string and count the spaces
|
||||
Look through the string and count the spaces.
|
||||
"""
|
||||
indent_amt = 0
|
||||
|
||||
|
|
@ -31,7 +33,7 @@ class Command(BaseCommand):
|
|||
@transaction.atomic
|
||||
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(
|
||||
name=string.strip(),
|
||||
|
|
@ -48,12 +50,12 @@ class Command(BaseCommand):
|
|||
|
||||
def parse_lines(self, lines):
|
||||
"""
|
||||
Do the work of parsing each line
|
||||
Do the work of parsing each line.
|
||||
"""
|
||||
indent = ""
|
||||
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.")
|
||||
|
||||
# This keeps track of the current parents at a given level
|
||||
|
|
@ -62,10 +64,10 @@ class Command(BaseCommand):
|
|||
for line in lines:
|
||||
if len(line) == 0:
|
||||
continue
|
||||
if line[0] == " " or line[0] == "\t":
|
||||
if line[0] in [" ", "\t"]:
|
||||
if indent == "":
|
||||
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")
|
||||
level = line.count(indent)
|
||||
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):
|
||||
"""
|
||||
Handle the basic import
|
||||
Handle the basic import.
|
||||
"""
|
||||
import os
|
||||
|
||||
|
|
@ -84,8 +86,6 @@ class Command(BaseCommand):
|
|||
if not os.path.isfile(file_path):
|
||||
print("File %s not found." % file_path)
|
||||
continue
|
||||
f = open(file_path, "r")
|
||||
data = f.readlines()
|
||||
f.close()
|
||||
|
||||
with open(file_path, "r") as f:
|
||||
data = f.readlines()
|
||||
self.parse_lines(data)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
"""Adds and removes category relations on the database."""
|
||||
from django.apps import apps
|
||||
from django.db import connection, transaction
|
||||
from django.db.utils import ProgrammingError
|
||||
|
|
@ -5,7 +6,7 @@ from django.db.utils import ProgrammingError
|
|||
|
||||
def table_exists(table_name):
|
||||
"""
|
||||
Check if a table exists in the database
|
||||
Check if a table exists in the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ def field_exists(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)
|
||||
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):
|
||||
"""
|
||||
Migrate all models of this app registered
|
||||
Migrate all models of this app registered.
|
||||
"""
|
||||
from .registration import registry
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
"""Category models."""
|
||||
from functools import reduce
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
|
@ -26,6 +27,8 @@ STORAGE = get_storage_class(THUMBNAIL_STORAGE)
|
|||
|
||||
|
||||
class Category(CategoryBase):
|
||||
"""A basic category model."""
|
||||
|
||||
thumbnail = models.FileField(
|
||||
upload_to=THUMBNAIL_UPLOAD_PATH,
|
||||
null=True,
|
||||
|
|
@ -53,10 +56,11 @@ class Category(CategoryBase):
|
|||
|
||||
@property
|
||||
def short_title(self):
|
||||
"""Return the name."""
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Return a path"""
|
||||
"""Return a path."""
|
||||
from django.urls import NoReverseMatch
|
||||
|
||||
if self.alternate_url:
|
||||
|
|
@ -74,17 +78,18 @@ class Category(CategoryBase):
|
|||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save the category."""
|
||||
if self.thumbnail:
|
||||
width, height = get_image_dimensions(self.thumbnail.file)
|
||||
else:
|
||||
|
|
@ -110,6 +115,8 @@ else:
|
|||
|
||||
|
||||
class CategoryRelationManager(models.Manager):
|
||||
"""Custom access functions for category relations."""
|
||||
|
||||
def get_content_type(self, content_type):
|
||||
"""
|
||||
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):
|
||||
"""Related category item"""
|
||||
"""Related category item."""
|
||||
|
||||
category = models.ForeignKey(Category, verbose_name=_("category"), on_delete=models.CASCADE)
|
||||
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.db.models import CASCADE, ForeignKey, ManyToManyField
|
||||
|
||||
|
|
@ -16,17 +20,26 @@ FIELD_TYPES = {
|
|||
|
||||
|
||||
class Registry(object):
|
||||
"""Keeps track of fields and models registered."""
|
||||
|
||||
def __init__(self):
|
||||
self._field_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 +
|
||||
app: app name/label
|
||||
model_name: name of the model
|
||||
field_definitions: a string, tuple or list of field configurations
|
||||
field_type: either 'ForeignKey' or 'ManyToManyField'
|
||||
Registration process for Django 1.7+.
|
||||
|
||||
Args:
|
||||
app: app name/label
|
||||
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
|
||||
|
||||
|
|
@ -93,14 +106,26 @@ class Registry(object):
|
|||
self._field_registry[registry_name] = FIELD_TYPES[field_type](**extra_params)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
extra_params = extra_params or {}
|
||||
registry_name = ".".join((app_label, model.__name__, field_name)).lower()
|
||||
|
||||
if registry_name in self._field_registry:
|
||||
|
|
@ -122,7 +147,7 @@ registry = Registry()
|
|||
|
||||
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.core.exceptions import ImproperlyConfigured
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
"""Manages settings for the categories application."""
|
||||
import collections
|
||||
|
||||
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.apps import apps
|
||||
from django.template import Node, TemplateSyntaxError, VariableDoesNotExist
|
||||
|
|
@ -9,7 +12,6 @@ from mptt.templatetags.mptt_tags import (
|
|||
tree_path,
|
||||
)
|
||||
from mptt.utils import drilldown_tree_for_node
|
||||
from six import string_types
|
||||
|
||||
from categories.base import CategoryBase
|
||||
from categories.models import Category
|
||||
|
|
@ -21,7 +23,8 @@ register.filter(tree_info)
|
|||
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:
|
||||
return var.resolve(context)
|
||||
except VariableDoesNotExist:
|
||||
|
|
@ -33,10 +36,11 @@ def resolve(var, context):
|
|||
|
||||
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:
|
||||
if isinstance(model, string_types):
|
||||
if isinstance(model, str):
|
||||
model_class = apps.get_model(*model.split("."))
|
||||
elif issubclass(model, CategoryBase):
|
||||
model_class = model
|
||||
|
|
@ -47,9 +51,16 @@ def get_cat_model(model):
|
|||
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)
|
||||
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 len(categories) == 1:
|
||||
return categories[0]
|
||||
else:
|
||||
for item in categories:
|
||||
if item.parent.name == cat_list[-2]:
|
||||
return item
|
||||
|
||||
for item in categories:
|
||||
if item.parent.name == cat_list[-2]:
|
||||
return item
|
||||
except model_class.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
class CategoryDrillDownNode(template.Node):
|
||||
"""A category drill down template node."""
|
||||
|
||||
def __init__(self, category, varname, model):
|
||||
self.category = category
|
||||
self.varname = varname
|
||||
self.model = model
|
||||
|
||||
def render(self, context):
|
||||
"""Render this node."""
|
||||
category = resolve(self.category, context)
|
||||
if isinstance(category, CategoryBase):
|
||||
cat = category
|
||||
else:
|
||||
cat = get_category(category, self.model)
|
||||
try:
|
||||
if cat is not None:
|
||||
context[self.varname] = drilldown_tree_for_node(cat)
|
||||
else:
|
||||
context[self.varname] = []
|
||||
context[self.varname] = drilldown_tree_for_node(cat) if cat is not None else []
|
||||
except Exception:
|
||||
context[self.varname] = []
|
||||
return ""
|
||||
|
|
@ -99,8 +110,7 @@ class CategoryDrillDownNode(template.Node):
|
|||
@register.tag
|
||||
def get_category_drilldown(parser, token):
|
||||
"""
|
||||
Retrieves the specified category, its ancestors and its immediate children
|
||||
as an iterable.
|
||||
Retrieves the specified category, its ancestors and its immediate children as an Iterable.
|
||||
|
||||
Syntax::
|
||||
|
||||
|
|
@ -117,6 +127,16 @@ def get_category_drilldown(parser, token):
|
|||
Sets family to::
|
||||
|
||||
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()
|
||||
error_str = (
|
||||
|
|
@ -124,13 +144,14 @@ def get_category_drilldown(parser, token):
|
|||
'"category name" [using "app.Model"] as varname %%} or '
|
||||
"{%% %(tagname)s category_obj as varname %%}."
|
||||
)
|
||||
varname = model = ""
|
||||
if len(bits) == 4:
|
||||
if bits[2] != "as":
|
||||
raise template.TemplateSyntaxError(error_str % {"tagname": bits[0]})
|
||||
if bits[2] == "as":
|
||||
varname = bits[3].strip("'\"")
|
||||
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"):
|
||||
raise template.TemplateSyntaxError(error_str % {"tagname": bits[0]})
|
||||
if bits[2] == "as":
|
||||
|
|
@ -139,6 +160,8 @@ def get_category_drilldown(parser, token):
|
|||
if bits[2] == "using":
|
||||
varname = bits[5].strip("'\"")
|
||||
model = bits[3].strip("'\"")
|
||||
else:
|
||||
raise template.TemplateSyntaxError(error_str % {"tagname": bits[0]})
|
||||
category = FilterExpression(bits[1], parser)
|
||||
return CategoryDrillDownNode(category, varname, model)
|
||||
|
||||
|
|
@ -146,10 +169,17 @@ def get_category_drilldown(parser, token):
|
|||
@register.inclusion_tag("categories/breadcrumbs.html")
|
||||
def breadcrumbs(category_string, separator=" > ", using="categories.category"):
|
||||
"""
|
||||
Render breadcrumbs, using the ``categories/breadcrumbs.html`` template.
|
||||
|
||||
{% breadcrumbs category separator="::" using="categories.category" %}
|
||||
|
||||
Render breadcrumbs, using the ``categories/breadcrumbs.html`` template,
|
||||
using the optional ``separator`` argument.
|
||||
Args:
|
||||
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)
|
||||
|
||||
|
|
@ -159,8 +189,7 @@ def breadcrumbs(category_string, separator=" > ", using="categories.category"):
|
|||
@register.inclusion_tag("categories/ul_tree.html")
|
||||
def display_drilldown_as_ul(category, using="categories.Category"):
|
||||
"""
|
||||
Render the category with ancestors and children using the
|
||||
``categories/ul_tree.html`` template.
|
||||
Render the category with ancestors and children using the ``categories/ul_tree.html`` template.
|
||||
|
||||
Example::
|
||||
|
||||
|
|
@ -189,6 +218,13 @@ def display_drilldown_as_ul(category, using="categories.Category"):
|
|||
</ul>
|
||||
</li>
|
||||
</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)
|
||||
if cat is None:
|
||||
|
|
@ -200,19 +236,18 @@ def display_drilldown_as_ul(category, using="categories.Category"):
|
|||
@register.inclusion_tag("categories/ul_tree.html")
|
||||
def display_path_as_ul(category, using="categories.Category"):
|
||||
"""
|
||||
Render the category with ancestors, but no children using the
|
||||
``categories/ul_tree.html`` template.
|
||||
|
||||
Example::
|
||||
Render the category with ancestors, but no children using the ``categories/ul_tree.html`` template.
|
||||
|
||||
Examples:
|
||||
```
|
||||
{% display_path_as_ul "/Grandparent/Parent" %}
|
||||
|
||||
or ::
|
||||
|
||||
```
|
||||
```
|
||||
{% display_path_as_ul category_obj %}
|
||||
```
|
||||
|
||||
Returns::
|
||||
|
||||
Output:
|
||||
```
|
||||
<ul>
|
||||
<li><a href="/categories/">Top</a>
|
||||
<ul>
|
||||
|
|
@ -220,21 +255,32 @@ def display_path_as_ul(category, using="categories.Category"):
|
|||
</ul>
|
||||
</li>
|
||||
</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):
|
||||
cat = category
|
||||
else:
|
||||
cat = get_category(category)
|
||||
cat = get_category(category, using)
|
||||
|
||||
return {"category": cat, "path": cat.get_ancestors() or []}
|
||||
|
||||
|
||||
class TopLevelCategoriesNode(template.Node):
|
||||
"""Template node for the top level categories."""
|
||||
|
||||
def __init__(self, varname, model):
|
||||
self.varname = varname
|
||||
self.model = model
|
||||
|
||||
def render(self, context):
|
||||
"""Render this node."""
|
||||
model = get_cat_model(self.model)
|
||||
context[self.varname] = model.objects.filter(parent=None).order_by("name")
|
||||
return ""
|
||||
|
|
@ -245,14 +291,25 @@ def get_top_level_categories(parser, token):
|
|||
"""
|
||||
Retrieves an alphabetical list of all the categories that have no parents.
|
||||
|
||||
Syntax::
|
||||
Usage:
|
||||
|
||||
```
|
||||
{% 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()
|
||||
usage = 'Usage: {%% %s [using "app.Model"] as <variable> %%}' % bits[0]
|
||||
|
||||
if len(bits) == 3:
|
||||
if bits[1] != "as":
|
||||
raise template.TemplateSyntaxError(usage)
|
||||
|
|
@ -267,11 +324,14 @@ def get_top_level_categories(parser, token):
|
|||
else:
|
||||
model = bits[4].strip("'\"")
|
||||
varname = bits[2].strip("'\"")
|
||||
else:
|
||||
raise template.TemplateSyntaxError(usage)
|
||||
|
||||
return TopLevelCategoriesNode(varname, model)
|
||||
|
||||
|
||||
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)
|
||||
if not isinstance(category, CategoryBase):
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Get latest objects of app_label.model_name
|
||||
"""
|
||||
self.category = category
|
||||
self.app_label = app_label
|
||||
self.model_name = model_name
|
||||
|
|
@ -299,7 +360,7 @@ class LatestObjectsNode(Node):
|
|||
|
||||
def render(self, context):
|
||||
"""
|
||||
Render this sucker
|
||||
Render this sucker.
|
||||
"""
|
||||
category = resolve(self.category, context)
|
||||
app_label = resolve(self.app_label, context)
|
||||
|
|
@ -316,11 +377,27 @@ class LatestObjectsNode(Node):
|
|||
|
||||
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()
|
||||
|
||||
if bits[-2] != "as":
|
||||
|
|
@ -351,8 +428,15 @@ register.tag("get_latest_objects_by_category", do_get_latest_objects_by_category
|
|||
@register.filter
|
||||
def tree_queryset(value):
|
||||
"""
|
||||
Converts a normal queryset from an MPTT model to include all the ancestors
|
||||
so a filtered subset of items can be formatted correctly
|
||||
Converts a normal queryset from an MPTT model to include all the ancestors.
|
||||
|
||||
Allows a filtered subset of items to be formatted correctly
|
||||
|
||||
Args:
|
||||
value: The queryset to convert
|
||||
|
||||
Returns:
|
||||
The converted queryset
|
||||
"""
|
||||
from copy import deepcopy
|
||||
|
||||
|
|
@ -389,22 +473,35 @@ def tree_queryset(value):
|
|||
def recursetree(parser, token):
|
||||
"""
|
||||
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 }}.
|
||||
Only one database query is required (children are cached for the whole tree)
|
||||
|
||||
Usage:
|
||||
<ul>
|
||||
{% recursetree nodes %}
|
||||
<li>
|
||||
{{ node.name }}
|
||||
{% if not node.is_leaf_node %}
|
||||
<ul>
|
||||
{{ children }}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endrecursetree %}
|
||||
</ul>
|
||||
```
|
||||
<ul>
|
||||
{% recursetree nodes %}
|
||||
<li>
|
||||
{{ node.name }}
|
||||
{% if not node.is_leaf_node %}
|
||||
<ul>
|
||||
{{ children }}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% 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()
|
||||
if len(bits) != 2:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
"""URL patterns for the categories app."""
|
||||
from django.conf.urls import url
|
||||
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.shortcuts import get_object_or_404
|
||||
from django.template.loader import select_template
|
||||
|
|
@ -7,7 +10,11 @@ from django.views.generic import DetailView, ListView
|
|||
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("/")
|
||||
if len(path_items) >= 2:
|
||||
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()):
|
||||
"""Return the category for a path."""
|
||||
path_items = path.strip("/").split("/")
|
||||
if len(path_items) >= 2:
|
||||
queryset = queryset.filter(
|
||||
|
|
@ -40,10 +48,13 @@ def get_category_for_path(path, queryset=Category.objects.all()):
|
|||
|
||||
|
||||
class CategoryDetailView(DetailView):
|
||||
"""Detail view for a category."""
|
||||
|
||||
model = Category
|
||||
path_field = "path"
|
||||
|
||||
def get_object(self, **kwargs):
|
||||
"""Get the category."""
|
||||
if self.path_field not in self.kwargs:
|
||||
raise AttributeError(
|
||||
"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):
|
||||
"""Get the potential template names."""
|
||||
names = []
|
||||
path_items = self.kwargs[self.path_field].strip("/").split("/")
|
||||
while path_items:
|
||||
|
|
@ -68,10 +80,13 @@ class CategoryDetailView(DetailView):
|
|||
|
||||
|
||||
class CategoryRelatedDetail(DetailView):
|
||||
"""Detailed view for a category relation."""
|
||||
|
||||
path_field = "category_path"
|
||||
object_name_field = None
|
||||
|
||||
def get_object(self, **kwargs):
|
||||
"""Get the object to render."""
|
||||
if self.path_field not in self.kwargs:
|
||||
raise AttributeError(
|
||||
"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)
|
||||
|
||||
def get_template_names(self):
|
||||
"""Get all template names."""
|
||||
names = []
|
||||
opts = self.object._meta
|
||||
path_items = self.kwargs[self.path_field].strip("/").split("/")
|
||||
|
|
@ -103,9 +119,12 @@ class CategoryRelatedDetail(DetailView):
|
|||
|
||||
|
||||
class CategoryRelatedList(ListView):
|
||||
"""List related category items."""
|
||||
|
||||
path_field = "category_path"
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get the list of items."""
|
||||
if self.path_field not in self.kwargs:
|
||||
raise AttributeError(
|
||||
"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)
|
||||
|
||||
def get_template_names(self):
|
||||
"""Get the template names."""
|
||||
names = []
|
||||
if hasattr(self.object_list, "model"):
|
||||
opts = self.object_list.model._meta
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
"""Entrypoint for custom django functions."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Django settings for sample project.
|
||||
"""Django settings for sample project."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
"""Admin interface for simple text."""
|
||||
from django.contrib import admin
|
||||
|
||||
from categories.admin import CategoryBaseAdmin, CategoryBaseAdminForm
|
||||
|
|
@ -6,6 +7,8 @@ from .models import SimpleCategory, SimpleText
|
|||
|
||||
|
||||
class SimpleTextAdmin(admin.ModelAdmin):
|
||||
"""Admin for simple text model."""
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
|
|
@ -20,12 +23,16 @@ class SimpleTextAdmin(admin.ModelAdmin):
|
|||
|
||||
|
||||
class SimpleCategoryAdminForm(CategoryBaseAdminForm):
|
||||
"""Admin form for simple category."""
|
||||
|
||||
class Meta:
|
||||
model = SimpleCategory
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class SimpleCategoryAdmin(CategoryBaseAdmin):
|
||||
"""Admin for simple category."""
|
||||
|
||||
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 categories.base import CategoryBase
|
||||
|
|
@ -5,7 +6,7 @@ from categories.base import CategoryBase
|
|||
|
||||
class SimpleText(models.Model):
|
||||
"""
|
||||
(SimpleText description)
|
||||
(SimpleText description).
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
|
|
@ -22,6 +23,7 @@ class SimpleText(models.Model):
|
|||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Get the absolute URL for this object."""
|
||||
try:
|
||||
from django.db.models import permalink
|
||||
|
||||
|
|
@ -33,7 +35,7 @@ class SimpleText(models.Model):
|
|||
|
||||
|
||||
class SimpleCategory(CategoryBase):
|
||||
"""A Test of catgorizing"""
|
||||
"""A Test of catgorizing."""
|
||||
|
||||
class Meta:
|
||||
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
|
||||
|
||||
from django.conf.urls import include, url
|
||||
|
|
|
|||
Loading…
Reference in a new issue