Completed
Push — master ( 0bffe1...d6203d )
by Diederik van der
11s
created

parler.TranslatableAdmin.delete_translation()   F

Complexity

Conditions 13

Size

Total Lines 95

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 13
dl 0
loc 95
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like parler.TranslatableAdmin.delete_translation() 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
"""
2
Translation support for admin forms.
3
4
*django-parler* provides the following classes:
5
6
* Model support: :class:`TranslatableAdmin`.
7
* Inline support: :class:`TranslatableInlineModelAdmin`, :class:`TranslatableStackedInline`, :class:`TranslatableTabularInline`.
8
* Utilities: :class:`SortedRelatedFieldListFilter`.
9
10
Admin classes can be created as expected:
11
12
.. code-block:: python
13
14
    from django.contrib import admin
15
    from parler.admin import TranslatableAdmin
16
    from myapp.models import Project
17
18
    class ProjectAdmin(TranslatableAdmin):
19
        list_display = ('title', 'status')
20
        fieldsets = (
21
            (None, {
22
                'fields': ('title', 'status'),
23
            }),
24
        )
25
26
    admin.site.register(Project, ProjectAdmin)
27
28
All translated fields can be used in the :attr:`~django.contrib.admin.ModelAdmin.list_display`
29
and :attr:`~django.contrib.admin.ModelAdmin.fieldsets` like normal fields.
30
31
While almost every admin feature just works, there are a few special cases to take care of:
32
33
* The :attr:`~django.contrib.admin.ModelAdmin.search_fields` needs the actual ORM fields.
34
* The :attr:`~django.contrib.admin.ModelAdmin.prepopulated_fields` needs to be replaced with a call
35
  to :func:`~django.contrib.admin.ModelAdmin.get_prepopulated_fields`.
36
37
See the :ref:`admin compatibility page <admin-compat>` for details.
38
"""
39
from __future__ import unicode_literals
40
import django
41
from django.conf import settings
42
from django.conf.urls import url
43
from django.contrib import admin
44
from django.contrib.admin.options import csrf_protect_m, BaseModelAdmin, InlineModelAdmin
45
try:
46
    from django.contrib.admin.utils import get_deleted_objects, unquote
47
except ImportError:
48
    from django.contrib.admin.util import get_deleted_objects, unquote
49
from django.core.exceptions import PermissionDenied, ImproperlyConfigured
50
from django.core.urlresolvers import reverse
51
from django.db import router
52
from django.forms import Media
53
from django.http import HttpResponseRedirect, Http404, HttpRequest
54
from django.shortcuts import render
55
from django.utils.encoding import iri_to_uri, force_text
56
from django.utils.functional import lazy
57
from django.utils.html import conditional_escape, escape
58
from django.utils.http import urlencode
59
from django.utils.translation import ugettext_lazy as _, get_language
60
from django.utils import six
61
from parler import appsettings
62
from parler.forms import TranslatableModelForm, TranslatableBaseInlineFormSet
63
from parler.managers import TranslatableQuerySet
64
from parler.models import TranslatableModelMixin
65
from parler.utils.compat import transaction_atomic, add_preserved_filters
66
from parler.utils.i18n import get_language_title, is_multilingual_project
67
from parler.utils.views import get_language_parameter, get_language_tabs
68
from parler.utils.template import select_template_name
69
70
# Code partially taken from django-hvad
71
# which is (c) 2011, Jonas Obrist, BSD licensed
72
73
__all__ = (
74
    'BaseTranslatableAdmin',
75
    'TranslatableAdmin',
76
    'TranslatableInlineModelAdmin',
77
    'TranslatableStackedInline',
78
    'TranslatableTabularInline',
79
    'SortedRelatedFieldListFilter',
80
)
81
82
_language_media = Media(css={
83
    'all': ('parler/admin/parler_admin.css',)
84
})
85
_language_prepopulated_media = _language_media + Media(js=(
86
    'admin/js/urlify.js',
87
    'admin/js/prepopulate.min.js'
88
))
89
90
_fakeRequest = HttpRequest()
91
92
93
class BaseTranslatableAdmin(BaseModelAdmin):
94
    """
95
    The shared code between the regular model admin and inline classes.
96
    """
