|
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
|
|
|
|