Coverage for src / mesh / models / roles / editor.py: 89%
114 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 collections.abc import Collection
2from itertools import chain
3from typing import ClassVar
5from django.db.models import Case, Q, QuerySet, Value, When
6from django.utils.translation import gettext as _
7from opentelemetry import trace
9from mesh.app_settings import BlindMode, app_settings
10from mesh.models.orm.editorial_models import EditorialDecision
11from mesh.models.orm.journal_models import JournalSection
12from mesh.models.orm.review_models import Review
13from mesh.models.orm.submission_models import (
14 Submission,
15 SubmissionLog,
16 SubmissionState,
17 SubmissionVersion,
18)
19from mesh.models.orm.user_models import User
21from ..submission_status import SubmissionStatus, SubmissionStatusData
22from .base_role import Role
24tracer = trace.get_tracer(__name__)
27class Editor(Role):
28 """
29 Editor role.
30 """
32 _CODE: ClassVar[str] = "editor"
33 _NAME: ClassVar[str] = _("Editor")
34 _ICON_CLASS: ClassVar[str] = "fa-user-tie"
35 _SUBMISSION_LIST_TITLE: ClassVar[str] = _("My assignments")
37 @tracer.start_as_current_span("Editor.__init__")
38 def __init__(self, user: User):
39 super().__init__(user)
40 if app_settings.BLIND_MODE == BlindMode.DOUBLE_BLIND:
41 # hide reviews when current user also has created the submission
42 self.reviews_queryset = Review.objects.exclude(
43 version__submission__created_by=self.user
44 ).all()
45 else:
46 self.reviews_queryset = Review.objects.all()
47 self.versions_queryset = SubmissionVersion.objects.filter(submitted=True)
49 all_children = self._get_all_journal_sections()
50 self.submissions_queryset = (
51 Submission.objects.all()
52 .annotate(
53 user_is_editor=Case(
54 When(
55 Q(editors__user=self.user) | Q(journal_section__in=all_children),
56 then=Value(True),
57 ),
58 default=Value(False),
59 ),
60 )
61 .filter(user_is_editor=True)
62 .exclude(created_by=self.user)
63 .exclude(state=SubmissionState.OPENED.value)
64 )
65 self.log_messages_queryset = SubmissionLog.objects.all().select_related("created_by")
67 def _get_is_active(self):
68 return (
69 JournalSection.objects.filter(editors__user=self.user).exists()
70 or Submission.objects.filter(editors__user=self.user).exists()
71 )
73 def _get_all_journal_sections(self):
74 journalsections = [perm.journal_section for perm in self.user.editor_sections.all()]
75 all_children = set[JournalSection](journalsections)
76 for section in journalsections:
77 section_children = section.all_children()
78 all_children.update(section_children)
79 return all_children
81 def get_journal_sections(self) -> list[JournalSection]:
82 """
83 Recursively gets ALL the submission journal_sections the user has access to.
84 That means all directly assigned journal_sections and all their children.
85 """
86 direct_journal_sections = JournalSection.objects.filter(editors__user=self.user)
87 if not direct_journal_sections.exists():
88 return []
90 base_journal_sections = chain.from_iterable(
91 JournalSection.objects.get_children_recursive(c) for c in direct_journal_sections
92 )
94 return [*direct_journal_sections, *base_journal_sections]
96 def get_submissions(self):
97 """
98 Editors have access to all submissions and associated data (TBD)
99 """
100 return self._annotate_submission_query(self.submissions_queryset)
102 def can_submit_submission(self, submission):
103 return False
105 def can_access_submission(self, submission) -> bool:
106 return submission.user_is_editor
108 def can_access_reviews(self, version) -> bool:
109 return self.can_access_submission(version.submission)
111 def can_access_review(self, submission, review) -> bool:
112 return self.can_access_submission(submission)
114 def can_access_review_file(self, file) -> bool:
115 return self.can_access_review(file.attached_to)
117 def can_access_review_details(self, submission, review) -> bool:
118 return submission.user_is_editor
120 def can_invite_reviewer(self, version) -> bool:
121 """
122 A reviewer can be invited only if the version is submitted by the author and
123 opened for review.
124 """
126 return version.submitted and version.review_open and version.submission.user_is_editor
128 def can_access_version(self, version: SubmissionVersion) -> bool:
129 """
130 Non-submitted versions are not available to anyone except the author.
131 """
132 return version.submitted and self.can_access_submission(version.submission)
134 def get_managed_users(self) -> QuerySet[User]:
135 """
136 Restrict to non-editor users.
138 TODO: Restrict actions to manageable_submissions ?
139 """
140 return (
141 User.objects.exclude(pk=self.user.pk)
142 .exclude(journal_manager=True)
143 .exclude(editor_sections__isnull=False)
144 .exclude(editor_submissions__isnull=False)
145 .exclude(is_superuser=True)
146 )
148 def can_impersonate(self) -> bool:
149 return True
151 def can_access_submission_log(self, submission: Submission) -> bool:
152 return self.can_access_submission(submission)
154 def can_create_editorial_decision(self, submission: Submission) -> bool:
155 return submission.user_is_editor
157 def can_edit_editorial_decision(self, decision: EditorialDecision) -> bool:
158 return decision.version.submission.user_is_editor
160 def can_start_review_process(self, submission: Submission) -> bool:
161 return submission.user_is_editor and submission.is_reviewable()
163 def can_assign_editor(self, submission: Submission) -> bool:
164 return submission.user_is_editor
166 def get_submission_status(self, submission: Submission) -> SubmissionStatusData:
167 """
168 For an editor+ role, the status highly depends on the submission state.
169 - Submission/Revisions submitted -> The editor must take action
170 - Revisions required -> Waiting for author to submit
171 - Submission accepted/declined -> Nothing to do
172 - Under review -> Tricky. Depends on the existing reviews (or not) state.
173 """
174 current_version = submission.get_current_version()
175 if not current_version:
176 return SubmissionStatusData(
177 submission=submission,
178 status=SubmissionStatus.ERROR,
179 description="Submission does not have a current version",
180 )
181 if not submission.user_is_editor or submission.is_draft:
182 return SubmissionStatusData(
183 submission=submission, status=SubmissionStatus.ERROR, description=""
184 )
186 if submission.state in [
187 SubmissionState.SUBMITTED.value,
188 SubmissionState.REVISION_SUBMITTED.value,
189 ]:
190 return SubmissionStatusData(
191 submission=submission,
192 status=SubmissionStatus.TODO,
193 description=_("Requires an editorial action."),
194 )
196 elif submission.state in [
197 SubmissionState.OPENED.value,
198 SubmissionState.REVISION_REQUESTED.value,
199 ]:
200 return SubmissionStatusData(
201 submission=submission,
202 status=SubmissionStatus.WAITING,
203 description=_("Waiting for a new version"),
204 )
206 elif submission.state in [
207 SubmissionState.ACCEPTED.value,
208 SubmissionState.REJECTED.value,
209 ]:
210 return SubmissionStatusData(
211 submission=submission,
212 status=SubmissionStatus.DONE,
213 description=_("Submission") + " " + submission.get_state_display(),
214 )
216 # Case under review
217 reviews = current_version.reviews.all()
218 if not reviews:
219 return SubmissionStatusData(
220 submission=submission,
221 status=SubmissionStatus.TODO,
222 description=_("No reviewers for the current round."),
223 # shortcut_actions=buttons,
224 )
226 # If some review requests are not answered and answer date is overdued
227 if any(r.is_response_overdue for r in reviews):
228 return SubmissionStatusData(
229 submission=submission,
230 status=SubmissionStatus.TODO,
231 description=_("At least 1 request answer is overdue."),
232 # shortcut_actions=buttons,
233 )
235 # If some reports are not submitted and report date is overdued
236 elif any(r.is_report_overdue for r in reviews):
237 return SubmissionStatusData(
238 submission=submission,
239 status=SubmissionStatus.TODO,
240 description=_("At least 1 report is overdue."),
241 # shortcut_actions=buttons,
242 )
244 # All reviews submitted
245 elif all(r.is_completed for r in reviews):
246 return SubmissionStatusData(
247 submission=submission,
248 status=SubmissionStatus.TODO,
249 description=_("All reviews have been declined or submitted."),
250 # shortcut_actions=buttons,
251 )
253 # Pending reviews, nothing to do ?
254 return SubmissionStatusData(
255 submission=submission,
256 status=SubmissionStatus.WAITING,
257 description=_("Waiting for review(s)."),
258 # shortcut_actions=buttons,
259 )
261 def can_filter_submissions(self) -> bool:
262 return True
264 def can_access_journal_sections(self) -> bool:
265 return True
267 def can_edit_review_file_right(self, review: Review, submission=None) -> bool:
268 # annotation is lost when accessing review.version.submission
269 # We either need to pass the annotated object as argument
270 # or re-fetch it...
271 if submission:
272 if review.version.submission != submission:
273 raise ValueError("Provided submission is not related to this review")
274 else:
275 submission = self.get_submissions().get(pk=review.version.submission.pk)
276 return submission.user_is_editor
279def get_section_editors(submission: Submission) -> Collection[User]:
280 """
281 Get the editors with access to the given submission from the EditorSectionRights.
282 """
283 # 1 - Get all journal_sections above the given submission
284 if not submission.journal_section:
285 return []
287 journal_sections = [
288 submission.journal_section,
289 *JournalSection.objects.get_parents_recursive(submission.journal_section),
290 ]
292 # 2 - Get all users with rights over one of the above journal_sections
293 return User.objects.filter(editor_sections__journal_section__in=journal_sections)