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