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

178 statements  

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

1""" 

2The parent admin displays the list view of the base model. 

3""" 

4from django.contrib import admin 

5from django.contrib.admin.helpers import AdminErrorList, AdminForm 

6from django.contrib.admin.templatetags.admin_urls import add_preserved_filters 

7from django.contrib.contenttypes.models import ContentType 

8from django.core.exceptions import ImproperlyConfigured, PermissionDenied 

9from django.db import models 

10from django.http import Http404, HttpResponseRedirect 

11from django.template.response import TemplateResponse 

12from django.urls import URLResolver 

13from django.utils.encoding import force_str 

14from django.utils.http import urlencode 

15from django.utils.safestring import mark_safe 

16from django.utils.translation import gettext_lazy as _ 

17 

18from polymorphic.utils import get_base_polymorphic_model 

19 

20from .forms import PolymorphicModelChoiceForm 

21 

22 

23class RegistrationClosed(RuntimeError): 

24 "The admin model can't be registered anymore at this point." 

25 

26 

27class ChildAdminNotRegistered(RuntimeError): 

28 "The admin site for the model is not registered." 

29 

30 

31class PolymorphicParentModelAdmin(admin.ModelAdmin): 

32 """ 

33 A admin interface that can displays different change/delete pages, depending on the polymorphic model. 

34 To use this class, one attribute need to be defined: 

35 

36 * :attr:`child_models` should be a list models. 

37 

38 Alternatively, the following methods can be implemented: 

39 

40 * :func:`get_child_models` should return a list of models. 

41 * optionally, :func:`get_child_type_choices` can be overwritten to refine the choices for the add dialog. 

42 

43 This class needs to be inherited by the model admin base class that is registered in the site. 

44 The derived models should *not* register the ModelAdmin, but instead it should be returned by :func:`get_child_models`. 

45 """ 

46 

47 #: The base model that the class uses (auto-detected if not set explicitly) 

48 base_model = None 

49 

50 #: The child models that should be displayed 

51 child_models = None 

52 

53 #: Whether the list should be polymorphic too, leave to ``False`` to optimize 

54 polymorphic_list = False 

55 

56 add_type_template = None 

57 add_type_form = PolymorphicModelChoiceForm 

58 

59 #: The regular expression to filter the primary key in the URL. 

60 #: This accepts only numbers as defensive measure against catch-all URLs. 

61 #: If your primary key consists of string values, update this regular expression. 

62 pk_regex = r"(\d+|__fk__)" 

63 

64 def __init__(self, model, admin_site, *args, **kwargs): 

65 super().__init__(model, admin_site, *args, **kwargs) 

66 self._is_setup = False 

67 

68 if self.base_model is None: 68 ↛ 69line 68 didn't jump to line 69, because the condition on line 68 was never true

69 self.base_model = get_base_polymorphic_model(model) 

70 

71 def _lazy_setup(self): 

72 if self._is_setup: 72 ↛ 73line 72 didn't jump to line 73, because the condition on line 72 was never true

73 return 

74 

75 self._child_models = self.get_child_models() 

76 

77 # Make absolutely sure that the child models don't use the old 0.9 format, 

78 # as of polymorphic 1.4 this deprecated configuration is no longer supported. 

79 # Instead, register the child models in the admin too. 

80 if self._child_models and not issubclass(self._child_models[0], models.Model): 80 ↛ 81line 80 didn't jump to line 81, because the condition on line 80 was never true

81 raise ImproperlyConfigured( 

82 "Since django-polymorphic 1.4, the `child_models` attribute " 

83 "and `get_child_models()` method should be a list of models only.\n" 

84 "The model-admin class should be registered in the regular Django admin." 

85 ) 

86 

87 self._child_admin_site = self.admin_site 

88 self._is_setup = True 

89 

90 def register_child(self, model, model_admin): 

91 """ 

92 Register a model with admin to display. 

93 """ 

94 # After the get_urls() is called, the URLs of the child model can't be exposed anymore to the Django URLconf, 

95 # which also means that a "Save and continue editing" button won't work. 

96 if self._is_setup: 

97 raise RegistrationClosed("The admin model can't be registered anymore at this point.") 

98 

99 if not issubclass(model, self.base_model): 

100 raise TypeError( 

101 "{} should be a subclass of {}".format(model.__name__, self.base_model.__name__) 

102 ) 

103 if not issubclass(model_admin, admin.ModelAdmin): 

104 raise TypeError( 

105 "{} should be a subclass of {}".format( 

106 model_admin.__name__, admin.ModelAdmin.__name__ 

107 ) 

108 ) 

109 

110 self._child_admin_site.register(model, model_admin) 

111 

112 def get_child_models(self): 

113 """ 

114 Return the derived model classes which this admin should handle. 

115 This should return a list of tuples, exactly like :attr:`child_models` is. 

116 

117 The model classes can be retrieved as ``base_model.__subclasses__()``, 

118 a setting in a config file, or a query of a plugin registration system at your option 

119 """ 

120 if self.child_models is None: 120 ↛ 121line 120 didn't jump to line 121, because the condition on line 120 was never true

