Passed
Pull Request — master (#1)
by
unknown
01:52
created

LocalizedValueDescriptor.__get__()   A

Complexity

Conditions 4

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.3244

Importance

Changes 0
Metric Value
cc 4
c 0
b 0
f 0
dl 0
loc 21
rs 9.0534
ccs 8
cts 11
cp 0.7272
crap 4.3244
1 1
import json
2
3 1
from django.conf import settings
4 1
from django.contrib.postgres.fields import HStoreField
5 1
from django.db.utils import IntegrityError
6 1
from django.utils import translation
7 1
from django.utils import six
8
9 1
from ..forms import LocalizedFieldForm
10 1
from .localized_value import LocalizedValue
11
12
13 1
class LocalizedValueDescriptor(object):
14
    """
15
    The descriptor for the localized value attribute on the model instance.
16
    Returns a :see:LocalizedValue when accessed so you can do stuff like::
17
18
        >>> from myapp.models import MyModel
19
        >>> instance = MyModel()
20
        >>> instance.value.en = 'English value'
21
22
    Assigns a strings to active language key in :see:LocalizedValue on
23
    assignment so you can do::
24
25
        >>> from django.utils import translation
26
        >>> from myapp.models import MyModel
27
28
        >>> translation.activate('nl')
29
        >>> instance = MyModel()
30
        >>> instance.title = 'dutch title'
31
        >>> print(instance.title.nl) # prints 'dutch title'
32
    """
33 1
    def __init__(self, field):
34 1
        self.field = field
35
36 1
    def __get__(self, instance, cls=None):
37 1
        if instance is None:
38
            return self
39
40
        # This is slightly complicated, so worth an explanation.
41
        # `instance.localizedvalue` needs to ultimately return some instance of
42
        # `LocalizedValue`, probably a subclass.
43
44
        # The instance dict contains whatever was originally assigned
45
        # in __set__.
46 1
        if self.field.name in instance.__dict__:
47 1
            value = instance.__dict__[self.field.name]
48
        else:
49
            instance.refresh_from_db(fields=[self.field.name])
50
            value = getattr(instance, self.field.name)
51
52 1
        if value is None:
53 1
            attr = self.field.attr_class()
54 1
            instance.__dict__[self.field.name] = attr
55
56 1
        return instance.__dict__[self.field.name]
57
58 1
    def __set__(self, instance, value):
59 1
        if isinstance(value, six.string_types):
60 1
            self.__get__(instance).set(translation.get_language() or
0 ignored issues
show
Bug introduced by
The Instance of LocalizedValueDescriptor does not seem to have a member named set.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
61
                                       settings.LANGUAGE_CODE, value)
62
        else:
63 1
            instance.__dict__[self.field.name] = value
64
65
66 1
class LocalizedField(HStoreField):
67
    """A field that has the same value in multiple languages.
68
69
    Internally this is stored as a :see:HStoreField where there
70
    is a key for every language."""
71
72
    # The class to wrap instance attributes in. Accessing to field attribute in
73
    # model instance will always return an instance of attr_class.
74 1
    attr_class = LocalizedValue
75
76
    # The descriptor to use for accessing the attribute off of the class.
77 1
    descriptor_class = LocalizedValueDescriptor
78
79 1
    def contribute_to_class(self, cls, name, **kwargs):
80 1
        super(LocalizedField, self).contribute_to_class(cls, name, **kwargs)
81 1
        setattr(cls, self.name, self.descriptor_class(self))
82
83 1
    def from_db_value(self, value, *_):
84
        """Turns the specified database value into its Python
85
        equivalent.
86
87
        Arguments:
88
            value:
89
                The value that is stored in the database and
90
                needs to be converted to its Python equivalent.
91
92
        Returns:
93
            A :see:LocalizedValue instance containing the
94
            data extracted from the database.
95
        """
96
97 1
        if not value:
98 1
            return self.attr_class()
99
100 1
        return self.attr_class(value)
101
102 1
    def to_python(self, value: dict) -> LocalizedValue:
103
        """Turns the specified database value into its Python
104
        equivalent.
105
106
        Arguments:
107
            value:
108
                The value that is stored in the database and
109
                needs to be converted to its Python equivalent.
110
111
        Returns:
112
            A :see:LocalizedValue instance containing the
113
            data extracted from the database.
114
        """
115 1
        value = super(LocalizedField, self).to_python(value)
116 1
        if not value or not isinstance(value, dict):
117 1
            return self.attr_class()
118
119 1
        return self.attr_class(value)
120
121 1
    def value_to_string(self, obj):
122
        """Converts obj to a string. Used to serialize the value of the field.
123
        """
124
        value = self.value_from_object(obj)
125
        if isinstance(value, LocalizedValue):
126
            return json.dumps(value.__dict__)
127
        return super(LocalizedField, self).value_to_string(obj)
128
129 1
    def get_prep_value(self, value: LocalizedValue) -> dict:
130
        """Turns the specified value into something the database
131
        can store.
132
133
        If an illegal value (non-LocalizedValue instance) is
134
        specified, we'll treat it as an empty :see:LocalizedValue
135
        instance, on which the validation will fail.
136
137
        Arguments:
138
            value:
139
                The :see:LocalizedValue instance to serialize
140
                into a data type that the database can understand.
141
142
        Returns:
143
            A dictionary containing a key for every language,
144
            extracted from the specified value.
145
        """
146
147
        # default to None if this is an unknown type
148 1
        if not isinstance(value, LocalizedValue) and value:
149 1
            value = None
150
151 1
        if value:
152 1
            cleaned_value = self.clean(value)
153 1
            self.validate(cleaned_value)
154
        else:
155 1
            cleaned_value = value
156
157 1
        return super(LocalizedField, self).get_prep_value(
158
            cleaned_value.__dict__ if cleaned_value else None
159
        )
160
161 1
    def clean(self, value, *_):
162
        """Cleans the specified value into something we
163
        can store in the database.
164
165
        For example, when all the language fields are
166
        left empty, and the field is allows to be null,
167
        we will store None instead of empty keys.
168
169
        Arguments:
170
            value:
171
                The value to clean.
172
173
        Returns:
174
            The cleaned value, ready for database storage.
175
        """
176
177 1
        if not value or not isinstance(value, LocalizedValue):
178 1
            return None
179
180
        # are any of the language fiels None/empty?
181 1
        is_all_null = True
182 1
        for lang_code, _ in settings.LANGUAGES:
183
            # NOTE(seroy): use check for None, instead of
184
            # `bool(value.get(lang_code))==True` condition, cause in this way
185
            # we can not save '' or False values
186 1
            if value.get(lang_code) is not None:
187 1
                is_all_null = False
188 1
                break
189
190
        # all fields have been left empty and we support
191
        # null values, let's return null to represent that
192 1
        if is_all_null and self.null:
193 1
            return None
194
195 1
        return value
196
197 1
    def validate(self, value: LocalizedValue, *_):
198
        """Validates that the value for the primary language
199
        has been filled in.
200
201
        Exceptions are raises in order to notify the user
202
        of invalid values.
203
204
        Arguments:
205
            value:
206
                The value to validate.
207
        """
208
209 1
        if self.null:
210 1
            return
211
212 1
        primary_lang_val = getattr(value, settings.LANGUAGE_CODE)
213
        # NOTE(seroy): use check for None, instead of `not primary_lang_val`
214
        # condition, cause in this way we can not save '' or False values
215 1
        if primary_lang_val is None:
216 1
            raise IntegrityError(
217
                'null value in column "%s.%s" violates not-null constraint' % (
218
                    self.name,
219
                    settings.LANGUAGE_CODE
220
                )
221
            )
222
223 1
    def formfield(self, **kwargs):
224
        """Gets the form field associated with this field."""
225
226 1
        defaults = {
227
            'form_class': LocalizedFieldForm
228
        }
229
230 1
        defaults.update(kwargs)
231
        return super().formfield(**defaults)
232