Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Polymorphic admin interface #10

Merged
merged 11 commits into from

2 participants

@vdboor
Collaborator

Hi Chris,

While using django-polymorphic for my app, I've build a generic admin interface that displays the polymorphic models in the edit/delete views of the admin. It was extracted out of django-fluent-pages / django-polymorphic-tree where it was originally developped.

You can find the code attached here, and the DOCS.rst is updated with an example how to use it.

I noticed the rest of the project still has older python compatibility. If this is still needed in your opinion, the string formatting will have to be changed back to the % syntax. It's using python 2.6 str.format() syntax currently.

Greetings,
Diederik

vdboor added some commits
@vdboor vdboor 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.
5d65bf4
@vdboor vdboor Adding PolymorphicChildModelAdmin to __all__ as well 2e76811
@vdboor vdboor 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.
b2e308d
@vdboor vdboor Added requires files to MANIFEST.in and setup.py 0950b86
@vdboor vdboor Added `url` in setup.py, so sdist actually works again. 42d525a
@vdboor vdboor 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.
a6d62ed
@vdboor vdboor Fix settings.py for Django 1.4 d5b1b6b
@vdboor
Collaborator

as far as I'm concerned some of this could be merged into just one commit, but for the sake of sanity in this pull request I haven't changed that :)

vdboor added some commits
@vdboor vdboor Remove abc marker because methods are no longer abstract 8d426d9
@vdboor vdboor Minor extension: allow apps to override the type label in the add for…
…m easily

When overriding apps, this turns out to be a very useful feature to
have.
0b608cc
@vdboor vdboor 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.
0d5f2fd
@vdboor vdboor Also update the DOCS for the get_child_models() admin change. bba8db1
@chrisglass
Owner

Fantastic! Thanks a lot for your contribution!

@chrisglass chrisglass merged commit 28b8885 into chrisglass:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jul 5, 2012
  1. @vdboor

    Added polymorphic admin interface

    vdboor authored
    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.
  2. @vdboor
Commits on Jul 13, 2012
  1. @vdboor

    Improve PolymorphicParentAdmin, simplify, fix templates

    vdboor authored
    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.
  2. @vdboor
  3. @vdboor
  4. @vdboor

    Update the example app to demonstrate the polymorphic admin

    vdboor authored
    This really begs for a separate folder, but I leave that to a new commit
    for now.
  5. @vdboor

    Fix settings.py for Django 1.4

    vdboor authored
  6. @vdboor
Commits on Jul 17, 2012
  1. @vdboor

    Minor extension: allow apps to override the type label in the add for…

    vdboor authored
    …m easily
    
    When overriding apps, this turns out to be a very useful feature to
    have.
Commits on Jul 24, 2012
  1. @vdboor

    Change the child model registration to fix raw_id_fields.

    vdboor authored
    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.
  2. @vdboor
