Coverage for src/ptf_tools/forms.py: 38%
365 statements
« prev ^ index » next coverage.py v7.9.0, created at 2025-11-26 13:32 +0000
« prev ^ index » next coverage.py v7.9.0, created at 2025-11-26 13:32 +0000
1import glob
2import os
3from operator import itemgetter
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)
25from .models import Invitation, InvitationExtraData
27TYPE_CHOICES = (
28 ("doi", "doi"),
29 ("mr-item-id", "mr"),
30 ("zbl-item-id", "zbl"),
31 ("numdam-id", "numdam"),
32 ("pmid", "pubmed"),
33)
35RESOURCE_ID_CHOICES = (
36 ("issn", "p-issn"),
37 ("e-issn", "e-issn"),
38)
40REL_CHOICES = (
41 ("small_icon", "small_icon"),
42 ("icon", "icon"),
43 ("test_website", "test_website"),
44 ("website", "website"),
45)
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)
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
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
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
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"
84class BibItemIdForm(forms.ModelForm):
85 class Meta:
86 model = BibItemId
87 fields = ["bibitem", "id_type", "id_value"]
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()
95class ExtIdForm(forms.ModelForm):
96 class Meta:
97 model = ExtId
98 fields = ["resource", "id_type", "id_value"]
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()
106class ExtLinkForm(forms.ModelForm):
107 class Meta:
108 model = ExtLink
109 fields = ["rel", "location"]
110 widgets = {
111 "rel": forms.Select(choices=REL_CHOICES),
112 }
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 }
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 }
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 )
151 super().__init__(*args, **kwargs)
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
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)
172 def __init__(self, container, *args, **kwargs):
173 super().__init__(*args, **kwargs)
174 self.container = container
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
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
204 for extlink in container.extlink_set.all():
205 if extlink.rel == "icon":
206 self.fields["icon"].initial = os.path.basename(extlink.location)
208 def clean(self):
209 cleaned_data = super().clean()
210 return cleaned_data
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)
226 def __init__(self, article, *args, **kwargs):
227 super().__init__(*args, **kwargs)
228 self.article = article
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
262 for count in article.resourcecount_set.all():
263 if count.name == "page-count":
264 self.fields["page_count"].initial = count.value
266 for extlink in article.extlink_set.all():
267 if extlink.rel == "icon":
268 self.fields["icon"].initial = os.path.basename(extlink.location)
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
275 def clean(self):
276 cleaned_data = super().clean()
277 return cleaned_data
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
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 )
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)
318 if to_appear:
319 issue_folders = [
320 volume for volume in os.listdir(collection_folder) if f"{colid}_0" in volume
321 ]
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)
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")
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
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)
362 return article_choices
365class ImportArticleForm(forms.Form):
366 issue = forms.ChoiceField(
367 label="Numéro",
368 )
369 article = forms.ChoiceField(
370 label="Article",
371 )
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
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 )
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)
408class DiffContainerForm(forms.Form):
409 import_choice = forms.ChoiceField(
410 choices=IMPORT_CHOICES, label="Que faire des différences ?", widget=forms.RadioSelect()
411 )
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)
420 self.fields["import_choice"].initial = IMPORT_CHOICES[0][0]
423class ImportEditflowArticleForm(forms.Form):
424 @staticmethod
425 def validate_xml_file(value):
426 if not value.name.lower().endswith(".xml"):
427 raise forms.ValidationError("Only .xml files are allowed.")
429 editflow_xml_file = forms.FileField(
430 label="Import an article from an XML file provided by Editflow.",
431 required=True,
432 validators=[validate_xml_file.__func__],
433 widget=forms.ClearableFileInput(attrs={"accept": ".xml"}),
434 help_text="Only .xml files are accepted.",
435 )
438class RegisterPubmedForm(forms.Form):
439 CHOICES = [
440 ("off", "Yes"),
441 ("on", "No, update the article in PubMed"),
442 ]
443 update_article = forms.ChoiceField(
444 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/",
445 widget=forms.RadioSelect,
446 choices=CHOICES,
447 required=False,
448 initial="on",
449 )
452class CreateFrontpageForm(forms.Form):
453 create_frontpage = forms.BooleanField(
454 label="Update des frontpages des articles avec date de mise en ligne ?",
455 initial=False,
456 required=False,
457 )
460class RelatedForm(forms.ModelForm):
461 doi_list = forms.CharField(
462 required=False,
463 widget=forms.Textarea(attrs={"rows": "10", "placeholder": "doi_1\ndoi_2\ndoi_3\n"}),
464 )
466 exclusion_list = forms.CharField(
467 required=False,
468 widget=forms.Textarea(attrs={"rows": "10"}),
469 )
471 class Meta:
472 model = RelatedArticles
473 fields = ["doi_list", "exclusion_list", "automatic_list"]
476class GraphicalAbstractForm(forms.ModelForm):
477 """Form for the Graphical Abstract model"""
479 class Meta:
480 model = GraphicalAbstract
481 fields = ("graphical_abstract", "illustration")
484class PageForm(forms.ModelForm):
485 class Meta:
486 model = Page
487 fields = [
488 "menu_title_en",
489 "menu_title_fr",
490 "parent_page",
491 "content_en",
492 "content_fr",
493 "state",
494 "slug_en",
495 "slug_fr",
496 "menu_order",
497 "position",
498 "mersenne_id",
499 "site_id",
500 ]
502 def __init__(self, *args, **kwargs):
503 site_id = kwargs.pop("site_id")
504 user = kwargs.pop("user")
505 super().__init__(*args, **kwargs)
507 self.fields["site_id"].initial = site_id
509 if not user.is_staff: 509 ↛ 516line 509 didn't jump to line 516 because the condition on line 509 was always true
510 for field_name in ["mersenne_id", "site_id"]:
511 field = self.fields[field_name]
512 # Hide the field is not enough, otherwise BaseForm._clean_fields will not get the value
513 field.disabled = True
514 field.widget = field.hidden_widget()
516 colid = get_collection_id(int(site_id))
518 # By default, CKEditor stores files in 1 folder
519 # We want to store the files in a @colid folder
520 for field_name in ["content_en", "content_fr"]:
521 field = self.fields[field_name]
522 widget = field.widget
523 widget.config["filebrowserUploadUrl"] = "/ckeditor/upload/" + colid
524 widget.config["filebrowserBrowseUrl"] = "/ckeditor/browse/" + colid
526 pages = Page.objects.filter(site_id=site_id, parent_page=None)
527 if self.instance: 527 ↛ 530line 527 didn't jump to line 530 because the condition on line 527 was always true
528 pages = pages.exclude(id=self.instance.id)
530 choices = [(p.id, p.menu_title_en) for p in pages if p.menu_title_en]
531 self.fields["parent_page"].choices = sorted(
532 choices + [(None, "---------")], key=lambda x: x[1]
533 )
535 self.fields["menu_title_en"].widget.attrs.update({"class": "menu_title"})
536 self.fields["menu_title_fr"].widget.attrs.update({"class": "menu_title"})
538 if is_site_en_only(site_id): 538 ↛ 539line 538 didn't jump to line 539 because the condition on line 538 was never true
539 self.fields.pop("content_fr")
540 self.fields.pop("menu_title_fr")
541 self.fields.pop("slug_fr")
542 elif is_site_fr_only(site_id): 542 ↛ 543line 542 didn't jump to line 543 because the condition on line 542 was never true
543 self.fields.pop("content_en")
544 self.fields.pop("menu_title_en")
545 self.fields.pop("slug_en")
547 def save_model(self, request, obj, form, change):
548 obj.site_id = form.cleaned_data["site_id"]
549 super().save_model(request, obj, form, change)
552class NewsForm(forms.ModelForm):
553 class Meta:
554 model = News
555 fields = [
556 "title_en",
557 "title_fr",
558 "content_en",
559 "content_fr",
560 "site_id",
561 ]
563 def __init__(self, *args, **kwargs):
564 site_id = kwargs.pop("site_id")
565 user = kwargs.pop("user")
566 super().__init__(*args, **kwargs)
568 self.fields["site_id"].initial = site_id
570 if not user.is_staff:
571 for field_name in ["site_id"]:
572 field = self.fields[field_name]
573 # Hide the field is not enough, otherwise BaseForm._clean_fields will not get the value
574 field.disabled = True
575 field.widget = field.hidden_widget()
577 colid = get_collection_id(int(site_id))
579 # By default, CKEditor stores files in 1 folder
580 # We want to store the files in a @colid folder
581 for field_name in ["content_en", "content_fr"]:
582 field = self.fields[field_name]
583 widget = field.widget
584 widget.config["filebrowserUploadUrl"] = "/ckeditor/upload/" + colid
585 widget.config["filebrowserBrowseUrl"] = "/ckeditor/browse/" + colid
587 if is_site_en_only(site_id):
588 self.fields.pop("content_fr")
589 self.fields.pop("title_fr")
590 elif is_site_fr_only(site_id):
591 self.fields.pop("content_en")
592 self.fields.pop("title_en")
594 def save_model(self, request, obj, form, change):
595 obj.site_id = form.cleaned_data["site_id"]
596 super().save_model(request, obj, form, change)
599class InviteUserForm(forms.Form):
600 """Base form to invite user."""
602 required_css_class = "required"
604 first_name = forms.CharField(label="First name", max_length=150, required=True)
605 last_name = forms.CharField(label="Last name", max_length=150, required=True)
606 email = forms.EmailField(label="E-mail address", required=True)
609class InvitationAdminChangeForm(forms.ModelForm):
610 class Meta:
611 model = Invitation
612 fields = "__all__"
614 def clean_extra_data(self):
615 """
616 Enforce the JSON structure with the InvitationExtraData dataclass interface.
617 """
618 try:
619 InvitationExtraData(**self.cleaned_data["extra_data"])
620 except Exception as e:
621 raise forms.ValidationError(e)
623 return self.cleaned_data["extra_data"]
626class InvitationAdminAddForm(InvitationAdminChangeForm, CleanEmailMixin):
627 class Meta:
628 fields = ("email", "first_name", "last_name", "extra_data")
630 def save(self, *args, **kwargs):
631 """
632 Populate the invitation data, save in DB and send the invitation e-mail.
633 """
634 cleaned_data = self.clean()
635 email = cleaned_data["email"]
636 params = {"email": email}
637 if cleaned_data.get("inviter"):
638 params["inviter"] = cleaned_data["inviter"]
639 else:
640 user = getattr(self, "user", None)
641 if isinstance(user, get_user_model()):
642 params["inviter"] = user
643 instance = Invitation.create(**params)
644 instance.first_name = cleaned_data["first_name"]
645 instance.last_name = cleaned_data["last_name"]
646 instance.extra_data = cleaned_data.get("extra_data", {})
647 instance.save()
648 full_name = f"{instance.first_name} {instance.last_name}"
649 instance.send_invitation(self.request, **{"full_name": full_name})
650 super().save(*args, **kwargs)
651 return instance
654class SignupForm(BaseSignupForm):
655 email = forms.EmailField(widget=forms.HiddenInput())