LanguageChoiceMixin   A
last analyzed

Complexity

Total Complexity 9

Size/Duplication

Total Lines 56
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 9
dl 0
loc 56
c 1
b 0
f 0
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A get_object() 0 8 2
A get_current_language() 0 9 2
A get_language_tabs() 0 11 2
A get_context_data() 0 4 1
A get_language() 0 5 1
A get_default_language() 0 7 1
1
"""
2
The views provide high-level utilities to integrate translation support into other projects.
3
4
The following mixins are available:
5
6
* :class:`ViewUrlMixin` - provide a ``get_view_url`` for the :ref:`{% get_translated_url %} <get_translated_url>` template tag.
7
* :class:`TranslatableSlugMixin` - enrich the :class:`~django.views.generic.detail.DetailView` to support translatable slugs.
8
* :class:`LanguageChoiceMixin` - add ``?language=xx`` support to a view (e.g. for editing).
9
* :class:`TranslatableModelFormMixin` - add support for translatable forms, e.g. for creating/updating objects.
10
11
The following views are available:
12
13
* :class:`TranslatableCreateView` - The :class:`~django.views.generic.edit.CreateView` with :class:`TranslatableModelFormMixin` support.
14
* :class:`TranslatableUpdateView` - The :class:`~django.views.generic.edit.UpdateView` with :class:`TranslatableModelFormMixin` support.
15
"""
16
from __future__ import unicode_literals
17
import django
18
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
19
from django.core.urlresolvers import reverse
20
from django.forms.models import modelform_factory
21
from django.http import Http404, HttpResponsePermanentRedirect
22
from django.utils import translation
23
from django.views import generic
24
from django.views.generic.edit import ModelFormMixin
25
from parler.forms import TranslatableModelForm
26
from parler.models import TranslatableModelMixin
27
from parler.utils import get_active_language_choices
28
from parler.utils.context import switch_language
29
from parler.utils.views import get_language_parameter, get_language_tabs
30
31
__all__ = (
32
    'ViewUrlMixin',
33
    'TranslatableSlugMixin',
34
    'LanguageChoiceMixin',
35
    'TranslatableModelFormMixin',
36
    'TranslatableCreateView',
37
    'TranslatableUpdateView',
38
)
39
40
41
class ViewUrlMixin(object):
42
    """
43
    Provide a ``view.get_view_url`` method in the template.
44
45
    This tells the template what the exact canonical URL should be of a view.
46
    The :ref:`{% get_translated_url %} <get_translated_url>` template tag uses this
47
    to find the proper translated URL of the current page.
48
49
    Typically, setting the :attr:`view_url_name` just works::
50
51
        class ArticleListView(ViewUrlMixin, ListView):
52
            view_url_name = 'article:list'
53
54
    The :func:`get_view_url` will use the :attr:`view_url_name` together
55
    with ``view.args`` and ``view.kwargs`` construct the URL.
56
    When some arguments are translated (e.g. a slug), the :func:`get_view_url`
57
    can be overwritten to generate the proper URL::
58
59
        from parler.views import ViewUrlMixin, TranslatableUpdateView
60
        from parler.utils.context import switch_language
61
62
        class ArticleEditView(ViewUrlMixin, TranslatableUpdateView):
63
            view_url_name = 'article:edit'
64
65
            def get_view_url(self):
66
                with switch_language(self.object, get_language()):
67
                    return reverse(self.view_url_name, kwargs={'slug': self.object.slug})
68
    """
69
    #: The default view name used by :func:`get_view_url`, which
70
    #: should correspond with the view name in the URLConf.
71
    view_url_name = None
72
73
    def get_view_url(self):
74
        """
75
        This method is used by the ``get_translated_url`` template tag.
76
77
        By default, it uses the :attr:`view_url_name` to generate an URL.
78
        When the URL ``args`` and ``kwargs`` are translatable,
79
        override this function instead to generate the proper URL.
80
        """
81
        if not self.view_url_name:
82
            # Sadly, class based views can't work with reverse(func_pointer) as that's unknown.
83
            # Neither is it possible to use resolve(self.request.path).view_name in this function as auto-detection.
84
            # This function can be called in the context of a different language.
85
            # When i18n_patterns() is applied, that resolve() will fail.
86
            #
87
            # Hence, you need to provide a "view_url_name" as static configuration option.
88
            raise ImproperlyConfigured("Missing `view_url_name` attribute on {0}".format(self.__class__.__name__))
89
90
        return reverse(self.view_url_name, args=self.args, kwargs=self.kwargs)
