Coverage for src/mesh/model/roles/editor.py: 90%
111 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-28 07:45 +0000
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-28 07:45 +0000
1from collections.abc import Collection
2from functools import cached_property
3from itertools import chain
4from typing import ClassVar
6from django.db.models import Q, QuerySet
7from django.utils.translation import gettext as _
9from mesh.models.editorial_models import EditorialDecision
10from mesh.models.journal_models import JournalSection
11from mesh.models.review_models import Review, ReviewAdditionalFile
12from mesh.models.submission_models import Submission, SubmissionState, SubmissionVersion
13from mesh.models.user_models import User
15from ..submission_status import SubmissionStatus, SubmissionStatusData
16from .base_role import Role, RoleRights
19class EditorRights(RoleRights):
20 """
21 Base rights for every editor derived class.
22 Basically they would have the same rights over a submission. The only difference is
23 the available list of submissions.
24 """
26 @cached_property
27 def journal_sections(self) -> list[JournalSection]:
28 """
29 Recursively gets ALL the submission journal_sections the user has access to.
30 That means all directly assigned journal_sections and all their children.
31 """
32 direct_journal_sections = JournalSection.objects.filter(editors__user=self.user)
33 if not direct_journal_sections:
34 return []
36 base_journal_sections = chain.from_iterable(
37 JournalSection.objects.get_children_recursive(c) for c in direct_journal_sections
38 )
40 return [*direct_journal_sections, *base_journal_sections]
42 @cached_property
43 def managed_submissions(self) -> QuerySet[Submission]:
44 """
45 Editors have edit access to all submissions in their assigned sections that
46 they did not author + all directly assigned submissions.
47 """
48 submissions = Submission.objects.get_submissions().filter(editors__user=self.user)
49 if self.journal_sections:
50 submissions = submissions.union(
51 Submission.objects.get_submissions()
52 .filter(journal_section__in=self.journal_sections)
53 .exclude(Q(created_by=self.user) | Q(state=SubmissionState.OPENED.value))
54 )
55 return submissions
57 @cached_property
58 def submissions(self) -> QuerySet[Submission]:
59 """
60 Editors have access to all submissions and associated data (TBD)
61 """
62 return self.managed_submissions
64 def can_access_submission(self, submission: Submission) -> bool:
65 return submission in self.submissions
67 def can_manage_submission(self, submission: Submission) -> bool:
68 return submission in self.managed_submissions
70 def can_access_reviews(self, version: SubmissionVersion) -> bool:
71 return self.can_access_submission(version.submission)
73 def can_access_review(self, review: Review) -> bool:
74 return self.can_access_submission(review.version.submission)
76 def can_access_review_author(self, review: Review) -> bool:
77 return self.can_manage_submission(review.version.submission)
79 def can_access_review_file(self, file: ReviewAdditionalFile) -> bool:
80 return self.can_access_review(file.attached_to)
82 def can_access_review_details(self, review: Review) -> bool:
83 return self.can_manage_submission(review.version.submission)
85 def can_invite_reviewer(self, version: SubmissionVersion) -> bool:
86 """
87 A reviewer can be invited only if the version is submitted by the author and
88 opened for review.
89 """
90 return (
91 version.submitted
92 and version.review_open
93 and self.can_manage_submission(version.submission)
94 )
96 def can_access_submission_author(self, submission: Submission) -> bool:
97 return self.can_access_submission(submission)
99 def can_access_version(self, version: SubmissionVersion) -> bool:
100 """
101 Non-submitted versions are not available to anyone except the author.
102 """
103 return version.submitted and self.can_access_submission(version.submission)
105 @cached_property
106 def managed_users(self) -> QuerySet[User]:
107 """
108 Restrict to non-editor users.
110 TODO: Restrict actions to manageable_submissions ?
111 """
112 return (
113 User.objects.exclude(pk=self.user.pk)
114 .exclude(journal_manager=True)
115 .exclude(editor_sections__isnull=False)
116 .exclude(editor_submissions__isnull=False)
117 .exclude(is_superuser=True)
118 )
120 def can_impersonate(self) -> bool:
121 return True
123 def can_access_submission_log(self, submission: Submission) -> bool:
124 return self.can_access_submission(submission)
126 def can_create_editorial_decision(self, submission: Submission) -> bool:
127 return self.can_manage_submission(submission)
129 def can_edit_editorial_decision(self, decision: EditorialDecision) -> bool:
130 return self.can_manage_submission(decision.version.submission)
132 def can_start_review_process(self, submission: Submission) -> bool:
133 return self.can_manage_submission(submission) and submission.is_reviewable
135 def can_assign_editor(self, submission: Submission) -> bool:
136 return self.can_manage_submission(submission)
138 def get_submission_status(self, submission: Submission) -> SubmissionStatusData:
139 """
140 For an editor+ role, the status highly depends on the submission state.
141 - Submission/Revisions submitted -> The editor must take action
142 - Revisions required -> Waiting for author to submit
143 - Submission accepted/declined -> Nothing to do
144 - Under review -> Tricky. Depends on the existing reviews (or not) state.
145 """
146 if not self.can_access_submission(submission) or submission.is_draft:
147 return SubmissionStatusData(
148 submission=submission, status=SubmissionStatus.ERROR, description=""
149 )
151 # buttons = []
152 # if self.can_create_editorial_decision(submission):
153 # buttons.append(
154 # Button(
155 # id=f"submission-editorial-decision-{submission.pk}",
156 # title=_("Editorial decision"),
157 # icon_class="fa-plus",
158 # attrs={
159 # "href": [
160 # reverse_lazy(
161 # "mesh:editorial_decision_create",
162 # kwargs={"submission_pk": submission.pk},
163 # )
164 # ],
165 # "class": ["as-button"],
166 # },
167 # )
168 # )
170 if self.can_assign_editor(submission) and not submission.all_assigned_editors:
171 pass
172 # buttons.append(
173 # Button(
174 # id=f"submission-assign-editor-{submission.pk}",
175 # title=_("Assign editor"),
176 # icon_class="fa-user-tie",
177 # attrs={
178 # "href": [
179 # reverse_lazy("mesh:submission_editors", kwargs={"pk": submission.pk})
180 # ],
181 # "class": ["as-button"],
182 # },
183 # )
184 # )
186 if submission.state in [ 186 ↛ 207line 186 didn't jump to line 207 because the condition on line 186 was never true
187 SubmissionState.SUBMITTED.value,
188 SubmissionState.REVISION_SUBMITTED.value,
189 ]:
190 # if self.can_start_review_process(submission):
191 # buttons.append(
192 # Button(
193 # id=f"submission-send-to-review-{submission.pk}",
194 # title=_("Start review process"),
195 # icon_class="fa-file-import",
196 # form=StartReviewProcessForm(initial={"process": True}),
197 # attrs={
198 # "href": [
199 # reverse_lazy(
200 # "mesh:submission_start_review_process",
201 # kwargs={"pk": submission.pk},
202 # )
203 # ]
204 # },
205 # )
206 # )
207 return SubmissionStatusData(
208 submission=submission,
209 status=SubmissionStatus.TODO,
210 description=_("Requires an editorial action."),
211 # shortcut_actions=buttons,
212 )
214 elif submission.state in [
215 SubmissionState.OPENED.value,
216 SubmissionState.REVISION_REQUESTED.value,
217 ]:
218 return SubmissionStatusData(
219 submission=submission,
220 status=SubmissionStatus.WAITING,
221 description=_("Waiting for a new version"),
222 # shortcut_actions=buttons,
223 )
225 elif submission.state in [
226 SubmissionState.ACCEPTED.value,
227 SubmissionState.REJECTED.value,
228 ]:
229 return SubmissionStatusData(
230 submission=submission,
231 status=SubmissionStatus.DONE,
232 description=_("Submission") + " " + submission.get_state_display(), # type:ignore
233 )
235 # Case under review
236 version: SubmissionVersion = submission.current_version # type:ignore
237 # if self.can_invite_reviewer(version):
238 # buttons.append(
239 # Button(
240 # id=f"submission-referee-request-{submission.pk}",
241 # title=_("Request review"),
242 # icon_class="fa-user-group",
243 # attrs={
244 # "href": [
245 # reverse_lazy("mesh:review_create", kwargs={"version_pk": version.pk})
246 # ],
247 # "class": ["as-button"],
248 # },
249 # )
250 # )
251 reviews: QuerySet[Review] = version.reviews.all() # type:ignore
252 if not reviews:
253 return SubmissionStatusData(
254 submission=submission,
255 status=SubmissionStatus.TODO,
256 description=_("No reviewers for the current round."),
257 # shortcut_actions=buttons,
258 )
260 # If some review requests are not answered and answer date is overdued
261 if any(r.is_response_overdue for r in reviews):
262 # if self.can_manage_submission(submission):
263 # buttons.append(
264 # Button(
265 # id=f"submission-inspect-review-{submission.pk}",
266 # title=_("Inspect review"),
267 # icon_class="fa-magnifying-glass",
268 # attrs={
269 # "href": [
270 # reverse_lazy(
271 # "mesh:submission_details", kwargs={"pk": submission.pk}
272 # )
273 # ],
274 # "class": ["as-button", "button-light"],
275 # },
276 # )
277 # )
278 return SubmissionStatusData(
279 submission=submission,
280 status=SubmissionStatus.TODO,
281 description=_("At least 1 request answer is overdue."),
282 # shortcut_actions=buttons,
283 )
285 # If some reports are not submitted and report date is overdued
286 elif any(r.is_report_overdue for r in reviews): 286 ↛ 303line 286 didn't jump to line 303 because the condition on line 286 was never true
287 # if self.can_manage_submission(submission):
288 # buttons.append(
289 # Button(
290 # id=f"submission-inspect-review-{submission.pk}",
291 # title=_("Inspect review"),
292 # icon_class="fa-magnifying-glass",
293 # attrs={
294 # "href": [
295 # reverse_lazy(
296 # "mesh:submission_details", kwargs={"pk": submission.pk}
297 # )
298 # ],
299 # "class": ["as-button", "button-light"],
300 # },
301 # )
302 # )
303 return SubmissionStatusData(
304 submission=submission,
305 status=SubmissionStatus.TODO,
306 description=_("At least 1 report is overdue."),
307 # shortcut_actions=buttons,
308 )
310 # All reviews submitted
311 elif all(r.is_completed for r in reviews):
312 return SubmissionStatusData(
313 submission=submission,
314 status=SubmissionStatus.TODO,
315 description=_("All reviews have been declined or submitted."),
316 # shortcut_actions=buttons,
317 )
319 # Pending reviews, nothing to do ?
320 return SubmissionStatusData(
321 submission=submission,
322 status=SubmissionStatus.WAITING,
323 description=_("Waiting for review(s)."),
324 # shortcut_actions=buttons,
325 )
327 # def get_submission_list_config(self) -> list[SubmissionListConfig]:
328 # """
329 # Returns the config to display the submissions for the user role.
330 # """
331 # return [
332 # SubmissionListConfig(
333 # key=SubmissionStatus.TODO, title=_("Requires action"), html_classes="todo"
334 # ),
335 # SubmissionListConfig(
336 # key=SubmissionStatus.WAITING,
337 # title=_("Waiting for other's input"),
338 # html_classes="waiting",
339 # ),
340 # SubmissionListConfig(
341 # key=SubmissionStatus.ARCHIVED,
342 # title=_("Closed / Archived"),
343 # html_classes="archived",
344 # in_filters=False,
345 # display=False,
346 # display_html=f"""<p>Please visit <a href="{reverse_lazy("mesh:submission_list_archived")}">this page</a> to consult all archived submissions.</p>""",
347 # ),
348 # ]
349 #
350 # def get_archived_submission_list_config(self) -> list[SubmissionListConfig]:
351 # """
352 # Returns the config to display only the Archived submissions.
353 # """
354 # return [
355 # SubmissionListConfig(
356 # key=SubmissionStatus.ARCHIVED,
357 # title=_("Closed / Archived"),
358 # html_classes="archived",
359 # ),
360 # ]
362 def can_filter_submissions(self) -> bool:
363 return True
365 def can_access_journal_sections(self) -> bool:
366 return True
368 def can_edit_review_file_right(self, review: Review) -> bool:
369 return self.can_manage_submission(review.version.submission)
372class Editor(Role):
373 """
374 Editor role.
375 """
377 _CODE: ClassVar[str] = "editor"
378 _NAME: ClassVar[str] = _("Editor")
379 _ICON_CLASS: ClassVar[str] = "fa-user-tie"
380 _SUBMISSION_LIST_TITLE: ClassVar[str] = _("My assignments")
381 rights: EditorRights
383 def __init__(self, user: User):
384 super().__init__(user)
386 @property
387 def active(self) -> bool:
388 """
389 Editor role is active if the user has rights over any
390 `JournalSection` or `Submission`.
391 """
392 return len(self.rights.journal_sections) > 0 or len(self.rights.managed_submissions) > 0
394 def get_rights(self) -> EditorRights:
395 return EditorRights(self.user)
398def get_section_editors(submission: Submission) -> Collection[User]:
399 """
400 Get the editors with access to the given submission from the EditorSectionRights.
401 """
402 # 1 - Get all journal_sections above the given submission
403 if not submission.journal_section: 403 ↛ 404line 403 didn't jump to line 404 because the condition on line 403 was never true
404 return []
406 journal_sections = [
407 submission.journal_section,
408 *JournalSection.objects.get_parents_recursive(submission.journal_section),
409 ]
411 # 2 - Get all users with rights over one of the above journal_sections
412 return User.objects.filter(editor_sections__journal_section__in=journal_sections)