Coverage for src / mesh / views / components / button.py: 82%
96 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 dataclasses import dataclass, field
2from typing import TYPE_CHECKING
4from django import forms
5from django.urls import reverse
6from django.utils.translation import gettext_lazy as _
8from mesh.models.orm.review_models import RecommendationValue, ReviewState
9from mesh.models.orm.submission_models import Submission
11if TYPE_CHECKING:
12 from mesh.models.roles.base_role import Role
15@dataclass
16class Button:
17 """
18 Interface for a button component.
19 """
21 # Unique ID for the button - not used for now
22 id: str
23 # Title to display as text in the button
24 title: str
25 # Optional font-awesome 6 icon code/class to be added next to the title.
26 icon_class: str = field(default="")
27 # Additional HTML attributes (ex: `{"type": ["submit"]})`
28 attrs: dict[str, list[str]] = field(default_factory=dict)
29 # A Django form - If not None, the button will be rendered as a full form
30 # with this unique button. You must provide the "href" HTML attribute for the form
31 # to be considered as one.
32 form: forms.Form | None = field(default=None)
34 def is_form(self) -> bool:
35 """
36 Whether the button should be displayed as a form.
37 """
38 return bool(self.form) and bool(self.attrs.get("href", False))
40 def is_link(self) -> bool:
41 """
42 Whether the button is a link.
43 """
44 return bool(self.attrs.get("href", False) or self.attrs.get("data-modal-href", False))
46 def is_modal_link(self):
47 """
48 Whether the button is a link.
49 """
50 return bool(self.attrs.get("data-modal-href", False))
52 def add_attr(self, attr: str, values: list[str]):
53 """
54 Add the given value to the given attribute values array.
56 Ex:
57 `button.add_attr("class", ["btn", "btn-primary"])`
58 `button.add_attr("class", ["btn"])`
59 """
60 if attr not in self.attrs:
61 self.attrs[attr] = []
63 for value in values:
64 if value in self.attrs[attr]:
65 continue
66 self.attrs[attr].append(value)
68 def set_attr(self, attr: str, values: list[str]):
69 """
70 Set the given value for the given attribute.
72 Ex:
73 `button.set_attr("class", ["btn", "btn-primary"])`
74 `button.set_attr("href", ["http://myserver"])`
75 """
76 self.attrs[attr] = values
78 def remove_attr(self, attr: str):
79 """
80 Remove the given attribute from the attributes array.
81 """
82 if attr in self.attrs:
83 del self.attrs[attr]
86@dataclass
87class SubmissionActionList:
88 title: str
89 actions: list[Button] = field(default_factory=list)
92def build_submission_actions(
93 submission: Submission, role: "Role", *args, **kwargs
94) -> list[SubmissionActionList]:
95 """
96 Returns the complete list of available actions to the current user.
97 """
98 article_actions = []
99 submission_actions: list[Button] = []
100 review_actions = []
101 manage_actions: list[Button] = []
102 btn_classes = ["as-button"] if kwargs.get("display_as_btn", False) else []
103 shortlist = kwargs.get("shortlist", True)
105 if role.can_access_submission_log(submission) and shortlist:
106 submission_actions.append(
107 Button(
108 id=f"submission-view-{submission.pk}",
109 title=_("VIEW SUBMISSION"),
110 icon_class="fa-eye",
111 attrs={
112 "href": [
113 reverse("mesh:submission_details", kwargs={"submission_pk": submission.pk})
114 ],
115 "class": btn_classes,
116 },
117 )
118 )
120 #### [BEGIN] Author actions ####
121 # Resume submit process
122 can_submit_submission = role.can_submit_submission(submission)
123 if can_submit_submission:
124 submission_actions.append(
125 Button(
126 id=f"submission-delete-{submission.pk}",
127 title=_("DELETE SUBMISSION"),
128 icon_class="fa-trash",
129 attrs={
130 "href": [reverse("mesh:submission_delete", kwargs={"pk": submission.pk})],
131 "class": btn_classes,
132 },
133 )
134 )
136 # submission_actions.append(
137 # Button(
138 # id=f"submission-submit-{submission.pk}",
139 # title=_("RESUME SUBMISSION"),
140 # icon_class="fa-list-check",
141 # attrs={
142 # "href": [reverse("mesh:submission_resume", kwargs={"pk": submission.pk})],
143 # "class": btn_classes,
144 # },
145 # )
146 # )
148 # Edit submission metadata & authors
149 elif role.can_edit_submission(submission):
150 if not shortlist or can_submit_submission:
151 article_actions.append(
152 Button(
153 id=f"submission-edit-metadata-{submission.pk}",
154 title=_("EDIT METADATA"),
155 icon_class="fa-pen-to-square",
156 attrs={
157 "href": [
158 reverse(
159 "mesh:submission_edit_article_metadata",
160 kwargs={"submission_pk": submission.pk},
161 )
162 ],
163 "class": btn_classes,
164 },
165 )
166 )
167 article_actions.append(
168 Button(
169 id=f"submission-edit-authors-{submission.pk}",
170 title=_("EDIT AUTHOR"),
171 icon_class="fa-pen-to-square",
172 attrs={
173 "href": [
174 reverse(
175 "mesh:submission_authors", kwargs={"submission_pk": submission.pk}
176 )
177 ],
178 "class": btn_classes,
179 },
180 )
181 )
183 # Create a new submission version
184 if not can_submit_submission and role.can_create_version(submission):
185 submission_actions.append(
186 Button(
187 id=f"submission-create-version-{submission.pk}",
188 title=_("SUBMIT REVISIONS"),
189 icon_class="fa-plus",
190 attrs={
191 "href": [
192 reverse(
193 "mesh:submission_version_create",
194 kwargs={"submission_pk": submission.pk},
195 )
196 ],
197 "class": btn_classes,
198 },
199 )
200 )
202 # Edit the current submission version
203 current_version = submission.get_current_version()
204 if not can_submit_submission and current_version and role.can_edit_version(current_version):
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(
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.can_assign_editor(submission):
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(
240 "mesh:async_assign_editor", kwargs={"submission_pk": submission.pk}
241 )
242 ],
243 "data-reload": ["true"] if kwargs.get("reload_page", False) else ["false"],
244 "data-reload-id": [kwargs.get("reload_id", "")],
245 "data-reload-href": (
246 [
247 reverse(
248 "mesh:api_fetch_submission",
249 kwargs={
250 "submission_pk": submission.pk,
251 "row_id": kwargs.get("reload_id", ""),
252 },
253 )
254 ]
255 if kwargs.get("reload_id", "") != ""
256 else [""]
257 ),
258 },
259 )
260 )
262 # Send the submission version to review
263 if role.can_start_review_process(submission):
264 from ...views.forms.editorial_forms import StartReviewProcessForm
266 form = StartReviewProcessForm(initial={"process": True})
268 review_actions.append(
269 Button(
270 id=f"submission-send-to-review-{submission.pk}",
271 title=_("SEND TO REVIEW"),
272 icon_class="fa-file-import",
273 form=form,
274 attrs={
275 "href": [
276 reverse(
277 "mesh:submission_start_review_process",
278 kwargs={"pk": submission.pk},
279 )
280 ],
281 "class": ["as-link"],
282 },
283 )
284 )
286 # Make an editorial decision
287 if role.can_create_editorial_decision(submission):
288 submission_actions.append(
289 Button(
290 id=f"submission-editorial-decision-{submission.pk}",
291 title=_("EDITORIAL DECISION"),
292 icon_class="fa-plus",
293 attrs={
294 "href": [
295 reverse(
296 "mesh:editorial_decision_create",
297 kwargs={"submission_pk": submission.pk},
298 )
299 ],
300 "class": btn_classes,
301 },
302 )
303 )
305 # Request a new review
306 if current_version and role.can_invite_reviewer(current_version):
307 review_actions.append(
308 Button(
309 id=f"submission-referee-request-{submission.pk}",
310 title=_("REQUEST REVIEW"),
311 icon_class="fa-user-group",
312 attrs={
313 "href": [
314 reverse(
315 "mesh:review_create",
316 kwargs={
317 "submission_pk": submission.pk,
318 "version_pk": submission.get_current_version().pk,
319 },
320 )
321 ],
322 "class": btn_classes,
323 },
324 )
325 )
327 if current_version.number > 1:
328 # Auto re-assign reviewers, only available after round 1
329 existing_reviewers = [review.reviewer for review in current_version.reviews.all()]
331 previous_round_number = current_version.number - 1
332 previous_round = submission.versions.get(number=previous_round_number)
333 reviews_that_can_be_auto_reassigned = previous_round.reviews.filter(
334 state=ReviewState.SUBMITTED.value
335 ).exclude(
336 recommendation__in=[
337 RecommendationValue.REJECTED.value,
338 RecommendationValue.RESUBMIT_SOMEWHERE_ELSE.value,
339 ]
340 )
341 previous_reviewers = [
342 review.reviewer
343 for review in reviews_that_can_be_auto_reassigned
344 if review.reviewer not in existing_reviewers
345 ]
347 if len(previous_reviewers) > 0:
348 review_actions.append(
349 Button(
350 id=f"submission-referee-auto-request-{submission.pk}",
351 title=_("ASSIGN REVIEWERS OF THE PREVIOUS ROUND"),
352 icon_class="fa-user-group",
353 attrs={
354 "href": [
355 reverse(
356 "mesh:review_auto_create",
357 kwargs={
358 "submission_pk": submission.pk,
359 "version_pk": current_version.pk,
360 },
361 )
362 ],
363 "class": btn_classes,
364 },
365 )
366 )
368 # if role_handler.check_rights("can_access_submission_log", submission) and not shortlist:
369 # manage_actions.append(
370 # Button(
371 # id=f"submission-log-{submission.pk}",
372 # title=_("VIEW HISTORY"),
373 # icon_class="fa-clock-rotate-left",
374 # attrs={
375 # "href": [reverse("mesh:submission_log", kwargs={"pk": submission.pk})],
376 # "class": btn_classes,
377 # },
378 # )
379 # )
380 #### [END] Editor actions ####
382 #### [BEGIN] Reviewer actions ####
383 # Submit / Edit the user current review
384 current_review = None
385 if current_version:
386 current_review = role.get_current_open_review(current_version)
388 if current_review is not None and current_review.accepted is None:
389 # Display 2 buttons: Accept & Decline if the review has not been accepted yet.
390 review_actions.extend(
391 [
392 Button(
393 id=f"review-accept-{current_review.pk}",
394 title=_("Accept review"),
395 icon_class="fa-check",
396 attrs={
397 "href": [reverse("mesh:review_accept", kwargs={"pk": current_review.pk})],
398 # "href": [
399 # add_query_parameters_to_url(
400 # reverse(
401 # "mesh:review_accept",
402 # kwargs={"pk": current_review.pk},
403 # ),
404 # {"accepted": ["true"]},
405 # )
406 # ],
407 "class": ["as-button", "button-success"],
408 },
409 ),
410 Button(
411 id=f"review-accept-{current_review.pk}",
412 title=_("Decline review"),
413 icon_class="fa-xmark",
414 attrs={
415 "href": [
416 reverse(
417 "mesh:review_decline",
418 kwargs={
419 "submission_pk": submission.pk,
420 "version_pk": current_version.pk,
421 "review_pk": current_review.pk,
422 },
423 )
424 ],
425 "class": ["as-button", "button-error"],
426 },
427 ),
428 ]
429 )
430 elif current_review is not None:
431 action_name = _("SUBMIT REVIEW")
432 if current_review.state in [
433 ReviewState.SUBMITTED.value,
434 ReviewState.DECLINED.value,
435 ]:
436 action_name = _("EDIT REVIEW")
437 review_actions.append(
438 Button(
439 id=f"review-{current_review.pk}",
440 title=action_name,
441 icon_class="fa-pen-to-square",
442 attrs={
443 "href": [
444 reverse(
445 "mesh:review_update",
446 kwargs={
447 "submission_pk": submission.pk,
448 "version_pk": current_version.pk,
449 "review_pk": current_review.pk,
450 },
451 )
452 ],
453 "class": btn_classes,
454 },
455 )
456 )
457 #### [END] Reviewer actions ####
458 action_lists = []
460 if article_actions:
461 action_lists.append(SubmissionActionList(title=_("Article"), actions=article_actions))
463 if submission_actions:
464 action_lists.append(
465 SubmissionActionList(title=_("Submission"), actions=submission_actions)
466 )
468 if review_actions:
469 action_lists.append(SubmissionActionList(title=_("Review"), actions=review_actions))
471 if manage_actions:
472 action_lists.append(SubmissionActionList(title=_("Manage"), actions=manage_actions))
474 return action_lists