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

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.review_models import RecommendationValue, ReviewState 

9from mesh.models.orm.submission_models import Submission 

10 

11if TYPE_CHECKING: 

12 from mesh.models.roles.base_role import Role 

13 

14 

15@dataclass 

16class Button: 

17 """ 

18 Interface for a button component. 

19 """ 

20 

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) 

33 

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

39 

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

45 

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

51 

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

53 """ 

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

55 

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

62 

63 for value in values: 

64 if value in self.attrs[attr]: 

65 continue 

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

67 

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

69 """ 

70 Set the given value for the given attribute. 

71 

72 Ex: 

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

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

75 """ 

76 self.attrs[attr] = values 

77 

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] 

84 

85 

86@dataclass 

87class SubmissionActionList: 

88 title: str 

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

90 

91 

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) 

104 

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 ) 

119 

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 ) 

137 

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

149 

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 ) 

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