commit
28b8885236
|
|
@ -12,3 +12,4 @@ Contributors
|
||||||
* Charles Leifer (python 2.4 compatibility)
|
* Charles Leifer (python 2.4 compatibility)
|
||||||
* Germán M. Bravo
|
* Germán M. Bravo
|
||||||
* Martin Brochhaus
|
* Martin Brochhaus
|
||||||
|
* Diederik van der Boor (polymorphic admin interface)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,12 @@
|
||||||
Changelog
|
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
|
2011-12-20 Renaming, refactoring, new maintainer
|
||||||
================================================
|
================================================
|
||||||
|
|
|
||||||
95
DOCS.rst
95
DOCS.rst
|
|
@ -153,6 +153,98 @@ In the examples below, these models are being used::
|
||||||
field3 = models.CharField(max_length=10)
|
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() ):
|
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
|
``extra()`` has one restriction: the resulting objects are required to have
|
||||||
a unique primary key within the result set.
|
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
|
* Diamond shaped inheritance: There seems to be a general problem
|
||||||
with diamond shaped multiple model inheritance with Django models
|
with diamond shaped multiple model inheritance with Django models
|
||||||
(tested with V1.1 - V1.3).
|
(tested with V1.1 - V1.3).
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@ include README.rst
|
||||||
include LICENSE
|
include LICENSE
|
||||||
include DOCS.rst
|
include DOCS.rst
|
||||||
include CHANGES.rst
|
include CHANGES.rst
|
||||||
|
recursive-include polymorphic/templates/ *.html
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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> ›
|
||||||
|
<a href="../../">{{ app_label|capfirst|escape }}</a> ›
|
||||||
|
{% if has_change_permission %}<a href="../">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %} ›
|
||||||
|
{% 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 %}
|
||||||
|
|
@ -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> ›
|
||||||
|
<a href="../../">{{ app_label|capfirst|escape }}</a> ›
|
||||||
|
{% if has_change_permission %}<a href="../">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %} ›
|
||||||
|
{% if add %}{% trans "Add" %} {{ opts.verbose_name }}{% else %}{{ original|truncatewords:"18" }}{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endbreadcrumb_scope %}{% endif %}{% endblock %}
|
||||||
|
|
@ -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> ›
|
||||||
|
<a href="../../../">{{ app_label|capfirst|escape }}</a> ›
|
||||||
|
<a href="../../">{{ opts.verbose_name_plural|capfirst }}</a> ›
|
||||||
|
<a href="../">{{ object|truncatewords:"18" }}</a> ›
|
||||||
|
{% trans 'Delete' %}
|
||||||
|
</div>
|
||||||
|
{% endbreadcrumb_scope %}{% endblock %}
|
||||||
|
|
@ -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)
|
||||||
20
settings.py
20
settings.py
|
|
@ -68,18 +68,20 @@ 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.
|
# List of callables that know how to import templates from various sources.
|
||||||
TEMPLATE_LOADERS = (
|
TEMPLATE_LOADERS = (
|
||||||
'django.template.loaders.filesystem.load_template_source',
|
'django.template.loaders.filesystem.Loader',
|
||||||
'django.template.loaders.app_directories.load_template_source',
|
'django.template.loaders.app_directories.Loader',
|
||||||
# 'django.template.loaders.eggs.load_template_source',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = (
|
MIDDLEWARE_CLASSES = (
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'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 = (
|
TEMPLATE_DIRS = (
|
||||||
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
|
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
|
||||||
|
|
@ -88,10 +90,14 @@ TEMPLATE_DIRS = (
|
||||||
)
|
)
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
#'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
|
'django.contrib.admin',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
#'django.contrib.sessions',
|
'django.contrib.messages',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
|
||||||
#'django.contrib.sites',
|
#'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
|
'pexp', # this Django app is for testing and experimentation; not needed otherwise
|
||||||
)
|
)
|
||||||
|
|
|
||||||
6
setup.py
6
setup.py
|
|
@ -8,7 +8,11 @@ setup(
|
||||||
author_email = 'bert.constantin@gmx.de',
|
author_email = 'bert.constantin@gmx.de',
|
||||||
maintainer = 'Christopher Glass',
|
maintainer = 'Christopher Glass',
|
||||||
maintainer_email = 'tribaal@gmail.com',
|
maintainer_email = 'tribaal@gmail.com',
|
||||||
packages = [ 'polymorphic' ],
|
url = 'https://github.com/chrisglass/django_polymorphic',
|
||||||
|
packages = [
|
||||||
|
'polymorphic',
|
||||||
|
'polymorphic.templatetags',
|
||||||
|
],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Framework :: Django',
|
'Framework :: Django',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue