Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/mptt/forms.py: 44%
85 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"""
2Form components for working with trees.
3"""
4from django import forms
5from django.forms.forms import NON_FIELD_ERRORS
6from django.utils.encoding import smart_str
7from django.utils.html import conditional_escape, mark_safe
8from django.utils.translation import gettext_lazy as _
10from mptt.exceptions import InvalidMove
11from mptt.settings import DEFAULT_LEVEL_INDICATOR
14__all__ = (
15 "TreeNodeChoiceField",
16 "TreeNodeMultipleChoiceField",
17 "TreeNodePositionField",
18 "MoveNodeForm",
19)
21# Fields ######################################################################
24class TreeNodeChoiceFieldMixin:
25 def __init__(self, queryset, *args, **kwargs):
26 self.level_indicator = kwargs.pop("level_indicator", DEFAULT_LEVEL_INDICATOR)
27 self.start_level = kwargs.pop("start_level", 0)
29 # if a queryset is supplied, enforce ordering
30 if hasattr(queryset, "model"): 30 ↛ 31line 30 didn't jump to line 31, because the condition on line 30 was never true
31 mptt_opts = queryset.model._mptt_meta
32 queryset = queryset.order_by(mptt_opts.tree_id_attr, mptt_opts.left_attr)
34 super().__init__(queryset, *args, **kwargs)
36 def _get_relative_level(self, obj):
37 level = getattr(obj, obj._mptt_meta.level_attr)
38 return level - self.start_level
40 def _get_level_indicator(self, obj):
41 level = self._get_relative_level(obj)
42 return mark_safe(conditional_escape(self.level_indicator) * level)
44 def label_from_instance(self, obj):
45 """
46 Creates labels which represent the tree level of each node when
47 generating option labels.
48 """
49 level_indicator = self._get_level_indicator(obj)
50 return mark_safe(level_indicator + " " + conditional_escape(smart_str(obj)))
53class TreeNodeChoiceField(TreeNodeChoiceFieldMixin, forms.ModelChoiceField):
54 """A ModelChoiceField for tree nodes."""
57class TreeNodeMultipleChoiceField(
58 TreeNodeChoiceFieldMixin, forms.ModelMultipleChoiceField
59):
60 """A ModelMultipleChoiceField for tree nodes."""
63class TreeNodePositionField(forms.ChoiceField):
64 """A ChoiceField for specifying position relative to another node."""
66 FIRST_CHILD = "first-child"
67 LAST_CHILD = "last-child"
68 LEFT = "left"
69 RIGHT = "right"
71 DEFAULT_CHOICES = (
72 (FIRST_CHILD, _("First child")),
73 (LAST_CHILD, _("Last child")),
74 (LEFT, _("Left sibling")),
75 (RIGHT, _("Right sibling")),
76 )
78 def __init__(self, *args, **kwargs):
79 if "choices" not in kwargs: 79 ↛ 81line 79 didn't jump to line 81, because the condition on line 79 was never false
80 kwargs["choices"] = self.DEFAULT_CHOICES
81 super().__init__(*args, **kwargs)
84# Forms #######################################################################
87class MoveNodeForm(forms.Form):
88 """
89 A form which allows the user to move a given node from one location
90 in its tree to another, with optional restriction of the nodes which
91 are valid target nodes for the move.
92 """
94 target = TreeNodeChoiceField(queryset=None)
95 position = TreeNodePositionField()
97 def __init__(self, node, *args, **kwargs):
98 """
99 The ``node`` to be moved must be provided. The following keyword
100 arguments are also accepted::
102 ``valid_targets``
103 Specifies a ``QuerySet`` of valid targets for the move. If
104 not provided, valid targets will consist of everything other
105 node of the same type, apart from the node itself and any
106 descendants.
108 For example, if you want to restrict the node to moving
109 within its own tree, pass a ``QuerySet`` containing
110 everything in the node's tree except itself and its
111 descendants (to prevent invalid moves) and the root node (as
112 a user could choose to make the node a sibling of the root
113 node).
115 ``target_select_size``
116 The size of the select element used for the target node.
117 Defaults to ``10``.
119 ``position_choices``
120 A tuple of allowed position choices and their descriptions.
121 Defaults to ``TreeNodePositionField.DEFAULT_CHOICES``.
123 ``level_indicator``
124 A string which will be used to represent a single tree level
125 in the target options.
126 """
127 self.node = node
128 valid_targets = kwargs.pop("valid_targets", None)
129 target_select_size = kwargs.pop("target_select_size", 10)
130 position_choices = kwargs.pop("position_choices", None)
131 level_indicator = kwargs.pop("level_indicator", None)
132 super().__init__(*args, **kwargs)
133 opts = node._mptt_meta
134 if valid_targets is None:
135 valid_targets = node._tree_manager.exclude(
136 **{
137 opts.tree_id_attr: getattr(node, opts.tree_id_attr),
138 opts.left_attr + "__gte": getattr(node, opts.left_attr),
139 opts.right_attr + "__lte": getattr(node, opts.right_attr),
140 }
141 )
142 self.fields["target"].queryset = valid_targets
143 self.fields["target"].widget.attrs["size"] = target_select_size
144 if level_indicator:
145 self.fields["target"].level_indicator = level_indicator
146 if position_choices:
147 self.fields["position"].choices = position_choices
149 def save(self):
150 """
151 Attempts to move the node using the selected target and
152 position.
154 If an invalid move is attempted, the related error message will
155 be added to the form's non-field errors and the error will be
156 re-raised. Callers should attempt to catch ``InvalidNode`` to
157 redisplay the form with the error, should it occur.
158 """
159 try:
160 self.node.move_to(
161 self.cleaned_data["target"], self.cleaned_data["position"]
162 )
163 return self.node
164 except InvalidMove as e:
165 self.errors[NON_FIELD_ERRORS] = self.error_class(e)
166 raise
169class MPTTAdminForm(forms.ModelForm):
170 """
171 A form which validates that the chosen parent for a node isn't one of
172 its descendants.
173 """
175 def __init__(self, *args, **kwargs):
176 super().__init__(*args, **kwargs)
177 if self.instance and self.instance.pk and not self.instance._state.adding:
178 instance = self.instance
179 opts = self._meta.model._mptt_meta
180 parent_field = self.fields.get(opts.parent_attr)
181 if parent_field:
182 parent_qs = parent_field.queryset
183 parent_qs = parent_qs.exclude(
184 pk__in=instance.get_descendants(include_self=True).values_list(
185 "pk", flat=True
186 )
187 )
188 parent_field.queryset = parent_qs
190 def clean(self):
191 cleaned_data = super().clean()
192 opts = self._meta.model._mptt_meta
193 parent = cleaned_data.get(opts.parent_attr)
194 if self.instance and parent:
195 if parent.is_descendant_of(self.instance, include_self=True):
196 if opts.parent_attr not in self._errors:
197 self._errors[opts.parent_attr] = self.error_class()
198 self._errors[opts.parent_attr].append(_("Invalid parent"))
199 del self.cleaned_data[opts.parent_attr]
200 return cleaned_data