From 5d65bf4bbc427561ddfa0149e04a5b3065e77499 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Thu, 5 Jul 2012 22:12:01 +0200 Subject: [PATCH 01/11] Added polymorphic admin interface Extracted from django-fluent-pages, ready for other apps too. The polymorphic admin is implemented via a parent admin for the base model, and separate admin interfaces for the child models. The parent model needs to inherit PolymorphicParentModelAdmin, and override `get_admin_for_model()` and `get_child_model_classes()` to find the child admin interfaces. The derived models have their own `ModelAdmin` class, which inherits from `PolymorphicChildModelAdmin`. The parent admin redirects it's change and delete views to the child admin. By adding `polymorphic` to the INSTALLED_APPS, the breadcrumbs will be fixed as well, to remain unchanged between the child applications. --- AUTHORS.rst | 1 + CHANGES.rst | 6 + DOCS.rst | 101 +++++ polymorphic/admin.py | 361 ++++++++++++++++++ .../admin/polymorphic/change_form.html | 15 + .../polymorphic/delete_confirmation.html | 15 + polymorphic/templatetags/__init__.py | 0 .../templatetags/polymorphic_admin_tags.py | 52 +++ 8 files changed, 551 insertions(+) create mode 100644 polymorphic/admin.py create mode 100644 polymorphic/templates/admin/polymorphic/change_form.html create mode 100644 polymorphic/templates/admin/polymorphic/delete_confirmation.html create mode 100644 polymorphic/templatetags/__init__.py create mode 100644 polymorphic/templatetags/polymorphic_admin_tags.py diff --git a/AUTHORS.rst b/AUTHORS.rst index 0aa569a..840ecd1 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -12,3 +12,4 @@ Contributors * Charles Leifer (python 2.4 compatibility) * Germán M. Bravo * Martin Brochhaus +* Diederik van der Boor (polymorphic admin interface) diff --git a/CHANGES.rst b/CHANGES.rst index e98152b..4053fb2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,12 @@ Changelog ++++++++++ +Juli 5, 2012, Polymorphic admin interface +========================================= + +Added a polymorphic admin interface. The admin interface is able to add polymorphic models, +and the admin edit screen also displays the custom fields of the polymorphic model. + 2011-12-20 Renaming, refactoring, new maintainer ================================================ diff --git a/DOCS.rst b/DOCS.rst index ded88dd..2454730 100644 --- a/DOCS.rst +++ b/DOCS.rst @@ -153,6 +153,107 @@ In the examples below, these models are being used:: field3 = models.CharField(max_length=10) +Using polymorphic models in the admin interface +----------------------------------------------- + +Naturally, it's possible to register individual polymorphic models in the Django admin interface. +However, to use these models in a single cohesive interface, some extra base classes are available. + +The polymorphic admin interface works in a simple way: + +* The add screen gains an additional step where the desired child model is selected. +* The edit screen displays the admin interface of the child model. +* The list screen still displays all objects of the base class. + +The polymorphic admin is implemented via a parent admin that forwards the *edit* and *delete* views +to the ``ModelAdmin`` of the derived child model. Hence, both the parent model and child model +need to have a ``ModelAdmin`` class. Only the ``ModelAdmin`` class of the parent/base model +has to be registered in the Django admin site. + +The parent model +~~~~~~~~~~~~~~~~ + +The parent model needs to inherit ``PolymorphicParentModelAdmin``, and implement the following: + +* ``base_model`` should be set +* ``get_admin_for_model()`` should return the model class for the child model. +* ``get_child_model_classes()`` should return a list of all child model classes. + +The exact implementation can depend on the way your module is structured. +Either a plugin registration system, or configuration setting could be used. +The parent admin redirects it's change and delete views to the child admin. + +The child models +~~~~~~~~~~~~~~~~ + +The admin interface of the derived models should inherit from ``PolymorphicChildModelAdmin``. +Again, ``base_model`` should be set in this class as well. +This class implements the following features: + +* It corrects the breadcrumbs in the admin pages. +* It extends the template lookup paths, to look for both the parent model and child model in the ``admin/app/model/change_form.html`` path. +* 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. + +By adding ``polymorphic`` to the ``INSTALLED_APPS``, the breadcrumbs will be +fixed as well, to stay the same for all child models. + +The standard ``ModelAdmin`` attributes ``form`` and ``fieldsets`` should rather be avoided at the base class, +because it will hide any additional fields which are defined in the derived model. Instead, +use the ``base_form`` and ``base_fieldsets`` instead. The ``PolymorphicChildModelAdmin`` will +automatically detect the additional fields that the child model has, display those in a separate fieldset. + + +Example +~~~~~~~ + +:: + + from django.contrib import admin + from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin + + + class ModelAParentAdmin(PolymorphicParentModelAdmin): + """ The parent model admin """ + base_model = ModelA + + def get_admin_for_model(self, model): + # just `return ModelAChildAdmin` would also work, if you don't customize anything. + return CHILD_ADMINS[model] + + def get_child_model_classes(self, model): + return CHILD_ADMINS.keys() + + + # Only the parent needs to be registered: + admin.site.register(ModelA, ModelAParentAdmin) + + + class ModelAChildAdmin(PolymorphicChildModelAdmin): + """ Base admin class for all child models """ + base_model = ModelA + + # By using these `base_...` attributes instead of the regular ModelAdmin `form` and `fieldsets`, + # the additional fields of the child models are automatically added to the admin form. + base_form = ... + base_fieldsets = ( + ... + ) + + class ModelBAdmin(ModelAChildAdmin): + # define custom features here + + class ModelCAdmin(ModelBAdmin): + # define custom features here + + + # This could be replaced with a registration system: + CHILD_ADMINS = { + ModelB: ModelBAdmin, + ModelC: ModelCAdmin, + } + + Filtering for classes (equivalent to python's isinstance() ): ------------------------------------------------------------- diff --git a/polymorphic/admin.py b/polymorphic/admin.py new file mode 100644 index 0000000..bb53e00 --- /dev/null +++ b/polymorphic/admin.py @@ -0,0 +1,361 @@ +""" +ModelAdmin code to display polymorphic models. +""" +from django import forms +from django.conf.urls.defaults import patterns, url +from django.contrib import admin +from django.contrib.admin.helpers import AdminForm, AdminErrorList +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 +from django.http import Http404, HttpResponseRedirect +from django.shortcuts import render_to_response +from django.template.context import RequestContext +from django.utils.encoding import force_unicode +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ +import abc + +__all__ = ('PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin') + + +class PolymorphicModelChoiceForm(forms.Form): + """ + The default form for the ``add_type_form``. Can be overwritten and replaced. + """ + ct_id = forms.ChoiceField(label=_("Type"), widget=AdminRadioSelect(attrs={'class': 'radiolist'})) + + + +class PolymorphicParentModelAdmin(admin.ModelAdmin): + """ + A admin interface that can displays different change/delete pages, depending on the polymorphic model. + To use this class, two methods need to be defined: + + * :func:`get_admin_for_model` should return a ModelAdmin instance for the derived model. + * :func:`get_polymorphic_model_classes` should return the available derived models. + * optionally, :func:`get_polymorphic_type_choices` can be overwritten to refine the choices for the add dialog. + + This class needs to be inherited by the model admin base class that is registered in the site. + The derived models should *not* register the ModelAdmin, but instead it should be returned by :func:`get_admin_for_model`. + """ + base_model = None + add_type_template = None + add_type_form = PolymorphicModelChoiceForm + + + @abc.abstractmethod + def get_admin_for_model(self, model): + """ + Return the polymorphic admin interface for a given model. + """ + raise NotImplementedError("Implement get_admin_for_model()") + + + @abc.abstractmethod + def get_child_model_classes(self): + """ + Return the derived model classes which this admin should handle. + + This could either be implemented as ``base_model.__subclasses__()``, + a setting in a config file, or a query of a plugin registration system. + """ + raise NotImplementedError("Implement get_child_model_classes()") + + + def get_child_type_choices(self): + """ + Return a list of polymorphic types which can be added. + """ + choices = [] + for model in self.get_child_model_classes(): + ct = ContentType.objects.get_for_model(model) + choices.append((ct.id, model._meta.verbose_name)) + return choices + + + def _get_real_admin(self, object_id): + obj = self.model.objects.non_polymorphic().values('polymorphic_ctype').get(pk=object_id) + return self._get_real_admin_by_ct(obj['polymorphic_ctype']) + + + def _get_real_admin_by_ct(self, ct_id): + try: + ct = ContentType.objects.get_for_id(ct_id) + except ContentType.DoesNotExist as e: + raise Http404(e) # Handle invalid GET parameters + + model_class = ct.model_class() + if not model_class: + raise Http404("No model found for '{0}.{1}'.".format(*ct.natural_key())) # Handle model deletion + + # The views are already checked for permissions, so ensure the model is a derived object. + # Otherwise, it would open all admin views to users who can edit the base object. + if not issubclass(model_class, self.base_model): + raise PermissionDenied("Invalid model '{0}.{1}', must derive from {name}.".format(*ct.natural_key(), name=self.base_model.__name__)) + + return self.get_admin_for_model(model_class) + + + def queryset(self, request): + return super(PolymorphicParentModelAdmin, self).queryset(request).non_polymorphic() # optimize the list display. + + + def add_view(self, request, form_url='', extra_context=None): + """Redirect the add view to the real admin.""" + ct_id = int(request.GET.get('ct_id', 0)) + if not ct_id: + # Display choices + return self.add_type_view(request) + else: + real_admin = self._get_real_admin_by_ct(ct_id) + return real_admin.add_view(request, form_url, extra_context) + + + def change_view(self, request, object_id, *args, **kwargs): + """Redirect the change view to the real admin.""" + # between Django 1.3 and 1.4 this method signature differs. Hence the *args, **kwargs + real_admin = self._get_real_admin(object_id) + return real_admin.change_view(request, object_id, *args, **kwargs) + + + def delete_view(self, request, object_id, extra_context=None): + """Redirect the delete view to the real admin.""" + real_admin = self._get_real_admin(object_id) + return real_admin.delete_view(request, object_id, extra_context) + + + def get_urls(self): + """ + Expose the custom URLs for the subclasses and the URL resolver. + """ + urls = super(PolymorphicParentModelAdmin, self).get_urls() + info = self.model._meta.app_label, self.model._meta.module_name + + # Patch the change URL so it's not a big catch-all; allowing all custom URLs to be added to the end. + # The url needs to be recreated, patching url.regex is not an option Django 1.4's LocaleRegexProvider changed it. + new_change_url = url(r'^(\d+)/$', self.admin_site.admin_view(self.change_view), name='{0}_{1}_change'.format(*info)) + for i, oldurl in enumerate(urls): + if oldurl.name == new_change_url.name: + urls[i] = new_change_url + + # Define the catch-all for custom views + custom_urls = patterns('', + url(r'^(?P.+)$', self.admin_site.admin_view(self.subclass_view)) + ) + + # Add reverse names for all polymorphic models, so the delete button and "save and add" just work. + # These definitions are masked by the definition above, since it needs special handling (and a ct_id parameter). + from fluent_pages.extensions import page_type_pool + dummy_urls = [] + for model in page_type_pool.get_model_classes(): + admin = self.get_admin_for_model(model) + dummy_urls += admin.get_urls() + + return urls + custom_urls + dummy_urls + + + def subclass_view(self, request, path): + """ + Forward any request to a custom view of the real admin. + """ + ct_id = int(request.GET.get('ct_id', 0)) + if not ct_id: + raise Http404("No ct_id parameter, unable to find admin subclass for path '{0}'.".format(path)) + + real_admin = self._get_real_admin_by_ct(ct_id) + resolver = RegexURLResolver('^', real_admin.urls) + resolvermatch = resolver.resolve(path) + if not resolvermatch: + raise Http404("No match for path '{0}' in admin subclass.".format(path)) + + return resolvermatch.func(request, *resolvermatch.args, **resolvermatch.kwargs) + + + def add_type_view(self, request, form_url=''): + """ + Display a choice form to select which page type to add. + """ + extra_qs = '' + if request.META['QUERY_STRING']: + extra_qs = '&' + request.META['QUERY_STRING'] + + choices = self.get_child_type_choices() + if len(choices) == 1: + return HttpResponseRedirect('?ct_id={0}{1}'.format(choices[0][0], extra_qs)) + + # Create form + form = self.add_type_form( + data=request.POST if request.method == 'POST' else None, + initial={'ct_id': choices[0][0]} + ) + form.fields['ct_id'].choices = choices + + if form.is_valid(): + return HttpResponseRedirect('?ct_id={0}{1}'.format(form.cleaned_data['ct_id'], extra_qs)) + + # Wrap in all admin layout + fieldsets = ((None, {'fields': ('ct_id',)}),) + adminForm = AdminForm(form, fieldsets, {}, model_admin=self) + media = self.media + adminForm.media + opts = self.model._meta + + context = { + 'title': _('Add %s') % force_unicode(opts.verbose_name), + 'adminform': adminForm, + 'is_popup': "_popup" in request.REQUEST, + 'media': mark_safe(media), + 'errors': AdminErrorList(form, ()), + 'app_label': opts.app_label, + } + return self.render_add_type_form(request, context, form_url) + + + def render_add_type_form(self, request, context, form_url=''): + """ + Render the page type choice form. + """ + opts = self.model._meta + app_label = opts.app_label + context.update({ + 'has_change_permission': self.has_change_permission(request), + 'form_url': mark_safe(form_url), + 'opts': opts, + }) + if hasattr(self.admin_site, 'root_path'): + context['root_path'] = self.admin_site.root_path # Django < 1.4 + context_instance = RequestContext(request, current_app=self.admin_site.name) + return render_to_response(self.add_type_template or [ + "admin/%s/%s/add_type_form.html" % (app_label, opts.object_name.lower()), + "admin/%s/add_type_form.html" % app_label, + "admin/add_type_form.html" + ], context, context_instance=context_instance) + + + +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 + + + 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. + if not self.form: + kwargs['form'] = self.base_form + return super(PolymorphicChildModelAdmin, self).get_form(request, obj, **kwargs) + + + @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" + ] + + + 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) + + + # ---- Extra: improving the form/fieldset default display ---- + + def get_fieldsets(self, request, obj=None): + # If subclass declares fieldsets, this is respected + if self.declared_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 = form.base_fields.keys() + 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/templates/admin/polymorphic/change_form.html b/polymorphic/templates/admin/polymorphic/change_form.html new file mode 100644 index 0000000..7e2bf53 --- /dev/null +++ b/polymorphic/templates/admin/polymorphic/change_form.html @@ -0,0 +1,15 @@ +{% extends "admin/change_form.html" %} +{% load i18n polymorphic_admin_tags %} + +{% block breadcrumbs %}{% if not is_popup %}{% breadcrumb_scope base_opts %} + +{% endbreadcrumb_scope %}{% endif %}{% endblock %} + diff --git a/polymorphic/templates/admin/polymorphic/delete_confirmation.html b/polymorphic/templates/admin/polymorphic/delete_confirmation.html new file mode 100644 index 0000000..d0e69ba --- /dev/null +++ b/polymorphic/templates/admin/polymorphic/delete_confirmation.html @@ -0,0 +1,15 @@ +{% extends "admin/delete_confirmation.html" %} +{% load i18n polymorphic_admin_tags %} + +{% block breadcrumbs %}{% breadcrumb_scope base_opts %} + +{% endbreadcrumb_scope %}{% endblock %} diff --git a/polymorphic/templatetags/__init__.py b/polymorphic/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/polymorphic/templatetags/polymorphic_admin_tags.py b/polymorphic/templatetags/polymorphic_admin_tags.py new file mode 100644 index 0000000..bf645bf --- /dev/null +++ b/polymorphic/templatetags/polymorphic_admin_tags.py @@ -0,0 +1,52 @@ +from django.template import Library, Node, TemplateSyntaxError + +register = Library() + + +class BreadcrumbScope(Node): + def __init__(self, base_opts, nodelist): + self.base_opts = base_opts + self.nodelist = nodelist # Note, takes advantage of Node.child_nodelists + + @classmethod + def parse(cls, parser, token): + bits = token.split_contents() + if len(bits) == 2: + (tagname, base_opts) = bits + base_opts = parser.compile_filter(base_opts) + nodelist = parser.parse(('endbreadcrumb_scope',)) + parser.delete_first_token() + + return cls( + base_opts=base_opts, + nodelist=nodelist + ) + else: + raise TemplateSyntaxError("{0} tag expects 1 argument".format(token.contents[0])) + + + def render(self, context): + # app_label is really hard to overwrite in the standard Django ModelAdmin. + # To insert it in the template, the entire render_change_form() and delete_view() have to copied and adjusted. + # Instead, have an assignment tag that inserts that in the template. + base_opts = self.base_opts.resolve(context) + new_vars = {} + if base_opts and not isinstance(base_opts, basestring): + new_vars = { + 'app_label': base_opts.app_label, # What this is all about + 'opts': base_opts, + } + + new_scope = context.push() + new_scope.update(new_vars) + html = self.nodelist.render(context) + context.pop() + return html + + +@register.tag +def breadcrumb_scope(parser, token): + """ + Easily allow the breadcrumb to be generated in the admin change templates. + """ + return BreadcrumbScope.parse(parser, token) From 2e76811adb41a3e470948683e58cce9f5b5a0368 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Thu, 5 Jul 2012 23:27:34 +0200 Subject: [PATCH 02/11] Adding PolymorphicChildModelAdmin to __all__ as well --- polymorphic/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polymorphic/admin.py b/polymorphic/admin.py index bb53e00..adf4df6 100644 --- a/polymorphic/admin.py +++ b/polymorphic/admin.py @@ -17,7 +17,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ import abc -__all__ = ('PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin') +__all__ = ('PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin', 'PolymorphicChildModelAdmin') class PolymorphicModelChoiceForm(forms.Form): From b2e308d30c8ff495882260784a9c17d0ef364867 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Fri, 13 Jul 2012 10:32:12 +0200 Subject: [PATCH 03/11] Improve PolymorphicParentAdmin, simplify, fix templates During the development of django-polymorphic-tree it was discovered that the PolymorphicParentModelAdmin could actually be made much simpler. It features a `child_models` attribute now, so there is very little code needed to actually implement a polymorphic admin now. Also found various issues which are together fixed in this commit for pulling. --- DOCS.rst | 51 +++++------- polymorphic/admin.py | 81 ++++++++++++++++--- .../admin/polymorphic/add_type_form.html | 43 ++++++++++ .../admin/polymorphic/change_form.html | 5 +- .../polymorphic/delete_confirmation.html | 3 - 5 files changed, 136 insertions(+), 47 deletions(-) create mode 100644 polymorphic/templates/admin/polymorphic/add_type_form.html diff --git a/DOCS.rst b/DOCS.rst index 2454730..528f97e 100644 --- a/DOCS.rst +++ b/DOCS.rst @@ -166,9 +166,10 @@ The polymorphic admin interface works in a simple way: * The list screen still displays all objects of the base class. The polymorphic admin is implemented via a parent admin that forwards the *edit* and *delete* views -to the ``ModelAdmin`` of the derived child model. Hence, both the parent model and child model -need to have a ``ModelAdmin`` class. Only the ``ModelAdmin`` class of the parent/base model -has to be registered in the Django admin site. +to the ``ModelAdmin`` of the derived child model. The *list* page is still implemented by the parent model admin. + +Both the parent model and child model need to have a ``ModelAdmin`` class. +Only the ``ModelAdmin`` class of the parent/base model has to be registered in the Django admin site. The parent model ~~~~~~~~~~~~~~~~ @@ -176,12 +177,14 @@ The parent model The parent model needs to inherit ``PolymorphicParentModelAdmin``, and implement the following: * ``base_model`` should be set -* ``get_admin_for_model()`` should return the model class for the child model. -* ``get_child_model_classes()`` should return a list of all child model classes. +* ``child_models`` should be set, or: + + * ``get_admin_for_model()`` should return the model class for the child model. + * ``get_child_model_classes()`` should return a list of all child model classes. The exact implementation can depend on the way your module is structured. -Either a plugin registration system, or configuration setting could be used. -The parent admin redirects it's change and delete views to the child admin. +For simple inheritance situations, ``child_models`` is best suited. +For large applications, this leaves room for a plugin registration system. The child models ~~~~~~~~~~~~~~~~ @@ -213,22 +216,6 @@ Example from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin - class ModelAParentAdmin(PolymorphicParentModelAdmin): - """ The parent model admin """ - base_model = ModelA - - def get_admin_for_model(self, model): - # just `return ModelAChildAdmin` would also work, if you don't customize anything. - return CHILD_ADMINS[model] - - def get_child_model_classes(self, model): - return CHILD_ADMINS.keys() - - - # Only the parent needs to be registered: - admin.site.register(ModelA, ModelAParentAdmin) - - class ModelAChildAdmin(PolymorphicChildModelAdmin): """ Base admin class for all child models """ base_model = ModelA @@ -247,11 +234,16 @@ Example # define custom features here - # This could be replaced with a registration system: - CHILD_ADMINS = { - ModelB: ModelBAdmin, - ModelC: ModelCAdmin, - } + class ModelAParentAdmin(PolymorphicParentModelAdmin): + """ The parent model admin """ + base_model = ModelA + child_models = ( + (ModelB, ModelBAdmin), + (ModelC, ModelCAdmin), + } + + # Only the parent needs to be registered: + admin.site.register(ModelA, ModelAParentAdmin) Filtering for classes (equivalent to python's isinstance() ): @@ -605,9 +597,6 @@ Restrictions & Caveats ``extra()`` has one restriction: the resulting objects are required to have a unique primary key within the result set. -* Django Admin Integration: There currently is no specific admin integration, - but it would most likely make sense to have one. - * Diamond shaped inheritance: There seems to be a general problem with diamond shaped multiple model inheritance with Django models (tested with V1.1 - V1.3). diff --git a/polymorphic/admin.py b/polymorphic/admin.py index adf4df6..0ba35dd 100644 --- a/polymorphic/admin.py +++ b/polymorphic/admin.py @@ -5,6 +5,7 @@ from django import forms from django.conf.urls.defaults import patterns, url from django.contrib import admin from django.contrib.admin.helpers import AdminForm, AdminErrorList +from django.contrib.admin.sites import AdminSite from django.contrib.admin.widgets import AdminRadioSelect from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied @@ -12,6 +13,7 @@ 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.datastructures import SortedDict from django.utils.encoding import force_unicode from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ @@ -31,26 +33,61 @@ class PolymorphicModelChoiceForm(forms.Form): class PolymorphicParentModelAdmin(admin.ModelAdmin): """ A admin interface that can displays different change/delete pages, depending on the polymorphic model. - To use this class, two methods need to be defined: + To use this class, two variables need to be defined: + + * :attr:`base_model` should + * :attr:`child_models` should be a list of (Model, Admin) tuples + + Alternatively, the following methods can be implemented: * :func:`get_admin_for_model` should return a ModelAdmin instance for the derived model. - * :func:`get_polymorphic_model_classes` should return the available derived models. - * optionally, :func:`get_polymorphic_type_choices` can be overwritten to refine the choices for the add dialog. + * :func:`get_child_model_classes` should return the available derived models. + * optionally, :func:`get_child_type_choices` can be overwritten to refine the choices for the add dialog. This class needs to be inherited by the model admin base class that is registered in the site. The derived models should *not* register the ModelAdmin, but instead it should be returned by :func:`get_admin_for_model`. """ + + #: The base model that the class uses base_model = None + + #: The child models that should be displayed + child_models = None + + #: Whether the list should be polymorphic too, leave to ``False`` to optimize + polymorphic_list = False + add_type_template = None add_type_form = PolymorphicModelChoiceForm + def __init__(self, model, admin_site, *args, **kwargs): + super(PolymorphicParentModelAdmin, self).__init__(model, admin_site, *args, **kwargs) + self.initialized_child_models = None + self.child_admin_site = AdminSite(name=self.admin_site.name) + + # Allow to declaratively define the child models + admin classes + if self.child_models is not None: + self.initialized_child_models = SortedDict() + for Model, Admin in self.child_models: + assert issubclass(Model, self.base_model), "{0} should be a subclass of {1}".format(Model.__name__, self.base_model.__name__) + assert issubclass(Admin, admin.ModelAdmin), "{0} should be a subclass of {1}".format(Admin.__name__, admin.ModelAdmin.__name__) + self.child_admin_site.register(Model, Admin) + + # HACK: need to get admin instance. + admin_instance = self.child_admin_site._registry[Model] + self.initialized_child_models[Model] = admin_instance + + @abc.abstractmethod def get_admin_for_model(self, model): """ Return the polymorphic admin interface for a given model. """ - raise NotImplementedError("Implement get_admin_for_model()") + if self.initialized_child_models is None: + raise NotImplementedError("Implement get_admin_for_model() or child_models") + + return self.initialized_child_models[model] @abc.abstractmethod @@ -61,7 +98,10 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): This could either be implemented as ``base_model.__subclasses__()``, a setting in a config file, or a query of a plugin registration system. """ - raise NotImplementedError("Implement get_child_model_classes()") + if self.initialized_child_models is None: + raise NotImplementedError("Implement get_child_model_classes() or child_models") + + return self.initialized_child_models.keys() def get_child_type_choices(self): @@ -99,7 +139,11 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): def queryset(self, request): - return super(PolymorphicParentModelAdmin, self).queryset(request).non_polymorphic() # optimize the list display. + # optimize the list display. + qs = super(PolymorphicParentModelAdmin, self).queryset(request) + if not self.polymorphic_list: + qs = qs.non_polymorphic() + return qs def add_view(self, request, form_url='', extra_context=None): @@ -147,9 +191,8 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): # Add reverse names for all polymorphic models, so the delete button and "save and add" just work. # These definitions are masked by the definition above, since it needs special handling (and a ct_id parameter). - from fluent_pages.extensions import page_type_pool dummy_urls = [] - for model in page_type_pool.get_model_classes(): + for model in self.get_child_model_classes(): admin = self.get_admin_for_model(model) dummy_urls += admin.get_urls() @@ -229,10 +272,30 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): return render_to_response(self.add_type_template or [ "admin/%s/%s/add_type_form.html" % (app_label, opts.object_name.lower()), "admin/%s/add_type_form.html" % app_label, + "admin/polymorphic/add_type_form.html", # added default here "admin/add_type_form.html" ], context, context_instance=context_instance) + @property + def change_list_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_list.html" % (app_label, opts.object_name.lower()), + "admin/%s/change_list.html" % app_label, + # Added base class: + "admin/%s/%s/change_list.html" % (base_app_label, base_opts.object_name.lower()), + "admin/%s/change_list.html" % base_app_label, + "admin/change_list.html" + ] + + class PolymorphicChildModelAdmin(admin.ModelAdmin): """ @@ -324,7 +387,7 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): def get_fieldsets(self, request, obj=None): # If subclass declares fieldsets, this is respected - if self.declared_fieldsets: + if self.declared_fieldsets or not self.base_fieldsets: return super(PolymorphicChildModelAdmin, self).get_fieldsets(request, obj) # Have a reasonable default fieldsets, diff --git a/polymorphic/templates/admin/polymorphic/add_type_form.html b/polymorphic/templates/admin/polymorphic/add_type_form.html new file mode 100644 index 0000000..f528040 --- /dev/null +++ b/polymorphic/templates/admin/polymorphic/add_type_form.html @@ -0,0 +1,43 @@ +{% extends "admin/change_form.html" %} +{% load i18n admin_modify adminmedia %} +{% load url from future %} + +{% block breadcrumbs %}{% if not is_popup %} + +{% endif %}{% endblock %} + +{% block content %}
+
{% csrf_token %}{% block form_top %}{% endblock %} +
+{% if is_popup %}{% endif %} + +{% if save_on_top %} +
+ +
+{% endif %} + +{% if errors %} +

