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

359 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-07-30 15:02 +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 Title, 

23) 

24 

25from .models import Invitation, InvitationExtraData 

26 

27TYPE_CHOICES = ( 

28 ("doi", "doi"), 

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

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

31 ("numdam-id", "numdam"), 

32 ("pmid", "pubmed"), 

33) 

34 

35RESOURCE_ID_CHOICES = ( 

36 ("issn", "p-issn"), 

37 ("e-issn", "e-issn"), 

38) 

39 

40REL_CHOICES = ( 

41 ("small_icon", "small_icon"), 

42 ("icon", "icon"), 

43 ("test_website", "test_website"), 

44 ("website", "website"), 

45) 

46 

47IMPORT_CHOICES = ( 

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

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

50) 

51 

52 

53class PtfFormHelper(FormHelper): 

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

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

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

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

58 self.form_tag = False 

59 

60 

61class PtfModalFormHelper(FormHelper): 

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

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

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

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

66 self.form_tag = False 

67 

68 

69class PtfLargeModalFormHelper(FormHelper): 

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

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

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

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

74 self.form_tag = False 

75 

76 

77class FormSetHelper(FormHelper): 

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

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

80 self.form_tag = False 

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

82 

83 

84class BibItemIdForm(forms.ModelForm): 

85 class Meta: 

86 model = BibItemId 

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

88 

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

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

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

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

93 

94 

95class ExtIdForm(forms.ModelForm): 

96 class Meta: 

97 model = ExtId 

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

99 

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

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

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

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

104 

105 

106class ExtLinkForm(forms.ModelForm): 

107 class Meta: 

108 model = ExtLink 

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

110 widgets = { 

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

112 } 

113 

114 

115class ResourceIdForm(forms.ModelForm): 

116 class Meta: 

117 model = ResourceId 

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

119 widgets = { 

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

121 } 

122 

123 

124class CollectionForm(forms.ModelForm): 

125 class Meta: 

126 model = Collection 

127 fields = [ 

128 "pid", 

129 "provider", 

130 "coltype", 

131 "title_tex", 

132 "abbrev", 

133 "doi", 

134 "wall", 

135 "alive", 

136 "sites", 

137 ] 

138 widgets = { 

139 "title_tex": forms.TextInput(), 

140 } 

141 

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

143 # Add extra fields before the base class __init__ 

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

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

146 ) 

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

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

149 ) 

150 

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

152 

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

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

155 for abstract in qs: 

156 if abstract.lang == "fr": 

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

158 elif abstract.lang == "en": 

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

160 

161 

162class ContainerForm(forms.Form): 

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

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

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

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

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

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

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

170 icon = forms.FileField(required=False) 

171 

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

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

174 self.container = container 

175 

176 if "data" in kwargs: 

177 # form_invalid: preserve input values 

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

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

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

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

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

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

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

185 elif container: 

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

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

188 if self.container.trans_lang and self.container.trans_lang != "und": 

189 try: 

190 self.fields["trans_title"].initial = Title.objects.get( 

191 resource=self.container, lang=self.container.trans_lang 

192 ).title_html 

193 except Title.DoesNotExist: 

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

195 else: 

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

197 

198 if container.my_publisher: 

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

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

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

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

203 

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

205 if extlink.rel == "icon": 

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

207 

208 def clean(self): 

209 cleaned_data = super().clean() 

210 return cleaned_data 

211 

212 

213class ArticleForm(forms.Form): 

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

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

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

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

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

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

220 icon = forms.FileField(required=False) 

221 pdf = forms.FileField(required=False) 

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

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

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

225 

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

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

228 self.article = article 

229 

230 if "data" in kwargs: 

231 data = kwargs["data"] 

232 # form_invalid: preserve input values 

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

234 if "title" in data: 

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

236 if "fpage" in data: 

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

238 if "lpage" in data: 

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

240 if "page_range" in data: 

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

242 if "page_count" in data: 

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

244 if "coi_statement" in data: 

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

246 if "show_body" in data: 

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

