Coverage for src / mesh / models / orm / editorial_models.py: 70%
60 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
3import os
4from typing import TYPE_CHECKING
6from django.db import models
7from django.urls import reverse
8from django.utils.translation import gettext_lazy as _
10from mesh.models.file_helpers import FILE_DELETE_DEFAULT_ERROR
11from mesh.models.orm.base_models import BaseChangeTrackingModel
12from mesh.models.orm.file_models import BaseFileWrapperModel, BaseModelWithFiles
13from mesh.models.orm.journal_models import JournalSection
14from mesh.models.orm.submission_models import (
15 SUBMISSION_STATE_EDITOR_CHOICES,
16 Submission,
17 SubmissionState,
18 SubmissionVersion,
19)
20from mesh.models.orm.user_models import User
22if TYPE_CHECKING:
23 from collections.abc import Callable
25 from django.db.models.manager import RelatedManager
28class EditorSectionRight(BaseChangeTrackingModel):
29 """
30 ManyToMany table between User & JournalSection.
31 Used to track Editor access/right to a submission journal_section (section).
32 """
34 user = models.ForeignKey(
35 User,
36 on_delete=models.CASCADE,
37 related_name="editor_sections",
38 )
39 journal_section = models.ForeignKey(
40 JournalSection,
41 verbose_name=_("Journal Section"),
42 on_delete=models.CASCADE,
43 related_name="editors",
44 )
47class EditorSubmissionRight(BaseModelWithFiles):
48 """
49 ManyToMany table representing an editor right to a submission, ie. an editor
50 "assigned" to a submission.
51 """
53 user = models.ForeignKey(
54 User,
55 on_delete=models.CASCADE,
56 related_name="editor_submissions",
57 )
58 submission = models.ForeignKey(
59 Submission,
60 verbose_name=_("Submission"),
61 on_delete=models.CASCADE,
62 related_name="editors",
63 )
65 class Meta:
66 constraints = [
67 models.UniqueConstraint(
68 fields=["user", "submission"], name="unique_submission_per_editor"
69 )
70 ]
73class EditorialDecision(BaseModelWithFiles):
74 """
75 Model representing an editorial decision. Edit restricted to editors and
76 above rights a priori.
77 """
79 file_fields_deletable = ["additional_files"]
80 version = models.OneToOneField(
81 SubmissionVersion,
82 on_delete=models.CASCADE,
83 editable=False, # the field will not be displayed in the admin or any other ModelForm
84 related_name="editorial_decision",
85 )
86 value = models.CharField(
87 verbose_name=_("Decision"),
88 max_length=64,
89 choices=SUBMISSION_STATE_EDITOR_CHOICES,
90 null=False,
91 blank=False,
92 )
93 comment = models.TextField(verbose_name=_("Description"), null=True)
95 get_value_display: Callable[[], str]
96 "Generated by django https://docs.djangoproject.com/en/4.2/ref/models/instances/#django.db.models.Model.get_FOO_display"
98 additional_files: RelatedManager[EditorialDecisionFile]
100 def can_delete_file(self, file_field: str) -> tuple[bool, str]:
101 """
102 An additional file cannot be deleted if there's only one and the comment
103 is empty.
104 """
105 if not super().can_delete_file(file_field):
106 return False, FILE_DELETE_DEFAULT_ERROR
108 if (
109 file_field == "additional_files"
110 and not self.comment
111 and len(self.additional_files.all()) < 2
112 ):
113 return False, _(
114 "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."
115 )
117 return True, ""
119 def get_decision_display(self):
120 if self.value == SubmissionState.ACCEPTED.value:
121 return _("Accepted")
122 elif self.value == SubmissionState.REJECTED.value:
123 return _("Rejected")
124 elif self.value == SubmissionState.REVISION_REQUESTED.value:
125 return _("Revision requested")
126 else:
127 return "Uknown"
130class EditorialDecisionFile(BaseFileWrapperModel[EditorialDecision]):
131 file_extensions = [".pdf"]
132 attached_to = models.ForeignKey(
133 EditorialDecision, on_delete=models.CASCADE, related_name="additional_files"
134 )
136 def get_upload_path(self, filename: str) -> str:
137 return os.path.join(
138 "submissions",
139 str(self.attached_to.version.submission.pk),
140 "editor_decisions",
141 str(self.attached_to.pk),
142 filename,
143 )
145 def check_access_right(self, role, right_code: str) -> bool:
146 if right_code == "read":
147 return role.can_access_submission(self.attached_to.version.submission)
149 elif right_code in ["delete"]:
150 return role.can_edit_editorial_decision(self.attached_to)
152 return False
154 def get_absolute_url(self) -> str:
155 """
156 Returns the URL to the model's file.
157 """
158 if not self.file.url:
159 return ""
160 file_identifier = self.get_file_identifier()
161 version_pk = self.attached_to.version.pk
162 submission_pk = self.attached_to.version.submission.pk
163 return reverse(
164 "mesh:serve_decision_file",
165 kwargs={
166 "file_identifier": file_identifier,
167 "version_pk": version_pk,
168 "submission_pk": submission_pk,
169 },
170 )