diff --git a/categories/admin.py b/categories/admin.py index 5c11755..abf3f72 100644 --- a/categories/admin.py +++ b/categories/admin.py @@ -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 = ( diff --git a/categories/apps.py b/categories/apps.py index 1bc058e..5f58385 100644 --- a/categories/apps.py +++ b/categories/apps.py @@ -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 diff --git a/categories/base.py b/categories/base.py index 9517565..f2e5521 100644 --- a/categories/base.py +++ b/categories/base.py @@ -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")]) diff --git a/categories/editor/models.py b/categories/editor/models.py index 3542496..8c35c95 100644 --- a/categories/editor/models.py +++ b/categories/editor/models.py @@ -1 +1 @@ -# Placeholder for Django +"""Placeholder for Django.""" diff --git a/categories/editor/settings.py b/categories/editor/settings.py index 5ea2e61..164cf21 100644 --- a/categories/editor/settings.py +++ b/categories/editor/settings.py @@ -1,3 +1,4 @@ +"""Settings management for the editor.""" import django from django.conf import settings diff --git a/categories/editor/templatetags/admin_tree_list_tags.py b/categories/editor/templatetags/admin_tree_list_tags.py index 160752b..11088c4 100644 --- a/categories/editor/templatetags/admin_tree_list_tags.py +++ b/categories/editor/templatetags/admin_tree_list_tags.py @@ -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 diff --git a/categories/editor/tree_editor.py b/categories/editor/tree_editor.py index 2d64b51..d528c95 100644 --- a/categories/editor/tree_editor.py +++ b/categories/editor/tree_editor.py @@ -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 diff --git a/categories/editor/utils.py b/categories/editor/utils.py index dab0511..3ce0165 100644 --- a/categories/editor/utils.py +++ b/categories/editor/utils.py @@ -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: diff --git a/categories/fields.py b/categories/fields.py index 719b72e..12f254e 100644 --- a/categories/fields.py +++ b/categories/fields.py @@ -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 diff --git a/categories/genericcollection.py b/categories/genericcollection.py index 1745914..7eab863 100644 --- a/categories/genericcollection.py +++ b/categories/genericcollection.py @@ -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" diff --git a/categories/management/commands/add_category_fields.py b/categories/management/commands/add_category_fields.py index 377f5e5..de315a4 100644 --- a/categories/management/commands/add_category_fields.py +++ b/categories/management/commands/add_category_fields.py @@ -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 diff --git a/categories/management/commands/drop_category_field.py b/categories/management/commands/drop_category_field.py index f10a220..ca48b39 100644 --- a/categories/management/commands/drop_category_field.py +++ b/categories/management/commands/drop_category_field.py @@ -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 diff --git a/categories/management/commands/import_categories.py b/categories/management/commands/import_categories.py index 6009afb..b41469e 100644 --- a/categories/management/commands/import_categories.py +++ b/categories/management/commands/import_categories.py @@ -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) diff --git a/categories/migration.py b/categories/migration.py index ac4f36c..13ff0c5 100644 --- a/categories/migration.py +++ b/categories/migration.py @@ -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 diff --git a/categories/models.py b/categories/models.py index 3789de6..0cba3fb 100644 --- a/categories/models.py +++ b/categories/models.py @@ -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( diff --git a/categories/registration.py b/categories/registration.py index d871e14..793c908 100644 --- a/categories/registration.py +++ b/categories/registration.py @@ -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 diff --git a/categories/settings.py b/categories/settings.py index 2fedf6f..03519fe 100644 --- a/categories/settings.py +++ b/categories/settings.py @@ -1,3 +1,4 @@ +"""Manages settings for the categories application.""" import collections from django.conf import settings diff --git a/categories/templatetags/category_tags.py b/categories/templatetags/category_tags.py index a93f70e..460509a 100644 --- a/categories/templatetags/category_tags.py +++ b/categories/templatetags/category_tags.py @@ -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"): + + 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: + ``` + ``` + + 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 [, , , , - {% recursetree nodes %} -
  • - {{ node.name }} - {% if not node.is_leaf_node %} -
      - {{ children }} -
    - {% endif %} -
  • - {% endrecursetree %} - + ``` +
      + {% recursetree nodes %} +
    • + {{ node.name }} + {% if not node.is_leaf_node %} +
        + {{ children }} +
      + {% endif %} +
    • + {% endrecursetree %} +
    + ``` + + 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: diff --git a/categories/urls.py b/categories/urls.py index 7160f61..ecb9649 100644 --- a/categories/urls.py +++ b/categories/urls.py @@ -1,3 +1,4 @@ +"""URL patterns for the categories app.""" from django.conf.urls import url from django.views.generic import ListView diff --git a/categories/views.py b/categories/views.py index c92752d..cb81054 100644 --- a/categories/views.py +++ b/categories/views.py @@ -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 diff --git a/example/manage.py b/example/manage.py index f9726f9..195f6b9 100755 --- a/example/manage.py +++ b/example/manage.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +"""Entrypoint for custom django functions.""" import os import sys diff --git a/example/settings.py b/example/settings.py index c303377..ababf90 100644 --- a/example/settings.py +++ b/example/settings.py @@ -1,4 +1,4 @@ -# Django settings for sample project. +"""Django settings for sample project.""" import os import sys diff --git a/example/simpletext/admin.py b/example/simpletext/admin.py index 9539514..2d12815 100644 --- a/example/simpletext/admin.py +++ b/example/simpletext/admin.py @@ -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 diff --git a/example/simpletext/models.py b/example/simpletext/models.py old mode 100755 new mode 100644 index e7ccaed..7d01720 --- a/example/simpletext/models.py +++ b/example/simpletext/models.py @@ -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" diff --git a/example/simpletext/views.py b/example/simpletext/views.py old mode 100755 new mode 100644 index 60f00ef..8a89169 --- a/example/simpletext/views.py +++ b/example/simpletext/views.py @@ -1 +1 @@ -# Create your views here. +"""Create your views here.""" diff --git a/example/urls.py b/example/urls.py index cab9d28..b20f351 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,3 +1,4 @@ +"""URL patterns for the example project.""" import os from django.conf.urls import include, url