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

364 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-02-28 09:09 +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 

8 

9from django import forms 

10from django.conf import settings 

11from django.contrib.auth import get_user_model 

12 

13from invitations.forms import CleanEmailMixin 

14from mersenne_cms.models import News 

15from mersenne_cms.models import Page 

16from ptf.model_helpers import get_collection_id 

17from ptf.model_helpers import is_site_en_only 

18from ptf.model_helpers import is_site_fr_only 

19from ptf.models import BibItemId 

20from ptf.models import Collection 

21from ptf.models import ExtId 

22from ptf.models import ExtLink 

23from ptf.models import GraphicalAbstract 

24from ptf.models import RelatedArticles 

25from ptf.models import ResourceId 

26 

27from .models import Invitation 

28from .models import InvitationExtraData 

29 

30TYPE_CHOICES = ( 

31 ("doi", "doi"), 

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

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

34 ("numdam-id", "numdam"), 

35 ("pmid", "pubmed"), 

36) 

37 

38RESOURCE_ID_CHOICES = ( 

39 ("issn", "p-issn"), 

40 ("e-issn", "e-issn"), 

41) 

42 

43REL_CHOICES = ( 

44 ("small_icon", "small_icon"), 

45 ("icon", "icon"), 

46 ("test_website", "test_website"), 

47 ("website", "website"), 

48) 

49 

50IMPORT_CHOICES = ( 

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

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

53) 

54 

55 

56class PtfFormHelper(FormHelper): 

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

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

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

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

61 self.form_tag = False 

62 

63 

64class PtfModalFormHelper(FormHelper): 

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

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

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

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

69 self.form_tag = False 

70 

71 

72class PtfLargeModalFormHelper(FormHelper): 

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

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

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

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

77 self.form_tag = False 

78 

79 

80class FormSetHelper(FormHelper): 

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

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

83 self.form_tag = False 

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

85 

86 

87class BibItemIdForm(forms.ModelForm): 

88 class Meta: 

89 model = BibItemId 

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

91 

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

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

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

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

96 

97 

98class ExtIdForm(forms.ModelForm): 

99 class Meta: 

100 model = ExtId 

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

102 

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

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

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

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

107 

108 

109class ExtLinkForm(forms.ModelForm): 

110 class Meta: 

111 model = ExtLink 

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

113 widgets = { 

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

115 } 

116 

117 

118class ResourceIdForm(forms.ModelForm): 

119 class Meta: 

120 model = ResourceId 

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

122 widgets = { 

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

124 } 

125 

126 

127class CollectionForm(forms.ModelForm): 

128 class Meta: 

129 model = Collection 

130 fields = [ 

131 "pid", 

132 "provider", 

133 "coltype", 

134 "title_tex", 

135 "abbrev", 

136 "doi", 

137 "wall", 

138 "alive", 

139 "sites", 

140 ] 

141 widgets = { 

142 "title_tex": forms.TextInput(), 

143 } 

144 

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

146 # Add extra fields before the base class __init__ 

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

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

149 ) 

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

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

152 ) 

153 

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

155 

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

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

158 for abstract in qs: 

159 if abstract.lang == "fr": 

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

161 elif abstract.lang == "en": 

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

163 

164 

165class ContainerForm(forms.Form): 

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

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

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

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

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

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

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

173 icon = forms.FileField(required=False) 

174 

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

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

177 self.container = container 

178 

179 if "data" in kwargs: 

180 # form_invalid: preserve input values 

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

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

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

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

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

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

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

188 elif container: 

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

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

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

192 if container.my_publisher: 

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

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

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

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

197 

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

199 if extlink.rel == "icon": 

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

201 

202 def clean(self): 

203 cleaned_data = super().clean() 

204 return cleaned_data 

205 

206 

207class ArticleForm(forms.Form): 

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

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

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

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

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

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

214 icon = forms.FileField(required=False) 

215 pdf = forms.FileField(required=False) 

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

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

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

219 

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

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

222 self.article = article 

223 

224 if "data" in kwargs: 

225 data = kwargs["data"] 

226 # form_invalid: preserve input values 

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

228 if "title" in data: 

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

230 if "fpage" in data: 

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

232 if "lpage" in data: 

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

