Coverage for src / mesh / views / components / button.py: 78%
107 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-08-27 14:14 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-08-27 14:14 +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.comment_models import Comment
9from mesh.models.orm.review_models import RecommendationValue, ReviewState
10from mesh.models.orm.submission_models import Submission
11from mesh.models.user.user_interfaces import User
13if TYPE_CHECKING:
14 from mesh.models.roles.base_role import Role
17@dataclass
18class Button:
19 """
20 Interface for a button component.
21 """
23 # Unique ID for the button - not used for now
24 id: str
25 # Title to display as text in the button
26 title: str
27 # Optional font-awesome 6 icon code/class to be added next to the title.
28 icon_class: str = field(default="")
29 icon_attrs: dict[str, list[str]] = field(default_factory=dict)
30 # Additional HTML attributes (ex: `{"type": ["submit"]})`
31 attrs: dict[str, list[str]] = field(default_factory=dict)
32 # A Django form - If not None, the button will be rendered as a full form
33 # with this unique button. You must provide the "href" HTML attribute for the form
34 # to be considered as one.
35 form: forms.Form | None = field(default=None)
37 def is_form(self) -> bool:
38 """
39 Whether the button should be displayed as a form.
40 """
41 return bool(self.form) and bool(self.attrs.get("href", False))
43 def is_link(self) -> bool:
44 """
45 Whether the button is a link.
46 """
47 return bool(self.attrs.get("href", False) or self.attrs.get("data-modal-href", False))
49 def is_modal_link(self):
50 """
51 Whether the button is a link.
52 """
53 return bool(self.attrs.get("data-modal-href", False))
55 def add_attr(self, attr: str, values: list[str]):
56 """
57 Add the given value to the given attribute values array.
59 Ex:
60 `button.add_attr("class", ["btn", "btn-primary"])`
61 `button.add_attr("class", ["btn"])`
62 """
63 if attr not in self.attrs:
64 self.attrs[attr] = []
66 for value in values:
67 if value in self.attrs[attr]:
68 continue
69 self.attrs[attr].append(value)
71 def set_attr(self, attr: str, values: list[str]):
72 """
73 Set the given value for the given attribute.
75 Ex:
76 `button.set_attr("class", ["btn", "btn-primary"])`
77 `button.set_attr("href", ["http://myserver"])`
78 """
79 self.attrs[attr] = values
81 def remove_attr(self, attr: str):
82 """
83 Remove the given attribute from the attributes array.
84 """
85 if attr in self.attrs:
86 del self.attrs[attr]
89@dataclass
90class SubmissionActionList:
91 title: str
92 actions: list[Button] = field(default_factory=list)
95def build_submission_actions(
96 submission: Submission, role: "Role", *args, **kwargs
97) -> list[SubmissionActionList]:
98 """
99 Returns the complete list of available actions to the current user.
100 """
101 article_actions = []
102 submission_actions: list[Button] = []
103 review_actions = []
104 manage_actions: list[Button] = []
105 btn_classes = ["as-button"] if kwargs.get("display_as_btn", False) else []
106 shortlist = kwargs.get("shortlist", True)
108 if role.can_access_submission_log(submission) and shortlist:
109 submission_actions.append(
110 Button(
111 id=f"submission-view-{submission.pk}",
112 title=_("VIEW SUBMISSION"),
113 icon_class="fa-eye",
114 attrs={
115 "href": [
116 reverse("mesh:submission_details", kwargs={"submission_pk": submission.pk})
117 ],
118 "class": btn_classes,
119 },
120 )
121 )
123 #### [BEGIN] Author actions ####
124 # Resume submit process
125 can_submit_submission = role.can_submit_submission(submission)
126 if can_submit_submission:
127 submission_actions.append(
128 Button(
129 id=f"submission-delete-{submission.pk}",
130 title=_("DELETE SUBMISSION"),
131 icon_class="fa-trash",
132 attrs={
133 "href": [
134 reverse("mesh:submission_delete", kwargs={"submission_pk": submission.pk})
135 ],
136 "class": btn_classes,
137 },
138 )
139 )
141 # submission_actions.append(
142 # Button(
143 # id=f"submission-submit-{submission.pk}",
144 # title=_("RESUME SUBMISSION"),
145 # icon_class="fa-list-check",
146 # attrs={
147 # "href": [reverse("mesh:submission_resume", kwargs={"submission_pk": submission.pk})],
148 # "class": btn_classes,
149 # },
150 # )
151 # )
153 # Edit submission metadata & authors
154 elif role.can_edit_submission(submission):
155 if not shortlist or can_submit_submission:
156 article_actions.append(
157 Button(
158 id=f"submission-edit-metadata-{submission.pk}",
159 title=_("EDIT METADATA"),
160 icon_class="fa-pen-to-square",
161 attrs={
162 "href": [
163 reverse(
164 "mesh:submission_edit_article_metadata",
165 kwargs={"submission_pk": submission.pk},
166 )
167 ],
168 "class": btn_classes,
169 },
170 )
171 )
172 article_actions.append(
173 Button(
174 id=f"submission-edit-authors-{submission.pk}",
175 title=_("EDIT AUTHOR"),
176 icon_class="fa-pen-to-square",
177 attrs={
178 "href": [
179 reverse(
180 "mesh:submission_authors", kwargs={"submission_pk": submission.pk}
181 )
182 ],
183 "class": btn_classes,
184 },
185 )
186 )
188 # Create a new submission version
189 if not can_submit_submission and role.can_create_version(submission):
190 submission_actions.append(
191 Button(
192 id=f"submission-create-version-{submission.pk}",
193 title=_("SUBMIT REVISIONS"),
194 icon_class="fa-plus",
195 attrs={
196 "href": [
197 reverse(
198 "mesh:submission_version_create",
199 kwargs={"submission_pk": submission.pk},
200 )
201 ],
202 "class": btn_classes,
203 },
204 )
205 )
207 # Edit the current submission version
208 current_version = submission.get_current_version()
209 if not can_submit_submission and current_version and role.can_edit_version(current_version):
210 submission_actions.append(
211 Button(
212 id=f"submission-edit-version-{submission.pk}",
213 title=_("RESUME REVISIONS"),
214 icon_class="fa-list-check",
215 attrs={
216 "href": [
217 reverse(
218 "mesh:submission_version_update",
219 kwargs={"version_pk": current_version.pk},
220 )
221 ],
222 "class": btn_classes,
223 },
224 )
225 )
226 #### [END] Author actions ####
228 #### [BEGIN] Editor actions ####
229 if role.can_assign_editor(submission):
230 """
231 <a id="assigned-editors-edit" class="as-button" href="{% url "mesh:submission_editors" pk=submission.pk%}">
232 <i class="fa-solid fa-plus"></i>
233 {% translate "Assign editor" %}
234 </a>
235 """
236 submission_actions.append(
237 Button(
238 id=f"assigned-editors-edit-{submission.pk}",
239 title=_("ASSIGN EDITOR"),
240 icon_class="fa-plus",
241 attrs={
242 "class": btn_classes,
243 "data-modal-href": [
244 reverse(
245 "mesh:async_assign_editor", kwargs={"submission_pk": submission.pk}
246 )
247 ],
248 "data-reload": ["true"] if kwargs.get("reload_page", False) else ["false"],
249 "data-reload-id": [kwargs.get("reload_id", "")],
250 "data-reload-href": (
251 [
252 reverse(
253 "mesh:api_fetch_submission",
254 kwargs={
255 "submission_pk": submission.pk,
256 "row_id": kwargs.get("reload_id", ""),
257 },
258 )
259 ]
260 if kwargs.get("reload_id", "") != ""
261 else [""]
262 ),
263 },
264 )
265 )
267 # Send the submission version to review
268 if role.can_start_review_process(submission):
269 from ...views.forms.editorial_forms import StartReviewProcessForm
271 form = StartReviewProcessForm(initial={"process": True})
273 review_actions.append(
274 Button(
275 id=f"submission-send-to-review-{submission.pk}",
276 title=_("SEND TO REVIEW"),
277 icon_class="fa-file-import",
278 form=form,
279 attrs={
280 "href": [
281 reverse(
282 "mesh:submission_start_review_process",
283 kwargs={"pk": submission.pk},
284 )
285 ],
286 "class": ["as-link"],
287 },
288 )
289 )
291 # Make an editorial decision
292 if role.can_create_editorial_decision(submission):
293 submission_actions.append(
294 Button(
295 id=f"submission-editorial-decision-{submission.pk}",
296 title=_("EDITORIAL DECISION"),
297 icon_class="fa-plus",
298 attrs={
299 "href": [
300 reverse(
301 "mesh:editorial_decision_create",
302 kwargs={"submission_pk": submission.pk},
303 )
304 ],
305 "class": btn_classes,
306 },
307 )
308 )
310 # Request a new review
311 if current_version and role.can_invite_reviewer(current_version):
312 review_actions.append(
313 Button(
314 id=f"submission-referee-request-{submission.pk}",
315 title=_("REQUEST REVIEW"),
316 icon_class="fa-user-group",
317 attrs={
318 "href": [
319 reverse(
320 "mesh:review_create",
321 kwargs={
322 "submission_pk": submission.pk,
323 "version_pk": submission.get_current_version().pk,
324 },
325 )
326 ],
327 "class": btn_classes,
328 },
329 )
330 )
332 if current_version.number > 1:
333 # Auto re-assign reviewers, only available after round 1
334 existing_reviewers = [review.reviewer for review in current_version.reviews.all()]
336 previous_round_number = current_version.number - 1
337 previous_round = submission.versions.get(number=previous_round_number)
338 reviews_that_can_be_auto_reassigned = previous_round.reviews.filter(
339 state=ReviewState.SUBMITTED.value
340 ).exclude(
341 recommendation__in=[
342 RecommendationValue.REJECTED.value,
343 RecommendationValue.RESUBMIT_SOMEWHERE_ELSE.value,
344 ]
345 )
346 previous_reviewers = [
347 review.reviewer
348 for review in reviews_that_can_be_auto_reassigned
349 if review.reviewer not in existing_reviewers
350 ]
352 if len(previous_reviewers) > 0:
353 review_actions.append(
354 Button(
355 id=f"submission-referee-auto-request-{submission.pk}",
356 title=_("ASSIGN REVIEWERS OF THE PREVIOUS ROUND"),
357 icon_class="fa-user-group",
358 attrs={
359 "href": [
360 reverse(
361 "mesh:review_auto_create",
362 kwargs={
363 "submission_pk": submission.pk,
364 "version_pk": current_version.pk,
365 },
366 )
367 ],
368 "class": btn_classes,
369 },
370 )
371 )
373 # if role_handler.check_rights("can_access_submission_log", submission) and not shortlist:
374 # manage_actions.append(
375 # Button(
376 # id=f"submission-log-{submission.pk}",
377 # title=_("VIEW HISTORY"),
378 # icon_class="fa-clock-rotate-left",
379 # attrs={
380 # "href": [reverse("mesh:submission_log", kwargs={"pk": submission.pk})],
381 # "class": btn_classes,
382 # },
383 # )
384 # )
385 #### [END] Editor actions ####
387 #### [BEGIN] Reviewer actions ####
388 # Submit / Edit the user current review
389 current_review = None
390 if current_version:
391 current_review = role.get_current_open_review(current_version)
393 if current_review is not None and current_review.accepted is None:
394 # Display 2 buttons: Accept & Decline if the review has not been accepted yet.
395 review_actions.extend(
396 [
397 Button(
398 id=f"review-accept-{current_review.pk}",
399 title=_("Accept review"),
400 icon_class="fa-check",
401 attrs={
402 "href": [
403 reverse(
404 "mesh:review_accept",
405 kwargs={
406 "submission_pk": submission.pk,
407 "version_pk": current_version.pk,
408 "review_pk": current_review.pk,
409 },
410 )
411 ],
412 # "href": [
413 # add_query_parameters_to_url(
414 # reverse(
415 # "mesh:review_accept",
416 # kwargs={"pk": current_review.pk},
417 # ),
418 # {"accepted": ["true"]},
419 # )
420 # ],
421 "class": ["as-button", "button-success"],
422 },
423 ),
424 Button(
425 id=f"review-accept-{current_review.pk}",
426 title=_("Decline review"),
427 icon_class="fa-xmark",
428 attrs={
429 "href": [
430 reverse(
431 "mesh:review_decline",
432 kwargs={
433 "submission_pk": submission.pk,
434 "version_pk": current_version.pk,
435 "review_pk": current_review.pk,
436 },
437 )
438 ],
439 "class": ["as-button", "button-error"],
440 },
441 ),
442 ]
443 )
444 elif current_review is not None:
445 action_name = _("SUBMIT REVIEW")
446 if current_review.state in [
447 ReviewState.SUBMITTED.value,
448 ReviewState.DECLINED.value,
449 ]:
450 action_name = _("EDIT REVIEW")
451 review_actions.append(
452 Button(
453 id=f"review-{current_review.pk}",
454 title=action_name,
455 icon_class="fa-pen-to-square",
456 attrs={
457 "href": [
458 reverse(
459 "mesh:review_update",
460 kwargs={
461 "submission_pk": submission.pk,
462 "version_pk": current_version.pk,
463 "review_pk": current_review.pk,
464 },
465 )
466 ],
467 "class": btn_classes,
468 },
469 )
470 )
471 #### [END] Reviewer actions ####
472 action_lists = []
474 if article_actions:
475 action_lists.append(SubmissionActionList(title=_("Article"), actions=article_actions))
477 if submission_actions:
478 action_lists.append(
479 SubmissionActionList(title=_("Submission"), actions=submission_actions)
480 )
482 if review_actions:
483 action_lists.append(SubmissionActionList(title=_("Review"), actions=review_actions))
485 if manage_actions:
486 action_lists.append(SubmissionActionList(title=_("Manage"), actions=manage_actions))
488 return action_lists
491def build_comment_actions(comment: Comment, user: User):
492 if comment.author == user:
493 edit_comment_button = Button(
494 id=f"edit-comment-{comment.pk}",
495 title="",
496 icon_class="fa-pen-to-square",
497 icon_attrs={"style": ["color:black"]},
498 attrs={
499 "data-comment-id": [comment.pk],
500 "data-tooltip": ["Edit"],
501 "type": ["button"],
502 "class": ["edit-button", "comment-action"],
503 },
504 )
505 delete_comment_button = Button(
506 id=f"edit-comment-{comment.pk}",
507 title="",
508 icon_class="fa-trash-can",
509 icon_attrs={"style": ["color:black"]},
510 attrs={
511 "data-comment-id": [comment.pk],
512 "data-tooltip": ["Delete"],
513 "type": ["button"],
514 "class": ["delete-button", "comment-action"],
515 },
516 )
517 comment_actions = [edit_comment_button, delete_comment_button]
518 else:
519 answer_comment_button = Button(
520 id=f"answer-comment-{comment.pk}",
521 title="",
522 icon_class="fa-reply",
523 icon_attrs={"style": ["color:black"]},
524 attrs={
525 "data-comment-id": [comment.pk],
526 "data-author": [f"{comment.author.first_name} {comment.author.last_name}"],
527 "data-tooltip": ["Answer"],
528 "type": ["button"],
529 "class": ["reply-button", "comment-action"],
530 },
531 )
532 comment_actions = [answer_comment_button]
533 return comment_actions