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

130 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-03 13:52 +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.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: 19 ↛ 20line 19 didn't jump to line 20 because the condition on line 19 was never true

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=True, label=_("E-mail")) 

35 request_email_subject = forms.CharField( 

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

37 ) 

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

39 reviewer_first_name = forms.CharField( 

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

41 max_length=150, 

42 required=False, 

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

44 ) 

45 reviewer_last_name = forms.CharField( 

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

47 max_length=150, 

48 required=False, 

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

50 ) 

51 reviewer_email = forms.EmailField( 

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

53 required=False, 

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

55 ) 

56 

57 class Meta: 

58 model = Review 

59 fields = [ 

60 "suggested_user", 

61 "reviewer_first_name", 

62 "reviewer_last_name", 

63 "reviewer_email", 

64 "request_email_subject", 

65 "request_email", 

66 "date_response_due", 

67 "date_review_due", 

68 ] 

69 

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

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

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

73 choices = kwargs.pop("_choices") 

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

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

76 select_field = self.fields["suggested_user"] 

77 select_field.choices = choices 

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

79 select_field.required = False 

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

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

82 select_field.empty_label = None 

83 

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

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

86 # reviewer_field.queryset = reviewers 

87 # reviewer_field.required = False 

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

89 # reviewer_field.separator_after_or = True 

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

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

92 # reviewer_field.empty_label = None 

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

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

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

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

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

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

99 

100 def clean_date_response_due(self) -> date: 

101 date_input = self.cleaned_data["date_response_due"] 

102 if date_input < date.today(): 

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

104 

105 return date_input 

106 

107 def clean_date_review_due(self) -> date: 

108 date_input = self.cleaned_data["date_review_due"] 

109 if date_input < date.today(): 

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

111 

112 return date_input 

113 

114 def clean_reviewer_email(self) -> str: 

115 email = self.cleaned_data["reviewer_email"] 

116 if not email: 

117 return email 

118 if ( 

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

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

121 ): 

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

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

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

125 return email 

126 

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

128 cleaned_data = super().clean() 

129 if self.errors: 

130 return cleaned_data 

131 

132 # Check dates validity 

133 response_date = cleaned_data["date_response_due"] 

134 review_date = cleaned_data["date_review_due"] 

135 if response_date > review_date: 

136 raise ValidationError( 

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

138 ) 

139 

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

141 suggested_user = cleaned_data["suggested_user"] 

142 reviewer_select = cleaned_data["reviewer_select"] 

143 first_name = cleaned_data["reviewer_first_name"] 

144 last_name = cleaned_data["reviewer_last_name"] 

145 email = cleaned_data["reviewer_email"] 

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

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

148 ): 

149 raise ValidationError( 

150 _( 

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

152 ) 

153 ) 

154 

155 return cleaned_data 

156 

157 

158class ReviewAcceptForm(MeshFormMixin, forms.ModelForm): 

159 """ 

160 Accept/decline form for the reviewer. 

161 """ 

162 

163 custom_display = "block" 

164 

165 date_response_due_display = forms.DateField( 

166 label=_("Response due date"), disabled=True, required=False 

167 ) 

168 date_review_due_display = forms.DateField( 

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

170 ) 

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

172 

173 class Meta: 

174 model = Review 

175 fields = [ 

176 "date_response_due_display", 

177 "date_review_due_display", 

178 "accepted", 

179 "accept_comment", 

180 ] 

181 

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

183 """ 

184 Set date fields input type. 

185 """ 

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

187 self.fields["date_response_due_display"].widget.input_type = "date" 

188 # self.fields["date_response_due_display"].custom_display = "inline" 

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

190 # self.fields["date_review_due_display"].custom_display = "inline" 

191 

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

193 cleaned_data = super().clean() 

194 if cleaned_data["accepted"] is False and not cleaned_data["accept_comment"]: 

195 raise ValidationError( 

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

197 ) 

198 return cleaned_data 

199 

200 

201class ReviewSubmitForm(SubmittableModelForm, FileModelForm): 

202 """ 

203 Form used to submit/complete a review. 

204 """ 

205 

206 custom_display = "block" 

207 

208 date_review_due_display = forms.DateField( 

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

210 ) 

211 additional_files = FileField( 

212 required=True, 

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

214 allow_multiple_selected=True, 

215 model_class=ReviewAdditionalFile, 

216 related_name="additional_files", 

217 ) 

218 comment = CKEditorFormField( 

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

220 ) 

221 

222 class Meta: 

223 model = Review 

224 fields = [ 

225 "date_review_due_display", 

226 "recommendation", 

227 "additional_files", 

228 "comment", 

229 ] 

230 

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

232 """ 

233 Sets the date fields input type. 

234 """ 

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

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

237 

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

239 cleaned_data = super().clean() 

240 

241 if cleaned_data["recommendation"] is None: 

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

243 

244 return cleaned_data 

245 

246 

247class ReviewConfirmForm(MeshFormMixin, forms.Form): 

248 custom_display = "inline" 

249 

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

251 

252 

253class ReviewAutoCreateForm(MeshFormMixin, forms.ModelForm): 

254 """ 

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

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

257 """ 

258 

259 custom_display = "block" 

260 

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

262 request_email_subject = forms.CharField( 

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

264 ) 

265 

266 class Meta: 

267 model = Review 

268 fields = [ 

269 "request_email_subject", 

270 "request_email", 

271 "date_review_due", 

272 ] 

273 

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

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

276 

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

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

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

280 

281 def clean_date_review_due(self) -> date: 

282 date_input = self.cleaned_data["date_review_due"] 

283 if date_input < date.today(): 

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

285 

286 return date_input