This repository has been archived by the owner on Jan 19, 2021. It is now read-only.
/
forms.py
318 lines (256 loc) · 12.4 KB
/
forms.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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
import re
from cStringIO import StringIO
from datetime import datetime
from django import forms
from django.conf import settings
from django.contrib.auth.models import User
from django.core.files.uploadedfile import UploadedFile
from django.forms.models import BaseInlineFormSet, inlineformset_factory
import django_filters
import happyforms
from PIL import Image
from tower import ugettext as _, ugettext_lazy as _lazy
from mozillians.groups.models import Skill
from mozillians.phonebook.models import Invite
from mozillians.phonebook.validators import validate_username
from mozillians.phonebook.widgets import MonthYearWidget
from mozillians.users import get_languages_for_locale
from mozillians.users.models import ExternalAccount, Language, UserProfile
REGEX_NUMERIC = re.compile('\d+', re.IGNORECASE)
class ExternalAccountForm(happyforms.ModelForm):
class Meta:
model = ExternalAccount
fields = ['type', 'identifier', 'privacy']
def clean(self):
cleaned_data = super(ExternalAccountForm, self).clean()
identifier = cleaned_data.get('identifier')
account_type = cleaned_data.get('type')
if account_type and identifier:
# If the Account expects an identifier and user provided a
# full URL, try to extract the identifier from the URL.
url = ExternalAccount.ACCOUNT_TYPES[account_type].get('url')
if url and identifier.startswith('http'):
url_pattern_re = url.replace('{identifier}', '(.+)')
identifier = identifier.rstrip('/')
url_pattern_re = url_pattern_re.rstrip('/')
match = re.match(url_pattern_re, identifier)
if match:
identifier = match.groups()[0]
validator = ExternalAccount.ACCOUNT_TYPES[account_type].get('validator')
if validator:
identifier = validator(identifier)
cleaned_data['identifier'] = identifier
return cleaned_data
AccountsFormset = inlineformset_factory(UserProfile, ExternalAccount,
form=ExternalAccountForm, extra=1)
class SearchForm(happyforms.Form):
q = forms.CharField(required=False)
limit = forms.IntegerField(
widget=forms.HiddenInput, required=False, min_value=1,
max_value=settings.ITEMS_PER_PAGE)
include_non_vouched = forms.BooleanField(
label=_lazy(u'Include non-vouched'), required=False)
def clean_limit(self):
limit = self.cleaned_data['limit'] or settings.ITEMS_PER_PAGE
return limit
def filter_vouched(qs, choice):
if choice == SearchFilter.CHOICE_ONLY_VOUCHED:
return qs.filter(is_vouched=True)
elif choice == SearchFilter.CHOICE_ONLY_UNVOUCHED:
return qs.filter(is_vouched=False)
return qs
class SearchFilter(django_filters.FilterSet):
CHOICE_ONLY_VOUCHED = 'yes'
CHOICE_ONLY_UNVOUCHED = 'no'
CHOICE_ALL = 'all'
CHOICES = (
(CHOICE_ONLY_VOUCHED, _lazy('Vouched')),
(CHOICE_ONLY_UNVOUCHED, _lazy('Unvouched')),
(CHOICE_ALL, _lazy('All')),
)
vouched = django_filters.ChoiceFilter(
name='vouched', label=_lazy(u'Display only'), required=False,
choices=CHOICES, action=filter_vouched)
class Meta:
model = UserProfile
fields = ['vouched', 'skills', 'groups', 'timezone']
def __init__(self, *args, **kwargs):
super(SearchFilter, self).__init__(*args, **kwargs)
self.filters['timezone'].field.choices.insert(0, ('', _lazy(u'All timezones')))
class UserForm(happyforms.ModelForm):
"""Instead of just inhereting form a UserProfile model form, this
base class allows us to also abstract over methods that have to do
with the User object that need to exist in both Registration and
Profile.
"""
username = forms.CharField(label=_lazy(u'Username'))
class Meta:
model = User
fields = ['username']
def clean_username(self):
username = self.cleaned_data['username']
if not username:
return self.instance.username
# Don't be jacking somebody's username
# This causes a potential race condition however the worst that can
# happen is bad UI.
if (User.objects.filter(username=username).
exclude(pk=self.instance.id).exists()):
raise forms.ValidationError(_(u'This username is in use. Please try'
u' another.'))
# No funky characters in username.
if not re.match(r'^[\w.@+-]+$', username):
raise forms.ValidationError(_(u'Please use only alphanumeric'
u' characters'))
if not validate_username(username):
raise forms.ValidationError(_(u'This username is not allowed, '
u'please choose another.'))
return username
class ProfileForm(happyforms.ModelForm):
photo = forms.ImageField(label=_lazy(u'Profile Photo'), required=False)
photo_delete = forms.BooleanField(label=_lazy(u'Remove Profile Photo'),
required=False)
date_mozillian = forms.DateField(
required=False,
label=_lazy(u'When did you get involved with Mozilla?'),
widget=MonthYearWidget(years=range(1998, datetime.today().year + 1),
required=False))
skills = forms.CharField(
label='',
help_text=_lazy(u'Start typing to add a skill (example: Python, '
u'javascript, Graphic Design, User Research)'),
required=False)
lat = forms.FloatField(widget=forms.HiddenInput)
lng = forms.FloatField(widget=forms.HiddenInput)
savecountry = forms.BooleanField(
label=_lazy(u'Required'),
initial=True, required=False,
widget=forms.CheckboxInput(attrs={'disabled': 'disabled'})
)
saveregion = forms.BooleanField(label=_lazy(u'Save'), required=False, show_hidden_initial=True)
savecity = forms.BooleanField(label=_lazy(u'Save'), required=False, show_hidden_initial=True)
class Meta:
model = UserProfile
fields = ('full_name', 'ircname', 'bio', 'photo',
'allows_community_sites', 'tshirt',
'title', 'allows_mozilla_sites',
'date_mozillian', 'story_link', 'timezone',
'privacy_photo', 'privacy_full_name', 'privacy_ircname',
'privacy_timezone', 'privacy_tshirt',
'privacy_bio', 'privacy_geo_city', 'privacy_geo_region',
'privacy_geo_country', 'privacy_groups',
'privacy_skills', 'privacy_languages',
'privacy_date_mozillian', 'privacy_story_link', 'privacy_title')
widgets = {'bio': forms.Textarea()}
def clean_photo(self):
"""Clean possible bad Image data.
Try to load EXIF data from image. If that fails, remove EXIF
data by re-saving the image. Related bug 919736.
"""
photo = self.cleaned_data['photo']
if photo and isinstance(photo, UploadedFile):
image = Image.open(photo.file)
try:
image._get_exif()
except (AttributeError, IOError, KeyError, IndexError):
cleaned_photo = StringIO()
if image.mode != 'RGB':
image = image.convert('RGB')
image.save(cleaned_photo, format='JPEG', quality=95)
photo.file = cleaned_photo
photo.size = cleaned_photo.tell()
return photo
def clean_skills(self):
if not re.match(r'^[a-zA-Z0-9 +.:,-]*$', self.cleaned_data['skills']):
# Commas cannot be included in skill names because we use them to
# separate names in a list
raise forms.ValidationError(_(u'Skills can only contain '
u'alphanumeric characters '
u'and +.:-.'))
skills = self.cleaned_data['skills']
return filter(lambda x: x,
map(lambda x: x.strip() or False,
skills.lower().split(',')))
def clean(self):
# If lng/lat were provided, make sure they point at a country somewhere...
if self.cleaned_data.get('lat') is not None and self.cleaned_data.get('lng') is not None:
# We only want to call reverse_geocode if some location data changed.
if ('lat' in self.changed_data or 'lng' in self.changed_data or
'saveregion' in self.changed_data or 'savecity' in self.changed_data):
self.instance.lat = self.cleaned_data['lat']
self.instance.lng = self.cleaned_data['lng']
self.instance.reverse_geocode()
if not self.instance.geo_country:
error_msg = _('Location must be inside a country.')
self.errors['savecountry'] = self.error_class([error_msg])
del self.cleaned_data['savecountry']
# If the user doesn't want their region/city saved, respect it.
if not self.cleaned_data.get('saveregion'):
if not self.cleaned_data.get('savecity'):
self.instance.geo_region = None
else:
error_msg = _('Region must also be saved if city is saved.')
self.errors['saveregion'] = self.error_class([error_msg])
if not self.cleaned_data.get('savecity'):
self.instance.geo_city = None
else:
self.errors['location'] = self.error_class([_('Search for your country on the map.')])
self.errors['savecountry'] = self.error_class([_('Country cannot be empty.')])
del self.cleaned_data['savecountry']
return self.cleaned_data
def save(self, *args, **kwargs):
"""Save the data to profile."""
self.instance.set_membership(Skill, self.cleaned_data['skills'])
super(ProfileForm, self).save(*args, **kwargs)
class BaseLanguageFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
self.locale = kwargs.pop('locale', 'en')
super(BaseLanguageFormSet, self).__init__(*args, **kwargs)
def add_fields(self, form, index):
super(BaseLanguageFormSet, self).add_fields(form, index)
choices = [('', '---------')] + get_languages_for_locale(self.locale)
form.fields['code'].choices = choices
class Meta:
models = Language
fields = ['code']
LanguagesFormset = inlineformset_factory(UserProfile, Language,
formset=BaseLanguageFormSet,
extra=1)
class EmailForm(happyforms.Form):
email = forms.EmailField(label=_lazy(u'Email'))
def clean_email(self):
email = self.cleaned_data['email']
if (User.objects
.exclude(pk=self.initial['user_id']).filter(email=email).exists()):
raise forms.ValidationError(_(u'Email is currently associated with another user.'))
return email
def email_changed(self):
return self.cleaned_data['email'] != self.initial['email']
class RegisterForm(ProfileForm):
optin = forms.BooleanField(
widget=forms.CheckboxInput(attrs={'class': 'checkbox'}),
required=True)
class VouchForm(happyforms.Form):
"""Vouching is captured via a user's id and a description of the reason for vouching."""
description = forms.CharField(
label=_lazy(u'Provide a reason for vouching with relevant links'),
widget=forms.Textarea(attrs={'rows': 10, 'cols': 20, 'maxlength': 500}),
max_length=500,
error_messages={'required': _(u'You must enter a reason for vouching for this person.')}
)
class InviteForm(happyforms.ModelForm):
message = forms.CharField(
label=_lazy(u'Personal message to be included in the invite email'),
required=False, widget=forms.Textarea(),
)
recipient = forms.EmailField(label=_lazy(u"Recipient's email"))
def clean_recipient(self):
recipient = self.cleaned_data['recipient']
if User.objects.filter(email=recipient,
userprofile__is_vouched=True).exists():
raise forms.ValidationError(
_(u'You cannot invite someone who has already been vouched.'))
return recipient
class Meta:
model = Invite
fields = ['recipient']