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

354 statements  

« prev     ^ index     » next       coverage.py v7.7.0, created at 2025-04-09 14:54 +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 ?", 

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