Source code for feincms3.admin

from collections import defaultdict
from functools import update_wrapper

from django import forms
from django.contrib import messages
from django.contrib.admin import helpers
from django.contrib.admin.utils import unquote
from django.core.exceptions import PermissionDenied
from django.db import router, transaction
from django.forms.models import _get_foreign_key  # Since 2009.
from django.shortcuts import redirect
from django.urls import re_path
from django.utils.html import format_html, mark_safe
from django.utils.text import capfirst
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_protect
from tree_queries.admin import AncestorFilter, TreeAdmin as BaseTreeAdmin
from tree_queries.forms import TreeNodeChoiceField


__all__ = (
    "TreeAdmin",
    "MoveForm",
    "CloneForm",
    "AncestorFilter",
    "action_form_view_decorator",
)


def action_form_view_decorator(modeladmin):
    def wrap(view):
        def wrapper(request, object_id):
            with transaction.atomic(using=router.db_for_write(modeladmin.model)):
                model = modeladmin.model
                opts = model._meta

                obj = modeladmin.get_object(request, unquote(object_id))

                if not modeladmin.has_change_permission(request, obj):
                    raise PermissionDenied

                if obj is None:
                    return modeladmin._get_obj_does_not_exist_redirect(
                        request, opts, object_id
                    )

                return modeladmin.admin_site.admin_view(view)(request, obj)

        wrapper.model_admin = modeladmin
        return csrf_protect(update_wrapper(wrapper, view))

    return wrap


