Coverage for src / mesh / models / roles / base_role.py: 86%

148 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-22 07:20 +0000

1from __future__ import annotations 

2 

3from abc import ABC, abstractmethod 

4from dataclasses import asdict, dataclass 

5from typing import TYPE_CHECKING, ClassVar 

6 

7from django.db.models import Case, CharField, F, Prefetch, Value, When 

8from django.db.models.functions import Concat 

9from django.utils.translation import gettext 

10 

11from mesh.models.orm import Comment, Discussion 

12from mesh.models.orm.editorial_models import EditorialDecision 

13from mesh.models.orm.review_models import ReviewAdditionalFile 

14from mesh.models.orm.submission_models import SubmissionAuthor, SubmissionLog 

15from mesh.models.roles import aggregates_conf 

16 

17if TYPE_CHECKING: 

18 from django.db.models import BaseManager, Combinable, QuerySet 

19 

20 from mesh.models.orm.review_models import Review 

21 from mesh.models.orm.submission_models import ( 

22 Submission, 

23 SubmissionVersion, 

24 ) 

25 from mesh.models.orm.submission_status import SubmissionStatusData 

26 from mesh.models.orm.user_models import User 

27 

28 

29@dataclass 

30class RoleSummary: 

31 code: str 

32 name: str 

33 icon_class: str 

34 submission_list_title: str 

35 

36 def serialize(self) -> dict: 

37 return asdict(self) 

38 

39 

40class Role(ABC): 

41 """ 

42 Base interface for a role object. 

43 """ 

44 

45 # Role code - Stored in user table keep track of the user current role. 

46 _CODE: ClassVar[str] 

47 # Role name 

48 _NAME: ClassVar[str] 

49 # Font-awesome 6 icon class (ex: "fa-user") used to represent the role. 

50 _ICON_CLASS: ClassVar[str] 

51 # Title for the "mesh:submission_list" view 

52 _SUBMISSION_LIST_TITLE: ClassVar[str] = gettext("My submissions") 

53 user: User 

54 

55 # Filtered querysets for permission management 

56 submissions_queryset: BaseManager[Submission] 

57 "filters the submissions the user has access to" 

58 

59 reviews_queryset: BaseManager[Review] 

60 "populates SubmissionVersion.reviews_censored" 

61 

62 created_by_censored_annotate: Combinable 

63 "populates Submission/SubmissionVersion.created_by_censored" 

64 

65 authors_string_annotate: Combinable 

66 "populates Submission.authors_string" 

67 

68 authors_queryset: BaseManager[SubmissionAuthor] 

69 "populates Submission.authors_censored" 

70 

71 reviewer_censored_annotate: Combinable 

72 "populates Review.reviewer_censored" 

73 

74 versions_queryset: BaseManager[SubmissionVersion] 

75 "populates Submission.versions_censored" 

76 

77 reviews_additional_files_queryset: BaseManager[ReviewAdditionalFile] 

78 "populates Review.additional_files_censored" 

79 

80 log_messages_queryset: BaseManager[SubmissionLog] 

81 "populates Submission.log_messages_censored" 

82 

83 def __init__(self, user: User) -> None: 

84 self.created_by_censored_annotate = Case( 

85 When(created_by=None, then=Value("None")), 

86 default=Concat( 

87 F("created_by__first_name"), 

88 Value(" "), 

89 F("created_by__last_name"), 

90 Value(" ("), 

91 F("created_by__email"), 

92 Value(")"), 

93 output_field=CharField(), 

94 ), 

95 ) 

96 self.authors_string_annotate = aggregates_conf["authors_string_aggregate"] 

97 self.authors_queryset = SubmissionAuthor.objects.order_by("first_name") 

98 self.reviewer_censored_annotate = aggregates_conf["reviewer_string_aggregate"] 

99 self.reviews_additional_files_queryset = ReviewAdditionalFile.objects.all() 

100 self.log_messages_queryset = SubmissionLog.objects.none() 

101 self.user = user 

102 self.is_active = self._get_is_active() 

103 

104 @abstractmethod 

105 def _get_is_active(self) -> bool: 

106 """This is a private method. Do not use ! 

107 

108 Use `role.is_active` instead 

109 """ 

110 return False 

111 

112 @classmethod 

113 def code(cls) -> str: 

114 """ 

115 Returns the role's code. 

116 """ 

117 return cls._CODE 

118 

119 @classmethod 

120 def name(cls) -> str: 