248 if "do_not_publish" in data: 

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

250 elif article: 

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

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

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

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

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

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

257 article.coi_statement if article.coi_statement else "" 

258 ) 

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

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

261 

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

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

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

265 

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

267 if extlink.rel == "icon": 

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

269 

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

271 if qs.exists(): 

272 datastream = qs.first() 

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

274 

275 def clean(self): 

276 cleaned_data = super().clean() 

277 return cleaned_data 

278 

279 

280def cast_volume(element): 

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

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

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

284 if not element: 

285 return "", "" 

286 try: 

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

288 extra = "" 

289 except ValueError as _: 

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

291 extra = element 

292 return extra, casted 

293 

294 

295def unpack_pid(filename): 

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

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

298 # Permet un tri efficace par la suite 

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

300 extra_volume, casted_volume = cast_volume(volume) 

301 extra_issue, casted_issue = cast_volume(issue) 

302 return ( 

303 filename, 

304 collection, 

305 year, 

306 vseries, 

307 extra_volume, 

308 casted_volume, 

309 extra_issue, 

310 casted_issue, 

311 ) 

312 

313 

314def get_volume_choices(colid, to_appear=False): 

315 if settings.IMPORT_CEDRICS_DIRECTLY: 

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

317 

318 if to_appear: 

319 issue_folders = [ 

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

321 ] 

322 

323 else: 

324 issue_folders = [ 

325 d 

326 for d in os.listdir(collection_folder) 

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

328 ] 

329 issue_folders = sorted(issue_folders, reverse=True) 

330 

331 files = [ 

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

333 for d in issue_folders 

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

335 ] 

336 else: 

337 if to_appear: 

338 volumes_path = os.path.join( 

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

340 ) 

341 else: 

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

343 

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

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

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

347 return files 

348 

349 

350def get_article_choices(colid, issue_name): 

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

352 article_choices = [ 

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

354 for d in os.listdir(issue_folder) 

355 if ( 

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

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

358 ) 

359 ] 

360 article_choices = sorted(article_choices, reverse=True) 

361 

362 return article_choices 

363 

364 

365class ImportArticleForm(forms.Form): 

366 issue = forms.ChoiceField( 

367 label="Numéro", 

368 ) 

369 article = forms.ChoiceField( 

370 label="Article", 

371 ) 

372 

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

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

375 colid = kwargs.pop("colid") 

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

377 volumes = get_volume_choices(colid) 

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

379 articles = [] 

380 if volumes: 

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

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

383 

384 

385class ImportContainerForm(forms.Form): 

386 filename = forms.ChoiceField( 

387 label="Numéro", 

388 ) 

389 remove_email = forms.BooleanField( 

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

391 initial=True, 

392 required=False, 

393 ) 

394 remove_date_prod = forms.BooleanField( 

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

396 initial=True, 

397 required=False, 

398 ) 

399 

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

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

402 colid = kwargs.pop("colid") 

403 to_appear = kwargs.pop("to_appear") 

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

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

406 

407 

408class DiffContainerForm(forms.Form): 

409 import_choice = forms.ChoiceField( 

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

411 ) 

412 

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

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

415 kwargs.pop("colid") 

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

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

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

419 

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

421 

422 

423class RegisterPubmedForm(forms.Form): 

424 CHOICES = [ 

425 ("off", "Yes"), 

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

427 ] 

428 update_article = forms.ChoiceField( 

429 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/", 

430 widget=forms.RadioSelect, 

431 choices=CHOICES, 

432 required=False, 

433 initial="on", 

434 ) 

435 

436 

437class CreateFrontpageForm(forms.Form): 

438 create_frontpage = forms.BooleanField( 

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

440 initial=False, 

441 required=False, 

442 ) 

443 

444 

445class RelatedForm(forms.ModelForm): 

446 doi_list = forms.CharField( 

447 required=False, 

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

449 ) 

450 

451 exclusion_list = forms.CharField( 

452 required=False, 

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

454 ) 

455 

456 class Meta: 

