Coverage for src / mesh / views / forms / review_forms.py: 85%

145 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-09 13:14 +0000

1from __future__ import annotations 

2 

3from datetime import date 

4from typing import TYPE_CHECKING, Any 

5 

6from allauth.account.models import EmailAddress 

7from django import forms 

8from django.contrib.auth import get_user_model 

9from django.core.exceptions import ValidationError 

10from django.forms.widgets import RadioSelect 

11from django.utils.translation import gettext_lazy as _ 

12 

13from mesh.models.orm.review_models import Review, ReviewAdditionalFile 

14from mesh.views.components.ckeditor_config import DEFAULT_CKEDITOR_CONFIG, EMAIL_CKEDITOR_CONFIG 

15 

16from .base_forms import FileModelForm, MeshFormMixin, SubmittableModelForm 

17from .fields import CKEditorFormField, FileField 

18 

19if TYPE_CHECKING: 

20 pass 

21 

22UserModel = get_user_model() 

23 

24 

25class ReviewCreateForm(MeshFormMixin, forms.ModelForm): 

26 """ 

27 Form used to create a new review, ie. request a new review from another user. 

28 """ 

29 

30 custom_display = "block" 

31 reviewer_select = forms.ChoiceField( 

32 required=False, choices=[("shortlist", "Shortlist"), ("new", "New")], widget=RadioSelect 

33 ) 

34 request_email = CKEditorFormField(EMAIL_CKEDITOR_CONFIG, required=False, label=_("E-mail")) 

35 request_email_subject = forms.CharField( 

36 label=_("E-mail subject"), max_length=256, required=False 

37 ) 

38 quick_request_email = CKEditorFormField( 

39 EMAIL_CKEDITOR_CONFIG, required=False, label=_("E-mail") 

40 ) 

41 quick_request_email_subject = forms.CharField( 

42 label=_("E-mail subject"), max_length=256, required=False 

43 ) 

44 

45 suggested_user = forms.ChoiceField(choices=[]) 

46 reviewer_first_name = forms.CharField( 

47 label=_("Reviewer - First name"), 

48 max_length=150, 

49 required=False, 

50 help_text=_("Leave empty for an existing user"), 

51 ) 

52 reviewer_last_name = forms.CharField( 

53 label=_("Reviewer - Last name"), 

54 max_length=150, 

55 required=False, 

56 help_text=_("Leave empty for an existing user"), 

57 ) 

58 reviewer_email = forms.EmailField( 

59 label=_("Reviewer - E-mail address"), 

60 required=False, 

61 help_text=_("Leave empty for an existing user"), 

62 ) 

63 

64 class Meta: 

65 model = Review 

66 fields = [ 

67 "suggested_user", 

68 "reviewer_first_name", 

69 "reviewer_last_name", 

70 "reviewer_email", 

71 "request_email_subject", 

72 "request_email", 

73 "quick", 

74 "date_response_due", 

75 "date_review_due", 

76 ] 

77 

78 def __init__(self, *args, **kwargs): 

79 # self.suggestions = kwargs.pop("_suggestions") 

80 # self.existing_reviewers = kwargs.pop("_existing_reviewers") 

81 choices = kwargs.pop("_choices") 

82 self.version = kwargs.pop("_version") 

83 super().__init__(*args, **kwargs) 

84 select_field = self.fields["suggested_user"] 

85 select_field.choices = choices 

86 # select_field.choices = get_suggested_users_choices(self.suggestions, self.existing_reviewers) 

87 select_field.required = False 

88 # select_field.widget.attrs["size"] = 10 

89 # select_field.widget.attrs["class"] = "form-select form-select-lg" 

90 select_field.empty_label = None 

91 

92 # reviewer_field.help_text = _("Existing user") 

93 # reviewer_field = self.fields["reviewer"] 

94 # reviewer_field.queryset = reviewers 

95 # reviewer_field.required = False 

96 # reviewer_field.help_text = _("Existing user") 

97 # reviewer_field.separator_after_or = True 

