ObjectLanguageNode.__init__()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
rs 10
cc 1
1
import inspect
2
from django.core.urlresolvers import reverse
3
from django.template import Node, Library, TemplateSyntaxError
4
from django.utils.encoding import force_text
5
from django.utils.translation import get_language
6
from django.utils import six
7
from parler.models import TranslatableModel, TranslationDoesNotExist
8
from parler.utils.context import switch_language, smart_override
9
10
register = Library()
11
12
13
class ObjectLanguageNode(Node):
14
15
    def __init__(self, nodelist, object_var, language_var=None):
16
        self.nodelist = nodelist  # This name is special in the Node baseclass
17
        self.object_var = object_var
18
        self.language_var = language_var
19
20
    def render(self, context):
21
        # Read context data
22
        object = self.object_var.resolve(context)
23
        new_language = self.language_var.resolve(context) if self.language_var else get_language()
24
        if not isinstance(object, TranslatableModel):
25
            raise TemplateSyntaxError("Object '{0}' is not an instance of TranslableModel".format(object))
26
27
        with switch_language(object, new_language):
28
            # Render contents inside
29
            output = self.nodelist.render(context)
30
31
        return output
32
33
34
@register.tag
35
def objectlanguage(parser, token):
36
    """
37
    Template tag to switch an object language
38
    Example::
39
40
        {% objectlanguage object "en" %}
41
          {{ object.title }}
42
        {% endobjectlanguage %}
43
44
    A TranslatedObject is not affected by the ``{% language .. %}`` tag
45
    as it maintains it's own state. This tag temporary switches the object state.
46
47
    Note that using this tag is not thread-safe if the object is shared between threads.
48
    It temporary changes the current language of the object.
49
    """
50
    bits = token.split_contents()
51
    if len(bits) == 2:
52
        object_var = parser.compile_filter(bits[1])
53
        language_var = None
54
    elif len(bits) == 3:
55
        object_var = parser.compile_filter(bits[1])
56
        language_var = parser.compile_filter(bits[2])
57
    else:
58
        raise TemplateSyntaxError("'%s' takes one argument (object) and has one optional argument (language)" % bits[0])
59
60
    nodelist = parser.parse(('endobjectlanguage',))
61
    parser.delete_first_token()
62
    return ObjectLanguageNode(nodelist, object_var, language_var)
63
64
65
@register.assignment_tag(takes_context=True)
66
def get_translated_url(context, lang_code, object=None):
67
    """
68
    Get the proper URL for this page in a different language.
69
70
    Note that this algorithm performs a "best effect" approach to give a proper URL.
71
    To make sure the proper view URL is returned, add the :class:`~parler.views.ViewUrlMixin` to your view.
72
73
    Example, to build a language menu::
74
75
        <ul>
76
            {% for lang_code, title in LANGUAGES %}
77
                {% get_language_info for lang_code as lang %}
78
                {% get_translated_url lang_code as tr_url %}
79
                {% if tr_url %}<li{% if lang_code == LANGUAGE_CODE %} class="is-selected"{% endif %}><a href="{{ tr_url }}" hreflang="{{ lang_code }}">{{ lang.name_local|capfirst }}</a></li>{% endif %}
80
            {% endfor %}
81
        </ul>
82
83
    Or to inform search engines about the translated pages::
84
85
       {% for lang_code, title in LANGUAGES %}
86
           {% get_translated_url lang_code as tr_url %}
87
           {% if tr_url %}<link rel="alternate" hreflang="{{ lang_code }}" href="{{ tr_url }}" />{% endif %}
88
       {% endfor %}
89
90
    Note that using this tag is not thread-safe if the object is shared between threads.
91
    It temporary changes the current language of the view object.
92
93
    The query string of the current page is preserved in the translated URL.
94
    When the ``object`` variable is explicitly provided however, the query string will not be added.
95
    In such situation, *django-parler* assumes that the object may point to a completely different page,
96
    hence to query string is added.
97
    """
98
    view = context.get('view', None)
99
    request = context['request']
100
101
    if object is not None:
102
        # Cannot reliable determine whether the current page is being translated,
103
        # or the template code provides a custom object to translate.
104
        # Hence, not passing the querystring of the current page
105
        qs = ''