457 model = RelatedArticles 

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

459 

460 

461class GraphicalAbstractForm(forms.ModelForm): 

462 """Form for the Graphical Abstract model""" 

463 

464 class Meta: 

465 model = GraphicalAbstract 

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

467 

468 

469class PageForm(forms.ModelForm): 

470 class Meta: 

471 model = Page 

472 fields = [ 

473 "menu_title_en", 

474 "menu_title_fr", 

475 "parent_page", 

476 "content_en", 

477 "content_fr", 

478 "state", 

479 "slug_en", 

480 "slug_fr", 

481 "menu_order", 

482 "position", 

483 "mersenne_id", 

484 "site_id", 

485 ] 

486 

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

488 site_id = kwargs.pop("site_id") 

489 user = kwargs.pop("user") 

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

491 

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

493 

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

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

496 field = self.fields[field_name] 

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

498 field.disabled = True 

499 field.widget = field.hidden_widget() 

500 

501 colid = get_collection_id(int(site_id)) 

502 

503 # By default, CKEditor stores files in 1 folder 

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

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

506 field = self.fields[field_name] 

507 widget = field.widget 

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

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

510 

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

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

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

514 

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

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

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

518 ) 

519 

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

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

522 

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

524 self.fields.pop("content_fr") 

525 self.fields.pop("menu_title_fr") 

526 self.fields.pop("slug_fr") 

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

528 self.fields.pop("content_en") 

529 self.fields.pop("menu_title_en") 

530 self.fields.pop("slug_en") 

531 

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

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

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

535 

536 

537class NewsForm(forms.ModelForm): 

538 class Meta: 

539 model = News 

540 fields = [ 

541 "title_en", 

542 "title_fr", 

543 "content_en", 

544 "content_fr", 

545 "site_id", 

546 ] 

547 

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

549 site_id = kwargs.pop("site_id") 

550 user = kwargs.pop("user") 

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

552 

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

554 

555 if not user.is_staff: 

556 for field_name in ["site_id"]: 

557 field = self.fields[field_name] 

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

559 field.disabled = True 

560 field.widget = field.hidden_widget() 

561 

562 colid = get_collection_id(int(site_id)) 

563 

564 # By default, CKEditor stores files in 1 folder 

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

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

567 field = self.fields[field_name] 

568 widget = field.widget 

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

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

571 

572 if is_site_en_only(site_id): 

573 self.fields.pop("content_fr") 

574 self.fields.pop("title_fr") 

575 elif is_site_fr_only(site_id): 

576 self.fields.pop("content_en") 

577 self.fields.pop("title_en") 

578 

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

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

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

582 

583 

584class InviteUserForm(forms.Form): 

585 """Base form to invite user.""" 

586 

587 required_css_class = "required" 

588 

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

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

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

592 

593 

594class InvitationAdminChangeForm(forms.ModelForm): 

595 class Meta: 

596 model = Invitation 

597 fields = "__all__" 

598 

599 def clean_extra_data(self): 

600 """ 

601 Enforce the JSON structure with the InvitationExtraData dataclass interface. 

602 """ 

603 try: 

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

605 except Exception as e: 

606 raise forms.ValidationError(e) 

607 

608 return self.cleaned_data["extra_data"] 

609 

610 

611class InvitationAdminAddForm(InvitationAdminChangeForm, CleanEmailMixin): 

612 class Meta: 

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

614 

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

616 """ 

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

618 """ 

619 cleaned_data = self.clean() 

620 email = cleaned_data["email"] 

621 params = {"email": email} 

622 if cleaned_data.get("inviter"): 

623 params["inviter"] = cleaned_data["inviter"] 

624 else: 

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

626 if isinstance(user, get_user_model()): 

627 params["inviter"] = user 

628 instance = Invitation.create(**params) 

629 instance.first_name = cleaned_data["first_name"] 

630 instance.last_name = cleaned_data["last_name"] 

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

632 instance.save() 

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

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

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

636 return instance 

637 

638 

639class SignupForm(BaseSignupForm): 

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