TranslatableAdmin.change_form_template()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
c 0
b 0
f 0
rs 9.4285
cc 2
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, cached_property
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
if django.VERSION >= (1, 9) or 'flat' in settings.INSTALLED_APPS:
83
    _language_media = Media(css={
84
        'all': (
85
            'parler/admin/parler_admin.css',
86
            'parler/admin/parler_admin_flat.css',
87
        )
88
    })
89
else:
90
    _language_media = Media(css={
91
        'all': ('parler/admin/parler_admin.css',)
92
    })
93
94
_language_prepopulated_media = _language_media + Media(js=(
95
    'admin/js/urlify.js',
96
    'admin/js/prepopulate.min.js'
97
))
98
99
_fakeRequest = HttpRequest()
100
101
102
class BaseTranslatableAdmin(BaseModelAdmin):
103
    """
104
    The shared code between the regular model admin and inline classes.
105
    """
106
    #: The form to use for the model.
107
    form = TranslatableModelForm
108
109
    #: The URL parameter for the language value.
110
    query_language_key = 'language'
111
112
    @property
113
    def media(self):
114
        # Currently, `prepopulated_fields` can't be used because it breaks the admin validation.
115
        # TODO: as a fix TranslatedFields should become a RelatedField on the shared model (may also support ORM queries)
116
        # As workaround, declare the fields in get_prepopulated_fields() and we'll provide the admin media automatically.
117
        has_prepoplated = len(self.get_prepopulated_fields(_fakeRequest))
118
        base_media = super(BaseTranslatableAdmin, self).media
119
        if has_prepoplated:
120
            return base_media + _language_prepopulated_media
121
        else:
122
            return base_media + _language_media
123
124
    def _has_translatable_model(self):
125
        # Allow fallback to regular models when needed.
126
        return issubclass(self.model, TranslatableModelMixin)
127
128
    def _language(self, request, obj=None):
129
        """
130
        Get the language parameter from the current request.
131
        """
132
        return get_language_parameter(request, self.query_language_key)
133
134
    def get_form_language(self, request, obj=None):
135
        """
136
        Return the current language for the currently displayed object fields.
137
        """
138
        if obj is not None:
139
            return obj.get_current_language()
140
        else:
141
            return self._language(request)
142
143
    def get_queryset_language(self, request):
144
        """
145
        Return the language to use in the queryset.
146
        """
147
        if not is_multilingual_project():
148
            # Make sure the current translations remain visible, not the dynamically set get_language() value.
149
            return appsettings.PARLER_LANGUAGES.get_default_language()
150
        else:
151
            # Allow to adjust to current language
152
            # This is overwritten for the inlines, which follow the primary object.
153
            return get_language()
154
155
    def get_queryset(self, request):
156
        """
157
        Make sure the current language is selected.
158
        """
159
        if django.VERSION >= (1, 6):
160
            qs = super(BaseTranslatableAdmin, self).get_queryset(request)
161
        else:
162
            qs = super(BaseTranslatableAdmin, self).queryset(request)
163
164
        if self._has_translatable_model():
165
            if not isinstance(qs, TranslatableQuerySet):
166
                raise ImproperlyConfigured("{0} class does not inherit from TranslatableQuerySet".format(qs.__class__.__name__))
167
168
            # Apply a consistent language to all objects.
169
            qs_language = self.get_queryset_language(request)
170
            if qs_language:
171
                qs = qs.language(qs_language)
172
173
        return qs
174
175
    # For Django 1.5
176
    queryset = get_queryset
177
178
    def get_language_tabs(self, request, obj, available_languages, css_class=None):
179
        """
180
        Determine the language tabs to show.
181
        """
182
        current_language = self.get_form_language(request, obj)
183
        return get_language_tabs(request, current_language, available_languages, css_class=css_class)
184
185
186
class TranslatableAdmin(BaseTranslatableAdmin, admin.ModelAdmin):
187
    """
188
    Base class for translated admins.
189
190
    This class also works as regular admin for non TranslatableModel objects.
191
    When using this class with a non-TranslatableModel,
192
    all operations effectively become a NO-OP.
193
    """
