Completed
Push — master ( 0bffe1...d6203d )
by Diederik van der
11s
created

get_translated_url()   F

Complexity

Conditions 14

Size

Total Lines 99

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 99
rs 2
cc 14

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like get_translated_url() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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