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..d0974e0 100644 --- a/DOCS.rst +++ b/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). 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/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/polymorphic/admin.py b/polymorphic/admin.py new file mode 100644 index 0000000..72a0dc7 --- /dev/null +++ b/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.+)$', 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 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 new file mode 100644 index 0000000..56043a1 --- /dev/null +++ b/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 %} + +{% 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..c37c385 --- /dev/null +++ b/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 %} + +{% 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) diff --git a/settings.py b/settings.py index 01e4ac4..4439e8f 100644 --- a/settings.py +++ b/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. 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 @@ 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/setup.py b/setup.py index 740d206..7beb24f 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,11 @@ setup( 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', 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)), +)