194
    #: Whether the translations should be prefetched when displaying the 'language_column' in the list.
195
    prefetch_language_column = True
196
197
    deletion_not_allowed_template = 'admin/parler/deletion_not_allowed.html'
198
199
    #: Whether translations of inlines should also be deleted when deleting a translation.
200
    delete_inline_translations = True
201
202
    @property
203
    def change_form_template(self):
204
        """
205
        Dynamic property to support transition to regular models.
206
207
        This automatically picks ``admin/parler/change_form.html`` when the admin uses a translatable model.
208
        """
209
        if self._has_translatable_model():
210
            # While this breaks the admin template name detection,
211
            # the get_change_form_base_template() makes sure it inherits from your template.
212
            return 'admin/parler/change_form.html'
213
        else:
214
            return None # get default admin selection
215
216
    def language_column(self, object):
217
        """
218
        The language column which can be included in the ``list_display``.
219
        """
220
        return self._languages_column(object, span_classes='available-languages')  # span class for backwards compatibility
221
    language_column.allow_tags = True
222
    language_column.short_description = _("Languages")
223
224
    def all_languages_column(self, object):
225
        """
226
        The language column which can be included in the ``list_display``.
227
        It also shows untranslated languages
228
        """
229
        all_languages = [code for code, __ in settings.LANGUAGES]
230
        return self._languages_column(object, all_languages, span_classes='all-languages')
231
    all_languages_column.allow_tags = True
232
    all_languages_column.short_description = _("Languages")
233
234
    def _languages_column(self, object, all_languages=None, span_classes=''):
235
        active_languages = self.get_available_languages(object)
236
        if all_languages is None:
237
            all_languages = active_languages
238
239
        current_language = object.get_current_language()
240
        buttons = []
241
        opts = self.opts
242
        for code in (all_languages or active_languages):
243
            classes = ['lang-code']
244
            if code in active_languages:
245
                classes.append('active')
246
            else:
247
                classes.append('untranslated')
248
            if code == current_language:
249
                classes.append('current')
250
251
            info = _get_model_meta(opts)
252
            admin_url = reverse('admin:{0}_{1}_change'.format(*info), args=(object.pk,), current_app=self.admin_site.name)
253
            buttons.append('<a class="{classes}" href="{href}?language={language_code}">{title}</a>'.format(
254
                language_code=code,
255
                classes=' '.join(classes),
256
                href=escape(admin_url),
257
                title=conditional_escape(self.get_language_short_title(code))
258
           ))
259
        return '<span class="language-buttons {0}">{1}</span>'.format(
260
            span_classes,
261
            ' '.join(buttons)
262
        )
263
264
    def get_language_short_title(self, language_code):
265
        """
266
        Hook for allowing to change the title in the :func:`language_column` of the list_display.
267
        """
268
        # Show language codes in uppercase by default.
269
        # This avoids a general text-transform CSS rule,
270
        # that might conflict with showing longer titles for a language instead of the code.
271
        # (e.g. show "Global" instead of "EN")
272
        return language_code.upper()
273
274
    def get_available_languages(self, obj):
275
        """
276
        Fetching the available languages as queryset.
277
        """
278
        if obj:
279
            return obj.get_available_languages()
280
        else:
281
            return self.model._parler_meta.root_model.objects.none()
282
283
    def get_queryset(self, request):
284
        qs = super(TranslatableAdmin, self).get_queryset(request)
285
286
        if self.prefetch_language_column:
287
            # When the available languages are shown in the listing, prefetch available languages.
288
            # This avoids an N-query issue because each row needs the available languages.
289
            list_display = self.get_list_display(request)
290
            if 'language_column' in list_display or 'all_languages_column' in list_display:
291
                qs = qs.prefetch_related(self.model._parler_meta.root_rel_name)
292
293
        return qs
294
295
    def get_object(self, request, object_id, *args, **kwargs):
296
        """
297
        Make sure the object is fetched in the correct language.
298
        """
299
        # The args/kwargs are to support Django 1.8, which adds a from_field parameter
300
        obj = super(TranslatableAdmin, self).get_object(request, object_id, *args, **kwargs)
