Coverage for src/mesh/models/editorial_models.py: 47%
60 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 __future__ import annotations
3import os
4import re
5from typing import TYPE_CHECKING
7from django.conf import settings
8from django.db import models
9from django.utils.translation import gettext_lazy as _
11from mesh.model.file_helpers import FILE_DELETE_DEFAULT_ERROR
13from .base_models import BaseChangeTrackingModel
14from .file_models import BaseFileWrapperModel, BaseModelWithFiles
15from .journal_models import JournalSection
16from .submission_models import (
17 SUBMISSION_STATE_EDITOR_CHOICES,
18 Submission,
19 SubmissionState,
20 SubmissionVersion,
21)
23if TYPE_CHECKING: 23 ↛ 24line 23 didn't jump to line 24 because the condition on line 23 was never true
24 from mesh.model.roles.role_handler import RoleHandler
27class EditorSectionRight(BaseChangeTrackingModel):
28 """
29 ManyToMany table between User & JournalSection.
30 Used to track Editor access/right to a submission journal_section (section).
31 """
33 user = models.ForeignKey(
34 settings.AUTH_USER_MODEL,
35 on_delete=models.CASCADE,
36 related_name="editor_sections",
37 )
38 journal_section = models.ForeignKey(
39 JournalSection,
40 verbose_name=_("Journal Section"),
41 on_delete=models.CASCADE,
42 related_name="editors",
43 )
46class EditorSubmissionRight(BaseModelWithFiles):
47 """
48 ManyToMany table representing an editor right to a submission, ie. an editor
49 "assigned" to a submission.
50 """
52 user = models.ForeignKey(
53 settings.AUTH_USER_MODEL,
54 on_delete=models.CASCADE,
55 related_name="editor_submissions",
56 )
57 submission = models.ForeignKey(
58 Submission,
59 verbose_name=_("Submission"),
60 on_delete=models.CASCADE,
61 related_name="editors",
62 )
64 class Meta:
65 constraints = [
66 models.UniqueConstraint(
67 fields=["user", "submission"], name="unique_submission_per_editor"
68 )
69 ]
72class EditorialDecision(BaseModelWithFiles):
73 """
74 Model representing an editorial decision. Edit restricted to editors and
75 above rights a priori.
76 """
78 file_fields_deletable = ["additional_files"]
79 version = models.OneToOneField(
80 SubmissionVersion,
81 on_delete=models.CASCADE,
82 editable=False, # the field will not be displayed in the admin or any other ModelForm
83 related_name="editorial_decision",
84 )
85 value = models.CharField(
86 verbose_name=_("Decision"),
87 max_length=64,
88 choices=SUBMISSION_STATE_EDITOR_CHOICES,
89 null=False,
90 blank=False,
91 )
92 comment = models.TextField(verbose_name=_("Description"), blank=True, null=True)
94 def can_delete_file(self, file_field: str) -> tuple[bool, str]:
95 """
96 An additional file cannot be deleted if there's only one and the comment
97 is empty.
98 """
99 if not super().can_delete_file(file_field):
100 return False, FILE_DELETE_DEFAULT_ERROR
102 if (
103 file_field == "additional_files"
104 and not self.comment
105 and len(self.additional_files.all()) < 2 # type:ignore
106 ):
107 return False, _(
108 "The decision requires either a file or a description. If you want to change the file, add the new one then delete the old one."
109 )
111 return True, ""
113 def get_decision_display(self):
114 if self.value == SubmissionState.ACCEPTED.value:
115 return _("Accepted")
116 elif self.value == SubmissionState.REJECTED.value:
117 return _("Rejected")
118 elif self.value == SubmissionState.REVISION_REQUESTED.value:
119 return _("Revision requested")
120 else:
121 return "Uknown"
124class EditorialDecisionFile(BaseFileWrapperModel):
125 file_extensions = [".pdf"]
126 attached_to: models.ForeignKey[EditorialDecision] = models.ForeignKey(
127 EditorialDecision, on_delete=models.CASCADE, related_name="additional_files"
128 )
130 @staticmethod
131 def get_upload_path(instance: EditorialDecisionFile, filename: str) -> str:
132 return os.path.join(
133 "submissions",
134 str(instance.attached_to.version.submission.pk),
135 "editor_decisions",
136 str(instance.attached_to.pk),
137 filename,
138 )
140 @classmethod
141 def reverse_file_path(cls, file_path: str) -> EditorialDecisionFile | None:
142 regex = "/".join(
143 [
144 "submissions",
145 r"(?P<submission_pk>[0-9]+)",
146 "editor_decisions",
147 r"(?P<decision_pk>[0-9]+)",
148 r"(?P<file_name>[^\/]+)$",
149 ]
150 )
151 match = re.match(re.compile(regex), file_path)
152 if not match:
153 return None
155 return cls._default_manager.filter(file=file_path).first()
157 def check_access_right(self, role_handler: RoleHandler, right_code: str) -> bool:
158 if right_code == "read":
159 return role_handler.check_global_rights(
160 "can_access_submission", self.attached_to.version.submission
161 )
163 elif right_code in ["delete"]:
164 return role_handler.check_rights("can_edit_editorial_decision", self.attached_to)
166 return False