Coverage for src / mesh / views / components / button.py: 82%

96 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-05-04 12:41 +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": [reverse("mesh:submission_delete", kwargs={"pk": submission.pk})], 

131 "class": btn_classes, 

132 }, 

133 ) 

134 ) 

135 

136 # submission_actions.append( 

137 # Button( 

138 # id=f"submission-submit-{submission.pk}", 

139 # title=_("RESUME SUBMISSION"), 

140 # icon_class="fa-list-check", 

141 # attrs={ 

142 # "href": [reverse("mesh:submission_resume", kwargs={"pk": submission.pk})], 

143 # "class": btn_classes, 

144 # }, 

145 # ) 

146 # ) 

147 

148 # Edit submission metadata & authors 

149 elif role.can_edit_submission(submission): 

150 if not shortlist or can_submit_submission: 

151 article_actions.append( 

152 Button( 

153 id=f"submission-edit-metadata-{submission.pk}", 

154 title=_("EDIT METADATA"), 

155 icon_class="fa-pen-to-square", 

156 attrs={ 

157 "href": [ 

158 reverse( 

159 "mesh:submission_edit_article_metadata", 

160 kwargs={"submission_pk": submission.pk}, 

161 ) 

162 ], 

163 "class": btn_classes, 

164 }, 

165 ) 

166 ) 

167 article_actions.append( 

168 Button( 

169 id=f"submission-edit-authors-{submission.pk}", 

170 title=_("EDIT AUTHOR"), 

171 icon_class="fa-pen-to-square", 

172 attrs={ 

173 "href": [ 

174 reverse( 

175 "mesh:submission_authors", kwargs={"submission_pk": submission.pk} 

176 ) 

177 ], 

178 "class": btn_classes, 

179 }, 

180 ) 

181 ) 

182 

183 # Create a new submission version 

184 if not can_submit_submission and role.can_create_version(submission): 

185 submission_actions.append( 

186 Button( 

187 id=f"submission-create-version-{submission.pk}", 

188 title=_("SUBMIT REVISIONS"), 

189 icon_class="fa-plus", 

190 attrs={ 

191 "href": [ 

192 reverse( 

193 "mesh:submission_version_create", 

194 kwargs={"submission_pk": submission.pk}, 

195 ) 

196 ], 

197 "class": btn_classes, 

198 }, 

199 ) 

200 ) 

201 

202 # Edit the current submission version 

203 current_version = submission.get_current_version() 

204 if not can_submit_submission and current_version and role.can_edit_version(current_version): 

205 submission_actions.append( 

206 Button( 

207 id=f"submission-edit-version-{submission.pk}", 

208 title=_("RESUME REVISIONS"), 

209 icon_class="fa-list-check", 

210 attrs={ 

211 "href": [ 

212 reverse( 

213 "mesh:submission_version_update", 

214 kwargs={"pk": current_version.pk}, 

215 ) 

216 ], 

217 "class": btn_classes, 

218 }, 

219 ) 

220 ) 

221 #### [END] Author actions #### 

222 

223 #### [BEGIN] Editor actions #### 

224 if role.can_assign_editor(submission): 

225 """ 

226 <a id="assigned-editors-edit" class="as-button" href="{% url "mesh:submission_editors" pk=submission.pk%}"> 

227 <i class="fa-solid fa-plus"></i> 

228 {% translate "Assign editor" %} 

229 </a> 

230 """ 

231 submission_actions.append( 

232 Button( 

233 id=f"assigned-editors-edit-{submission.pk}", 

234 title=_("ASSIGN EDITOR"), 

235 icon_class="fa-plus", 

236 attrs={ 

237 "class": btn_classes, 

238 "data-modal-href": [ 

239 reverse( 

240 "mesh:async_assign_editor", kwargs={"submission_pk": submission.pk} 

241 ) 

242 ], 

243 "data-reload": ["true"] if kwargs.get("reload_page", False) else ["false"], 

244 "data-reload-id": [kwargs.get("reload_id", "")], 

245 "data-reload-href": ( 

246 [ 

247 reverse( 

248 "mesh:api_fetch_submission", 

249 kwargs={ 

250 "submission_pk": submission.pk, 

251 "row_id": kwargs.get("reload_id", ""), 

252 }, 

253 ) 

254 ] 

255 if kwargs.get("reload_id", "") != "" 

256 else [""] 

257 ), 

258 }, 

259 ) 

260 ) 

261 

262 # Send the submission version to review 

263 if role.can_start_review_process(submission): 

264 from ...views.forms.editorial_forms import StartReviewProcessForm 

265 

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

267 

268 review_actions.append( 

269 Button( 

270 id=f"submission-send-to-review-{submission.pk}", 

271 title=_("SEND TO REVIEW"), 

272 icon_class="fa-file-import", 

273 form=form, 

274 attrs={ 

275 "href": [ 

276 reverse( 

277 "mesh:submission_start_review_process", 

278 kwargs={"pk": submission.pk}, 

279 ) 

280 ], 

281 "class": ["as-link"], 

282 }, 

283 ) 

284 ) 

285 

286 # Make an editorial decision 

287 if role.can_create_editorial_decision(submission): 

288 submission_actions.append( 

289 Button( 

290 id=f"submission-editorial-decision-{submission.pk}", 

291 title=_("EDITORIAL DECISION"), 

292 icon_class="fa-plus", 

293 attrs={ 

294 "href": [ 

295 reverse( 

296 "mesh:editorial_decision_create", 

297 kwargs={"submission_pk": submission.pk}, 

298 ) 

299 ], 

300 "class": btn_classes, 

301 }, 

302 ) 

303 ) 

