Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/polymorphic/admin/inlines.py: 31%

92 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1""" 

2Django Admin support for polymorphic inlines. 

3 

4Each row in the inline can correspond with a different subclass. 

5""" 

6from functools import partial 

7 

8from django.conf import settings 

9from django.contrib.admin.options import InlineModelAdmin 

10from django.contrib.admin.utils import flatten_fieldsets 

11from django.core.exceptions import ImproperlyConfigured 

12from django.forms import Media 

13 

14from polymorphic.formsets import ( 

15 BasePolymorphicInlineFormSet, 

16 PolymorphicFormSetChild, 

17 UnsupportedChildType, 

18 polymorphic_child_forms_factory, 

19) 

20from polymorphic.formsets.utils import add_media 

21 

22from .helpers import PolymorphicInlineSupportMixin 

23 

24 

25class PolymorphicInlineModelAdmin(InlineModelAdmin): 

26 """ 

27 A polymorphic inline, where each formset row can be a different form. 

28 

29 Note that: 

30 

31 * Permissions are only checked on the base model. 

32 * The child inlines can't override the base model fields, only this parent inline can do that. 

33 """ 

34 

35 formset = BasePolymorphicInlineFormSet 

36 

37 #: The extra media to add for the polymorphic inlines effect. 

38 #: This can be redefined for subclasses. 

39 polymorphic_media = Media( 

40 js=( 

41 "admin/js/vendor/jquery/jquery{}.js".format("" if settings.DEBUG else ".min"), 

42 "admin/js/jquery.init.js", 

43 "polymorphic/js/polymorphic_inlines.js", 

44 ), 

45 css={"all": ("polymorphic/css/polymorphic_inlines.css",)}, 

46 ) 

47 

48 #: The extra forms to show 

49 #: By default there are no 'extra' forms as the desired type is unknown. 

50 #: Instead, add each new item using JavaScript that first offers a type-selection. 

51 extra = 0 

52 

53 #: Inlines for all model sub types that can be displayed in this inline. 

54 #: Each row is a :class:`PolymorphicInlineModelAdmin.Child` 

55 child_inlines = () 

56 

57 def __init__(self, parent_model, admin_site): 

58 super().__init__(parent_model, admin_site) 

59 

60 # Extra check to avoid confusion 

61 # While we could monkeypatch the admin here, better stay explicit. 

62 parent_admin = admin_site._registry.get(parent_model, None) 

63 if parent_admin is not None: # Can be None during check 

64 if not isinstance(parent_admin, PolymorphicInlineSupportMixin): 

65 raise ImproperlyConfigured( 

66 "To use polymorphic inlines, add the `PolymorphicInlineSupportMixin` mixin " 

67 "to the ModelAdmin that hosts the inline." 

68 ) 

69 

70 # While the inline is created per request, the 'request' object is not known here. 

71 # Hence, creating all child inlines unconditionally, without checking permissions. 

72 self.child_inline_instances = self.get_child_inline_instances() 

73 

74 # Create a lookup table 

75 self._child_inlines_lookup = {} 

76 for child_inline in self.child_inline_instances: 

77 self._child_inlines_lookup[child_inline.model] = child_inline 

78 

79 def get_child_inline_instances(self): 

80 """ 

81 :rtype List[PolymorphicInlineModelAdmin.Child] 

82 """ 

83 instances = [] 

84 for ChildInlineType in self.child_inlines: 

85 instances.append(ChildInlineType(parent_inline=self)) 

86 return instances 

87 

88 def get_child_inline_instance(self, model): 

89 """ 

90 Find the child inline for a given model. 

91 

92 :rtype: PolymorphicInlineModelAdmin.Child 

93 """ 

94 try: 

95 return self._child_inlines_lookup[model] 

96 except KeyError: 

97 raise UnsupportedChildType(f"Model '{model.__name__}' not found in child_inlines") 

98 

99 def get_formset(self, request, obj=None, **kwargs): 

100 """ 

101 Construct the inline formset class. 

102 

103 This passes all class attributes to the formset. 

104 

105 :rtype: type 

106 """ 

107 # Construct the FormSet class 

108 FormSet = super().get_formset(request, obj=obj, **kwargs) 

109 

110 # Instead of completely redefining super().get_formset(), we use 

111 # the regular inlineformset_factory(), and amend that with our extra bits. 

112 # This code line is the essence of what polymorphic_inlineformset_factory() does. 

113 FormSet.child_forms = polymorphic_child_forms_factory( 

114 formset_children=self.get_formset_children(request, obj=obj) 

115 ) 

116 return FormSet 

117 

118 def get_formset_children(self, request, obj=None): 

119 """ 

120 The formset 'children' provide the details for all child models that are part of this formset. 

121 It provides a stripped version of the modelform/formset factory methods. 

122 """ 

123 formset_children = [] 

124 for child_inline in self.child_inline_instances: 

125 # TODO: the children can be limited here per request based on permissions. 

126 formset_children.append(child_inline.get_formset_child(request, obj=obj)) 

127 return formset_children 

128 

129 def get_fieldsets(self, request, obj=None): 