98 # reviewer_field.widget.attrs["size"] = 10 

99 # reviewer_field.widget.attrs["class"] = "form-select form-select-lg" 

100 # reviewer_field.empty_label = None 

101 self.fields["reviewer_email"].separator_after = True 

102 self.fields["date_response_due"].widget.input_type = "date" 

103 self.fields["date_response_due"].widget.attrs["class"] = "form-control form-control-lg" 

104 self.fields["date_review_due"].widget.input_type = "date" 

105 self.fields["date_review_due"].widget.attrs["class"] = "form-control form-control-lg" 

106 self.fields["request_email"].widget.attrs["class"] = "form-control" 

107 

108 def clean_date_response_due(self) -> date: 

109 date_input = self.cleaned_data["date_response_due"] 

110 if date_input < date.today(): 

111 raise ValidationError(_("The response due date must be later than today.")) 

112 

113 return date_input 

114 

115 def clean_date_review_due(self) -> date: 

116 date_input = self.cleaned_data["date_review_due"] 

117 if date_input < date.today(): 

118 raise ValidationError(_("The response due date must be later than today.")) 

119 

120 return date_input 

121 

122 def clean_reviewer_email(self) -> str: 

123 email = self.cleaned_data["reviewer_email"] 

124 if not email: 

125 return email 

126 if ( 

127 EmailAddress.objects.filter(email=email).exists() 

128 or UserModel.objects.filter(email=email).exists() 

129 ): 

130 raise ValidationError(_("The given e-mail address is already used.")) 

131 if self.version.reviews.filter(reviewer__email=email).exists(): 

132 raise ValidationError(_("The user is already assigned for a review.")) 

133 return email 

134 

135 def clean(self) -> dict[str, Any]: 

136 cleaned_data = super().clean() 

137 

138 if self.errors: 

139 return cleaned_data 

140 

141 # Check dates validity 

142 response_date = cleaned_data["date_response_due"] 

143 review_date = cleaned_data["date_review_due"] 

144 if response_date > review_date: 

145 raise ValidationError( 

146 _("The response due date must be earlier than the review due date.") 

147 ) 

148 

149 # Check reviewer validity (either existing user or name + email fields) 

150 suggested_user = cleaned_data["suggested_user"] 

151 reviewer_select = cleaned_data["reviewer_select"] 

152 first_name = cleaned_data["reviewer_first_name"] 

153 last_name = cleaned_data["reviewer_last_name"] 

154 email = cleaned_data["reviewer_email"] 

155 if (reviewer_select == "shortlist" and not suggested_user) or ( 

156 reviewer_select == "new" and not (first_name and last_name and email) 

157 ): 

158 raise ValidationError( 

159 _( 

160 "You must either pick an existing user OR fill the first name, last name and e-mail fields." 

161 ) 

162 ) 

163 

164 quick = cleaned_data["quick"] 

165 if quick: 

166 quick_request_email = cleaned_data["quick_request_email"] 

167 quick_request_email_subject = cleaned_data["quick_request_email_subject"] 

168 

169 if quick_request_email == "" or quick_request_email_subject == "": 

170 raise ValidationError(_("You must fill the email subject and content")) 

171 else: 

172 request_email = cleaned_data["request_email"] 

173 request_email_subject = cleaned_data["request_email_subject"] 

174 

175 if request_email == "" or request_email_subject == "": 

176 raise ValidationError(_("You must fill the email subject and content")) 

177 

178 return cleaned_data 

179 

180 

181class ReviewAcceptForm(MeshFormMixin, forms.ModelForm): 

182 """ 

183 Accept/decline form for the reviewer. 

184 """ 

185 

186 custom_display = "block" 

187 

188 accept_comment = CKEditorFormField(DEFAULT_CKEDITOR_CONFIG, required=False, label=_("Comment")) 

189 

190 class Meta: 

191 model = Review 

192 fields = [ 

193 "accept_comment", 

194 ] 

195 

196 def clean(self) -> dict[str, Any]: 

