-
Notifications
You must be signed in to change notification settings - Fork 536
/
Copy pathfields.py
193 lines (156 loc) Β· 7.53 KB
/
fields.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
from django.conf import settings
from django.utils.translation import get_language, ugettext_lazy as _
from django.core.exceptions import ValidationError
from rest_framework import fields
from olympia.amo.utils import to_language
from olympia.translations.models import Translation
class ReverseChoiceField(fields.ChoiceField):
"""
A ChoiceField that exposes the "human-readable" values of its choices,
while storing the "actual" corresponding value as normal.
This is useful when you want to expose string constants to clients while
storing integers in the database.
Note that the values in the `choices_dict` must be unique, since they are
used for both serialization and de-serialization.
"""
def __init__(self, *args, **kwargs):
self.reversed_choices = {v: k for k, v in kwargs['choices']}
super(ReverseChoiceField, self).__init__(*args, **kwargs)
def to_representation(self, value):
"""
Convert to representation by getting the "human-readable" value from
the "actual" one.
"""
value = self.choices.get(value, None)
return super(ReverseChoiceField, self).to_representation(value)
def to_internal_value(self, value):
"""
Convert to internal value by getting the "actual" value from the
"human-readable" one that is passed.
"""
try:
value = self.reversed_choices[value]
except KeyError:
self.fail('invalid_choice', input=value)
return super(ReverseChoiceField, self).to_internal_value(value)
class TranslationSerializerField(fields.Field):
"""
Django-rest-framework custom serializer field for our TranslatedFields.
- When deserializing, in `to_internal_value`, it accepts both a string
or a dictionary. If a string is given, it'll be considered to be in the
default language.
- When serializing, its behavior depends on the parent's serializer
context:
If a request was included, and its method is 'GET', and a 'lang'
parameter was passed, then only returns one translation (letting the
TranslatedField figure out automatically which language to use).
Else, just returns a dict with all translations for the given
`field_name` on `obj`, with languages as the keys.
"""
default_error_messages = {
'min_length': _(u'The field must have a length of at least {num} '
u'characters.'),
'unknown_locale': _(u'The language code {lang_code} is invalid.')
}
def __init__(self, *args, **kwargs):
self.min_length = kwargs.pop('min_length', None)
super(TranslationSerializerField, self).__init__(*args, **kwargs)
def fetch_all_translations(self, obj, source, field):
translations = field.__class__.objects.filter(
id=field.id, localized_string__isnull=False)
return {to_language(trans.locale): unicode(trans)
for trans in translations} if translations else None
def fetch_single_translation(self, obj, source, field, requested_language):
return unicode(field) if field else None
def get_attribute(self, obj):
source = self.source or self.field_name
field = fields.get_attribute(obj, source.split('.'))
if not field:
return None
requested_language = None
request = self.context.get('request', None)
if request and request.method == 'GET' and 'lang' in request.GET:
requested_language = request.GET['lang']
if requested_language:
return self.fetch_single_translation(obj, source, field,
requested_language)
else:
return self.fetch_all_translations(obj, source, field)
def to_representation(self, val):
return val
def to_internal_value(self, data):
if isinstance(data, basestring):
self.validate(data)
return data.strip()
elif isinstance(data, dict):
self.validate(data)
for key, value in data.items():
data[key] = value and value.strip()
return data
return unicode(data)
def validate(self, value):
value_too_short = True
if isinstance(value, basestring):
if len(value.strip()) >= self.min_length:
value_too_short = False
else:
for locale, string in value.items():
if locale.lower() not in settings.LANGUAGES:
raise ValidationError(
self.error_messages['unknown_locale'].format(
lang_code=repr(locale)))
if string and (len(string.strip()) >= self.min_length):
value_too_short = False
break
if self.min_length and value_too_short:
raise ValidationError(
self.error_messages['min_length'].format(num=self.min_length))
class ESTranslationSerializerField(TranslationSerializerField):
"""
Like TranslationSerializerField, but fetching the data from a dictionary
built from ES data that we previously attached on the object.
"""
suffix = '_translations'
_source = None
def get_source(self):
if self._source is None:
return None
return self._source + self.suffix
def set_source(self, val):
self._source = val
source = property(get_source, set_source)
def attach_translations(self, obj, data, source_name, target_name=None):
"""
Look for the translation of `source_name` in `data` and create a dict
with all translations for this field (which will look like
{'en-US': 'mytranslation'}) and attach it to a property on `obj`.
The property name is built with `target_name` and `cls.suffix`. If
`target_name` is None, `source_name` is used instead.
The suffix is necessary for two reasons:
1) The translations app won't let us set the dict on the real field
without making db queries
2) This also exactly matches how we store translations in ES, so we can
directly fetch the translations in the data passed to this method.
"""
if target_name is None:
target_name = source_name
target_key = '%s%s' % (target_name, self.suffix)
source_key = '%s%s' % (source_name, self.suffix)
target_translations = {v.get('lang', ''): v.get('string', '')
for v in data.get(source_key, {}) or {}}
setattr(obj, target_key, target_translations)
# Serializer might need the single translation in the current language,
# so fetch it and attach it directly under `target_name`. We need a
# fake Translation() instance to prevent SQL queries from being
# automatically made by the translations app.
translation = self.fetch_single_translation(
obj, target_name, target_translations, get_language())
setattr(obj, target_name, Translation(localized_string=translation))
def fetch_all_translations(self, obj, source, field):
return field or None
def fetch_single_translation(self, obj, source, field, requested_language):
translations = self.fetch_all_translations(obj, source, field) or {}
return (translations.get(requested_language) or
translations.get(getattr(obj, 'default_locale', None)) or
translations.get(getattr(obj, 'default_language', None)) or
translations.get(settings.LANGUAGE_CODE) or None)