301
302
        if obj is not None and self._has_translatable_model():  # Allow fallback to regular models.
303
            obj.set_current_language(self._language(request, obj), initialize=True)
304
305
        return obj
306
307
    def get_form(self, request, obj=None, **kwargs):
308
        """
309
        Pass the current language to the form.
310
        """
311
        form_class = super(TranslatableAdmin, self).get_form(request, obj, **kwargs)
312
        if self._has_translatable_model():
313
            form_class.language_code = self.get_form_language(request, obj)
314
315
        return form_class
316
317
    def get_urls(self):
318
        """
319
        Add a delete-translation view.
320
        """
321
        urlpatterns = super(TranslatableAdmin, self).get_urls()
322
        if not self._has_translatable_model():
323
            return urlpatterns
324
        else:
325
            opts = self.model._meta
326
            info = _get_model_meta(opts)
327
328
            if django.VERSION < (1, 9):
329
                delete_path = url(
330
                    r'^(.+)/delete-translation/(.+)/$',
331
                    self.admin_site.admin_view(self.delete_translation),
332
                    name='{0}_{1}_delete_translation'.format(*info)
333
                )
334
            else:
335
                delete_path = url(
336
                    r'^(.+)/change/delete-translation/(.+)/$',
337
                    self.admin_site.admin_view(self.delete_translation),
338
                    name='{0}_{1}_delete_translation'.format(*info)
339
                )
340
341
            return [delete_path] + urlpatterns
342
343
    def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
344
        """
345
        Insert the language tabs.
346
        """
347
        if self._has_translatable_model():
348
            lang_code = self.get_form_language(request, obj)
349
            lang = get_language_title(lang_code)
350
351
            available_languages = self.get_available_languages(obj)
352
            language_tabs = self.get_language_tabs(request, obj, available_languages)
353
            context['language_tabs'] = language_tabs
354
            if language_tabs:
355
                context['title'] = '%s (%s)' % (context['title'], lang)
356
            if not language_tabs.current_is_translated:
357
                add = True  # lets prepopulated_fields_js work.
358
359
            # Patch form_url to contain the "language" GET parameter.
360
            # Otherwise AdminModel.render_change_form will clean the URL
361
            # and remove the "language" when coming from a filtered object
362
            # list causing the wrong translation to be changed.
363
364
            params = request.GET.dict()
365
            params['language'] = lang_code
366
            form_url = add_preserved_filters({
367
                'preserved_filters': urlencode(params),
368
                'opts': self.model._meta
369
            }, form_url)
370
371
        # django-fluent-pages uses the same technique
372
        if 'default_change_form_template' not in context:
373
            context['default_change_form_template'] = self.default_change_form_template
374
375
        #context['base_template'] = self.get_change_form_base_template()
376
        return super(TranslatableAdmin, self).render_change_form(request, context, add, change, form_url, obj)
377
378
    def response_add(self, request, obj, post_url_continue=None):
379
        # Minor behavior difference for Django 1.4
380
        if post_url_continue is None and django.VERSION < (1, 5):
381
            post_url_continue = '../%s/'
382
383
        # Make sure ?language=... is included in the redirects.
384
        redirect = super(TranslatableAdmin, self).response_add(request, obj, post_url_continue)
385
        return self._patch_redirect(request, obj, redirect)
386
387
    def response_change(self, request, obj):
388
        # Make sure ?language=... is included in the redirects.
389
        redirect = super(TranslatableAdmin, self).response_change(request, obj)
390
        return self._patch_redirect(request, obj, redirect)
391
392
    def _patch_redirect(self, request, obj, redirect):
393
        if redirect.status_code not in (301, 302):
394
            return redirect  # a 200 response likely.
395
396
        uri = iri_to_uri(request.path)
397
        opts = self.model._meta
398
        info = _get_model_meta(opts)
399
400
        # Pass ?language=.. to next page.
401
        language = request.GET.get(self.query_language_key)
402
        if language:
403
            continue_urls = (uri, "../add/", reverse('admin:{0}_{1}_add'.format(*info)))
404
            if redirect['Location'] in continue_urls and self.query_language_key in request.GET:
