Coverage for src/mesh/views/components/button.py: 52%
94 statements
« prev ^ index » next coverage.py v7.9.0, created at 2025-09-10 11:20 +0000
« prev ^ index » next coverage.py v7.9.0, created at 2025-09-10 11:20 +0000
1from dataclasses import dataclass, field
3from django import forms
4from django.urls import reverse_lazy
5from django.utils.translation import gettext_lazy as _
7from mesh.model.roles.role_handler import RoleHandler
9from ...models.review_models import RecommendationValue, Review, ReviewState
10from ...models.submission_models import Submission
13@dataclass
14class Button:
15 """
16 Interface for a button component.
17 """
19 # Unique ID for the button - not used for now
20 id: str
21 # Title to display as text in the button
22 title: str
23 # Optional font-awesome 6 icon code/class to be added next to the title.
24 icon_class: str = field(default="")
25 # Additional HTML attributes (ex: `{"type": ["submit"]})`
26 attrs: dict[str, list[str]] = field(default_factory=dict)
27 # A Django form - If not None, the button will be rendered as a full form
28 # with this unique button. You must provide the "href" HTML attribute for the form
29 # to be considered as one.
30 form: forms.Form | None = field(default=None)
32 def is_form(self) -> bool:
33 """
34 Whether the button should be displayed as a form.
35 """
36 return bool(self.form) and bool(self.attrs.get("href", False))
38 def is_link(self) -> bool:
39 """
40 Whether the button is a link.
41 """
42 return bool(self.attrs.get("href", False) or self.attrs.get("data-modal-href", False))
44 def is_modal_link(self):
45 """
46 Whether the button is a link.
47 """
48 return bool(self.attrs.get("data-modal-href", False))
50 def add_attr(self, attr: str, values: list[str]):
51 """
52 Add the given value to the given attribute values array.
54 Ex:
55 `button.add_attr("class", ["btn", "btn-primary"])`
56 `button.add_attr("class", ["btn"])`
57 """
58 if attr not in self.attrs:
59 self.attrs[attr] = []
61 for value in values:
62 if value in self.attrs[attr]:
63 continue
64 self.attrs[attr].append(value)
66 def set_attr(self, attr: str, values: list[str]):
67 """
68 Set the given value for the given attribute.
70 Ex:
71 `button.set_attr("class", ["btn", "btn-primary"])`
72 `button.set_attr("href", ["http://myserver"])`
73 """
74 self.attrs[attr] = values
76 def remove_attr(self, attr: str):
77 """
78 Remove the given attribute from the attributes array.
79 """
80 if attr in self.attrs:
81 del self.attrs[attr]
84@dataclass
85class SubmissionActionList:
86 title: str
87 actions: list[Button] = field(default_factory=list)
90def build_submission_actions(
91 submission: Submission, role_handler: RoleHandler, *args, **kwargs
92) -> list[SubmissionActionList]:
93 """
94 Returns the complete list of available actions to the current user.
95 """
96 article_actions = []
97 submission_actions: list[Button] = []
98 review_actions = []
99 manage_actions: list[Button] = []
100 btn_classes = ["as-button"] if kwargs.get("display_as_btn", False) else []
101 shortlist = kwargs.get("shortlist", True)
103 if role_handler.check_rights("can_access_submission_log", submission) and shortlist: 103 ↛ 120line 103 didn't jump to line 120 because the condition on line 103 was always true
104 submission_actions.append(
105 Button(
106 id=f"submission-view-{submission.pk}",
107 title=_("VIEW SUBMISSION"),
108 icon_class="fa-eye",
109 attrs={
110 "href": [
111 reverse_lazy("mesh:submission_details", kwargs={"pk": submission.pk})
112 ],
113 "class": btn_classes,
114 },
115 )
116 )
118 #### [BEGIN] Author actions ####
119 # Resume submit process
120 can_submit_submission = role_handler.check_rights("can_submit_submission", submission)
121 if can_submit_submission: 121 ↛ 122line 121 didn't jump to line 122 because the condition on line 121 was never true
122 submission_actions.append(
123 Button(
124 id=f"submission-delete-{submission.pk}",
125 title=_("DELETE SUBMISSION"),
126 icon_class="fa-trash",
127 attrs={
128 "href": [reverse_lazy("mesh:submission_delete", kwargs={"pk": submission.pk})],
129 "class": btn_classes,
130 },
131 )
132 )
134 # submission_actions.append(
135 # Button(
136 # id=f"submission-submit-{submission.pk}",
137 # title=_("RESUME SUBMISSION"),
138 # icon_class="fa-list-check",
139 # attrs={
140 # "href": [reverse_lazy("mesh:submission_resume", kwargs={"pk": submission.pk})],
141 # "class": btn_classes,
142 # },
143 # )
144 # )
146 # Edit submission metadata & authors
147 elif role_handler.check_rights("can_edit_submission", submission): 147 ↛ 180line 147 didn't jump to line 180 because the condition on line 147 was always true
148 if not shortlist or can_submit_submission: 148 ↛ 149line 148 didn't jump to line 149 because the condition on line 148 was never true
149 article_actions.append(
150 Button(
151 id=f"submission-edit-metadata-{submission.pk}",
152 title=_("EDIT METADATA"),
153 icon_class="fa-pen-to-square",
154 attrs={
155 "href": [
156 reverse_lazy(
157 "mesh:submission_edit_article_metadata",
158 kwargs={"pk": submission.pk},
159 )
160 ],
161 "class": btn_classes,
162 },
163 )
164 )
165 article_actions.append(
166 Button(
167 id=f"submission-edit-authors-{submission.pk}",
168 title=_("EDIT AUTHOR"),
169 icon_class="fa-pen-to-square",
170 attrs={
171 "href": [
172 reverse_lazy("mesh:submission_authors", kwargs={"pk": submission.pk})
173 ],
174 "class": btn_classes,
175 },
176 )
177 )
179 # Create a new submission version
180 if not can_submit_submission and role_handler.check_rights("can_create_version", submission): 180 ↛ 181line 180 didn't jump to line 181 because the condition on line 180 was never true
181 submission_actions.append(
182 Button(
183 id=f"submission-create-version-{submission.pk}",
184 title=_("SUBMIT REVISIONS"),
185 icon_class="fa-plus",
186 attrs={
187 "href": [
188 reverse_lazy(
189 "mesh:submission_version_create",
190 kwargs={"submission_pk": submission.pk},
191 )
192 ],
193 "class": btn_classes,
194 },
195 )
196 )
198 # Edit the current submission version
199 current_version = submission.current_version
200 if ( 200 ↛ 205line 200 didn't jump to line 205 because the condition on line 200 was never true
201 not can_submit_submission
202 and current_version
203 and role_handler.check_rights("can_edit_version", current_version)
204 ):
205 submission_actions.append(
206 Button(
207 id=f"submission-edit-version-{submission.pk}",
208 title=_("RESUME REVISIONS"),
209 icon_class="fa-list-check",
210 attrs={
211 "href": [
212 reverse_lazy(
213 "mesh:submission_version_update",
214 kwargs={"pk": current_version.pk},
215 )
216 ],
217 "class": btn_classes,
218 },
219 )
220 )
221 #### [END] Author actions ####
223 #### [BEGIN] Editor actions ####
224 if role_handler.check_rights("can_assign_editor", submission): 224 ↛ 261line 224 didn't jump to line 261 because the condition on line 224 was always true
225 """
226 <a id="assigned-editors-edit" class="as-button" href="{% url "mesh:submission_editors" pk=submission.pk%}">
227 <i class="fa-solid fa-plus"></i>
228 {% translate "Assign editor" %}
229 </a>
230 """
231 submission_actions.append(
232 Button(
233 id=f"assigned-editors-edit-{submission.pk}",
234 title=_("ASSIGN EDITOR"),
235 icon_class="fa-plus",
236 attrs={
237 "class": btn_classes,
238 "data-modal-href": [
239 reverse_lazy("mesh:async_assign_editor", kwargs={"pk": submission.pk})
240 ],
241 "data-reload": ["true"] if kwargs.get("reload_page", False) else ["false"],
242 "data-reload-id": [kwargs.get("reload_id", "")],
243 "data-reload-href": (
244 [
245 reverse_lazy(
246 "mesh:api_fetch_submission",
247 kwargs={
248 "pk": submission.pk,
249 "row_id": kwargs.get("reload_id", ""),
250 },
251 )
252 ]
253 if kwargs.get("reload_id", "") != ""
254 else [""]
255 ),
256 },
257 )
258 )
260 # Send the submission version to review
261 if role_handler.check_rights("can_start_review_process", submission): 261 ↛ 262line 261 didn't jump to line 262 because the condition on line 261 was never true
262 from ...views.forms.editorial_forms import StartReviewProcessForm
264 form = StartReviewProcessForm(initial={"process": True})
266 review_actions.append(
267 Button(
268 id=f"submission-send-to-review-{submission.pk}",
269 title=_("SEND TO REVIEW"),
270 icon_class="fa-file-import",
271 form=form,
272 attrs={
273 "href": [
274 reverse_lazy(
275 "mesh:submission_start_review_process",
276 kwargs={"pk": submission.pk},
277 )
278 ],
279 "class": ["as-link"],
280 },
281 )
282 )
284 # Make an editorial decision
285 if role_handler.check_rights("can_create_editorial_decision", submission): 285 ↛ 304line 285 didn't jump to line 304 because the condition on line 285 was always true
286 submission_actions.append(
287 Button(
288 id=f"submission-editorial-decision-{submission.pk}",
289 title=_("EDITORIAL DECISION"),
290 icon_class="fa-plus",
291 attrs={
292 "href": [
293 reverse_lazy(
294 "mesh:editorial_decision_create",
295 kwargs={"submission_pk": submission.pk},
296 )
297 ],
298 "class": btn_classes,
299 },
300 )
301 )
303 # Request a new review
304 if current_version and role_handler.check_rights("can_invite_reviewer", current_version): 304 ↛ 305line 304 didn't jump to line 305 because the condition on line 304 was never true
305 review_actions.append(
306 Button(
307 id=f"submission-referee-request-{submission.pk}",
308 title=_("REQUEST REVIEW"),
309 icon_class="fa-user-group",
310 attrs={
311 "href": [
312 reverse_lazy(
313 "mesh:review_create",
314 kwargs={"version_pk": submission.current_version.pk}, # type:ignore
315 )
316 ],
317 "class": btn_classes,
318 },
319 )
320 )
322 if current_version.number > 1:
323 # Auto re-assign reviewers, only available after round 1
324 existing_reviewers = [review.reviewer for review in current_version.reviews.all()]
326 previous_round_number = current_version.number - 1
327 previous_round = submission.versions.get(number=previous_round_number)
328 reviews_that_can_be_auto_reassigned = previous_round.reviews.filter(
329 state=ReviewState.SUBMITTED.value
330 ).exclude(
331 recommendation__in=[
332 RecommendationValue.REJECTED.value,
333 RecommendationValue.RESUBMIT_SOMEWHERE_ELSE.value,
334 ]
335 )
336 previous_reviewers = [
337 review.reviewer
338 for review in reviews_that_can_be_auto_reassigned
339 if review.reviewer not in existing_reviewers
340 ]
342 if len(previous_reviewers) > 0:
343 review_actions.append(
344 Button(
345 id=f"submission-referee-auto-request-{submission.pk}",
346 title=_("ASSIGN REVIEWERS OF THE PREVIOUS ROUND"),
347 icon_class="fa-user-group",
348 attrs={
349 "href": [
350 reverse_lazy(
351 "mesh:review_auto_create",
352 kwargs={"version_pk": current_version.pk}, # type:ignore
353 )
354 ],
355 "class": btn_classes,
356 },
357 )
358 )
360 # if role_handler.check_rights("can_access_submission_log", submission) and not shortlist:
361 # manage_actions.append(
362 # Button(
363 # id=f"submission-log-{submission.pk}",
364 # title=_("VIEW HISTORY"),
365 # icon_class="fa-clock-rotate-left",
366 # attrs={
367 # "href": [reverse_lazy("mesh:submission_log", kwargs={"pk": submission.pk})],
368 # "class": btn_classes,
369 # },
370 # )
371 # )
372 #### [END] Editor actions ####
374 #### [BEGIN] Reviewer actions ####
375 # Submit / Edit the user current review
376 current_review: Review | None = role_handler.get_from_rights(
377 "get_current_open_review", current_version
378 )
379 if current_review is not None and current_review.accepted is None: 379 ↛ 381line 379 didn't jump to line 381 because the condition on line 379 was never true
380 # Display 2 buttons: Accept & Decline if the review has not been accepted yet.
381 review_actions.extend(
382 [
383 Button(
384 id=f"review-accept-{current_review.pk}",
385 title=_("Accept review"),
386 icon_class="fa-check",
387 attrs={
388 "href": [
389 reverse_lazy("mesh:review_accept", kwargs={"pk": current_review.pk})
390 ],
391 # "href": [
392 # add_query_parameters_to_url(
393 # reverse(
394 # "mesh:review_accept",
395 # kwargs={"pk": current_review.pk},
396 # ),
397 # {"accepted": ["true"]},
398 # )
399 # ],
400 "class": ["as-button", "button-success"],
401 },
402 ),
403 Button(
404 id=f"review-accept-{current_review.pk}",
405 title=_("Decline review"),
406 icon_class="fa-xmark",
407 attrs={
408 "href": [
409 reverse_lazy("mesh:review_decline", kwargs={"pk": current_review.pk})
410 ],
411 "class": ["as-button", "button-error"],
412 },
413 ),
414 ]
415 )
416 elif current_review is not None: 416 ↛ 417line 416 didn't jump to line 417 because the condition on line 416 was never true
417 action_name = _("SUBMIT REVIEW")
418 if current_review.state in [
419 ReviewState.SUBMITTED.value,
420 ReviewState.DECLINED.value,
421 ]:
422 action_name = _("EDIT REVIEW")
423 review_actions.append(
424 Button(
425 id=f"review-{current_review.pk}",
426 title=action_name,
427 icon_class="fa-pen-to-square",
428 attrs={
429 "href": [reverse_lazy("mesh:review_update", kwargs={"pk": current_review.pk})],
430 "class": btn_classes,
431 },
432 )
433 )
434 #### [END] Reviewer actions ####
435 action_lists = []
437 if article_actions: 437 ↛ 438line 437 didn't jump to line 438 because the condition on line 437 was never true
438 action_lists.append(SubmissionActionList(title=_("Article"), actions=article_actions))
440 if submission_actions: 440 ↛ 445line 440 didn't jump to line 445 because the condition on line 440 was always true
441 action_lists.append(
442 SubmissionActionList(title=_("Submission"), actions=submission_actions)
443 )
445 if review_actions: 445 ↛ 446line 445 didn't jump to line 446 because the condition on line 445 was never true
446 action_lists.append(SubmissionActionList(title=_("Review"), actions=review_actions))
448 if manage_actions: 448 ↛ 449line 448 didn't jump to line 449 because the condition on line 448 was never true
449 action_lists.append(SubmissionActionList(title=_("Manage"), actions=manage_actions))
451 return action_lists