Coverage for src/mesh/model/roles/editor.py: 90%

111 statements  

« prev     ^ index     » next       coverage.py v7.7.0, created at 2025-04-28 07:45 +0000

1from collections.abc import Collection 

2from functools import cached_property 

3from itertools import chain 

4from typing import ClassVar 

5 

6from django.db.models import Q, QuerySet 

7from django.utils.translation import gettext as _ 

8 

9from mesh.models.editorial_models import EditorialDecision 

10from mesh.models.journal_models import JournalSection 

11from mesh.models.review_models import Review, ReviewAdditionalFile 

12from mesh.models.submission_models import Submission, SubmissionState, SubmissionVersion 

13from mesh.models.user_models import User 

14 

15from ..submission_status import SubmissionStatus, SubmissionStatusData 

16from .base_role import Role, RoleRights 

17 

18 

19class EditorRights(RoleRights): 

20 """ 

21 Base rights for every editor derived class. 

22 Basically they would have the same rights over a submission. The only difference is 

23 the available list of submissions. 

24 """ 

25 

26 @cached_property 

27 def journal_sections(self) -> list[JournalSection]: 

28 """ 

29 Recursively gets ALL the submission journal_sections the user has access to. 

30 That means all directly assigned journal_sections and all their children. 

31 """ 

32 direct_journal_sections = JournalSection.objects.filter(editors__user=self.user) 

33 if not direct_journal_sections: 

34 return [] 

35 

36 base_journal_sections = chain.from_iterable( 

37 JournalSection.objects.get_children_recursive(c) for c in direct_journal_sections 

38 ) 

39 

40 return [*direct_journal_sections, *base_journal_sections] 

41 

42 @cached_property 

43 def managed_submissions(self) -> QuerySet[Submission]: 

44 """ 

45 Editors have edit access to all submissions in their assigned sections that 

46 they did not author + all directly assigned submissions. 

47 """ 

48 submissions = Submission.objects.get_submissions().filter(editors__user=self.user) 

49 if self.journal_sections: 

50 submissions = submissions.union( 

51 Submission.objects.get_submissions() 

52 .filter(journal_section__in=self.journal_sections) 

53 .exclude(Q(created_by=self.user) | Q(state=SubmissionState.OPENED.value)) 

54 ) 

55 return submissions 

56 

57 @cached_property 

58 def submissions(self) -> QuerySet[Submission]: 

59 """ 

60 Editors have access to all submissions and associated data (TBD) 

61 """ 

62 return self.managed_submissions 

63 

64 def can_access_submission(self, submission: Submission) -> bool: 

65 return submission in self.submissions 

66 

67 def can_manage_submission(self, submission: Submission) -> bool: 

68 return submission in self.managed_submissions 

69 

70 def can_access_reviews(self, version: SubmissionVersion) -> bool: 

71 return self.can_access_submission(version.submission) 

72 

73 def can_access_review(self, review: Review) -> bool: 

74 return self.can_access_submission(review.version.submission) 

75 

76 def can_access_review_author(self, review: Review) -> bool: 

77 return self.can_manage_submission(review.version.submission) 

78 

79 def can_access_review_file(self, file: ReviewAdditionalFile) -> bool: 

80 return self.can_access_review(file.attached_to) 

81 

82 def can_access_review_details(self, review: Review) -> bool: 

83 return self.can_manage_submission(review.version.submission) 

84 

85 def can_invite_reviewer(self, version: SubmissionVersion) -> bool: 

86 """ 

87 A reviewer can be invited only if the version is submitted by the author and 

88 opened for review. 

89 """ 

90 return ( 

91 version.submitted 

92 and version.review_open 

93 and self.can_manage_submission(version.submission) 

94 ) 

95 

96 def can_access_submission_author(self, submission: Submission) -> bool: 

97 return self.can_access_submission(submission) 

98 

99 def can_access_version(self, version: SubmissionVersion) -> bool: 

