Coverage for src/ptf_tools/forms.py: 39%

354 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-07-03 13:04 +0000

1import glob 

2import os 

3from operator import itemgetter 

4 

5from allauth.account.forms import SignupForm as BaseSignupForm 

6from ckeditor_uploader.fields import RichTextUploadingFormField 

7from crispy_forms.helper import FormHelper 

8from django import forms 

9from django.conf import settings 

10from django.contrib.auth import get_user_model 

11from invitations.forms import CleanEmailMixin 

12from mersenne_cms.models import News, Page 

13from ptf.model_helpers import get_collection_id, is_site_en_only, is_site_fr_only 

14from ptf.models import ( 

15 BibItemId, 

16 Collection, 

17 ExtId, 

18 ExtLink, 

19 GraphicalAbstract, 

20 RelatedArticles, 

21 ResourceId, 

22) 

23 

24from .models import Invitation, InvitationExtraData 

25 

26TYPE_CHOICES = ( 

27 ("doi", "doi"), 

28 ("mr-item-id", "mr"), 

29 ("zbl-item-id", "zbl"), 

30 ("numdam-id", "numdam"), 

31 ("pmid", "pubmed"), 

32) 

33 

34RESOURCE_ID_CHOICES = ( 

35 ("issn", "p-issn"), 

36 ("e-issn", "e-issn"), 

37) 

38 

39REL_CHOICES = ( 

40 ("small_icon", "small_icon"), 

41 ("icon", "icon"), 

42 ("test_website", "test_website"), 

43 ("website", "website"), 

44) 

45 

46IMPORT_CHOICES = ( 

47 ("1", "Préserver des métadonnées existantes dans ptf-tools (equal-contrib, coi_statement)"), 

48 ("2", "Remplacer tout par le fichier XML"), 

49) 

50 

51 

52class PtfFormHelper(FormHelper): 

53 def __init__(self, *args, **kwargs): 

54 super().__init__(*args, **kwargs) 

55 self.label_class = "col-xs-4 col-sm-2" 

56 self.field_class = "col-xs-8 col-sm-6" 

57 self.form_tag = False 

58 

59 

60class PtfModalFormHelper(FormHelper): 

61 def __init__(self, *args, **kwargs): 

62 super().__init__(*args, **kwargs) 

63 self.label_class = "col-xs-3" 

64 self.field_class = "col-xs-8" 

65 self.form_tag = False 

66 

67 

68class PtfLargeModalFormHelper(FormHelper): 

69 def __init__(self, *args, **kwargs): 

70 super().__init__(*args, **kwargs) 

71 self.label_class = "col-xs-6" 

72 self.field_class = "col-xs-6" 

73 self.form_tag = False 

74 

75 

76class FormSetHelper(FormHelper): 

77 def __init__(self, *args, **kwargs): 

78 super().__init__(*args, **kwargs) 

79 self.form_tag = False 

80 self.template = "bootstrap3/whole_uni_formset.html" 

81 

82 

83class BibItemIdForm(forms.ModelForm): 

84 class Meta: 

85 model = BibItemId 

86 fields = ["bibitem", "id_type", "id_value"] 

87 

88 def __init__(self, *args, **kwargs): 

89 super().__init__(*args, **kwargs) 

90 self.fields["id_type"].widget = forms.Select(choices=TYPE_CHOICES) 

91 self.fields["bibitem"].widget = forms.HiddenInput() 

92 

93 

94class ExtIdForm(forms.ModelForm): 

95 class Meta: 

96 model = ExtId 

97 fields = ["resource", "id_type", "id_value"] 

98 

99 def __init__(self, *args, **kwargs): 

100 super().__init__(*args, **kwargs) 

101 self.fields["id_type"].widget = forms.Select(choices=TYPE_CHOICES) 

102 self.fields["resource"].widget = forms.HiddenInput() 

103 

104 

105class ExtLinkForm(forms.ModelForm): 

106 class Meta: 

107 model = ExtLink 

108 fields = ["rel", "location"] 

109 widgets = { 

110 "rel": forms.Select(choices=REL_CHOICES), 

111 } 

112 

113 

