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
« prev ^ index » next coverage.py v7.8.2, created at 2025-07-30 15:02 +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 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 )
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 )
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 )
451 exclusion_list = forms.CharField(
452 required=False,
453 widget=forms.Textarea(attrs={"rows": "10"}),
454 )
456 class Meta:
457 model = RelatedArticles
458 fields = ["doi_list", "exclusion_list", "automatic_list"]
461class GraphicalAbstractForm(forms.ModelForm):
462 """Form for the Graphical Abstract model"""
464 class Meta:
465 model = GraphicalAbstract
466 fields = ("graphical_abstract", "illustration")
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 ]
487 def __init__(self, *args, **kwargs):
488 site_id = kwargs.pop("site_id")
489 user = kwargs.pop("user")
490 super().__init__(*args, **kwargs)
492 self.fields["site_id"].initial = site_id
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()
501 colid = get_collection_id(int(site_id))
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
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)
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 )
520 self.fields["menu_title_en"].widget.attrs.update({"class": "menu_title"})
521 self.fields["menu_title_fr"].widget.attrs.update({"class": "menu_title"})
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")
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)
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 ]
548 def __init__(self, *args, **kwargs):
549 site_id = kwargs.pop("site_id")
550 user = kwargs.pop("user")
551 super().__init__(*args, **kwargs)
553 self.fields["site_id"].initial = site_id
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()
562 colid = get_collection_id(int(site_id))
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
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")
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)
584class InviteUserForm(forms.Form):
585 """Base form to invite user."""
587 required_css_class = "required"
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)
594class InvitationAdminChangeForm(forms.ModelForm):
595 class Meta:
596 model = Invitation
597 fields = "__all__"
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)
608 return self.cleaned_data["extra_data"]
611class InvitationAdminAddForm(InvitationAdminChangeForm, CleanEmailMixin):
612 class Meta:
613 fields = ("email", "first_name", "last_name", "extra_data")
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
639class SignupForm(BaseSignupForm):
640 email = forms.EmailField(widget=forms.HiddenInput())