121 """ 

122 Returns the role's display name. 

123 """ 

124 return cls._NAME 

125 

126 @classmethod 

127 def icon_class(cls) -> str: 

128 """ 

129 Returns the role's icon HTML tag (it uses font awesome 6). 

130 """ 

131 return cls._ICON_CLASS 

132 

133 @classmethod 

134 def submissions_list_title(cls) -> str: 

135 return cls._SUBMISSION_LIST_TITLE 

136 

137 @classmethod 

138 def summary(cls) -> RoleSummary: 

139 return RoleSummary( 

140 code=cls.code(), 

141 name=cls.name(), 

142 icon_class=cls.icon_class(), 

143 submission_list_title=cls.submissions_list_title(), 

144 ) 

145 

146 def accept(self, visitor, submission, *args, **kwargs): 

147 return visitor.visit(submission, *args, **kwargs) 

148 

149 @abstractmethod 

150 def get_submissions(self) -> QuerySet[Submission]: 

151 """ 

152 Returns the queryset of submissions the user has access to. 

153 """ 

154 

155 def get_current_open_review(self, version: SubmissionVersion) -> Review | None: 

156 """ 

157 Returns the current open review for the given submission, if any. 

158 Current review = Round not closed + review not submitted. 

159 """ 

160 return None 

161 

162 @abstractmethod 

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

164 """ 

165 Returns the submission status according to the user role + an optional string 

166 describing the submission status. 

167 Ex: (WAITING, "X reports missing") for an editor+ 

168 (WAITING, "Under review") for the author 

169 (TODO, "Reports due for {{date}}") for a reviewer 

170 """ 

171 

172 def show_no_actions_message(self, submission: Submission) -> bool: 

173 """ 

174 Whether to show a 'thank you, wait' message instead of action buttons. 

175 Only relevant for the author role when the submission is not editable. 

176 """ 

177 return False 

178 

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

180 # """ 

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

182 # """ 

183 # return [ 

184 # SubmissionListConfig( 

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

186 # ), 

187 # SubmissionListConfig( 

188 # key=SubmissionStatus.WAITING, 

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

190 # html_classes="waiting", 

191 # ), 

192 # SubmissionListConfig( 

193 # key=SubmissionStatus.ARCHIVED, 

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

195 # html_classes="archived", 

196 # ), 

197 # ] 

198 # 

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

200 # """ 

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

202 # """ 

203 # return self.get_archived_submission_list_config() 

204 

205 def can_create_submission(self) -> bool: 

206 """ 

207 Wether the user role has rights to create a new submission. 

208 """ 

209 return False 

210 

211 @abstractmethod 

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

213 """ 

214 Wether the user role can access the given submission. 

215 """ 

216 return False 

217 

218 def can_edit_submission(self, submission: Submission) -> bool: 

219 """ 

220 Whether the user role can edit the given submission. 

221 """ 

222 return True 

223 

224 def can_submit_submission(self, submission: Submission) -> bool: 

225 """ 

226 Whether the user role can submit the given submission. 

227 It doesn't check whether the submission has the required fields to be submitted. 

228 """ 

229 return self.can_edit_submission(submission) and submission.is_draft 

230 

231 def can_create_version(self, submission: Submission) -> bool: 

232 """ 

233 Whether the user role can submit a new version for the given submission 

234 """ 

235 return False 

236 

237 def can_edit_version(self, version: SubmissionVersion) -> bool: 

238 """ 

239 Whether the user role can edit the given submission version. 

240 """ 

241 return False 

242 

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

244 """ 

245 Whether the user role can view the data of the given submission version. 

246 """ 

247 return False 

248 

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

250 """ 

251 Whether the user role can send the submission into the review process. 

252 """ 

253 return False 

254 

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

256 """ 

257 Whether the user role can create an editorial decision for the given submission. 

258 """ 

259 return False 

260 

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

262 """ 

263 Whether the user role can edit the given editorial decision. 

264 """ 

265 return False 

266 

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

268 """ 

269 Whether the user role can view the reviews section of the given submission 

270 version. 

271 """ 

272 return False 

273 

274 def can_access_review(self, submission: Submission, review: Review) -> bool: 

275 """ 

276 Whether the user role can view the data of the given review. 

277 """ 

278 return False 

279 

280 def can_edit_review(self, submission: Submission, review: Review) -> bool: 

281 """ 

282 Whether the user role can edit the given review. 

283 """ 

284 return False 