This page is out of date. Refresh to see the latest.
View
1  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)
View
6 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
================================================
View
95 DOCS.rst
@@ -153,6 +153,98 @@ 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. 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
+~~~~~~~~~~~~~~~~
+
+The parent model needs to inherit ``PolymorphicParentModelAdmin``, and implement the following:
+
+* ``base_model`` should be set
+* ``child_models`` should be set, or:
+
+ * ``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.
+For large applications, this leaves room for a plugin registration system.
+
+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 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
+
+
+ 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() ):
-------------------------------------------------------------
@@ -504,9 +596,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).
View
2  MANIFEST.in
@@ -2,4 +2,4 @@ include README.rst
include LICENSE
include DOCS.rst
include CHANGES.rst
-
+recursive-include polymorphic/templates/ *.html
View
63 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)
+
View
466 polymorphic/admin.py
@@ -0,0 +1,466 @@
+"""
+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.sites import AdminSite
+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 _
+
+__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.
+ """
+ #: 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 PolymorphicParentModelAdmin(admin.ModelAdmin):
+ """
+ A admin interface that can displays different change/delete pages, depending on the polymorphic model.
+ 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_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_child_models`.
+ """
+
+ #: 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._child_admin_site = AdminSite(name=self.admin_site.name)
+ self._is_setup = False
+
+
+ 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):
+ """
+ Register a model with admin to display.
+ """
+ # 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.")
+
+ 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_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.
+
+ 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.child_models is None:
+ raise NotImplementedError("Implement get_child_models() or child_models")
+
+ return self.child_models
+
+
+ def get_child_type_choices(self):
+ """
+ Return a list of polymorphic types which can be added.
+ """
+ choices = []
+ for model, _ in self.get_child_models():
+ 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
+
+ return self._get_real_admin_by_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):
+ # 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):
+ """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<path>.+)$', 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_models():
+ admin = self._get_real_admin_by_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/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):
+ """
+ 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 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 = 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
View
43 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 %}
+<div class="breadcrumbs">
+ <a href="../../../">{% trans "Home" %}</a> &rsaquo;
+ <a href="../../">{{ app_label|capfirst|escape }}</a> &rsaquo;
+ {% if has_change_permission %}<a href="../">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %} &rsaquo;
+ {% trans "Add" %} {{ opts.verbose_name }}
+</div>
+{% endif %}{% endblock %}
+
+{% block content %}<div id="content-main">
+<form action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% csrf_token %}{% block form_top %}{% endblock %}
+<div>
+{% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %}
+
+{% if save_on_top %}
+ <div class="submit-row" {% if is_popup %}style="overflow: auto;"{% endif %}>
+ <input type="submit" value="{% trans 'Submit' %}" class="default" name="_save" />
+ </div>
+{% endif %}
+
+{% if errors %}
+ <p class="errornote">
+ {% blocktrans count errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
+ </p>
+ {{ adminform.form.non_field_errors }}
+{% endif %}
+
+{% for fieldset in adminform %}
+ {% include "admin/includes/fieldset.html" %}
+{% endfor %}
+
+<div class="submit-row" {% if is_popup %}style="overflow: auto;"{% endif %}>
+ <input type="submit" value="{% trans 'Submit' %}" class="default" name="_save" />
+</div>
+
+<script type="text/javascript">document.getElementById("{{ adminform.first_field.id_for_label }}").focus();</script>
+</div>
+</form></div>
+{% endblock %}
View
12 polymorphic/templates/admin/polymorphic/change_form.html
@@ -0,0 +1,12 @@
+{% extends "admin/change_form.html" %}
+{% load i18n polymorphic_admin_tags %}
+
+{# fix breadcrumb #}
+{% block breadcrumbs %}{% if not is_popup %}{% breadcrumb_scope base_opts %}
+<div class="breadcrumbs">
+ <a href="../../../">{% trans "Home" %}</a> &rsaquo;
+ <a href="../../">{{ app_label|capfirst|escape }}</a> &rsaquo;
+ {% if has_change_permission %}<a href="../">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %} &rsaquo;
+ {% if add %}{% trans "Add" %} {{ opts.verbose_name }}{% else %}{{ original|truncatewords:"18" }}{% endif %}
+</div>
+{% endbreadcrumb_scope %}{% endif %}{% endblock %}
View
12 polymorphic/templates/admin/polymorphic/delete_confirmation.html
@@ -0,0 +1,12 @@
+{% extends "admin/delete_confirmation.html" %}
+{% load i18n polymorphic_admin_tags %}
+
+{% block breadcrumbs %}{% breadcrumb_scope base_opts %}
+<div class="breadcrumbs">
+ <a href="../../../../">{% trans "Home" %}</a> &rsaquo;
+ <a href="../../../">{{ app_label|capfirst|escape }}</a> &rsaquo;
+ <a href="../../">{{ opts.verbose_name_plural|capfirst }}</a> &rsaquo;
+ <a href="../">{{ object|truncatewords:"18" }}</a> &rsaquo;
+ {% trans 'Delete' %}
+</div>
+{% endbreadcrumb_scope %}{% endblock %}
View
0  polymorphic/templatetags/__init__.py
No changes.
View
52 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)
View
20 settings.py
@@ -68,18 +68,20 @@
# 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 = (
'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 +90,14 @@
)
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
)
View
6 setup.py
@@ -8,7 +8,11 @@
author_email = 'bert.constantin@gmx.de',
maintainer = 'Christopher Glass',
maintainer_email = 'tribaal@gmail.com',
- packages = [ 'polymorphic' ],
+ url = 'https://github.com/chrisglass/django_polymorphic',
+ packages = [
+ 'polymorphic',
+ 'polymorphic.templatetags',
+ ],
classifiers=[
'Framework :: Django',
'Intended Audience :: Developers',
View
10 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)),
+)
Something went wrong with that request. Please try again.