Source code for superdjango.ui.options.interfaces

# 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
[docs] def get_form_field(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] return control.get_form_field(model_field, request, record=record, **kwargs)
# 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
[docs] def get_form_class(self, request, record=None): """Get the form class to use in the inline formset. :param request: The current HTTP request instance. :param record: The current model instance. :returns: A form class. """ return self.form_options.form_class or forms.ModelForm
[docs] def get_formset(self, request, record=None, **kwargs): """Get the formset instance. :param request: The current HTTP request instance. :param record: The current model instance. :returns: An inline formset instance. """ formset_class = self.get_formset_class(request, record=record, **kwargs) return formset_class(request.POST or None, instance=record)
[docs] def get_formset_class(self, request, record=None, **kwargs): """Get the formset class for the inline records. :param request: The current HTTP request instance. :param record: The current model instance. :returns: An inline formset class. ``kwargs`` are passed to ``inlineformset_factory()``. """ if self.form_options is None: return None delete_enabled = self.form_options.delete_enabled and self.check_permission(request, "delete") form_class = self.get_form_class(request, record=record) # formfield_callback? _kwargs = { 'can_delete': delete_enabled, 'extra': self.form_options.extra_forms, 'fk_name': self.foreign_key_field, 'fields': self.form_options.fields, 'form': form_class, 'formfield_callback': partial( self.get_form_field, request=request, form_options=self.form_options, record=record ), 'formset': self.form_options.formset or forms.BaseInlineFormSet, 'max_num': self.form_options.maximum_number, 'min_num': self.form_options.minimum_number, **kwargs } return forms.inlineformset_factory(self.parent_model, self.model, **_kwargs)
# 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)
[docs] def get_form(self, request, data=None, files=None, record=None, **kwargs): """Get the form instance. :param request: The current request. :param data: The data to provide to the form. :param files: Submitted files. :param record: The record (model instance). Keyword arguments are passed to the form class. """ # Select the options to use based on whether a record has given given. if record is not None: if request.is_ajax(): _options = self.ajax_update_options or self.update_options or self.form_options else: _options = self.update_options or self.form_options else: if request.is_ajax(): _options = self.ajax_create_options or self.create_options or self.form_options else: _options = self.create_options or self.form_options # Get the class to be used for instantiated the form. form_class = self.get_form_class(_options, request, record=record) # Add the record instance to form instantiation. if record is not None: kwargs['instance'] = record # If allowed, scan GET for initial values. initial = None if _options.fields_from_get is not None: initial = dict() for f in _options.fields_from_get: key = "i_%s" % f if key in request.GET: initial[f] = request.GET.get(key) # Load tabs and fields. Note that the form_class may throw an error if RequestEnabledModelForm is not used. fieldsets = self.get_fieldsets(_options, request, record=record) tabs = self.get_form_tabs(_options, request, record=record) # noinspection PyArgumentList return form_class(data=data, fieldsets=fieldsets, files=files, initial=initial, request=request, tabs=tabs, **kwargs)
[docs] def get_form_class(self, form_options, request, record=None): """Get the form class to use for instantiating a model form. :param form_options: The UI options instance. :param request: The current request instance. :param record: The current record (model instance). :returns: The form class, by default using ``forms.modelform_factory()``. """ form_class = form_options.form_class or RequestEnabledModelForm fields = self.get_fields(form_options, request, record=record) _kwargs = { 'fields': fields, 'form': form_class, 'formfield_callback': partial(self.get_form_field, request=request, form_options=form_options, record=record) } return forms.modelform_factory(self.model, **_kwargs)
# 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
[docs] def get_form_tabs(self, form_options, request, record=None): """Get the fields to be included in a form with a tabbed interface. :param form_options: The options instance. :param request: The current request instance. :param record: The current model instance. :rtype: list[Tab] | None :returns: A list of tab instances or ``None`` if no tabs have been defined. """ return form_options.tabs
# 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_form(self, form, request, verb): """Save a record on form submit. :param form: The form instance. :param request: The current HTTP request instance. :param verb: The verb of the view calling the save. :returns: The new or updated record (model instance). """ record = form.save(commit=False) return self.save_record(record, request, verb, form=form)
[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_menus(self, request): """Get the site's menus. :param request: The current request instance. :rtype: dict :returns: A dictionary where the keys are the menu locations and values are a list of menu instances. """ from ..runtime.menus import Location # Initialize the unique locations. locations = dict() for name, instance in list(self._registry.items()): if instance.location not in locations: locations[instance.location] = Location(instance.location) # Add menus to each location. The instance here is a ui.menus.library.Menu instance. for location in locations.keys(): for name, instance in list(self._registry.items()): # Check permission on the instance as a whole. if not instance.check_permission(request): continue # Add a menu or menu item to the location. sort_order = 0 if location == instance.location: # Make sure the menu has a sort order within the location. if instance.sort_order is None: instance.sort_order = sort_order sort_order += 1 runtime = instance.as_runtime(request) if runtime is not None: locations[location].append(runtime) ''' # First condition represents a menu with sub-items while the second is a menu with only one item. items = instance.get_items(request) if len(items) > 1: # Resort the sub-items. items.sort(key=lambda x: x.sort_order) # Create a runtime menu based on the configured instance. menu = Menu( instance.label, name, flat=instance.flat, icon=instance.icon, sort_order=instance.sort_order, **instance.kwargs ) menu.items = items elif len(items) == 1: menu = Item( items[0].label, name, icon=items[0].icon or instance.icon, sort_order=items[0].sort_order or instance.sort_order, url=items[0].url ) else: # The user may not be logged in. continue locations[location].append(menu) ''' # Sort menus within the location. for location in locations.keys(): locations[location].items.sort(key=lambda x: x.sort_order) return locations
[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()