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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -1,3 +1,4 @@
"""Manages settings for the categories application."""
import collections import collections
from django.conf import settings 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 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:

View file

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

View file

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

View file

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

View file

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

View file

@ -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
View 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
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 import os
from django.conf.urls import include, url from django.conf.urls import include, url