405
                # "Save and add another" / "Save and continue" URLs
406
                redirect['Location'] += "?{0}={1}".format(self.query_language_key, language)
407
        return redirect
408
409
    @csrf_protect_m
410
    @transaction_atomic
411
    def delete_translation(self, request, object_id, language_code):
412
        """
413
        The 'delete translation' admin view for this model.
414
        """
415
        opts = self.model._meta
416
        root_model = self.model._parler_meta.root_model
417
418
        # Get object and translation
419
        shared_obj = self.get_object(request, unquote(object_id))
420
        if shared_obj is None:
421
            raise Http404
422
423
        shared_obj.set_current_language(language_code)
424
        try:
425
            translation = root_model.objects.get(master=shared_obj, language_code=language_code)
426
        except root_model.DoesNotExist:
427
            raise Http404
428
429
        if not self.has_delete_permission(request, translation):
430
            raise PermissionDenied
431
432
        if len(self.get_available_languages(shared_obj)) <= 1:
433
            return self.deletion_not_allowed(request, translation, language_code)
434
435
        # Populate deleted_objects, a data structure of all related objects that
436
        # will also be deleted.
437
438
        using = router.db_for_write(root_model)  # NOTE: all same DB for now.
439
        lang = get_language_title(language_code)
440
441
        # There are potentially multiple objects to delete;
442
        # the translation object at the base level,
443
        # and additional objects that can be added by inherited models.
444
        deleted_objects = []
445
        perms_needed = False
446
        protected = []
447
448
        # Extend deleted objects with the inlines.
449
        for qs in self.get_translation_objects(request, translation.language_code, obj=shared_obj, inlines=self.delete_inline_translations):
450
            if isinstance(qs, (list, tuple)):
451
                qs_opts = qs[0]._meta
452
            else:
453
                qs_opts = qs.model._meta
454
455
            deleted_result = get_deleted_objects(qs, qs_opts, request.user, self.admin_site, using)
456
            if django.VERSION >= (1, 8):
457
                (del2, model_counts, perms2, protected2) = deleted_result
458
            else:
459
                (del2, perms2, protected2) = deleted_result
460
461
            deleted_objects += del2
462
            perms_needed = perms_needed or perms2
463
            protected += protected2
464
465
        if request.POST: # The user has already confirmed the deletion.
466
            if perms_needed:
467
                raise PermissionDenied
468
            obj_display = _('{0} translation of {1}').format(lang, force_text(translation))  # in hvad: (translation.master)
469
470
            self.log_deletion(request, translation, obj_display)
471
            self.delete_model_translation(request, translation)
472
            self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % dict(
473
                name=force_text(opts.verbose_name), obj=force_text(obj_display)
474
            ))
475
476
            if self.has_change_permission(request, None):
477
                info = _get_model_meta(opts)
478
                return HttpResponseRedirect(reverse('admin:{0}_{1}_change'.format(*info), args=(object_id,), current_app=self.admin_site.name))
479
            else:
480
                return HttpResponseRedirect(reverse('admin:index', current_app=self.admin_site.name))
481
482
        object_name = _('{0} Translation').format(force_text(opts.verbose_name))
483
        if perms_needed or protected:
484
            title = _("Cannot delete %(name)s") % {"name": object_name}
485
        else:
486
            title = _("Are you sure?")
487
488
        context = {
489
            "title": title,
490
            "object_name": object_name,
491
            "object": translation,
492
            "deleted_objects": deleted_objects,
493
            "perms_lacking": perms_needed,
494
            "protected": protected,
495
            "opts": opts,
496
            "app_label": opts.app_label,
497
        }
498
499
        return render(request, self.delete_confirmation_template or [
500
            "admin/%s/%s/delete_confirmation.html" % (opts.app_label, opts.object_name.lower()),
501
            "admin/%s/delete_confirmation.html" % opts.app_label,
502
            "admin/delete_confirmation.html"
503
        ], context)
504
505
    def deletion_not_allowed(self, request, obj, language_code):
506
        """
507
        Deletion-not-allowed view.
508
        """
509
        opts = self.model._meta