100 """ 

101 Non-submitted versions are not available to anyone except the author. 

102 """ 

103 return version.submitted and self.can_access_submission(version.submission) 

104 

105 @cached_property 

106 def managed_users(self) -> QuerySet[User]: 

107 """ 

108 Restrict to non-editor users. 

109 

110 TODO: Restrict actions to manageable_submissions ? 

111 """ 

112 return ( 

113 User.objects.exclude(pk=self.user.pk) 

114 .exclude(journal_manager=True) 

115 .exclude(editor_sections__isnull=False) 

116 .exclude(editor_submissions__isnull=False) 

117 .exclude(is_superuser=True) 

118 ) 

119 

120 def can_impersonate(self) -> bool: 

121 return True 

122 

123 def can_access_submission_log(self, submission: Submission) -> bool: 

124 return self.can_access_submission(submission) 

125 

126 def can_create_editorial_decision(self, submission: Submission) -> bool: 

127 return self.can_manage_submission(submission) 

128 

129 def can_edit_editorial_decision(self, decision: EditorialDecision) -> bool: 

130 return self.can_manage_submission(decision.version.submission) 

131 

132 def can_start_review_process(self, submission: Submission) -> bool: 

133 return self.can_manage_submission(submission) and submission.is_reviewable 

134 

135 def can_assign_editor(self, submission: Submission) -> bool: 

136 return self.can_manage_submission(submission) 

137 

138 def get_submission_status(self, submission: Submission) -> SubmissionStatusData: 

139 """ 

140 For an editor+ role, the status highly depends on the submission state. 

141 - Submission/Revisions submitted -> The editor must take action 

142 - Revisions required -> Waiting for author to submit 

143 - Submission accepted/declined -> Nothing to do 

144 - Under review -> Tricky. Depends on the existing reviews (or not) state. 

145 """ 

146 if not self.can_access_submission(submission) or submission.is_draft: 

147 return SubmissionStatusData( 

148 submission=submission, status=SubmissionStatus.ERROR, description="" 

149 ) 

150 

151 # buttons = [] 

152 # if self.can_create_editorial_decision(submission): 

153 # buttons.append( 

154 # Button( 

155 # id=f"submission-editorial-decision-{submission.pk}", 

156 # title=_("Editorial decision"), 

157 # icon_class="fa-plus", 

158 # attrs={ 

159 # "href": [ 

160 # reverse_lazy( 

161 # "mesh:editorial_decision_create", 

162 # kwargs={"submission_pk": submission.pk}, 

163 # ) 

164 # ], 

165 # "class": ["as-button"], 

166 # }, 

167 # ) 

168 # ) 

169 

170 if self.can_assign_editor(submission) and not submission.all_assigned_editors: 

171 pass 

172 # buttons.append( 

173 # Button( 

174 # id=f"submission-assign-editor-{submission.pk}", 

175 # title=_("Assign editor"), 

176 # icon_class="fa-user-tie", 

177 # attrs={ 

178 # "href": [ 

179 # reverse_lazy("mesh:submission_editors", kwargs={"pk": submission.pk}) 

180 # ], 

181 # "class": ["as-button"], 

182 # }, 

183 # ) 

184 # ) 

185 

186 if submission.state in [ 186 ↛ 207line 186 didn't jump to line 207 because the condition on line 186 was never true

187 SubmissionState.SUBMITTED.value, 

188 SubmissionState.REVISION_SUBMITTED.value, 

189 ]: 

190 # if self.can_start_review_process(submission): 

191 # buttons.append( 

192 # Button( 

193 # id=f"submission-send-to-review-{submission.pk}", 

194 # title=_("Start review process"), 

195 # icon_class="fa-file-import", 

196 # form=StartReviewProcessForm(initial={"process": True}), 

197 # attrs={ 

198 # "href": [ 

199 # reverse_lazy( 