121 raise NotImplementedError("Implement get_child_models() or child_models") 

122 

123 return self.child_models 

124 

125 def get_child_type_choices(self, request, action): 

126 """ 

127 Return a list of polymorphic types for which the user has the permission to perform the given action. 

128 """ 

129 self._lazy_setup() 

130 choices = [] 

131 content_types = ContentType.objects.get_for_models( 

132 *self.get_child_models(), for_concrete_models=False 

133 ) 

134 

135 for model, ct in content_types.items(): 

136 perm_function_name = f"has_{action}_permission" 

137 model_admin = self._get_real_admin_by_model(model) 

138 perm_function = getattr(model_admin, perm_function_name) 

139 if not perm_function(request): 

140 continue 

141 choices.append((ct.id, model._meta.verbose_name)) 

142 return choices 

143 

144 def _get_real_admin(self, object_id, super_if_self=True): 

145 try: 

146 obj = ( 

147 self.model.objects.non_polymorphic().values("polymorphic_ctype").get(pk=object_id) 

148 ) 

149 except self.model.DoesNotExist: 

150 raise Http404 

151 return self._get_real_admin_by_ct(obj["polymorphic_ctype"], super_if_self=super_if_self) 

152 

153 def _get_real_admin_by_ct(self, ct_id, super_if_self=True): 

154 try: 

155 ct = ContentType.objects.get_for_id(ct_id) 

156 except ContentType.DoesNotExist as e: 

157 raise Http404(e) # Handle invalid GET parameters 

158 

159 model_class = ct.model_class() 

160 if not model_class: 

161 # Handle model deletion 

162 raise Http404("No model found for '{}.{}'.".format(*ct.natural_key())) 

163 

164 return self._get_real_admin_by_model(model_class, super_if_self=super_if_self) 

165 

166 def _get_real_admin_by_model(self, model_class, super_if_self=True): 

167 # In case of a ?ct_id=### parameter, the view is already checked for permissions. 

168 # Hence, make sure this is a derived object, or risk exposing other admin interfaces. 

169 if model_class not in self._child_models: 

170 raise PermissionDenied( 

171 "Invalid model '{}', it must be registered as child model.".format(model_class) 

172 ) 

173 

174 try: 

175 # HACK: the only way to get the instance of an model admin, 

176 # is to read the registry of the AdminSite. 

177 real_admin = self._child_admin_site._registry[model_class] 

178 except KeyError: 

179 raise ChildAdminNotRegistered( 

180 "No child admin site was registered for a '{}' model.".format(model_class) 

181 ) 

182 

183 if super_if_self and real_admin is self: 

184 return super() 

185 else: 

186 return real_admin 

187 

188 def get_queryset(self, request): 

189 # optimize the list display. 

190 qs = super().get_queryset(request) 

191 if not self.polymorphic_list: 

192 qs = qs.non_polymorphic() 

193 return qs 

194 

195 def add_view(self, request, form_url="", extra_context=None): 

196 """Redirect the add view to the real admin.""" 

197 ct_id = int(request.GET.get("ct_id", 0)) 

198 if not ct_id: 

199 # Display choices 

200 return self.add_type_view(request) 

201 else: 

202 real_admin = self._get_real_admin_by_ct(ct_id) 

203 # rebuild form_url, otherwise libraries below will override it. 

204 form_url = add_preserved_filters( 

205 { 

206 "preserved_filters": urlencode({"ct_id": ct_id}), 

207 "opts": self.model._meta, 

208 }, 

209 form_url, 

210 ) 

211 return real_admin.add_view(request, form_url, extra_context) 

212 

213 def change_view(self, request, object_id, *args, **kwargs): 

214 """Redirect the change view to the real admin.""" 

215 real_admin = self._get_real_admin(object_id) 

216 return real_admin.change_view(request, object_id, *args, **kwargs) 

217 

218 def changeform_view(self, request, object_id=None, *args, **kwargs): 

219 # The `changeform_view` is available as of Django 1.7, combining the add_view and change_view. 

220 # As it's directly called by django-reversion, this method is also overwritten to make sure it 

221 # also redirects to the child admin. 

222 if object_id: 

223 real_admin = self._get_real_admin(object_id) 

224 return real_admin.changeform_view(request, object_id, *args, **kwargs) 

225 else: 

226 # Add view. As it should already be handled via `add_view`, this means something custom is done here! 

227 return super().changeform_view(request, object_id, *args, **kwargs) 

228 

229 def history_view(self, request, object_id, extra_context=None): 

230 """Redirect the history view to the real admin.""" 

231 real_admin = self._get_real_admin(object_id) 

232 return real_admin.history_view(request, object_id, extra_context=extra_context) 

233 

234 def delete_view(self, request, object_id, extra_context=None): 

235 """Redirect the delete view to the real admin.""" 

236 real_admin = self._get_real_admin(object_id) 

237 return real_admin.delete_view(request, object_id, extra_context) 

238 

239 def get_preserved_filters(self, request): 

240 if "_changelist_filters" in request.GET: 

241 request.GET = request.GET.copy() 

242 filters = request.GET.get("_changelist_filters") 