91
92
    if django.VERSION < (1, 5):
93
        # The `get_translated_url` tag relies on the fact that the template can access the view again.
94
        # This was not possible until Django 1.5, so provide the `ContextMixin` logic for earlier Django versions.
95
96
        def get_context_data(self, **kwargs):
97
            if 'view' not in kwargs:
98
                kwargs['view'] = self
99
            return kwargs
100
101
102
class TranslatableSlugMixin(object):
103
    """
104
    An enhancement for the :class:`~django.views.generic.DetailView` to deal with translated slugs.
105
    This view makes sure that:
106
107
    * The object is fetched in the proper translation.
108
    * The slug field is read from the translation model, instead of the shared model.
109
    * Fallback languages are handled.
110
    * Objects are not accidentally displayed in their fallback slug, but redirect to the translated slug.
111
112
    Example:
113
114
    .. code-block:: python
115
116
        class ArticleDetailView(TranslatableSlugMixin, DetailView):
117
            model = Article
118
            template_name = 'article/details.html'
119
    """
120
121
    def get_translated_filters(self, slug):
122
        """
123
        Allow passing other filters for translated fields.
124
        """
125
        return {
126
            self.get_slug_field(): slug
127
        }
128
129
    def get_language(self):
130
        """
131
        Define the language of the current view, defaults to the active language.
132
        """
133
        return translation.get_language()
134
135
    def get_language_choices(self):
136
        """
137
        Define the language choices for the view, defaults to the defined settings.
138
        """
139
        return get_active_language_choices(self.get_language())
140
141
    def dispatch(self, request, *args, **kwargs):
142
        try:
143
            return super(TranslatableSlugMixin, self).dispatch(request, *args, **kwargs)
144
        except FallbackLanguageResolved as e:
145
            # Handle the fallback language redirect for get_object()
146
            with switch_language(e.object, e.correct_language):
147
                return HttpResponsePermanentRedirect(e.object.get_absolute_url())
148
149
    def get_object(self, queryset=None):
150
        """
151
        Fetch the object using a translated slug.
152
        """
153
        if queryset is None:
154
            queryset = self.get_queryset()
155
156
        slug = self.kwargs[self.slug_url_kwarg]
157
        choices = self.get_language_choices()
158
159
        obj = None
160
        using_fallback = False
161
        prev_choices = []
162
        for lang_choice in choices:
163
            try:
164
                # Get the single item from the filtered queryset
165
                # NOTE. Explicitly set language to the state the object was fetched in.
166
                filters = self.get_translated_filters(slug=slug)
167
                obj = queryset.translated(lang_choice, **filters).language(lang_choice).get()
168
            except ObjectDoesNotExist:
169
                # Translated object not found, next object is marked as fallback.
170
                using_fallback = True
171
                prev_choices.append(lang_choice)
172
            else:
173
                break
174
175
        if obj is None:
176
            tried_msg = ", tried languages: {0}".format(", ".join(choices))
177
            error_message = translation.ugettext("No %(verbose_name)s found matching the query") % {'verbose_name': queryset.model._meta.verbose_name}
178
            raise Http404(error_message + tried_msg)
179
180
        # Object found!
181
        if using_fallback:
182
            # It could happen that objects are resolved using their fallback language,
183
            # but the actual translation also exists. Either that means this URL should
184
            # raise a 404, or a redirect could be made as service to the users.
185
            # It's possible that the old URL was active before in the language domain/subpath
186
            # when there was no translation yet.
187
            for prev_choice in prev_choices:
188
                if obj.has_translation(prev_choice):
189
                    # Only dispatch() and render_to_response() can return a valid response,
190
                    # By breaking out here, this functionality can't be broken by users overriding render_to_response()
191
                    raise FallbackLanguageResolved(obj, prev_choice)
192
193
        return obj
194
195
196
class FallbackLanguageResolved(Exception):
197
    """
198
    An object was resolved in the fallback language, while it could be in the normal language.
199
    This exception is used internally to control code flow.
200
    """
201
202
    def __init__(self, object, correct_language):
203
        self.object = object
204
        self.correct_language = correct_language
205
206
207
class LanguageChoiceMixin(object):
208
    """
209
    Mixin to add language selection support to class based views, particularly create and update views.
210
    It adds support for the ``?language=..`` parameter in the query string, and tabs in the context.
211
    """
212
    query_language_key = 'language'
213
214
    def get_object(self, queryset=None):
215
        """
216
        Assign the language for the retrieved object.
217
        """