200 # "mesh:submission_start_review_process", 

201 # kwargs={"pk": submission.pk}, 

202 # ) 

203 # ] 

204 # }, 

205 # ) 

206 # ) 

207 return SubmissionStatusData( 

208 submission=submission, 

209 status=SubmissionStatus.TODO, 

210 description=_("Requires an editorial action."), 

211 # shortcut_actions=buttons, 

212 ) 

213 

214 elif submission.state in [ 

215 SubmissionState.OPENED.value, 

216 SubmissionState.REVISION_REQUESTED.value, 

217 ]: 

218 return SubmissionStatusData( 

219 submission=submission, 

220 status=SubmissionStatus.WAITING, 

221 description=_("Waiting for a new version"), 

222 # shortcut_actions=buttons, 

223 ) 

224 

225 elif submission.state in [ 

226 SubmissionState.ACCEPTED.value, 

227 SubmissionState.REJECTED.value, 

228 ]: 

229 return SubmissionStatusData( 

230 submission=submission, 

231 status=SubmissionStatus.DONE, 

232 description=_("Submission") + " " + submission.get_state_display(), # type:ignore 

233 ) 

234 

235 # Case under review 

236 version: SubmissionVersion = submission.current_version # type:ignore 

237 # if self.can_invite_reviewer(version): 

238 # buttons.append( 

239 # Button( 

240 # id=f"submission-referee-request-{submission.pk}", 

241 # title=_("Request review"), 

242 # icon_class="fa-user-group", 

243 # attrs={ 

244 # "href": [ 

245 # reverse_lazy("mesh:review_create", kwargs={"version_pk": version.pk}) 

246 # ], 

247 # "class": ["as-button"], 

248 # }, 

249 # ) 

250 # ) 

251 reviews: QuerySet[Review] = version.reviews.all() # type:ignore 

252 if not reviews: 

253 return SubmissionStatusData( 

254 submission=submission, 

255 status=SubmissionStatus.TODO, 

256 description=_("No reviewers for the current round."), 

257 # shortcut_actions=buttons, 

258 ) 

259 

260 # If some review requests are not answered and answer date is overdued 

261 if any(r.is_response_overdue for r in reviews): 

262 # if self.can_manage_submission(submission): 

263 # buttons.append( 

264 # Button( 

265 # id=f"submission-inspect-review-{submission.pk}", 

266 # title=_("Inspect review"), 

267 # icon_class="fa-magnifying-glass", 

268 # attrs={ 

269 # "href": [ 

270 # reverse_lazy( 

271 # "mesh:submission_details", kwargs={"pk": submission.pk} 

272 # ) 

273 # ], 

274 # "class": ["as-button", "button-light"], 

275 # }, 

276 # ) 

277 # ) 

278 return SubmissionStatusData( 

279 submission=submission, 

280 status=SubmissionStatus.TODO, 

281 description=_("At least 1 request answer is overdue."), 

282 # shortcut_actions=buttons, 

283 ) 

284 

285 # If some reports are not submitted and report date is overdued 

286 elif any(r.is_report_overdue for r in reviews): 286 ↛ 303line 286 didn't jump to line 303 because the condition on line 286 was never true

287 # if self.can_manage_submission(submission): 

288 # buttons.append( 

289 # Button( 

290 # id=f"submission-inspect-review-{submission.pk}", 

291 # title=_("Inspect review"), 

292 # icon_class="fa-magnifying-glass", 

293 # attrs={ 

294 # "href": [ 

295 # reverse_lazy( 

296 # "mesh:submission_details", kwargs={"pk": submission.pk} 

297 # ) 

298 # ], 

299 # "class": ["as-button", "button-light"], 

300 # }, 

301 # ) 

302 # ) 

303 return SubmissionStatusData( 

304 submission=submission, 

305 status=SubmissionStatus.TODO, 

306 description=_("At least 1 report is overdue."), 

307 # shortcut_actions=buttons, 

308 ) 