234 if "page_range" in data: 

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

236 if "page_count" in data: 

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

238 if "coi_statement" in data: 

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

240 if "show_body" in data: 

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

242 if "do_not_publish" in data: 

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

244 elif article: 

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

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

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

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

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

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

251 article.coi_statement if article.coi_statement else "" 

252 ) 

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

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

255 

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

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

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

259 

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

261 if extlink.rel == "icon": 

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

263 

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

265 if qs.exists(): 

266 datastream = qs.first() 

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

268 

269 def clean(self): 

270 cleaned_data = super().clean() 

271 return cleaned_data 

272 

273 

274def cast_volume(element): 

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

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

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

278 if not element: 

279 return "", "" 

280 try: 

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

282 extra = "" 

283 except ValueError as _: 

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

285 extra = element 

286 return extra, casted 

287 

288 

289def unpack_pid(filename): 

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

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

292 # Permet un tri efficace par la suite 

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

294 extra_volume, casted_volume = cast_volume(volume) 

295 extra_issue, casted_issue = cast_volume(issue) 

296 return ( 

297 filename, 

298 collection, 

299 year, 

300 vseries, 

301 extra_volume, 

302 casted_volume, 

303 extra_issue, 

304 casted_issue, 

305 ) 

306 

307 

308def get_volume_choices(colid, to_appear=False): 

309 if settings.IMPORT_CEDRICS_DIRECTLY: 

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

311 

312 if to_appear: 

313 issue_folders = [ 

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

315 ] 

316 

317 else: 

318 issue_folders = [ 

319 d 

320 for d in os.listdir(collection_folder) 

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

322 ] 

323 issue_folders = sorted(issue_folders, reverse=True) 

324 

325 files = [ 

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

327 for d in issue_folders 

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

329 ] 

330 else: 

331 if to_appear: 

332 volumes_path = os.path.join( 

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

334 ) 

335 else: 

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

337 

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

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

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

341 return files 

342 

343 

344def get_article_choices(colid, issue_name): 

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

346 article_choices = [ 

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

348 for d in os.listdir(issue_folder) 

349 if ( 

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

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

352 ) 

353 ] 

354 article_choices = sorted(article_choices, reverse=True) 

355 

356 return article_choices 

357 

358 

359class ImportArticleForm(forms.Form): 

360 issue = forms.ChoiceField( 

361 label="Numéro", 

362 ) 

363 article = forms.ChoiceField( 

364 label="Article", 

365 ) 

366 

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

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

369 colid = kwargs.pop("colid") 

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

371 volumes = get_volume_choices(colid) 

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

373 articles = [] 

374 if volumes: 

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

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

377 

378 

379class ImportContainerForm(forms.Form): 

380 filename = forms.ChoiceField( 

381 label="Numéro", 

382 ) 

383 remove_email = forms.BooleanField( 

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

385 initial=True, 

386 required=False, 

387 ) 

388 remove_date_prod = forms.BooleanField( 

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

390 initial=True, 

391 required=False, 

392 ) 

393 

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

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

396 colid = kwargs.pop("colid") 

397 to_appear = kwargs.pop("to_appear") 

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

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

400 

401 

402class DiffContainerForm(forms.Form): 

403 import_choice = forms.ChoiceField( 

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

405 ) 

406 

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

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

409 kwargs.pop("colid") 

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

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

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

413 

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

415 

416 

417class RegisterPubmedForm(forms.Form): 

418 CHOICES = [ 

419 ("off", "Yes"), 

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

421 ] 

422 update_article = forms.ChoiceField( 

423 label="Are you registering the article for the first time ?", 

424 widget=forms.RadioSelect, 

425 choices=CHOICES, 

426 required=False, 

427 initial="on", 

428 ) 

429 

430 

431class CreateFrontpageForm(forms.Form): 

432 create_frontpage = forms.BooleanField( 

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

434 initial=False, 

435 required=False, 

436 ) 

437 

438 

439class RelatedForm(forms.ModelForm): 

440 doi_list = forms.CharField( 

441 required=False, 

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

443 ) 

444 

445 exclusion_list = forms.CharField( 

446 required=False, 

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

448 ) 

449 

450 class Meta: 

451 model = RelatedArticles 

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