114class ResourceIdForm(forms.ModelForm): 

115 class Meta: 

116 model = ResourceId 

117 fields = ["id_type", "id_value"] 

118 widgets = { 

119 "id_type": forms.Select(choices=RESOURCE_ID_CHOICES), 

120 } 

121 

122 

123class CollectionForm(forms.ModelForm): 

124 class Meta: 

125 model = Collection 

126 fields = [ 

127 "pid", 

128 "provider", 

129 "coltype", 

130 "title_tex", 

131 "abbrev", 

132 "doi", 

133 "wall", 

134 "alive", 

135 "sites", 

136 ] 

137 widgets = { 

138 "title_tex": forms.TextInput(), 

139 } 

140 

141 def __init__(self, *args, **kwargs): 

142 # Add extra fields before the base class __init__ 

143 self.base_fields["description_en"] = RichTextUploadingFormField( 

144 required=False, label="Description (EN)" 

145 ) 

146 self.base_fields["description_fr"] = RichTextUploadingFormField( 

147 required=False, label="Description (FR)" 

148 ) 

149 

150 super().__init__(*args, **kwargs) 

151 

152 # self.instance is now set, specify initial values 

153 qs = self.instance.abstract_set.filter(tag="description") 

154 for abstract in qs: 

155 if abstract.lang == "fr": 

156 self.initial["description_fr"] = abstract.value_html 

157 elif abstract.lang == "en": 

158 self.initial["description_en"] = abstract.value_html 

159 

160 

161class ContainerForm(forms.Form): 

162 pid = forms.CharField(required=True, initial="") 

163 title = forms.CharField(required=False, initial="") 

164 trans_title = forms.CharField(required=False, initial="") 

165 publisher = forms.CharField(required=True, initial="") 

166 year = forms.CharField(required=False, initial="") 

167 volume = forms.CharField(required=False, initial="") 

168 number = forms.CharField(required=False, initial="") 

169 icon = forms.FileField(required=False) 

170 

171 def __init__(self, container, *args, **kwargs): 

172 super().__init__(*args, **kwargs) 

173 self.container = container 

174 

175 if "data" in kwargs: 

176 # form_invalid: preserve input values 

177 self.fields["pid"].initial = kwargs["data"]["pid"] 

178 self.fields["publisher"].initial = kwargs["data"]["publisher"] 

179 self.fields["year"].initial = kwargs["data"]["year"] 

180 self.fields["volume"].initial = kwargs["data"]["volume"] 

181 self.fields["number"].initial = kwargs["data"]["number"] 

182 self.fields["title"].initial = kwargs["data"]["title"] 

183 self.fields["trans_title"].initial = kwargs["data"]["trans_title"] 

184 elif container: 

185 self.fields["pid"].initial = container.pid 

186 self.fields["title"].initial = container.title_tex 

187 self.fields["trans_title"].initial = container.trans_title_tex 

188 if container.my_publisher: 

189 self.fields["publisher"].initial = container.my_publisher.pub_name 

190 self.fields["year"].initial = container.year 

191 self.fields["volume"].initial = container.volume 

192 self.fields["number"].initial = container.number 

193 

194 for extlink in container.extlink_set.all(): 

195 if extlink.rel == "icon": 

196 self.fields["icon"].initial = os.path.basename(extlink.location) 

197 

198 def clean(self): 

199 cleaned_data = super().clean() 

200 return cleaned_data 

201 

202 

203class ArticleForm(forms.Form): 

204 pid = forms.CharField(required=True, initial="") 

205 title = forms.CharField(required=False, initial="") 

206 fpage = forms.CharField(required=False, initial="") 

207 lpage = forms.CharField(required=False, initial="") 

208 page_count = forms.CharField(required=False, initial="") 

209 page_range = forms.CharField(required=False, initial="") 

210 icon = forms.FileField(required=False) 

211 pdf = forms.FileField(required=False) 

212 coi_statement = forms.CharField(required=False, initial="") 

213 show_body = forms.BooleanField(required=False, initial=True) 

214 do_not_publish = forms.BooleanField(required=False, initial=True) 

215 

216 def __init__(self, article, *args, **kwargs): 