197 cleaned_data = super().clean() 

198 return cleaned_data 

199 

200 

201class ReviewDeclineForm(MeshFormMixin, forms.ModelForm): 

202 """ 

203 Accept/decline form for the reviewer. 

204 """ 

205 

206 custom_display = "block" 

207 

208 accept_comment = CKEditorFormField(DEFAULT_CKEDITOR_CONFIG, required=False, label=_("Comment")) 

209 

210 class Meta: 

211 model = Review 

212 fields = [ 

213 "accept_comment", 

214 ] 

215 

216 def __init__(self, *args, **kwargs): 

217 """ 

218 Set date fields input type. 

219 """ 

220 super().__init__(*args, **kwargs) 

221 

222 def clean(self) -> dict[str, Any]: 

223 cleaned_data = super().clean() 

224 if not cleaned_data["accept_comment"]: 

225 raise ValidationError( 

226 _("You must fill the comment field when declining the review request.") 

227 ) 

228 return cleaned_data 

229 

230 

231class ReviewSubmitForm(SubmittableModelForm, FileModelForm): 

232 """ 

233 Form used to submit/complete a review. 

234 """ 

235 

236 custom_display = "block" 

237 

238 date_review_due_display = forms.DateField( 

239 label=_("Review due date"), disabled=True, required=False 

240 ) 

241 additional_files = FileField( 

242 required=True, 

243 label=_("Review file(s)"), 

244 allow_multiple_selected=True, 

245 model_class=ReviewAdditionalFile, 

246 related_name="additional_files", 

247 ) 

248 comment = CKEditorFormField( 

249 DEFAULT_CKEDITOR_CONFIG, label=_("Comment / Message"), required=False 

250 ) 

251 

252 class Meta: 

253 model = Review 

254 fields = [ 

255 "date_review_due_display", 

256 "recommendation", 

257 "additional_files", 

258 "comment", 

259 ] 

260 

261 def __init__(self, *args, **kwargs) -> None: 

262 """ 

263 Sets the date fields input type. 

264 """ 

265 super().__init__(*args, **kwargs) 

266 self.fields["date_review_due_display"].widget.input_type = "date" 

267 

268 def clean(self) -> dict[str, Any]: 

269 cleaned_data = super().clean() 

270 

271 if cleaned_data["recommendation"] is None: 

272 raise ValidationError(_("You must choose a recommendation value.")) 

273 

274 return cleaned_data 

275 

276 

277class ReviewConfirmForm(MeshFormMixin, forms.Form): 

278 custom_display = "inline" 

279 

280 confirm = forms.BooleanField(label=_("Confirm"), required=True) 

281 

282 

283class ReviewAutoCreateForm(MeshFormMixin, forms.ModelForm): 

284 """ 

285 Form used to create new reviews, based on the previous round 

286 Reviewers who submitted a "positive" review (Accept ou Revision request) are auto assigned (no acceptance deadline) 

287 """ 

288 

289 custom_display = "block" 

290 

291 request_email = CKEditorFormField(EMAIL_CKEDITOR_CONFIG, required=True, label=_("E-mail")) 

292 request_email_subject = forms.CharField( 

293 label=_("E-mail subject"), max_length=256, required=True 

294 ) 

295 

296 class Meta: 

297 model = Review 

298 fields = [ 

299 "request_email_subject", 

300 "request_email", 

301 "date_review_due", 

302 ] 

303 

304 def __init__(self, *args, **kwargs): 

305 super().__init__(*args, **kwargs) 

306 

307 self.fields["date_review_due"].widget.input_type = "date" 

308 self.fields["date_review_due"].widget.attrs["class"] = "form-control form-control-lg" 

309 self.fields["request_email"].widget.attrs["class"] = "form-control" 

310 

311 def clean_date_review_due(self) -> date: 

312 date_input = self.cleaned_data["date_review_due"] 

313 if date_input < date.today(): 

314 raise ValidationError(_("The response due date must be later than today.")) 

315 

316 return date_input