Coverage for src / ptf_tools / forms.py: 38%
357 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-09-18 08:07 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-09-18 08:07 +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 Collection,
16 ExtId,
17 ExtLink,
18 GraphicalAbstract,
19 RelatedArticles,
20 ResourceId,
21)
23from .models import Invitation, InvitationExtraData
25TYPE_CHOICES = (
26 ("doi", "doi"),
27 ("mr-item-id", "mr"),
28 ("zbl-item-id", "zbl"),
29 ("numdam-id", "numdam"),
30 ("pmid", "pubmed"),
31)
33RESOURCE_ID_CHOICES = (
34 ("issn", "p-issn"),
35 ("e-issn", "e-issn"),
36)
38REL_CHOICES = (
39 ("small_icon", "small_icon"),
40 ("icon", "icon"),
41 ("test_website", "test_website"),
42 ("website", "website"),
43)
45IMPORT_CHOICES = (
46 ("1", "Préserver des métadonnées existantes dans ptf-tools (equal-contrib, coi_statement)"),
47 ("2", "Remplacer tout par le fichier XML"),
48)
51class PtfFormHelper(FormHelper):
52 def __init__(self, *args, **kwargs):
53 super().__init__(*args, **kwargs)
54 self.label_class = "col-4 col-sm-2"
55 self.field_class = "col-8 col-sm-6"
56 self.form_tag = False
59class PtfModalFormHelper(FormHelper):
60 def __init__(self, *args, **kwargs):
61 super().__init__(*args, **kwargs)
62 self.label_class = "col-3"
63 self.field_class = "col-8"
64 self.form_tag = False
67class PtfLargeModalFormHelper(FormHelper):
68 def __init__(self, *args, **kwargs):
69 super().__init__(*args, **kwargs)
70 self.label_class = "col-6"
71 self.field_class = "col-6"
72 self.form_tag = False
75class FormSetHelper(FormHelper):
76 def __init__(self, *args, **kwargs):
77 super().__init__(*args, **kwargs)
78 self.form_tag = False
79 self.template = "bootstrap5/whole_uni_formset.html"
82class ExtIdForm(forms.ModelForm):
83 class Meta:
84 model = ExtId
85 fields = ["resource", "id_type", "id_value"]
87 def __init__(self, *args, **kwargs):
88 super().__init__(*args, **kwargs)
89 self.fields["id_type"].widget = forms.Select(choices=TYPE_CHOICES)
90 self.fields["resource"].widget = forms.HiddenInput()
93class ExtLinkForm(forms.ModelForm):
94 class Meta:
95 model = ExtLink
96 fields = ["rel", "location"]
97 widgets = {
98 "rel": forms.Select(choices=REL_CHOICES),
99 }
102class ResourceIdForm(forms.ModelForm):
103 class Meta:
104 model = ResourceId
105 fields = ["id_type", "id_value"]
106 widgets = {
107 "id_type": forms.Select(choices=RESOURCE_ID_CHOICES),
108 }
111class CollectionForm(forms.ModelForm):
112 class Meta:
113 model = Collection
114 fields = [
115 "pid",
116 "provider",
117 "coltype",
118 "title_tex",
119 "abbrev",
120 "doi",
121 "wall",
122 "alive",
123 "sites",
124 ]
125 widgets = {
126 "title_tex": forms.TextInput(),
127 }
129 def __init__(self, *args, **kwargs):
130 # Add extra fields before the base class __init__
131 self.base_fields["description_en"] = RichTextUploadingFormField(
132 required=False, label="Description (EN)"
133 )
134 self.base_fields["description_fr"] = RichTextUploadingFormField(
135 required=False, label="Description (FR)"
136 )
138 super().__init__(*args, **kwargs)
140 # self.instance is now set, specify initial values
141 qs = self.instance.abstract_set.filter(tag="description")
142 for abstract in qs:
143 if abstract.lang == "fr":
144 self.initial["description_fr"] = abstract.value_html
145 elif abstract.lang == "en":
146 self.initial["description_en"] = abstract.value_html
149class ContainerForm(forms.Form):
150 pid = forms.CharField(required=True, initial="")
151 title = forms.CharField(required=False, initial="")
152 trans_title = forms.CharField(required=False, initial="")
153 publisher = forms.CharField(required=True, initial="")
154 year = forms.CharField(required=False, initial="")
155 volume = forms.CharField(required=False, initial="")
156 number = forms.CharField(required=False, initial="")
157 icon = forms.FileField(required=False)
159 def __init__(self, container, *args, **kwargs):
160 super().__init__(*args, **kwargs)
161 self.container = container
163 if "data" in kwargs:
164 # form_invalid: preserve input values
165 self.fields["pid"].initial = kwargs["data"]["pid"]
166 self.fields["publisher"].initial = kwargs["data"]["publisher"]
167 self.fields["year"].initial = kwargs["data"]["year"]
168 self.fields["volume"].initial = kwargs["data"]["volume"]
169 self.fields["number"].initial = kwargs["data"]["number"]
170 self.fields["title"].initial = kwargs["data"]["title"]
171 self.fields["trans_title"].initial = kwargs["data"]["trans_title"]
172 elif container:
173 self.fields["pid"].initial = container.pid
174 self.fields["title"].initial = container.title_tex
175 # We arbitrarily take the first translated title
176 trans_title = self.container.title_set.filter(type="main").first()
177 if trans_title:
178 self.fields["trans_title"].initial = trans_title.title_html
179 else:
180 self.fields["trans_title"].initial = ""
182 if container.my_publisher:
183 self.fields["publisher"].initial = container.my_publisher.pub_name
184 self.fields["year"].initial = container.fyear
185 self.fields["volume"].initial = container.volume
186 self.fields["number"].initial = container.number
188 for extlink in container.extlink_set.all():
189 if extlink.rel == "icon":
190 self.fields["icon"].initial = os.path.basename(extlink.location)
192 def clean(self):
193 cleaned_data = super().clean()
194 return cleaned_data
197class ArticleForm(forms.Form):
198 pid = forms.CharField(required=True, initial="")
199 title = forms.CharField(required=False, initial="")
200 fpage = forms.CharField(required=False, initial="")
201 lpage = forms.CharField(required=False, initial="")
202 page_count = forms.CharField(required=False, initial="")
203 page_range = forms.CharField(required=False, initial="")
204 icon = forms.FileField(required=False)
205 pdf = forms.FileField(required=False)
206 coi_statement = forms.CharField(required=False, initial="")
207 show_body = forms.BooleanField(required=False, initial=True)
208 do_not_publish = forms.BooleanField(required=False, initial=True)
210 def __init__(self, article, *args, **kwargs):
211 super().__init__(*args, **kwargs)
212 self.article = article
214 if "data" in kwargs:
215 data = kwargs["data"]
216 # form_invalid: preserve input values
217 self.fields["pid"].initial = data["pid"]
218 if "title" in data:
219 self.fields["title"].initial = data["title"]
220 if "fpage" in data:
221 self.fields["fpage"].initial = data["fpage"]
222 if "lpage" in data:
223 self.fields["lpage"].initial = data["lpage"]
224 if "page_range" in data:
225 self.fields["page_range"].initial = data["page_range"]
226 if "page_count" in data:
227 self.fields["page_count"].initial = data["page_count"]
228 if "coi_statement" in data:
229 self.fields["coi_statement"].initial = data["coi_statement"]
230 if "show_body" in data:
231 self.fields["show_body"].initial = data["show_body"]
232 if "do_not_publish" in data:
233 self.fields["do_not_publish"].initial = data["do_not_publish"]
234 elif article:
235 # self.fields['pid'].initial = article.pid
236 self.fields["title"].initial = article.title_tex
237 self.fields["fpage"].initial = article.fpage
238 self.fields["lpage"].initial = article.lpage
239 self.fields["page_range"].initial = article.page_range
240 self.fields["coi_statement"].initial = (
241 article.coi_statement if article.coi_statement else ""
242 )
243 self.fields["show_body"].initial = article.show_body
244 self.fields["do_not_publish"].initial = article.do_not_publish
246 for count in article.resourcecount_set.all():
247 if count.name == "page-count":
248 self.fields["page_count"].initial = count.value
250 for extlink in article.extlink_set.all():
251 if extlink.rel == "icon":
252 self.fields["icon"].initial = os.path.basename(extlink.location)
254 qs = article.datastream_set.filter(rel="full-text", mimetype="application/pdf")
255 if qs.exists():
256 datastream = qs.first()
257 self.fields["pdf"].initial = datastream.location
259 def clean(self):
260 cleaned_data = super().clean()
261 return cleaned_data
264def cast_volume(element):
265 # Permet le classement des volumes dans le cas où :
266 # - un numero de volume est de la forme "11-12" (cf crchim)
267 # - un volume est de la forme "S5" (cf smai)
268 if not element:
269 return "", ""
270 try:
271 casted = int(element.split("-")[0])
272 extra = ""
273 except ValueError as _:
274 casted = int(element.split("-")[0][1:])
275 extra = element
276 return extra, casted
279def unpack_pid(filename):
280 # retourne un tableau pour chaque filename de la forme :
281 # [filename, collection, year, vseries, volume_extra, volume, issue_extra, issue]
282 # Permet un tri efficace par la suite
283 collection, year, vseries, volume, issue = filename.split("/")[-1].split(".")[0].split("_")
284 extra_volume, casted_volume = cast_volume(volume)
285 extra_issue, casted_issue = cast_volume(issue)
286 return (
287 filename,
288 collection,
289 year,
290 vseries,
291 extra_volume,
292 casted_volume,
293 extra_issue,
294 casted_issue,
295 )
298def get_volume_choices(colid, to_appear=False):
299 if settings.IMPORT_CEDRICS_DIRECTLY:
300 collection_folder = os.path.join(settings.CEDRAM_TEX_FOLDER, colid)
302 if to_appear:
303 issue_folders = [
304 volume for volume in os.listdir(collection_folder) if f"{colid}_0" in volume
305 ]
307 else:
308 issue_folders = [
309 d
310 for d in os.listdir(collection_folder)
311 if os.path.isdir(os.path.join(collection_folder, d))
312 ]
313 issue_folders = sorted(issue_folders, reverse=True)
315 files = [
316 (os.path.join(collection_folder, d, d + "-cdrxml.xml"), d)
317 for d in issue_folders
318 if os.path.isfile(os.path.join(collection_folder, d, d + "-cdrxml.xml"))
319 ]
320 else:
321 if to_appear:
322 volumes_path = os.path.join(
323 settings.CEDRAM_XML_FOLDER, colid, "metadata", f"{colid}_0*.xml"
324 )
325 else:
326 volumes_path = os.path.join(settings.CEDRAM_XML_FOLDER, colid, "metadata", "*.xml")
328 files = [unpack_pid(filename) for filename in glob.glob(volumes_path)]
329 sort = sorted(files, key=itemgetter(1, 2, 3, 4, 5, 6, 7), reverse=True)
330 files = [(item[0], item[0].split("/")[-1]) for item in sort]
331 return files
334def get_article_choices(colid, issue_name):
335 issue_folder = os.path.join(settings.CEDRAM_TEX_FOLDER, colid, issue_name)
336 article_choices = [
337 (d, os.path.basename(d))
338 for d in os.listdir(issue_folder)
339 if (
340 os.path.isdir(os.path.join(issue_folder, d))
341 and os.path.isfile(os.path.join(issue_folder, d, d + "-cdrxml.xml"))
342 )
343 ]
344 article_choices = sorted(article_choices, reverse=True)
346 return article_choices
349class ImportArticleForm(forms.Form):
350 issue = forms.ChoiceField(
351 label="Numéro",
352 )
353 article = forms.ChoiceField(
354 label="Article",
355 )
357 def __init__(self, *args, **kwargs):
358 # we need to pop this extra colid kwarg if not, the call to super.__init__ won't work
359 colid = kwargs.pop("colid")
360 super().__init__(*args, **kwargs)
361 volumes = get_volume_choices(colid)
362 self.fields["issue"].choices = volumes
363 articles = []
364 if volumes:
365 articles = get_article_choices(colid, volumes[0][1])
366 self.fields["article"].choices = articles
369class ImportContainerForm(forms.Form):
370 filename = forms.ChoiceField(
371 label="Numéro",
372 )
373 remove_email = forms.BooleanField(
374 label="Delete emails from CEDRAM contribs?",
375 initial=True,
376 required=False,
377 )
378 remove_date_prod = forms.BooleanField(
379 label="Delete published dates from CEDRAM?",
380 initial=True,
381 required=False,
382 )
384 def __init__(self, *args, **kwargs):
385 # we need to pop this extra colid kwarg if not, the call to super.__init__ won't work
386 colid = kwargs.pop("colid")
387 to_appear = kwargs.pop("to_appear")
388 super().__init__(*args, **kwargs)
389 self.fields["filename"].choices = get_volume_choices(colid, to_appear)
392class DiffContainerForm(forms.Form):
393 import_choice = forms.ChoiceField(
394 choices=IMPORT_CHOICES, label="Que faire des différences ?", widget=forms.RadioSelect()
395 )
397 def __init__(self, *args, **kwargs):
398 # we need to pop this extra full_path kwarg if not, the call to super.__init__ won't work
399 kwargs.pop("colid")
400 # filename = kwargs.pop('filename')
401 # to_appear = kwargs.pop('to_appear')
402 super().__init__(*args, **kwargs)
404 self.fields["import_choice"].initial = IMPORT_CHOICES[0][0]
407class ImportEditflowArticleForm(forms.Form):
408 def validate_xml_file(value):
409 if not value.name.lower().endswith(".xml"):
410 raise forms.ValidationError("Only .xml files are allowed.")
412 editflow_xml_file = forms.FileField(
413 label="Import an article from an XML file provided by Editflow.",
414 required=True,
415 validators=[validate_xml_file],
416 widget=forms.ClearableFileInput(attrs={"accept": ".xml"}),
417 help_text="Only .xml files are accepted.",
418 )
420 def __init__(self, *args, **kwargs):
421 # we need to pop this extra full_path kwarg if not, the call to super.__init__ won't work
422 kwargs.pop("colid", None)
423 super().__init__(*args, **kwargs)
426class RegisterPubmedForm(forms.Form):
427 CHOICES = [
428 ("off", "Yes"),
429 ("on", "No, update the article in PubMed"),
430 ]
431 update_article = forms.ChoiceField(
432 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/",
433 widget=forms.RadioSelect,
434 choices=CHOICES,
435 required=False,
436 initial="on",
437 )
440class CreateFrontpageForm(forms.Form):
441 create_frontpage = forms.BooleanField(
442 label="Update des frontpages des articles avec date de mise en ligne ?",
443 initial=False,
444 required=False,
445 )
448class RelatedForm(forms.ModelForm):
449 doi_list = forms.CharField(
450 required=False,
451 widget=forms.Textarea(attrs={"rows": "10", "placeholder": "doi_1\ndoi_2\ndoi_3\n"}),
452 )
454 exclusion_list = forms.CharField(
455 required=False,
456 widget=forms.Textarea(attrs={"rows": "10"}),
457 )
459 class Meta:
460 model = RelatedArticles
461 fields = ["doi_list", "exclusion_list", "automatic_list"]
464class GraphicalAbstractForm(forms.ModelForm):
465 """Form for the Graphical Abstract model"""
467 class Meta:
468 model = GraphicalAbstract
469 fields = ("graphical_abstract", "illustration")
472class PageForm(forms.ModelForm):
473 class Meta:
474 model = Page
475 fields = [
476 "menu_title_en",
477 "menu_title_fr",
478 "parent_page",
479 "content_en",
480 "content_fr",
481 "state",
482 "slug_en",
483 "slug_fr",
484 "menu_order",
485 "position",
486 "mersenne_id",
487 "site_id",
488 ]
490 def __init__(self, *args, **kwargs):
491 site_id = kwargs.pop("site_id")
492 user = kwargs.pop("user")
493 super().__init__(*args, **kwargs)
495 self.fields["site_id"].initial = site_id
497 if not user.is_staff: 497 ↛ 504line 497 didn't jump to line 504 because the condition on line 497 was always true
498 for field_name in ["mersenne_id", "site_id"]:
499 field = self.fields[field_name]
500 # Hide the field is not enough, otherwise BaseForm._clean_fields will not get the value
501 field.disabled = True
502 field.widget = field.hidden_widget()
504 colid = get_collection_id(int(site_id))
506 # By default, CKEditor stores files in 1 folder
507 # We want to store the files in a @colid folder
508 for field_name in ["content_en", "content_fr"]:
509 field = self.fields[field_name]
510 widget = field.widget
511 widget.config["filebrowserUploadUrl"] = "/ckeditor/upload/" + colid
512 widget.config["filebrowserBrowseUrl"] = "/ckeditor/browse/" + colid
514 pages = Page.objects.filter(site_id=site_id, parent_page=None)
515 if self.instance: 515 ↛ 518line 515 didn't jump to line 518 because the condition on line 515 was always true
516 pages = pages.exclude(id=self.instance.id)
518 choices = [(p.id, p.menu_title_en) for p in pages if p.menu_title_en]
519 self.fields["parent_page"].choices = sorted(
520 choices + [(None, "---------")], key=lambda x: x[1]
521 )
523 self.fields["menu_title_en"].widget.attrs.update({"class": "menu_title"})
524 self.fields["menu_title_fr"].widget.attrs.update({"class": "menu_title"})
526 if is_site_en_only(site_id): 526 ↛ 527line 526 didn't jump to line 527 because the condition on line 526 was never true
527 self.fields.pop("content_fr")
528 self.fields.pop("menu_title_fr")
529 self.fields.pop("slug_fr")
530 elif is_site_fr_only(site_id): 530 ↛ 531line 530 didn't jump to line 531 because the condition on line 530 was never true
531 self.fields.pop("content_en")
532 self.fields.pop("menu_title_en")
533 self.fields.pop("slug_en")
535 def save_model(self, request, obj, form, change):
536 obj.site_id = form.cleaned_data["site_id"]
537 super().save_model(request, obj, form, change)
540class NewsForm(forms.ModelForm):
541 class Meta:
542 model = News
543 fields = [
544 "title_en",
545 "title_fr",
546 "content_en",
547 "content_fr",
548 "site_id",
549 ]
551 def __init__(self, *args, **kwargs):
552 site_id = kwargs.pop("site_id")
553 user = kwargs.pop("user")
554 super().__init__(*args, **kwargs)
556 self.fields["site_id"].initial = site_id
558 if not user.is_staff:
559 for field_name in ["site_id"]:
560 field = self.fields[field_name]
561 # Hide the field is not enough, otherwise BaseForm._clean_fields will not get the value
562 field.disabled = True
563 field.widget = field.hidden_widget()
565 colid = get_collection_id(int(site_id))
567 # By default, CKEditor stores files in 1 folder
568 # We want to store the files in a @colid folder
569 for field_name in ["content_en", "content_fr"]:
570 field = self.fields[field_name]
571 widget = field.widget
572 widget.config["filebrowserUploadUrl"] = "/ckeditor/upload/" + colid
573 widget.config["filebrowserBrowseUrl"] = "/ckeditor/browse/" + colid
575 if is_site_en_only(site_id):
576 self.fields.pop("content_fr")
577 self.fields.pop("title_fr")
578 elif is_site_fr_only(site_id):
579 self.fields.pop("content_en")
580 self.fields.pop("title_en")
582 def save_model(self, request, obj, form, change):
583 obj.site_id = form.cleaned_data["site_id"]
584 super().save_model(request, obj, form, change)
587class InviteUserForm(forms.Form):
588 """Base form to invite user."""
590 required_css_class = "required"
592 first_name = forms.CharField(label="First name", max_length=150, required=True)
593 last_name = forms.CharField(label="Last name", max_length=150, required=True)
594 email = forms.EmailField(label="E-mail address", required=True)
597class InvitationAdminChangeForm(forms.ModelForm):
598 class Meta:
599 model = Invitation
600 fields = "__all__"
602 def clean_extra_data(self):
603 """
604 Enforce the JSON structure with the InvitationExtraData dataclass interface.
605 """
606 try:
607 InvitationExtraData(**self.cleaned_data["extra_data"])
608 except Exception as e:
609 raise forms.ValidationError(e)
611 return self.cleaned_data["extra_data"]
614class InvitationAdminAddForm(InvitationAdminChangeForm, CleanEmailMixin):
615 class Meta:
616 fields = ("email", "first_name", "last_name", "extra_data")
618 def save(self, *args, **kwargs):
619 """
620 Populate the invitation data, save in DB and send the invitation e-mail.
621 """
622 cleaned_data = self.clean()
623 email = cleaned_data["email"]
624 params = {"email": email}
625 if cleaned_data.get("inviter"):
626 params["inviter"] = cleaned_data["inviter"]
627 else:
628 user = getattr(self, "user", None)
629 if isinstance(user, get_user_model()):
630 params["inviter"] = user
631 instance = Invitation.create(**params)
632 instance.first_name = cleaned_data["first_name"]
633 instance.last_name = cleaned_data["last_name"]
634 instance.extra_data = cleaned_data.get("extra_data", {})
635 instance.save()
636 full_name = f"{instance.first_name} {instance.last_name}"
637 instance.send_invitation(self.request, **{"full_name": full_name})
638 super().save(*args, **kwargs)
639 return instance
642class SignupForm(BaseSignupForm):
643 email = forms.EmailField(widget=forms.HiddenInput())