97
    #: The form to use for the model.
98
    form = TranslatableModelForm
99
100
    #: The URL parameter for the language value.
101
    query_language_key = 'language'
102
103
    @property
104
    def media(self):
105
        # Currently, `prepopulated_fields` can't be used because it breaks the admin validation.
106
        # TODO: as a fix TranslatedFields should become a RelatedField on the shared model (may also support ORM queries)
107
        # As workaround, declare the fields in get_prepopulated_fields() and we'll provide the admin media automatically.
108
        has_prepoplated = len(self.get_prepopulated_fields(_fakeRequest))
109
        base_media = super(BaseTranslatableAdmin, self).media
110
        if has_prepoplated:
111
            return base_media + _language_prepopulated_media
112
        else:
113
            return base_media + _language_media
114
115
    def _has_translatable_model(self):
116
        # Allow fallback to regular models when needed.
117
        return issubclass(self.model, TranslatableModelMixin)
118
119
    def _language(self, request, obj=None):
120
        """
121
        Get the language parameter from the current request.
122
        """
123
        return get_language_parameter(request, self.query_language_key)
124
125
    def get_form_language(self, request, obj=None):
126
        """
127
        Return the current language for the currently displayed object fields.
128
        """
129
        if obj is not None:
130
            return obj.get_current_language()
131
        else:
132
            return self._language(request)
133
134
    def get_queryset_language(self, request):
135
        """
136
        Return the language to use in the queryset.
137
        """
138
        if not is_multilingual_project():
139
            # Make sure the current translations remain visible, not the dynamically set get_language() value.
140
            return appsettings.PARLER_LANGUAGES.get_default_language()
141
        else:
142
            # Allow to adjust to current language
143
            # This is overwritten for the inlines, which follow the primary object.
144
            return get_language()
145
146
    def get_queryset(self, request):
147
        """
148
        Make sure the current language is selected.
149
        """
150
        if django.VERSION >= (1, 6):
151
            qs = super(BaseTranslatableAdmin, self).get_queryset(request)
152
        else:
153
            qs = super(BaseTranslatableAdmin, self).queryset(request)
154
155
        if self._has_translatable_model():
156
            if not isinstance(qs, TranslatableQuerySet):
157
                raise ImproperlyConfigured("{0} class does not inherit from TranslatableQuerySet".format(qs.__class__.__name__))
158
159
            # Apply a consistent language to all objects.
160
            qs_language = self.get_queryset_language(request)
161
            if qs_language:
162
                qs = qs.language(qs_language)
163
164
        return qs
165
166
    # For Django 1.5
167
    queryset = get_queryset
168
169
    def get_language_tabs(self, request, obj, available_languages, css_class=None):
170
        """
171
        Determine the language tabs to show.
172
        """
173
        current_language = self.get_form_language(request, obj)
174
        return get_language_tabs(request, current_language, available_languages, css_class=css_class)
175
176
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
        ))
588
589
590
_lazy_select_template_name = lazy(select_template_name, six.text_type)
591
592
593
class TranslatableInlineModelAdmin(BaseTranslatableAdmin, InlineModelAdmin):
594
    """
595
    Base class for inline models.
596
    """
597
    #: The form to use.
598
    form = TranslatableModelForm
599
    #: The formset to use.
600
    formset = TranslatableBaseInlineFormSet
601
602
    @property
603
    def inline_tabs(self):
604
        """
605
        Whether to show inline tabs, can be set as attribute on the inline.
606
        """
607
        return not self._has_translatable_parent_model()
608
609
    def _has_translatable_parent_model(self):
610
        # Allow fallback to regular models when needed.
611
        return issubclass(self.parent_model, TranslatableModelMixin)
612
613
    def get_queryset_language(self, request):
614
        if not is_multilingual_project():
615
            # Make sure the current translations remain visible, not the dynamically set get_language() value.
