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

1from dataclasses import dataclass, field 

2from typing import TYPE_CHECKING 

3 

4from django import forms 

5from django.urls import reverse 

6from django.utils.translation import gettext_lazy as _ 

7 

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 

12 

13if TYPE_CHECKING: 

14 from mesh.models.roles.base_role import Role 

15 

16 

17@dataclass 

18class Button: 

19 """ 

20 Interface for a button component. 

21 """ 

22 

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) 

36 

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)) 

42 

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)) 

48 

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)) 

54 

55 def add_attr(self, attr: str, values: list[str]): 

56 """ 

57 Add the given value to the given attribute values array. 

58 

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] = [] 

65 

66 for value in values: 

67 if value in self.attrs[attr]: 

68 continue 

69 self.attrs[attr].append(value) 

70 

71 def set_attr(self, attr: str, values: list[str]): 

72 """ 

73 Set the given value for the given attribute. 

74 

75 Ex: 

76 `button.set_attr("class", ["btn", "btn-primary"])` 

77 `button.set_attr("href", ["http://myserver"])` 

78 """ 

79 self.attrs[attr] = values 

80 

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] 

87 

88 

89@dataclass 

90class SubmissionActionList: 

91 title: str 

92 actions: list[Button] = field(default_factory=list) 

93 

94 

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) 

107 

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 ) 

122 

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 ) 

140 

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 # ) 

152 

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 ) 

184 

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 ) 

203 

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 #### 

224 

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 ) 

263 

264 # Send the submission version to review 

265 if role.can_start_review_process(submission): 

266 from ...views.forms.editorial_forms import StartReviewProcessForm 

267 

268 form = StartReviewProcessForm(initial={"process": True}) 

269 

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 ) 

287 

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 ) 

306 

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 ) 

328 

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()] 

332 

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 ] 

348 

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 ) 

369 

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 #### 

383 

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) 

389 

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 = [] 

470 

471 if article_actions: 

472 action_lists.append(SubmissionActionList(title=_("Article"), actions=article_actions)) 

473 

474 if submission_actions: 

475 action_lists.append( 

476 SubmissionActionList(title=_("Submission"), actions=submission_actions) 

477 ) 

478 

479 if review_actions: 

480 action_lists.append(SubmissionActionList(title=_("Review"), actions=review_actions)) 

481 

482 if manage_actions: 

483 action_lists.append(SubmissionActionList(title=_("Manage"), actions=manage_actions)) 

484 

485 return action_lists 

486 

487 

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