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
« prev ^ index » next coverage.py v7.13.1, created at 2026-05-04 12:41 +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 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()
200 def can_create_submission(self) -> bool:
201 """
202 Wether the user role has rights to create a new submission.
203 """
204 return False
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
213 def can_edit_submission(self, submission: Submission) -> bool:
214 """
215 Whether the user role can edit the given submission.
216 """
217 return True
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
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
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
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
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
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
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
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
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
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
281 def can_submit_review(self, review: "Review") -> bool:
282 """
283 Whether the user role can submit the given review.
284 """
285 return False
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
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
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
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
311 def can_impersonate(self) -> bool:
312 """
313 Whether the user role can impersonate other users.
314 """
315 return False
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
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
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
335 def can_access_journal_sections(self) -> bool:
336 """
337 Whether the user can access the submission journal_sections views.
338 """
339 return False
341 def can_edit_journal_sections(self) -> bool:
342 """
343 Whether the user can edit the submission journal_sections.
344 """
345 return False
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
353 def can_access_last_activity(self) -> bool:
354 """
355 Whether the user role can view the last activity.
356 """
357 return True
359 def can_access_shortcut_actions(self) -> bool:
360 return False
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 )