217 super().__init__(*args, **kwargs) 

218 self.article = article 

219 

220 if "data" in kwargs: 

221 data = kwargs["data"] 

222 # form_invalid: preserve input values 

223 self.fields["pid"].initial = data["pid"] 

224 if "title" in data: 

225 self.fields["title"].initial = data["title"] 

226 if "fpage" in data: 

227 self.fields["fpage"].initial = data["fpage"] 

228 if "lpage" in data: 

229 self.fields["lpage"].initial = data["lpage"] 

230 if "page_range" in data: 

231 self.fields["page_range"].initial = data["page_range"] 

232 if "page_count" in data: 

233 self.fields["page_count"].initial = data["page_count"] 

234 if "coi_statement" in data: 

235 self.fields["coi_statement"].initial = data["coi_statement"] 

236 if "show_body" in data: 

237 self.fields["show_body"].initial = data["show_body"] 

238 if "do_not_publish" in data: 

239 self.fields["do_not_publish"].initial = data["do_not_publish"] 

240 elif article: 

241 # self.fields['pid'].initial = article.pid 

242 self.fields["title"].initial = article.title_tex 

243 self.fields["fpage"].initial = article.fpage 

244 self.fields["lpage"].initial = article.lpage 

245 self.fields["page_range"].initial = article.page_range 

246 self.fields["coi_statement"].initial = ( 

247 article.coi_statement if article.coi_statement else "" 

248 ) 

249 self.fields["show_body"].initial = article.show_body 

250 self.fields["do_not_publish"].initial = article.do_not_publish 

251 

252 for count in article.resourcecount_set.all(): 

253 if count.name == "page-count": 

254 self.fields["page_count"].initial = count.value 

255 

256 for extlink in article.extlink_set.all(): 

257 if extlink.rel == "icon": 

258 self.fields["icon"].initial = os.path.basename(extlink.location) 

259 

260 qs = article.datastream_set.filter(rel="full-text", mimetype="application/pdf") 

261 if qs.exists(): 

262 datastream = qs.first() 

263 self.fields["pdf"].initial = datastream.location 

264 

265 def clean(self): 

266 cleaned_data = super().clean() 

267 return cleaned_data 

268 

269 

270def cast_volume(element): 

271 # Permet le classement des volumes dans le cas où : 

272 # - un numero de volume est de la forme "11-12" (cf crchim) 

273 # - un volume est de la forme "S5" (cf smai) 

274 if not element: 

275 return "", "" 

276 try: 

277 casted = int(element.split("-")[0]) 

278 extra = "" 

279 except ValueError as _: 

280 casted = int(element.split("-")[0][1:]) 

281 extra = element 

282 return extra, casted 

283 

284 

285def unpack_pid(filename): 

286 # retourne un tableau pour chaque filename de la forme : 

287 # [filename, collection, year, vseries, volume_extra, volume, issue_extra, issue] 

288 # Permet un tri efficace par la suite 

289 collection, year, vseries, volume, issue = filename.split("/")[-1].split(".")[0].split("_") 

290 extra_volume, casted_volume = cast_volume(volume) 

291 extra_issue, casted_issue = cast_volume(issue) 

292 return ( 

293 filename, 

294 collection, 

295 year, 

296 vseries, 

297 extra_volume, 

298 casted_volume, 

299 extra_issue, 

300 casted_issue, 

301 ) 

302 

303 

304def get_volume_choices(colid, to_appear=False): 

305 if settings.IMPORT_CEDRICS_DIRECTLY: 

306 collection_folder = os.path.join(settings.CEDRAM_TEX_FOLDER, colid) 

307 

308 if to_appear: 

309 issue_folders = [ 

310 volume for volume in os.listdir(collection_folder) if f"{colid}_0" in volume 

311 ] 

312 

313 else: 

314 issue_folders = [ 

315 d 

316 for d in os.listdir(collection_folder) 

317 if os.path.isdir(os.path.join(collection_folder, d)) 

318 ] 

319 issue_folders = sorted(issue_folders, reverse=True) 

320 

