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

1import logging 

2 

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 _ 

10 

11from .models import ConsentRecord 

12from .models import User 

13from .phone import normalize_phone 

14from .phone import validate_e164 

15 

16logger = logging.getLogger(__name__) 

17 

18PROFILE_FIELDS = ["name", "city", "country"] 

19 

20 

21class HoneypotMixin: 

22 """Adds a hidden field that people leave empty and simple bots fill in.""" 

23 

24 honeypot_field_name = "website" 

25 

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 ) 

33 

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 

43 

44 

45class PhoneNumberField(PhoneField): 

46 """allauth's phone field, normalising the number before validating it.""" 

47 

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) 

52 

53 def to_python(self, value): 

54 return normalize_phone(super().to_python(value)) 

55 

56 

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 ) 

62 

63 field_order = ["name", "email", "phone", "password1", "password2", "accept_terms"] 

64 

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 ) 

72 

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 

78 

79 

80class UserAccountForm(forms.ModelForm): 

81 class Meta: 

82 model = User 

83 fields = PROFILE_FIELDS 

84 

85 

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 ) 

102 

103 

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 ) 

115 

116 

117class UserAdminChangeForm(admin_forms.UserChangeForm): 

118 class Meta(admin_forms.UserChangeForm.Meta): # type: ignore[name-defined] 

119 model = User 

120 

121 def clean_phone(self): 

122 return normalize_phone(self.cleaned_data.get("phone")) or None 

123 

124 

125class UserAdminCreationForm(forms.ModelForm): 

126 """Admin form for creating a user with email-only sign-in.""" 

127 

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) 

131 

132 class Meta: 

133 model = User 

134 fields = ("email",) 

135 

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 

142 

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