Coverage for src / mesh / views / components / button.py: 82%
96 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-01 15:10 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-01 15:10 +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": [
131 reverse("mesh:submission_delete", kwargs={"submission_pk": submission.pk})
132 ],
133 "class": btn_classes,
134 },
135 )
136 )
138 # submission_actions.append(
139 # Button(
140 # id=f"submission-submit-{submission.pk}",
141 # title=_("RESUME SUBMISSION"),
142 # icon_class="fa-list-check",
143 # attrs={
144 # "href": [reverse("mesh:submission_resume", kwargs={"submission_pk": submission.pk})],
145 # "class": btn_classes,
146 # },
147 # )
148 # )
150 # Edit submission metadata & authors
151 elif role.can_edit_submission(submission):
152 if not shortlist or can_submit_submission:
153 article_actions.append(
154 Button(
155 id=f"submission-edit-metadata-{submission.pk}",
156 title=_("EDIT METADATA"),
157 icon_class="fa-pen-to-square",
158 attrs={
159 "href": [
160 reverse(
161 "mesh:submission_edit_article_metadata",
162 kwargs={"submission_pk": submission.pk},
163 )
164 ],
165 "class": btn_classes,
166 },
167 )
168 )
169 article_actions.append(
170 Button(
171 id=f"submission-edit-authors-{submission.pk}",
172 title=_("EDIT AUTHOR"),
173 icon_class="fa-pen-to-square",
174 attrs={
175 "href": [
176 reverse(
177 "mesh:submission_authors", kwargs={"submission_pk": submission.pk}
178 )
179 ],
180 "class": btn_classes,
181 },
182 )
183 )
185 # Create a new submission version
186 if not can_submit_submission and role.can_create_version(submission):
187 submission_actions.append(
188 Button(
189 id=f"submission-create-version-{submission.pk}",
190 title=_("SUBMIT REVISIONS"),
191 icon_class="fa-plus",
192 attrs={
193 "href": [
194 reverse(
195 "mesh:submission_version_create",
196 kwargs={"submission_pk": submission.pk},
197 )
198 ],
199 "class": btn_classes,
200 },
201 )
202 )
204 # Edit the current submission version
205 current_version = submission.get_current_version()
206 if not can_submit_submission and current_version and role.can_edit_version(current_version):
207 submission_actions.append(
208 Button(
209 id=f"submission-edit-version-{submission.pk}",
210 title=_("RESUME REVISIONS"),
211 icon_class="fa-list-check",
212 attrs={
213 "href": [
214 reverse(
215 "mesh:submission_version_update",
216 kwargs={"version_pk": current_version.pk},
217 )
218 ],
219 "class": btn_classes,
220 },
221 )
222 )
223 #### [END] Author actions ####
225 #### [BEGIN] Editor actions ####
226 if role.can_assign_editor(submission):
227 """
228 <a id="assigned-editors-edit" class="as-button" href="{% url "mesh:submission_editors" pk=submission.pk%}">
229 <i class="fa-solid fa-plus"></i>
230 {% translate "Assign editor" %}
231 </a>
232 """
233 submission_actions.append(
234 Button(
235 id=f"assigned-editors-edit-{submission.pk}",
236 title=_("ASSIGN EDITOR"),
237 icon_class="fa-plus",
238 attrs={
239 "class": btn_classes,
240 "data-modal-href": [
241 reverse(
242 "mesh:async_assign_editor", kwargs={"submission_pk": submission.pk}
243 )
244 ],
245 "data-reload": ["true"] if kwargs.get("reload_page", False) else ["false"],
246 "data-reload-id": [kwargs.get("reload_id", "")],
247 "data-reload-href": (
248 [
249 reverse(
250 "mesh:api_fetch_submission",
251 kwargs={
252 "submission_pk": submission.pk,
253 "row_id": kwargs.get("reload_id", ""),
254 },
255 )
256 ]
257 if kwargs.get("reload_id", "") != ""
258 else [""]
259 ),
260 },
261 )
262 )
264 # Send the submission version to review
265 if role.can_start_review_process(submission):
266 from ...views.forms.editorial_forms import StartReviewProcessForm
268 form = StartReviewProcessForm(initial={"process": True})
270 review_actions.append(
271 Button(
272 id=f"submission-send-to-review-{submission.pk}",
273 title=_("SEND TO REVIEW"),
274 icon_class="fa-file-import",
275 form=form,
276 attrs={
277 "href": [
278 reverse(
279 "mesh:submission_start_review_process",
280 kwargs={"pk": submission.pk},
281 )
282 ],
283 "class": ["as-link"],
284 },
285 )
286 )
288 # Make an editorial decision
289 if role.can_create_editorial_decision(submission):
290 submission_actions.append(
291 Button(
292 id=f"submission-editorial-decision-{submission.pk}",
293 title=_("EDITORIAL DECISION"),
294 icon_class="fa-plus",
295 attrs={
296 "href": [
297 reverse(
298 "mesh:editorial_decision_create",
299 kwargs={"submission_pk": submission.pk},
300 )
301 ],
302 "class": btn_classes,
303 },
304 )
305 )
307 # Request a new review
308 if current_version and role.can_invite_reviewer(current_version):
309 review_actions.append(
310 Button(
311 id=f"submission-referee-request-{submission.pk}",
312 title=_("REQUEST REVIEW"),
313 icon_class="fa-user-group",
314 attrs={
315 "href": [
316 reverse(
317 "mesh:review_create",
318 kwargs={
319 "submission_pk": submission.pk,
320 "version_pk": submission.get_current_version().pk,
321 },
322 )
323 ],
324 "class": btn_classes,
325 },
326 )
327 )
329 if current_version.number > 1:
330 # Auto re-assign reviewers, only available after round 1
331 existing_reviewers = [review.reviewer for review in current_version.reviews.all()]
333 previous_round_number = current_version.number - 1
334 previous_round = submission.versions.get(number=previous_round_number)
335 reviews_that_can_be_auto_reassigned = previous_round.reviews.filter(
336 state=ReviewState.SUBMITTED.value
337 ).exclude(
338 recommendation__in=[
339 RecommendationValue.REJECTED.value,
340 RecommendationValue.RESUBMIT_SOMEWHERE_ELSE.value,
341 ]
342 )
343 previous_reviewers = [
344 review.reviewer
345 for review in reviews_that_can_be_auto_reassigned
346 if review.reviewer not in existing_reviewers
347 ]
349 if len(previous_reviewers) > 0:
350 review_actions.append(
351 Button(
352 id=f"submission-referee-auto-request-{submission.pk}",
353 title=_("ASSIGN REVIEWERS OF THE PREVIOUS ROUND"),
354 icon_class="fa-user-group",
355 attrs={
356 "href": [
357 reverse(
358 "mesh:review_auto_create",
359 kwargs={
360 "submission_pk": submission.pk,
361 "version_pk": current_version.pk,
362 },
363 )
364 ],
365 "class": btn_classes,
366 },
367 )
368 )
370 # if role_handler.check_rights("can_access_submission_log", submission) and not shortlist:
371 # manage_actions.append(
372 # Button(
373 # id=f"submission-log-{submission.pk}",
374 # title=_("VIEW HISTORY"),
375 # icon_class="fa-clock-rotate-left",
376 # attrs={
377 # "href": [reverse("mesh:submission_log", kwargs={"pk": submission.pk})],
378 # "class": btn_classes,
379 # },
380 # )
381 # )
382 #### [END] Editor actions ####
384 #### [BEGIN] Reviewer actions ####
385 # Submit / Edit the user current review
386 current_review = None
387 if current_version:
388 current_review = role.get_current_open_review(current_version)
390 if current_review is not None and current_review.accepted is None:
391 # Display 2 buttons: Accept & Decline if the review has not been accepted yet.
392 review_actions.extend(
393 [
394 Button(
395 id=f"review-accept-{current_review.pk}",
396 title=_("Accept review"),
397 icon_class="fa-check",
398 attrs={
399 "href": [
400 reverse(
401 "mesh:review_accept",
402 kwargs={
403 "submission_pk": submission.pk,
404 "version_pk": current_version.pk,
405 "review_pk": current_review.pk,
406 },
407 )
408 ],
409 # "href": [
410 # add_query_parameters_to_url(
411 # reverse(
412 # "mesh:review_accept",
413 # kwargs={"pk": current_review.pk},
414 # ),
415 # {"accepted": ["true"]},
416 # )
417 # ],
418 "class": ["as-button", "button-success"],
419 },
420 ),
421 Button(
422 id=f"review-accept-{current_review.pk}",
423 title=_("Decline review"),
424 icon_class="fa-xmark",
425 attrs={
426 "href": [
427 reverse(
428 "mesh:review_decline",
429 kwargs={
430 "submission_pk": submission.pk,
431 "version_pk": current_version.pk,
432 "review_pk": current_review.pk,
433 },
434 )
435 ],
436 "class": ["as-button", "button-error"],
437 },
438 ),
439 ]
440 )
441 elif current_review is not None:
442 action_name = _("SUBMIT REVIEW")
443 if current_review.state in [
444 ReviewState.SUBMITTED.value,
445 ReviewState.DECLINED.value,
446 ]:
447 action_name = _("EDIT REVIEW")
448 review_actions.append(
449 Button(
450 id=f"review-{current_review.pk}",
451 title=action_name,
452 icon_class="fa-pen-to-square",
453 attrs={
454 "href": [
455 reverse(
456 "mesh:review_update",
457 kwargs={
458 "submission_pk": submission.pk,
459 "version_pk": current_version.pk,
460 "review_pk": current_review.pk,
461 },
462 )
463 ],
464 "class": btn_classes,
465 },
466 )
467 )
468 #### [END] Reviewer actions ####
469 action_lists = []
471 if article_actions:
472 action_lists.append(SubmissionActionList(title=_("Article"), actions=article_actions))
474 if submission_actions:
475 action_lists.append(
476 SubmissionActionList(title=_("Submission"), actions=submission_actions)
477 )
479 if review_actions:
480 action_lists.append(SubmissionActionList(title=_("Review"), actions=review_actions))
482 if manage_actions:
483 action_lists.append(SubmissionActionList(title=_("Manage"), actions=manage_actions))
485 return action_lists