321 files = [ 

322 (os.path.join(collection_folder, d, d + "-cdrxml.xml"), d) 

323 for d in issue_folders 

324 if os.path.isfile(os.path.join(collection_folder, d, d + "-cdrxml.xml")) 

325 ] 

326 else: 

327 if to_appear: 

328 volumes_path = os.path.join( 

329 settings.CEDRAM_XML_FOLDER, colid, "metadata", f"{colid}_0*.xml" 

330 ) 

331 else: 

332 volumes_path = os.path.join(settings.CEDRAM_XML_FOLDER, colid, "metadata", "*.xml") 

333 

334 files = [unpack_pid(filename) for filename in glob.glob(volumes_path)] 

335 sort = sorted(files, key=itemgetter(1, 2, 3, 4, 5, 6, 7), reverse=True) 

336 files = [(item[0], item[0].split("/")[-1]) for item in sort] 

337 return files 

338 

339 

340def get_article_choices(colid, issue_name): 

341 issue_folder = os.path.join(settings.CEDRAM_TEX_FOLDER, colid, issue_name) 

342 article_choices = [ 

343 (d, os.path.basename(d)) 

344 for d in os.listdir(issue_folder) 

345 if ( 

346 os.path.isdir(os.path.join(issue_folder, d)) 

347 and os.path.isfile(os.path.join(issue_folder, d, d + "-cdrxml.xml")) 

348 ) 

349 ] 

350 article_choices = sorted(article_choices, reverse=True) 

351 

352 return article_choices 

353 

354 

355class ImportArticleForm(forms.Form): 

356 issue = forms.ChoiceField( 

357 label="Numéro", 

358 ) 

359 article = forms.ChoiceField( 

360 label="Article", 

361 ) 

362 

363 def __init__(self, *args, **kwargs): 

364 # we need to pop this extra colid kwarg if not, the call to super.__init__ won't work 

365 colid = kwargs.pop("colid") 

366 super().__init__(*args, **kwargs) 

367 volumes = get_volume_choices(colid) 

368 self.fields["issue"].choices = volumes 

369 articles = [] 

370 if volumes: 

371 articles = get_article_choices(colid, volumes[0][1]) 

372 self.fields["article"].choices = articles 

373 

374 

375class ImportContainerForm(forms.Form): 

376 filename = forms.ChoiceField( 

377 label="Numéro", 

378 ) 

379 remove_email = forms.BooleanField( 

380 label="Supprimer les mails des contribs issus de CEDRAM ?", 

381 initial=True, 

382 required=False, 

383 ) 

384 remove_date_prod = forms.BooleanField( 

385 label="Supprimer les dates de mise en prod issues de CEDRAM ?", 

386 initial=True, 

387 required=False, 

388 ) 

389 

390 def __init__(self, *args, **kwargs): 

391 # we need to pop this extra colid kwarg if not, the call to super.__init__ won't work 

392 colid = kwargs.pop("colid") 

393 to_appear = kwargs.pop("to_appear") 

394 super().__init__(*args, **kwargs) 

395 self.fields["filename"].choices = get_volume_choices(colid, to_appear) 

396 

397 

398class DiffContainerForm(forms.Form): 

399 import_choice = forms.ChoiceField( 

400 choices=IMPORT_CHOICES, label="Que faire des différences ?", widget=forms.RadioSelect() 

401 ) 

402 

403 def __init__(self, *args, **kwargs): 

404 # we need to pop this extra full_path kwarg if not, the call to super.__init__ won't work 

405 kwargs.pop("colid") 

406 # filename = kwargs.pop('filename') 

407 # to_appear = kwargs.pop('to_appear') 

408 super().__init__(*args, **kwargs) 

409 

410 self.fields["import_choice"].initial = IMPORT_CHOICES[0][0] 

411 

412 

413class RegisterPubmedForm(forms.Form): 

414 CHOICES = [ 

415 ("off", "Yes"), 

416 ("on", "No, update the article in PubMed"), 

417 ] 

