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
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-22 07:20 +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 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
17if TYPE_CHECKING:
18 from django.db.models import BaseManager, Combinable, QuerySet
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
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 """
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
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 """
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
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()
205 def can_create_submission(self) -> bool:
206 """
207 Wether the user role has rights to create a new submission.
208 """
209 return False
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
218 def can_edit_submission(self, submission: Submission) -> bool:
219 """
220 Whether the user role can edit the given submission.
221 """
222 return True
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
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
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
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
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
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
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
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
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
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
286 def can_submit_review(self, review: Review) -> bool:
287 """
288 Whether the user role can submit the given review.
289 """
290 return False
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
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
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
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
316 def can_impersonate(self) -> bool:
317 """
318 Whether the user role can impersonate other users.
319 """
320 return False
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
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
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
340 def can_access_journal_sections(self) -> bool:
341 """
342 Whether the user can access the submission journal_sections views.
343 """
344 return False
346 def can_edit_journal_sections(self) -> bool:
347 """
348 Whether the user can edit the submission journal_sections.
349 """
350 return False
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
358 def can_access_last_activity(self) -> bool:
359 """
360 Whether the user role can view the last activity.
361 """
362 return True
364 def can_access_shortcut_actions(self) -> bool:
365 return False
367 def _is_comment_author(self, comment: Comment) -> bool:
368 return self.user.pk == comment.author.pk
370 def can_delete_comment(self, comment: Comment):
371 return self._is_comment_author(comment)
373 def can_access_discussion(self, discussion: Discussion) -> bool:
374 return bool(discussion) and discussion.members.filter(pk=self.user.pk).exists()
376 def can_edit_comment(self, comment: Comment) -> bool:
377 return self._is_comment_author(comment)
379 def can_create_discussion(self, submission: Submission) -> bool:
380 return False
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 )