510
        context = {
511
            'object': obj.master,
512
            'language_code': language_code,
513
            'opts': opts,
514
            'app_label': opts.app_label,
515
            'language_name': get_language_title(language_code),
516
            'object_name': force_text(opts.verbose_name)
517
        }
518
        return render(request, self.deletion_not_allowed_template, context)
519
520
    def delete_model_translation(self, request, translation):
521
        """
522
        Hook for deleting a translation.
523
        This calls :func:`get_translation_objects` to collect all related objects for the translation.
524
        By default, that includes the translations for inline objects.
525
        """
526
        master = translation.master
527
        for qs in self.get_translation_objects(request, translation.language_code, obj=master, inlines=self.delete_inline_translations):
528
            if isinstance(qs, (tuple, list)):
529
                # The objects are deleted one by one.
530
                # This triggers the post_delete signals and such.
531
                for obj in qs:
532
                    obj.delete()
533
            else:
534
                # Also delete translations of inlines which the user has access to.
535
                # This doesn't trigger signals, just like the regular
536
                qs.delete()
537
538
    def get_translation_objects(self, request, language_code, obj=None, inlines=True):
539
        """
540
        Return all objects that should be deleted when a translation is deleted.
541
        This method can yield all QuerySet objects or lists for the objects.
542
        """
543
        if obj is not None:
544
            # A single model can hold multiple TranslatedFieldsModel objects.
545
            # Return them all.
546
            for translations_model in obj._parler_meta.get_all_models():
547
                try:
548
                    translation = translations_model.objects.get(master=obj, language_code=language_code)
549
                except translations_model.DoesNotExist:
550
                    continue
551
                yield [translation]
552
553
        if inlines:
554
            for inline, qs in self._get_inline_translations(request, language_code, obj=obj):
555
                yield qs
556
557
    def _get_inline_translations(self, request, language_code, obj=None):
558
        """
559
        Fetch the inline translations
560
        """
561
        # django 1.4 do not accept the obj parameter
562
        if django.VERSION < (1, 5):
563
            inline_instances = self.get_inline_instances(request)
564
        else:
565
            inline_instances = self.get_inline_instances(request, obj=obj)
566
567
        for inline in inline_instances:
568
            if issubclass(inline.model, TranslatableModelMixin):
569
                # leverage inlineformset_factory() to find the ForeignKey.
570
                # This also resolves the fk_name if it's set.
571
                fk = inline.get_formset(request, obj).fk
572
573
                rel_name = 'master__{0}'.format(fk.name)
574
                filters = {
575
                    'language_code': language_code,
576
                    rel_name: obj
577
                }
578
579
                for translations_model in inline.model._parler_meta.get_all_models():
580
                    qs = translations_model.objects.filter(**filters)
581
                    if obj is not None:
582
                        qs = qs.using(obj._state.db)
583
584
                    yield inline, qs
585
586
    @cached_property
587
    def default_change_form_template(self):
588
        """
589
        Determine what the actual `change_form_template` should be.
590
        """
591
        opts = self.model._meta
592
        app_label = opts.app_label
593
        return select_template_name((
594
            "admin/{0}/{1}/change_form.html".format(app_label, opts.object_name.lower()),
595
            "admin/{0}/change_form.html".format(app_label),
596
            "admin/change_form.html"
597
        ))
598
599
600
class TranslatableInlineModelAdmin(BaseTranslatableAdmin, InlineModelAdmin):
601
    """
602
    Base class for inline models.
603
    """
604
    #: The form to use.
605
    form = TranslatableModelForm
606
    #: The formset to use.
607
    formset = TranslatableBaseInlineFormSet
608
609
    @property
610
    def inline_tabs(self):
611
        """
612
        Whether to show inline tabs, can be set as attribute on the inline.
613
        """
614
        return not self._has_translatable_parent_model()
615
616
    def _has_translatable_parent_model(self):
617
        # Allow fallback to regular models when needed.
618
        return issubclass(self.parent_model, TranslatableModelMixin)
619
620
    def get_queryset_language(self, request):
621
        if not is_multilingual_project():
