PageRenderTime 43ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/kitsune/users/forms.py

https://gitlab.com/jslee1/kitsune
Python | 427 lines | 394 code | 20 blank | 13 comment | 6 complexity | ab572bf9c4ff82776a577de7aa1d986c MD5 | raw file
  1. import re
  2. from datetime import datetime
  3. from django import forms
  4. from django.conf import settings
  5. from django.contrib.auth import authenticate, forms as auth_forms
  6. from django.contrib.auth.forms import (PasswordResetForm as
  7. DjangoPasswordResetForm)
  8. from django.contrib.auth.models import User
  9. from django.contrib.auth.tokens import default_token_generator
  10. from django.contrib.sites.models import get_current_site
  11. from django.core.cache import cache
  12. from django.utils.http import int_to_base36
  13. from django.utils.translation import ugettext as _, ugettext_lazy as _lazy
  14. from kitsune.sumo import email_utils
  15. from kitsune.sumo.urlresolvers import reverse
  16. from kitsune.sumo.widgets import ImageWidget
  17. from kitsune.upload.forms import clean_image_extension
  18. from kitsune.upload.utils import check_file_size, FileTooLargeError
  19. from kitsune.users.models import Profile
  20. from kitsune.users.widgets import FacebookURLWidget
  21. from kitsune.users.widgets import MonthYearWidget
  22. USERNAME_INVALID = _lazy(u'Username may contain only English letters, '
  23. 'numbers and ./-/_ characters.')
  24. USERNAME_REQUIRED = _lazy(u'Username is required.')
  25. USERNAME_SHORT = _lazy(u'Username is too short (%(show_value)s characters). '
  26. 'It must be at least %(limit_value)s characters.')
  27. USERNAME_LONG = _lazy(u'Username is too long (%(show_value)s characters). '
  28. 'It must be %(limit_value)s characters or less.')
  29. EMAIL_REQUIRED = _lazy(u'Email address is required.')
  30. EMAIL_SHORT = _lazy(u'Email address is too short (%(show_value)s characters). '
  31. 'It must be at least %(limit_value)s characters.')
  32. EMAIL_LONG = _lazy(u'Email address is too long (%(show_value)s characters). '
  33. 'It must be %(limit_value)s characters or less.')
  34. PASSWD_REQUIRED = _lazy(u'Password is required.')
  35. PASSWD2_REQUIRED = _lazy(u'Please enter your password twice.')
  36. PASSWD_MIN_LENGTH = 8
  37. PASSWD_MIN_LENGTH_MSG = _lazy('Password must be 8 or more characters.')
  38. # Enforces at least one digit and at least one alpha character.
  39. password_re = re.compile(r'(?=.*\d)(?=.*[a-zA-Z])')
  40. class SettingsForm(forms.Form):
  41. forums_watch_new_thread = forms.BooleanField(
  42. required=False, initial=True,
  43. label=_lazy(u'Watch forum threads I start'))
  44. forums_watch_after_reply = forms.BooleanField(
  45. required=False, initial=True,
  46. label=_lazy(u'Watch forum threads I comment in'))
  47. kbforums_watch_new_thread = forms.BooleanField(
  48. required=False, initial=True,
  49. label=_lazy(u'Watch KB discussion threads I start'))
  50. kbforums_watch_after_reply = forms.BooleanField(
  51. required=False, initial=True,
  52. label=_lazy(u'Watch KB discussion threads I comment in'))
  53. questions_watch_after_reply = forms.BooleanField(
  54. required=False, initial=True,
  55. label=_lazy(u'Watch Question threads I comment in'))
  56. email_private_messages = forms.BooleanField(
  57. required=False, initial=True,
  58. label=_lazy(u'Send emails for private messages'))
  59. def save_for_user(self, user):
  60. for field in self.fields.keys():
  61. value = str(self.cleaned_data[field])
  62. setting = user.settings.filter(name=field)
  63. update_count = setting.update(value=value)
  64. if update_count == 0:
  65. # This user didn't have this setting so create it.
  66. user.settings.create(name=field, value=value)
  67. class RegisterForm(forms.ModelForm):
  68. """A user registration form that requires unique email addresses.
  69. The default Django user creation form does not require an email address,
  70. let alone that it be unique. This form does, and sets a minimum length
  71. for usernames.
  72. """
  73. username = forms.RegexField(
  74. label=_lazy(u'Username:'), max_length=30, min_length=4,
  75. regex=r'^[\w.-]+$',
  76. help_text=_lazy(u'Required. 30 characters or fewer. Letters, digits '
  77. u'and ./- only.'),
  78. error_messages={'invalid': USERNAME_INVALID,
  79. 'required': USERNAME_REQUIRED,
  80. 'min_length': USERNAME_SHORT,
  81. 'max_length': USERNAME_LONG})
  82. email = forms.EmailField(
  83. label=_lazy(u'Email address:'),
  84. error_messages={'required': EMAIL_REQUIRED,
  85. 'min_length': EMAIL_SHORT,
  86. 'max_length': EMAIL_LONG})
  87. password = forms.CharField(
  88. label=_lazy(u'Password:'),
  89. min_length=PASSWD_MIN_LENGTH,
  90. widget=forms.PasswordInput(render_value=False),
  91. error_messages={'required': PASSWD_REQUIRED,
  92. 'min_length': PASSWD_MIN_LENGTH_MSG})
  93. interested = forms.BooleanField(required=False)
  94. class Meta(object):
  95. model = User
  96. fields = ('email', 'username', 'password',)
  97. def clean(self):
  98. super(RegisterForm, self).clean()
  99. username = self.cleaned_data.get('username')
  100. password = self.cleaned_data.get('password')
  101. _check_password(password)
  102. _check_username(username)
  103. return self.cleaned_data
  104. def clean_email(self):
  105. email = self.cleaned_data['email']
  106. if User.objects.filter(email=email).exists():
  107. raise forms.ValidationError(_('A user with that email address '
  108. 'already exists.'))
  109. return email
  110. def __init__(self, request=None, *args, **kwargs):
  111. super(RegisterForm, self).__init__(request, auto_id='id_for_%s',
  112. *args, **kwargs)
  113. class AuthenticationForm(auth_forms.AuthenticationForm):
  114. """Overrides the default django form.
  115. * Doesn't prefill password on validation error.
  116. * Allows logging in inactive users (initialize with `only_active=False`).
  117. """
  118. username = forms.CharField(
  119. label=_lazy(u'Username:'),
  120. error_messages={'required': USERNAME_REQUIRED})
  121. password = forms.CharField(
  122. label=_lazy(u'Password:'),
  123. widget=forms.PasswordInput(render_value=False),
  124. error_messages={'required': PASSWD_REQUIRED})
  125. def __init__(self, request=None, only_active=True, *args, **kwargs):
  126. self.only_active = only_active
  127. super(AuthenticationForm, self).__init__(request, *args, **kwargs)
  128. def clean(self):
  129. username = self.cleaned_data.get('username')
  130. password = self.cleaned_data.get('password')
  131. if username and password:
  132. self.user_cache = authenticate(username=username,
  133. password=password)
  134. if self.user_cache is None:
  135. raise forms.ValidationError(
  136. _('Please enter a correct username and password. Note '
  137. 'that both fields are case-sensitive.'))
  138. elif self.only_active and not self.user_cache.is_active:
  139. raise forms.ValidationError(_('This account is inactive.'))
  140. if self.request:
  141. if not self.request.session.test_cookie_worked():
  142. raise forms.ValidationError(
  143. _("Your Web browser doesn't appear to have cookies "
  144. "enabled. Cookies are required for logging in."))
  145. return self.cleaned_data
  146. class ProfileForm(forms.ModelForm):
  147. """The form for editing the user's profile."""
  148. involved_from = forms.DateField(
  149. required=False,
  150. label=_lazy(u'Involved with Mozilla from'),
  151. widget=MonthYearWidget(years=range(1998, datetime.today().year + 1),
  152. required=False))
  153. class Meta(object):
  154. model = Profile
  155. fields = ('name', 'public_email', 'bio', 'website', 'twitter',
  156. 'facebook', 'mozillians', 'irc_handle', 'timezone', 'country', 'city',
  157. 'locale', 'involved_from')
  158. widgets = {
  159. 'facebook': FacebookURLWidget,
  160. }
  161. def clean_facebook(self):
  162. facebook = self.cleaned_data['facebook']
  163. if facebook and not re.match(FacebookURLWidget.pattern, facebook):
  164. raise forms.ValidationError(_(u'Please enter a facebook.com URL.'))
  165. return facebook
  166. class AvatarForm(forms.ModelForm):
  167. """The form for editing the user's avatar."""
  168. avatar = forms.ImageField(required=True, widget=ImageWidget)
  169. def __init__(self, *args, **kwargs):
  170. super(AvatarForm, self).__init__(*args, **kwargs)
  171. self.fields['avatar'].help_text = (
  172. _('Your avatar will be resized to {size}x{size}').format(
  173. size=settings.AVATAR_SIZE))
  174. class Meta(object):
  175. model = Profile
  176. fields = ('avatar',)
  177. def clean_avatar(self):
  178. if not ('avatar' in self.cleaned_data and self.cleaned_data['avatar']):
  179. return self.cleaned_data['avatar']
  180. try:
  181. check_file_size(self.cleaned_data['avatar'],
  182. settings.MAX_AVATAR_FILE_SIZE)
  183. except FileTooLargeError as e:
  184. raise forms.ValidationError(e.args[0])
  185. clean_image_extension(self.cleaned_data.get('avatar'))
  186. return self.cleaned_data['avatar']
  187. class EmailConfirmationForm(forms.Form):
  188. """A simple form that requires an email address."""
  189. email = forms.EmailField(label=_lazy(u'Email address:'))
  190. class EmailChangeForm(forms.Form):
  191. """A simple form that requires an email address and validates that it is
  192. not the current user's email."""
  193. email = forms.EmailField(label=_lazy(u'Email address:'))
  194. def __init__(self, user, *args, **kwargs):
  195. super(EmailChangeForm, self).__init__(*args, **kwargs)
  196. self.user = user
  197. def clean_email(self):
  198. email = self.cleaned_data['email']
  199. if self.user.email == email:
  200. raise forms.ValidationError(_('This is your current email.'))
  201. if User.objects.filter(email=email).exists():
  202. raise forms.ValidationError(_('A user with that email address '
  203. 'already exists.'))
  204. return self.cleaned_data['email']
  205. class SetPasswordForm(auth_forms.SetPasswordForm):
  206. new_password1 = forms.CharField(
  207. label=_lazy(u'New password:'),
  208. min_length=PASSWD_MIN_LENGTH,
  209. widget=forms.PasswordInput(render_value=False),
  210. error_messages={'required': PASSWD_REQUIRED,
  211. 'min_length': PASSWD_MIN_LENGTH_MSG})
  212. def clean(self):
  213. super(SetPasswordForm, self).clean()
  214. _check_password(self.cleaned_data.get('new_password1'))
  215. return self.cleaned_data
  216. class PasswordChangeForm(auth_forms.PasswordChangeForm):
  217. new_password1 = forms.CharField(
  218. label=_lazy(u'New password:'),
  219. min_length=PASSWD_MIN_LENGTH,
  220. widget=forms.PasswordInput(render_value=False),
  221. error_messages={'required': PASSWD_REQUIRED,
  222. 'min_length': PASSWD_MIN_LENGTH_MSG})
  223. def clean(self):
  224. super(PasswordChangeForm, self).clean()
  225. _check_password(self.cleaned_data.get('new_password1'))
  226. return self.cleaned_data
  227. class ForgotUsernameForm(forms.Form):
  228. """A simple form to retrieve username.
  229. Requires an email address."""
  230. email = forms.EmailField(label=_lazy(u'Email address:'))
  231. def clean_email(self):
  232. """
  233. Validates that an active user exists with the given e-mail address.
  234. """
  235. email = self.cleaned_data["email"]
  236. try:
  237. self.user = User.objects.get(email__iexact=email, is_active=True)
  238. except User.DoesNotExist:
  239. raise forms.ValidationError(
  240. _(u"That e-mail address doesn't have an associated user "
  241. u"account. Are you sure you've registered?"))
  242. return email
  243. def save(self, text_template='users/email/forgot_username.ltxt',
  244. html_template='users/email/forgot_username.html', use_https=False,
  245. request=None):
  246. """Sends email with username."""
  247. user = self.user
  248. current_site = get_current_site(request)
  249. site_name = current_site.name
  250. domain = current_site.domain
  251. @email_utils.safe_translation
  252. def _send_mail(locale, user, context):
  253. subject = _('Your username on %s') % site_name
  254. mail = email_utils.make_mail(
  255. subject=subject,
  256. text_template=text_template,
  257. html_template=html_template,
  258. context_vars=context,
  259. from_email=settings.TIDINGS_FROM_ADDRESS,
  260. to_email=user.email)
  261. email_utils.send_messages([mail])
  262. c = {
  263. 'email': user.email,
  264. 'domain': domain,
  265. 'login_url': reverse('users.login'),
  266. 'site_name': site_name,
  267. 'username': user.username,
  268. 'protocol': use_https and 'https' or 'http'}
  269. # The user is not logged in, the user object comes from the
  270. # supplied email address, and is filled in by `clean_email`. If
  271. # an invalid email address was given, an exception would have
  272. # been raised already.
  273. locale = user.profile.locale or settings.WIKI_DEFAULT_LANGUAGE
  274. _send_mail(locale, user, c)
  275. class PasswordResetForm(DjangoPasswordResetForm):
  276. def save(self, domain_override=None,
  277. subject_template_name='registration/password_reset_subject.txt',
  278. text_template=None,
  279. html_template=None,
  280. use_https=False, token_generator=default_token_generator,
  281. from_email=None, request=None):
  282. """
  283. Based off of django's but handles html and plain-text emails.
  284. """
  285. users = User.objects.filter(
  286. email__iexact=self.cleaned_data["email"], is_active=True)
  287. for user in users:
  288. if not domain_override:
  289. current_site = get_current_site(request)
  290. site_name = current_site.name
  291. domain = current_site.domain
  292. else:
  293. site_name = domain = domain_override
  294. c = {
  295. 'email': user.email,
  296. 'domain': domain,
  297. 'site_name': site_name,
  298. 'uid': int_to_base36(user.id),
  299. 'user': user,
  300. 'token': token_generator.make_token(user),
  301. 'protocol': use_https and 'https' or 'http',
  302. }
  303. subject = email_utils.render_email(subject_template_name, c)
  304. # Email subject *must not* contain newlines
  305. subject = ''.join(subject.splitlines())
  306. @email_utils.safe_translation
  307. def _make_mail(locale):
  308. mail = email_utils.make_mail(
  309. subject=subject,
  310. text_template=text_template,
  311. html_template=html_template,
  312. context_vars=c,
  313. from_email=from_email,
  314. to_email=user.email)
  315. return mail
  316. if request:
  317. locale = request.LANGUAGE_CODE
  318. else:
  319. locale = settings.WIKI_DEFAULT_LANGUAGE
  320. email_utils.send_messages([_make_mail(locale)])
  321. def _check_password(password):
  322. if password: # Oddly, empty password validation happens after this.
  323. if not password_re.search(password):
  324. msg = _('At least one number and one English letter are required '
  325. 'in the password.')
  326. raise forms.ValidationError(msg)
  327. USERNAME_CACHE_KEY = 'username-blacklist'
  328. def username_allowed(username):
  329. if not username:
  330. return False
  331. """Returns True if the given username is not a blatent bad word."""
  332. blacklist = cache.get(USERNAME_CACHE_KEY)
  333. if blacklist is None:
  334. f = open(settings.USERNAME_BLACKLIST, 'r')
  335. blacklist = [w.strip() for w in f.readlines()]
  336. cache.set(USERNAME_CACHE_KEY, blacklist, 60 * 60) # 1 hour
  337. # Lowercase
  338. username = username.lower()
  339. # Add lowercased and non alphanumerics to start.
  340. usernames = set([username, re.sub("\W", "", username)])
  341. # Add words split on non alphanumerics.
  342. for u in re.findall(r'\w+', username):
  343. usernames.add(u)
  344. # Do any match the bad words?
  345. return not usernames.intersection(blacklist)
  346. def _check_username(username):
  347. if username and not username_allowed(username):
  348. msg = _('The user name you entered is inappropriate. Please pick '
  349. 'another and consider that our helpers are other Firefox '
  350. 'users just like you.')
  351. raise forms.ValidationError(msg)