616
            return appsettings.PARLER_LANGUAGES.get_default_language()
617
        else:
618
            # Set the initial language for fetched objects.
619
            # This is needed for the TranslatableInlineModelAdmin
620
            return self._language(request)
621
622
    def get_formset(self, request, obj=None, **kwargs):
623
        """
624
        Return the formset, and provide the language information to the formset.
625
        """
626
        FormSet = super(TranslatableInlineModelAdmin, self).get_formset(request, obj, **kwargs)
627
        # Existing objects already got the language code from the queryset().language() method.
628
        # For new objects, the language code should be set here.
629
        FormSet.language_code = self.get_form_language(request, obj)
630
631
        if self.inline_tabs:
632
            # Need to pass information to the template, this can only happen via the FormSet object.
633
            available_languages = self.get_available_languages(obj, FormSet)
634
            FormSet.language_tabs = self.get_language_tabs(request, obj, available_languages, css_class='parler-inline-language-tabs')
635
            FormSet.language_tabs.allow_deletion = self._has_translatable_parent_model()   # Views not available otherwise.
636
637
        return FormSet
638
639
    def get_form_language(self, request, obj=None):
640
        """
641
        Return the current language for the currently displayed object fields.
642
        """
643
        if self._has_translatable_parent_model():
644
            return super(TranslatableInlineModelAdmin, self).get_form_language(request, obj=obj)
645
        else:
646
            # Follow the ?language parameter
647
            return self._language(request)
648
649
    def get_available_languages(self, obj, formset):
650
        """
651
        Fetching the available inline languages as queryset.
652
        """
653
        if obj:
654
            # Inlines dictate language code, not the parent model.
655
            # Hence, not looking at obj.get_available_languages(), but see what languages
656
            # are used by the inline objects that point to it.
657
            filter = {
658
                'master__{0}'.format(formset.fk.name): obj
659
            }
660
            return self.model._parler_meta.root_model.objects.using(obj._state.db).filter(**filter) \
661
                   .values_list('language_code', flat=True).distinct().order_by('language_code')
662
        else:
663
            return self.model._parler_meta.root_model.objects.none()
664
665
666
class TranslatableStackedInline(TranslatableInlineModelAdmin):
667
    """
668
    The inline class for stacked layout.
669
    """
670
    @property
671
    def template(self):
672
        if self.inline_tabs:
673
            return 'admin/parler/edit_inline/stacked_tabs.html'
674
        else:
675
            # Admin default
676
            return 'admin/edit_inline/stacked.html'
677
678
679
class TranslatableTabularInline(TranslatableInlineModelAdmin):
680
    """
681
    The inline class for tabular layout.
682
    """
683
    @property
684
    def template(self):
685
        if self.inline_tabs:
686
            return 'admin/parler/edit_inline/tabular_tabs.html'
687
        else:
688
            # Admin default
689
            return 'admin/edit_inline/tabular.html'
690
691
692
class SortedRelatedFieldListFilter(admin.RelatedFieldListFilter):
693
    """
694
    Override the standard :class:`~django.contrib.admin.RelatedFieldListFilter`,
695
    to sort the values after rendering their ``__unicode__()`` values.
696
    This can be used for translated models, which are difficult to sort beforehand.
697
    Usage:
698
699
    .. code-block:: python
700
701
        from django.contrib import admin
702
        from parler.admin import SortedRelatedFieldListFilter
703
704
        class MyAdmin(admin.ModelAdmin):
705
706
            list_filter = (
707
                ('related_field_name', SortedRelatedFieldListFilter),
708
            )
709
    """
710
711
    def __init__(self, *args, **kwargs):
712
        super(SortedRelatedFieldListFilter, self).__init__(*args, **kwargs)
713
        self.lookup_choices = sorted(self.lookup_choices, key=lambda a: a[1].lower())
714
715
716
if django.VERSION >= (1, 7):
717
    def _get_model_meta(opts):
718
        return opts.app_label, opts.model_name
719
else:
720
    def _get_model_meta(opts):
721
        return opts.app_label, opts.module_name
722