2013-05-30 10:33:00 +00:00
|
|
|
import logging
|
2013-07-18 13:02:07 +00:00
|
|
|
import os
|
2013-11-28 15:50:41 +00:00
|
|
|
import sys
|
2016-05-07 23:31:16 +00:00
|
|
|
from collections import namedtuple
|
2013-05-30 10:33:00 +00:00
|
|
|
|
2016-05-07 23:31:16 +00:00
|
|
|
import extra_views
|
2016-05-26 17:14:39 +00:00
|
|
|
from django.forms import modelform_factory
|
2021-03-09 12:14:40 +00:00
|
|
|
from django.urls import re_path, reverse
|
2013-05-30 10:33:00 +00:00
|
|
|
|
2016-05-07 23:31:16 +00:00
|
|
|
from . import actions
|
2013-05-30 10:33:00 +00:00
|
|
|
from . import apiviews
|
2013-06-03 20:44:16 +00:00
|
|
|
from . import settings
|
2013-05-30 10:33:00 +00:00
|
|
|
from . import utils
|
2016-05-07 23:31:16 +00:00
|
|
|
from . import views
|
2013-05-30 10:33:00 +00:00
|
|
|
|
2013-05-31 14:33:28 +00:00
|
|
|
|
2021-03-09 12:14:40 +00:00
|
|
|
logger = logging.getLogger("djadmin2")
|
2013-05-30 10:33:00 +00:00
|
|
|
|
|
|
|
|
|
2013-06-21 05:41:28 +00:00
|
|
|
class ModelAdminBase2(type):
|
|
|
|
|
def __new__(cls, name, bases, attrs):
|
2021-10-17 08:51:27 +00:00
|
|
|
new_class = super().__new__(cls, name, bases, attrs)
|
2013-11-25 17:51:55 +00:00
|
|
|
view_list = []
|
2013-06-23 18:36:01 +00:00
|
|
|
for key, value in attrs.items():
|
|
|
|
|
if isinstance(value, views.AdminView):
|
|
|
|
|
if not value.name:
|
|
|
|
|
value.name = key
|
|
|
|
|
view_list.append(value)
|
|
|
|
|
|
2021-03-09 12:14:40 +00:00
|
|
|
view_list.extend(getattr(new_class, "views", []))
|
2013-11-25 17:51:55 +00:00
|
|
|
new_class.views = view_list
|
2013-06-21 13:16:08 +00:00
|
|
|
return new_class
|
2013-06-21 05:41:28 +00:00
|
|
|
|
|
|
|
|
|
2020-09-26 21:21:43 +00:00
|
|
|
class ModelAdmin2(metaclass=ModelAdminBase2):
|
2013-05-30 10:33:00 +00:00
|
|
|
"""
|
2013-07-07 10:10:50 +00:00
|
|
|
Adding new ModelAdmin2 attributes:
|
|
|
|
|
|
|
|
|
|
Step 1: Add the attribute to this class
|
|
|
|
|
Step 2: Add the attribute to djadmin2.settings.MODEL_ADMIN_ATTRS
|
|
|
|
|
|
|
|
|
|
Reasoning:
|
|
|
|
|
|
|
|
|
|
Changing values on ModelAdmin2 objects or their attributes from
|
|
|
|
|
within a view results in leaky scoping issues. Therefore, we use
|
|
|
|
|
the immutable_admin_factory to render the ModelAdmin2 class
|
|
|
|
|
practically immutable before passing it to the view. To constrain
|
|
|
|
|
things further (in order to protect ourselves from causing
|
|
|
|
|
hard-to-find security problems), we also restrict which attrs are
|
|
|
|
|
passed to the final ImmutableAdmin object (i.e. a namedtuple).
|
|
|
|
|
This prevents us from easily implementing methods/setters which
|
|
|
|
|
bypass the blocking features of the ImmutableAdmin.
|
2013-05-30 10:33:00 +00:00
|
|
|
"""
|
2021-03-09 12:14:40 +00:00
|
|
|
|
2013-07-29 18:48:05 +00:00
|
|
|
actions_selection_counter = True
|
2013-07-30 20:28:24 +00:00
|
|
|
date_hierarchy = False
|
2021-03-09 12:14:40 +00:00
|
|
|
list_display = ("__str__",)
|
2013-05-30 10:33:00 +00:00
|
|
|
list_display_links = ()
|
|
|
|
|
list_filter = ()
|
|
|
|
|
list_select_related = False
|
|
|
|
|
list_per_page = 100
|
|
|
|
|
list_max_show_all = 200
|
|
|
|
|
list_editable = ()
|
|
|
|
|
search_fields = ()
|
|
|
|
|
save_as = False
|
|
|
|
|
save_on_top = False
|
|
|
|
|
verbose_name = None
|
|
|
|
|
verbose_name_plural = None
|
2013-06-03 20:44:16 +00:00
|
|
|
model_admin_attributes = settings.MODEL_ADMIN_ATTRS
|
2013-08-01 16:00:03 +00:00
|
|
|
ordering = False
|
2013-07-07 12:19:23 +00:00
|
|
|
save_on_top = False
|
|
|
|
|
save_on_bottom = True
|
|
|
|
|
|
|
|
|
|
# Not yet implemented. See #267 and #268
|
|
|
|
|
actions_on_bottom = False
|
|
|
|
|
actions_on_top = True
|
2013-05-30 10:33:00 +00:00
|
|
|
|
|
|
|
|
search_fields = []
|
|
|
|
|
|
|
|
|
|
# Show the fields to be displayed as columns
|
|
|
|
|
# TODO: Confirm that this is what the Django admin uses
|
|
|
|
|
list_fields = []
|
|
|
|
|
|
2013-07-07 09:47:19 +00:00
|
|
|
# This shows up on the DocumentListView of the Posts
|
2013-05-31 14:47:25 +00:00
|
|
|
list_actions = [actions.DeleteSelectedAction]
|
2013-05-30 10:33:00 +00:00
|
|
|
|
|
|
|
|
# This shows up in the DocumentDetailView of the Posts.
|
|
|
|
|
document_actions = []
|
|
|
|
|
|
2013-07-07 09:47:19 +00:00
|
|
|
# Shows up on a particular field
|
2013-05-30 10:33:00 +00:00
|
|
|
field_actions = {}
|
|
|
|
|
|
2013-07-07 09:47:19 +00:00
|
|
|
# Defines custom field renderers
|
|
|
|
|
field_renderers = {}
|
|
|
|
|
|
2013-05-30 10:33:00 +00:00
|
|
|
fields = None
|
|
|
|
|
exclude = None
|
|
|
|
|
fieldsets = None
|
|
|
|
|
form_class = None
|
|
|
|
|
filter_vertical = ()
|
|
|
|
|
filter_horizontal = ()
|
|
|
|
|
radio_fields = {}
|
|
|
|
|
prepopulated_fields = {}
|
|
|
|
|
formfield_overrides = {}
|
|
|
|
|
readonly_fields = ()
|
|
|
|
|
ordering = None
|
|
|
|
|
|
|
|
|
|
create_form_class = None
|
|
|
|
|
update_form_class = None
|
|
|
|
|
|
|
|
|
|
inlines = []
|
|
|
|
|
|
|
|
|
|
# Views
|
2021-03-09 12:14:40 +00:00
|
|
|
index_view = views.AdminView(r"^$", views.ModelListView, name="index")
|
|
|
|
|
create_view = views.AdminView(r"^create/$", views.ModelAddFormView, name="create")
|
|
|
|
|
update_view = views.AdminView(
|
|
|
|
|
r"^(?P<pk>[0-9]+)/$", views.ModelEditFormView, name="update"
|
|
|
|
|
)
|
|
|
|
|
detail_view = views.AdminView(
|
|
|
|
|
r"^(?P<pk>[0-9]+)/update/$", views.ModelDetailView, name="detail"
|
|
|
|
|
)
|
|
|
|
|
delete_view = views.AdminView(
|
|
|
|
|
r"^(?P<pk>[0-9]+)/delete/$", views.ModelDeleteView, name="delete"
|
|
|
|
|
)
|
|
|
|
|
history_view = views.AdminView(
|
|
|
|
|
r"^(?P<pk>[0-9]+)/history/$", views.ModelHistoryView, name="history"
|
|
|
|
|
)
|
2013-06-21 05:41:28 +00:00
|
|
|
views = []
|
2013-05-30 10:33:00 +00:00
|
|
|
|
|
|
|
|
# API configuration
|
|
|
|
|
api_serializer_class = None
|
|
|
|
|
|
|
|
|
|
# API Views
|
|
|
|
|
api_list_view = apiviews.ListCreateAPIView
|
|
|
|
|
api_detail_view = apiviews.RetrieveUpdateDestroyAPIView
|
|
|
|
|
|
|
|
|
|
def __init__(self, model, admin, name=None, **kwargs):
|
|
|
|
|
self.name = name
|
|
|
|
|
self.model = model
|
|
|
|
|
self.admin = admin
|
|
|
|
|
model_options = utils.model_options(model)
|
|
|
|
|
self.app_label = model_options.app_label
|
|
|
|
|
self.model_name = model_options.object_name.lower()
|
|
|
|
|
|
|
|
|
|
if self.name is None:
|
2021-03-09 12:14:40 +00:00
|
|
|
self.name = "{}_{}".format(self.app_label, self.model_name)
|
2013-05-30 10:33:00 +00:00
|
|
|
|
|
|
|
|
if self.verbose_name is None:
|
|
|
|
|
self.verbose_name = model_options.verbose_name
|
|
|
|
|
if self.verbose_name_plural is None:
|
|
|
|
|
self.verbose_name_plural = model_options.verbose_name_plural
|
|
|
|
|
|
|
|
|
|
def get_default_view_kwargs(self):
|
|
|
|
|
return {
|
2021-03-09 12:14:40 +00:00
|
|
|
"app_label": self.app_label,
|
|
|
|
|
"model": self.model,
|
|
|
|
|
"model_name": self.model_name,
|
|
|
|
|
"model_admin": immutable_admin_factory(self),
|
2013-05-30 10:33:00 +00:00
|
|
|
}
|
|
|
|
|
|
2013-07-28 02:26:08 +00:00
|
|
|
def get_index_kwargs(self):
|
|
|
|
|
kwargs = self.get_default_view_kwargs()
|
2021-03-09 12:14:40 +00:00
|
|
|
kwargs.update(
|
|
|
|
|
{
|
|
|
|
|
"paginate_by": self.list_per_page,
|
|
|
|
|
}
|
|
|
|
|
)
|
2013-07-28 02:26:08 +00:00
|
|
|
return kwargs
|
|
|
|
|
|
2013-05-30 10:33:00 +00:00
|
|
|
def get_default_api_view_kwargs(self):
|
|
|
|
|
kwargs = self.get_default_view_kwargs()
|
2021-03-09 12:14:40 +00:00
|
|
|
kwargs.update(
|
|
|
|
|
{
|
|
|
|
|
"serializer_class": self.api_serializer_class,
|
|
|
|
|
}
|
|
|
|
|
)
|
2013-05-30 10:33:00 +00:00
|
|
|
return kwargs
|
|
|
|
|
|
|
|
|
|
def get_prefixed_view_name(self, view_name):
|
2021-03-09 12:14:40 +00:00
|
|
|
return "{}_{}".format(self.name, view_name)
|
2013-05-30 10:33:00 +00:00
|
|
|
|
|
|
|
|
def get_create_kwargs(self):
|
|
|
|
|
kwargs = self.get_default_view_kwargs()
|
2021-03-09 12:14:40 +00:00
|
|
|
kwargs.update(
|
|
|
|
|
{
|
|
|
|
|
"inlines": self.inlines,
|
|
|
|
|
"form_class": (
|
|
|
|
|
self.create_form_class
|
|
|
|
|
if self.create_form_class
|
|
|
|
|
else self.form_class
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
)
|
2013-05-30 10:33:00 +00:00
|
|
|
return kwargs
|
|
|
|
|
|
|
|
|
|
def get_update_kwargs(self):
|
|
|
|
|
kwargs = self.get_default_view_kwargs()
|
2021-03-09 12:14:40 +00:00
|
|
|
form_class = (
|
|
|
|
|
self.update_form_class if self.update_form_class else self.form_class
|
|
|
|
|
)
|
2013-05-30 10:33:00 +00:00
|
|
|
if form_class is None:
|
2021-03-09 12:14:40 +00:00
|
|
|
form_class = modelform_factory(self.model, fields="__all__")
|
|
|
|
|
kwargs.update(
|
|
|
|
|
{
|
|
|
|
|
"inlines": self.inlines,
|
|
|
|
|
"form_class": form_class,
|
|
|
|
|
}
|
|
|
|
|
)
|
2013-05-30 10:33:00 +00:00
|
|
|
return kwargs
|
|
|
|
|
|
|
|
|
|
def get_index_url(self):
|
2021-03-09 12:14:40 +00:00
|
|
|
return reverse("admin2:{}".format(self.get_prefixed_view_name("index")))
|
2013-05-30 10:33:00 +00:00
|
|
|
|
|
|
|
|
def get_api_list_kwargs(self):
|
|
|
|
|
kwargs = self.get_default_api_view_kwargs()
|
2021-03-09 12:14:40 +00:00
|
|
|
kwargs.update(
|
|
|
|
|
{
|
|
|
|
|
"queryset": self.model.objects.all(),
|
|
|
|
|
# 'paginate_by': self.list_per_page,
|
|
|
|
|
}
|
|
|
|
|
)
|
2013-05-30 10:33:00 +00:00
|
|
|
return kwargs
|
|
|
|
|
|
|
|
|
|
def get_api_detail_kwargs(self):
|
|
|
|
|
return self.get_default_api_view_kwargs()
|
|
|
|
|
|
|
|
|
|
def get_urls(self):
|
2013-06-23 05:52:58 +00:00
|
|
|
pattern_list = []
|
2013-11-28 15:50:41 +00:00
|
|
|
for admin_view in self.views:
|
|
|
|
|
admin_view.model_admin = self
|
|
|
|
|
get_kwargs = getattr(self, "get_%s_kwargs" % admin_view.name, None)
|
2013-06-23 05:52:58 +00:00
|
|
|
if not get_kwargs:
|
2013-11-28 15:50:41 +00:00
|
|
|
get_kwargs = admin_view.get_view_kwargs
|
|
|
|
|
try:
|
|
|
|
|
view_instance = admin_view.view.as_view(**get_kwargs())
|
|
|
|
|
except Exception as e:
|
|
|
|
|
trace = sys.exc_info()[2]
|
|
|
|
|
new_exception = TypeError(
|
|
|
|
|
'Cannot instantiate admin view "{}.{}". '
|
2021-03-09 12:14:40 +00:00
|
|
|
"The error that got raised was: {}".format(
|
|
|
|
|
self.__class__.__name__, admin_view.name, e
|
|
|
|
|
)
|
|
|
|
|
)
|
2015-03-14 23:17:10 +00:00
|
|
|
try:
|
|
|
|
|
raise new_exception.with_traceback(trace)
|
|
|
|
|
except AttributeError:
|
|
|
|
|
raise (new_exception, None, trace)
|
|
|
|
|
|
2013-06-23 05:52:58 +00:00
|
|
|
pattern_list.append(
|
2021-03-09 12:14:40 +00:00
|
|
|
re_path(
|
|
|
|
|
admin_view.url,
|
2013-11-28 15:50:41 +00:00
|
|
|
view=view_instance,
|
2021-03-09 12:14:40 +00:00
|
|
|
name=self.get_prefixed_view_name(admin_view.name),
|
2013-06-23 05:52:58 +00:00
|
|
|
)
|
|
|
|
|
)
|
2016-05-22 12:38:41 +00:00
|
|
|
return pattern_list
|
2013-05-30 10:33:00 +00:00
|
|
|
|
|
|
|
|
def get_api_urls(self):
|
2016-05-22 12:38:41 +00:00
|
|
|
return [
|
2021-03-09 12:14:40 +00:00
|
|
|
re_path(
|
|
|
|
|
r"^$",
|
2013-05-30 10:33:00 +00:00
|
|
|
view=self.api_list_view.as_view(**self.get_api_list_kwargs()),
|
2021-03-09 12:14:40 +00:00
|
|
|
name=self.get_prefixed_view_name("api_list"),
|
2013-05-30 10:33:00 +00:00
|
|
|
),
|
2021-03-09 12:14:40 +00:00
|
|
|
re_path(
|
|
|
|
|
r"^(?P<pk>[0-9]+)/$",
|
|
|
|
|
view=self.api_detail_view.as_view(**self.get_api_detail_kwargs()),
|
|
|
|
|
name=self.get_prefixed_view_name("api_detail"),
|
2013-05-30 10:33:00 +00:00
|
|
|
),
|
2016-05-22 12:38:41 +00:00
|
|
|
]
|
2013-05-30 10:33:00 +00:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def urls(self):
|
|
|
|
|
# We set the application and instance namespace here
|
|
|
|
|
return self.get_urls(), None, None
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def api_urls(self):
|
|
|
|
|
return self.get_api_urls(), None, None
|
|
|
|
|
|
2013-05-31 13:37:55 +00:00
|
|
|
def get_list_actions(self):
|
2013-05-30 10:33:00 +00:00
|
|
|
actions_dict = {}
|
|
|
|
|
|
|
|
|
|
for cls in type(self).mro()[::-1]:
|
2021-03-09 12:14:40 +00:00
|
|
|
class_actions = getattr(cls, "list_actions", [])
|
2013-05-30 10:33:00 +00:00
|
|
|
for action in class_actions:
|
|
|
|
|
actions_dict[action.__name__] = {
|
2021-03-09 12:14:40 +00:00
|
|
|
"name": action.__name__,
|
|
|
|
|
"description": actions.get_description(action),
|
|
|
|
|
"action_callable": action,
|
2013-05-30 10:33:00 +00:00
|
|
|
}
|
|
|
|
|
return actions_dict
|
|
|
|
|
|
2013-08-01 16:00:03 +00:00
|
|
|
def get_ordering(self, request):
|
|
|
|
|
return self.ordering
|
|
|
|
|
|
2013-05-30 10:33:00 +00:00
|
|
|
|
2018-11-02 13:31:16 +00:00
|
|
|
class Admin2Inline(extra_views.InlineFormSetFactory):
|
2013-05-30 10:33:00 +00:00
|
|
|
"""
|
|
|
|
|
A simple extension of django-extra-view's InlineFormSet that
|
|
|
|
|
adds some useful functionality.
|
|
|
|
|
"""
|
2021-03-09 12:14:40 +00:00
|
|
|
|
2013-07-18 13:02:07 +00:00
|
|
|
template = None
|
2021-03-09 12:14:40 +00:00
|
|
|
fields = "__all__"
|
2013-05-30 10:33:00 +00:00
|
|
|
|
|
|
|
|
def construct_formset(self):
|
|
|
|
|
"""
|
|
|
|
|
Overrides construct_formset to attach the model class as
|
|
|
|
|
an attribute of the returned formset instance.
|
|
|
|
|
"""
|
2021-10-17 08:51:27 +00:00
|
|
|
formset = super().construct_formset()
|
2013-05-30 10:33:00 +00:00
|
|
|
formset.model = self.inline_model
|
2013-07-18 13:02:07 +00:00
|
|
|
formset.template = self.template
|
2013-05-30 10:33:00 +00:00
|
|
|
return formset
|
|
|
|
|
|
|
|
|
|
|
2013-07-18 13:02:07 +00:00
|
|
|
class Admin2TabularInline(Admin2Inline):
|
2013-07-18 23:30:40 +00:00
|
|
|
template = os.path.join(
|
2021-03-09 12:14:40 +00:00
|
|
|
settings.ADMIN2_THEME_DIRECTORY, "edit_inlines/tabular.html"
|
|
|
|
|
)
|
2013-05-30 10:11:49 +00:00
|
|
|
|
2013-07-18 13:02:07 +00:00
|
|
|
|
|
|
|
|
class Admin2StackedInline(Admin2Inline):
|
2013-07-18 23:30:40 +00:00
|
|
|
template = os.path.join(
|
2021-03-09 12:14:40 +00:00
|
|
|
settings.ADMIN2_THEME_DIRECTORY, "edit_inlines/stacked.html"
|
|
|
|
|
)
|
2013-07-18 13:02:07 +00:00
|
|
|
|
|
|
|
|
|
2013-05-30 10:11:49 +00:00
|
|
|
def immutable_admin_factory(model_admin):
|
2013-07-18 23:30:40 +00:00
|
|
|
"""
|
|
|
|
|
Provide an ImmutableAdmin to make it harder for developers to
|
|
|
|
|
dig themselves into holes.
|
|
|
|
|
See https://github.com/twoscoops/django-admin2/issues/99
|
|
|
|
|
Frozen class implementation as namedtuple suggested by Audrey Roy
|
|
|
|
|
|
|
|
|
|
Note: This won't stop developers from saving mutable objects to
|
|
|
|
|
the result, but hopefully developers attempting that
|
|
|
|
|
'workaround/hack' will read our documentation.
|
2013-05-30 10:11:49 +00:00
|
|
|
"""
|
2021-03-09 12:14:40 +00:00
|
|
|
ImmutableAdmin = namedtuple("ImmutableAdmin", model_admin.model_admin_attributes)
|
|
|
|
|
return ImmutableAdmin(
|
|
|
|
|
*[getattr(model_admin, x) for x in model_admin.model_admin_attributes]
|
|
|
|
|
)
|