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