[docs] class TreeAdmin(BaseTreeAdmin): position_field = "position"
[docs] def get_urls(self): """ Add our own ``move`` view. """ info = self.model._meta.app_label, self.model._meta.model_name return [ re_path( r"^(.+)/move/$", action_form_view_decorator(self)(self.move_view), name="{}_{}_move".format(*info), ), re_path( r"^(.+)/clone/$", action_form_view_decorator(self)(self.clone_view), name="{}_{}_clone".format(*info), ), ] + super().get_urls()
def move_view(self, request, obj): return self.action_form_view( request, obj, form_class=MoveForm, title=_("Move %s") % obj ) def clone_view(self, request, obj): return self.action_form_view( request, obj, form_class=CloneForm, title=_("Clone %s") % obj ) def action_form_view(self, request, obj, *, form_class, title): kw = {"request": request, "obj": obj, "modeladmin": self} form = form_class(request.POST if request.method == "POST" else None, **kw) if form.is_valid(): return form.process() return self.render_action_form(request, form, title=title, obj=obj) def render_action_form(self, request, form, *, title, obj): adminform = helpers.AdminForm( form, [ (None, {"fields": form.fields.keys()}) ], # list(self.get_fieldsets(request, obj)), {}, # self.get_prepopulated_fields(request, obj), (), # self.get_readonly_fields(request, obj), model_admin=self, ) media = self.media + adminform.media context = dict( self.admin_site.each_context(request), title=title, object_id=obj.pk, original=obj, adminform=adminform, errors=helpers.AdminErrorList(form, ()), preserved_filters=self.get_preserved_filters(request), media=media, is_popup=False, inline_admin_formsets=[], save_as_new=False, show_save_and_add_another=False, show_save_and_continue=False, show_delete=False, ) response = self.render_change_form( request, context, add=False, change=True, obj=obj ) # Suppress the rendering of the "save and add another" button. response.context_data["has_add_permission"] = False return response
[docs] class MoveForm(forms.Form): """ Allows making the node the left or right sibling or the first or last child of another node. Requires the node to be moved as ``obj`` keyword argument. """ class Media: css = {"screen": ["feincms3/move-form.css"]} def __init__(self, *args, **kwargs): self.instance = kwargs.pop("obj") self.modeladmin = kwargs.pop("modeladmin") self.request = kwargs.pop("request") self.model = self.instance.__class__ super().__init__(*args, **kwargs) choices = self._generate_choices( self.modeladmin.get_queryset(self.request).with_tree_fields() ) self.fields["new_location"] = forms.ChoiceField( label=_("New location"), widget=forms.RadioSelect, choices=choices, ) if len(choices) <= 1: messages.warning( self.request, _( "Moving isn't possible because there are no valid targets." " Maybe you selected the only root node?" ), ) def _generate_choices(self, queryset): children = defaultdict(list) for node in queryset: children[node.parent_id].append(node) def _text_indent(depth): return mark_safe(f' style="text-indent:{depth * 30}px"') choices = [] def _iterate(parent_id): for index, node in enumerate(children[parent_id]): if node == self.instance: choice = ( "", format_html( '<div class="mv is-self"{}><strong>{}</strong>', _text_indent(node.tree_depth), node, ), ) if index == 0 and parent_id: # Moving the first child of parent_id; do not remove parent_id choices[-1] = ( choices[-1][0], mark_safe(choices[-1][1].replace("mv-mark", "hidden")), ) choices.append(choice) else: choices[-1] = choice continue choices.append( ( f"{node.id}:first", format_html( '<div class="mv to-first"{}><strong>{}</strong>' '<div class="mv-mark"{}>&rarr; {}</div></div>', _text_indent(node.tree_depth), node, _text_indent(node.tree_depth + 1), _("move here"), ), ) ) _iterate(node.id) choices.append( ( f"{node.id}:right", format_html( '<div class="mv to-right mv-mark"{}>&rarr; {}</div>', _text_indent(node.tree_depth), _("move here"), ), ) ) choices.append( ( "0:first", format_html( '<div class="mv to-root mv-mark">&rarr; {}</div>', _("move here"), ), ) ) _iterate(None) return choices
[docs] def clean(self): data = super().clean() if not data.get("new_location"): return data pk, _sep, first_or_right = data["new_location"].partition(":") data["first_or_right"] = first_or_right if pk == "0": self.instance.parent = None data["relative"] = None else: data["relative"] = self.instance.__class__._base_manager.get(pk=pk) if first_or_right == "first": self.instance.parent = data["relative"] else: self.instance.parent = data["relative"].parent # FIXME feincms3-sites would also require site_id, # feincms3-language-sites would also require language_code to be set # for the cleaning step to work correctly in all cases. # All fields of model are not in this form self.instance.full_clean( exclude=[f.name for f in self.model._meta.get_fields()] ) return data
def process(self): siblings = list( self.model._default_manager.filter(parent=self.instance.parent).exclude( pk=self.instance.pk ) ) relative = self.cleaned_data["relative"] first_or_right = self.cleaned_data["first_or_right"] if relative is None or first_or_right == "first": siblings.insert(0, self.instance) else: siblings.insert(siblings.index(relative) + 1, self.instance) for index, instance in enumerate(siblings): if instance == self.instance: instance.position = (index + 1) * 10 instance.save() else: self.model._default_manager.filter(pk=instance.pk).update( position=(index + 1) * 10 ) self.modeladmin.message_user( self.request, _("The node %(node)s has been moved to the new position.") % {"node": self.instance}, ) opts = self.modeladmin.model._meta return redirect(f"admin:{opts.app_label}_{opts.model_name}_changelist")
[docs] class CloneForm(forms.Form): def __init__(self, *args, **kwargs): self.instance = kwargs.pop("obj") self.modeladmin = kwargs.pop("modeladmin") self.request = kwargs.pop("request") super().__init__(*args, **kwargs) self.fields["target"] = self.instance._meta.get_field("parent").formfield( form_class=TreeNodeChoiceField, label=capfirst(_("target")), required=True, label_from_instance=lambda obj: "{}{}".format( "".join(["*** " if obj == self.instance else "--- "] * obj.tree_depth), obj, ), ) self.fields["target"].widget.attrs.update({"size": 30, "style": "height:auto"}) self.fields["_set_content"] = forms.BooleanField( label=_("Replace target's content"), required=False, help_text=_("Affects the following models: %s.") % ( ", ".join( "%s" % inline.model._meta.verbose_name_plural for inline in self.modeladmin.inlines ), ), ) for field in sorted( self.instance._meta.get_fields(), key=lambda field: field.name ): if field.auto_created or not field.editable: continue self.fields[f"set_{field.name}"] = forms.BooleanField( label=( f"{capfirst(field.verbose_name)} ({field.name})" if hasattr(field, "verbose_name") else field.name ), required=False, help_text=_('Current: "%s"') % (getattr(self.instance, field.name),), )
[docs] def clean(self): data = super().clean() target = data.get("target") if target is None: return data if target == self.instance: raise forms.ValidationError({"target": _("Cannot clone node to itself.")}) for field in self.instance._meta.get_fields(): if self.cleaned_data.get(f"set_{field.name}"): setattr(target, field.name, getattr(self.instance, field.name)) # All fields of model are not in this form target.full_clean(exclude=[f.name for f in target._meta.get_fields()]) return data
def process(self): target = self.cleaned_data["target"] fields = [] for field in self.instance._meta.get_fields(): if self.cleaned_data.get(f"set_{field.name}"): setattr(target, field.name, getattr(self.instance, field.name)) fields.append("{}".format(getattr(field, "verbose_name", field.name))) if fields: self.modeladmin.message_user( self.request, _("Updated fields of %(node)s: %(fields)s") % {"node": target, "fields": ", ".join(fields)}, ) if self.cleaned_data.get("_set_content"): for inline in self.modeladmin.inlines: fk = _get_foreign_key( self.modeladmin.model, inline.model, inline.fk_name, can_fail=False ) # Remove all existing instances inline.model._default_manager.filter(**{fk.name: target}).delete() for obj in inline.model._default_manager.filter( **{fk.name: self.instance} ): obj.pk = None setattr(obj, fk.name, target) obj.save(force_insert=True) self.modeladmin.message_user( self.request, _("Replaced the content of %(target)s with the contents of %(source)s.") % {"target": target, "source": self.instance}, ) target.save() opts = self.modeladmin.model._meta return redirect(f"admin:{opts.app_label}_{opts.model_name}_change", target.pk)