+ {% blocktrans count errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} +

+ {{ adminform.form.non_field_errors }} +{% endif %} + +{% for fieldset in adminform %} + {% include "admin/includes/fieldset.html" %} +{% endfor %} + +
+ +
+ + +
+
+{% endblock %} diff --git a/polymorphic/templates/admin/polymorphic/change_form.html b/polymorphic/templates/admin/polymorphic/change_form.html index 7e2bf53..56043a1 100644 --- a/polymorphic/templates/admin/polymorphic/change_form.html +++ b/polymorphic/templates/admin/polymorphic/change_form.html @@ -1,15 +1,12 @@ {% extends "admin/change_form.html" %} {% load i18n polymorphic_admin_tags %} +{# fix breadcrumb #} {% block breadcrumbs %}{% if not is_popup %}{% breadcrumb_scope base_opts %} {% endbreadcrumb_scope %}{% endif %}{% endblock %} - diff --git a/polymorphic/templates/admin/polymorphic/delete_confirmation.html b/polymorphic/templates/admin/polymorphic/delete_confirmation.html index d0e69ba..c37c385 100644 --- a/polymorphic/templates/admin/polymorphic/delete_confirmation.html +++ b/polymorphic/templates/admin/polymorphic/delete_confirmation.html @@ -6,9 +6,6 @@ {% trans "Home" %}{{ app_label|capfirst|escape }}{{ opts.verbose_name_plural|capfirst }} › - {% for p in parent_object.breadcrumb %} - {{ p.title }} › - {% endfor %} {{ object|truncatewords:"18" }} › {% trans 'Delete' %} From 0950b86ca98f1dc13a6e252499aa241ff0de971d Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Fri, 13 Jul 2012 10:58:33 +0200 Subject: [PATCH 04/11] Added requires files to MANIFEST.in and setup.py --- MANIFEST.in | 2 +- setup.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 851ca6c..bcadbb7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,4 +2,4 @@ include README.rst include LICENSE include DOCS.rst include CHANGES.rst - +recursive-include polymorphic/templates/ *.html diff --git a/setup.py b/setup.py index 740d206..3a956dd 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,10 @@ setup( author_email = 'bert.constantin@gmx.de', maintainer = 'Christopher Glass', maintainer_email = 'tribaal@gmail.com', - packages = [ 'polymorphic' ], + packages = [ + 'polymorphic', + 'polymorphic.templatetags', + ], classifiers=[ 'Framework :: Django', 'Intended Audience :: Developers', From 42d525a8950d025566d63bf64b75b3c1f7f7f59d Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Fri, 13 Jul 2012 10:58:52 +0200 Subject: [PATCH 05/11] Added `url` in setup.py, so sdist actually works again. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 3a956dd..7beb24f 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ setup( author_email = 'bert.constantin@gmx.de', maintainer = 'Christopher Glass', maintainer_email = 'tribaal@gmail.com', + url = 'https://github.com/chrisglass/django_polymorphic', packages = [ 'polymorphic', 'polymorphic.templatetags', From a6d62ed63066634f1bfd400e4207a14c63873f5c Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Fri, 13 Jul 2012 10:59:35 +0200 Subject: [PATCH 06/11] Update the example app to demonstrate the polymorphic admin This really begs for a separate folder, but I leave that to a new commit for now. --- pexp/admin.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++ settings.py | 15 ++++++++---- urls.py | 10 ++++++++ 3 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 pexp/admin.py create mode 100644 urls.py diff --git a/pexp/admin.py b/pexp/admin.py new file mode 100644 index 0000000..5eb48fc --- /dev/null +++ b/pexp/admin.py @@ -0,0 +1,63 @@ +from django.contrib import admin +from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin +from pexp.models import * + + +class ProjectChildAdmin(PolymorphicChildModelAdmin): + base_model = Project + +class ProjectAdmin(PolymorphicParentModelAdmin): + base_model = Project + child_models = ( + (Project, ProjectChildAdmin), + (ArtProject, ProjectChildAdmin), + (ResearchProject, ProjectChildAdmin), + ) + +admin.site.register(Project, ProjectAdmin) + + + +class ModelAChildAdmin(PolymorphicChildModelAdmin): + base_model = ModelA + +class ModelAAdmin(PolymorphicParentModelAdmin): + base_model = ModelA + child_models = ( + (ModelA, ModelAChildAdmin), + (ModelB, ModelAChildAdmin), + (ModelC, ModelAChildAdmin), + ) + +admin.site.register(ModelA, ModelAAdmin) + + +if 'Model2A' in globals(): + class Model2AChildAdmin(PolymorphicChildModelAdmin): + base_model = Model2A + + class Model2AAdmin(PolymorphicParentModelAdmin): + base_model = Model2A + child_models = ( + (Model2A, Model2AChildAdmin), + (Model2B, Model2AChildAdmin), + (Model2C, Model2AChildAdmin), + ) + + admin.site.register(Model2A, Model2AAdmin) + + +if 'UUIDModelA' in globals(): + class UUIDModelAChildAdmin(PolymorphicChildModelAdmin): + base_model = UUIDModelA + + class UUIDModelAAdmin(PolymorphicParentModelAdmin): + base_model = UUIDModelA + child_models = ( + (UUIDModelA, UUIDModelAChildAdmin), + (UUIDModelB, UUIDModelAChildAdmin), + (UUIDModelC, UUIDModelAChildAdmin), + ) + + admin.site.register(UUIDModelA, UUIDModelAAdmin) + diff --git a/settings.py b/settings.py index 01e4ac4..ed43b51 100644 --- a/settings.py +++ b/settings.py @@ -77,9 +77,12 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', ) -ROOT_URLCONF = '' +ROOT_URLCONF = 'urls' +STATIC_URL = '/static/' +ADMIN_MEDIA_PREFIX = '/static/admin/' # 1.3 compatibility TEMPLATE_DIRS = ( # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". @@ -88,10 +91,14 @@ TEMPLATE_DIRS = ( ) INSTALLED_APPS = ( - #'django.contrib.auth', + 'django.contrib.auth', + 'django.contrib.admin', 'django.contrib.contenttypes', - #'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.sessions', + 'django.contrib.staticfiles', + #'django.contrib.sites', - 'polymorphic', # only needed if you want to use polymorphic_dumpdata + 'polymorphic', # needed if you want to use the polymorphic admin 'pexp', # this Django app is for testing and experimentation; not needed otherwise ) diff --git a/urls.py b/urls.py new file mode 100644 index 0000000..d54ec31 --- /dev/null +++ b/urls.py @@ -0,0 +1,10 @@ +from django.conf import settings +from django.conf.urls.defaults import patterns, include, url +from django.conf.urls.static import static +from django.contrib import admin + +admin.autodiscover() + +urlpatterns = patterns('', + url(r'^admin/', include(admin.site.urls)), +) From d5b1b6b56ca3d7fb37888f806af7de529272491b Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Fri, 13 Jul 2012 17:44:37 +0200 Subject: [PATCH 07/11] Fix settings.py for Django 1.4 --- settings.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/settings.py b/settings.py index ed43b51..4439e8f 100644 --- a/settings.py +++ b/settings.py @@ -68,9 +68,8 @@ SECRET_KEY = 'nk=c&k+c&#+)8557)%&0auysdd3g^sfq6@rw8_x1k8)-p@y)!(' # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', -# 'django.template.loaders.eggs.load_template_source', + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', ) MIDDLEWARE_CLASSES = ( From 8d426d9243a1d189a2c03fb633cddabadd9baa92 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Fri, 13 Jul 2012 18:29:02 +0200 Subject: [PATCH 08/11] Remove abc marker because methods are no longer abstract --- polymorphic/admin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/polymorphic/admin.py b/polymorphic/admin.py index 0ba35dd..3035fcd 100644 --- a/polymorphic/admin.py +++ b/polymorphic/admin.py @@ -17,7 +17,6 @@ from django.utils.datastructures import SortedDict from django.utils.encoding import force_unicode from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ -import abc __all__ = ('PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin', 'PolymorphicChildModelAdmin') @@ -79,7 +78,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): self.initialized_child_models[Model] = admin_instance - @abc.abstractmethod def get_admin_for_model(self, model): """ Return the polymorphic admin interface for a given model. @@ -90,7 +88,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): return self.initialized_child_models[model] - @abc.abstractmethod def get_child_model_classes(self): """ Return the derived model classes which this admin should handle. From 0b608cc67e51eecfe043524d08acacf2e857dd0a Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Wed, 18 Jul 2012 00:50:43 +0200 Subject: [PATCH 09/11] Minor extension: allow apps to override the type label in the add form easily When overriding apps, this turns out to be a very useful feature to have. --- polymorphic/admin.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/polymorphic/admin.py b/polymorphic/admin.py index 3035fcd..c03980f 100644 --- a/polymorphic/admin.py +++ b/polymorphic/admin.py @@ -25,7 +25,15 @@ class PolymorphicModelChoiceForm(forms.Form): """ The default form for the ``add_type_form``. Can be overwritten and replaced. """ - ct_id = forms.ChoiceField(label=_("Type"), widget=AdminRadioSelect(attrs={'class': 'radiolist'})) + #: 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 From 0d5f2fd943bdad2afda159479ac05068d80d4ad1 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Tue, 24 Jul 2012 21:50:52 +0200 Subject: [PATCH 10/11] Change the child model registration to fix raw_id_fields. As discovered in django-polymorphic-tree and django-fluent-pages, the raw_id_fields didn't work in Django 1.4 because the fields actively check which models are actually registered in the admin site. Hence, the parent admin site _registry is inserted in the child admin as well. This also completely moves the initialisation of the child admin into this class, using a `get_child_models()` function, akin to the static `child_models` attribute. --- polymorphic/admin.py | 111 ++++++++++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 37 deletions(-) diff --git a/polymorphic/admin.py b/polymorphic/admin.py index c03980f..72a0dc7 100644 --- a/polymorphic/admin.py +++ b/polymorphic/admin.py @@ -13,7 +13,6 @@ 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.datastructures import SortedDict from django.utils.encoding import force_unicode from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ @@ -21,6 +20,15 @@ from django.utils.translation import ugettext_lazy as _ __all__ = ('PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin', 'PolymorphicChildModelAdmin') +class RegistrationClosed(RuntimeError): + "The admin model can't be registered anymore at this point." + pass + +class ChildAdminNotRegistered(RuntimeError): + "The admin site for the model is not registered." + pass + + class PolymorphicModelChoiceForm(forms.Form): """ The default form for the ``add_type_form``. Can be overwritten and replaced. @@ -47,12 +55,11 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): Alternatively, the following methods can be implemented: - * :func:`get_admin_for_model` should return a ModelAdmin instance for the derived model. - * :func:`get_child_model_classes` should return the available derived models. + * :func:`get_child_models` should return a list of (Model, ModelAdmin) tuples * optionally, :func:`get_child_type_choices` can be overwritten to refine the choices for the add dialog. This class needs to be inherited by the model admin base class that is registered in the site. - The derived models should *not* register the ModelAdmin, but instead it should be returned by :func:`get_admin_for_model`. + The derived models should *not* register the ModelAdmin, but instead it should be returned by :func:`get_child_models`. """ #: The base model that the class uses @@ -70,43 +77,61 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): def __init__(self, model, admin_site, *args, **kwargs): super(PolymorphicParentModelAdmin, self).__init__(model, admin_site, *args, **kwargs) - self.initialized_child_models = None - self.child_admin_site = AdminSite(name=self.admin_site.name) - - # Allow to declaratively define the child models + admin classes - if self.child_models is not None: - self.initialized_child_models = SortedDict() - for Model, Admin in self.child_models: - assert issubclass(Model, self.base_model), "{0} should be a subclass of {1}".format(Model.__name__, self.base_model.__name__) - assert issubclass(Admin, admin.ModelAdmin), "{0} should be a subclass of {1}".format(Admin.__name__, admin.ModelAdmin.__name__) - self.child_admin_site.register(Model, Admin) - - # HACK: need to get admin instance. - admin_instance = self.child_admin_site._registry[Model] - self.initialized_child_models[Model] = admin_instance + self._child_admin_site = AdminSite(name=self.admin_site.name) + self._is_setup = False - def get_admin_for_model(self, model): + def _lazy_setup(self): + if self._is_setup: + return + + # By not having this in __init__() there is less stress on import dependencies as well, + # considering an advanced use cases where a plugin system scans for the child models. + child_models = self.get_child_models() + for Model, Admin in child_models: + self.register_child(Model, Admin) + self._child_models = dict(child_models) + + # This is needed to deal with the improved ForeignKeyRawIdWidget in Django 1.4 and perhaps other widgets too. + # The ForeignKeyRawIdWidget checks whether the referenced model is registered in the admin, otherwise it displays itself as a textfield. + # As simple solution, just make sure all parent admin models are also know in the child admin site. + # This should be done after all parent models are registered off course. + complete_registry = self.admin_site._registry.copy() + complete_registry.update(self._child_admin_site._registry) + + self._child_admin_site._registry = complete_registry + self._is_setup = True + + + def register_child(self, model, model_admin): """ - Return the polymorphic admin interface for a given model. + Register a model with admin to display. """ - if self.initialized_child_models is None: - raise NotImplementedError("Implement get_admin_for_model() or child_models") + # After the get_urls() is called, the URLs of the child model can't be exposed anymore to the Django URLconf, + # which also means that a "Save and continue editing" button won't work. + if self._is_setup: + raise RegistrationClosed("The admin model can't be registered anymore at this point.") - return self.initialized_child_models[model] + if not issubclass(model, self.base_model): + raise TypeError("{0} should be a subclass of {1}".format(model.__name__, self.base_model.__name__)) + if not issubclass(model_admin, admin.ModelAdmin): + raise TypeError("{0} should be a subclass of {1}".format(model_admin.__name__, admin.ModelAdmin.__name__)) + + self._child_admin_site.register(model, model_admin) - def get_child_model_classes(self): + def get_child_models(self): """ Return the derived model classes which this admin should handle. + This should return a list of tuples, exactly like :attr:`child_models` is. - This could either be implemented as ``base_model.__subclasses__()``, - a setting in a config file, or a query of a plugin registration system. + The model classes can be retrieved as ``base_model.__subclasses__()``, + a setting in a config file, or a query of a plugin registration system at your option """ - if self.initialized_child_models is None: - raise NotImplementedError("Implement get_child_model_classes() or child_models") + if self.child_models is None: + raise NotImplementedError("Implement get_child_models() or child_models") - return self.initialized_child_models.keys() + return self.child_models def get_child_type_choices(self): @@ -114,7 +139,7 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): Return a list of polymorphic types which can be added. """ choices = [] - for model in self.get_child_model_classes(): + for model, _ in self.get_child_models(): ct = ContentType.objects.get_for_model(model) choices.append((ct.id, model._meta.verbose_name)) return choices @@ -135,12 +160,21 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): if not model_class: raise Http404("No model found for '{0}.{1}'.".format(*ct.natural_key())) # Handle model deletion - # The views are already checked for permissions, so ensure the model is a derived object. - # Otherwise, it would open all admin views to users who can edit the base object. - if not issubclass(model_class, self.base_model): - raise PermissionDenied("Invalid model '{0}.{1}', must derive from {name}.".format(*ct.natural_key(), name=self.base_model.__name__)) + return self._get_real_admin_by_model(model_class) - return self.get_admin_for_model(model_class) + + def _get_real_admin_by_model(self, model_class): + # In case of a ?ct_id=### parameter, the view is already checked for permissions. + # Hence, make sure this is a derived object, or risk exposing other admin interfaces. + if model_class not in self._child_models: + raise PermissionDenied("Invalid model '{0}', it must be registered as child model.".format(model_class)) + + try: + # HACK: the only way to get the instance of an model admin, + # is to read the registry of the AdminSite. + return self._child_admin_site._registry[model_class] + except KeyError: + raise ChildAdminNotRegistered("No child admin site was registered for a '{0}' model.".format(model_class)) def queryset(self, request): @@ -194,11 +228,14 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): url(r'^(?P.+)$', self.admin_site.admin_view(self.subclass_view)) ) + # At this point. all admin code needs to be known. + self._lazy_setup() + # Add reverse names for all polymorphic models, so the delete button and "save and add" just work. # These definitions are masked by the definition above, since it needs special handling (and a ct_id parameter). dummy_urls = [] - for model in self.get_child_model_classes(): - admin = self.get_admin_for_model(model) + for model, _ in self.get_child_models(): + admin = self._get_real_admin_by_model(model) dummy_urls += admin.get_urls() return urls + custom_urls + dummy_urls From bba8db1210946755e439c3932f86d5a8b54007b9 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Tue, 24 Jul 2012 21:55:01 +0200 Subject: [PATCH 11/11] Also update the DOCS for the get_child_models() admin change. --- DOCS.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DOCS.rst b/DOCS.rst index 528f97e..d0974e0 100644 --- a/DOCS.rst +++ b/DOCS.rst @@ -179,8 +179,7 @@ The parent model needs to inherit ``PolymorphicParentModelAdmin``, and implement * ``base_model`` should be set * ``child_models`` should be set, or: - * ``get_admin_for_model()`` should return the model class for the child model. - * ``get_child_model_classes()`` should return a list of all child model classes. + * ``get_child_models()`` should return a list with (Model, ModelAdmin) tuple. The exact implementation can depend on the way your module is structured. For simple inheritance situations, ``child_models`` is best suited.