django-admin2/djadmin2/types.py

345 lines
10 KiB
Python
Raw Permalink Normal View History

import logging
import os
import sys
2016-05-07 23:31:16 +00:00
from collections import namedtuple
2016-05-07 23:31:16 +00:00
import extra_views
from django.forms import modelform_factory
2021-03-09 12:14:40 +00:00
from django.urls import re_path, reverse
2016-05-07 23:31:16 +00:00
from . import actions
from . import apiviews
2013-06-03 20:44:16 +00:00
from . import settings
from . import utils
2016-05-07 23:31:16 +00:00
from . import views
2021-03-09 12:14:40 +00:00
logger = logging.getLogger("djadmin2")
2013-06-21 05:41:28 +00:00
class ModelAdminBase2(type):
def __new__(cls, name, bases, attrs):
new_class = super().__new__(cls, name, bases, attrs)
view_list = []
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", []))
new_class.views = view_list
2013-06-21 13:16:08 +00:00
return new_class
2013-06-21 05:41:28 +00:00
class ModelAdmin2(metaclass=ModelAdminBase2):
"""
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.
"""
2021-03-09 12:14:40 +00:00
actions_selection_counter = True
date_hierarchy = False
2021-03-09 12:14:40 +00:00
list_display = ("__str__",)
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
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
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
list_actions = [actions.DeleteSelectedAction]
# 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
field_actions = {}
2013-07-07 09:47:19 +00:00
# Defines custom field renderers
field_renderers = {}
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 = []
# 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)
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),
}
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,
}
)
return kwargs
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,
}
)
return kwargs
def get_prefixed_view_name(self, view_name):
2021-03-09 12:14:40 +00:00
return "{}_{}".format(self.name, view_name)
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
),
}
)
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
)
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,
}
)
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")))
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,
}
)
return kwargs
def get_api_detail_kwargs(self):
return self.get_default_api_view_kwargs()
def get_urls(self):
pattern_list = []
for admin_view in self.views:
admin_view.model_admin = self
get_kwargs = getattr(self, "get_%s_kwargs" % admin_view.name, None)
if not get_kwargs:
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
)
)
try:
raise new_exception.with_traceback(trace)
except AttributeError:
raise (new_exception, None, trace)
pattern_list.append(
2021-03-09 12:14:40 +00:00
re_path(
admin_view.url,
view=view_instance,
2021-03-09 12:14:40 +00:00
name=self.get_prefixed_view_name(admin_view.name),
)
)
return pattern_list
def get_api_urls(self):
return [
2021-03-09 12:14:40 +00:00
re_path(
r"^$",
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"),
),
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"),
),
]
@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
def get_list_actions(self):
actions_dict = {}
for cls in type(self).mro()[::-1]:
2021-03-09 12:14:40 +00:00
class_actions = getattr(cls, "list_actions", [])
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,
}
return actions_dict
def get_ordering(self, request):
return self.ordering
class Admin2Inline(extra_views.InlineFormSetFactory):
"""
A simple extension of django-extra-view's InlineFormSet that
adds some useful functionality.
"""
2021-03-09 12:14:40 +00:00
template = None
2021-03-09 12:14:40 +00:00
fields = "__all__"
def construct_formset(self):
"""
Overrides construct_formset to attach the model class as
an attribute of the returned formset instance.
"""
formset = super().construct_formset()
formset.model = self.inline_model
formset.template = self.template
return formset
class Admin2TabularInline(Admin2Inline):
template = os.path.join(
2021-03-09 12:14:40 +00:00
settings.ADMIN2_THEME_DIRECTORY, "edit_inlines/tabular.html"
)
class Admin2StackedInline(Admin2Inline):
template = os.path.join(
2021-03-09 12:14:40 +00:00
settings.ADMIN2_THEME_DIRECTORY, "edit_inlines/stacked.html"
)
def immutable_admin_factory(model_admin):
"""
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.
"""
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]
)