106
    else:
107
        # Try a few common object variables, the SingleObjectMixin object,
108
        # The Django CMS "current_page" variable, or the "page" from django-fluent-pages and Mezzanine.
109
        # This makes this tag work with most CMSes out of the box.
110
        object = context.get('object', None) \
111
              or context.get('current_page', None) \
112
              or context.get('page', None)
113
114
        # Assuming current page, preserve query string filters.
115
        qs = request.META.get('QUERY_STRING', '')
116
117
    try:
118
        if view is not None:
119
            # Allow a view to specify what the URL should be.
120
            # This handles situations where the slug might be translated,
121
            # and gives you complete control over the results of this template tag.
122
            get_view_url = getattr(view, 'get_view_url', None)
123
            if get_view_url:
124
                with smart_override(lang_code):
125
                    return _url_qs(view.get_view_url(), qs)
126
127
            # Now, the "best effort" part starts.
128
            # See if it's a DetailView that exposes the object.
129
            if object is None:
130
                object = getattr(view, 'object', None)
131
132
        if object is not None and hasattr(object, 'get_absolute_url'):
133
            # There is an object, get the URL in the different language.
134
            # NOTE: this *assumes* that there is a detail view, not some edit view.
135
            # In such case, a language menu would redirect a user from the edit page
136
            # to a detail page; which is still way better a 404 or homepage.
137
            if isinstance(object, TranslatableModel):
138
                # Need to handle object URL translations.
139
                # Just using smart_override() should be enough, as a translated object
140
                # should use `switch_language(self)` internally before returning an URL.
141
                # However, it doesn't hurt to help a bit here.
142
                with switch_language(object, lang_code):
143
                    return _url_qs(object.get_absolute_url(), qs)
144
            else:
145
                # Always switch the language before resolving, so i18n_patterns() are supported.
146
                with smart_override(lang_code):
147
                    return _url_qs(object.get_absolute_url(), qs)
148
    except TranslationDoesNotExist:
149
        # Typically projects have a fallback language, so even unknown languages will return something.
150
        # This either means fallbacks are disabled, or the fallback language is not found!
151
        return ''
152
153
    # Just reverse the current URL again in a new language, and see where we end up.
154
    # This doesn't handle translated slugs, but will resolve to the proper view name.
155
    resolver_match = _get_resolver_match(request)
156
    if resolver_match is None:
157
        # Can't resolve the page itself, the page is apparently a 404.
158
        # This can also happen for the homepage in an i18n_patterns situation.
159
        return ''
160
161
    with smart_override(lang_code):
162
        clean_kwargs = _cleanup_urlpattern_kwargs(resolver_match.kwargs)
163
        return _url_qs(reverse(resolver_match.view_name, args=resolver_match.args, kwargs=clean_kwargs, current_app=resolver_match.app_name), qs)
164
165
166
def _url_qs(url, qs):
167
    if qs and '?' not in url:
168
        return force_text('{0}?{1}').format(url, qs)
169
    else:
170
        return force_text(url)
171
172
173
@register.filter
174
def get_translated_field(object, field):
175
    """
176
    Fetch a translated field in a thread-safe way, using the current language.
177
    Example::
178
179
        {% language 'en' %}{{ object|get_translated_field:'name' }}{% endlanguage %}
180
    """
181
    return object.safe_translation_getter(field, language_code=get_language())
182
183
184
def _cleanup_urlpattern_kwargs(kwargs):
185
    # For old function-based views, the url kwargs can pass extra arguments to the view.
186
    # Although these arguments don't have to be passed back to reverse(),
187
    # it's not a problem because the reverse() function just ignores them as there is no match.
188
    # However, for class values, an exception occurs because reverse() wants to force_text() them.
189
    # Hence, remove the kwargs to avoid internal server errors on some exotic views.
190
    return dict((k, v) for k, v in six.iteritems(kwargs) if not inspect.isclass(v))
191
192
193
# request only provides `resolver_match` from 1.5 onwards, it's by BaseHandler.get_response()
194
def _get_resolver_match(request):
195
    try:
196
        return request.resolver_match
197
    except AttributeError:
198
        # Django < 1.5
199
        from django.core.urlresolvers import resolve
200
        return resolve(request.path_info)
201