130 """ 

131 Hook for specifying fieldsets. 

132 """ 

133 if self.fieldsets: 

134 return self.fieldsets 

135 else: 

136 return [] # Avoid exposing fields to the child 

137 

138 def get_fields(self, request, obj=None): 

139 if self.fields: 

140 return self.fields 

141 else: 

142 return [] # Avoid exposing fields to the child 

143 

144 @property 

145 def media(self): 

146 # The media of the inline focuses on the admin settings, 

147 # whether to expose the scripts for filter_horizontal etc.. 

148 # The admin helper exposes the inline + formset media. 

149 base_media = super().media 

150 all_media = Media() 

151 add_media(all_media, base_media) 

152 

153 # Add all media of the child inline instances 

154 for child_instance in self.child_inline_instances: 

155 child_media = child_instance.media 

156 

157 # Avoid adding the same media object again and again 

158 if child_media._css != base_media._css and child_media._js != base_media._js: 

159 add_media(all_media, child_media) 

160 

161 add_media(all_media, self.polymorphic_media) 

162 

163 return all_media 

164 

165 class Child(InlineModelAdmin): 

166 """ 

167 The child inline; which allows configuring the admin options 

168 for the child appearance. 

169 

170 Note that not all options will be honored by the parent, notably the formset options: 

171 * :attr:`extra` 

172 * :attr:`min_num` 

173 * :attr:`max_num` 

174 

175 The model form options however, will all be read. 

176 """ 

177 

178 formset_child = PolymorphicFormSetChild 

179 extra = 0 # TODO: currently unused for the children. 

180 

181 def __init__(self, parent_inline): 

182 self.parent_inline = parent_inline 

183 super(PolymorphicInlineModelAdmin.Child, self).__init__( 

184 parent_inline.parent_model, parent_inline.admin_site 

185 ) 

186 

187 def get_formset(self, request, obj=None, **kwargs): 

188 # The child inline is only used to construct the form, 

189 # and allow to override the form field attributes. 

190 # The formset is created by the parent inline. 

191 raise RuntimeError("The child get_formset() is not used.") 

192 

193 def get_fields(self, request, obj=None): 

194 if self.fields: 

195 return self.fields 

196 

197 # Standard Django logic, use the form to determine the fields. 

198 # The form needs to pass through all factory logic so all 'excludes' are set as well. 

199 # Default Django does: form = self.get_formset(request, obj, fields=None).form 

200 # Use 'fields=None' avoids recursion in the field autodetection. 

201 form = self.get_formset_child(request, obj, fields=None).get_form() 

202 return list(form.base_fields) + list(self.get_readonly_fields(request, obj)) 

203 

204 def get_formset_child(self, request, obj=None, **kwargs): 

205 """ 

206 Return the formset child that the parent inline can use to represent us. 

207 

208 :rtype: PolymorphicFormSetChild 

209 """ 

210 # Similar to the normal get_formset(), the caller may pass fields to override the defaults settings 

211 # in the inline. In Django's GenericInlineModelAdmin.get_formset() this is also used in the same way, 

212 # to make sure the 'exclude' also contains the GFK fields. 

213 # 

214 # Hence this code is almost identical to InlineModelAdmin.get_formset() 

215 # and GenericInlineModelAdmin.get_formset() 

216 # 

217 # Transfer the local inline attributes to the formset child, 

218 # this allows overriding settings. 

219 if "fields" in kwargs: 

220 fields = kwargs.pop("fields") 

221 else: 

222 fields = flatten_fieldsets(self.get_fieldsets(request, obj)) 

223 

224 if self.exclude is None: 

225 exclude = [] 

226 else: 

227 exclude = list(self.exclude) 

228 

229 exclude.extend(self.get_readonly_fields(request, obj)) 

230 # Add forcefully, as Django 1.10 doesn't include readonly fields. 

231 exclude.append("polymorphic_ctype") 

232 

233 if self.exclude is None and hasattr(self.form, "_meta") and self.form._meta.exclude: 

234 # Take the custom ModelForm's Meta.exclude into account only if the 

235 # InlineModelAdmin doesn't define its own. 

236 exclude.extend(self.form._meta.exclude) 

237 

238 # can_delete = self.can_delete and self.has_delete_permission(request, obj) 

239 defaults = { 

240 "form": self.form, 

241 "fields": fields, 

242 "exclude": exclude or None, 

243 "formfield_callback": partial(self.formfield_for_dbfield, request=request), 

244 } 

245 defaults.update(kwargs) 

246 

247 # This goes through the same logic that get_formset() calls 

248 # by passing the inline class attributes to modelform_factory() 

249 FormSetChildClass = self.formset_child 

250 return FormSetChildClass(self.model, **defaults) 

251 

252 

253class StackedPolymorphicInline(PolymorphicInlineModelAdmin): 

254 """ 

255 Stacked inline for django-polymorphic models. 

256 Since tabular doesn't make much sense with changed fields, just offer this one. 

257 """ 

258 

259 #: The default template to use. 

260 template = "admin/polymorphic/edit_inline/stacked.html"