622
            # Make sure the current translations remain visible, not the dynamically set get_language() value.
623
            return appsettings.PARLER_LANGUAGES.get_default_language()
624
        else:
625
            # Set the initial language for fetched objects.
626
            # This is needed for the TranslatableInlineModelAdmin
627
            return self._language(request)
628
629
    def get_formset(self, request, obj=None, **kwargs):
630
        """
631
        Return the formset, and provide the language information to the formset.
632
        """
633
        FormSet = super(TranslatableInlineModelAdmin, self).get_formset(request, obj, **kwargs)
634
        # Existing objects already got the language code from the queryset().language() method.
635
        # For new objects, the language code should be set here.
636
        FormSet.language_code = self.get_form_language(request, obj)
637
638
        if self.inline_tabs:
639
            # Need to pass information to the template, this can only happen via the FormSet object.
640
            available_languages = self.get_available_languages(obj, FormSet)
641
            FormSet.language_tabs = self.get_language_tabs(request, obj, available_languages, css_class='parler-inline-language-tabs')
642
            FormSet.language_tabs.allow_deletion = self._has_translatable_parent_model()   # Views not available otherwise.
643
644
        return FormSet
645
646
    def get_form_language(self, request, obj=None):
647
        """
648
        Return the current language for the currently displayed object fields.
649
        """
650
        if self._has_translatable_parent_model():
651
            return super(TranslatableInlineModelAdmin, self).get_form_language(request, obj=obj)
652
        else:
653
            # Follow the ?language parameter
654
            return self._language(request)
655
656
    def get_available_languages(self, obj, formset):
657
        """
658
        Fetching the available inline languages as queryset.
659
        """
660
        if obj:
661
            # Inlines dictate language code, not the parent model.
662
            # Hence, not looking at obj.get_available_languages(), but see what languages
663
            # are used by the inline objects that point to it.
664
            filter = {
665
                'master__{0}'.format(formset.fk.name): obj
666
            }
667
            return self.model._parler_meta.root_model.objects.using(obj._state.db).filter(**filter) \
668
                   .values_list('language_code', flat=True).distinct().order_by('language_code')
669
        else:
670
            return self.model._parler_meta.root_model.objects.none()
671
672
673
class TranslatableStackedInline(TranslatableInlineModelAdmin):
674
    """
675
    The inline class for stacked layout.
676
    """
677
    @property
678
    def template(self):
679
        if self.inline_tabs:
680
            return 'admin/parler/edit_inline/stacked_tabs.html'
681
        else:
682
            # Admin default
683
            return 'admin/edit_inline/stacked.html'
684
685
686
class TranslatableTabularInline(TranslatableInlineModelAdmin):
687
    """
688
    The inline class for tabular layout.
689
    """
690
    @property
691
    def template(self):
692
        if self.inline_tabs:
693
            return 'admin/parler/edit_inline/tabular_tabs.html'
694
        else:
695
            # Admin default
696
            return 'admin/edit_inline/tabular.html'
697
698
699
class SortedRelatedFieldListFilter(admin.RelatedFieldListFilter):
700
    """
701
    Override the standard :class:`~django.contrib.admin.RelatedFieldListFilter`,
702
    to sort the values after rendering their ``__unicode__()`` values.
703
    This can be used for translated models, which are difficult to sort beforehand.
704
    Usage:
705
706
    .. code-block:: python
707
708
        from django.contrib import admin
709
        from parler.admin import SortedRelatedFieldListFilter
710
711
        class MyAdmin(admin.ModelAdmin):
712
713
            list_filter = (
714
                ('related_field_name', SortedRelatedFieldListFilter),
715
            )
716
    """
717
718
    def __init__(self, *args, **kwargs):
719
        super(SortedRelatedFieldListFilter, self).__init__(*args, **kwargs)
720
        self.lookup_choices = sorted(self.lookup_choices, key=lambda a: a[1].lower())
721
722
723
if django.VERSION >= (1, 7):
724
    def _get_model_meta(opts):
725
        return opts.app_label, opts.model_name
726
else:
727
    def _get_model_meta(opts):
728
        return opts.app_label, opts.module_name
729