285 

286 def can_submit_review(self, review: Review) -> bool: 

287 """ 

288 Whether the user role can submit the given review. 

289 """ 

290 return False 

291 

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

293 """ 

294 Whether the user role can view the review's author name 

295 """ 

296 return False 

297 

298 def can_access_review_file(self, submission: Submission, file: ReviewAdditionalFile) -> bool: 

299 """ 

300 Whether the user role can access the given review's file. 

301 """ 

302 return False 

303 

304 def can_access_review_details(self, submission: Submission, review: Review) -> bool: 

305 """ 

306 Whether the user role can access the review details. 

307 """ 

308 return False 

309 

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

311 """ 

312 Whether the user role can invite reviewer for the given submission version. 

313 """ 

314 return False 

315 

316 def can_impersonate(self) -> bool: 

317 """ 

318 Whether the user role can impersonate other users. 

319 """ 

320 return False 

321 

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

323 """ 

324 Whether the user role can view the submission log. 

325 """ 

326 return False 

327 

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

329 """ 

330 Whether the user role can assign an editor to the given submission. 

331 """ 

332 return False 

333 

334 def can_filter_submissions(self) -> bool: 

335 """ 

336 Whether the user role can use filters on the submission list dashboard. 

337 """ 

338 return False 

339 

340 def can_access_journal_sections(self) -> bool: 

341 """ 

342 Whether the user can access the submission journal_sections views. 

343 """ 

344 return False 

345 

346 def can_edit_journal_sections(self) -> bool: 

347 """ 

348 Whether the user can edit the submission journal_sections. 

349 """ 

350 return False 

351 

352 def can_edit_review_file_right(self, review: Review, submission=None) -> bool: 

353 """ 

354 Whether the user can edit the review file access right. 

355 """ 

356 return False 

357 

358 def can_access_last_activity(self) -> bool: 

359 """ 

360 Whether the user role can view the last activity. 

361 """ 

362 return True 

363 

364 def can_access_shortcut_actions(self) -> bool: 

365 return False 

366 

367 def _is_comment_author(self, comment: Comment) -> bool: 

368 return self.user.pk == comment.author.pk 

369 

370 def can_delete_comment(self, comment: Comment): 

371 return self._is_comment_author(comment) 

372 

373 def can_access_discussion(self, discussion: Discussion) -> bool: 

374 return bool(discussion) and discussion.members.filter(pk=self.user.pk).exists() 

375 

376 def can_edit_comment(self, comment: Comment) -> bool: 

377 return self._is_comment_author(comment) 

378 

379 def can_create_discussion(self, submission: Submission) -> bool: 

380 return False 

381 

382 def _annotate_submission_query( 

383 self, 

384 submission_qs: QuerySet[Submission], 

385 ): 

386 return submission_qs.annotate( 

387 authors_string=self.authors_string_annotate, 

388 created_by_censored=self.created_by_censored_annotate, 

389 ).prefetch_related( 

390 # authors_censored 

391 Prefetch( 

392 "authors", 

393 queryset=self.authors_queryset, 

394 to_attr="authors_censored", 

395 ), 

396 # versions_censored 

397 Prefetch( 

398 "versions", 

399 queryset=self.versions_queryset.order_by("-number") 

400 .select_related("main_file") 

401 .annotate( 

402 created_by_censored=self.created_by_censored_annotate, 

403 ) 

404 .prefetch_related( 

405 # reviews_censored 

406 Prefetch( 

407 "reviews", 

408 queryset=self.reviews_queryset.annotate( 

409 reviewer_censored=self.reviewer_censored_annotate 

410 ).prefetch_related( 

411 Prefetch( 

412 "additional_files", 

413 queryset=self.reviews_additional_files_queryset, 

414 to_attr="additional_files_censored", 

415 ) 

416 ), 

417 to_attr="reviews_censored", 

418 ), 

419 # editorial_decision 

420 # TBD: Should this be censored (for the reviewer) ? 

421 Prefetch( 

422 "editorial_decision", 

423 queryset=EditorialDecision.objects.all() 

424 .select_related("created_by") 

425 .prefetch_related("additional_files"), 

426 ), 

427 "additional_files", 

428 ), 

429 to_attr="versions_censored", 

430 ), 

431 # log_messages_censored 

432 Prefetch( 

433 "log_messages", 

434 queryset=self.log_messages_queryset, 

435 to_attr="log_messages_censored", 

436 ), 

437 )