418 update_article = forms.ChoiceField( 

419 label="Are you registering the article for the first time ? Note: If you are updating the article, consider that only AuthorList (Author, Affiliation, Identifier), InvestigatorList (Investigator, Affiliation, Identifier), Pagination, ELocationID, OtherAbstract, PII and DOI fields can be updated. All other edits must be made using the PubMed Data Management system: https://www.ncbi.nlm.nih.gov/pubmed/management/", 

420 widget=forms.RadioSelect, 

421 choices=CHOICES, 

422 required=False, 

423 initial="on", 

424 ) 

425 

426 

427class CreateFrontpageForm(forms.Form): 

428 create_frontpage = forms.BooleanField( 

429 label="Update des frontpages des articles avec date de mise en ligne ?", 

430 initial=False, 

431 required=False, 

432 ) 

433 

434 

435class RelatedForm(forms.ModelForm): 

436 doi_list = forms.CharField( 

437 required=False, 

438 widget=forms.Textarea(attrs={"rows": "10", "placeholder": "doi_1\ndoi_2\ndoi_3\n"}), 

439 ) 

440 

441 exclusion_list = forms.CharField( 

442 required=False, 

443 widget=forms.Textarea(attrs={"rows": "10"}), 

444 ) 

445 

446 class Meta: 

447 model = RelatedArticles 

448 fields = ["doi_list", "exclusion_list", "automatic_list"] 

449 

450 

451class GraphicalAbstractForm(forms.ModelForm): 

452 """Form for the Graphical Abstract model""" 

453 

454 class Meta: 

455 model = GraphicalAbstract 

456 fields = ("graphical_abstract", "illustration") 

457 

458 

459class PageForm(forms.ModelForm): 

460 class Meta: 

461 model = Page 

462 fields = [ 

463 "menu_title_en", 

464 "menu_title_fr", 

465 "parent_page", 

466 "content_en", 

467 "content_fr", 

468 "state", 

469 "slug_en", 

470 "slug_fr", 

471 "menu_order", 

472 "position", 

473 "mersenne_id", 

474 "site_id", 

475 ] 

476 

477 def __init__(self, *args, **kwargs): 

478 site_id = kwargs.pop("site_id") 

479 user = kwargs.pop("user") 

480 super().__init__(*args, **kwargs) 

481 

482 self.fields["site_id"].initial = site_id 

483 

484 if not user.is_staff: 484 ↛ 491line 484 didn't jump to line 491 because the condition on line 484 was always true

485 for field_name in ["mersenne_id", "site_id"]: 

486 field = self.fields[field_name] 

487 # Hide the field is not enough, otherwise BaseForm._clean_fields will not get the value 

488 field.disabled = True 

489 field.widget = field.hidden_widget() 

490 

491 colid = get_collection_id(int(site_id)) 

492 

493 # By default, CKEditor stores files in 1 folder 

494 # We want to store the files in a @colid folder 

495 for field_name in ["content_en", "content_fr"]: 

496 field = self.fields[field_name] 

497 widget = field.widget 

498 widget.config["filebrowserUploadUrl"] = "/ckeditor/upload/" + colid 

499 widget.config["filebrowserBrowseUrl"] = "/ckeditor/browse/" + colid 

500 

501 pages = Page.objects.filter(site_id=site_id, parent_page=None) 

502 if self.instance: 502 ↛ 505line 502 didn't jump to line 505 because the condition on line 502 was always true

503 pages = pages.exclude(id=self.instance.id) 

504 

505 choices = [(p.id, p.menu_title_en) for p in pages if p.menu_title_en] 

506 self.fields["parent_page"].choices = sorted( 

507 choices + [(None, "---------")], key=lambda x: x[1] 

508 ) 

509 

510 self.fields["menu_title_en"].widget.attrs.update({"class": "menu_title"}) 

511 self.fields["menu_title_fr"].widget.attrs.update({"class": "menu_title"}) 

512 

513 if is_site_en_only(site_id): 513 ↛ 514line 513 didn't jump to line 514 because the condition on line 513 was never true

514 self.fields.pop("content_fr") 

515 self.fields.pop("menu_title_fr") 

516 self.fields.pop("slug_fr") 

517 elif is_site_fr_only(site_id): 517 ↛ 518line 517 didn't jump to line 518 because the condition on line 517 was never true

518 self.fields.pop("content_en") 

519 self.fields.pop("menu_title_en") 

