# Imports
import collections
from django import forms
from django.conf import settings
from django.core.exceptions import FieldDoesNotExist
from django.db import models
from functools import partial
import inspect
import logging
from superdjango.db.history.utils import get_field_changes
from superdjango.db.utils import Behavior
from superdjango.exceptions import ModelAlreadyRegistered, ViewAlreadyExists
from superdjango.forms import RequestEnabledModelForm
from superdjango.html.library import Breadcrumbs
from superdjango.patterns import ModelPattern
from superdjango.sessions import Session
from superdjango.shortcuts import title
from superdjango.ui import views
from urllib.parse import quote_plus
from ..constants import VERB
from . import actions
from . import controls
from . import filters
from .policies import AccessPolicy, PermissionPolicy
from .utils import Default, Dropdown, FieldGroup
from .views.ajax import AjaxMarkCompleteOptions
from .views.base import BaseOptions, TemplateOptions
from .views.crud import DeleteOptions
from .views.extended import RedirectOptions
log = logging.getLogger(__name__)
# Exports
__all__ = (
"BaseModelUI",
"InlineModelUI",
"ModelUI",
"ModelViewMixin",
"SiteUI",
"site",
)
# Base
[docs]class BaseModelUI(object):
"""Base class for model user interfaces. This class provides the commonality between InlineModelUI and ModelUI.
Child classes are expected to:
- Set ``self.meta`` to the model's ``_meta`` attribute as part of ``__init__``.
- Call ``_init_controls()`` during ``__init__``.
- Initialize a default PermissionPolicy if one is not provided.
Example: ``self.permission_policy = PermissionPolicy(self.meta.app_label, self.model)``
"""
actions = dict()
bulk_actions = dict()
controls = dict()
filters = dict()
help = None
icon = None
logging_enabled = not settings.DEBUG
lookup_field = None
lookup_key = None
menu_enabled = True
model = None
permission_policy = None
preserve_verbs = [
VERB.CREATE,
VERB.DELETE,
VERB.UPDATE,
]
"""A list of verb names that should preserve filters (and other GET parameters) for the ``next_url`` value."""
raise_exception = settings.DEBUG
sort_order = None
[docs] def __init__(self, *args, **kwargs):
"""Initialize the UI."""
self.behaviors = Behavior(self.model)
# noinspection PyProtectedMember
self.meta = self.model._meta
self._actions = dict()
self._bulk_actions = dict()
self._controls = dict()
def __repr__(self):
# noinspection PyUnresolvedReferences
return "<%s %s>" % (self.__class__.__name__, self.meta.model_name)
# noinspection PyUnusedLocal
[docs] def check_lock(self, request, verb, field=None, record=None):
"""Check whether a record is locked.
:param request: The current request instance.
:param verb: The verb in question. This method looks only at verbs that delete or update a record.
:type verb: str
:param field: The field name, if any. Not used by default.
:type field: str
:param record: The model instance. Note that ``record`` is an optional parameter (for the sake of runtime
operation), it is required to check for locking. It is, however, safe to pass record as ``None``.
:rtype: bool | None
:returns: ``True`` if the user has permission on a locked record, or ``False`` if permission should be denied.
``None`` indicates "no opinion"; the model doesn't support locking or the conditions for granting or
denying permission have not been met. See the ``check_permission()`` method.
.. tip::
Override this method if you have implemented custom permissions which grant delete or update on locked
records to authorized users.
"""
if not self.behaviors.is_locked_model():
return None
if record is None:
return None
conditions = [
record.is_locked,
verb in (VERB.AJAX_DELETE, VERB.AJAX_UPDATE, VERB.DELETE, VERB.UPDATE),
]
if all(conditions):
if request.user.is_superuser:
return True
else:
return False
return None
[docs] def check_permission(self, request, verb, field=None, record=None):
"""Use the UI ``permission_policy`` to verify that user has permission on a view.
:param request: The current request instance.
:param verb: The verb in question.
:type verb: str
:param field: The field name, if any.
:type field: str
:param record: The model instance, if any.
:rtype: bool
"""
# The permission policy works with defined permissions -- built-in or custom -- but cannot account for locked
# records. Check here if the model supports locking.
result = self.check_lock(request, verb, field=field, record=record)
if result is not None:
return result
return self.permission_policy.check(request, verb, field=field, record=record)
# noinspection PyUnusedLocal
[docs] def get_control(self, name, record=None, request=None):
"""Get the control instance for the named control.
:param name: The name of the field.
:type name: str
:param record: The model instance. Not used in the default implementation.
:param request: The current HTTP request instance.
:rtype: BaseType[BaseControl] | None
"""
# It's an actual field.
if name in self._controls:
return self._controls[name]
# Controls have only been initialized for actual fields. However, the specified name may be a property or
# callable. So we initialize the controls as needed.
if not hasattr(self.model, name):
return None
label = title(name.replace("_", " "))
control = controls.CallableControl(field_name=name, label=label)
self.controls[name] = control
return control
# noinspection PyMethodMayBeStatic
[docs] def get_display_value(self, record, as_choice=False, label_field=None):
"""Get the human-friendly representation of a record.
:param record: The record (model instance).
:param as_choice: Indicates whether the record should be represented specifically as a lookup choice
(if supported).
:type as_choice: bool
:param label_field: The field on the model that may be used to represent the record.
:type label_field: str
:rtype: str
This resolves the display value in the following order:
1. If ``label_field`` exists, the value for the record is returned.
2. When ``as_choice`` is ``True`` and ``get_choice_name()`` exists on the model, this value is returned.
3. The value of ``get_display_name()`` if this method exists on the model.
4. The ``str(record)`` representation of the model instance.
"""
# TODO: Rename get_display_value to get_display_name()?
if label_field is not None:
try:
return getattr(record, label_field)
except AttributeError:
pass
if as_choice:
try:
return record.get_choice_name()
except AttributeError:
pass
try:
return record.get_display_name()
except AttributeError:
pass
return str(record)
[docs] def get_dotted_path(self):
"""Get the "dotted" path to the model.
:rtype: str
"""
# noinspection PyUnresolvedReferences
return "%s.%s" % (self.meta.app_label, self.meta.model_name)
# noinspection PyUnusedLocal,PyMethodMayBeStatic
[docs] def get_field_queryset(self, model_field, request, criteria=None):
"""Get the queryset for a remote (foreign key, many to many, one to one) field.
:param model_field: The field instance on the current model that refers to a remote field.
:param request: The current request instance.
:param criteria: Additional criteria to be used when obtaining the queryset.
:type criteria: dict
:rtype: django.db.models.QuerySet
"""
if criteria is not None:
# noinspection PyProtectedMember
return model_field.remote_field.model._default_manager.filter(**criteria)
# noinspection PyProtectedMember
return model_field.remote_field.model._default_manager.all()
# noinspection PyUnusedLocal
# noinspection PyUnusedLocal
# def get_form_field_old(self, model_field, request, form_options=None, record=None, **kwargs):
# """Get the field instance for a given field on the model.
#
# :param model_field: The model field instance.
#
# :param request: The current HTTP request instance.
#
# :param form_options: The form options in use.
#
# :param record: The current model instance.
#
# :returns: A field instance.
#
# ``kwargs`` are updated and passed to the ``formfield()`` method of the ``model_field``.
#
# """
# control = self._controls[model_field.name]
#
# widget_attrs = control.get_widget_attributes(request, record=record)
#
# if control.type == "country":
# kwargs['form_class'] = forms.ChoiceField
# kwargs['choices'] = control.choices
# if 'max_length' in kwargs:
# del(kwargs['max_length'])
# if control.type == "datetime":
# kwargs['form_class'] = forms.SplitDateTimeField
# kwargs['input_date_formats'] = [control.date_format]
# kwargs['input_time_formats'] = [control.time_format]
# elif control.type == "foreignkey":
# criteria = self.get_limit_choices_to(control, request, record=record)
# kwargs['queryset'] = self.get_field_queryset(model_field, request, criteria=criteria)
# elif control.type == "manytomany":
# criteria = self.get_limit_choices_to(control, request, record=record)
# kwargs['queryset'] = self.get_field_queryset(model_field, request, criteria=criteria)
# elif control.type == "time":
# kwargs['input_formats'] = [control.time_format]
# else:
# pass
#
# if control.help_text != model_field.help_text:
# kwargs['help_text'] = control.help_text
#
# if control.initial is not None:
# if isinstance(control.initial, Default):
# kwargs['initial'] = control.initial.get(request, record=record)
# else:
# kwargs['initial'] = control.initial
#
# if control.label != model_field.verbose_name:
# kwargs['label'] = control.label
#
# if control.widget is not None:
# kwargs['widget'] = control.widget
#
# form_field = model_field.formfield(**kwargs)
#
# if control.input_prefix is not None:
# form_field.input_prefix = control.input_prefix
#
# if control.input_suffix is not None:
# form_field.input_suffix = control.input_suffix
#
# if widget_attrs:
# form_field.widget.attrs.update(widget_attrs)
#
# return form_field
# noinspection PyUnusedLocal
[docs] def get_help(self, request):
"""Get help information.
:param request: The current request instance.
"""
return self.help
[docs] def get_identifier(self, record):
"""Get the value of the ``lookup_field`` (the record identifier) for a given record.
:param record: The model instance.
:rtype: int | str
"""
lookup_field = self.get_lookup_field()
return getattr(record, lookup_field)
[docs] def get_limit_choices_to(self, control, request, record=None):
"""Get the criteria for limiting choices of a foreign key or many to many field.
:param control: The control upon which ``limit_choices_to`` is defined.
:type control: ForeignKeyControl | ManyToManyControl | OneToOneControl
:param request: The current HTTP request instance.
:param record: The current model instance.
:rtype: dict | None
"""
if control.limit_choices_to is None:
return None
if type(control.limit_choices_to) is dict:
return control.limit_choices_to
if callable(control.limit_choices_to):
return control.limit_choices_to(request, record=record)
if self.logging_enabled:
log.warning("limit_choices_to given for %s control, but is not a dict or callable." % control.name)
return None
[docs] def get_lookup_field(self):
"""Get the name of the field used to uniquely identify a record.
:rtype: str
.. note::
By default, ``pk`` is returned if the ``lookup_field`` is not set. See :py:class:`ModelUI` for how this is
overridden and improved.
"""
return self.lookup_field or "pk"
[docs] def get_lookup_key(self):
"""Get the key used in GET to uniquely identify a model instance.
:rtype: str
"""
if self.lookup_key is not None:
return self.lookup_key
return self.get_lookup_field()
[docs] def get_remote_model(self, field, dotted=False):
"""Get the remote model for the given reference field.
:param field: The name or instance of the foreign key, many to many, or one to one field that exists on the UI's
model.
:type field: str | BaseType[models.Field]
:param dotted: Return the dotted path rather the model.
:type dotted: bool
:rtype: str | Model | None
"""
if isinstance(field, models.Field):
remote = self.get_control(field.name).remote_field
else:
remote = self.get_control(field).remote_field
if not remote:
return None
if dotted:
# noinspection PyProtectedMember
return "%s.%s" % (remote.model._meta.app_label, remote.model._meta.model_name)
return remote.model
[docs] def get_verbose_name(self):
"""Get the singular verbose name of the inline model.
:rtype: str
"""
# noinspection PyUnresolvedReferences
return self.meta.verbose_name
[docs] def get_verbose_name_plural(self):
"""Get the plural verbose name of the inline model.
:rtype: str
"""
# noinspection PyUnresolvedReferences
return self.meta.verbose_name_plural
[docs] def is_audit_model(self):
"""Indicates whether the model has an audit method.
:rtype: bool
"""
if hasattr(self.model, "audit") and callable(self.model.audit):
return True
return False
[docs] def is_locked_model(self):
"""Indicates whether the model supports record locking.
:rtype: bool
"""
if hasattr(self.model, "lock_record") and callable(self.model.lock_record):
return True
return False
[docs] def is_owned_model(self):
"""Indicates whether record ownership is defined for the model.
:rtype: bool
"""
if hasattr(self.model, "set_record_owner") and callable(self.model.set_record_owner):
return True
return False
[docs] def is_polymorphic_model(self):
"""Indicates whether the model implements polymorphic behaviors.
:rtype: bool
"""
try:
return self.model.is_polymorphic_model()
except AttributeError:
return False
[docs] def is_published_model(self):
"""Indicates whether the model supports publish-type workflows.
:rtype: bool
"""
if hasattr(self.model, "mark_published") and callable(self.model.mark_published):
return True
return False
[docs] def is_reviewed_model(self):
"""Indicates whether the model supports review-type workflows.
:rtype: bool
"""
if hasattr(self.model, "mark_reviewed") and callable(self.model.mark_reviewed):
return True
return False
[docs] def is_resolved_model(self):
"""Indicates whether the model supports resolution workflows.
:rtype: bool
"""
if hasattr(self.model, "mark_resolved") and callable(self.model.mark_resolved):
return True
return False
[docs] def is_sort_model(self):
"""Indicates whether the model is sortable.
:rtype: bool
"""
try:
return self.model.is_sort_model()
except AttributeError:
pass
return self._field_exists("sort_order")
[docs] def is_viewed_model(self):
"""Indicates whether the model supports viewed by functionality.
:rtype: bool
"""
if hasattr(self.model, "mark_viewed") and callable(self.model.mark_viewed):
return True
return False
@property
def verbose_name(self):
"""Alias for ``get_verbose_name()``."""
return self.get_verbose_name()
@property
def verbose_name_plural(self):
"""Alias for ``get_verbose_name_plural()``."""
return self.get_verbose_name_plural()
def _field_exists(self, field_name, unique=False):
"""Determine if the given field name is defined on the model.
:param field_name: The name of the field to check.
:type field_name: str
:param unique: Also test whether the field is unique.
:type unique: bool
:rtype: bool
"""
try:
# noinspection PyUnresolvedReferences
field = self.meta.get_field(field_name)
if unique:
return field.unique
return True
except FieldDoesNotExist:
return False
def _field_has_value(self, field_name, record):
"""Indicates whether a field has a not ``None`` value on the given record.
:param field_name: The field name to check.
:type field_name: str
:param record: The record (model instance).
:rtype: bool
"""
if not self._field_exists(field_name):
return False
value = getattr(record, field_name)
return value is not None
def _init_actions(self):
"""Initialize all available actions."""
self._actions = {
"divider": actions.DividerAction(),
VERB.AJAX_CREATE: actions.AjaxCreateAction(),
VERB.AJAX_DELETE: actions.AjaxDeleteAction(),
VERB.AJAX_DETAIL: actions.AjaxDetailAction(),
VERB.AJAX_MARK_COMPLETE: actions.AjaxMarkCompleteAction(),
VERB.AJAX_UPDATE: actions.AjaxUpdateAction(),
VERB.CREATE: actions.CreateAction(),
VERB.DASHBOARD: actions.DashboardAction(),
VERB.DELETE: actions.DeleteAction(),
VERB.DETAIL: actions.DetailAction(),
VERB.DUPLICATE: actions.DuplicateAction(),
VERB.LIST: actions.ListAction(),
VERB.MARK_ARCHIVED: actions.MarkArchivedAction(),
VERB.MARK_COMPLETE: actions.MarkCompleteAction(),
VERB.MARK_PUBLISHED: actions.MarkPublishedAction(),
VERB.MARK_REVIEWED: actions.MarkReviewedAction(),
VERB.MARK_RESOLVED: actions.MarkResolvedAction(),
VERB.SEARCH: actions.SearchAction(),
VERB.TRASH: actions.TrashAction(),
VERB.UPDATE: actions.UpdateAction(),
}
self._actions.update(self.actions.copy())
# self._actions = self.actions.copy()
#
# default_actions = {
# "divider": actions.DividerAction(),
# VERB.AJAX_CREATE: actions.AjaxCreateAction(),
# VERB.AJAX_DELETE: actions.AjaxDeleteAction(),
# VERB.AJAX_DETAIL: actions.AjaxDetailAction(),
# VERB.AJAX_MARK_COMPLETE: actions.AjaxMarkCompleteAction(),
# VERB.AJAX_UPDATE: actions.AjaxUpdateAction(),
# VERB.CREATE: actions.CreateAction(),
# VERB.DASHBOARD: actions.DashboardAction(),
# VERB.DELETE: actions.DeleteAction(),
# VERB.DETAIL: actions.DetailAction(),
# VERB.DUPLICATE: actions.DuplicateAction(),
# VERB.LIST: actions.ListAction(),
# VERB.MARK_ARCHIVED: actions.MarkArchivedAction(),
# VERB.MARK_COMPLETE: actions.MarkCompleteAction(),
# VERB.MARK_PUBLISHED: actions.MarkPublishedAction(),
# VERB.MARK_REVIEWED: actions.MarkReviewedAction(),
# VERB.MARK_RESOLVED: actions.MarkResolvedAction(),
# VERB.SEARCH: actions.SearchAction(),
# VERB.TRASH: actions.TrashAction(),
# VERB.UPDATE: actions.UpdateAction(),
# }
#
# self._actions.update(default_actions)
def _init_bulk_actions(self):
"""Initialize all available bulk actions."""
self._bulk_actions = {
"divider": actions.DividerAction(),
VERB.BATCH_CHANGE: actions.BatchChangeAction(),
VERB.BULK_COMPARE: actions.BulkCompareAction(),
VERB.BULK_DELETE: actions.BulkDeleteAction(),
VERB.BULK_EDIT: actions.BulkEditAction(),
VERB.BULK_TRASH: actions.BaseBulkAction(),
}
self._bulk_actions.update(self.bulk_actions.copy())
# self._bulk_actions = self.bulk_actions.copy()
#
# default_actions = {
# "divider": actions.DividerAction(),
# VERB.BATCH_CHANGE: actions.BatchChangeAction(),
# VERB.BULK_COMPARE: actions.BulkCompareAction(),
# VERB.BULK_DELETE: actions.BulkDeleteAction(),
# VERB.BULK_EDIT: actions.BulkEditAction(),
# VERB.BULK_TRASH: actions.BaseBulkAction(),
# }
#
# self._bulk_actions.update(default_actions)
def _init_controls(self):
"""Initialize control instances for all fields on the model.
.. note::
This updates the ``controls`` attribute.
"""
# Get the fields attached to the model.
# noinspection PyUnresolvedReferences
fields = self.meta.get_fields()
# If a control has been defined by the developer implementing the UI, we just need to add the field name since
# this is optional when instantiating control instance, but defined as part of the controls dictionary. Do this
# first because problems arise if the control factory is invoked in the same loop.
for field_instance in fields:
if field_instance.name in self.controls:
control = self.controls[field_instance.name]
control.name = field_instance.name
control.ui = self
if control.help_text is None:
control.help_text = field_instance.help_text
if control._label is None:
control._label = title(field_instance.verbose_name) or title(field_instance.name.replace("_", " "))
if field_instance.max_length:
control.max_length = field_instance.max_length
if control.on_select is not None:
if isinstance(control.on_select, Dropdown):
control.on_select.control_field = field_instance.name
elif type(control.on_select) in (list, tuple):
on_select_list = control.on_select
control.on_select = Dropdown(*on_select_list, control_field=field_instance.name)
else:
raise TypeError("on_select for %s control must be an instance of Dropdown or list of "
"OnSelect instances." % field_instance.name)
if control.remote_field is None:
control.remote_field = field_instance.remote_field
try:
if control.toggle is not None:
for toggle in control.toggle:
toggle.control_field = field_instance.name
except AttributeError:
pass
self._controls[field_instance.name] = control
# Now initialize controls for any remaining fields.
for field_instance in fields:
if field_instance.name in self._controls:
continue
control = controls.factory(field_instance, self)
if control is not None:
self._controls[field_instance.name] = control
# Handle specialist controls.
for name, control in self.controls.items():
if control.is_special:
if control._label is None:
control._label = title(name.replace("_", " "))
control.name = name
self._controls[name] = control
[docs]class ModelViewMixin(object):
"""A mixin which defines view classes, options, and related methods.
.. note::
The extending class must define the ``model`` and implement the ``get_form_field()`` method.
"""
# General settings.
logging_enabled = not settings.DEBUG
lookup_field = None
lookup_key = None
namespace = None
# View classes.
ajax_auto_complete_class = views.UIAjaxAutoCompleteView
ajax_chained_lookup_class = views.UIAjaxChainedLookupView
ajax_chooser_class = views.UIAjaxChooserView
ajax_create_class = views.UIAjaxCreateView
ajax_detail_class = views.UIAjaxDetailView
ajax_drag_and_drop_class = views.UIAjaxDragAndDropView
ajax_list_class = views.UIAjaxListView
ajax_mark_complete_class = views.UIAjaxMarkCompleteView
ajax_update_class = views.UIAjaxUpdateView
ajax_reorder_class = views.UIAjaxReorderView
ajax_search_class = views.UIAjaxSearchView
batch_change_class = views.UIBatchChangeView
bulk_compare_class = views.UIBulkCompareView
bulk_delete_class = views.UIBulkDeleteView
bulk_edit_class = views.UIBulkEditView
chooser_class = views.UIChooserView
create_class = views.UICreateView
dashboard_class = views.UIDashboardView
delete_class = views.UIDeleteView
detail_class = views.UIDetailView
duplicate_class = views.UIDuplicateView
history_class = views.UIHistoryView
intermediate_choice_class = views.UIIntermediateChoiceView
intermediate_form_class = views.UIIntermediateFormView
list_class = views.UIListView
mark_archived_class = views.UIMarkArchivedRedirect
mark_complete_class = views.UIMarkCompleteRedirect
mark_published_class = views.UIMarkPublishedRedirect
mark_reviewed_class = views.UIMarkReviewedRedirect
mark_resolved_class = views.UIMarkResolvedRedirect
save_as_class = views.UISaveAsView
search_class = views.UISearchView
update_class = views.UIUpdateView
# View options.
ajax_auto_complete_options = None
ajax_chained_lookup_options = None
ajax_chooser_options = None
ajax_create_options = None
ajax_detail_options = None
ajax_drag_and_drop_options = None
ajax_list_options = None
ajax_mark_complete_options = None
ajax_update_options = None
ajax_reorder_options = None
ajax_search_options = None
batch_change_options = None
bulk_compare_options = None
bulk_delete_options = None
bulk_edit_options = None
chooser_options = None
create_options = None
dashboard_options = None
delete_options = None
detail_options = None
duplicate_options = None
form_options = None
history_options = None
intermediate_choice_options = None
intermediate_form_options = None
list_options = None
mark_archived_options = None
mark_complete_options = None
mark_published_options = None
mark_reviewed_options = None
mark_resolved_options = None
save_as_options = None
search_options = None
update_options = None
# def __init__(self):
# # noinspection PyProtectedMember
# self.meta = self.model._meta
# self.patterns = dict()
#
# super().__init__()
[docs] def get_namespace(self):
"""Get the URL namespace for this UI.
:rtype: str | None
"""
return self.namespace
[docs] def get_pattern(self, verb):
"""Get the pattern for the given verb.
:param verb: The name of the verb.
:type verb: str
:rtype: ModelPattern | None
.. important::
This patterns dictionary is empty until ``get_urls()`` is called.
"""
if verb in self.patterns:
return self.patterns[verb]
if self.logging_enabled:
log.warning("A pattern for %s was requested, but does not exist: %s" % (self.meta.model_name, verb))
return None
[docs] def get_patterns(self, prefix=None):
"""Get the URL patterns supported by the UI.
:rtype: list[ModelPattern]
See ``get_urls()``.
.. important::
A pattern is instantiated only if the corresponding view options have been specified.
"""
namespace = self.get_namespace()
a = list()
# Standard CRUD.
if self.list_options is not None:
pattern = ModelPattern(
self.model,
self.list_view,
lookup_key=self.get_lookup_key(),
namespace=namespace,
prefix=prefix
)
a.append(pattern)
if self.create_options is not None or self.form_options is not None:
pattern = ModelPattern(
self.model,
self.create_view,
namespace=namespace,
prefix=prefix
)
a.append(pattern)
if self.delete_options is not None:
pattern = ModelPattern(
self.model,
self.delete_view,
lookup_field=self.get_lookup_field(),
lookup_key=self.get_lookup_key(),
namespace=namespace,
prefix=prefix
)
a.append(pattern)
if self.detail_options is not None:
pattern = ModelPattern(
self.model,
self.detail_view,
lookup_field=self.get_lookup_field(),
lookup_key=self.get_lookup_key(),
namespace=namespace,
prefix=prefix
)
a.append(pattern)
if self.update_options is not None or self.form_options is not None:
pattern = ModelPattern(
self.model,
self.update_view,
lookup_field=self.get_lookup_field(),
lookup_key=self.get_lookup_key(),
namespace=namespace,
prefix=prefix
)
a.append(pattern)
# Bulk views.
if self.batch_change_options is not None:
pattern = ModelPattern(
self.model,
self.batch_change_view,
prefix=prefix
)
a.append(pattern)
if self.bulk_compare_options is not None:
pattern = ModelPattern(
self.model,
self.bulk_compare_view,
prefix=prefix
)
a.append(pattern)
if self.bulk_delete_options is not None:
pattern = ModelPattern(
self.model,
self.bulk_delete_view,
prefix=prefix
)
a.append(pattern)
# AJAX views.
if self.ajax_auto_complete_options is not None:
pattern = ModelPattern(
self.model,
self.ajax_auto_complete_view,
prefix=prefix
)
a.append(pattern)
if self.ajax_chained_lookup_options is not None:
pattern = ModelPattern(
self.model,
self.ajax_chained_lookup_view,
name=self.ajax_chained_lookup_options.pattern_name,
prefix=prefix
)
a.append(pattern)
if self.ajax_chooser_options is not None:
pattern = ModelPattern(
self.model,
self.ajax_chooser_view,
name=self.ajax_chooser_options.pattern_name,
prefix=prefix
)
a.append(pattern)
if self.ajax_detail_options is not None:
pattern = ModelPattern(
self.model,
self.ajax_detail_view,
lookup_field=self.get_lookup_field(),
lookup_key=self.get_lookup_key(),
prefix=prefix
)
a.append(pattern)
if self.ajax_drag_and_drop_options is not None:
pattern = ModelPattern(
self.model,
self.ajax_drag_and_drop_view,
prefix=prefix
)
a.append(pattern)
if self.ajax_list_options is not None:
pattern = ModelPattern(
self.model,
self.ajax_list_view,
list_path="ajax/",
prefix=prefix
)
a.append(pattern)
if self.ajax_reorder_options is not None:
pattern = ModelPattern(
self.model,
self.ajax_reorder_view,
prefix=prefix
)
a.append(pattern)
if self.ajax_search_options is not None:
pattern = ModelPattern(
self.model,
self.ajax_search_view,
prefix=prefix
)
a.append(pattern)
if self.ajax_update_options is not None:
pattern = ModelPattern(
self.model,
self.ajax_update_view,
lookup_field=self.get_lookup_field(),
lookup_key=self.get_lookup_key(),
namespace=namespace,
prefix=prefix
)
a.append(pattern)
# Extended views.
if self.chooser_options is not None:
pattern = ModelPattern(
self.model,
self.chooser_view,
prefix=prefix
)
a.append(pattern)
if self.dashboard_options is not None:
pattern = ModelPattern(
self.model,
self.dashboard_view,
prefix=prefix
)
a.append(pattern)
if self.duplicate_options is not None:
pattern = ModelPattern(
self.model,
self.duplicate_view,
lookup_field=self.get_lookup_field(),
lookup_key=self.get_lookup_key(),
prefix=prefix
)
a.append(pattern)
if self.history_options is not None:
pattern = ModelPattern(
self.history_options.history_class,
self.history_view,
lookup_key="pk",
namespace=namespace,
prefix=prefix
)
a.append(pattern)
if self.behaviors.is_archived_model():
pattern = ModelPattern(
self.model,
self.mark_archived_redirect_view,
lookup_field=self.get_lookup_field(),
lookup_key=self.get_lookup_key(),
namespace=namespace,
prefix=prefix,
verb=VERB.MARK_ARCHIVED
)
a.append(pattern)
if self.behaviors.is_completed_model():
pattern = ModelPattern(
self.model,
self.mark_complete_redirect_view,
lookup_field=self.get_lookup_field(),
lookup_key=self.get_lookup_key(),
namespace=namespace,
prefix=prefix,
verb=VERB.MARK_COMPLETE
)
a.append(pattern)
pattern = ModelPattern(
self.model,
self.ajax_mark_complete_view,
lookup_field=self.get_lookup_field(),
lookup_key=self.get_lookup_key(),
namespace=namespace,
prefix=prefix,
verb=VERB.AJAX_MARK_COMPLETE
)
a.append(pattern)
if self.behaviors.is_published_model():
pattern = ModelPattern(
self.model,
self.mark_published_redirect_view,
lookup_field=self.get_lookup_field(),
lookup_key=self.get_lookup_key(),
namespace=namespace,
prefix=prefix,
verb=VERB.MARK_PUBLISHED
)
a.append(pattern)
if self.behaviors.is_reviewed_model():
pattern = ModelPattern(
self.model,
self.mark_reviewed_redirect_view,
lookup_field=self.get_lookup_field(),
lookup_key=self.get_lookup_key(),
namespace=namespace,
prefix=prefix,
verb=VERB.MARK_REVIEWED
)
a.append(pattern)
if self.behaviors.is_resolved_model():
pattern = ModelPattern(
self.model,
self.mark_resolved_redirect_view,
lookup_field=self.get_lookup_field(),
lookup_key=self.get_lookup_key(),
namespace=namespace,
prefix=prefix,
verb=VERB.MARK_RESOLVED
)
a.append(pattern)
if self.save_as_options is not None:
pattern = ModelPattern(
self.model,
self.save_as_view,
lookup_field=self.get_lookup_field(),
lookup_key=self.get_lookup_key(),
prefix=prefix
)
a.append(pattern)
if self.search_options is not None:
pattern = ModelPattern(
self.model,
self.search_view,
prefix=prefix
)
a.append(pattern)
return a
[docs] def get_url(self, verb, record=None):
"""Get (reverse) the URL for the given verb.
:param verb: The verb (action) representing the requested view.
:type verb: str
:param record: The model instance for record-specific views.
:rtype: str | None
"""
pattern = self.get_pattern(verb)
if pattern is None:
return None
return pattern.reverse(record=record)
[docs] def get_urls(self, prefix=None):
"""Get the URLs associated with the model.
:rtype: list
"""
patterns = self.get_patterns(prefix=prefix)
urls = list()
for p in patterns:
urls.append(p.get_url())
self.patterns[p.get_verb()] = p
# print(self.patterns)
return urls
# View Functions in Alphabetical Order
[docs] def ajax_auto_complete_view(self, request):
"""Get the auto-complete (AJAX) view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.ajax_auto_complete_options)
view_class = self.ajax_auto_complete_class
return view_class.as_view(**kwargs)(request)
[docs] def ajax_chained_lookup_view(self, request):
"""Get the chained lookup (AJAX) view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.ajax_chained_lookup_options)
view_class = self.ajax_chained_lookup_class
return view_class.as_view(**kwargs)(request)
[docs] def ajax_chooser_view(self, request):
"""Get the chooser (AJAX) view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.ajax_chooser_options)
view_class = self.ajax_chooser_class
return view_class.as_view(**kwargs)(request)
[docs] def ajax_create_view(self, request):
"""Get the AJAX create view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.ajax_create_options or self.create_options or self.form_options)
view_class = self.ajax_create_class
# AJAX view does not support steps.
if "steps" in kwargs:
fields = list()
steps = kwargs.pop("steps")
for step in steps:
fields += step.fields
kwargs['fields'] = fields
return view_class.as_view(**kwargs)(request)
[docs] def ajax_detail_view(self, request, identifier):
"""Get the AJAX detail view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:param identifier: The record identifier, e.g. the value of the primary key, UUID, or unique slug.
:type identifier: int | str
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.ajax_detail_options)
view_class = self.ajax_detail_class
return view_class.as_view(**kwargs)(request, identifier)
[docs] def ajax_drag_and_drop_view(self, request):
"""Get the drag and drop view (AJAX) view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.ajax_drag_and_drop_options)
view_class = self.ajax_drag_and_drop_class
return view_class.as_view(**kwargs)(request)
[docs] def ajax_list_view(self, request):
"""Get the AJAX list view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.ajax_list_options)
view_class = self.ajax_list_class
return view_class.as_view(**kwargs)(request)
[docs] def ajax_mark_complete_view(self, request, identifier):
"""Get the AJAX mark complete view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.ajax_mark_complete_options or AjaxMarkCompleteOptions())
view_class = self.ajax_mark_complete_class
return view_class.as_view(**kwargs)(request, identifier)
[docs] def ajax_update_view(self, request, identifier):
"""Get the AJAX create view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.ajax_update_options or self.update_options or self.form_options)
view_class = self.ajax_update_class
# AJAX view does not support steps.
if "steps" in kwargs:
fields = list()
steps = kwargs.pop("steps")
for step in steps:
fields += step.fields
kwargs['fields'] = fields
return view_class.as_view(**kwargs)(request, identifier)
[docs] def ajax_reorder_view(self, request):
"""Get the reorder view (AJAX) view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.ajax_reorder_options)
view_class = self.ajax_reorder_class
return view_class.as_view(**kwargs)(request)
[docs] def ajax_search_view(self, request):
"""Get the AJAX search view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.ajax_search_options)
view_class = self.ajax_search_class
return view_class.as_view(**kwargs)(request)
[docs] def batch_change_view(self, request):
"""Get the batch-change view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.batch_change_options)
view_class = self.batch_change_class
return view_class.as_view(**kwargs)(request)
[docs] def bulk_compare_view(self, request):
"""Get the bulk-compare view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.bulk_compare_options)
view_class = self.bulk_compare_class
return view_class.as_view(**kwargs)(request)
[docs] def bulk_delete_view(self, request):
"""Get the bulk-delete view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.bulk_delete_options)
view_class = self.bulk_delete_class
return view_class.as_view(**kwargs)(request)
[docs] def bulk_edit_view(self, request):
"""Get the bulk-edit view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.bulk_edit_options)
view_class = self.bulk_edit_class
return view_class.as_view(**kwargs)(request)
[docs] def chooser_view(self, request):
"""Get the chooser view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.chooser_options)
view_class = self.chooser_class
return view_class.as_view(**kwargs)(request)
[docs] def create_view(self, request):
"""Get the create view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.create_options or self.form_options)
view_class = self.create_class
if "steps" in kwargs:
form_list = list()
for step in kwargs['steps']:
# noinspection PyUnresolvedReferences
_kwargs = {
'fields': step.fields,
'form': step.form_class,
'formfield_callback': partial(self.get_form_field, request=request),
}
# noinspection PyUnresolvedReferences
form = forms.modelform_factory(self.model, **_kwargs)
form_list.append(form)
del(kwargs['steps'])
kwargs['form_list'] = form_list
return view_class.as_view(**kwargs)(request)
[docs] def dashboard_view(self, request):
"""Get the dashboard view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.dashboard_options)
view_class = self.dashboard_class
return view_class.as_view(**kwargs)(request)
[docs] def delete_view(self, request, identifier):
"""Get the delete view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:param identifier: The record identifier, e.g. the value of the primary key, UUID, or unique slug.
:type identifier: int | str
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.delete_options)
view_class = self.delete_class
return view_class.as_view(**kwargs)(request, identifier)
[docs] def detail_view(self, request, identifier):
"""Get the detail view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:param identifier: The record identifier, e.g. the value of the primary key, UUID, or unique slug.
:type identifier: int | str
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.detail_options)
view_class = self.detail_class
return view_class.as_view(**kwargs)(request, identifier)
[docs] def duplicate_view(self, request, identifier):
"""Get the duplicate view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:param identifier: The record identifier, e.g. the value of the primary key, UUID, or unique slug.
:type identifier: int | str
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.duplicate_options)
view_class = self.duplicate_class
return view_class.as_view(**kwargs)(request, identifier)
[docs] def history_view(self, request, identifier):
"""Get the history view for the model."""
kwargs = self._get_view_kwargs(self.history_options)
view_class = self.history_class
return view_class.as_view(**kwargs)(request, identifier)
[docs] def list_view(self, request):
"""Get the list view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.list_options)
view_class = self.list_class
return view_class.as_view(**kwargs)(request)
[docs] def mark_archived_redirect_view(self, request, identifier):
"""Provide the view for marking a record as archived."""
kwargs = self._get_view_kwargs(self.mark_archived_options or RedirectOptions())
view_class = self.mark_archived_class
return view_class.as_view(**kwargs)(request, identifier)
[docs] def mark_complete_redirect_view(self, request, identifier):
"""Provide the view for marking a record as completed."""
kwargs = self._get_view_kwargs(self.mark_complete_options or RedirectOptions())
view_class = self.mark_complete_class
return view_class.as_view(**kwargs)(request, identifier)
[docs] def mark_published_redirect_view(self, request, identifier):
"""Provide the view for marking a record as published."""
kwargs = self._get_view_kwargs(self.mark_published_options or RedirectOptions())
view_class = self.mark_published_class
return view_class.as_view(**kwargs)(request, identifier)
[docs] def mark_reviewed_redirect_view(self, request, identifier):
"""Provide the view for marking a record as reviewed."""
kwargs = self._get_view_kwargs(self.mark_reviewed_options or RedirectOptions())
view_class = self.mark_reviewed_class
return view_class.as_view(**kwargs)(request, identifier)
[docs] def mark_resolved_redirect_view(self, request, identifier):
"""Provide the view for marking a record as reviewed."""
kwargs = self._get_view_kwargs(self.mark_resolved_options or RedirectOptions())
view_class = self.mark_resolved_class
return view_class.as_view(**kwargs)(request, identifier)
[docs] def save_as_view(self, request, identifier):
"""Get the save as view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:param identifier: The record identifier, e.g. the value of the primary key, UUID, or unique slug.
:type identifier: int | str
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.save_as_options)
view_class = self.save_as_class
return view_class.as_view(**kwargs)(request, identifier)
[docs] def search_view(self, request):
"""Get the search view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.search_options)
view_class = self.search_class
return view_class.as_view(**kwargs)(request)
[docs] def update_view(self, request, identifier):
"""Get the update view for the model.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:param identifier: The record identifier, e.g. the value of the primary key, UUID, or unique slug.
:type identifier: int | str
:returns: The view function.
"""
kwargs = self._get_view_kwargs(self.update_options or self.form_options)
# TODO: Should update view detect steps as `create_view()` does?
view_class = self.update_class
return view_class.as_view(**kwargs)(request, identifier)
# Supporting Methods
def _get_view_kwargs(self, options):
"""Get the kwargs passed to the the ``as_view()`` method of a view class.
:param options: The options instance for the view.
:type options: BaseType[Options] | BaseType[AjaxOptions]
:rtype: dict
"""
kwargs = {
'ui': self,
**options.to_dict(),
}
if 'active_page' not in kwargs and isinstance(options, (BaseOptions, TemplateOptions)):
# noinspection PyUnresolvedReferences
kwargs['active_page'] = self.meta.model_name
return kwargs
# Classes
[docs]class InlineModelUI(BaseModelUI):
"""A UI for handling "inlines"."""
foreign_key_field = None
orientation = "horizontal"
form_options = None
detail_options = None
list_options = None
[docs] def __init__(self, parent_model, current_site=None):
"""Initialize the inline UI.
:param parent_model: The parent model class to which the inline refers.
:param current_site: The current SiteUI instance.
:type current_site: SiteUI
"""
super().__init__()
# noinspection PyProtectedMember
# self.meta = self.model._meta
self.parent_model = parent_model
self.queryset = None
self.records = list()
self.site = current_site
if self.permission_policy is None:
self.permission_policy = PermissionPolicy(self.meta.app_label, self.meta.model_name)
self._init_actions()
self._init_controls()
[docs] def get_action(self, request, verb, record=None):
"""Get an action to be used with an inline model.
:param request: The current HTTP request instance.
:param verb: The action to be performed.
:type verb: str
:param record: The model instance, if any, to which the action applies.
:rtype: Action | None
.. note::
This requires that a model interface also be defined for the inline model *and* that it is registered with
the same site instance as the parent model.
"""
if isinstance(verb, actions.BaseAction):
return verb.as_runtime(request, self, record=record)
if self.site is None:
log.warning("Can't resolve inline action, because a current site not provided to: %s" % self)
return None
if not self.site.is_registered_model(self.get_dotted_path()):
log.warning("Inline model is not registered with the current site instance: %s" % self)
return None
if verb in self._actions:
ui = self.site.get_model_ui(self.get_dotted_path())
return self._actions[verb].as_runtime(request, ui, record=record)
return None
[docs] def get_foreign_key(self):
"""Get the foreign key (field instance) that points to the parent model.
:rtype: BaseType[models.Field]
:raise: ValueError
:raises: ``ValueError`` if the key could not be identified.
"""
# Discovered keys are stored in a list. There should be only one.
foreign_keys = list()
# This code was adapted by grokking django.forms.models _get_foreign_key and the __init__ of BaseInlineFormset.
if self.foreign_key_field is not None:
for field in self.meta.fields:
if field.name == self.foreign_key_field:
if isinstance(field, models.ForeignKey):
# noinspection PyProtectedMember
if field.remote_field.model == self.parent_model:
foreign_keys.append(field)
elif field.remote_field.model in self.parent_model._meta.get_parent_list():
foreign_keys.append(field)
else:
pass
else:
for field in self.meta.fields:
if isinstance(field, models.ForeignKey):
# noinspection PyProtectedMember
if field.remote_field.model == self.parent_model:
foreign_keys.append(field)
elif field.remote_field.model in self.parent_model._meta.get_parent_list():
foreign_keys.append(field)
else:
pass
# Single, definitive foreign key is what we want.
if len(foreign_keys) == 1:
return foreign_keys[0]
elif len(foreign_keys) == 0:
raise ValueError("%s has no ForeignKey to %s" % (self.meta.label, self.parent_model._meta.label))
else:
raise ValueError("%s has more than one ForeignKey to %s" % self.meta.label, self.parent_model._meta.label)
# noinspection PyUnusedLocal
# noinspection PyUnusedLocal
def get_queryset(self, request, record=None):
# noinspection PyProtectedMember
qs = self.model._default_manager.get_queryset()
if record is None or record.pk is None:
return qs.none()
fk = self.get_foreign_key()
criteria = {
fk.name: record,
}
self.queryset = qs.filter(**criteria)
return self.queryset
[docs] def get_url(self, verb, record=None):
"""Get the URL of the inline model.
:param verb: The action to be performed.
:type verb: str
:param record: The model instance, if any, to which the action applies.
:rtype: str | None
.. note::
This requires that a model interface also be defined for the inline model *and* that it is registered with
the same site instance as the parent model.
"""
if self.site is None:
log.warning("Can't resolve inline URL, because a current site not provided to: %s" % self)
return None
return self.site.get_url(self.get_dotted_path(), verb, record=record)
def _init_actions(self):
"""Inline models have limited default actions."""
self._actions = {
"divider": actions.DividerAction(),
VERB.CREATE: actions.CreateAction(),
VERB.DELETE: actions.DeleteAction(),
VERB.DETAIL: actions.DetailAction(),
VERB.UPDATE: actions.UpdateAction(),
}
self._actions.update(self.actions.copy())
# self._actions = self.actions.copy()
#
# default_actions = {
# "divider": actions.DividerAction(),
# VERB.CREATE: actions.CreateAction(),
# VERB.DELETE: actions.DeleteAction(),
# VERB.DETAIL: actions.DetailAction(),
# VERB.UPDATE: actions.UpdateAction(),
# }
#
# self._actions.update(default_actions)
[docs]class ModelUI(ModelViewMixin, BaseModelUI):
"""Builds a user interface for a given model."""
# General attributes.
base_template = None
history_callback = None
index_name = "list"
lookup_key = "identifier"
max_url_history = 10
# Policies.
access_policy = None
[docs] def __init__(self, site=None):
"""Initial a user interface for a model.
:param site: The site instance to which this UI is registered.
:type site: SiteUI
"""
super().__init__()
# noinspection PyProtectedMember
# self.meta = self.model._meta
self.patterns = dict()
self.site = site
if self.access_policy is None:
if self.site and self.site.access_policy is not None:
# log.debug("Using access policy provided by the site UI: %s" % self.meta.model_name)
self.access_policy = self.site.access_policy
else:
# log.debug("Applying the default access policy: %s" % self.meta.model_name)
self.access_policy = AccessPolicy(login_required=True)
# print("instantiate", self.meta.model_name, "ui with site", self.site)
# Standard action instances may be initialized now. The view that is the subject of each action may not exist,
# but it's okay to create actions because get_action() calls load() which has the option of returning None if
# a corresponding view/pattern/url cannot be reversed. However, the presence of standard actions normalizes
# operation by insuring the action instances already exist when the corresponding views are provided.
# self.actions = self._init_actions()
# self.bulk_actions = self._init_bulk_actions()
self._init_actions()
self._init_bulk_actions()
self._init_controls()
# The standard delete view should work in most cases, so it is possible to auto-generate options.
if self.delete_options is None:
self.delete_options = DeleteOptions()
# Initialize all fields that are eligible for filtering within a list. Similar to initializing actions, in that
# the filters are available even if they aren't used.
# self.filters = self._init_filters()
self._filters = dict()
self._init_filtering()
# print(self.filters)
# If no permissions are defined, the default permission policy is used. This implements the standard checks for
# CRUD permissions, which will be just fine in most cases.
if self.permission_policy is None:
self.permission_policy = PermissionPolicy(self.meta.app_label, self.meta.model_name)
[docs] def after_save_record(self, record, request, verb, form=None):
"""Executed just after a record (and many to many relationships) is saved.
:param record: The model instance.
:type record: django.db.models.Model
:param request: The current HTTP request instance.
:param verb: The verb of the view calling the save.
:param form: The form instance.
"""
pass
[docs] def before_save_record(self, record, request, verb, form=None):
"""Executed just before a record is saved.
:param form: The form instance.
:param record: The model instance.
:type record: django.db.models.Model
:param request: The current HTTP request instance.
:param verb: The verb of the view calling the save.
"""
pass
[docs] def delete_record(self, record, request):
"""Delete a record.
:param record: The model instance to be deleted.
:param request: The current HTTP request.
"""
self.save_history(None, record, request, VERB.DELETE)
record.delete()
[docs] def get_action(self, request, verb, check=False, record=None):
"""Get the action instance for the given verb.
:param request: The current request instance.
:param verb: The name of action/verb.
:type verb: str
:param check: Also check permissions for the given verb.
:type check: bool
:param record: The model instance, if any, to which the action applies.
:rtype: BaseType[BaseAction] | None
.. tip::
This method does *not* check permission by default. In general, when processing records, this should be done
prior to getting an action. However, doing both may impact performance as the permission check will run
more than once. Use ``check`` carefully.
"""
if isinstance(verb, actions.BaseAction):
return verb.as_runtime(request, self, record=record)
if verb in self._actions:
if check and not self.check_permission(request, verb, record=record):
log.debug("%s %s permission denied for: %s" % (self.meta.model_name, verb, request.user))
return None
next_url = None
if verb in self.preserve_verbs:
next_url = quote_plus(request.get_full_path())
return self._actions[verb].as_runtime(request, self, next_url=next_url, record=record)
return None
[docs] def get_base_template(self, request, verb=None):
"""Get the base template to use for rendering a view.
:param request: The current HTTP instance.
:param verb: The verb/action of the current view.
:type verb: str
:rtype: str | None
"""
if self.base_template is not None:
return self.base_template
if self.site is not None:
return self.site.get_base_template(request, verb=verb)
return None
[docs] def get_bulk_action(self, request, verb, queryset=None):
"""Get the action instance for the given verb.
:param request: The current request instance.
:param verb: The name of action/verb.
:type verb: str
:param queryset: The queryset instance, if any, to which the action applies.
:rtype: BaseType[BaseBulkAction] | None
"""
if isinstance(verb, actions.BaseBulkAction):
return verb.as_runtime(request, self)
if verb in self._bulk_actions:
return self._bulk_actions[verb].as_runtime(request, self, queryset=queryset)
return None
[docs] def get_breadcrumbs(self, request, verb, record=None):
"""Get the breadcrumbs for the current view.
:param request: The current request instance.
:param verb: The verb of the current view.
:type verb: str
:param record: The current model instance.
:rtype: Breadcrumbs
"""
crumbs = Breadcrumbs()
# The "root" URL may be that of an app "above" this app.
label, root_url = self.get_root_url()
if root_url is not None:
crumbs.add(label, root_url)
# If the index and root are different, also add the index of the current app.
index_url = self.get_index_url()
if index_url != root_url:
crumbs.add(self.get_verbose_name_plural(), index_url)
if record is not None:
url = self.get_url(VERB.DETAIL, record=record)
if url is not None:
crumbs.add(self.get_display_value(record), url)
elif verb in VERB.LABELS:
crumbs.add(VERB.LABELS[verb], "")
else:
pass
return crumbs
# noinspection PyMethodMayBeStatic,PyUnusedLocal
[docs] def get_css(self, request, verb, queryset=None, record=None):
"""Get adhoc styles to be supplied in the response..
:param request: The current HTTP request instance.
:param verb: The action/verb of the current view.
:type verb: str
:param queryset: The current queryset.
:type queryset: QuerySet
:param record: The current record (model) instance.
:rtype: Stylesheet | None
"""
return None
# noinspection PyUnusedLocal,PyMethodMayBeStatic
# def get_field_queryset(self, model_field, request, criteria=None):
# """Get the queryset for a remote (foreign key, many to many, one to one) field.
#
# :param model_field: The field instance on the current model that refers to a remote field.
#
# :param request: The current request instance.
#
# :param criteria: Additional criteria to be used when obtaining the queryset.
# :type criteria: dict
#
# :rtype: django.db.models.QuerySet
#
# """
# if criteria is not None:
# # noinspection PyProtectedMember
# return model_field.remote_field.model._default_manager.filter(**criteria)
#
# # noinspection PyProtectedMember
# return model_field.remote_field.model._default_manager.all()
# noinspection PyUnusedLocal,PyMethodMayBeStatic
[docs] def get_fields(self, form_options, request, record=None):
"""Get the fields to be included in a form.
:param form_options: The options instance.
:param request: The current request instance.
:param record: The current model instance.
:rtype: list[str]
:returns: A list of field names.
"""
fields = list()
for f in form_options.fields:
if isinstance(f, controls.BaseControl):
fields.append(f.name)
else:
fields.append(f)
return fields
# noinspection PyMethodMayBeStatic,PyUnusedLocal
[docs] def get_fieldsets(self, form_options, request, record=None):
"""Get the fields to be included in a form.
:param form_options: The options instance.
:param request: The current request instance.
:param record: The current model instance.
:rtype: list[FieldSet] | None
:returns: A list of fieldset instances or ``None`` if no fieldsets have been defined.
"""
return form_options.fieldsets
# noinspection PyUnusedLocal
[docs] def get_filter(self, field, request):
"""Get the filter for a given field.
:param field: The field name.
:type field: str
:param request: The current request instance. Not used by default.
:returns: The filter instance or ``None`` if no filter is defined for the field.
"""
return self._filters.get(field, None)
# noinspection PyUnusedLocal
# def get_form_field(self, model_field, request, form_options=None, record=None, **kwargs):
#
# control = self._controls[model_field.name]
# if control.type == "datetime":
# kwargs['form_class'] = forms.SplitDateTimeField
# kwargs['input_date_formats'] = [control.date_format]
# kwargs['input_time_formats'] = [control.time_format]
# elif control.type == "foreignkey":
# criteria = self._get_limit_choices_to(control, request, record=record)
# kwargs['queryset'] = self.get_field_queryset(model_field, request, criteria=criteria)
# # print(control.name, control.limit_choices_to)
# # if control.limit_choices_to is not None:
# # if type(control.limit_choices_to) is dict:
# # kwargs['queryset'] = self.get_field_queryset(
# # model_field,
# # request,
# # criteria=control.limit_choices_to
# # )
# # elif callable(control.limit_choices_to):
# # criteria = control.limit_choices_to(request, record=record)
# # kwargs['queryset'] = self.get_field_queryset(model_field, request, criteria=criteria)
# # else:
# # log.warning("limit_choices_to given for %s control, but is not a dict or callable." % control.name)
# elif control.type == "manytomany":
# criteria = self._get_limit_choices_to(control, request, record=record)
# kwargs['queryset'] = self.get_field_queryset(model_field, request, criteria=criteria)
# # if control.limit_choices_to is not None:
# # if type(control.limit_choices_to) is dict:
# # kwargs['queryset'] = self.get_field_queryset(
# # model_field,
# # request,
# # criteria=control.limit_choices_to
# # )
# # elif callable(control.limit_choices_to):
# # criteria = control.limit_choices_to(request, record=record)
# # kwargs['queryset'] = self.get_field_queryset(model_field, request, criteria=criteria)
# # else:
# # log.warning("limit_choices_to given for %s control, but is not a dict or callable." % control.name)
# elif control.type == "time":
# kwargs['input_formats'] = [control.time_format]
# else:
# pass
#
# # Defaults are now handled either by the model, form, or in save_record().
# # if control.default is not None:
# # if isinstance(control.default, Default):
# # kwargs['initial'] = control.default.get(request, record=record, ui=self)
# # elif inspect.isclass(control.default) and issubclass(control.default, models.fields.NOT_PROVIDED):
# # pass
# # else:
# # kwargs['initial'] = control.default
#
# # if control.choices:
# # kwargs['choices'] = control.choices
#
# if control.help_text != model_field.help_text:
# kwargs['help_text'] = control.help_text
#
# if control.initial is not None:
# if isinstance(control.initial, Default):
# kwargs['initial'] = control.initial.get(request, record=record)
# else:
# kwargs['initial'] = control.initial
#
# if control.label != model_field.verbose_name:
# kwargs['label'] = control.label
#
# if control.widget is not None:
# kwargs['widget'] = control.widget
#
# return model_field.formfield(**kwargs)
# noinspection PyMethodMayBeStatic,PyUnusedLocal
# noinspection PyUnusedLocal
[docs] def get_history_callback(self, request):
"""Get the function used to save model history.
:param request: The current HTTP request.
:type request: django.http.request.HttpRequest
:returns: A callable that accepts the model instance, user, verb, and (optionally) ``fields``, ``url`` and
``verb_display``. ``fields`` should be a list of :py:class`superdjango.db.history.utils.FieldChange``
instances. If no ``history_callback`` is defined, then ``None`` is returned.
"""
return self.history_callback
[docs] def get_index_url(self):
"""Get (reverse) the model's index page.
:rtype: str | None
"""
return self.get_url(self.index_name)
# noinspection PyUnusedLocal
[docs] def get_inlines(self, request, verb):
"""Get the inline instances associated with the model.
:param request: The current HTTP request instance.
:param verb: The current verb being requested.
:type verb: str
:rtype: list[InlineModelUI] | None
"""
_verbs = {
VERB.CREATE: self.create_options or self.form_options,
VERB.DETAIL: self.detail_options,
VERB.UPDATE: self.update_options or self.form_options,
}
if verb not in _verbs:
return None
inlines = None
options = _verbs[verb]
if options is not None:
inlines = options.inlines
if inlines is None:
return None
a = list()
for inline_class in inlines:
inline = inline_class(self.model, current_site=self.site)
a.append(inline)
return a
# noinspection PyMethodMayBeStatic,PyUnusedLocal
[docs] def get_js(self, request, verb, queryset=None, record=None):
"""Get adhoc JavaScript to be supplied in the response..
:param request: The current HTTP request instance.
:param verb: The action/verb of the current view.
:type verb: str
:param queryset: The current queryset.
:type queryset: QuerySet
:param record: The current record (model) instance.
:rtype: JavaScript | None
"""
return None
[docs] def get_lookup_field(self):
"""Automatically get the name of the lookup field used to uniquely identify a record.
:rtype: str
.. note::
This method is overridden to provide automatic support for fields that guarantee uniqueness; it checks for
``unique_id``, ``uuid``, and ``slug`` fields that are unique before returning ``pk`` as the default.
"""
if self.lookup_field is not None:
return self.lookup_field
if self._field_exists("unique_id", unique=True):
return "unique_id"
if self._field_exists("uuid", unique=True):
return "uuid"
if self._field_exists("slug", unique=True):
return "slug"
return "pk"
# noinspection PyUnusedLocal
[docs] def get_messages(self, request, object_or_object_list=None):
"""Get the messages to be displayed to the user.
:param request: The current request instance.
:param object_or_object_list: The record or queryset of the current view.
:returns: A list of level, message tuples to be displayed to the user.
"""
return list()
[docs] def get_namespace(self):
"""Get the URL namespace for this UI.
:rtype: str | None
.. note::
This method is overridden to check the site's namespace.
"""
if self.site and self.site.namespace:
return self.site.namespace
if self.namespace:
return self.namespace
return None
# noinspection PyUnusedLocal
[docs] def get_queryset(self, request, criteria=None):
"""Get the queryset for the model.
:param request: The current HTTP request instance.
:param criteria: Additional criteria.
:type criteria: dict
:rtype: django.db.models.QuerySet
"""
if criteria is not None:
# noinspection PyProtectedMember
qs = self.model._default_manager.filter(**criteria)
else:
# noinspection PyProtectedMember
qs = self.model._default_manager.all()
if self.is_polymorphic_model():
return qs.select_subclasses(self.model)
return qs
[docs] def get_root_url(self):
"""Get the "root" URL of the UI.
If the UI is associated with a site, an attempt is made to find out what other app this UI may "live under".
Otherwise (or failing that), the index of the UI is returned.
:rtype: tuple[str, str]
:returns: The menu label and URL of the parent UI.
"""
# TODO: Should [Home, /] (or None, None) be returned to allow simplification of UIModelView.get_breadcrumbs()?
if self.site is not None:
# noinspection PyProtectedMember
for menu_name, menu_instance in list(self.site._registry.items()):
if self in menu_instance.items:
return menu_instance.root.get_verbose_name_plural(), menu_instance.root.get_index_url()
return self.get_verbose_name_plural(), self.get_index_url()
[docs] def get_url_history(self, request, back=1):
"""Get URL history.
:param request: The current HTTP request instance.
:param back: The number of steps to go back.
:type back: int
:rtype: list(str, str, str | None)
:returns: A list with 3 elements; the URL, title, and optional icon.
See the ``get_cancel_url()`` and ``get_success_url()`` methods of :py:class:`UIFormMixin`` for how this
method is called.
.. note::
History is scanned backward until a URL is found that doesn't match current request path.
"""
session = Session(request, prefix="%s_ui" % self.meta.model_name)
history = session.get("url_history", default=[])
try:
url = history[-back]
while url[0] == request.get_full_path():
back += 1
url = history[-back]
return url
except IndexError:
return None
[docs] def save_history(self, form, record, request, verb):
"""Save history if a callback is defined.
:param form: The validated form instance (used to support field changes).
:param record: The current record instance. If this is an update, it should be passed *before*
``record.save()`` is called. See ``save_record()`` for an example.
:param request: The current request.
:param verb: The verb (action) being taken.
:returns: The history instance received from the callback.
"""
callback = self.get_history_callback(request)
if callback is None:
return None
# There are no field changes during create and delete.
field_changes = None
if verb == VERB.UPDATE:
field_changes = get_field_changes(form, self.model, record=record)
# A delete will result in None from the attempt to reverse the pattern. This is less sloppy, though.
url = None
if verb != VERB.DELETE:
url = self.get_url(VERB.DETAIL, record=record)
return callback(record, request.user, verb, fields=field_changes, url=url)
[docs] def save_record(self, record, request, verb, form=None):
"""Save a record on submit.
:param record: The model instance.
:param request: The current HTTP request instance.
:param verb: The verb of the view calling the save.
:param form: The form instance.
:returns: The new or updated record (model instance).
"""
# Before save callback.
self.before_save_record(record, request, verb, form=form)
# Handle standard AddedByModel and ModifiedByModel.
if self.behaviors.is_audit_model():
record.audit(request.user, commit=False)
# Handle OwnedByModel. Wrapped in a try in case owned_by_id does not exist on the model. The is_owned_model()
# method validates whether the set_record_owner() method exists.
try:
ownership_conditions = [
self.behaviors.is_owned_model(),
not record.owned_by_id,
hasattr(record, "added_by_id"),
]
if all(ownership_conditions):
record.set_record_owner(record.added_by, commit=False)
except AttributeError:
pass
# Apply after POST defaults.
for control in self._controls.values():
if control.default and isinstance(control.default, Default):
if not self._field_has_value(control.name, record):
setattr(record, control.name, control.default.get(request, record=record))
# Handle ViewedByModel.
if self.behaviors.is_viewed_model():
record.mark_viewed(request.user, commit=False)
# Handle record locking.
conditions = [
self.behaviors.is_locked_model(),
form is not None,
]
if all(conditions):
if "is_locked" in form.cleaned_data and form.cleaned_data['is_locked'] is True:
record.lock_record(request.user, commit=False)
# If this is an existing record, we need to call history before the record is saved when access to previous
# field values are still available.
if verb == VERB.UPDATE:
self.save_history(form, record, request, verb)
# Now save the record and related fields.
record.save()
form.save_m2m()
# After save callback.
self.after_save_record(record, request, verb, form=form)
# If this is a new record save the history.
if verb == VERB.CREATE:
self.save_history(form, record, request, verb)
# Return the model instance.
return record
[docs] def save_url_history(self, request, title):
"""Save URL history to a session variable.
:param request: The current HTTP request instance.
:param title: A title to save with the current URL.
:type title: str
See the ``render_to_response()`` and ``save_url_history()`` methods of :py:class:`UIModelView`` for how this
method is called.
"""
# Get the current history.
session = Session(request, prefix="%s_ui" % self.meta.model_name)
history = session.get("url_history", default=[])
# Remove the oldest URL history.
history = list(collections.deque(iterable=history, maxlen=self.max_url_history))
# Don't do anything if the current request is the same as the last request.
try:
if history[-1][0] == request.get_full_path():
return
except IndexError:
pass
# Add the current URL to the history.
history.append([request.get_full_path(), title, self.icon])
session.set("url_history", history)
# def _get_limit_choices_to(self, control, request, record=None):
# """Get the criteria for limiting choices of a foreign key or many to many field.
#
# :param control: The control upon which ``limit_choices_to`` is defined.
# :type control: ForeignKeyControl | ManyToManyControl
#
# :param request: The current HTTP request instance.
#
# :param record: The current model instance.
#
# :rtype: dict | None
#
# """
# if control.limit_choices_to is None:
# return None
#
# if type(control.limit_choices_to) is dict:
# return control.limit_choices_to
#
# if callable(control.limit_choices_to):
# return control.limit_choices_to(request, record=record)
#
# if self.logging_enabled:
# log.warning("limit_choices_to given for %s control, but is not a dict or callable." % control.name)
#
# return None
def _init_filtering(self):
"""Initialize all possible field filters regardless of filtering options.
Populates the filters dictionary.
"""
# Get the fields attached to the model.
fields = self.meta.get_fields()
# Handle filtering pre-defined by the developer.
for field_instance in fields:
if field_instance.name in self.filters:
# print("predefined filter", self.model, field_instance.name)
_filter = self.filters[field_instance.name]
_filter.field_instance = field_instance
if _filter.label is None:
_filter.label = field_instance.verbose_name or field_instance.name.replace("_", " ").title()
self._filters[field_instance.name] = _filter
# Handle remaining filter fields automatically.
for field_instance in fields:
if field_instance.name in self._filters:
continue
# print("auto", self.model, field_instance, type(field_instance))
_filter = filters.factory(field_instance)
if _filter is not None:
self._filters[_filter.field_instance.name] = _filter
# print(self.model, self._filters)
[docs]class SiteUI(object):
"""A *site* is a collection of menus."""
access_policy = None
base_template = None
namespace = None
[docs] def __init__(self):
"""Initialize the models and registry for the site."""
self._models = dict()
self._registry = dict()
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self.namespace or "default")
[docs] def check_permission(self, model, request, verb, field=None, record=None):
"""Check permission on a model that has been registered with the site.
:param model: The model in question. This may be a model class or a dotted path of ``app_label.ModelName``.
:type model: class | str
:param request: The current request instance.
:param verb: The verb in question.
:type verb: str
:param field: The field name, if any.
:type field: str
:param record: The model instance, if any.
:rtype: bool
:raise: ValueError
"""
if inspect.isclass(model) and issubclass(model, models.Model):
# noinspection PyUnresolvedReferences,PyProtectedMember
dotted = "%s.%s" % (model._meta.app_label, model._meta.model_name)
elif type(model) is str:
dotted = model
else:
raise TypeError("The model parameter must be a class or string.")
if not self.is_registered_model(dotted):
log.warning("%s is not registered with the %s site." % (model, self))
return False
ui = self.get_model_ui(dotted)
return ui.check_permission(request, verb, field=field, record=record)
# noinspection PyUnusedLocal
[docs] def get_base_template(self, request, verb=None):
"""Get the base template to use for rendering a view.
:param request: The current HTTP instance.
:param verb: The verb/action of the current view.
:type verb: str
:rtype: str | None
"""
return self.base_template
[docs] def get_model_ui(self, dotted):
"""Get the model UI instance for the given dotted path.
:param dotted: The ``app_label.model_name``.
:type dotted: str
:rtype: ModelUI | None
"""
return self._models.get(dotted)
[docs] def get_url(self, dotted, verb, record=None):
"""Get the URL from a model registered with the site.
:param dotted: The ``app_label.model_name``.
:type dotted: str
:param verb: The action to be performed.
:type verb: str
:param record: The model instance, if any, to which the action applies.
:rtype: str | None
"""
ui = self.get_model_ui(dotted)
if ui is not None:
return ui.get_url(verb, record=record)
return None
[docs] def get_urls(self):
"""Get the URLs for the site's apps.
:rtype: list
"""
urlpatterns = list()
for name, instance in list(self._registry.items()):
urlpatterns += instance.get_urls()
# print(self._models)
return urlpatterns
[docs] def is_registered_model(self, dotted):
"""Indicates whether the given model has a UI registered with the site.
:param dotted: The ``app_label.model_name``.
:type dotted: str
:rtype: bool
"""
return dotted in self._models
[docs] def register(self, menu):
"""Register a menu with the site.
:param menu: The menu to be registered.
:type menu: BaseType[superdjango.ui.menus.library.Menu]
:raise: ModelAlreadyRegistered, ViewAlreadyExists
"""
from .menus import Menu
# Automatically set the name. If the standard convention is followed (ModelNameMenu), this will result in the
# correct menu name being used for navigation. Otherwise, the name will need to set manually.
if menu.name:
name = menu.name
else:
name = menu.__name__.lower().replace("menu", "")
menu.name = name
if name in self._registry:
raise ViewAlreadyExists(name + " (SiteUI Menu)")
# Set the site attribute for any model UI instances in the menu.
index = 0
for item in menu.items:
if inspect.isclass(item):
if issubclass(item, ModelUI):
_item = item(site=self)
dotted_path = _item.get_dotted_path()
if self.is_registered_model(dotted_path):
raise ModelAlreadyRegistered(item.model)
menu.items[index] = _item
self._models[dotted_path] = _item
elif issubclass(item, Menu):
self.register(item)
else:
pass
index += 1
# Add the menu to the register.
# noinspection PyCallingNonCallable
self._registry[name] = menu(site=self)
site = SiteUI()