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

139 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-08-11 09:47 +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.editorial_models import EditorialDecision 

12from mesh.models.orm.review_models import ReviewAdditionalFile 

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

14from mesh.models.roles import aggregates_conf 

15 

16if TYPE_CHECKING: 

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

18 

19 from mesh.models.orm.review_models import Review 

20 from mesh.models.orm.submission_models import ( 

21 Submission, 

22 SubmissionAuthor, 

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 pass 

155 

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

157 """ 

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

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

160 """ 

161 return None 

162 

163 @abstractmethod 

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

165 """ 

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

167 describing the submission status. 

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

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

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

171 """ 

172 pass 

173 

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

175 """ 

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

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

178 """ 

179 return False 

180 

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

182 # """ 

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

184 # """ 

185 # return [ 

186 # SubmissionListConfig( 

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

188 # ), 

189 # SubmissionListConfig( 

190 # key=SubmissionStatus.WAITING, 

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

192 # html_classes="waiting", 

193 # ), 

194 # SubmissionListConfig( 

195 # key=SubmissionStatus.ARCHIVED, 

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

197 # html_classes="archived", 

198 # ), 

199 # ] 

200 # 

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

202 # """ 

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

204 # """ 

205 # return self.get_archived_submission_list_config() 

206 

207 def can_create_submission(self) -> bool: 

208 """ 

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

210 """ 

211 return False 

212 

213 @abstractmethod 

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

215 """ 

216 Wether the user role can access the given submission. 

217 """ 

218 return False 

219 

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

221 """ 

222 Whether the user role can edit the given submission. 

223 """ 

224 return True 

225 

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

227 """ 

228 Whether the user role can submit the given submission. 

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

230 """ 

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

232 

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

234 """ 

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

236 """ 

237 return False 

238 

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

240 """ 

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

242 """ 

243 return False 

244 

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

246 """ 

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

248 """ 

249 return False 

250 

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

252 """ 

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

254 """ 

255 return False 

256 

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

258 """ 

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

260 """ 

261 return False 

262 

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

264 """ 

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

266 """ 

267 return False 

268 

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

270 """ 

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

272 version. 

273 """ 

274 return False 

275 

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

277 """ 

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

279 """ 

280 return False 

281 

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

283 """ 

284 Whether the user role can edit the given review. 

285 """ 

286 return False 

287 

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

289 """ 

290 Whether the user role can submit the given review. 

291 """ 

292 return False 

293 

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

295 """ 

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

297 """ 

298 return False 

299 

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

301 """ 

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

303 """ 

304 return False 

305 

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

307 """ 

308 Whether the user role can access the review details. 

309 """ 

310 return False 

311 

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

313 """ 

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

315 """ 

316 return False 

317 

318 def can_impersonate(self) -> bool: 

319 """ 

320 Whether the user role can impersonate other users. 

321 """ 

322 return False 

323 

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

325 """ 

326 Whether the user role can view the submission log. 

327 """ 

328 return False 

329 

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

331 """ 

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

333 """ 

334 return False 

335 

336 def can_filter_submissions(self) -> bool: 

337 """ 

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

339 """ 

340 return False 

341 

342 def can_access_journal_sections(self) -> bool: 

343 """ 

344 Whether the user can access the submission journal_sections views. 

345 """ 

346 return False 

347 

348 def can_edit_journal_sections(self) -> bool: 

349 """ 

350 Whether the user can edit the submission journal_sections. 

351 """ 

352 return False 

353 

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

355 """ 

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

357 """ 

358 return False 

359 

360 def can_access_last_activity(self) -> bool: 

361 """ 

362 Whether the user role can view the last activity. 

363 """ 

364 return True 

365 

366 def can_access_shortcut_actions(self) -> bool: 

367 return False 

368 

369 def _annotate_submission_query( 

370 self, 

371 submission_qs: QuerySet[Submission], 

372 ): 

373 return submission_qs.annotate( 

374 authors_string=self.authors_string_annotate, 

375 created_by_censored=self.created_by_censored_annotate, 

376 ).prefetch_related( 

377 # authors_censored 

378 Prefetch( 

379 "authors", 

380 queryset=self.authors_queryset, 

381 to_attr="authors_censored", 

382 ), 

383 # versions_censored 

384 Prefetch( 

385 "versions", 

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

387 .select_related("main_file") 

388 .annotate( 

389 created_by_censored=self.created_by_censored_annotate, 

390 ) 

391 .prefetch_related( 

392 # reviews_censored 

393 Prefetch( 

394 "reviews", 

395 queryset=self.reviews_queryset.annotate( 

396 reviewer_censored=self.reviewer_censored_annotate 

397 ).prefetch_related( 

398 Prefetch( 

399 "additional_files", 

400 queryset=self.reviews_additional_files_queryset, 

401 to_attr="additional_files_censored", 

402 ) 

403 ), 

404 to_attr="reviews_censored", 

405 ), 

406 # editorial_decision 

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

408 Prefetch( 

409 "editorial_decision", 

410 queryset=EditorialDecision.objects.all() 

411 .select_related("created_by") 

412 .prefetch_related("additional_files"), 

413 ), 

414 "additional_files", 

415 ), 

416 to_attr="versions_censored", 

417 ), 

418 # log_messages_censored 

419 Prefetch( 

420 "log_messages", 

421 queryset=self.log_messages_queryset, 

422 to_attr="log_messages_censored", 

423 ), 

424 )