309 

310 # All reviews submitted 

311 elif all(r.is_completed for r in reviews): 

312 return SubmissionStatusData( 

313 submission=submission, 

314 status=SubmissionStatus.TODO, 

315 description=_("All reviews have been declined or submitted."), 

316 # shortcut_actions=buttons, 

317 ) 

318 

319 # Pending reviews, nothing to do ? 

320 return SubmissionStatusData( 

321 submission=submission, 

322 status=SubmissionStatus.WAITING, 

323 description=_("Waiting for review(s)."), 

324 # shortcut_actions=buttons, 

325 ) 

326 

327 # def get_submission_list_config(self) -> list[SubmissionListConfig]: 

328 # """ 

329 # Returns the config to display the submissions for the user role. 

330 # """ 

331 # return [ 

332 # SubmissionListConfig( 

333 # key=SubmissionStatus.TODO, title=_("Requires action"), html_classes="todo" 

334 # ), 

335 # SubmissionListConfig( 

336 # key=SubmissionStatus.WAITING, 

337 # title=_("Waiting for other's input"), 

338 # html_classes="waiting", 

339 # ), 

340 # SubmissionListConfig( 

341 # key=SubmissionStatus.ARCHIVED, 

342 # title=_("Closed / Archived"), 

343 # html_classes="archived", 

344 # in_filters=False, 

345 # display=False, 

346 # display_html=f"""<p>Please visit <a href="{reverse_lazy("mesh:submission_list_archived")}">this page</a> to consult all archived submissions.</p>""", 

347 # ), 

348 # ] 

349 # 

350 # def get_archived_submission_list_config(self) -> list[SubmissionListConfig]: 

351 # """ 

352 # Returns the config to display only the Archived submissions. 

353 # """ 

354 # return [ 

355 # SubmissionListConfig( 

356 # key=SubmissionStatus.ARCHIVED, 

357 # title=_("Closed / Archived"), 

358 # html_classes="archived", 

359 # ), 

360 # ] 

361 

362 def can_filter_submissions(self) -> bool: 

363 return True 

364 

365 def can_access_journal_sections(self) -> bool: 

366 return True 

367 

368 def can_edit_review_file_right(self, review: Review) -> bool: 

369 return self.can_manage_submission(review.version.submission) 

370 

371 

372class Editor(Role): 

373 """ 

374 Editor role. 

375 """ 

376 

377 _CODE: ClassVar[str] = "editor" 

378 _NAME: ClassVar[str] = _("Editor") 

379 _ICON_CLASS: ClassVar[str] = "fa-user-tie" 

380 _SUBMISSION_LIST_TITLE: ClassVar[str] = _("My assignments") 

381 rights: EditorRights 

382 

383 def __init__(self, user: User): 

384 super().__init__(user) 

385 

386 @property 

387 def active(self) -> bool: 

388 """ 

389 Editor role is active if the user has rights over any 

390 `JournalSection` or `Submission`. 

391 """ 

392 return len(self.rights.journal_sections) > 0 or len(self.rights.managed_submissions) > 0 

393 

394 def get_rights(self) -> EditorRights: 

395 return EditorRights(self.user) 

396 

397 

398def get_section_editors(submission: Submission) -> Collection[User]: 

399 """ 

400 Get the editors with access to the given submission from the EditorSectionRights. 

401 """ 

402 # 1 - Get all journal_sections above the given submission 

403 if not submission.journal_section: 403 ↛ 404line 403 didn't jump to line 404 because the condition on line 403 was never true

404 return [] 

405 

406 journal_sections = [ 

407 submission.journal_section, 

408 *JournalSection.objects.get_parents_recursive(submission.journal_section), 

409 ] 

410 

411 # 2 - Get all users with rights over one of the above journal_sections 

412 return User.objects.filter(editor_sections__journal_section__in=journal_sections)