Coverage for src/ptf_tools/forms.py: 39%
354 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-09 14:54 +0000
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-09 14:54 +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 ?",
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())