Completed
Pull Request — master (#126)
by Iacopo
01:02
created

parler.TranslatableAdmin.get_urls()   B

Complexity

Conditions 3

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 3
dl 0
loc 25
rs 8.8571
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
        print(object_id)
412
        if shared_obj is None:
413
            raise Http404
414
415
        shared_obj.set_current_language(language_code)
416
        try:
417
            translation = root_model.objects.get(master=shared_obj, language_code=language_code)
418
        except root_model.DoesNotExist:
419
            raise Http404
420
421
        if not self.has_delete_permission(request, translation):
422
            raise PermissionDenied
423
424
        if len(self.get_available_languages(shared_obj)) <= 1:
425
            return self.deletion_not_allowed(request, translation, language_code)
426
427
        # Populate deleted_objects, a data structure of all related objects that
428
        # will also be deleted.
429
430
        using = router.db_for_write(root_model)  # NOTE: all same DB for now.
431
        lang = get_language_title(language_code)
432
433
        # There are potentially multiple objects to delete;
434
        # the translation object at the base level,
435
        # and additional objects that can be added by inherited models.
436
        deleted_objects = []
437
        perms_needed = False
438
        protected = []
439
440
        # Extend deleted objects with the inlines.
441
        for qs in self.get_translation_objects(request, translation.language_code, obj=shared_obj, inlines=self.delete_inline_translations):
442
            if isinstance(qs, (list, tuple)):
443
                qs_opts = qs[0]._meta
444
            else:
445
                qs_opts = qs.model._meta
446
447
            deleted_result = get_deleted_objects(qs, qs_opts, request.user, self.admin_site, using)
448
            if django.VERSION >= (1, 8):
449
                (del2, model_counts, perms2, protected2) = deleted_result
450
            else:
451
                (del2, perms2, protected2) = deleted_result
452
453
            deleted_objects += del2
454
            perms_needed = perms_needed or perms2
455
            protected += protected2
456
457
        if request.POST: # The user has already confirmed the deletion.
458
            if perms_needed:
459
                raise PermissionDenied
460
            obj_display = _('{0} translation of {1}').format(lang, force_text(translation))  # in hvad: (translation.master)
461
462
            self.log_deletion(request, translation, obj_display)
463
            self.delete_model_translation(request, translation)
464
            self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % dict(
465
                name=force_text(opts.verbose_name), obj=force_text(obj_display)
466
            ))
467
468
            if self.has_change_permission(request, None):
469
                info = _get_model_meta(opts)
470
                return HttpResponseRedirect(reverse('admin:{0}_{1}_change'.format(*info), args=(object_id,), current_app=self.admin_site.name))
471
            else:
472
                return HttpResponseRedirect(reverse('admin:index', current_app=self.admin_site.name))
473
474
        object_name = _('{0} Translation').format(force_text(opts.verbose_name))
475
        if perms_needed or protected:
476
            title = _("Cannot delete %(name)s") % {"name": object_name}
477
        else:
478
            title = _("Are you sure?")
479
480
        context = {
481
            "title": title,
482
            "object_name": object_name,
483
            "object": translation,
484
            "deleted_objects": deleted_objects,
485
            "perms_lacking": perms_needed,
486
            "protected": protected,
487
            "opts": opts,
488
            "app_label": opts.app_label,
489
        }
490
491
        return render(request, self.delete_confirmation_template or [
492
            "admin/%s/%s/delete_confirmation.html" % (opts.app_label, opts.object_name.lower()),
493
            "admin/%s/delete_confirmation.html" % opts.app_label,
494
            "admin/delete_confirmation.html"
495
        ], context)
496
497
    def deletion_not_allowed(self, request, obj, language_code):
498
        """
499
        Deletion-not-allowed view.
500
        """
501
        opts = self.model._meta
502
        context = {
503
            'object': obj.master,
504
            'language_code': language_code,
505
            'opts': opts,
506
            'app_label': opts.app_label,
507
            'language_name': get_language_title(language_code),
508
            'object_name': force_text(opts.verbose_name)
509
        }