218
        object = super(LanguageChoiceMixin, self).get_object(queryset)
219
        if isinstance(object, TranslatableModelMixin):
220
            object.set_current_language(self.get_language(), initialize=True)
221
        return object
222
223
    def get_language(self):
224
        """
225
        Get the language parameter from the current request.
226
        """
227
        return get_language_parameter(self.request, self.query_language_key, default=self.get_default_language(object=object))
228
229
    def get_default_language(self, object=None):
230
        """
231
        Return the default language to use, if no language parameter is given.
232
        By default, it uses the default parler-language.
233
        """
234
        # Some users may want to override this, to return get_language()
235
        return None
236
237
    def get_current_language(self):
238
        """
239
        Return the current language for the currently displayed object fields.
240
        This reads ``self.object.get_current_language()`` and falls back to :func:`get_language`.
241
        """
242
        if self.object is not None:
243
            return self.object.get_current_language()
244
        else:
245
            return self.get_language()
246
247
    def get_context_data(self, **kwargs):
248
        context = super(LanguageChoiceMixin, self).get_context_data(**kwargs)
249
        context['language_tabs'] = self.get_language_tabs()
250
        return context
251
252
    def get_language_tabs(self):
253
        """
254
        Determine the language tabs to show.
255
        """
256
        current_language = self.get_current_language()
257
        if self.object:
258
            available_languages = list(self.object.get_available_languages())
259
        else:
260
            available_languages = []
261
262
        return get_language_tabs(self.request, current_language, available_languages)
263
264
265
class TranslatableModelFormMixin(LanguageChoiceMixin):
266
    """
267
    Mixin to add translation support to class based views.
268
269
    For example, adding translation support to django-oscar::
270
271
        from oscar.apps.dashboard.catalogue import views as oscar_views
272
        from parler.views import TranslatableModelFormMixin
273
274
        class ProductCreateUpdateView(TranslatableModelFormMixin, oscar_views.ProductCreateUpdateView):
275
            pass
276
    """
277
278
    def get_form_class(self):
279
        """
280
        Return a ``TranslatableModelForm`` by default if no form_class is set.
281
        """
282
        super_method = super(TranslatableModelFormMixin, self).get_form_class
283
        # no "__func__" on the class level function in python 3
284
        default_method = getattr(ModelFormMixin.get_form_class, '__func__', ModelFormMixin.get_form_class)
285
        if not (super_method.__func__ is default_method):
286
            # Don't get in your way, if you've overwritten stuff.
287
            return super_method()
288
        else:
289
            # Same logic as ModelFormMixin.get_form_class, but using the right form base class.
290
            if self.form_class:
291
                return self.form_class
292
            else:
293
                model = _get_view_model(self)
294
                return modelform_factory(model, form=TranslatableModelForm)
295
296
    def get_form_kwargs(self):
297
        """
298
        Pass the current language to the form.
299
        """
300
        kwargs = super(TranslatableModelFormMixin, self).get_form_kwargs()
301
        # The TranslatableAdmin can set form.language_code, because the modeladmin always creates a fresh subclass.
302
        # If that would be done here, the original globally defined form class would be updated.
303
        kwargs['_current_language'] = self.get_form_language()
304
        return kwargs
305
306
    # Backwards compatibility
307
    # Make sure overriding get_current_language() affects get_form_language() too.
308
    def get_form_language(self):
309
        return self.get_current_language()
310
311
312
# For the lazy ones:
313
class TranslatableCreateView(TranslatableModelFormMixin, generic.CreateView):
314
    """
315
    Create view that supports translated models.
316
    This is a mix of the :class:`TranslatableModelFormMixin`
317
    and Django's :class:`~django.views.generic.edit.CreateView`.
318
    """
319
    pass
320
321
322
class TranslatableUpdateView(TranslatableModelFormMixin, generic.UpdateView):
323
    """
324
    Update view that supports translated models.
325
    This is a mix of the :class:`TranslatableModelFormMixin`
326
    and Django's :class:`~django.views.generic.edit.UpdateView`.
327
    """
328
    pass
329
330
331
def _get_view_model(self):
332
    if self.model is not None:
333
        # If a model has been explicitly provided, use it
334
        return self.model
335
    elif hasattr(self, 'object') and self.object is not None:
336
        # If this view is operating on a single object, use the class of that object
337
        return self.object.__class__
338
    else:
339
        # Try to get a queryset and extract the model class from that
340
        return self.get_queryset().model
341
342
343
# Backwards compatibility
344
TranslatableSingleObjectMixin = LanguageChoiceMixin
345