Total Complexity | 70 |
Total Lines | 410 |
Duplicated Lines | 0 % |
Complex classes like parler.TranslatableAdmin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | """ |
||
177 | class TranslatableAdmin(BaseTranslatableAdmin, admin.ModelAdmin): |
||
178 | """ |
||
179 | Base class for translated admins. |
||
180 | |||
181 | This class also works as regular admin for non TranslatableModel objects. |
||
182 | When using this class with a non-TranslatableModel, |
||
183 | all operations effectively become a NO-OP. |
||
184 | """ |
||
185 | #: Whether the translations should be prefetched when displaying the 'language_column' in the list. |
||
186 | prefetch_language_column = True |
||
187 | |||
188 | deletion_not_allowed_template = 'admin/parler/deletion_not_allowed.html' |
||
189 | |||
190 | #: Whether translations of inlines should also be deleted when deleting a translation. |
||
191 | delete_inline_translations = True |
||
192 | |||
193 | @property |
||
194 | def change_form_template(self): |
||
195 | """ |
||
196 | Dynamic property to support transition to regular models. |
||
197 | |||
198 | This automatically picks ``admin/parler/change_form.html`` when the admin uses a translatable model. |
||
199 | """ |
||
200 | if self._has_translatable_model(): |
||
201 | # While this breaks the admin template name detection, |
||
202 | # the get_change_form_base_template() makes sure it inherits from your template. |
||
203 | return 'admin/parler/change_form.html' |
||
204 | else: |
||
205 | return None # get default admin selection |
||
206 | |||
207 | def language_column(self, object): |
||
208 | """ |
||
209 | The language column which can be included in the ``list_display``. |
||
210 | """ |
||
211 | return self._languages_column(object, span_classes='available-languages') # span class for backwards compatibility |
||
212 | language_column.allow_tags = True |
||
213 | language_column.short_description = _("Languages") |
||
214 | |||
215 | def all_languages_column(self, object): |
||
216 | """ |
||
217 | The language column which can be included in the ``list_display``. |
||
218 | It also shows untranslated languages |
||
219 | """ |
||
220 | all_languages = [code for code, __ in settings.LANGUAGES] |
||
221 | return self._languages_column(object, all_languages, span_classes='all-languages') |
||
222 | all_languages_column.allow_tags = True |
||
223 | all_languages_column.short_description = _("Languages") |
||
224 | |||
225 | def _languages_column(self, object, all_languages=None, span_classes=''): |
||
226 | active_languages = self.get_available_languages(object) |
||
227 | if all_languages is None: |
||
228 | all_languages = active_languages |
||
229 | |||
230 | current_language = object.get_current_language() |
||
231 | buttons = [] |
||
232 | opts = self.opts |
||
233 | for code in (all_languages or active_languages): |
||
234 | classes = ['lang-code'] |
||
235 | if code in active_languages: |
||
236 | classes.append('active') |
||
237 | else: |
||
238 | classes.append('untranslated') |
||
239 | if code == current_language: |
||
240 | classes.append('current') |
||
241 | |||
242 | info = _get_model_meta(opts) |
||
243 | admin_url = reverse('admin:{0}_{1}_change'.format(*info), args=(object.pk,), current_app=self.admin_site.name) |
||
244 | buttons.append('<a class="{classes}" href="{href}?language={language_code}">{title}</a>'.format( |
||
245 | language_code=code, |
||
246 | classes=' '.join(classes), |
||
247 | href=escape(admin_url), |
||
248 | title=conditional_escape(self.get_language_short_title(code)) |
||
249 | )) |
||
250 | return '<span class="language-buttons {0}">{1}</span>'.format( |
||
251 | span_classes, |
||
252 | ' '.join(buttons) |
||
253 | ) |
||
254 | |||
255 | def get_language_short_title(self, language_code): |
||
256 | """ |
||
257 | Hook for allowing to change the title in the :func:`language_column` of the list_display. |
||
258 | """ |
||
259 | # Show language codes in uppercase by default. |
||
260 | # This avoids a general text-transform CSS rule, |
||
261 | # that might conflict with showing longer titles for a language instead of the code. |
||
262 | # (e.g. show "Global" instead of "EN") |
||
263 | return language_code.upper() |
||
264 | |||
265 | def get_available_languages(self, obj): |
||
266 | """ |
||
267 | Fetching the available languages as queryset. |
||
268 | """ |
||
269 | if obj: |
||
270 | return obj.get_available_languages() |
||
271 | else: |
||
272 | return self.model._parler_meta.root_model.objects.none() |
||
273 | |||
274 | def get_queryset(self, request): |
||
275 | qs = super(TranslatableAdmin, self).get_queryset(request) |
||
276 | |||
277 | if self.prefetch_language_column: |
||
278 | # When the available languages are shown in the listing, prefetch available languages. |
||
279 | # This avoids an N-query issue because each row needs the available languages. |
||
280 | list_display = self.get_list_display(request) |
||
281 | if 'language_column' in list_display or 'all_languages_column' in list_display: |
||
282 | qs = qs.prefetch_related(self.model._parler_meta.root_rel_name) |
||
283 | |||
284 | return qs |
||
285 | |||
286 | def get_object(self, request, object_id, *args, **kwargs): |
||
287 | """ |
||
288 | Make sure the object is fetched in the correct language. |
||
289 | """ |
||
290 | # The args/kwargs are to support Django 1.8, which adds a from_field parameter |
||
291 | obj = super(TranslatableAdmin, self).get_object(request, object_id, *args, **kwargs) |
||
292 | |||
293 | if obj is not None and self._has_translatable_model(): # Allow fallback to regular models. |
||
294 | obj.set_current_language(self._language(request, obj), initialize=True) |
||
295 | |||
296 | return obj |
||
297 | |||
298 | def get_form(self, request, obj=None, **kwargs): |
||
299 | """ |
||
300 | Pass the current language to the form. |
||
301 | """ |
||
302 | form_class = super(TranslatableAdmin, self).get_form(request, obj, **kwargs) |
||
303 | if self._has_translatable_model(): |
||
304 | form_class.language_code = self.get_form_language(request, obj) |
||
305 | |||
306 | return form_class |
||
307 | |||
308 | def get_urls(self): |
||
309 | """ |
||
310 | Add a delete-translation view. |
||
311 | """ |
||
312 | urlpatterns = super(TranslatableAdmin, self).get_urls() |
||
313 | if not self._has_translatable_model(): |
||
314 | return urlpatterns |
||
315 | else: |
||
316 | opts = self.model._meta |
||
317 | info = _get_model_meta(opts) |
||
318 | |||
319 | if django.VERSION < (1, 9): |
||
320 | delete_path = url( |
||
321 | r'^(.+)/delete-translation/(.+)/$', |
||
322 | self.admin_site.admin_view(self.delete_translation), |
||
323 | name='{0}_{1}_delete_translation'.format(*info) |
||
324 | ) |
||
325 | else: |
||
326 | delete_path = url( |
||
327 | r'^(.+)/change/delete-translation/(.+)/$', |
||
328 | self.admin_site.admin_view(self.delete_translation), |
||
329 | name='{0}_{1}_delete_translation'.format(*info) |
||
330 | ) |
||
331 | |||
332 | return [delete_path] + urlpatterns |
||
333 | |||
334 | def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): |
||
335 | """ |
||
336 | Insert the language tabs. |
||
337 | """ |
||
338 | if self._has_translatable_model(): |
||
339 | lang_code = self.get_form_language(request, obj) |
||
340 | lang = get_language_title(lang_code) |
||
341 | |||
342 | available_languages = self.get_available_languages(obj) |
||
343 | language_tabs = self.get_language_tabs(request, obj, available_languages) |
||
344 | context['language_tabs'] = language_tabs |
||
345 | if language_tabs: |
||
346 | context['title'] = '%s (%s)' % (context['title'], lang) |
||
347 | if not language_tabs.current_is_translated: |
||
348 | add = True # lets prepopulated_fields_js work. |
||
349 | |||
350 | # Patch form_url to contain the "language" GET parameter. |
||
351 | # Otherwise AdminModel.render_change_form will clean the URL |
||
352 | # and remove the "language" when coming from a filtered object |
||
353 | # list causing the wrong translation to be changed. |
||
354 | |||
355 | params = request.GET.dict() |
||
356 | params['language'] = lang_code |
||
357 | form_url = add_preserved_filters({ |
||
358 | 'preserved_filters': urlencode(params), |
||
359 | 'opts': self.model._meta |
||
360 | }, form_url) |
||
361 | |||
362 | # django-fluent-pages uses the same technique |
||
363 | if 'default_change_form_template' not in context: |
||
364 | context['default_change_form_template'] = self.get_change_form_base_template() |
||
365 | |||
366 | #context['base_template'] = self.get_change_form_base_template() |
||
367 | return super(TranslatableAdmin, self).render_change_form(request, context, add, change, form_url, obj) |
||
368 | |||
369 | def response_add(self, request, obj, post_url_continue=None): |
||
370 | # Minor behavior difference for Django 1.4 |
||
371 | if post_url_continue is None and django.VERSION < (1, 5): |
||
372 | post_url_continue = '../%s/' |
||
373 | |||
374 | # Make sure ?language=... is included in the redirects. |
||
375 | redirect = super(TranslatableAdmin, self).response_add(request, obj, post_url_continue) |
||
376 | return self._patch_redirect(request, obj, redirect) |
||
377 | |||
378 | def response_change(self, request, obj): |
||
379 | # Make sure ?language=... is included in the redirects. |
||
380 | redirect = super(TranslatableAdmin, self).response_change(request, obj) |
||
381 | return self._patch_redirect(request, obj, redirect) |
||
382 | |||
383 | def _patch_redirect(self, request, obj, redirect): |
||
384 | if redirect.status_code not in (301, 302): |
||
385 | return redirect # a 200 response likely. |
||
386 | |||
387 | uri = iri_to_uri(request.path) |
||
388 | opts = self.model._meta |
||
389 | info = _get_model_meta(opts) |
||
390 | |||
391 | # Pass ?language=.. to next page. |
||
392 | language = request.GET.get(self.query_language_key) |
||
393 | if language: |
||
394 | continue_urls = (uri, "../add/", reverse('admin:{0}_{1}_add'.format(*info))) |
||
395 | if redirect['Location'] in continue_urls and self.query_language_key in request.GET: |
||
396 | # "Save and add another" / "Save and continue" URLs |
||
397 | redirect['Location'] += "?{0}={1}".format(self.query_language_key, language) |
||
398 | return redirect |
||
399 | |||
400 | @csrf_protect_m |
||
401 | @transaction_atomic |
||
402 | def delete_translation(self, request, object_id, language_code): |
||
403 | """ |
||
404 | The 'delete translation' admin view for this model. |
||
405 | """ |
||
406 | opts = self.model._meta |
||
407 | root_model = self.model._parler_meta.root_model |
||
408 | |||
409 | # Get object and translation |
||
410 | shared_obj = self.get_object(request, unquote(object_id)) |
||
411 | if shared_obj is None: |
||
412 | raise Http404 |
||
413 | |||
414 | shared_obj.set_current_language(language_code) |
||
415 | try: |
||
416 | translation = root_model.objects.get(master=shared_obj, language_code=language_code) |
||
417 | except root_model.DoesNotExist: |
||
418 | raise Http404 |
||
419 | |||
420 | if not self.has_delete_permission(request, translation): |
||
421 | raise PermissionDenied |
||
422 | |||
423 | if len(self.get_available_languages(shared_obj)) <= 1: |
||
424 | return self.deletion_not_allowed(request, translation, language_code) |
||
425 | |||
426 | # Populate deleted_objects, a data structure of all related objects that |
||
427 | # will also be deleted. |
||
428 | |||
429 | using = router.db_for_write(root_model) # NOTE: all same DB for now. |
||
430 | lang = get_language_title(language_code) |
||
431 | |||
432 | # There are potentially multiple objects to delete; |
||
433 | # the translation object at the base level, |
||
434 | # and additional objects that can be added by inherited models. |
||
435 | deleted_objects = [] |
||
436 | perms_needed = False |
||
437 | protected = [] |
||
438 | |||
439 | # Extend deleted objects with the inlines. |
||
440 | for qs in self.get_translation_objects(request, translation.language_code, obj=shared_obj, inlines=self.delete_inline_translations): |
||
441 | if isinstance(qs, (list, tuple)): |
||
442 | qs_opts = qs[0]._meta |
||
443 | else: |
||
444 | qs_opts = qs.model._meta |
||
445 | |||
446 | deleted_result = get_deleted_objects(qs, qs_opts, request.user, self.admin_site, using) |
||
447 | if django.VERSION >= (1, 8): |
||
448 | (del2, model_counts, perms2, protected2) = deleted_result |
||
449 | else: |
||
450 | (del2, perms2, protected2) = deleted_result |
||
451 | |||
452 | deleted_objects += del2 |
||
453 | perms_needed = perms_needed or perms2 |
||
454 | protected += protected2 |
||
455 | |||
456 | if request.POST: # The user has already confirmed the deletion. |
||
457 | if perms_needed: |
||
458 | raise PermissionDenied |
||
459 | obj_display = _('{0} translation of {1}').format(lang, force_text(translation)) # in hvad: (translation.master) |
||
460 | |||
461 | self.log_deletion(request, translation, obj_display) |
||
462 | self.delete_model_translation(request, translation) |
||
463 | self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % dict( |
||
464 | name=force_text(opts.verbose_name), obj=force_text(obj_display) |
||
465 | )) |
||
466 | |||
467 | if self.has_change_permission(request, None): |
||
468 | info = _get_model_meta(opts) |
||
469 | return HttpResponseRedirect(reverse('admin:{0}_{1}_change'.format(*info), args=(object_id,), current_app=self.admin_site.name)) |
||
470 | else: |
||
471 | return HttpResponseRedirect(reverse('admin:index', current_app=self.admin_site.name)) |
||
472 | |||
473 | object_name = _('{0} Translation').format(force_text(opts.verbose_name)) |
||
474 | if perms_needed or protected: |
||
475 | title = _("Cannot delete %(name)s") % {"name": object_name} |
||
476 | else: |
||
477 | title = _("Are you sure?") |
||
478 | |||
479 | context = { |
||
480 | "title": title, |
||
481 | "object_name": object_name, |
||
482 | "object": translation, |
||
483 | "deleted_objects": deleted_objects, |
||
484 | "perms_lacking": perms_needed, |
||
485 | "protected": protected, |
||
486 | "opts": opts, |
||
487 | "app_label": opts.app_label, |
||
488 | } |
||
489 | |||
490 | return render(request, self.delete_confirmation_template or [ |
||
491 | "admin/%s/%s/delete_confirmation.html" % (opts.app_label, opts.object_name.lower()), |
||
492 | "admin/%s/delete_confirmation.html" % opts.app_label, |
||
493 | "admin/delete_confirmation.html" |
||
494 | ], context) |
||
495 | |||
496 | def deletion_not_allowed(self, request, obj, language_code): |
||
497 | """ |
||
498 | Deletion-not-allowed view. |
||
499 | """ |
||
500 | opts = self.model._meta |
||
501 | context = { |
||
502 | 'object': obj.master, |
||
503 | 'language_code': language_code, |
||
504 | 'opts': opts, |
||
505 | 'app_label': opts.app_label, |
||
506 | 'language_name': get_language_title(language_code), |
||
507 | 'object_name': force_text(opts.verbose_name) |
||
508 | } |
||
509 | return render(request, self.deletion_not_allowed_template, context) |
||
510 | |||
511 | def delete_model_translation(self, request, translation): |
||
512 | """ |
||
513 | Hook for deleting a translation. |
||
514 | This calls :func:`get_translation_objects` to collect all related objects for the translation. |
||
515 | By default, that includes the translations for inline objects. |
||
516 | """ |
||
517 | master = translation.master |
||
518 | for qs in self.get_translation_objects(request, translation.language_code, obj=master, inlines=self.delete_inline_translations): |
||
519 | if isinstance(qs, (tuple, list)): |
||
520 | # The objects are deleted one by one. |
||
521 | # This triggers the post_delete signals and such. |
||
522 | for obj in qs: |
||
523 | obj.delete() |
||
524 | else: |
||
525 | # Also delete translations of inlines which the user has access to. |
||
526 | # This doesn't trigger signals, just like the regular |
||
527 | qs.delete() |
||
528 | |||
529 | def get_translation_objects(self, request, language_code, obj=None, inlines=True): |
||
530 | """ |
||
531 | Return all objects that should be deleted when a translation is deleted. |
||
532 | This method can yield all QuerySet objects or lists for the objects. |
||
533 | """ |
||
534 | if obj is not None: |
||
535 | # A single model can hold multiple TranslatedFieldsModel objects. |
||
536 | # Return them all. |
||
537 | for translations_model in obj._parler_meta.get_all_models(): |
||
538 | try: |
||
539 | translation = translations_model.objects.get(master=obj, language_code=language_code) |
||
540 | except translations_model.DoesNotExist: |
||
541 | continue |
||
542 | yield [translation] |
||
543 | |||
544 | if inlines: |
||
545 | for inline, qs in self._get_inline_translations(request, language_code, obj=obj): |
||
546 | yield qs |
||
547 | |||
548 | def _get_inline_translations(self, request, language_code, obj=None): |
||
549 | """ |
||
550 | Fetch the inline translations |
||
551 | """ |
||
552 | # django 1.4 do not accept the obj parameter |
||
553 | if django.VERSION < (1, 5): |
||
554 | inline_instances = self.get_inline_instances(request) |
||
555 | else: |
||
556 | inline_instances = self.get_inline_instances(request, obj=obj) |
||
557 | |||
558 | for inline in inline_instances: |
||
559 | if issubclass(inline.model, TranslatableModelMixin): |
||
560 | # leverage inlineformset_factory() to find the ForeignKey. |
||
561 | # This also resolves the fk_name if it's set. |
||
562 | fk = inline.get_formset(request, obj).fk |
||
563 | |||
564 | rel_name = 'master__{0}'.format(fk.name) |
||
565 | filters = { |
||
566 | 'language_code': language_code, |
||
567 | rel_name: obj |
||
568 | } |
||
569 | |||
570 | for translations_model in inline.model._parler_meta.get_all_models(): |
||
571 | qs = translations_model.objects.filter(**filters) |
||
572 | if obj is not None: |
||
573 | qs = qs.using(obj._state.db) |
||
574 | |||
575 | yield inline, qs |
||
576 | |||
577 | def get_change_form_base_template(self): |
||
578 | """ |
||
579 | Determine what the actual `change_form_template` should be. |
||
580 | """ |
||
581 | opts = self.model._meta |
||
582 | app_label = opts.app_label |
||
583 | return _lazy_select_template_name(( |
||
584 | "admin/{0}/{1}/change_form.html".format(app_label, opts.object_name.lower()), |
||
585 | "admin/{0}/change_form.html".format(app_label), |
||
586 | "admin/change_form.html" |
||
587 | )) |
||
722 |