510
        return render(request, self.deletion_not_allowed_template, context)
511
512
    def delete_model_translation(self, request, translation):
513
        """
514
        Hook for deleting a translation.
515
        This calls :func:`get_translation_objects` to collect all related objects for the translation.
516
        By default, that includes the translations for inline objects.
517
        """
518
        master = translation.master
519
        for qs in self.get_translation_objects(request, translation.language_code, obj=master, inlines=self.delete_inline_translations):
520
            if isinstance(qs, (tuple, list)):
521
                # The objects are deleted one by one.
522
                # This triggers the post_delete signals and such.
523
                for obj in qs:
524
                    obj.delete()
525
            else:
526
                # Also delete translations of inlines which the user has access to.
527
                # This doesn't trigger signals, just like the regular
528
                qs.delete()
529
530
    def get_translation_objects(self, request, language_code, obj=None, inlines=True):
531
        """
532
        Return all objects that should be deleted when a translation is deleted.
533
        This method can yield all QuerySet objects or lists for the objects.
534
        """
535
        if obj is not None:
536
            # A single model can hold multiple TranslatedFieldsModel objects.
537
            # Return them all.
538
            for translations_model in obj._parler_meta.get_all_models():
539
                try:
540
                    translation = translations_model.objects.get(master=obj, language_code=language_code)
541
                except translations_model.DoesNotExist:
542
                    continue
543
                yield [translation]
544
545
        if inlines:
546
            for inline, qs in self._get_inline_translations(request, language_code, obj=obj):
547
                yield qs
548
549
    def _get_inline_translations(self, request, language_code, obj=None):
550
        """
551
        Fetch the inline translations
552
        """
553
        # django 1.4 do not accept the obj parameter
554
        if django.VERSION < (1, 5):
555
            inline_instances = self.get_inline_instances(request)
556
        else:
557
            inline_instances = self.get_inline_instances(request, obj=obj)
558
559
        for inline in inline_instances:
560
            if issubclass(inline.model, TranslatableModelMixin):
561
                # leverage inlineformset_factory() to find the ForeignKey.
562
                # This also resolves the fk_name if it's set.
563
                fk = inline.get_formset(request, obj).fk
564
565
                rel_name = 'master__{0}'.format(fk.name)
566
                filters = {
567
                    'language_code': language_code,
568
                    rel_name: obj
569
                }
570
571
                for translations_model in inline.model._parler_meta.get_all_models():
572
                    qs = translations_model.objects.filter(**filters)
573
                    if obj is not None:
574
                        qs = qs.using(obj._state.db)
575
576
                    yield inline, qs
577
578
    def get_change_form_base_template(self):
579
        """
580
        Determine what the actual `change_form_template` should be.
581
        """
582
        opts = self.model._meta
583
        app_label = opts.app_label
584
        return _lazy_select_template_name((
585
            "admin/{0}/{1}/change_form.html".format(app_label, opts.object_name.lower()),
586
            "admin/{0}/change_form.html".format(app_label),
587
            "admin/change_form.html"
588
        ))
589
590
591
_lazy_select_template_name = lazy(select_template_name, six.text_type)
592
593
594
class TranslatableInlineModelAdmin(BaseTranslatableAdmin, InlineModelAdmin):
595
    """
596
    Base class for inline models.
597
    """
598
    #: The form to use.
599
    form = TranslatableModelForm
600
    #: The formset to use.
601
    formset = TranslatableBaseInlineFormSet
602
603
    @property
604
    def inline_tabs(self):
605
        """
606
        Whether to show inline tabs, can be set as attribute on the inline.
607
        """
608
        return not self._has_translatable_parent_model()
609
610
    def _has_translatable_parent_model(self):
611
        # Allow fallback to regular models when needed.
612
        return issubclass(self.parent_model, TranslatableModelMixin)
613
614
    def get_queryset_language(self, request):
615
        if not is_multilingual_project():
616
            # Make sure the current translations remain visible, not the dynamically set get_language() value.