520 self.fields.pop("slug_en") 

521 

522 def save_model(self, request, obj, form, change): 

523 obj.site_id = form.cleaned_data["site_id"] 

524 super().save_model(request, obj, form, change) 

525 

526 

527class NewsForm(forms.ModelForm): 

528 class Meta: 

529 model = News 

530 fields = [ 

531 "title_en", 

532 "title_fr", 

533 "content_en", 

534 "content_fr", 

535 "site_id", 

536 ] 

537 

538 def __init__(self, *args, **kwargs): 

539 site_id = kwargs.pop("site_id") 

540 user = kwargs.pop("user") 

541 super().__init__(*args, **kwargs) 

542 

543 self.fields["site_id"].initial = site_id 

544 

545 if not user.is_staff: 

546 for field_name in ["site_id"]: 

547 field = self.fields[field_name] 

548 # Hide the field is not enough, otherwise BaseForm._clean_fields will not get the value 

549 field.disabled = True 

550 field.widget = field.hidden_widget() 

551 

552 colid = get_collection_id(int(site_id)) 

553 

554 # By default, CKEditor stores files in 1 folder 

555 # We want to store the files in a @colid folder 

556 for field_name in ["content_en", "content_fr"]: 

557 field = self.fields[field_name] 

558 widget = field.widget 

559 widget.config["filebrowserUploadUrl"] = "/ckeditor/upload/" + colid 

560 widget.config["filebrowserBrowseUrl"] = "/ckeditor/browse/" + colid 

561 

562 if is_site_en_only(site_id): 

563 self.fields.pop("content_fr") 

564 self.fields.pop("title_fr") 

565 elif is_site_fr_only(site_id): 

566 self.fields.pop("content_en") 

567 self.fields.pop("title_en") 

568 

569 def save_model(self, request, obj, form, change): 

570 obj.site_id = form.cleaned_data["site_id"] 

571 super().save_model(request, obj, form, change) 

572 

573 

574class InviteUserForm(forms.Form): 

575 """Base form to invite user.""" 

576 

577 required_css_class = "required" 

578 

579 first_name = forms.CharField(label="First name", max_length=150, required=True) 

580 last_name = forms.CharField(label="Last name", max_length=150, required=True) 

581 email = forms.EmailField(label="E-mail address", required=True) 

582 

583 

584class InvitationAdminChangeForm(forms.ModelForm): 

585 class Meta: 

586 model = Invitation 

587 fields = "__all__" 

588 

589 def clean_extra_data(self): 

590 """ 

591 Enforce the JSON structure with the InvitationExtraData dataclass interface. 

592 """ 

593 try: 

594 InvitationExtraData(**self.cleaned_data["extra_data"]) 

595 except Exception as e: 

596 raise forms.ValidationError(e) 

597 

598 return self.cleaned_data["extra_data"] 

599 

600 

601class InvitationAdminAddForm(InvitationAdminChangeForm, CleanEmailMixin): 

602 class Meta: 

603 fields = ("email", "first_name", "last_name", "extra_data") 

604 

605 def save(self, *args, **kwargs): 

606 """ 

607 Populate the invitation data, save in DB and send the invitation e-mail. 

608 """ 

609 cleaned_data = self.clean() 

610 email = cleaned_data["email"] 

611 params = {"email": email} 

612 if cleaned_data.get("inviter"): 

613 params["inviter"] = cleaned_data["inviter"] 

614 else: 

615 user = getattr(self, "user", None) 

616 if isinstance(user, get_user_model()): 

617 params["inviter"] = user 

618 instance = Invitation.create(**params) 

619 instance.first_name = cleaned_data["first_name"] 

620 instance.last_name = cleaned_data["last_name"] 

621 instance.extra_data = cleaned_data.get("extra_data", {}) 

622 instance.save() 

623 full_name = f"{instance.first_name} {instance.last_name}" 

624 instance.send_invitation(self.request, **{"full_name": full_name}) 

625 super().save(*args, **kwargs) 

626 return instance 

627 

628 

629class SignupForm(BaseSignupForm): 

630 email = forms.EmailField(widget=forms.HiddenInput())