Completed
Pull Request — master (#135)
by Iacopo
01:03
created

TranslatableQuerySet.iterator()   A

Complexity

Conditions 4

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 14
rs 9.2
cc 4
1
"""
2
Custom generic managers
3
"""
4
import django
5
from django.core.exceptions import ImproperlyConfigured
6
from django.db import models
7
from django.db.models.query import QuerySet
8
from django.utils.translation import get_language
9
from django.utils import six
10
from parler import appsettings
11
from parler.utils import get_active_language_choices
12
13
14
class TranslatableQuerySet(QuerySet):
15
    """
16
    An enhancement of the QuerySet which sets the objects language before they are returned.
17
18
    When using this class in combination with *django-polymorphic*, make sure this
19
    class is first in the chain of inherited classes.
20
    """
21
22
    def __init__(self, *args, **kwargs):
23
        super(TranslatableQuerySet, self).__init__(*args, **kwargs)
24
        self._language = None
25
26
    def _clone(self, klass=None, setup=False, **kw):
27
        if django.VERSION < (1, 9):
28
            kw['klass'] = klass
29
            kw['setup'] = setup
30
        c = super(TranslatableQuerySet, self)._clone(**kw)
31
        c._language = self._language
32
        return c
33
34
    def create(self, **kwargs):
35
        # Pass language setting to the object, as people start assuming things
36
        # like .language('xx').create(..) which is a nice API after all.
37
        if self._language:
38
            kwargs['_current_language'] = self._language
39
        return super(TranslatableQuerySet, self).create(**kwargs)
40
41
    def language(self, language_code=None):
42
        """
43
        Set the language code to assign to objects retrieved using this QuerySet.
44
        """
45
        if language_code is None:
46
            language_code = appsettings.PARLER_LANGUAGES.get_default_language()
47
48
        self._language = language_code
49
        return self
50
51
    def translated(self, *language_codes, **translated_fields):
52
        """
53
        Only return translated objects which of the given languages.
54
55
        When no language codes are given, only the currently active language is returned.
56
57
        .. note::
58
59
            Due to Django `ORM limitations <https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships>`_,
60
            this method can't be combined with other filters that access the translated fields. As such, query the fields in one filter:
61
62
            .. code-block:: python
63
64
                qs.translated('en', name="Cheese Omelette")
65
66
            This will query the translated model for the ``name`` field.
67
        """
68
        relname = self.model._parler_meta.root_rel_name
69
70
        if not language_codes:
71
            language_codes = (get_language(),)
72
73
        filters = {}
74
        for field_name, val in six.iteritems(translated_fields):
75
            if field_name.startswith('master__'):
76
                filters[field_name[8:]] = val  # avoid translations__master__ back and forth
77
            else:
78
                filters["{0}__{1}".format(relname, field_name)] = val
79
80
        if len(language_codes) == 1:
81
            filters[relname + '__language_code'] = language_codes[0]
82
            return self.filter(**filters)
83
        else:
84
            filters[relname + '__language_code__in'] = language_codes
85
            return self.filter(**filters).distinct()
86
87
    def active_translations(self, language_code=None, **translated_fields):
88
        """
89
        Only return objects which are translated, or have a fallback that should be displayed.
90
91
        Typically that's the currently active language and fallback language.
92
        This should be combined with ``.distinct()``.
93
94
        When ``hide_untranslated = True``, only the currently active language will be returned.
95
        """
96
        # Default:     (language, fallback) when hide_translated == False
97
        # Alternative: (language,)          when hide_untranslated == True
98
        language_codes = get_active_language_choices(language_code)
99
        return self.translated(*language_codes, **translated_fields)
100
101
    def iterator(self):
102
        """
103
        Overwritten iterator which will set the current language before returning the object.
104
        """
105
        # Based on django-queryset-transform.
106
        # This object however, operates on a per-object instance
107
        # without breaking the result generators
108
        base_iterator = super(TranslatableQuerySet, self).iterator()
109
        for obj in base_iterator:
110
            # Apply the language setting to model instances only.
111
            if self._language and isinstance(obj, models.Model):
112
                obj.set_current_language(self._language)
113
114
            yield obj
115
116
117
class TranslatableManager(models.Manager):
118
    """
119
    The manager class which ensures the enhanced TranslatableQuerySet object is used.
120
    """
121
    queryset_class = TranslatableQuerySet
122
123
    def get_queryset(self):
124
        if not issubclass(self.queryset_class, TranslatableQuerySet):
125
            raise ImproperlyConfigured("{0}.queryset_class does not inherit from TranslatableQuerySet".format(self.__class__.__name__))
126
        return self.queryset_class(self.model, using=self._db)
127
128
    # For Django 1.5
129
    # Leave for Django 1.6/1.7, so backwards compatibility can be fixed.
130
    # It will be removed in Django 1.8, so remove it here too to avoid false promises.
131
    if django.VERSION < (1, 8):
132
        get_query_set = get_queryset
133
134
    # NOTE: Fetching the queryset is done by calling self.all() here on purpose.
135
    # By using .all(), the proper get_query_set()/get_queryset() will be used for each Django version.
136
    # Django 1.4/1.5 need to use get_query_set(), because the RelatedManager overrides that.
137
138
    def language(self, language_code=None):
139
        """
140
        Set the language code to assign to objects retrieved using this Manager.
141
        """
142
        return self.all().language(language_code)
143
144
    def translated(self, *language_codes, **translated_fields):
145
        """
146
        Only return objects which are translated in the given languages.
147
148
        NOTE: due to Django `ORM limitations <https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships>`_,
149
        this method can't be combined with other filters that access the translated fields. As such, query the fields in one filter:
150
151
        .. code-block:: python
152
153
            qs.translated('en', name="Cheese Omelette")
154
155
        This will query the translated model for the ``name`` field.
156
        """
157
        return self.all().translated(*language_codes, **translated_fields)
158
159
    def active_translations(self, language_code=None, **translated_fields):
160
        """
161
        Only return objects which are translated, or have a fallback that should be displayed.
162
163
        Typically that's the currently active language and fallback language.
164
        This should be combined with ``.distinct()``.
165
166
        When ``hide_untranslated = True``, only the currently active language will be returned.
167
        """
168
        return self.all().active_translations(language_code, **translated_fields)
169
170
171
# Export the names in django-hvad style too:
172
TranslationQueryset = TranslatableQuerySet
173
TranslationManager = TranslatableManager
174