617
            return appsettings.PARLER_LANGUAGES.get_default_language()
618
        else:
619
            # Set the initial language for fetched objects.
620
            # This is needed for the TranslatableInlineModelAdmin
621
            return self._language(request)
622
623
    def get_formset(self, request, obj=None, **kwargs):
624
        """
625
        Return the formset, and provide the language information to the formset.
626
        """
627
        FormSet = super(TranslatableInlineModelAdmin, self).get_formset(request, obj, **kwargs)
628
        # Existing objects already got the language code from the queryset().language() method.
629
        # For new objects, the language code should be set here.
630
        FormSet.language_code = self.get_form_language(request, obj)
631
632
        if self.inline_tabs:
633
            # Need to pass information to the template, this can only happen via the FormSet object.
634
            available_languages = self.get_available_languages(obj, FormSet)
635
            FormSet.language_tabs = self.get_language_tabs(request, obj, available_languages, css_class='parler-inline-language-tabs')
636
            FormSet.language_tabs.allow_deletion = self._has_translatable_parent_model()   # Views not available otherwise.
637
638
        return FormSet
639
640
    def get_form_language(self, request, obj=None):
641
        """
642
        Return the current language for the currently displayed object fields.
643
        """
644
        if self._has_translatable_parent_model():
645
            return super(TranslatableInlineModelAdmin, self).get_form_language(request, obj=obj)
646
        else:
647
            # Follow the ?language parameter
648
            return self._language(request)
649
650
    def get_available_languages(self, obj, formset):
651
        """
652
        Fetching the available inline languages as queryset.
653
        """
654
        if obj:
655
            # Inlines dictate language code, not the parent model.
656
            # Hence, not looking at obj.get_available_languages(), but see what languages
657
            # are used by the inline objects that point to it.
658
            filter = {
659
                'master__{0}'.format(formset.fk.name): obj
660
            }
661
            return self.model._parler_meta.root_model.objects.using(obj._state.db).filter(**filter) \
662
                   .values_list('language_code', flat=True).distinct().order_by('language_code')
663
        else:
664
            return self.model._parler_meta.root_model.objects.none()
665
666
667
class TranslatableStackedInline(TranslatableInlineModelAdmin):
668
    """
669
    The inline class for stacked layout.
670
    """
671
    @property
672
    def template(self):
673
        if self.inline_tabs:
674
            return 'admin/parler/edit_inline/stacked_tabs.html'
675
        else:
676
            # Admin default
677
            return 'admin/edit_inline/stacked.html'
678
679
680
class TranslatableTabularInline(TranslatableInlineModelAdmin):
681
    """
682
    The inline class for tabular layout.
683
    """
684
    @property
685
    def template(self):
686
        if self.inline_tabs:
687
            return 'admin/parler/edit_inline/tabular_tabs.html'
688
        else:
689
            # Admin default
690
            return 'admin/edit_inline/tabular.html'
691
692
693
class SortedRelatedFieldListFilter(admin.RelatedFieldListFilter):
694
    """
695
    Override the standard :class:`~django.contrib.admin.RelatedFieldListFilter`,
696
    to sort the values after rendering their ``__unicode__()`` values.
697
    This can be used for translated models, which are difficult to sort beforehand.
698
    Usage:
699
700
    .. code-block:: python
701
702
        from django.contrib import admin
703
        from parler.admin import SortedRelatedFieldListFilter
704
705
        class MyAdmin(admin.ModelAdmin):
706
707
            list_filter = (
708
                ('related_field_name', SortedRelatedFieldListFilter),
709
            )
710
    """
711
712
    def __init__(self, *args, **kwargs):
713
        super(SortedRelatedFieldListFilter, self).__init__(*args, **kwargs)
714
        self.lookup_choices = sorted(self.lookup_choices, key=lambda a: a[1].lower())
715
716
717
if django.VERSION >= (1, 7):
718
    def _get_model_meta(opts):
719
        return opts.app_label, opts.model_name
720
else:
721
    def _get_model_meta(opts):
722
        return opts.app_label, opts.module_name
723