Passed
Pull Request — master (#1)
by
unknown
02:19
created

LocalizedValueDescriptor.__init__()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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