Coverage for src/ptf_tools/forms.py: 39%
354 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-07-03 13:04 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-07-03 13:04 +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)
24from .models import Invitation, InvitationExtraData
26TYPE_CHOICES = (
27 ("doi", "doi"),
28 ("mr-item-id", "mr"),
29 ("zbl-item-id", "zbl"),
30 ("numdam-id", "numdam"),
31 ("pmid", "pubmed"),
32)
34RESOURCE_ID_CHOICES = (
35 ("issn", "p-issn"),
36 ("e-issn", "e-issn"),
37)
39REL_CHOICES = (
40 ("small_icon", "small_icon"),
41 ("icon", "icon"),
42 ("test_website", "test_website"),
43 ("website", "website"),
44)
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)
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
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
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
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"
83class BibItemIdForm(forms.ModelForm):
84 class Meta:
85 model = BibItemId
86 fields = ["bibitem", "id_type", "id_value"]
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()
94class ExtIdForm(forms.ModelForm):
95 class Meta:
96 model = ExtId
97 fields = ["resource", "id_type", "id_value"]
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()
105class ExtLinkForm(forms.ModelForm):
106 class Meta:
107 model = ExtLink
108 fields = ["rel", "location"]
109 widgets = {
110 "rel": forms.Select(choices=REL_CHOICES),
111 }
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 }
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 }
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 )
150 super().__init__(*args, **kwargs)
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
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)
171 def __init__(self, container, *args, **kwargs):
172 super().__init__(*args, **kwargs)
173 self.container = container
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
194 for extlink in container.extlink_set.all():
195 if extlink.rel == "icon":
196 self.fields["icon"].initial = os.path.basename(extlink.location)
198 def clean(self):
199 cleaned_data = super().clean()
200 return cleaned_data
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)
216 def __init__(self, article, *args, **kwargs):
217 super().__init__(*args, **kwargs)
218 self.article = article
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
252 for count in article.resourcecount_set.all():
253 if count.name == "page-count":
254 self.fields["page_count"].initial = count.value
256 for extlink in article.extlink_set.all():
257 if extlink.rel == "icon":
258 self.fields["icon"].initial = os.path.basename(extlink.location)
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
265 def clean(self):
266 cleaned_data = super().clean()
267 return cleaned_data
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
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 )
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)
308 if to_appear:
309 issue_folders = [
310 volume for volume in os.listdir(collection_folder) if f"{colid}_0" in volume
311 ]
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)
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")
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
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)
352 return article_choices
355class ImportArticleForm(forms.Form):
356 issue = forms.ChoiceField(
357 label="Numéro",
358 )
359 article = forms.ChoiceField(
360 label="Article",
361 )
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
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 )
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)
398class DiffContainerForm(forms.Form):
399 import_choice = forms.ChoiceField(
400 choices=IMPORT_CHOICES, label="Que faire des différences ?", widget=forms.RadioSelect()
401 )
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)
410 self.fields["import_choice"].initial = IMPORT_CHOICES[0][0]
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 ? 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/",
420 widget=forms.RadioSelect,
421 choices=CHOICES,
422 required=False,
423 initial="on",
424 )
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 )
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 )
441 exclusion_list = forms.CharField(
442 required=False,
443 widget=forms.Textarea(attrs={"rows": "10"}),
444 )
446 class Meta:
447 model = RelatedArticles
448 fields = ["doi_list", "exclusion_list", "automatic_list"]
451class GraphicalAbstractForm(forms.ModelForm):
452 """Form for the Graphical Abstract model"""
454 class Meta:
455 model = GraphicalAbstract
456 fields = ("graphical_abstract", "illustration")
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 ]
477 def __init__(self, *args, **kwargs):
478 site_id = kwargs.pop("site_id")
479 user = kwargs.pop("user")
480 super().__init__(*args, **kwargs)
482 self.fields["site_id"].initial = site_id
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()
491 colid = get_collection_id(int(site_id))
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
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)
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 )
510 self.fields["menu_title_en"].widget.attrs.update({"class": "menu_title"})
511 self.fields["menu_title_fr"].widget.attrs.update({"class": "menu_title"})
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")
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)
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 ]
538 def __init__(self, *args, **kwargs):
539 site_id = kwargs.pop("site_id")
540 user = kwargs.pop("user")
541 super().__init__(*args, **kwargs)
543 self.fields["site_id"].initial = site_id
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()
552 colid = get_collection_id(int(site_id))
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
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")
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)
574class InviteUserForm(forms.Form):
575 """Base form to invite user."""
577 required_css_class = "required"
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)
584class InvitationAdminChangeForm(forms.ModelForm):
585 class Meta:
586 model = Invitation
587 fields = "__all__"
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)
598 return self.cleaned_data["extra_data"]
601class InvitationAdminAddForm(InvitationAdminChangeForm, CleanEmailMixin):
602 class Meta:
603 fields = ("email", "first_name", "last_name", "extra_data")
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
629class SignupForm(BaseSignupForm):
630 email = forms.EmailField(widget=forms.HiddenInput())