Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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.
  • Loading branch information...
commit 0d5f2fd943bdad2afda159479ac05068d80d4ad1 1 parent 0b608cc
@vdboor vdboor authored
Showing with 72 additions and 35 deletions.
  1. +72 −35 polymorphic/admin.py
View
107 polymorphic/admin.py
@@ -13,7 +13,6 @@
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template.context import RequestContext
-from django.utils.datastructures import SortedDict
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
@@ -21,6 +20,15 @@
__all__ = ('PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin', 'PolymorphicChildModelAdmin')
+class RegistrationClosed(RuntimeError):
+ "The admin model can't be registered anymore at this point."
+ pass
+
+class ChildAdminNotRegistered(RuntimeError):
+ "The admin site for the model is not registered."
+ pass
+
+
class PolymorphicModelChoiceForm(forms.Form):
"""
The default form for the ``add_type_form``. Can be overwritten and replaced.
@@ -47,12 +55,11 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
Alternatively, the following methods can be implemented:
- * :func:`get_admin_for_model` should return a ModelAdmin instance for the derived model.
- * :func:`get_child_model_classes` should return the available derived models.
+ * :func:`get_child_models` should return a list of (Model, ModelAdmin) tuples
* optionally, :func:`get_child_type_choices` can be overwritten to refine the choices for the add dialog.
This class needs to be inherited by the model admin base class that is registered in the site.
- The derived models should *not* register the ModelAdmin, but instead it should be returned by :func:`get_admin_for_model`.
+ The derived models should *not* register the ModelAdmin, but instead it should be returned by :func:`get_child_models`.
"""
#: The base model that the class uses
@@ -70,43 +77,61 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
def __init__(self, model, admin_site, *args, **kwargs):
super(PolymorphicParentModelAdmin, self).__init__(model, admin_site, *args, **kwargs)
- self.initialized_child_models = None
- self.child_admin_site = AdminSite(name=self.admin_site.name)
+ 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)
- # Allow to declaratively define the child models + admin classes
- if self.child_models is not None:
- self.initialized_child_models = SortedDict()
- for Model, Admin in self.child_models:
- assert issubclass(Model, self.base_model), "{0} should be a subclass of {1}".format(Model.__name__, self.base_model.__name__)
- assert issubclass(Admin, admin.ModelAdmin), "{0} should be a subclass of {1}".format(Admin.__name__, admin.ModelAdmin.__name__)
- self.child_admin_site.register(Model, Admin)
+ # 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)
- # HACK: need to get admin instance.
- admin_instance = self.child_admin_site._registry[Model]
- self.initialized_child_models[Model] = admin_instance
+ self._child_admin_site._registry = complete_registry
+ self._is_setup = True
- def get_admin_for_model(self, model):
+ def register_child(self, model, model_admin):
"""
- Return the polymorphic admin interface for a given model.
+ Register a model with admin to display.
"""
- if self.initialized_child_models is None:
- raise NotImplementedError("Implement get_admin_for_model() or child_models")
+ # After the get_urls() is called, the URLs of the child model can't be exposed anymore to the Django URLconf,
+ # which also means that a "Save and continue editing" button won't work.
+ if self._is_setup:
+ raise RegistrationClosed("The admin model can't be registered anymore at this point.")
- return self.initialized_child_models[model]
+ if not issubclass(model, self.base_model):
+ raise TypeError("{0} should be a subclass of {1}".format(model.__name__, self.base_model.__name__))
+ if not issubclass(model_admin, admin.ModelAdmin):
+ raise TypeError("{0} should be a subclass of {1}".format(model_admin.__name__, admin.ModelAdmin.__name__))
+ self._child_admin_site.register(model, model_admin)
- def get_child_model_classes(self):
+
+ def get_child_models(self):
"""
Return the derived model classes which this admin should handle.
+ This should return a list of tuples, exactly like :attr:`child_models` is.
- This could either be implemented as ``base_model.__subclasses__()``,
- a setting in a config file, or a query of a plugin registration system.
+ The model classes can be retrieved as ``base_model.__subclasses__()``,
+ a setting in a config file, or a query of a plugin registration system at your option
"""
- if self.initialized_child_models is None:
- raise NotImplementedError("Implement get_child_model_classes() or child_models")
+ if self.child_models is None:
+ raise NotImplementedError("Implement get_child_models() or child_models")
- return self.initialized_child_models.keys()
+ return self.child_models
def get_child_type_choices(self):
@@ -114,7 +139,7 @@ def get_child_type_choices(self):
Return a list of polymorphic types which can be added.
"""
choices = []
- for model in self.get_child_model_classes():
+ for model, _ in self.get_child_models():
ct = ContentType.objects.get_for_model(model)
choices.append((ct.id, model._meta.verbose_name))
return choices
@@ -135,12 +160,21 @@ def _get_real_admin_by_ct(self, ct_id):
if not model_class:
raise Http404("No model found for '{0}.{1}'.".format(*ct.natural_key())) # Handle model deletion
- # The views are already checked for permissions, so ensure the model is a derived object.
- # Otherwise, it would open all admin views to users who can edit the base object.
- if not issubclass(model_class, self.base_model):
- raise PermissionDenied("Invalid model '{0}.{1}', must derive from {name}.".format(*ct.natural_key(), name=self.base_model.__name__))
+ return self._get_real_admin_by_model(model_class)
+
- return self.get_admin_for_model(model_class)
+ def _get_real_admin_by_model(self, model_class):
+ # In case of a ?ct_id=### parameter, the view is already checked for permissions.
+ # Hence, make sure this is a derived object, or risk exposing other admin interfaces.
+ if model_class not in self._child_models:
+ raise PermissionDenied("Invalid model '{0}', it must be registered as child model.".format(model_class))
+
+ try:
+ # HACK: the only way to get the instance of an model admin,
+ # is to read the registry of the AdminSite.
+ return self._child_admin_site._registry[model_class]
+ except KeyError:
+ raise ChildAdminNotRegistered("No child admin site was registered for a '{0}' model.".format(model_class))
def queryset(self, request):
@@ -194,11 +228,14 @@ def get_urls(self):
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_model_classes():
- admin = self.get_admin_for_model(model)
+ for model, _ in self.get_child_models():
+ admin = self._get_real_admin_by_model(model)
dummy_urls += admin.get_urls()
return urls + custom_urls + dummy_urls
Please sign in to comment.
Something went wrong with that request. Please try again.