243 f = filters.split("&") 

244 for x in f: 

245 c = x.split("=") 

246 request.GET[c[0]] = c[1] 

247 del request.GET["_changelist_filters"] 

248 return super().get_preserved_filters(request) 

249 

250 def get_urls(self): 

251 """ 

252 Expose the custom URLs for the subclasses and the URL resolver. 

253 """ 

254 urls = super().get_urls() 

255 

256 # At this point. all admin code needs to be known. 

257 self._lazy_setup() 

258 

259 return urls 

260 

261 def subclass_view(self, request, path): 

262 """ 

263 Forward any request to a custom view of the real admin. 

264 """ 

265 ct_id = int(request.GET.get("ct_id", 0)) 

266 if not ct_id: 

267 # See if the path started with an ID. 

268 try: 

269 pos = path.find("/") 

270 if pos == -1: 

271 object_id = int(path) 

272 else: 

273 object_id = int(path[0:pos]) 

274 except ValueError: 

275 raise Http404( 

276 "No ct_id parameter, unable to find admin subclass for path '{}'.".format(path) 

277 ) 

278 

279 ct_id = self.model.objects.values_list("polymorphic_ctype_id", flat=True).get( 

280 pk=object_id 

281 ) 

282 

283 real_admin = self._get_real_admin_by_ct(ct_id) 

284 resolver = URLResolver("^", real_admin.urls) 

285 resolvermatch = resolver.resolve(path) # May raise Resolver404 

286 if not resolvermatch: 

287 raise Http404(f"No match for path '{path}' in admin subclass.") 

288 

289 return resolvermatch.func(request, *resolvermatch.args, **resolvermatch.kwargs) 

290 

291 def add_type_view(self, request, form_url=""): 

292 """ 

293 Display a choice form to select which page type to add. 

294 """ 

295 if not self.has_add_permission(request): 

296 raise PermissionDenied 

297 

298 extra_qs = "" 

299 if request.META["QUERY_STRING"]: 

300 # QUERY_STRING is bytes in Python 3, using force_str() to decode it as string. 

301 # See QueryDict how Django deals with that. 

302 extra_qs = "&{}".format(force_str(request.META["QUERY_STRING"])) 

303 

304 choices = self.get_child_type_choices(request, "add") 

305 if len(choices) == 0: 

306 raise PermissionDenied 

307 if len(choices) == 1: 

308 return HttpResponseRedirect(f"?ct_id={choices[0][0]}{extra_qs}") 

309 

310 # Create form 

311 form = self.add_type_form( 

312 data=request.POST if request.method == "POST" else None, 

313 initial={"ct_id": choices[0][0]}, 

314 ) 

315 form.fields["ct_id"].choices = choices 

316 

317 if form.is_valid(): 

318 return HttpResponseRedirect("?ct_id={}{}".format(form.cleaned_data["ct_id"], extra_qs)) 

319 

320 # Wrap in all admin layout 

321 fieldsets = ((None, {"fields": ("ct_id",)}),) 

322 adminForm = AdminForm(form, fieldsets, {}, model_admin=self) 

323 media = self.media + adminForm.media 

324 opts = self.model._meta 

325 

326 context = { 

327 "title": _("Add %s") % force_str(opts.verbose_name), 

328 "adminform": adminForm, 

329 "is_popup": ("_popup" in request.POST or "_popup" in request.GET), 

330 "media": mark_safe(media), 

331 "errors": AdminErrorList(form, ()), 

332 "app_label": opts.app_label, 

333 } 

334 return self.render_add_type_form(request, context, form_url) 

335 

336 def render_add_type_form(self, request, context, form_url=""): 

337 """ 

338 Render the page type choice form. 

339 """ 

340 opts = self.model._meta 

341 app_label = opts.app_label 

342 context.update( 

343 { 

344 "has_change_permission": self.has_change_permission(request), 

345 "form_url": mark_safe(form_url), 

346 "opts": opts, 

347 "add": True, 

348 "save_on_top": self.save_on_top, 

349 } 

350 ) 

351 

352 templates = self.add_type_template or [ 

353 f"admin/{app_label}/{opts.object_name.lower()}/add_type_form.html", 

354 "admin/%s/add_type_form.html" % app_label, 

355 "admin/polymorphic/add_type_form.html", # added default here 

356 "admin/add_type_form.html", 

357 ] 

358 

359 request.current_app = self.admin_site.name 

360 return TemplateResponse(request, templates, context) 

361 

362 @property 

363 def change_list_template(self): 

364 opts = self.model._meta 

365 app_label = opts.app_label 

366 

367 # Pass the base options 

368 base_opts = self.base_model._meta 

369 base_app_label = base_opts.app_label 

370 

371 return [ 

372 f"admin/{app_label}/{opts.object_name.lower()}/change_list.html", 

373 "admin/%s/change_list.html" % app_label, 

374 # Added base class: 

375 "admin/%s/%s/change_list.html" % (base_app_label, base_opts.object_name.lower()), 

376 "admin/%s/change_list.html" % base_app_label, 

377 "admin/change_list.html", 

378 ]