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