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

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 _ 

9 

10from mptt.exceptions import InvalidMove 

11from mptt.settings import DEFAULT_LEVEL_INDICATOR 

12 

13 

14__all__ = ( 

15 "TreeNodeChoiceField", 

16 "TreeNodeMultipleChoiceField", 

17 "TreeNodePositionField", 

18 "MoveNodeForm", 

19) 

20 

21# Fields ###################################################################### 

22 

23 

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) 

28 

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) 

33 

34 super().__init__(queryset, *args, **kwargs) 

35 

36 def _get_relative_level(self, obj): 

37 level = getattr(obj, obj._mptt_meta.level_attr) 

38 return level - self.start_level 

39 

40 def _get_level_indicator(self, obj): 

41 level = self._get_relative_level(obj) 

42 return mark_safe(conditional_escape(self.level_indicator) * level) 

43 

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))) 

51 

52 

53class TreeNodeChoiceField(TreeNodeChoiceFieldMixin, forms.ModelChoiceField): 

54 """A ModelChoiceField for tree nodes.""" 

55 

56 

57class TreeNodeMultipleChoiceField( 

58 TreeNodeChoiceFieldMixin, forms.ModelMultipleChoiceField 

59): 

60 """A ModelMultipleChoiceField for tree nodes.""" 

61 

62 

63class TreeNodePositionField(forms.ChoiceField): 

64 """A ChoiceField for specifying position relative to another node.""" 

65 

66 FIRST_CHILD = "first-child" 

67 LAST_CHILD = "last-child" 

68 LEFT = "left" 

69 RIGHT = "right" 

70 

71 DEFAULT_CHOICES = ( 

72 (FIRST_CHILD, _("First child")), 

73 (LAST_CHILD, _("Last child")), 

74 (LEFT, _("Left sibling")), 

75 (RIGHT, _("Right sibling")), 

76 ) 

77 

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) 

82 

83 

84# Forms ####################################################################### 

85 

86 

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 """ 

93 

94 target = TreeNodeChoiceField(queryset=None) 

95 position = TreeNodePositionField() 

96 

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:: 

101 

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. 

107 

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). 

114 

115 ``target_select_size`` 

116 The size of the select element used for the target node. 

117 Defaults to ``10``. 

118 

119 ``position_choices`` 

120 A tuple of allowed position choices and their descriptions. 

121 Defaults to ``TreeNodePositionField.DEFAULT_CHOICES``. 

122 

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 

148 

149 def save(self): 

150 """ 

151 Attempts to move the node using the selected target and 

152 position. 

153 

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 

167 

168 

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 """ 

174 

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 

189 

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