Coverage for src / mesh / views / components / button.py: 77%
106 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-22 07:20 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-22 07:20 +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) and (not shortlist or can_submit_submission):
155 article_actions.append(
156 Button(
157 id=f"submission-edit-metadata-{submission.pk}",
158 title=_("EDIT METADATA"),
159 icon_class="fa-pen-to-square",
160 attrs={
161 "href": [
162 reverse(
163 "mesh:submission_edit_article_metadata",
164 kwargs={"submission_pk": submission.pk},
165 )
166 ],
167 "class": btn_classes,
168 },
169 )
170 )
171 article_actions.append(
172 Button(
173 id=f"submission-edit-authors-{submission.pk}",
174 title=_("EDIT AUTHOR"),
175 icon_class="fa-pen-to-square",
176 attrs={
177 "href": [
178 reverse("mesh:submission_authors", kwargs={"submission_pk": submission.pk})
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
488def build_comment_actions(comment: Comment, user: User):
489 if comment.author == user:
490 edit_comment_button = Button(
491 id=f"edit-comment-{comment.pk}",
492 title="",
493 icon_class="fa-pen-to-square",
494 icon_attrs={"style": ["color:black"]},
495 attrs={
496 "data-comment-id": [comment.pk],
497 "data-tooltip": ["Edit"],
498 "type": ["button"],
499 "class": ["edit-button", "comment-action"],
500 },
501 )
502 delete_comment_button = Button(
503 id=f"edit-comment-{comment.pk}",
504 title="",
505 icon_class="fa-trash-can",
506 icon_attrs={"style": ["color:black"]},
507 attrs={
508 "data-comment-id": [comment.pk],
509 "data-tooltip": ["Delete"],
510 "type": ["button"],
511 "class": ["delete-button", "comment-action"],
512 },
513 )
514 comment_actions = [edit_comment_button, delete_comment_button]
515 else:
516 answer_comment_button = Button(
517 id=f"answer-comment-{comment.pk}",
518 title="",
519 icon_class="fa-reply",
520 icon_attrs={"style": ["color:black"]},
521 attrs={
522 "data-comment-id": [comment.pk],
523 "data-author": [f"{comment.author.first_name} {comment.author.last_name}"],
524 "data-tooltip": ["Answer"],
525 "type": ["button"],
526 "class": ["reply-button", "comment-action"],
527 },
528 )
529 comment_actions = [answer_comment_button]
530 return comment_actions