From 2a599b5f994c8f006b1cb7e15296c83d1bfcc2e5 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Fri, 10 Jun 2016 14:24:33 +0200 Subject: [PATCH] Convert `admin.py` into a package. Clears up the code and prepare to receive more changes for formset support. --- polymorphic/admin/__init__.py | 15 ++ polymorphic/admin/childadmin.py | 169 ++++++++++++++ polymorphic/admin/filters.py | 29 +++ polymorphic/admin/forms.py | 18 ++ .../{admin.py => admin/parentadmin.py} | 218 +----------------- 5 files changed, 235 insertions(+), 214 deletions(-) create mode 100644 polymorphic/admin/__init__.py create mode 100644 polymorphic/admin/childadmin.py create mode 100644 polymorphic/admin/filters.py create mode 100644 polymorphic/admin/forms.py rename polymorphic/{admin.py => admin/parentadmin.py} (68%) diff --git a/polymorphic/admin/__init__.py b/polymorphic/admin/__init__.py new file mode 100644 index 0000000..fe0c31c --- /dev/null +++ b/polymorphic/admin/__init__.py @@ -0,0 +1,15 @@ +""" +ModelAdmin code to display polymorphic models. + +The admin consists of a parent admin (which shows in the admin with a list), +and a child admin (which is used internally to show the edit/delete dialog). +""" +from .parentadmin import PolymorphicParentModelAdmin +from .childadmin import PolymorphicChildModelAdmin +from .forms import PolymorphicModelChoiceForm +from .filters import PolymorphicChildModelFilter + +__all__ = ( + 'PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin', + 'PolymorphicChildModelAdmin', 'PolymorphicChildModelFilter' +) diff --git a/polymorphic/admin/childadmin.py b/polymorphic/admin/childadmin.py new file mode 100644 index 0000000..63cda0f --- /dev/null +++ b/polymorphic/admin/childadmin.py @@ -0,0 +1,169 @@ +""" +The child admin displays the change/delete view of the subclass model. +""" +from django.contrib import admin +from django.core.urlresolvers import resolve +from django.utils import six +from django.utils.translation import ugettext_lazy as _ + + +class PolymorphicChildModelAdmin(admin.ModelAdmin): + """ + The *optional* base class for the admin interface of derived models. + + This base class defines some convenience behavior for the admin interface: + + * It corrects the breadcrumbs in the admin pages. + * It adds the base model to the template lookup paths. + * It allows to set ``base_form`` so the derived class will automatically include other fields in the form. + * It allows to set ``base_fieldsets`` so the derived class will automatically display any extra fields. + + The ``base_model`` attribute must be set. + """ + base_model = None + base_form = None + base_fieldsets = None + extra_fieldset_title = _("Contents") # Default title for extra fieldset + show_in_index = False + + def get_form(self, request, obj=None, **kwargs): + # The django admin validation requires the form to have a 'class Meta: model = ..' + # attribute, or it will complain that the fields are missing. + # However, this enforces all derived ModelAdmin classes to redefine the model as well, + # because they need to explicitly set the model again - it will stick with the base model. + # + # Instead, pass the form unchecked here, because the standard ModelForm will just work. + # If the derived class sets the model explicitly, respect that setting. + kwargs.setdefault('form', self.base_form or self.form) + + # prevent infinite recursion in django 1.6+ + if not getattr(self, 'declared_fieldsets', None): + kwargs.setdefault('fields', None) + + return super(PolymorphicChildModelAdmin, self).get_form(request, obj, **kwargs) + + def get_model_perms(self, request): + match = resolve(request.path) + + if not self.show_in_index and match.app_name == 'admin' and match.url_name in ('index', 'app_list'): + return {'add': False, 'change': False, 'delete': False} + return super(PolymorphicChildModelAdmin, self).get_model_perms(request) + + @property + def change_form_template(self): + opts = self.model._meta + app_label = opts.app_label + + # Pass the base options + base_opts = self.base_model._meta + base_app_label = base_opts.app_label + + return [ + "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()), + "admin/%s/change_form.html" % app_label, + # Added: + "admin/%s/%s/change_form.html" % (base_app_label, base_opts.object_name.lower()), + "admin/%s/change_form.html" % base_app_label, + "admin/polymorphic/change_form.html", + "admin/change_form.html" + ] + + @property + def delete_confirmation_template(self): + opts = self.model._meta + app_label = opts.app_label + + # Pass the base options + base_opts = self.base_model._meta + base_app_label = base_opts.app_label + + return [ + "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()), + "admin/%s/delete_confirmation.html" % app_label, + # Added: + "admin/%s/%s/delete_confirmation.html" % (base_app_label, base_opts.object_name.lower()), + "admin/%s/delete_confirmation.html" % base_app_label, + "admin/polymorphic/delete_confirmation.html", + "admin/delete_confirmation.html" + ] + + @property + def object_history_template(self): + opts = self.model._meta + app_label = opts.app_label + + # Pass the base options + base_opts = self.base_model._meta + base_app_label = base_opts.app_label + + return [ + "admin/%s/%s/object_history.html" % (app_label, opts.object_name.lower()), + "admin/%s/object_history.html" % app_label, + # Added: + "admin/%s/%s/object_history.html" % (base_app_label, base_opts.object_name.lower()), + "admin/%s/object_history.html" % base_app_label, + "admin/polymorphic/object_history.html", + "admin/object_history.html" + ] + + def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): + context.update({ + 'base_opts': self.base_model._meta, + }) + return super(PolymorphicChildModelAdmin, self).render_change_form(request, context, add=add, change=change, form_url=form_url, obj=obj) + + def delete_view(self, request, object_id, context=None): + extra_context = { + 'base_opts': self.base_model._meta, + } + return super(PolymorphicChildModelAdmin, self).delete_view(request, object_id, extra_context) + + def history_view(self, request, object_id, extra_context=None): + # Make sure the history view can also display polymorphic breadcrumbs + context = { + 'base_opts': self.base_model._meta, + } + if extra_context: + context.update(extra_context) + return super(PolymorphicChildModelAdmin, self).history_view(request, object_id, extra_context=context) + + + # ---- Extra: improving the form/fieldset default display ---- + + def get_fieldsets(self, request, obj=None): + # If subclass declares fieldsets, this is respected + if (hasattr(self, 'declared_fieldset') and self.declared_fieldsets) \ + or not self.base_fieldsets: + return super(PolymorphicChildModelAdmin, self).get_fieldsets(request, obj) + + # Have a reasonable default fieldsets, + # where the subclass fields are automatically included. + other_fields = self.get_subclass_fields(request, obj) + + if other_fields: + return ( + self.base_fieldsets[0], + (self.extra_fieldset_title, {'fields': other_fields}), + ) + self.base_fieldsets[1:] + else: + return self.base_fieldsets + + def get_subclass_fields(self, request, obj=None): + # Find out how many fields would really be on the form, + # if it weren't restricted by declared fields. + exclude = list(self.exclude or []) + exclude.extend(self.get_readonly_fields(request, obj)) + + # By not declaring the fields/form in the base class, + # get_form() will populate the form with all available fields. + form = self.get_form(request, obj, exclude=exclude) + subclass_fields = list(six.iterkeys(form.base_fields)) + list(self.get_readonly_fields(request, obj)) + + # Find which fields are not part of the common fields. + for fieldset in self.base_fieldsets: + for field in fieldset[1]['fields']: + try: + subclass_fields.remove(field) + except ValueError: + pass # field not found in form, Django will raise exception later. + return subclass_fields diff --git a/polymorphic/admin/filters.py b/polymorphic/admin/filters.py new file mode 100644 index 0000000..16b043b --- /dev/null +++ b/polymorphic/admin/filters.py @@ -0,0 +1,29 @@ +from django.contrib import admin +from django.core.exceptions import PermissionDenied +from django.utils.translation import ugettext_lazy as _ + + +class PolymorphicChildModelFilter(admin.SimpleListFilter): + """ + An admin list filter for the PolymorphicParentModelAdmin which enables + filtering by its child models. + """ + title = _('Type') + parameter_name = 'polymorphic_ctype' + + def lookups(self, request, model_admin): + return model_admin.get_child_type_choices(request, 'change') + + def queryset(self, request, queryset): + try: + value = int(self.value()) + except TypeError: + value = None + if value: + # ensure the content type is allowed + for choice_value, _ in self.lookup_choices: + if choice_value == value: + return queryset.filter(polymorphic_ctype_id=choice_value) + raise PermissionDenied( + 'Invalid ContentType "{0}". It must be registered as child model.'.format(value)) + return queryset diff --git a/polymorphic/admin/forms.py b/polymorphic/admin/forms.py new file mode 100644 index 0000000..8f109c7 --- /dev/null +++ b/polymorphic/admin/forms.py @@ -0,0 +1,18 @@ +from django import forms +from django.contrib.admin.widgets import AdminRadioSelect +from django.utils.translation import ugettext_lazy as _ + + +class PolymorphicModelChoiceForm(forms.Form): + """ + The default form for the ``add_type_form``. Can be overwritten and replaced. + """ + #: Define the label for the radiofield + type_label = _('Type') + + ct_id = forms.ChoiceField(label=type_label, widget=AdminRadioSelect(attrs={'class': 'radiolist'})) + + def __init__(self, *args, **kwargs): + # Allow to easily redefine the label (a commonly expected usecase) + super(PolymorphicModelChoiceForm, self).__init__(*args, **kwargs) + self.fields['ct_id'].label = self.type_label diff --git a/polymorphic/admin.py b/polymorphic/admin/parentadmin.py similarity index 68% rename from polymorphic/admin.py rename to polymorphic/admin/parentadmin.py index a7f10b5..43db1f7 100644 --- a/polymorphic/admin.py +++ b/polymorphic/admin/parentadmin.py @@ -1,27 +1,26 @@ """ -ModelAdmin code to display polymorphic models. +The parent admin displays the list view of the base model. """ import sys import warnings import django -from django import forms from django.conf.urls import url from django.contrib import admin from django.contrib.admin.helpers import AdminErrorList, AdminForm -from django.contrib.admin.widgets import AdminRadioSelect from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied -from django.core.urlresolvers import RegexURLResolver, resolve +from django.core.urlresolvers import RegexURLResolver from django.http import Http404, HttpResponseRedirect from django.shortcuts import render_to_response from django.template.context import RequestContext -from django.utils import six from django.utils.encoding import force_text from django.utils.http import urlencode from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ +from .forms import PolymorphicModelChoiceForm + try: # Django 1.6 implements this from django.contrib.admin.templatetags.admin_urls import add_preserved_filters @@ -33,12 +32,6 @@ if sys.version_info[0] >= 3: long = int -__all__ = ( - 'PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin', - 'PolymorphicChildModelAdmin', 'PolymorphicChildModelFilter' -) - - class RegistrationClosed(RuntimeError): "The admin model can't be registered anymore at this point." pass @@ -49,47 +42,6 @@ class ChildAdminNotRegistered(RuntimeError): pass -class PolymorphicModelChoiceForm(forms.Form): - """ - The default form for the ``add_type_form``. Can be overwritten and replaced. - """ - #: Define the label for the radiofield - type_label = _('Type') - - ct_id = forms.ChoiceField(label=type_label, widget=AdminRadioSelect(attrs={'class': 'radiolist'})) - - def __init__(self, *args, **kwargs): - # Allow to easily redefine the label (a commonly expected usecase) - super(PolymorphicModelChoiceForm, self).__init__(*args, **kwargs) - self.fields['ct_id'].label = self.type_label - - -class PolymorphicChildModelFilter(admin.SimpleListFilter): - """ - An admin list filter for the PolymorphicParentModelAdmin which enables - filtering by its child models. - """ - title = _('Type') - parameter_name = 'polymorphic_ctype' - - def lookups(self, request, model_admin): - return model_admin.get_child_type_choices(request, 'change') - - def queryset(self, request, queryset): - try: - value = int(self.value()) - except TypeError: - value = None - if value: - # ensure the content type is allowed - for choice_value, _ in self.lookup_choices: - if choice_value == value: - return queryset.filter(polymorphic_ctype_id=choice_value) - raise PermissionDenied( - 'Invalid ContentType "{0}". It must be registered as child model.'.format(value)) - return queryset - - class PolymorphicParentModelAdmin(admin.ModelAdmin): """ A admin interface that can displays different change/delete pages, depending on the polymorphic model. @@ -487,168 +439,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): ] -class PolymorphicChildModelAdmin(admin.ModelAdmin): - """ - The *optional* base class for the admin interface of derived models. - - This base class defines some convenience behavior for the admin interface: - - * It corrects the breadcrumbs in the admin pages. - * It adds the base model to the template lookup paths. - * It allows to set ``base_form`` so the derived class will automatically include other fields in the form. - * It allows to set ``base_fieldsets`` so the derived class will automatically display any extra fields. - - The ``base_model`` attribute must be set. - """ - base_model = None - base_form = None - base_fieldsets = None - extra_fieldset_title = _("Contents") # Default title for extra fieldset - show_in_index = False - - def get_form(self, request, obj=None, **kwargs): - # The django admin validation requires the form to have a 'class Meta: model = ..' - # attribute, or it will complain that the fields are missing. - # However, this enforces all derived ModelAdmin classes to redefine the model as well, - # because they need to explicitly set the model again - it will stick with the base model. - # - # Instead, pass the form unchecked here, because the standard ModelForm will just work. - # If the derived class sets the model explicitly, respect that setting. - kwargs.setdefault('form', self.base_form or self.form) - - # prevent infinite recursion in django 1.6+ - if not getattr(self, 'declared_fieldsets', None): - kwargs.setdefault('fields', None) - - return super(PolymorphicChildModelAdmin, self).get_form(request, obj, **kwargs) - - def get_model_perms(self, request): - match = resolve(request.path) - - if not self.show_in_index and match.app_name == 'admin' and match.url_name in ('index', 'app_list'): - return {'add': False, 'change': False, 'delete': False} - return super(PolymorphicChildModelAdmin, self).get_model_perms(request) - - @property - def change_form_template(self): - opts = self.model._meta - app_label = opts.app_label - - # Pass the base options - base_opts = self.base_model._meta - base_app_label = base_opts.app_label - - return [ - "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()), - "admin/%s/change_form.html" % app_label, - # Added: - "admin/%s/%s/change_form.html" % (base_app_label, base_opts.object_name.lower()), - "admin/%s/change_form.html" % base_app_label, - "admin/polymorphic/change_form.html", - "admin/change_form.html" - ] - - @property - def delete_confirmation_template(self): - opts = self.model._meta - app_label = opts.app_label - - # Pass the base options - base_opts = self.base_model._meta - base_app_label = base_opts.app_label - - return [ - "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()), - "admin/%s/delete_confirmation.html" % app_label, - # Added: - "admin/%s/%s/delete_confirmation.html" % (base_app_label, base_opts.object_name.lower()), - "admin/%s/delete_confirmation.html" % base_app_label, - "admin/polymorphic/delete_confirmation.html", - "admin/delete_confirmation.html" - ] - - @property - def object_history_template(self): - opts = self.model._meta - app_label = opts.app_label - - # Pass the base options - base_opts = self.base_model._meta - base_app_label = base_opts.app_label - - return [ - "admin/%s/%s/object_history.html" % (app_label, opts.object_name.lower()), - "admin/%s/object_history.html" % app_label, - # Added: - "admin/%s/%s/object_history.html" % (base_app_label, base_opts.object_name.lower()), - "admin/%s/object_history.html" % base_app_label, - "admin/polymorphic/object_history.html", - "admin/object_history.html" - ] - - def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): - context.update({ - 'base_opts': self.base_model._meta, - }) - return super(PolymorphicChildModelAdmin, self).render_change_form(request, context, add=add, change=change, form_url=form_url, obj=obj) - - def delete_view(self, request, object_id, context=None): - extra_context = { - 'base_opts': self.base_model._meta, - } - return super(PolymorphicChildModelAdmin, self).delete_view(request, object_id, extra_context) - - def history_view(self, request, object_id, extra_context=None): - # Make sure the history view can also display polymorphic breadcrumbs - context = { - 'base_opts': self.base_model._meta, - } - if extra_context: - context.update(extra_context) - return super(PolymorphicChildModelAdmin, self).history_view(request, object_id, extra_context=context) - - - # ---- Extra: improving the form/fieldset default display ---- - - def get_fieldsets(self, request, obj=None): - # If subclass declares fieldsets, this is respected - if (hasattr(self, 'declared_fieldset') and self.declared_fieldsets) \ - or not self.base_fieldsets: - return super(PolymorphicChildModelAdmin, self).get_fieldsets(request, obj) - - # Have a reasonable default fieldsets, - # where the subclass fields are automatically included. - other_fields = self.get_subclass_fields(request, obj) - - if other_fields: - return ( - self.base_fieldsets[0], - (self.extra_fieldset_title, {'fields': other_fields}), - ) + self.base_fieldsets[1:] - else: - return self.base_fieldsets - - def get_subclass_fields(self, request, obj=None): - # Find out how many fields would really be on the form, - # if it weren't restricted by declared fields. - exclude = list(self.exclude or []) - exclude.extend(self.get_readonly_fields(request, obj)) - - # By not declaring the fields/form in the base class, - # get_form() will populate the form with all available fields. - form = self.get_form(request, obj, exclude=exclude) - subclass_fields = list(six.iterkeys(form.base_fields)) + list(self.get_readonly_fields(request, obj)) - - # Find which fields are not part of the common fields. - for fieldset in self.base_fieldsets: - for field in fieldset[1]['fields']: - try: - subclass_fields.remove(field) - except ValueError: - pass # field not found in form, Django will raise exception later. - return subclass_fields - - def _get_opt(model): try: return model._meta.app_label, model._meta.model_name # Django 1.7 format