Coverage for baibu/users/forms.py: 99%
77 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
1import logging
3from allauth.account.fields import PhoneField
4from allauth.account.forms import SignupForm
5from django import forms
6from django.contrib.auth import forms as admin_forms
7from django.urls import reverse
8from django.utils.html import format_html
9from django.utils.translation import gettext_lazy as _
11from .models import ConsentRecord
12from .models import User
13from .phone import normalize_phone
14from .phone import validate_e164
16logger = logging.getLogger(__name__)
18PROFILE_FIELDS = ["name", "city", "country"]
21class HoneypotMixin:
22 """Adds a hidden field that people leave empty and simple bots fill in."""
24 honeypot_field_name = "website"
26 def __init__(self, *args, **kwargs):
27 super().__init__(*args, **kwargs)
28 self.fields[self.honeypot_field_name] = forms.CharField(
29 label=_("Website"),
30 required=False,
31 widget=forms.TextInput(attrs={"autocomplete": "off", "tabindex": "-1", "aria-hidden": "true"}),
32 )
34 def clean(self):
35 cleaned_data = super().clean()
36 if cleaned_data.get(self.honeypot_field_name):
37 logger.info("Form submission blocked by honeypot; form=%s", type(self).__name__)
38 self.add_error(
39 self.honeypot_field_name,
40 forms.ValidationError(_("Leave this field blank."), code="bot_trap"),
41 )
42 return cleaned_data
45class PhoneNumberField(PhoneField):
46 """allauth's phone field, normalising the number before validating it."""
48 def __init__(self, *args, **kwargs) -> None:
49 kwargs.setdefault("validators", [validate_e164])
50 kwargs.setdefault("help_text", _("Include the country code, starting with +."))
51 super().__init__(*args, **kwargs)
53 def to_python(self, value):
54 return normalize_phone(super().to_python(value))
57class UserSignupForm(HoneypotMixin, SignupForm):
58 name = forms.CharField(label=_("Name"), max_length=255)
59 accept_terms = forms.BooleanField(
60 error_messages={"required": _("You must agree to the terms to create an account.")},
61 )
63 field_order = ["name", "email", "phone", "password1", "password2", "accept_terms"]
65 def __init__(self, *args, **kwargs):
66 super().__init__(*args, **kwargs)
67 self.fields["accept_terms"].label = format_html(
68 _('I agree to the <a href="{terms_url}">terms of use</a> and <a href="{privacy_url}">privacy notice</a>.'),
69 terms_url=reverse("terms"),
70 privacy_url=reverse("privacy"),
71 )
73 def save(self, request):
74 user = super().save(request)
75 user.name = self.cleaned_data["name"]
76 user.save(update_fields=["name"])
77 return user
80class UserAccountForm(forms.ModelForm):
81 class Meta:
82 model = User
83 fields = PROFILE_FIELDS
86class ConsentForm(forms.Form):
87 tier = forms.ChoiceField(
88 label=_("How may your contributions be used?"),
89 choices=[
90 (
91 ConsentRecord.Tier.TRAINING_ELIGIBLE,
92 _("To evaluate and to train language tools (models) for my language."),
93 ),
94 (
95 ConsentRecord.Tier.EVAL_ONLY,
96 _("Only to test how well language tools work, not to train them."),
97 ),
98 (ConsentRecord.Tier.NONE, _("Not at all. Do not use my contributions.")),
99 ],
100 widget=forms.RadioSelect,
101 )
104class AccountDeletionRequestForm(HoneypotMixin, forms.Form):
105 request_details = forms.CharField(
106 label=_("Anything we should know? (optional)"),
107 required=False,
108 max_length=2000,
109 widget=forms.Textarea(attrs={"rows": 4}),
110 )
111 confirm = forms.BooleanField(
112 label=_("I understand my account will be deactivated straight away."),
113 error_messages={"required": _("Please confirm to continue.")},
114 )
117class UserAdminChangeForm(admin_forms.UserChangeForm):
118 class Meta(admin_forms.UserChangeForm.Meta): # type: ignore[name-defined]
119 model = User
121 def clean_phone(self):
122 return normalize_phone(self.cleaned_data.get("phone")) or None
125class UserAdminCreationForm(forms.ModelForm):
126 """Admin form for creating a user with email-only sign-in."""
128 password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
129 password2 = forms.CharField(label=_("Password confirmation"), widget=forms.PasswordInput)
130 email = forms.EmailField(label=_("Email address"), required=True)
132 class Meta:
133 model = User
134 fields = ("email",)
136 def clean_password2(self):
137 password1 = self.cleaned_data.get("password1")
138 password2 = self.cleaned_data.get("password2")
139 if password1 and password2 and password1 != password2:
140 raise forms.ValidationError(_("The two password fields didn't match."))
141 return password2
143 def save(self, commit=True): # noqa: FBT002
144 user = super().save(commit=False)
145 user.set_password(self.cleaned_data["password1"])
146 if commit:
147 user.save()
148 return user