Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/contrib/auth/forms.py: 40%
208 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import unicodedata
3from django import forms
4from django.contrib.auth import authenticate, get_user_model, password_validation
5from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, identify_hasher
6from django.contrib.auth.models import User
7from django.contrib.auth.tokens import default_token_generator
8from django.contrib.sites.shortcuts import get_current_site
9from django.core.exceptions import ValidationError
10from django.core.mail import EmailMultiAlternatives
11from django.template import loader
12from django.utils.encoding import force_bytes
13from django.utils.http import urlsafe_base64_encode
14from django.utils.text import capfirst
15from django.utils.translation import gettext
16from django.utils.translation import gettext_lazy as _
18UserModel = get_user_model()
21def _unicode_ci_compare(s1, s2):
22 """
23 Perform case-insensitive comparison of two identifiers, using the
24 recommended algorithm from Unicode Technical Report 36, section
25 2.11.2(B)(2).
26 """
27 return (
28 unicodedata.normalize("NFKC", s1).casefold()
29 == unicodedata.normalize("NFKC", s2).casefold()
30 )
33class ReadOnlyPasswordHashWidget(forms.Widget):
34 template_name = "auth/widgets/read_only_password_hash.html"
35 read_only = True
37 def get_context(self, name, value, attrs):
38 context = super().get_context(name, value, attrs)
39 summary = []
40 if not value or value.startswith(UNUSABLE_PASSWORD_PREFIX):
41 summary.append({"label": gettext("No password set.")})
42 else:
43 try:
44 hasher = identify_hasher(value)
45 except ValueError:
46 summary.append(
47 {
48 "label": gettext(
49 "Invalid password format or unknown hashing algorithm."
50 )
51 }
52 )
53 else:
54 for key, value_ in hasher.safe_summary(value).items():
55 summary.append({"label": gettext(key), "value": value_})
56 context["summary"] = summary
57 return context
59 def id_for_label(self, id_):
60 return None
63class ReadOnlyPasswordHashField(forms.Field):
64 widget = ReadOnlyPasswordHashWidget
66 def __init__(self, *args, **kwargs):
67 kwargs.setdefault("required", False)
68 kwargs.setdefault("disabled", True)
69 super().__init__(*args, **kwargs)
72class UsernameField(forms.CharField):
73 def to_python(self, value):
74 return unicodedata.normalize("NFKC", super().to_python(value))
76 def widget_attrs(self, widget):
77 return {
78 **super().widget_attrs(widget),
79 "autocapitalize": "none",
80 "autocomplete": "username",
81 }
84class UserCreationForm(forms.ModelForm):
85 """
86 A form that creates a user, with no privileges, from the given username and
87 password.
88 """
90 error_messages = {
91 "password_mismatch": _("The two password fields didn’t match."),
92 }
93 password1 = forms.CharField(
94 label=_("Password"),
95 strip=False,
96 widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
97 help_text=password_validation.password_validators_help_text_html(),
98 )
99 password2 = forms.CharField(
100 label=_("Password confirmation"),
101 widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
102 strip=False,
103 help_text=_("Enter the same password as before, for verification."),
104 )
106 class Meta:
107 model = User
108 fields = ("username",)
109 field_classes = {"username": UsernameField}
111 def __init__(self, *args, **kwargs):
112 super().__init__(*args, **kwargs)
113 if self._meta.model.USERNAME_FIELD in self.fields:
114 self.fields[self._meta.model.USERNAME_FIELD].widget.attrs[
115 "autofocus"
116 ] = True
118 def clean_password2(self):
119 password1 = self.cleaned_data.get("password1")
120 password2 = self.cleaned_data.get("password2")
121 if password1 and password2 and password1 != password2:
122 raise ValidationError(
123 self.error_messages["password_mismatch"],
124 code="password_mismatch",
125 )
126 return password2
128 def _post_clean(self):
129 super()._post_clean()
130 # Validate the password after self.instance is updated with form data
131 # by super().
132 password = self.cleaned_data.get("password2")
133 if password:
134 try:
135 password_validation.validate_password(password, self.instance)
136 except ValidationError as error:
137 self.add_error("password2", error)
139 def save(self, commit=True):
140 user = super().save(commit=False)
141 user.set_password(self.cleaned_data["password1"])
142 if commit:
143 user.save()
144 return user
147class UserChangeForm(forms.ModelForm):
148 password = ReadOnlyPasswordHashField(
149 label=_("Password"),
150 help_text=_(
151 "Raw passwords are not stored, so there is no way to see this "
152 "user’s password, but you can change the password using "
153 '<a href="{}">this form</a>.'
154 ),
155 )
157 class Meta:
158 model = User
159 fields = "__all__"
160 field_classes = {"username": UsernameField}
162 def __init__(self, *args, **kwargs):
163 super().__init__(*args, **kwargs)
164 password = self.fields.get("password")
165 if password:
166 password.help_text = password.help_text.format("../password/")
167 user_permissions = self.fields.get("user_permissions")
168 if user_permissions:
169 user_permissions.queryset = user_permissions.queryset.select_related(
170 "content_type"
171 )
174class AuthenticationForm(forms.Form):
175 """
176 Base class for authenticating users. Extend this to get a form that accepts
177 username/password logins.
178 """
180 username = UsernameField(widget=forms.TextInput(attrs={"autofocus": True}))
181 password = forms.CharField(
182 label=_("Password"),
183 strip=False,
184 widget=forms.PasswordInput(attrs={"autocomplete": "current-password"}),
185 )
187 error_messages = {
188 "invalid_login": _(
189 "Please enter a correct %(username)s and password. Note that both "
190 "fields may be case-sensitive."
191 ),
192 "inactive": _("This account is inactive."),
193 }
195 def __init__(self, request=None, *args, **kwargs):
196 """
197 The 'request' parameter is set for custom auth use by subclasses.
198 The form data comes in via the standard 'data' kwarg.
199 """
200 self.request = request
201 self.user_cache = None
202 super().__init__(*args, **kwargs)
204 # Set the max length and label for the "username" field.
205 self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
206 username_max_length = self.username_field.max_length or 254
207 self.fields["username"].max_length = username_max_length
208 self.fields["username"].widget.attrs["maxlength"] = username_max_length
209 if self.fields["username"].label is None:
210 self.fields["username"].label = capfirst(self.username_field.verbose_name)
212 def clean(self):
213 username = self.cleaned_data.get("username")
214 password = self.cleaned_data.get("password")
216 if username is not None and password:
217 self.user_cache = authenticate(
218 self.request, username=username, password=password
219 )
220 if self.user_cache is None:
221 raise self.get_invalid_login_error()
222 else:
223 self.confirm_login_allowed(self.user_cache)
225 return self.cleaned_data
227 def confirm_login_allowed(self, user):
228 """
229 Controls whether the given User may log in. This is a policy setting,
230 independent of end-user authentication. This default behavior is to
231 allow login by active users, and reject login by inactive users.
233 If the given user cannot log in, this method should raise a
234 ``ValidationError``.
236 If the given user may log in, this method should return None.
237 """
238 if not user.is_active:
239 raise ValidationError(
240 self.error_messages["inactive"],
241 code="inactive",
242 )
244 def get_user(self):
245 return self.user_cache
247 def get_invalid_login_error(self):
248 return ValidationError(
249 self.error_messages["invalid_login"],
250 code="invalid_login",
251 params={"username": self.username_field.verbose_name},
252 )
255class PasswordResetForm(forms.Form):
256 email = forms.EmailField(
257 label=_("Email"),
258 max_length=254,
259 widget=forms.EmailInput(attrs={"autocomplete": "email"}),
260 )
262 def send_mail(
263 self,
264 subject_template_name,
265 email_template_name,
266 context,
267 from_email,
268 to_email,
269 html_email_template_name=None,
270 ):
271 """
272 Send a django.core.mail.EmailMultiAlternatives to `to_email`.
273 """
274 subject = loader.render_to_string(subject_template_name, context)
275 # Email subject *must not* contain newlines
276 subject = "".join(subject.splitlines())
277 body = loader.render_to_string(email_template_name, context)
279 email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
280 if html_email_template_name is not None:
281 html_email = loader.render_to_string(html_email_template_name, context)
282 email_message.attach_alternative(html_email, "text/html")
284 email_message.send()
286 def get_users(self, email):
287 """Given an email, return matching user(s) who should receive a reset.
289 This allows subclasses to more easily customize the default policies
290 that prevent inactive users and users with unusable passwords from
291 resetting their password.
292 """
293 email_field_name = UserModel.get_email_field_name()
294 active_users = UserModel._default_manager.filter(
295 **{
296 "%s__iexact" % email_field_name: email,
297 "is_active": True,
298 }
299 )
300 return (
301 u
302 for u in active_users
303 if u.has_usable_password()
304 and _unicode_ci_compare(email, getattr(u, email_field_name))
305 )
307 def save(
308 self,
309 domain_override=None,
310 subject_template_name="registration/password_reset_subject.txt",
311 email_template_name="registration/password_reset_email.html",
312 use_https=False,
313 token_generator=default_token_generator,
314 from_email=None,
315 request=None,
316 html_email_template_name=None,
317 extra_email_context=None,
318 ):
319 """
320 Generate a one-use only link for resetting password and send it to the
321 user.
322 """
323 email = self.cleaned_data["email"]
324 if not domain_override:
325 current_site = get_current_site(request)
326 site_name = current_site.name
327 domain = current_site.domain
328 else:
329 site_name = domain = domain_override
330 email_field_name = UserModel.get_email_field_name()
331 for user in self.get_users(email):
332 user_email = getattr(user, email_field_name)
333 context = {
334 "email": user_email,
335 "domain": domain,
336 "site_name": site_name,
337 "uid": urlsafe_base64_encode(force_bytes(user.pk)),
338 "user": user,
339 "token": token_generator.make_token(user),
340 "protocol": "https" if use_https else "http",
341 **(extra_email_context or {}),
342 }
343 self.send_mail(
344 subject_template_name,
345 email_template_name,
346 context,
347 from_email,
348 user_email,
349 html_email_template_name=html_email_template_name,
350 )
353class SetPasswordForm(forms.Form):
354 """
355 A form that lets a user change set their password without entering the old
356 password
357 """
359 error_messages = {
360 "password_mismatch": _("The two password fields didn’t match."),
361 }
362 new_password1 = forms.CharField(
363 label=_("New password"),
364 widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
365 strip=False,
366 help_text=password_validation.password_validators_help_text_html(),
367 )
368 new_password2 = forms.CharField(
369 label=_("New password confirmation"),
370 strip=False,
371 widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
372 )
374 def __init__(self, user, *args, **kwargs):
375 self.user = user
376 super().__init__(*args, **kwargs)
378 def clean_new_password2(self):
379 password1 = self.cleaned_data.get("new_password1")
380 password2 = self.cleaned_data.get("new_password2")
381 if password1 and password2:
382 if password1 != password2:
383 raise ValidationError(
384 self.error_messages["password_mismatch"],
385 code="password_mismatch",
386 )
387 password_validation.validate_password(password2, self.user)
388 return password2
390 def save(self, commit=True):
391 password = self.cleaned_data["new_password1"]
392 self.user.set_password(password)
393 if commit:
394 self.user.save()
395 return self.user
398class PasswordChangeForm(SetPasswordForm):
399 """
400 A form that lets a user change their password by entering their old
401 password.
402 """
404 error_messages = {
405 **SetPasswordForm.error_messages,
406 "password_incorrect": _(
407 "Your old password was entered incorrectly. Please enter it again."
408 ),
409 }
410 old_password = forms.CharField(
411 label=_("Old password"),
412 strip=False,
413 widget=forms.PasswordInput(
414 attrs={"autocomplete": "current-password", "autofocus": True}
415 ),
416 )
418 field_order = ["old_password", "new_password1", "new_password2"]
420 def clean_old_password(self):
421 """
422 Validate that the old_password field is correct.
423 """
424 old_password = self.cleaned_data["old_password"]
425 if not self.user.check_password(old_password):
426 raise ValidationError(
427 self.error_messages["password_incorrect"],
428 code="password_incorrect",
429 )
430 return old_password
433class AdminPasswordChangeForm(forms.Form):
434 """
435 A form used to change the password of a user in the admin interface.
436 """
438 error_messages = {
439 "password_mismatch": _("The two password fields didn’t match."),
440 }
441 required_css_class = "required"
442 password1 = forms.CharField(
443 label=_("Password"),
444 widget=forms.PasswordInput(
445 attrs={"autocomplete": "new-password", "autofocus": True}
446 ),
447 strip=False,
448 help_text=password_validation.password_validators_help_text_html(),
449 )
450 password2 = forms.CharField(
451 label=_("Password (again)"),
452 widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
453 strip=False,
454 help_text=_("Enter the same password as before, for verification."),
455 )
457 def __init__(self, user, *args, **kwargs):
458 self.user = user
459 super().__init__(*args, **kwargs)
461 def clean_password2(self):
462 password1 = self.cleaned_data.get("password1")
463 password2 = self.cleaned_data.get("password2")
464 if password1 and password2 and password1 != password2:
465 raise ValidationError(
466 self.error_messages["password_mismatch"],
467 code="password_mismatch",
468 )
469 password_validation.validate_password(password2, self.user)
470 return password2
472 def save(self, commit=True):
473 """Save the new password."""
474 password = self.cleaned_data["password1"]
475 self.user.set_password(password)
476 if commit:
477 self.user.save()
478 return self.user
480 @property
481 def changed_data(self):
482 data = super().changed_data
483 for name in self.fields:
484 if name not in data:
485 return []
486 return ["password"]