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

137 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-05-04 12:41 +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 get_submission_list_config(self) -> list[SubmissionListConfig]: 

175 # """ 

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

177 # """ 

178 # return [ 

179 # SubmissionListConfig( 

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

181 # ), 

182 # SubmissionListConfig( 

183 # key=SubmissionStatus.WAITING, 

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

185 # html_classes="waiting", 

186 # ), 

187 # SubmissionListConfig( 

188 # key=SubmissionStatus.ARCHIVED, 

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

190 # html_classes="archived", 

191 # ), 

192 # ] 

193 # 

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

195 # """ 

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

197 # """ 

198 # return self.get_archived_submission_list_config() 

199 

200 def can_create_submission(self) -> bool: 

201 """ 

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

203 """ 

204 return False 

205 

206 @abstractmethod 

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

208 """ 

209 Wether the user role can access the given submission. 

210 """ 

211 return False 

212 

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

214 """ 

215 Whether the user role can edit the given submission. 

216 """ 

217 return True 

218 

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

220 """ 

221 Whether the user role can submit the given submission. 

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

223 """ 

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

225 

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

227 """ 

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

229 """ 

230 return False 

231 

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

233 """ 

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

235 """ 

236 return False 

237 

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

239 """ 

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

241 """ 

242 return False 

243 

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

245 """ 

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

247 """ 

248 return False 

249 

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

251 """ 

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

253 """ 

254 return False 

255 

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

257 """ 

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

259 """ 

260 return False 

261 

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

263 """ 

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

265 version. 

266 """ 

267 return False 

268 

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

270 """ 

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

272 """ 

273 return False 

274 

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

276 """ 

277 Whether the user role can edit the given review. 

278 """ 

279 return False 

280 

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

282 """ 

283 Whether the user role can submit the given review. 

284 """ 

285 return False 

286 

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

288 """ 

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

290 """ 

291 return False 

292 

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

294 """ 

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

296 """ 

297 return False 

298 

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

300 """ 

301 Whether the user role can access the review details. 

302 """ 

303 return False 

304 

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

306 """ 

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

308 """ 

309 return False 

310 

311 def can_impersonate(self) -> bool: 

312 """ 

313 Whether the user role can impersonate other users. 

314 """ 

315 return False 

316 

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

318 """ 

319 Whether the user role can view the submission log. 

320 """ 

321 return False 

322 

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

324 """ 

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

326 """ 

327 return False 

328 

329 def can_filter_submissions(self) -> bool: 

330 """ 

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

332 """ 

333 return False 

334 

335 def can_access_journal_sections(self) -> bool: 

336 """ 

337 Whether the user can access the submission journal_sections views. 

338 """ 

339 return False 

340 

341 def can_edit_journal_sections(self) -> bool: 

342 """ 

343 Whether the user can edit the submission journal_sections. 

344 """ 

345 return False 

346 

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

348 """ 

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

350 """ 

351 return False 

352 

353 def can_access_last_activity(self) -> bool: 

354 """ 

355 Whether the user role can view the last activity. 

356 """ 

357 return True 

358 

359 def can_access_shortcut_actions(self) -> bool: 

360 return False 

361 

362 def _annotate_submission_query( 

363 self, 

364 submission_qs: "QuerySet[Submission]", 

365 ): 

366 return submission_qs.annotate( 

367 authors_string=self.authors_string_annotate, 

368 created_by_censored=self.created_by_censored_annotate, 

369 ).prefetch_related( 

370 # authors_censored 

371 Prefetch( 

372 "authors", 

373 queryset=self.authors_queryset, 

374 to_attr="authors_censored", 

375 ), 

376 # versions_censored 

377 Prefetch( 

378 "versions", 

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

380 .select_related("main_file") 

381 .annotate( 

382 created_by_censored=self.created_by_censored_annotate, 

383 ) 

384 .prefetch_related( 

385 # reviews_censored 

386 Prefetch( 

387 "reviews", 

388 queryset=self.reviews_queryset.annotate( 

389 reviewer_censored=self.reviewer_censored_annotate 

390 ).prefetch_related( 

391 Prefetch( 

392 "additional_files", 

393 queryset=self.reviews_additional_files_queryset, 

394 to_attr="additional_files_censored", 

395 ) 

396 ), 

397 to_attr="reviews_censored", 

398 ), 

399 # editorial_decision 

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

401 Prefetch( 

402 "editorial_decision", 

403 queryset=EditorialDecision.objects.all() 

404 .select_related("created_by") 

405 .prefetch_related("additional_files"), 

406 ), 

407 "additional_files", 

408 ), 

409 to_attr="versions_censored", 

410 ), 

411 # log_messages_censored 

412 Prefetch( 

413 "log_messages", 

414 queryset=self.log_messages_queryset, 

415 to_attr="log_messages_censored", 

416 ), 

417 )