Adds doc strings for lots of functions.

This commit is contained in:
Corey Oordt 2021-12-22 12:03:21 -06:00
parent f9a46848b2
commit 076debb44d
26 changed files with 380 additions and 155 deletions

View file

@ -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 = (

View file

@ -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

View file

@ -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")])

View file

@ -1 +1 @@
# Placeholder for Django
"""Placeholder for Django."""

View file

@ -1,3 +1,4 @@
"""Settings management for the editor."""
import django
from django.conf import settings

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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(

View file

@ -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

View file

@ -1,3 +1,4 @@
"""Manages settings for the categories application."""
import collections
from django.conf import settings

View file

@ -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:

View file

@ -1,3 +1,4 @@
"""URL patterns for the categories app."""
from django.conf.urls import url
from django.views.generic import ListView

View file

@ -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

View file

@ -1,4 +1,5 @@
#!/usr/bin/env python
"""Entrypoint for custom django functions."""
import os
import sys

View file

@ -1,4 +1,4 @@
# Django settings for sample project.
"""Django settings for sample project."""
import os
import sys

View file

@ -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
View 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
View file

@ -1 +1 @@
# Create your views here.
"""Create your views here."""

View file

@ -1,3 +1,4 @@
"""URL patterns for the example project."""
import os
from django.conf.urls import include, url