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.fix_request_path_info
parent
0b608cc67e
commit
0d5f2fd943
|
|
@ -13,7 +13,6 @@ from django.core.urlresolvers import RegexURLResolver
|
|||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template.context import RequestContext
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.utils.encoding import force_unicode
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
|
@ -21,6 +20,15 @@ from django.utils.translation import ugettext_lazy as _
|
|||
__all__ = ('PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin', 'PolymorphicChildModelAdmin')
|
||||
|
||||
|
||||
class RegistrationClosed(RuntimeError):
|
||||
"The admin model can't be registered anymore at this point."
|
||||
pass
|
||||
|
||||
class ChildAdminNotRegistered(RuntimeError):
|
||||
"The admin site for the model is not registered."
|
||||
pass
|
||||
|
||||
|
||||
class PolymorphicModelChoiceForm(forms.Form):
|
||||
"""
|
||||
The default form for the ``add_type_form``. Can be overwritten and replaced.
|
||||
|
|
@ -47,12 +55,11 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
|
|||
|
||||
Alternatively, the following methods can be implemented:
|
||||
|
||||
* :func:`get_admin_for_model` should return a ModelAdmin instance for the derived model.
|
||||
* :func:`get_child_model_classes` should return the available derived models.
|
||||
* :func:`get_child_models` should return a list of (Model, ModelAdmin) tuples
|
||||
* optionally, :func:`get_child_type_choices` can be overwritten to refine the choices for the add dialog.
|
||||
|
||||
This class needs to be inherited by the model admin base class that is registered in the site.
|
||||
The derived models should *not* register the ModelAdmin, but instead it should be returned by :func:`get_admin_for_model`.
|
||||
The derived models should *not* register the ModelAdmin, but instead it should be returned by :func:`get_child_models`.
|
||||
"""
|
||||
|
||||
#: The base model that the class uses
|
||||
|
|
@ -70,43 +77,61 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
|
|||
|
||||
def __init__(self, model, admin_site, *args, **kwargs):
|
||||
super(PolymorphicParentModelAdmin, self).__init__(model, admin_site, *args, **kwargs)
|
||||
self.initialized_child_models = None
|
||||
self.child_admin_site = AdminSite(name=self.admin_site.name)
|
||||
|
||||
# Allow to declaratively define the child models + admin classes
|
||||
if self.child_models is not None:
|
||||
self.initialized_child_models = SortedDict()
|
||||
for Model, Admin in self.child_models:
|
||||
assert issubclass(Model, self.base_model), "{0} should be a subclass of {1}".format(Model.__name__, self.base_model.__name__)
|
||||
assert issubclass(Admin, admin.ModelAdmin), "{0} should be a subclass of {1}".format(Admin.__name__, admin.ModelAdmin.__name__)
|
||||
self.child_admin_site.register(Model, Admin)
|
||||
|
||||
# HACK: need to get admin instance.
|
||||
admin_instance = self.child_admin_site._registry[Model]
|
||||
self.initialized_child_models[Model] = admin_instance
|
||||
self._child_admin_site = AdminSite(name=self.admin_site.name)
|
||||
self._is_setup = False
|
||||
|
||||
|
||||
def get_admin_for_model(self, model):
|
||||
def _lazy_setup(self):
|
||||
if self._is_setup:
|
||||
return
|
||||
|
||||
# By not having this in __init__() there is less stress on import dependencies as well,
|
||||
# considering an advanced use cases where a plugin system scans for the child models.
|
||||
child_models = self.get_child_models()
|
||||
for Model, Admin in child_models:
|
||||
self.register_child(Model, Admin)
|
||||
self._child_models = dict(child_models)
|
||||
|
||||
# This is needed to deal with the improved ForeignKeyRawIdWidget in Django 1.4 and perhaps other widgets too.
|
||||
# The ForeignKeyRawIdWidget checks whether the referenced model is registered in the admin, otherwise it displays itself as a textfield.
|
||||
# As simple solution, just make sure all parent admin models are also know in the child admin site.
|
||||
# This should be done after all parent models are registered off course.
|
||||
complete_registry = self.admin_site._registry.copy()
|
||||
complete_registry.update(self._child_admin_site._registry)
|
||||
|
||||
self._child_admin_site._registry = complete_registry
|
||||
self._is_setup = True
|
||||
|
||||
|
||||
def register_child(self, model, model_admin):
|
||||
"""
|
||||
Return the polymorphic admin interface for a given model.
|
||||
Register a model with admin to display.
|
||||
"""
|
||||
if self.initialized_child_models is None:
|
||||
raise NotImplementedError("Implement get_admin_for_model() or child_models")
|
||||
# After the get_urls() is called, the URLs of the child model can't be exposed anymore to the Django URLconf,
|
||||
# which also means that a "Save and continue editing" button won't work.
|
||||
if self._is_setup:
|
||||
raise RegistrationClosed("The admin model can't be registered anymore at this point.")
|
||||
|
||||
return self.initialized_child_models[model]
|
||||
if not issubclass(model, self.base_model):
|
||||
raise TypeError("{0} should be a subclass of {1}".format(model.__name__, self.base_model.__name__))
|
||||
if not issubclass(model_admin, admin.ModelAdmin):
|
||||
raise TypeError("{0} should be a subclass of {1}".format(model_admin.__name__, admin.ModelAdmin.__name__))
|
||||
|
||||
self._child_admin_site.register(model, model_admin)
|
||||
|
||||
|
||||
def get_child_model_classes(self):
|
||||
def get_child_models(self):
|
||||
"""
|
||||
Return the derived model classes which this admin should handle.
|
||||
This should return a list of tuples, exactly like :attr:`child_models` is.
|
||||
|
||||
This could either be implemented as ``base_model.__subclasses__()``,
|
||||
a setting in a config file, or a query of a plugin registration system.
|
||||
The model classes can be retrieved as ``base_model.__subclasses__()``,
|
||||
a setting in a config file, or a query of a plugin registration system at your option
|
||||
"""
|
||||
if self.initialized_child_models is None:
|
||||
raise NotImplementedError("Implement get_child_model_classes() or child_models")
|
||||
if self.child_models is None:
|
||||
raise NotImplementedError("Implement get_child_models() or child_models")
|
||||
|
||||
return self.initialized_child_models.keys()
|
||||
return self.child_models
|
||||
|
||||
|
||||
def get_child_type_choices(self):
|
||||
|
|
@ -114,7 +139,7 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
|
|||
Return a list of polymorphic types which can be added.
|
||||
"""
|
||||
choices = []
|
||||
for model in self.get_child_model_classes():
|
||||
for model, _ in self.get_child_models():
|
||||
ct = ContentType.objects.get_for_model(model)
|
||||
choices.append((ct.id, model._meta.verbose_name))
|
||||
return choices
|
||||
|
|
@ -135,12 +160,21 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
|
|||
if not model_class:
|
||||
raise Http404("No model found for '{0}.{1}'.".format(*ct.natural_key())) # Handle model deletion
|
||||
|
||||
# The views are already checked for permissions, so ensure the model is a derived object.
|
||||
# Otherwise, it would open all admin views to users who can edit the base object.
|
||||
if not issubclass(model_class, self.base_model):
|
||||
raise PermissionDenied("Invalid model '{0}.{1}', must derive from {name}.".format(*ct.natural_key(), name=self.base_model.__name__))
|
||||
return self._get_real_admin_by_model(model_class)
|
||||
|
||||
return self.get_admin_for_model(model_class)
|
||||
|
||||
def _get_real_admin_by_model(self, model_class):
|
||||
# In case of a ?ct_id=### parameter, the view is already checked for permissions.
|
||||
# Hence, make sure this is a derived object, or risk exposing other admin interfaces.
|
||||
if model_class not in self._child_models:
|
||||
raise PermissionDenied("Invalid model '{0}', it must be registered as child model.".format(model_class))
|
||||
|
||||
try:
|
||||
# HACK: the only way to get the instance of an model admin,
|
||||
# is to read the registry of the AdminSite.
|
||||
return self._child_admin_site._registry[model_class]
|
||||
except KeyError:
|
||||
raise ChildAdminNotRegistered("No child admin site was registered for a '{0}' model.".format(model_class))
|
||||
|
||||
|
||||
def queryset(self, request):
|
||||
|
|
@ -194,11 +228,14 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin):
|
|||
url(r'^(?P<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
|
||||
|
|
|
|||
Loading…
Reference in New Issue