453 

454 

455class GraphicalAbstractForm(forms.ModelForm): 

456 """Form for the Graphical Abstract model""" 

457 

458 class Meta: 

459 model = GraphicalAbstract 

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

461 

462 

463class PageForm(forms.ModelForm): 

464 class Meta: 

465 model = Page 

466 fields = [ 

467 "menu_title_en", 

468 "menu_title_fr", 

469 "parent_page", 

470 "content_en", 

471 "content_fr", 

472 "state", 

473 "slug_en", 

474 "slug_fr", 

475 "menu_order", 

476 "position", 

477 "mersenne_id", 

478 "site_id", 

479 ] 

480 

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

482 site_id = kwargs.pop("site_id") 

483 user = kwargs.pop("user") 

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

485 

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

487 

488 if not user.is_staff: 488 ↛ 495line 488 didn't jump to line 495, because the condition on line 488 was never false

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

490 field = self.fields[field_name] 

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

492 field.disabled = True 

493 field.widget = field.hidden_widget() 

494 

495 colid = get_collection_id(int(site_id)) 

496 

497 # By default, CKEditor stores files in 1 folder 

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

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

500 field = self.fields[field_name] 

501 widget = field.widget 

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

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

504 

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

506 if self.instance: 506 ↛ 509line 506 didn't jump to line 509, because the condition on line 506 was never false

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

508 

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

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

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

512 ) 

513 

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

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

516 

517 if is_site_en_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_fr") 

519 self.fields.pop("menu_title_fr") 

520 self.fields.pop("slug_fr") 

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

522 self.fields.pop("content_en") 

523 self.fields.pop("menu_title_en") 

524 self.fields.pop("slug_en") 

525 

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

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

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

529 

530 

531class NewsForm(forms.ModelForm): 

532 class Meta: 

533 model = News 

534 fields = [ 

535 "title_en", 

536 "title_fr", 

537 "content_en", 

538 "content_fr", 

539 "site_id", 

540 ] 

541 

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

543 site_id = kwargs.pop("site_id") 

544 user = kwargs.pop("user") 

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

546 

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

548 

549 if not user.is_staff: 

550 for field_name in ["site_id"]: 

551 field = self.fields[field_name] 

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

553 field.disabled = True 

554 field.widget = field.hidden_widget() 

555 

556 colid = get_collection_id(int(site_id)) 

557 

558 # By default, CKEditor stores files in 1 folder 

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

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

561 field = self.fields[field_name] 

562 widget = field.widget 

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

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

565 

566 if is_site_en_only(site_id): 

567 self.fields.pop("content_fr") 

568 self.fields.pop("title_fr") 

569 elif is_site_fr_only(site_id): 

570 self.fields.pop("content_en") 

571 self.fields.pop("title_en") 

572 

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

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

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

576 

577 

578class InviteUserForm(forms.Form): 

579 """Base form to invite user.""" 

580 

581 required_css_class = "required" 

582 

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

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

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

586 

587 

588class InvitationAdminChangeForm(forms.ModelForm): 

589 class Meta: 

590 model = Invitation 

591 fields = "__all__" 

592 

593 def clean_extra_data(self): 

594 """ 

595 Enforce the JSON structure with the InvitationExtraData dataclass interface. 

596 """ 

597 try: 

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

599 except Exception as e: 

600 raise forms.ValidationError(e) 

601 

602 return self.cleaned_data["extra_data"] 

603 

604 

605class InvitationAdminAddForm(InvitationAdminChangeForm, CleanEmailMixin): 

606 class Meta: 

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

608 

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

610 """ 

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

612 """ 

613 cleaned_data = self.clean() 

614 email = cleaned_data["email"] 

615 params = {"email": email} 

616 if cleaned_data.get("inviter"): 

617 params["inviter"] = cleaned_data["inviter"] 

618 else: 

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

620 if isinstance(user, get_user_model()): 

621 params["inviter"] = user 

622 instance = Invitation.create(**params) 

623 instance.first_name = cleaned_data["first_name"] 

624 instance.last_name = cleaned_data["last_name"] 

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

626 instance.save() 

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

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

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

630 return instance 

631 

632 

633class SignupForm(BaseSignupForm): 

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