forms.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. from __future__ import unicode_literals
  2. from collections import OrderedDict
  3. from django import forms
  4. from django.forms.utils import flatatt
  5. from django.template import loader
  6. from django.utils.encoding import force_bytes
  7. from django.utils.html import format_html, format_html_join
  8. from django.utils.http import urlsafe_base64_encode
  9. from django.utils.safestring import mark_safe
  10. from django.utils.text import capfirst
  11. from django.utils.translation import ugettext, ugettext_lazy as _
  12. from django.contrib.auth import authenticate, get_user_model
  13. from django.contrib.auth.models import User
  14. from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, identify_hasher
  15. from django.contrib.auth.tokens import default_token_generator
  16. from django.contrib.sites.shortcuts import get_current_site
  17. UNMASKED_DIGITS_TO_SHOW = 6
  18. def mask_password(password):
  19. shown = password[:UNMASKED_DIGITS_TO_SHOW]
  20. masked = "*" * max(len(password) - UNMASKED_DIGITS_TO_SHOW, 0)
  21. return shown + masked
  22. class ReadOnlyPasswordHashWidget(forms.Widget):
  23. def render(self, name, value, attrs):
  24. encoded = value
  25. final_attrs = self.build_attrs(attrs)
  26. if not encoded or encoded.startswith(UNUSABLE_PASSWORD_PREFIX):
  27. summary = mark_safe("<strong>%s</strong>" % ugettext("No password set."))
  28. else:
  29. try:
  30. hasher = identify_hasher(encoded)
  31. except ValueError:
  32. summary = mark_safe("<strong>%s</strong>" % ugettext(
  33. "Invalid password format or unknown hashing algorithm."))
  34. else:
  35. summary = format_html_join('',
  36. "<strong>{0}</strong>: {1} ",
  37. ((ugettext(key), value)
  38. for key, value in hasher.safe_summary(encoded).items())
  39. )
  40. return format_html("<div{0}>{1}</div>", flatatt(final_attrs), summary)
  41. class ReadOnlyPasswordHashField(forms.Field):
  42. widget = ReadOnlyPasswordHashWidget
  43. def __init__(self, *args, **kwargs):
  44. kwargs.setdefault("required", False)
  45. super(ReadOnlyPasswordHashField, self).__init__(*args, **kwargs)
  46. def bound_data(self, data, initial):
  47. # Always return initial because the widget doesn't
  48. # render an input field.
  49. return initial
  50. def _has_changed(self, initial, data):
  51. return False
  52. class UserCreationForm(forms.ModelForm):
  53. """
  54. A form that creates a user, with no privileges, from the given username and
  55. password.
  56. """
  57. error_messages = {
  58. 'duplicate_username': _("A user with that username already exists."),
  59. 'password_mismatch': _("The two password fields didn't match."),
  60. }
  61. username = forms.RegexField(label=_("Username"), max_length=30,
  62. regex=r'^[\w.@+-]+$',
  63. help_text=_("Required. 30 characters or fewer. Letters, digits and "
  64. "@/./+/-/_ only."),
  65. error_messages={
  66. 'invalid': _("This value may contain only letters, numbers and "
  67. "@/./+/-/_ characters.")})
  68. password1 = forms.CharField(label=_("Password"),
  69. widget=forms.PasswordInput)
  70. password2 = forms.CharField(label=_("Password confirmation"),
  71. widget=forms.PasswordInput,
  72. help_text=_("Enter the same password as above, for verification."))
  73. class Meta:
  74. model = User
  75. fields = ("username",)
  76. def clean_username(self):
  77. # Since User.username is unique, this check is redundant,
  78. # but it sets a nicer error message than the ORM. See #13147.
  79. username = self.cleaned_data["username"]
  80. try:
  81. User._default_manager.get(username=username)
  82. except User.DoesNotExist:
  83. return username
  84. raise forms.ValidationError(
  85. self.error_messages['duplicate_username'],
  86. code='duplicate_username',
  87. )
  88. def clean_password2(self):
  89. password1 = self.cleaned_data.get("password1")
  90. password2 = self.cleaned_data.get("password2")
  91. if password1 and password2 and password1 != password2:
  92. raise forms.ValidationError(
  93. self.error_messages['password_mismatch'],
  94. code='password_mismatch',
  95. )
  96. return password2
  97. def save(self, commit=True):
  98. user = super(UserCreationForm, self).save(commit=False)
  99. user.set_password(self.cleaned_data["password1"])
  100. if commit:
  101. user.save()
  102. return user
  103. class UserChangeForm(forms.ModelForm):
  104. username = forms.RegexField(
  105. label=_("Username"), max_length=30, regex=r"^[\w.@+-]+$",
  106. help_text=_("Required. 30 characters or fewer. Letters, digits and "
  107. "@/./+/-/_ only."),
  108. error_messages={
  109. 'invalid': _("This value may contain only letters, numbers and "
  110. "@/./+/-/_ characters.")})
  111. password = ReadOnlyPasswordHashField(label=_("Password"),
  112. help_text=_("Raw passwords are not stored, so there is no way to see "
  113. "this user's password, but you can change the password "
  114. "using <a href=\"password/\">this form</a>."))
  115. class Meta:
  116. model = User
  117. fields = '__all__'
  118. def __init__(self, *args, **kwargs):
  119. super(UserChangeForm, self).__init__(*args, **kwargs)
  120. f = self.fields.get('user_permissions', None)
  121. if f is not None:
  122. f.queryset = f.queryset.select_related('content_type')
  123. def clean_password(self):
  124. # Regardless of what the user provides, return the initial value.
  125. # This is done here, rather than on the field, because the
  126. # field does not have access to the initial value
  127. return self.initial["password"]
  128. class AuthenticationForm(forms.Form):
  129. """
  130. Base class for authenticating users. Extend this to get a form that accepts
  131. username/password logins.
  132. """
  133. username = forms.CharField(max_length=254)
  134. password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
  135. error_messages = {
  136. 'invalid_login': _("Please enter a correct %(username)s and password. "
  137. "Note that both fields may be case-sensitive."),
  138. 'inactive': _("This account is inactive."),
  139. }
  140. def __init__(self, request=None, *args, **kwargs):
  141. """
  142. The 'request' parameter is set for custom auth use by subclasses.
  143. The form data comes in via the standard 'data' kwarg.
  144. """
  145. self.request = request
  146. self.user_cache = None
  147. super(AuthenticationForm, self).__init__(*args, **kwargs)
  148. # Set the label for the "username" field.
  149. UserModel = get_user_model()
  150. self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
  151. if self.fields['username'].label is None:
  152. self.fields['username'].label = capfirst(self.username_field.verbose_name)
  153. def clean(self):
  154. username = self.cleaned_data.get('username')
  155. password = self.cleaned_data.get('password')
  156. if username and password:
  157. self.user_cache = authenticate(username=username,
  158. password=password)
  159. if self.user_cache is None:
  160. raise forms.ValidationError(
  161. self.error_messages['invalid_login'],
  162. code='invalid_login',
  163. params={'username': self.username_field.verbose_name},
  164. )
  165. else:
  166. self.confirm_login_allowed(self.user_cache)
  167. return self.cleaned_data
  168. def confirm_login_allowed(self, user):
  169. """
  170. Controls whether the given User may log in. This is a policy setting,
  171. independent of end-user authentication. This default behavior is to
  172. allow login by active users, and reject login by inactive users.
  173. If the given user cannot log in, this method should raise a
  174. ``forms.ValidationError``.
  175. If the given user may log in, this method should return None.
  176. """
  177. if not user.is_active:
  178. raise forms.ValidationError(
  179. self.error_messages['inactive'],
  180. code='inactive',
  181. )
  182. def get_user_id(self):
  183. if self.user_cache:
  184. return self.user_cache.id
  185. return None
  186. def get_user(self):
  187. return self.user_cache
  188. class PasswordResetForm(forms.Form):
  189. email = forms.EmailField(label=_("Email"), max_length=254)
  190. def save(self, domain_override=None,
  191. subject_template_name='registration/password_reset_subject.txt',
  192. email_template_name='registration/password_reset_email.html',
  193. use_https=False, token_generator=default_token_generator,
  194. from_email=None, request=None, html_email_template_name=None):
  195. """
  196. Generates a one-use only link for resetting password and sends to the
  197. user.
  198. """
  199. from django.core.mail import send_mail
  200. UserModel = get_user_model()
  201. email = self.cleaned_data["email"]
  202. active_users = UserModel._default_manager.filter(
  203. email__iexact=email, is_active=True)
  204. for user in active_users:
  205. # Make sure that no email is sent to a user that actually has
  206. # a password marked as unusable
  207. if not user.has_usable_password():
  208. continue
  209. if not domain_override:
  210. current_site = get_current_site(request)
  211. site_name = current_site.name
  212. domain = current_site.domain
  213. else:
  214. site_name = domain = domain_override
  215. c = {
  216. 'email': user.email,
  217. 'domain': domain,
  218. 'site_name': site_name,
  219. 'uid': urlsafe_base64_encode(force_bytes(user.pk)),
  220. 'user': user,
  221. 'token': token_generator.make_token(user),
  222. 'protocol': 'https' if use_https else 'http',
  223. }
  224. subject = loader.render_to_string(subject_template_name, c)
  225. # Email subject *must not* contain newlines
  226. subject = ''.join(subject.splitlines())
  227. email = loader.render_to_string(email_template_name, c)
  228. if html_email_template_name:
  229. html_email = loader.render_to_string(html_email_template_name, c)
  230. else:
  231. html_email = None
  232. send_mail(subject, email, from_email, [user.email], html_message=html_email)
  233. class SetPasswordForm(forms.Form):
  234. """
  235. A form that lets a user change set their password without entering the old
  236. password
  237. """
  238. error_messages = {
  239. 'password_mismatch': _("The two password fields didn't match."),
  240. }
  241. new_password1 = forms.CharField(label=_("New password"),
  242. widget=forms.PasswordInput)
  243. new_password2 = forms.CharField(label=_("New password confirmation"),
  244. widget=forms.PasswordInput)
  245. def __init__(self, user, *args, **kwargs):
  246. self.user = user
  247. super(SetPasswordForm, self).__init__(*args, **kwargs)
  248. def clean_new_password2(self):
  249. password1 = self.cleaned_data.get('new_password1')
  250. password2 = self.cleaned_data.get('new_password2')
  251. if password1 and password2:
  252. if password1 != password2:
  253. raise forms.ValidationError(
  254. self.error_messages['password_mismatch'],
  255. code='password_mismatch',
  256. )
  257. return password2
  258. def save(self, commit=True):
  259. self.user.set_password(self.cleaned_data['new_password1'])
  260. if commit:
  261. self.user.save()
  262. return self.user
  263. class PasswordChangeForm(SetPasswordForm):
  264. """
  265. A form that lets a user change their password by entering their old
  266. password.
  267. """
  268. error_messages = dict(SetPasswordForm.error_messages, **{
  269. 'password_incorrect': _("Your old password was entered incorrectly. "
  270. "Please enter it again."),
  271. })
  272. old_password = forms.CharField(label=_("Old password"),
  273. widget=forms.PasswordInput)
  274. def clean_old_password(self):
  275. """
  276. Validates that the old_password field is correct.
  277. """
  278. old_password = self.cleaned_data["old_password"]
  279. if not self.user.check_password(old_password):
  280. raise forms.ValidationError(
  281. self.error_messages['password_incorrect'],
  282. code='password_incorrect',
  283. )
  284. return old_password
  285. PasswordChangeForm.base_fields = OrderedDict(
  286. (k, PasswordChangeForm.base_fields[k])
  287. for k in ['old_password', 'new_password1', 'new_password2']
  288. )
  289. class AdminPasswordChangeForm(forms.Form):
  290. """
  291. A form used to change the password of a user in the admin interface.
  292. """
  293. error_messages = {
  294. 'password_mismatch': _("The two password fields didn't match."),
  295. }
  296. password1 = forms.CharField(label=_("Password"),
  297. widget=forms.PasswordInput)
  298. password2 = forms.CharField(label=_("Password (again)"),
  299. widget=forms.PasswordInput)
  300. def __init__(self, user, *args, **kwargs):
  301. self.user = user
  302. super(AdminPasswordChangeForm, self).__init__(*args, **kwargs)
  303. def clean_password2(self):
  304. password1 = self.cleaned_data.get('password1')
  305. password2 = self.cleaned_data.get('password2')
  306. if password1 and password2:
  307. if password1 != password2:
  308. raise forms.ValidationError(
  309. self.error_messages['password_mismatch'],
  310. code='password_mismatch',
  311. )
  312. return password2
  313. def save(self, commit=True):
  314. """
  315. Saves the new password.
  316. """
  317. self.user.set_password(self.cleaned_data["password1"])
  318. if commit:
  319. self.user.save()
  320. return self.user
  321. def _get_changed_data(self):
  322. data = super(AdminPasswordChangeForm, self).changed_data
  323. for name in self.fields.keys():
  324. if name not in data:
  325. return []
  326. return ['password']
  327. changed_data = property(_get_changed_data)