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
« prev ^ index » next coverage.py v7.13.1, created at 2026-08-11 09:47 +0000
1from __future__ import annotations
3from abc import ABC, abstractmethod
4from dataclasses import asdict, dataclass
5from typing import TYPE_CHECKING, ClassVar
7from django.db.models import Case, CharField, F, Prefetch, Value, When
8from django.db.models.functions import Concat
9from django.utils.translation import gettext
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
16if TYPE_CHECKING:
17 from django.db.models import BaseManager, Combinable, QuerySet
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
29@dataclass
30class RoleSummary:
31 code: str
32 name: str
33 icon_class: str
34 submission_list_title: str
36 def serialize(self) -> dict:
37 return asdict(self)
40class Role(ABC):
41 """
42 Base interface for a role object.
43 """
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
55 # Filtered querysets for permission management
56 submissions_queryset: BaseManager[Submission]
57 "filters the submissions the user has access to"
59 reviews_queryset: BaseManager[Review]
60 "populates SubmissionVersion.reviews_censored"
62 created_by_censored_annotate: Combinable
63 "populates Submission/SubmissionVersion.created_by_censored"
65 authors_string_annotate: Combinable
66 "populates Submission.authors_string"
68 authors_queryset: BaseManager[SubmissionAuthor]
69 "populates Submission.authors_censored"
71 reviewer_censored_annotate: Combinable
72 "populates Review.reviewer_censored"
74 versions_queryset: BaseManager[SubmissionVersion]
75 "populates Submission.versions_censored"
77 reviews_additional_files_queryset: BaseManager[ReviewAdditionalFile]
78 "populates Review.additional_files_censored"
80 log_messages_queryset: BaseManager[SubmissionLog]
81 "populates Submission.log_messages_censored"
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()
104 @abstractmethod
105 def _get_is_active(self) -> bool:
106 """This is a private method. Do not use !
108 Use `role.is_active` instead
109 """
110 return False
112 @classmethod
113 def code(cls) -> str:
114 """
115 Returns the role's code.
116 """
117 return cls._CODE
119 @classmethod
120 def name(cls) -> str:
121 """
122 Returns the role's display name.
123 """
124 return cls._NAME
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
133 @classmethod
134 def submissions_list_title(cls) -> str:
135 return cls._SUBMISSION_LIST_TITLE
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 )
146 def accept(self, visitor, submission, *args, **kwargs):
147 return visitor.visit(submission, *args, **kwargs)
149 @abstractmethod
150 def get_submissions(self) -> QuerySet[Submission]:
151 """
152 Returns the queryset of submissions the user has access to.
153 """
154 pass
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
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
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
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()
207 def can_create_submission(self) -> bool:
208 """
209 Wether the user role has rights to create a new submission.
210 """
211 return False
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
220 def can_edit_submission(self, submission: Submission) -> bool:
221 """
222 Whether the user role can edit the given submission.
223 """
224 return True
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
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
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
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
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
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
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
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
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
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
288 def can_submit_review(self, review: Review) -> bool:
289 """
290 Whether the user role can submit the given review.
291 """
292 return False
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
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
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
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
318 def can_impersonate(self) -> bool:
319 """
320 Whether the user role can impersonate other users.
321 """
322 return False
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
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
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
342 def can_access_journal_sections(self) -> bool:
343 """
344 Whether the user can access the submission journal_sections views.
345 """
346 return False
348 def can_edit_journal_sections(self) -> bool:
349 """
350 Whether the user can edit the submission journal_sections.
351 """
352 return False
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
360 def can_access_last_activity(self) -> bool:
361 """
362 Whether the user role can view the last activity.
363 """
364 return True
366 def can_access_shortcut_actions(self) -> bool:
367 return False
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 )