304 

305 # Request a new review 

306 if current_version and role.can_invite_reviewer(current_version): 

307 review_actions.append( 

308 Button( 

309 id=f"submission-referee-request-{submission.pk}", 

310 title=_("REQUEST REVIEW"), 

311 icon_class="fa-user-group", 

312 attrs={ 

313 "href": [ 

314 reverse( 

315 "mesh:review_create", 

316 kwargs={ 

317 "submission_pk": submission.pk, 

318 "version_pk": submission.get_current_version().pk, 

319 }, 

320 ) 

321 ], 

322 "class": btn_classes, 

323 }, 

324 ) 

325 ) 

326 

327 if current_version.number > 1: 

328 # Auto re-assign reviewers, only available after round 1 

329 existing_reviewers = [review.reviewer for review in current_version.reviews.all()] 

330 

331 previous_round_number = current_version.number - 1 

332 previous_round = submission.versions.get(number=previous_round_number) 

333 reviews_that_can_be_auto_reassigned = previous_round.reviews.filter( 

334 state=ReviewState.SUBMITTED.value 

335 ).exclude( 

336 recommendation__in=[ 

337 RecommendationValue.REJECTED.value, 

338 RecommendationValue.RESUBMIT_SOMEWHERE_ELSE.value, 

339 ] 

340 ) 

341 previous_reviewers = [ 

342 review.reviewer 

343 for review in reviews_that_can_be_auto_reassigned 

344 if review.reviewer not in existing_reviewers 

345 ] 

346 

347 if len(previous_reviewers) > 0: 

348 review_actions.append( 

349 Button( 

350 id=f"submission-referee-auto-request-{submission.pk}", 

351 title=_("ASSIGN REVIEWERS OF THE PREVIOUS ROUND"), 

352 icon_class="fa-user-group", 

353 attrs={ 

354 "href": [ 

355 reverse( 

356 "mesh:review_auto_create", 

357 kwargs={ 

358 "submission_pk": submission.pk, 

359 "version_pk": current_version.pk, 

360 }, 

361 ) 

362 ], 

363 "class": btn_classes, 

364 }, 

365 ) 

366 ) 

367 

368 # if role_handler.check_rights("can_access_submission_log", submission) and not shortlist: 

369 # manage_actions.append( 

370 # Button( 

371 # id=f"submission-log-{submission.pk}", 

372 # title=_("VIEW HISTORY"), 

373 # icon_class="fa-clock-rotate-left", 

374 # attrs={ 

375 # "href": [reverse("mesh:submission_log", kwargs={"pk": submission.pk})], 

376 # "class": btn_classes, 

377 # }, 

378 # ) 

379 # ) 

380 #### [END] Editor actions #### 

381 

382 #### [BEGIN] Reviewer actions #### 

383 # Submit / Edit the user current review 

384 current_review = None 

385 if current_version: 

386 current_review = role.get_current_open_review(current_version) 

387 

388 if current_review is not None and current_review.accepted is None: 

389 # Display 2 buttons: Accept & Decline if the review has not been accepted yet. 

390 review_actions.extend( 

391 [ 

392 Button( 

393 id=f"review-accept-{current_review.pk}", 

394 title=_("Accept review"), 

395 icon_class="fa-check", 

396 attrs={ 

397 "href": [reverse("mesh:review_accept", kwargs={"pk": current_review.pk})], 

398 # "href": [ 

399 # add_query_parameters_to_url( 

400 # reverse( 

401 # "mesh:review_accept", 

402 # kwargs={"pk": current_review.pk}, 

403 # ), 

404 # {"accepted": ["true"]}, 

405 # ) 

406 # ], 

407 "class": ["as-button", "button-success"], 

408 }, 

409 ), 

410 Button( 

411 id=f"review-accept-{current_review.pk}", 

412 title=_("Decline review"), 

413 icon_class="fa-xmark", 

414 attrs={ 

415 "href": [ 

416 reverse( 

417 "mesh:review_decline", 

418 kwargs={ 

419 "submission_pk": submission.pk, 

420 "version_pk": current_version.pk, 

421 "review_pk": current_review.pk, 

422 }, 

423 ) 

424 ], 

425 "class": ["as-button", "button-error"], 

426 }, 

427 ), 

428 ] 

429 ) 

430 elif current_review is not None: 

431 action_name = _("SUBMIT REVIEW") 

432 if current_review.state in [ 

433 ReviewState.SUBMITTED.value, 

434 ReviewState.DECLINED.value, 

435 ]: 

436 action_name = _("EDIT REVIEW") 

437 review_actions.append( 

438 Button( 

439 id=f"review-{current_review.pk}", 

440 title=action_name, 

441 icon_class="fa-pen-to-square", 

442 attrs={ 

443 "href": [ 

444 reverse( 

445 "mesh:review_update", 

446 kwargs={ 

447 "submission_pk": submission.pk, 

448 "version_pk": current_version.pk, 

449 "review_pk": current_review.pk, 

450 }, 

451 ) 

452 ], 

453 "class": btn_classes, 

454 }, 

455 ) 

456 ) 

457 #### [END] Reviewer actions #### 

458 action_lists = [] 

459 

460 if article_actions: 

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

462 

463 if submission_actions: 

464 action_lists.append( 

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

466 ) 

467 

468 if review_actions: 

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

470 

471 if manage_actions: 

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

473 

474 return action_lists