Coverage for ivatar/ivataraccount/forms.py: 100%

77 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-12 23:12 +0000

1# -*- coding: utf-8 -*- 

2""" 

3Classes for our ivatar.ivataraccount.forms 

4""" 

5from urllib.parse import urlsplit, urlunsplit 

6 

7from django import forms 

8from django.utils.translation import gettext_lazy as _ 

9 

10from ipware import get_client_ip 

11 

12from ivatar import settings 

13from ivatar.settings import MIN_LENGTH_EMAIL, MAX_LENGTH_EMAIL 

14from ivatar.settings import MIN_LENGTH_URL, MAX_LENGTH_URL 

15from .models import UnconfirmedEmail, ConfirmedEmail, Photo 

16from .models import UnconfirmedOpenId, ConfirmedOpenId 

17from .models import UserPreference 

18 

19 

20MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT = 5 

21 

22 

23class AddEmailForm(forms.Form): 

24 """ 

25 Form to handle adding email addresses 

26 """ 

27 

28 email = forms.EmailField( 

29 label=_("Email"), 

30 min_length=MIN_LENGTH_EMAIL, 

31 max_length=MAX_LENGTH_EMAIL, 

32 ) 

33 

34 def clean_email(self): 

35 """ 

36 Enforce lowercase email 

37 """ 

38 # TODO: Domain restriction as in libravatar? 

39 return self.cleaned_data["email"].lower() 

40 

41 def save(self, request): 

42 """ 

43 Save the model, ensuring some safety 

44 """ 

45 user = request.user 

46 # Enforce the maximum number of unconfirmed emails a user can have 

47 num_unconfirmed = user.unconfirmedemail_set.count() 

48 

49 max_num_unconfirmed_emails = getattr( 

50 settings, "MAX_NUM_UNCONFIRMED_EMAILS", MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT 

51 ) 

52 

53 if num_unconfirmed >= max_num_unconfirmed_emails: 

54 self.add_error(None, _("Too many unconfirmed mail addresses!")) 

55 return False 

56 

57 # Check whether or not a confirmation email has been 

58 # sent by this user already 

59 if UnconfirmedEmail.objects.filter( # pylint: disable=no-member 

60 user=user, email=self.cleaned_data["email"] 

61 ).exists(): 

62 self.add_error("email", _("Address already added, currently unconfirmed")) 

63 return False 

64 

65 # Check whether or not the email is already confirmed (by someone) 

66 check_mail = ConfirmedEmail.objects.filter(email=self.cleaned_data["email"]) 

67 if check_mail.exists(): 

68 msg = _("Address already confirmed (by someone else)") 

69 if check_mail.first().user == request.user: 

70 msg = _("Address already confirmed (by you)") 

71 self.add_error("email", msg) 

72 return False 

73 

74 unconfirmed = UnconfirmedEmail() 

75 unconfirmed.email = self.cleaned_data["email"] 

76 unconfirmed.user = user 

77 unconfirmed.save() 

78 unconfirmed.send_confirmation_mail(url=request.build_absolute_uri("/")[:-1]) 

79 return True 

80 

81 

82class UploadPhotoForm(forms.Form): 

83 """ 

84 Form handling photo upload 

85 """ 

86 

87 photo = forms.FileField( 

88 label=_("Photo"), 

89 error_messages={"required": _("You must choose an image to upload.")}, 

90 ) 

91 not_porn = forms.BooleanField( 

92 label=_("suitable for all ages (i.e. no offensive content)"), 

93 required=True, 

94 error_messages={ 

95 "required": _( 

96 'We only host "G-rated" images and so this field must be checked.' 

97 ) 

98 }, 

99 ) 

100 can_distribute = forms.BooleanField( 

101 label=_("can be freely copied"), 

102 required=True, 

103 error_messages={ 

104 "required": _( 

105 "This field must be checked since we need to be able to distribute photos to third parties." 

106 ) 

107 }, 

108 ) 

109 

110 @staticmethod 

111 def save(request, data): 

112 """ 

113 Save the model and assign it to the current user 

114 """ 

115 # Link this file to the user's profile 

116 photo = Photo() 

117 photo.user = request.user 

118 photo.ip_address = get_client_ip(request)[0] 

119 photo.data = data.read() 

120 photo.save() 

121 return photo if photo.pk else None 

122 

123 

124class AddOpenIDForm(forms.Form): 

125 """ 

126 Form to handle adding OpenID 

127 """ 

128 

129 openid = forms.URLField( 

130 label=_("OpenID"), 

131 min_length=MIN_LENGTH_URL, 

132 max_length=MAX_LENGTH_URL, 

133 initial="http://", 

134 ) 

135 

136 def clean_openid(self): 

137 """ 

138 Enforce restrictions 

139 """ 

140 # Lowercase hostname port of the URL 

141 url = urlsplit(self.cleaned_data["openid"]) 

142 return urlunsplit( 

143 ( 

144 url.scheme.lower(), 

145 url.netloc.lower(), 

146 url.path, 

147 url.query, 

148 url.fragment, 

149 ) 

150 ) 

151 

152 def save(self, user): 

153 """ 

154 Save the model, ensuring some safety 

155 """ 

156 if ConfirmedOpenId.objects.filter( # pylint: disable=no-member 

157 openid=self.cleaned_data["openid"] 

158 ).exists(): 

159 self.add_error("openid", _("OpenID already added and confirmed!")) 

160 return False 

161 

162 if UnconfirmedOpenId.objects.filter( # pylint: disable=no-member 

163 openid=self.cleaned_data["openid"] 

164 ).exists(): 

165 self.add_error("openid", _("OpenID already added, but not confirmed yet!")) 

166 return False 

167 

168 unconfirmed = UnconfirmedOpenId() 

169 unconfirmed.openid = self.cleaned_data["openid"] 

170 unconfirmed.user = user 

171 unconfirmed.save() 

172 

173 return unconfirmed.pk 

174 

175 

176class UpdatePreferenceForm(forms.ModelForm): 

177 """ 

178 Form for updating user preferences 

179 """ 

180 

181 class Meta: # pylint: disable=too-few-public-methods 

182 """ 

183 Meta class for UpdatePreferenceForm 

184 """ 

185 

186 model = UserPreference 

187 fields = ["theme"] 

188 

189 

190class UploadLibravatarExportForm(forms.Form): 

191 """ 

192 Form handling libravatar user export upload 

193 """ 

194 

195 export_file = forms.FileField( 

196 label=_("Export file"), 

197 error_messages={"required": _("You must choose an export file to upload.")}, 

198 ) 

199 not_porn = forms.BooleanField( 

200 label=_("suitable for all ages (i.e. no offensive content)"), 

201 required=True, 

202 error_messages={ 

203 "required": _( 

204 'We only host "G-rated" images and so this field must be checked.' 

205 ) 

206 }, 

207 ) 

208 can_distribute = forms.BooleanField( 

209 label=_("can be freely copied"), 

210 required=True, 

211 error_messages={ 

212 "required": _( 

213 "This field must be checked since we need to be able to\ 

214 distribute photos to third parties." 

215 ) 

216 }, 

217 ) 

218 

219 

220class DeleteAccountForm(forms.Form): 

221 password = forms.CharField( 

222 label=_("Password"), required=False, widget=forms.PasswordInput() 

223 )