407 lines
16 KiB
Python
407 lines
16 KiB
Python
"""
|
|
The parent admin displays the list view of the base model.
|
|
"""
|
|
import sys
|
|
|
|
from django.contrib import admin
|
|
from django.contrib.admin.helpers import AdminErrorList, AdminForm
|
|
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
|
from django.db import models
|
|
from django.http import Http404, HttpResponseRedirect
|
|
from django.template.response import TemplateResponse
|
|
from django.utils.encoding import force_text
|
|
from django.utils.http import urlencode
|
|
from django.utils.safestring import mark_safe
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from polymorphic.utils import get_base_polymorphic_model
|
|
|
|
from .forms import PolymorphicModelChoiceForm
|
|
|
|
try:
|
|
# Django 2.0+
|
|
from django.urls import URLResolver
|
|
except ImportError:
|
|
# Django < 2.0
|
|
from django.urls import RegexURLResolver as URLResolver
|
|
|
|
|
|
if sys.version_info[0] >= 3:
|
|
long = int
|
|
|
|
|
|
class RegistrationClosed(RuntimeError):
|
|
"The admin model can't be registered anymore at this point."
|
|
|
|
|
|
class ChildAdminNotRegistered(RuntimeError):
|
|
"The admin site for the model is not registered."
|
|
|
|
|
|
class PolymorphicParentModelAdmin(admin.ModelAdmin):
|
|
"""
|
|
A admin interface that can displays different change/delete pages, depending on the polymorphic model.
|
|
To use this class, one attribute need to be defined:
|
|
|
|
* :attr:`child_models` should be a list models.
|
|
|
|
Alternatively, the following methods can be implemented:
|
|
|
|
* :func:`get_child_models` should return a list of models.
|
|
* 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 (auto-detected if not set explicitly)
|
|
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
|
|
|
|
#: The regular expression to filter the primary key in the URL.
|
|
#: This accepts only numbers as defensive measure against catch-all URLs.
|
|
#: If your primary key consists of string values, update this regular expression.
|
|
pk_regex = r"(\d+|__fk__)"
|
|
|
|
def __init__(self, model, admin_site, *args, **kwargs):
|
|
super(PolymorphicParentModelAdmin, self).__init__(
|
|
model, admin_site, *args, **kwargs
|
|
)
|
|
self._is_setup = False
|
|
|
|
if self.base_model is None:
|
|
self.base_model = get_base_polymorphic_model(model)
|
|
|
|
def _lazy_setup(self):
|
|
if self._is_setup:
|
|
return
|
|
|
|
self._child_models = self.get_child_models()
|
|
|
|
# Make absolutely sure that the child models don't use the old 0.9 format,
|
|
# as of polymorphic 1.4 this deprecated configuration is no longer supported.
|
|
# Instead, register the child models in the admin too.
|
|
if self._child_models and not issubclass(self._child_models[0], models.Model):
|
|
raise ImproperlyConfigured(
|
|
"Since django-polymorphic 1.4, the `child_models` attribute "
|
|
"and `get_child_models()` method should be a list of models only.\n"
|
|
"The model-admin class should be registered in the regular Django admin."
|
|
)
|
|
|
|
self._child_admin_site = self.admin_site
|
|
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, request, action):
|
|
"""
|
|
Return a list of polymorphic types for which the user has the permission to perform the given action.
|
|
"""
|
|
self._lazy_setup()
|
|
choices = []
|
|
for model in self.get_child_models():
|
|
perm_function_name = "has_{0}_permission".format(action)
|
|
model_admin = self._get_real_admin_by_model(model)
|
|
perm_function = getattr(model_admin, perm_function_name)
|
|
if not perm_function(request):
|
|
continue
|
|
ct = ContentType.objects.get_for_model(model, for_concrete_model=False)
|
|
choices.append((ct.id, model._meta.verbose_name))
|
|
return choices
|
|
|
|
def _get_real_admin(self, object_id, super_if_self=True):
|
|
try:
|
|
obj = (
|
|
self.model.objects.non_polymorphic()
|
|
.values("polymorphic_ctype")
|
|
.get(pk=object_id)
|
|
)
|
|
except self.model.DoesNotExist:
|
|
raise Http404
|
|
return self._get_real_admin_by_ct(
|
|
obj["polymorphic_ctype"], super_if_self=super_if_self
|
|
)
|
|
|
|
def _get_real_admin_by_ct(self, ct_id, super_if_self=True):
|
|
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:
|
|
# Handle model deletion
|
|
raise Http404("No model found for '{0}.{1}'.".format(*ct.natural_key()))
|
|
|
|
return self._get_real_admin_by_model(model_class, super_if_self=super_if_self)
|
|
|
|
def _get_real_admin_by_model(self, model_class, super_if_self=True):
|
|
# 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.
|
|
real_admin = self._child_admin_site._registry[model_class]
|
|
except KeyError:
|
|
raise ChildAdminNotRegistered(
|
|
"No child admin site was registered for a '{0}' model.".format(
|
|
model_class
|
|
)
|
|
)
|
|
|
|
if super_if_self and real_admin is self:
|
|
return super(PolymorphicParentModelAdmin, self)
|
|
else:
|
|
return real_admin
|
|
|
|
def get_queryset(self, request):
|
|
# optimize the list display.
|
|
qs = super(PolymorphicParentModelAdmin, self).get_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)
|
|
# rebuild form_url, otherwise libraries below will override it.
|
|
form_url = add_preserved_filters(
|
|
{
|
|
"preserved_filters": urlencode({"ct_id": ct_id}),
|
|
"opts": self.model._meta,
|
|
},
|
|
form_url,
|
|
)
|
|
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."""
|
|
real_admin = self._get_real_admin(object_id)
|
|
return real_admin.change_view(request, object_id, *args, **kwargs)
|
|
|
|
def changeform_view(self, request, object_id=None, *args, **kwargs):
|
|
# The `changeform_view` is available as of Django 1.7, combining the add_view and change_view.
|
|
# As it's directly called by django-reversion, this method is also overwritten to make sure it
|
|
# also redirects to the child admin.
|
|
if object_id:
|
|
real_admin = self._get_real_admin(object_id)
|
|
return real_admin.changeform_view(request, object_id, *args, **kwargs)
|
|
else:
|
|
# Add view. As it should already be handled via `add_view`, this means something custom is done here!
|
|
return super(PolymorphicParentModelAdmin, self).changeform_view(
|
|
request, object_id, *args, **kwargs
|
|
)
|
|
|
|
def history_view(self, request, object_id, extra_context=None):
|
|
"""Redirect the history view to the real admin."""
|
|
real_admin = self._get_real_admin(object_id)
|
|
return real_admin.history_view(request, object_id, extra_context=extra_context)
|
|
|
|
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_preserved_filters(self, request):
|
|
if "_changelist_filters" in request.GET:
|
|
request.GET = request.GET.copy()
|
|
filters = request.GET.get("_changelist_filters")
|
|
f = filters.split("&")
|
|
for x in f:
|
|
c = x.split("=")
|
|
request.GET[c[0]] = c[1]
|
|
del request.GET["_changelist_filters"]
|
|
return super(PolymorphicParentModelAdmin, self).get_preserved_filters(request)
|
|
|
|
def get_urls(self):
|
|
"""
|
|
Expose the custom URLs for the subclasses and the URL resolver.
|
|
"""
|
|
urls = super(PolymorphicParentModelAdmin, self).get_urls()
|
|
|
|
# At this point. all admin code needs to be known.
|
|
self._lazy_setup()
|
|
|
|
return 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:
|
|
# See if the path started with an ID.
|
|
try:
|
|
pos = path.find("/")
|
|
if pos == -1:
|
|
object_id = long(path)
|
|
else:
|
|
object_id = long(path[0:pos])
|
|
except ValueError:
|
|
raise Http404(
|
|
"No ct_id parameter, unable to find admin subclass for path '{0}'.".format(
|
|
path
|
|
)
|
|
)
|
|
|
|
ct_id = self.model.objects.values_list(
|
|
"polymorphic_ctype_id", flat=True
|
|
).get(pk=object_id)
|
|
|
|
real_admin = self._get_real_admin_by_ct(ct_id)
|
|
resolver = URLResolver("^", real_admin.urls)
|
|
resolvermatch = resolver.resolve(path) # May raise Resolver404
|
|
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.
|
|
"""
|
|
if not self.has_add_permission(request):
|
|
raise PermissionDenied
|
|
|
|
extra_qs = ""
|
|
if request.META["QUERY_STRING"]:
|
|
# QUERY_STRING is bytes in Python 3, using force_text() to decode it as string.
|
|
# See QueryDict how Django deals with that.
|
|
extra_qs = "&{0}".format(force_text(request.META["QUERY_STRING"]))
|
|
|
|
choices = self.get_child_type_choices(request, "add")
|
|
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_text(opts.verbose_name),
|
|
"adminform": adminForm,
|
|
"is_popup": ("_popup" in request.POST or "_popup" in request.GET),
|
|
"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,
|
|
"add": True,
|
|
"save_on_top": self.save_on_top,
|
|
}
|
|
)
|
|
|
|
templates = 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",
|
|
]
|
|
|
|
request.current_app = self.admin_site.name
|
|
return TemplateResponse(request, templates, context)
|
|
|
|
@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",
|
|
]
|