From 37a7c5aac3d393562dc2e56e3fb056daba706389 Mon Sep 17 00:00:00 2001 From: Sylvain Boissel Date: Mon, 24 Jun 2024 17:30:20 +0200 Subject: [PATCH 1/9] Restarting to work on that functionality --- config/settings.py | 1 + .../migrations/0035_formpage_formfield.py | 125 ++++++++++++++++++ content_manager/models.py | 103 ++++++++++++++- .../templates/content_manager/form_page.html | 24 ++++ .../content_manager/form_page_landing.html | 10 ++ 5 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 content_manager/migrations/0035_formpage_formfield.py create mode 100644 content_manager/templates/content_manager/form_page.html create mode 100644 content_manager/templates/content_manager/form_page_landing.html diff --git a/config/settings.py b/config/settings.py index 3a6f9515..e4d40a5e 100644 --- a/config/settings.py +++ b/config/settings.py @@ -50,6 +50,7 @@ INSTALLED_APPS = [ "storages", "dashboard", + "wagtail.contrib.forms", "wagtail.contrib.redirects", "wagtail.contrib.settings", "wagtail.embeds", diff --git a/content_manager/migrations/0035_formpage_formfield.py b/content_manager/migrations/0035_formpage_formfield.py new file mode 100644 index 00000000..eae1bb14 --- /dev/null +++ b/content_manager/migrations/0035_formpage_formfield.py @@ -0,0 +1,125 @@ +# Generated by Django 5.0.6 on 2024-06-24 14:34 + +import django.db.models.deletion +import modelcluster.fields +import wagtail.contrib.forms.models +import wagtail.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("content_manager", "0034_alter_contentpage_body"), + ("wagtailcore", "0093_uploadedfile"), + ] + + operations = [ + migrations.CreateModel( + name="FormPage", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ( + "to_address", + models.CharField( + blank=True, + help_text="Optional - form submissions will be emailed to these addresses. Separate multiple addresses by comma.", + max_length=255, + validators=[wagtail.contrib.forms.models.validate_to_address], + verbose_name="to address", + ), + ), + ("from_address", models.EmailField(blank=True, max_length=255, verbose_name="from address")), + ("subject", models.CharField(blank=True, max_length=255, verbose_name="subject")), + ("intro", wagtail.fields.RichTextField(blank=True)), + ("thank_you_text", wagtail.fields.RichTextField(blank=True)), + ], + options={ + "verbose_name": "Page de formulaire", + "verbose_name_plural": "Pages de formulaire", + }, + bases=(wagtail.contrib.forms.models.FormMixin, "wagtailcore.page", models.Model), + ), + migrations.CreateModel( + name="FormField", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("sort_order", models.IntegerField(blank=True, editable=False, null=True)), + ( + "clean_name", + models.CharField( + blank=True, + default="", + help_text="Safe name of the form field, the label converted to ascii_snake_case", + max_length=255, + verbose_name="name", + ), + ), + ( + "label", + models.CharField(help_text="The label of the form field", max_length=255, verbose_name="label"), + ), + ( + "field_type", + models.CharField( + choices=[ + ("singleline", "Single line text"), + ("multiline", "Multi-line text"), + ("email", "Email"), + ("number", "Number"), + ("url", "URL"), + ("checkbox", "Checkbox"), + ("checkboxes", "Checkboxes"), + ("dropdown", "Drop down"), + ("multiselect", "Multiple select"), + ("radio", "Radio buttons"), + ("date", "Date"), + ("datetime", "Date/time"), + ("hidden", "Hidden field"), + ], + max_length=16, + verbose_name="field type", + ), + ), + ("required", models.BooleanField(default=True, verbose_name="required")), + ( + "choices", + models.TextField( + blank=True, + help_text="Comma or new line separated list of choices. Only applicable in checkboxes, radio and dropdown.", + verbose_name="choices", + ), + ), + ( + "default_value", + models.TextField( + blank=True, + help_text="Default value. Comma or new line separated values supported for checkboxes.", + verbose_name="default value", + ), + ), + ("help_text", models.CharField(blank=True, max_length=255, verbose_name="help text")), + ( + "page", + modelcluster.fields.ParentalKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="form_fields", + to="content_manager.formpage", + ), + ), + ], + options={ + "ordering": ["sort_order"], + "abstract": False, + }, + ), + ] diff --git a/content_manager/models.py b/content_manager/models.py index 3232eeae..9e82eae7 100644 --- a/content_manager/models.py +++ b/content_manager/models.py @@ -1,11 +1,14 @@ from django.db import models -from django.forms.widgets import Textarea +from django.forms import widgets +from django.template.response import TemplateResponse from django.utils.translation import gettext_lazy as _ from modelcluster.fields import ParentalKey from modelcluster.models import ClusterableModel from modelcluster.tags import ClusterTaggableManager from taggit.models import Tag as TaggitTag, TaggedItemBase -from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel, ObjectList, TabbedInterface +from wagtail.admin.panels import FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel, ObjectList, TabbedInterface +from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField +from wagtail.contrib.forms.panels import FormSubmissionsPanel from wagtail.contrib.settings.models import BaseSiteSetting, register_setting from wagtail.fields import RichTextField from wagtail.images import get_image_model_string @@ -47,7 +50,7 @@ class MonospaceField(models.TextField): """ def formfield(self, **kwargs): - kwargs["widget"] = Textarea( + kwargs["widget"] = widgets.Textarea( attrs={ "rows": 12, "class": "monospace", @@ -332,3 +335,97 @@ def get_categories(self): class Meta: verbose_name = _("Mega menu") + + +class FormField(AbstractFormField): + FORM_FIELD_CHOICES = ( + ("singleline", _("Single line text")), + ("multiline", _("Multi-line text")), + ("email", _("Email")), + ("number", _("Number")), + ("url", _("URL")), + ("checkbox", _("Checkbox")), + ("cmsfr_checkboxes", _("Checkboxes")), + ("dropdown", _("Drop down")), + ("cmsfr_radio", _("Radio buttons")), + ("cmsfr_date", _("Date")), + ("cmsfr_datetime", _("Date/time")), + ("hidden", _("Hidden field")), + ) + + page = ParentalKey("FormPage", on_delete=models.CASCADE, related_name="form_fields") + + +class FormPage(AbstractEmailForm): + intro = RichTextField(blank=True) + thank_you_text = RichTextField(blank=True) + + content_panels = AbstractEmailForm.content_panels + [ + FormSubmissionsPanel(), + FieldPanel("intro", heading="Introduction"), + InlinePanel("form_fields", label="Champs de formulaire"), + FieldPanel("thank_you_text", heading="Texte de remerciement"), + MultiFieldPanel( + [ + FieldRowPanel( + [ + FieldPanel("from_address", classname="col6"), + FieldPanel("to_address", classname="col6"), + ] + ), + FieldPanel("subject"), + ], + "Courriel", + help_text="Facultatif", + ), + ] + + class Meta: + verbose_name = "Page de formulaire" + verbose_name_plural = "Pages de formulaire" + + def serve(self, request, *args, **kwargs): + # These input widgets don't need the fr-input class + if request.method == "POST": + form = self.get_form(request.POST, request.FILES, page=self, user=request.user) + + if form.is_valid(): + form_submission = self.process_form_submission(form) + return self.render_landing_page(request, form_submission, *args, **kwargs) + else: + form = self.get_form(page=self, user=request.user) + + WIDGETS_NO_FR_INPUT = [ + widgets.CheckboxInput, + widgets.FileInput, + widgets.ClearableFileInput, + ] + + for visible in form.visible_fields(): + """ + Depending on the widget, we have to add some classes: + - on the outer group + - on the form field itsef + If a class is already set, we don't force the DSFR-specific classes. + """ + if "class" not in visible.field.widget.attrs: + if type(visible.field.widget) in [ + widgets.Select, + widgets.SelectMultiple, + ]: + visible.field.widget.attrs["class"] = "fr-select" + visible.field.widget.group_class = "fr-select-group" + elif isinstance(visible.field.widget, widgets.DateInput): + visible.field.widget.attrs["class"] = "fr-input" + visible.field.widget.attrs["type"] = "date" + elif isinstance(visible.field.widget, widgets.RadioSelect): + visible.field.widget.attrs["dsfr"] = "dsfr" + visible.field.widget.group_class = "fr-radio-group" + elif isinstance(visible.field.widget, widgets.CheckboxSelectMultiple): + visible.field.widget.attrs["dsfr"] = "dsfr" + elif type(visible.field.widget) not in WIDGETS_NO_FR_INPUT: + visible.field.widget.attrs["class"] = "fr-input" + + context = self.get_context(request) + context["form"] = form + return TemplateResponse(request, self.get_template(request), context) diff --git a/content_manager/templates/content_manager/form_page.html b/content_manager/templates/content_manager/form_page.html new file mode 100644 index 00000000..ba9fe79f --- /dev/null +++ b/content_manager/templates/content_manager/form_page.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% load wagtailcore_tags dsfr_tags %} + +{% block content %} +
+
+
+

{{ page.title }}

+ {{ page.intro|richtext }} +
+ {% csrf_token %} + {% dsfr_form %} + +
    + {% for field in form %} +
  • {{ field.label }}, {{ field.type }}, {{ field.field.widget.attrs }} {{ field.field.widget }}
  • + {% endfor %} +
+
+
+
+
+{% endblock content %} diff --git a/content_manager/templates/content_manager/form_page_landing.html b/content_manager/templates/content_manager/form_page_landing.html new file mode 100644 index 00000000..b4e885e7 --- /dev/null +++ b/content_manager/templates/content_manager/form_page_landing.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% load wagtailcore_tags %} + +{% block content %} +
+

{{ page.title }}

+ {{ page.thank_you_text|richtext }} +
+{% endblock content %} From bf8af49bdd0d4a369bdd774e441addacce7d7037 Mon Sep 17 00:00:00 2001 From: Sylvain Boissel Date: Tue, 25 Jun 2024 10:18:03 +0200 Subject: [PATCH 2/9] Moved to a new app --- config/settings.py | 1 + content_manager/models.py | 99 +---------------- forms/__init__.py | 0 forms/admin.py | 1 + forms/apps.py | 6 + forms/locale/fr/LC_MESSAGES/django.mo | Bin 0 -> 885 bytes forms/locale/fr/LC_MESSAGES/django.po | 68 ++++++++++++ .../migrations/0001_initial.py | 9 +- forms/migrations/__init__.py | 0 forms/models.py | 103 ++++++++++++++++++ .../templates/forms}/form_page.html | 11 +- .../templates/forms}/form_page_landing.html | 0 forms/tests.py | 1 + forms/views.py | 1 + locale/fr/LC_MESSAGES/django.mo | Bin 988 -> 990 bytes locale/fr/LC_MESSAGES/django.po | 10 +- 16 files changed, 197 insertions(+), 113 deletions(-) create mode 100644 forms/__init__.py create mode 100644 forms/admin.py create mode 100644 forms/apps.py create mode 100644 forms/locale/fr/LC_MESSAGES/django.mo create mode 100644 forms/locale/fr/LC_MESSAGES/django.po rename content_manager/migrations/0035_formpage_formfield.py => forms/migrations/0001_initial.py (95%) create mode 100644 forms/migrations/__init__.py create mode 100644 forms/models.py rename {content_manager/templates/content_manager => forms/templates/forms}/form_page.html (82%) rename {content_manager/templates/content_manager => forms/templates/forms}/form_page_landing.html (100%) create mode 100644 forms/tests.py create mode 100644 forms/views.py diff --git a/config/settings.py b/config/settings.py index e4d40a5e..436db6bb 100644 --- a/config/settings.py +++ b/config/settings.py @@ -50,6 +50,7 @@ INSTALLED_APPS = [ "storages", "dashboard", + "forms", "wagtail.contrib.forms", "wagtail.contrib.redirects", "wagtail.contrib.settings", diff --git a/content_manager/models.py b/content_manager/models.py index 9e82eae7..e4f6585c 100644 --- a/content_manager/models.py +++ b/content_manager/models.py @@ -1,14 +1,11 @@ from django.db import models from django.forms import widgets -from django.template.response import TemplateResponse from django.utils.translation import gettext_lazy as _ from modelcluster.fields import ParentalKey from modelcluster.models import ClusterableModel from modelcluster.tags import ClusterTaggableManager from taggit.models import Tag as TaggitTag, TaggedItemBase -from wagtail.admin.panels import FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel, ObjectList, TabbedInterface -from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField -from wagtail.contrib.forms.panels import FormSubmissionsPanel +from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel, ObjectList, TabbedInterface from wagtail.contrib.settings.models import BaseSiteSetting, register_setting from wagtail.fields import RichTextField from wagtail.images import get_image_model_string @@ -335,97 +332,3 @@ def get_categories(self): class Meta: verbose_name = _("Mega menu") - - -class FormField(AbstractFormField): - FORM_FIELD_CHOICES = ( - ("singleline", _("Single line text")), - ("multiline", _("Multi-line text")), - ("email", _("Email")), - ("number", _("Number")), - ("url", _("URL")), - ("checkbox", _("Checkbox")), - ("cmsfr_checkboxes", _("Checkboxes")), - ("dropdown", _("Drop down")), - ("cmsfr_radio", _("Radio buttons")), - ("cmsfr_date", _("Date")), - ("cmsfr_datetime", _("Date/time")), - ("hidden", _("Hidden field")), - ) - - page = ParentalKey("FormPage", on_delete=models.CASCADE, related_name="form_fields") - - -class FormPage(AbstractEmailForm): - intro = RichTextField(blank=True) - thank_you_text = RichTextField(blank=True) - - content_panels = AbstractEmailForm.content_panels + [ - FormSubmissionsPanel(), - FieldPanel("intro", heading="Introduction"), - InlinePanel("form_fields", label="Champs de formulaire"), - FieldPanel("thank_you_text", heading="Texte de remerciement"), - MultiFieldPanel( - [ - FieldRowPanel( - [ - FieldPanel("from_address", classname="col6"), - FieldPanel("to_address", classname="col6"), - ] - ), - FieldPanel("subject"), - ], - "Courriel", - help_text="Facultatif", - ), - ] - - class Meta: - verbose_name = "Page de formulaire" - verbose_name_plural = "Pages de formulaire" - - def serve(self, request, *args, **kwargs): - # These input widgets don't need the fr-input class - if request.method == "POST": - form = self.get_form(request.POST, request.FILES, page=self, user=request.user) - - if form.is_valid(): - form_submission = self.process_form_submission(form) - return self.render_landing_page(request, form_submission, *args, **kwargs) - else: - form = self.get_form(page=self, user=request.user) - - WIDGETS_NO_FR_INPUT = [ - widgets.CheckboxInput, - widgets.FileInput, - widgets.ClearableFileInput, - ] - - for visible in form.visible_fields(): - """ - Depending on the widget, we have to add some classes: - - on the outer group - - on the form field itsef - If a class is already set, we don't force the DSFR-specific classes. - """ - if "class" not in visible.field.widget.attrs: - if type(visible.field.widget) in [ - widgets.Select, - widgets.SelectMultiple, - ]: - visible.field.widget.attrs["class"] = "fr-select" - visible.field.widget.group_class = "fr-select-group" - elif isinstance(visible.field.widget, widgets.DateInput): - visible.field.widget.attrs["class"] = "fr-input" - visible.field.widget.attrs["type"] = "date" - elif isinstance(visible.field.widget, widgets.RadioSelect): - visible.field.widget.attrs["dsfr"] = "dsfr" - visible.field.widget.group_class = "fr-radio-group" - elif isinstance(visible.field.widget, widgets.CheckboxSelectMultiple): - visible.field.widget.attrs["dsfr"] = "dsfr" - elif type(visible.field.widget) not in WIDGETS_NO_FR_INPUT: - visible.field.widget.attrs["class"] = "fr-input" - - context = self.get_context(request) - context["form"] = form - return TemplateResponse(request, self.get_template(request), context) diff --git a/forms/__init__.py b/forms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/forms/admin.py b/forms/admin.py new file mode 100644 index 00000000..846f6b40 --- /dev/null +++ b/forms/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/forms/apps.py b/forms/apps.py new file mode 100644 index 00000000..ec22dd82 --- /dev/null +++ b/forms/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FormsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "forms" diff --git a/forms/locale/fr/LC_MESSAGES/django.mo b/forms/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..07ac9ad50b7739a3336b888d168f916041f148c6 GIT binary patch literal 885 zcmYk4&2AGh5XTLaujR9FLmbQvgxqdIL1n8V(U8)jq>Vy~xPavBP7}k%PHiugBN7K* z0M6V{#bfl$8z7#b7oGtAlc2Vv9sfM5`P|vx*Ji#5$R+4a=qKnb^Z?3|o)F?3SOXI< z182Y{cpiNApFQw2?APE$umG=sJK!1cBgi#AgIB??;3@DscnkUidKvc5lhe3gAlEqn zIqnb0ef)kZ9#prAh&y5|FB#*qdx-HT1?hWNptrzMA`Cy#QWYVRv^HH_K zz!Ykw8U)pIc-`4lU3zEONpW|Xtf%z_l|E4Q`?8j1>BGBeJx!CA4pH^IwxQI~c|^9f z9aMTisz-Ws%+BPtPulD4=27w5T$(hTjbxGI!x%+)qWN*Dja`!YQ2Rh}d8@ao7LWY6 z);4+7w7DxVW+oT=CMKP-^14*3&W}PS?Ra8_<@%DGUYBoM`AF98EF~}1Q?lfz?RFd$ zCdzwr3v=~EG;|=j|4rsDAL5&`8IIY9YmyY@kSb52WkMub?C*J3l^Uan$AzbWG^8fG zYYg>hEOVU?_xD5(, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-06-25 10:02+0200\n" +"PO-Revision-Date: 2024-06-25 10:09+0200\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 3.4.2\n" + +#: forms/models.py:14 +msgid "Single line text" +msgstr "Texte sur une ligne" + +#: forms/models.py:15 +msgid "Multi-line text" +msgstr "Texte sur plusieurs lignes" + +#: forms/models.py:16 +msgid "Email" +msgstr "Adresse e-mail" + +#: forms/models.py:17 +msgid "Number" +msgstr "Nombre" + +#: forms/models.py:18 +msgid "URL" +msgstr "URL" + +#: forms/models.py:19 +msgid "Checkbox" +msgstr "Case à cocher" + +#: forms/models.py:20 +msgid "Checkboxes" +msgstr "Cases à cocher" + +#: forms/models.py:21 +msgid "Drop down" +msgstr "Liste déroulante" + +#: forms/models.py:22 +msgid "Radio buttons" +msgstr "Boutons radio" + +#: forms/models.py:23 +msgid "Date" +msgstr "Date" + +#: forms/models.py:24 +msgid "Date/time" +msgstr "Date et heure" + +#: forms/models.py:25 +msgid "Hidden field" +msgstr "Champ caché" diff --git a/content_manager/migrations/0035_formpage_formfield.py b/forms/migrations/0001_initial.py similarity index 95% rename from content_manager/migrations/0035_formpage_formfield.py rename to forms/migrations/0001_initial.py index eae1bb14..f60f624e 100644 --- a/content_manager/migrations/0035_formpage_formfield.py +++ b/forms/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.6 on 2024-06-24 14:34 +# Generated by Django 5.0.6 on 2024-06-25 07:10 import django.db.models.deletion import modelcluster.fields @@ -8,8 +8,9 @@ class Migration(migrations.Migration): + initial = True + dependencies = [ - ("content_manager", "0034_alter_contentpage_body"), ("wagtailcore", "0093_uploadedfile"), ] @@ -111,9 +112,7 @@ class Migration(migrations.Migration): ( "page", modelcluster.fields.ParentalKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="form_fields", - to="content_manager.formpage", + on_delete=django.db.models.deletion.CASCADE, related_name="form_fields", to="forms.formpage" ), ), ], diff --git a/forms/migrations/__init__.py b/forms/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/forms/models.py b/forms/models.py new file mode 100644 index 00000000..b3ddc790 --- /dev/null +++ b/forms/models.py @@ -0,0 +1,103 @@ +from django.db import models +from django.forms import widgets +from django.template.response import TemplateResponse +from django.utils.translation import gettext_lazy as _ +from modelcluster.fields import ParentalKey +from wagtail.admin.panels import FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel +from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField +from wagtail.contrib.forms.panels import FormSubmissionsPanel +from wagtail.fields import RichTextField + + +class FormField(AbstractFormField): + FORM_FIELD_CHOICES = ( + ("singleline", _("Single line text")), + ("multiline", _("Multi-line text")), + ("email", _("Email")), + ("number", _("Number")), + ("url", _("URL")), + ("checkbox", _("Checkbox")), + ("cmsfr_checkboxes", _("Checkboxes")), + ("dropdown", _("Drop down")), + ("cmsfr_radio", _("Radio buttons")), + ("cmsfr_date", _("Date")), + ("cmsfr_datetime", _("Date/time")), + ("hidden", _("Hidden field")), + ) + + page = ParentalKey("FormPage", on_delete=models.CASCADE, related_name="form_fields") + + +class FormPage(AbstractEmailForm): + intro = RichTextField(blank=True) + thank_you_text = RichTextField(blank=True) + + content_panels = AbstractEmailForm.content_panels + [ + FormSubmissionsPanel(), + FieldPanel("intro", heading="Introduction"), + InlinePanel("form_fields", label="Champs de formulaire"), + FieldPanel("thank_you_text", heading="Texte de remerciement"), + MultiFieldPanel( + [ + FieldRowPanel( + [ + FieldPanel("from_address", classname="col6"), + FieldPanel("to_address", classname="col6"), + ] + ), + FieldPanel("subject"), + ], + "Courriel", + help_text="Facultatif", + ), + ] + + class Meta: + verbose_name = "Page de formulaire" + verbose_name_plural = "Pages de formulaire" + + def serve(self, request, *args, **kwargs): + # These input widgets don't need the fr-input class + if request.method == "POST": + form = self.get_form(request.POST, request.FILES, page=self, user=request.user) + + if form.is_valid(): + form_submission = self.process_form_submission(form) + return self.render_landing_page(request, form_submission, *args, **kwargs) + else: + form = self.get_form(page=self, user=request.user) + + WIDGETS_NO_FR_INPUT = [ + widgets.CheckboxInput, + widgets.FileInput, + widgets.ClearableFileInput, + ] + + for visible in form.visible_fields(): + """ + Depending on the widget, we have to add some classes: + - on the outer group + - on the form field itsef + If a class is already set, we don't force the DSFR-specific classes. + """ + if "class" not in visible.field.widget.attrs: + if type(visible.field.widget) in [ + widgets.Select, + widgets.SelectMultiple, + ]: + visible.field.widget.attrs["class"] = "fr-select" + visible.field.widget.group_class = "fr-select-group" + elif isinstance(visible.field.widget, widgets.DateInput): + visible.field.widget.attrs["class"] = "fr-input" + visible.field.widget.attrs["type"] = "date" + elif isinstance(visible.field.widget, widgets.RadioSelect): + visible.field.widget.attrs["dsfr"] = "dsfr" + visible.field.widget.group_class = "fr-radio-group" + elif isinstance(visible.field.widget, widgets.CheckboxSelectMultiple): + visible.field.widget.attrs["dsfr"] = "dsfr" + elif type(visible.field.widget) not in WIDGETS_NO_FR_INPUT: + visible.field.widget.attrs["class"] = "fr-input" + + context = self.get_context(request) + context["form"] = form + return TemplateResponse(request, self.get_template(request), context) diff --git a/content_manager/templates/content_manager/form_page.html b/forms/templates/forms/form_page.html similarity index 82% rename from content_manager/templates/content_manager/form_page.html rename to forms/templates/forms/form_page.html index ba9fe79f..beba909e 100644 --- a/content_manager/templates/content_manager/form_page.html +++ b/forms/templates/forms/form_page.html @@ -12,11 +12,14 @@

{{ page.title }}

{% csrf_token %} {% dsfr_form %} -
    - {% for field in form %} + + diff --git a/content_manager/templates/content_manager/form_page_landing.html b/forms/templates/forms/form_page_landing.html similarity index 100% rename from content_manager/templates/content_manager/form_page_landing.html rename to forms/templates/forms/form_page_landing.html diff --git a/forms/tests.py b/forms/tests.py new file mode 100644 index 00000000..a39b155a --- /dev/null +++ b/forms/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/forms/views.py b/forms/views.py new file mode 100644 index 00000000..60f00ef0 --- /dev/null +++ b/forms/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/locale/fr/LC_MESSAGES/django.mo b/locale/fr/LC_MESSAGES/django.mo index 80ce423b889f22e9f5a363fed49e59e81235c903..e1b454bb2f783751001dfbacb8cae9650c6b29d9 100644 GIT binary patch delta 123 zcmcb^evf^^99>NY28Kvx1_pK@-3+ApfOI#I<^s~QfV2*fUJ9hSf%K(~Cj%Hcj7${_ z4Xg|eHY+kYGs@W{6)EW36_*w%lg28Kvx1_pK@T@R%BfOI>M<^s~wfV3WvUI?VQf%JuqCj%Hc3``UZ zO|1;gHY+kYGfHt4mli4Hq!ud_9bQ?Snpj$)P@JEf38E)AF)K4FOkT>Y!>7Oy{HS4O MVp4uyUTP5o0NhI*@&Et; diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 393e2776..21a8d225 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-04 15:08+0200\n" -"PO-Revision-Date: 2024-06-04 15:16+0200\n" +"POT-Creation-Date: 2024-06-25 10:02+0200\n" +"PO-Revision-Date: 2024-06-25 10:10+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: fr\n" @@ -22,7 +22,7 @@ msgstr "" #: templates/blocks/follow.html:9 msgctxt "Section title" msgid "Subscribe to our newsletter" -msgstr " Abonnez-vous à notre lettre d’information " +msgstr "Abonnez-vous à notre lettre d’information" #: templates/blocks/follow.html:13 msgctxt "Button title" @@ -35,9 +35,7 @@ msgstr "S’abonner" #: templates/blocks/follow.html:30 msgid "Follow us
    on social media" -msgstr "" -"Suivez-nous\n" -"sur les réseaux sociaux " +msgstr "Suivez-nous
    sur les réseaux sociaux" #: templates/blocks/footer.html:5 msgid "Back to home page" From b38fb7b4a69fca25e712066cc0e1696ac28958f1 Mon Sep 17 00:00:00 2001 From: Sylvain Boissel Date: Tue, 25 Jun 2024 10:19:41 +0200 Subject: [PATCH 3/9] Revert changes on content_manager --- content_manager/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content_manager/models.py b/content_manager/models.py index e4f6585c..3232eeae 100644 --- a/content_manager/models.py +++ b/content_manager/models.py @@ -1,5 +1,5 @@ from django.db import models -from django.forms import widgets +from django.forms.widgets import Textarea from django.utils.translation import gettext_lazy as _ from modelcluster.fields import ParentalKey from modelcluster.models import ClusterableModel @@ -47,7 +47,7 @@ class MonospaceField(models.TextField): """ def formfield(self, **kwargs): - kwargs["widget"] = widgets.Textarea( + kwargs["widget"] = Textarea( attrs={ "rows": 12, "class": "monospace", From 70d8ed597c69aa9d64c40f5dada8fdd4d3414701 Mon Sep 17 00:00:00 2001 From: Sylvain Boissel Date: Wed, 26 Jun 2024 14:36:48 +0200 Subject: [PATCH 4/9] Update form types --- config/settings.py | 2 +- forms/models.py | 36 +++------------------------- forms/templates/forms/form_page.html | 12 +++++----- 3 files changed, 10 insertions(+), 40 deletions(-) diff --git a/config/settings.py b/config/settings.py index 436db6bb..2bef4823 100644 --- a/config/settings.py +++ b/config/settings.py @@ -50,7 +50,6 @@ INSTALLED_APPS = [ "storages", "dashboard", - "forms", "wagtail.contrib.forms", "wagtail.contrib.redirects", "wagtail.contrib.settings", @@ -78,6 +77,7 @@ "sass_processor", "content_manager", "blog", + "forms", ] # Only add these on a dev machine, outside of tests diff --git a/forms/models.py b/forms/models.py index b3ddc790..4bffb849 100644 --- a/forms/models.py +++ b/forms/models.py @@ -1,7 +1,7 @@ from django.db import models -from django.forms import widgets from django.template.response import TemplateResponse from django.utils.translation import gettext_lazy as _ +from dsfr.utils import dsfr_input_class_attr from modelcluster.fields import ParentalKey from wagtail.admin.panels import FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField @@ -20,8 +20,7 @@ class FormField(AbstractFormField): ("cmsfr_checkboxes", _("Checkboxes")), ("dropdown", _("Drop down")), ("cmsfr_radio", _("Radio buttons")), - ("cmsfr_date", _("Date")), - ("cmsfr_datetime", _("Date/time")), + ("date", _("Date")), ("hidden", _("Hidden field")), ) @@ -57,7 +56,6 @@ class Meta: verbose_name_plural = "Pages de formulaire" def serve(self, request, *args, **kwargs): - # These input widgets don't need the fr-input class if request.method == "POST": form = self.get_form(request.POST, request.FILES, page=self, user=request.user) @@ -67,36 +65,8 @@ def serve(self, request, *args, **kwargs): else: form = self.get_form(page=self, user=request.user) - WIDGETS_NO_FR_INPUT = [ - widgets.CheckboxInput, - widgets.FileInput, - widgets.ClearableFileInput, - ] - for visible in form.visible_fields(): - """ - Depending on the widget, we have to add some classes: - - on the outer group - - on the form field itsef - If a class is already set, we don't force the DSFR-specific classes. - """ - if "class" not in visible.field.widget.attrs: - if type(visible.field.widget) in [ - widgets.Select, - widgets.SelectMultiple, - ]: - visible.field.widget.attrs["class"] = "fr-select" - visible.field.widget.group_class = "fr-select-group" - elif isinstance(visible.field.widget, widgets.DateInput): - visible.field.widget.attrs["class"] = "fr-input" - visible.field.widget.attrs["type"] = "date" - elif isinstance(visible.field.widget, widgets.RadioSelect): - visible.field.widget.attrs["dsfr"] = "dsfr" - visible.field.widget.group_class = "fr-radio-group" - elif isinstance(visible.field.widget, widgets.CheckboxSelectMultiple): - visible.field.widget.attrs["dsfr"] = "dsfr" - elif type(visible.field.widget) not in WIDGETS_NO_FR_INPUT: - visible.field.widget.attrs["class"] = "fr-input" + dsfr_input_class_attr(visible) context = self.get_context(request) context["form"] = form diff --git a/forms/templates/forms/form_page.html b/forms/templates/forms/form_page.html index beba909e..09abb40a 100644 --- a/forms/templates/forms/form_page.html +++ b/forms/templates/forms/form_page.html @@ -11,15 +11,15 @@

    {{ page.title }}

    {% csrf_token %} {% dsfr_form %} + - + {% endfor %} +
+ From e7d91ae338c891ccde8fc08ecf23c654bfd51661 Mon Sep 17 00:00:00 2001 From: Sylvain Boissel Date: Wed, 26 Jun 2024 17:59:32 +0200 Subject: [PATCH 5/9] Fix the non-working fields --- .../locale/fr/LC_MESSAGES/django.mo | Bin 15981 -> 15981 bytes .../locale/fr/LC_MESSAGES/django.po | 5 +- forms/locale/fr/LC_MESSAGES/django.mo | Bin 885 -> 1540 bytes forms/locale/fr/LC_MESSAGES/django.po | 83 +++++++++++++----- forms/migrations/0001_initial.py | 52 ++++++----- forms/models.py | 68 ++++++++++---- forms/templates/forms/form_page.html | 13 +-- forms/templates/forms/form_page_landing.html | 8 +- locale/fr/LC_MESSAGES/django.mo | Bin 990 -> 1016 bytes locale/fr/LC_MESSAGES/django.po | 14 +-- 10 files changed, 162 insertions(+), 81 deletions(-) diff --git a/content_manager/locale/fr/LC_MESSAGES/django.mo b/content_manager/locale/fr/LC_MESSAGES/django.mo index 0043cc12a61a71e56914c692da7fba1f0023d46e..4321fd1df894600d41fefba75cb77aeb1975383c 100644 GIT binary patch delta 20 bcmaD`^R{M#oEp2Cf}y#UsrhDAH4g~@RbB>= delta 20 bcmaD`^R{M#oEp1{f}xR>iRorlH4g~@RR#uz diff --git a/content_manager/locale/fr/LC_MESSAGES/django.po b/content_manager/locale/fr/LC_MESSAGES/django.po index 0c79cbcf..ec98e045 100644 --- a/content_manager/locale/fr/LC_MESSAGES/django.po +++ b/content_manager/locale/fr/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-24 12:44+0200\n" -"PO-Revision-Date: 2024-06-24 12:45+0200\n" +"POT-Creation-Date: 2024-06-26 16:26+0200\n" +"PO-Revision-Date: 2024-06-26 17:57+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: fr\n" @@ -946,6 +946,7 @@ msgstr "Méga menu" #: content_manager/templates/content_manager/blocks/card_horizontal.html:11 #: content_manager/templates/content_manager/blocks/card_vertical.html:11 #: content_manager/templates/content_manager/blocks/link.html:9 +#: content_manager/templates/content_manager/blocks/tile.html:12 msgid "Opens a new window" msgstr "Ouvre une nouvelle fenêtre" diff --git a/forms/locale/fr/LC_MESSAGES/django.mo b/forms/locale/fr/LC_MESSAGES/django.mo index 07ac9ad50b7739a3336b888d168f916041f148c6..60e11923aed87106dd6203acf6d8b53a8fd7d391 100644 GIT binary patch literal 1540 zcmZvbzmFS56vqcR2$DZQ*`C~b#+jKt z=R^e!e?j_4G;vEuL4zqGK}&~FXlW2aG*o=wu9Y(pBkz9Z&AffT{p-n@RE-#f*^^%xmiEoK#dBLSp z+apd7`tJ6_4F`2eM^=}-n^5MaR_!EXZE~_Mnt@4mG|tj=#Fn)D!jwIkK!I&kJEEyB zJ3pp@;XGT~hH5|2Lq6UG^|8q^n5wjQZS*cHQgNZK6yrX7b#)?=&flS_$#ZfxKccDg z2V@5HX{XnviS~NTk$s?%K4j`M>;;Z>wWl-Vs9(hBY*0PE9Z7rE)vof`8)nPivHNd$XT5pe!Z%?-reLhCc9a(k5CYDi)wp3 zaXu!UFf>_mu^5J==WyJ;n(XnR5v4?G-J)irc`j*eB+U(4+i0z?zujmy8nw0#G3j~K zIM>m6eA(7^SfGMQkM(#-ZqdNkI=h{nW9Mu2Ms3^Kh_)oXqY0cyhG!?aHg=QJk@l$3 z*0tW{dNxeq1D9I zG~Oz-&8VoLZe%f`aA@m!sSM-IEF?*AM; zs#e)cbvl~Q)PKj;jr%ZOjjd(OEn*xvTMo69hHe#1!BCF$%h{dZi-@%vW;BfEvpAoj zOm<*~1+oiubBb5QYZ<@dH25&KR?HHUuavv{NA=1~EY_)g0Uko7@N9?gl$zLSawzFBsy_ zp!OfAzktTV#7^G<6P?_9&+gsKyfgju=WOqNE%88zC1?}sLm6lcapYq}b6^2n0QbQJ zSOVw4<6qXnB<`1B8g#)G@CKX(??Cpr2Uo#IFizB?Cmgn+XJ{EeUS_6+uOJ(ZK`#6N zd9ZJg^D?MrBLQxJS&)s&Am^Wg>)-{5hi>qK1)+SB$KQx@(J?!qq!DdtBS{sdadk(-D72mv z7)b|iB(+9Lb~43L+wnW1=``D;A=P0mg|?CCcvdTP&~R%7Qd2eb8|Zf!S{_A2JlMZ{ Mi@_(ENy1R{10_gnc>n+a diff --git a/forms/locale/fr/LC_MESSAGES/django.po b/forms/locale/fr/LC_MESSAGES/django.po index 78a90b32..6c504d35 100644 --- a/forms/locale/fr/LC_MESSAGES/django.po +++ b/forms/locale/fr/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-25 10:02+0200\n" -"PO-Revision-Date: 2024-06-25 10:09+0200\n" +"POT-Creation-Date: 2024-06-26 16:33+0200\n" +"PO-Revision-Date: 2024-06-26 16:33+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: fr\n" @@ -19,50 +19,91 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Generator: Poedit 3.4.2\n" -#: forms/models.py:14 -msgid "Single line text" -msgstr "Texte sur une ligne" +#: forms/models.py:17 +msgid "Text field" +msgstr "Champ de texte" -#: forms/models.py:15 -msgid "Multi-line text" -msgstr "Texte sur plusieurs lignes" +#: forms/models.py:18 +msgid "Text area" +msgstr "Zone de texte" -#: forms/models.py:16 +#: forms/models.py:19 msgid "Email" msgstr "Adresse e-mail" -#: forms/models.py:17 +#: forms/models.py:20 msgid "Number" msgstr "Nombre" -#: forms/models.py:18 +#: forms/models.py:21 msgid "URL" msgstr "URL" -#: forms/models.py:19 +#: forms/models.py:22 msgid "Checkbox" msgstr "Case à cocher" -#: forms/models.py:20 +#: forms/models.py:23 msgid "Checkboxes" msgstr "Cases à cocher" -#: forms/models.py:21 +#: forms/models.py:24 msgid "Drop down" msgstr "Liste déroulante" -#: forms/models.py:22 +#: forms/models.py:25 msgid "Radio buttons" msgstr "Boutons radio" -#: forms/models.py:23 +#: forms/models.py:26 msgid "Date" msgstr "Date" -#: forms/models.py:24 -msgid "Date/time" -msgstr "Date et heure" - -#: forms/models.py:25 +#: forms/models.py:28 msgid "Hidden field" msgstr "Champ caché" + +#: forms/models.py:32 forms/models.py:74 +msgid "Form field" +msgstr "Champ de formulaire" + +#: forms/models.py:33 forms/models.py:74 +msgid "Form fields" +msgstr "Champs de formulaire" + +#: forms/models.py:73 +msgid "Introduction" +msgstr "Introduction" + +#: forms/models.py:75 +msgid "Thank you text" +msgstr "Texte de remerciement" + +#: forms/models.py:86 +msgid "E-mail notification when an answer is sent" +msgstr "Notification par e-mail quand une réponse est envoyée" + +#: forms/models.py:87 +msgid "Optional, will only work if SMTP parameters have been set." +msgstr "Optionnel, ne fonctionnera que si les paramètres SMTP ont été configurés." + +#: forms/models.py:92 +msgid "Form page" +msgstr "Page de formulaire" + +#: forms/models.py:93 +msgid "Form pages" +msgstr "Pages de formulaire" + +#: forms/templates/forms/form_page_landing.html:11 +msgid "Your form has been successfully submitted. Thank you!" +msgstr "Votre formulaire a été envoyé avec succès. Merci !" + +#~ msgid "Single line text" +#~ msgstr "Texte sur une ligne" + +#~ msgid "Multi-line text" +#~ msgstr "Texte sur plusieurs lignes" + +#~ msgid "Date/time" +#~ msgstr "Date et heure" diff --git a/forms/migrations/0001_initial.py b/forms/migrations/0001_initial.py index f60f624e..d7bb0440 100644 --- a/forms/migrations/0001_initial.py +++ b/forms/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.6 on 2024-06-25 07:10 +# Generated by Django 5.0.6 on 2024-06-26 14:51 import django.db.models.deletion import modelcluster.fields @@ -45,8 +45,8 @@ class Migration(migrations.Migration): ("thank_you_text", wagtail.fields.RichTextField(blank=True)), ], options={ - "verbose_name": "Page de formulaire", - "verbose_name_plural": "Pages de formulaire", + "verbose_name": "Form page", + "verbose_name_plural": "Form pages", }, bases=(wagtail.contrib.forms.models.FormMixin, "wagtailcore.page", models.Model), ), @@ -69,28 +69,6 @@ class Migration(migrations.Migration): "label", models.CharField(help_text="The label of the form field", max_length=255, verbose_name="label"), ), - ( - "field_type", - models.CharField( - choices=[ - ("singleline", "Single line text"), - ("multiline", "Multi-line text"), - ("email", "Email"), - ("number", "Number"), - ("url", "URL"), - ("checkbox", "Checkbox"), - ("checkboxes", "Checkboxes"), - ("dropdown", "Drop down"), - ("multiselect", "Multiple select"), - ("radio", "Radio buttons"), - ("date", "Date"), - ("datetime", "Date/time"), - ("hidden", "Hidden field"), - ], - max_length=16, - verbose_name="field type", - ), - ), ("required", models.BooleanField(default=True, verbose_name="required")), ( "choices", @@ -109,6 +87,26 @@ class Migration(migrations.Migration): ), ), ("help_text", models.CharField(blank=True, max_length=255, verbose_name="help text")), + ( + "field_type", + models.CharField( + choices=[ + ("singleline", "Text field"), + ("multiline", "Text area"), + ("email", "Email"), + ("number", "Number"), + ("url", "URL"), + ("checkbox", "Checkbox"), + ("checkboxes", "Checkboxes"), + ("dropdown", "Drop down"), + ("radio", "Radio buttons"), + ("date", "Date"), + ("hidden", "Hidden field"), + ], + max_length=16, + verbose_name="Field type", + ), + ), ( "page", modelcluster.fields.ParentalKey( @@ -117,8 +115,8 @@ class Migration(migrations.Migration): ), ], options={ - "ordering": ["sort_order"], - "abstract": False, + "verbose_name": "Form field", + "verbose_name_plural": "Form fields", }, ), ] diff --git a/forms/models.py b/forms/models.py index 4bffb849..e694e1ca 100644 --- a/forms/models.py +++ b/forms/models.py @@ -1,31 +1,70 @@ +from django import forms from django.db import models from django.template.response import TemplateResponse from django.utils.translation import gettext_lazy as _ +from dsfr.forms import DsfrDjangoTemplates from dsfr.utils import dsfr_input_class_attr from modelcluster.fields import ParentalKey from wagtail.admin.panels import FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel +from wagtail.contrib.forms.forms import BaseForm, FormBuilder from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField from wagtail.contrib.forms.panels import FormSubmissionsPanel from wagtail.fields import RichTextField class FormField(AbstractFormField): - FORM_FIELD_CHOICES = ( - ("singleline", _("Single line text")), - ("multiline", _("Multi-line text")), + CHOICES = ( + ("singleline", _("Text field")), + ("multiline", _("Text area")), ("email", _("Email")), ("number", _("Number")), ("url", _("URL")), ("checkbox", _("Checkbox")), - ("cmsfr_checkboxes", _("Checkboxes")), + ("checkboxes", _("Checkboxes")), ("dropdown", _("Drop down")), - ("cmsfr_radio", _("Radio buttons")), + ("radio", _("Radio buttons")), ("date", _("Date")), + # ("datetime", _("Date/time")), ("hidden", _("Hidden field")), ) page = ParentalKey("FormPage", on_delete=models.CASCADE, related_name="form_fields") + field_type = models.CharField(verbose_name=_("Field type"), max_length=16, choices=CHOICES) + + class Meta: + verbose_name = _("Form field") + verbose_name_plural = _("Form fields") + + +class SitesFacilesBaseForm(BaseForm): + """ + A base form that adds the necessary class on relevant fields + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for visible in self.visible_fields(): + dsfr_input_class_attr(visible) + + @property + def default_renderer(self): + return DsfrDjangoTemplates # Settings wasn't modified + + +class SitesFacilesFormBuilder(FormBuilder): + def create_date_field(self, field, options): + options["widget"] = forms.DateInput(attrs={"type": "date"}) + return forms.DateField(**options) + + # Datetime is currently not managed + def create_datetime_field(self, field, options): + options["widget"] = forms.DateInput(attrs={"type": "datetime-local"}) + return forms.DateField(**options) + + def get_form_class(self): + return type("WagtailForm", (SitesFacilesBaseForm,), self.formfields) + class FormPage(AbstractEmailForm): intro = RichTextField(blank=True) @@ -33,9 +72,9 @@ class FormPage(AbstractEmailForm): content_panels = AbstractEmailForm.content_panels + [ FormSubmissionsPanel(), - FieldPanel("intro", heading="Introduction"), - InlinePanel("form_fields", label="Champs de formulaire"), - FieldPanel("thank_you_text", heading="Texte de remerciement"), + FieldPanel("intro", heading=_("Introduction")), + InlinePanel("form_fields", label=_("Form field"), heading=_("Form fields")), + FieldPanel("thank_you_text", heading=_("Thank you text")), MultiFieldPanel( [ FieldRowPanel( @@ -46,14 +85,14 @@ class FormPage(AbstractEmailForm): ), FieldPanel("subject"), ], - "Courriel", - help_text="Facultatif", + _("E-mail notification when an answer is sent"), + help_text=_("Optional, will only work if SMTP parameters have been set."), ), ] class Meta: - verbose_name = "Page de formulaire" - verbose_name_plural = "Pages de formulaire" + verbose_name = _("Form page") + verbose_name_plural = _("Form pages") def serve(self, request, *args, **kwargs): if request.method == "POST": @@ -65,9 +104,8 @@ def serve(self, request, *args, **kwargs): else: form = self.get_form(page=self, user=request.user) - for visible in form.visible_fields(): - dsfr_input_class_attr(visible) - context = self.get_context(request) context["form"] = form return TemplateResponse(request, self.get_template(request), context) + + form_builder = SitesFacilesFormBuilder diff --git a/forms/templates/forms/form_page.html b/forms/templates/forms/form_page.html index 09abb40a..e26ee99e 100644 --- a/forms/templates/forms/form_page.html +++ b/forms/templates/forms/form_page.html @@ -1,7 +1,10 @@ {% extends "base.html" %} - {% load wagtailcore_tags dsfr_tags %} +{% block title %} + {{ page.seo_title|default:page.title }} — {{ settings.content_manager.CmsDsfrConfig.site_title }} +{% endblock title %} + {% block content %}
@@ -11,15 +14,7 @@

{{ page.title }}

{% csrf_token %} {% dsfr_form %} - - -
    - {% for field in form %} -
  • {{ field.label }}, {{ field.type }}, {{ field.field.widget.attrs }} {{ field.field.widget }}
  • - {% endfor %} -
-
diff --git a/forms/templates/forms/form_page_landing.html b/forms/templates/forms/form_page_landing.html index b4e885e7..94c8b281 100644 --- a/forms/templates/forms/form_page_landing.html +++ b/forms/templates/forms/form_page_landing.html @@ -1,10 +1,14 @@ {% extends "base.html" %} -{% load wagtailcore_tags %} +{% load i18n wagtailcore_tags %} {% block content %}

{{ page.title }}

- {{ page.thank_you_text|richtext }} + {% if page.thank_you_text %} + {{ page.thank_you_text|richtext }} + {% else %} +

{% translate "Your form has been successfully submitted. Thank you!" %}

+ {% endif %}
{% endblock content %} diff --git a/locale/fr/LC_MESSAGES/django.mo b/locale/fr/LC_MESSAGES/django.mo index e1b454bb2f783751001dfbacb8cae9650c6b29d9..e322a55ba3381186345bfcbfe65f87e0bae0ce5a 100644 GIT binary patch delta 292 zcmXxfAq&Dl7{>9P+nk#sYEm?8SQIQGI8kY$4( z!07+jeBt4@_uktb7aLz=6IZmoLKR4ZtVx-?h%8$qy@M_K9_l#763)=Z1)3P9o=~H| zU>2{~#weY~$e9!6%}14ir6wDiD1Y#gWfRzaFXp delta 269 zcmeyteviHWo)F7a1|Z-DVi_PV0b)TQJ_E!cAixOW3j%2kAYU0svjS-!AT0o-gMhRG zkWL2DoItt-O7{V2HXwfnkk$m!^P%D=fwUqJUj||c1_m#NuS^gHe9RCH7C>4OsKFjc zgEU40X?7sp45ayhbT^RZ0@Aa9v<{G73Zy}fzclftv?9m_96$^LAVtUk Date: Thu, 27 Jun 2024 14:53:14 +0200 Subject: [PATCH 6/9] add tests --- Makefile | 2 +- forms/tests.py | 1 - forms/tests/__init__.py | 0 forms/tests/test_forms.py | 53 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) delete mode 100644 forms/tests.py create mode 100644 forms/tests/__init__.py create mode 100644 forms/tests/test_forms.py diff --git a/Makefile b/Makefile index 82eeaa9a..6e34bb82 100644 --- a/Makefile +++ b/Makefile @@ -83,4 +83,4 @@ runserver: .PHONY: test test: - $(EXEC_CMD) poetry run python manage.py test + $(EXEC_CMD) poetry run python manage.py test --buffer diff --git a/forms/tests.py b/forms/tests.py deleted file mode 100644 index a39b155a..00000000 --- a/forms/tests.py +++ /dev/null @@ -1 +0,0 @@ -# Create your tests here. diff --git a/forms/tests/__init__.py b/forms/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/forms/tests/test_forms.py b/forms/tests/test_forms.py new file mode 100644 index 00000000..8f865548 --- /dev/null +++ b/forms/tests/test_forms.py @@ -0,0 +1,53 @@ +from django.core.management import call_command +from wagtail.test.utils import WagtailPageTestCase + +from forms.models import FormPage + + +class FormsTestCase(WagtailPageTestCase): + @classmethod + def setUpTestData(cls) -> None: + call_command("create_starter_pages") + + def test_form_page_is_renderable(self): + form_page = FormPage.objects.first() + self.assertPageIsRenderable(form_page) + + def test_correct_form_is_submitted(self): + form_page = FormPage.objects.first() + post_data = { + "votre_nom_complet": "Félix Faure", + "votre_adresse_electronique": "no7@elysee.fr", + "titre_de_votre_message": "Ma connaissance", + "votre_message": "S’est enfuie par l’escalier !", + } + response = self.client.post(form_page.url, post_data) + + self.assertEqual(response.status_code, 200) + + self.assertContains( + response, + "

Merci pour votre message ! Nous reviendrons vers vous rapidement.

", + ) + + def test_incorrect_form_is_rejected(self): + form_page = FormPage.objects.first() + post_data = { + "votre_nom_complet": "Félix Faure", + "votre_adresse_electronique": "bad_email", + "titre_de_votre_message": "", + "votre_message": "S’est enfuie par l’escalier !", + } + response = self.client.post(form_page.url, post_data) + + self.assertEqual(response.status_code, 200) + + self.assertInHTML( + """
  • Saisissez une adresse de courriel valide.
  • """, + response.content.decode(), + ) + + self.assertInHTML( + """
  • Champ requis.
  • """, + response.content.decode(), + ) From 272e4a908fec81c210751cc874d3a2eefd6fe717 Mon Sep 17 00:00:00 2001 From: Sylvain Boissel Date: Thu, 27 Jun 2024 16:23:43 +0200 Subject: [PATCH 7/9] Improve form templates --- forms/models.py | 4 +-- forms/templates/forms/form_page.html | 27 +++++++++++--------- forms/templates/forms/form_page_landing.html | 13 +++++++++- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/forms/models.py b/forms/models.py index e694e1ca..681d5044 100644 --- a/forms/models.py +++ b/forms/models.py @@ -39,7 +39,7 @@ class Meta: class SitesFacilesBaseForm(BaseForm): """ - A base form that adds the necessary class on relevant fields + A base form that adds the necessary DSFR class on relevant fields """ def __init__(self, *args, **kwargs): @@ -49,7 +49,7 @@ def __init__(self, *args, **kwargs): @property def default_renderer(self): - return DsfrDjangoTemplates # Settings wasn't modified + return DsfrDjangoTemplates class SitesFacilesFormBuilder(FormBuilder): diff --git a/forms/templates/forms/form_page.html b/forms/templates/forms/form_page.html index e26ee99e..f6a66386 100644 --- a/forms/templates/forms/form_page.html +++ b/forms/templates/forms/form_page.html @@ -6,17 +6,20 @@ {% endblock title %} {% block content %} -
    -
    -
    -

    {{ page.title }}

    - {{ page.intro|richtext }} -
    - {% csrf_token %} - {% dsfr_form %} - -
    -
    -
    +
    + {% include "content_manager/blocks/breadcrumbs.html" %} +

    {{ page.title }}

    + +
    + + {% include "content_manager/blocks/messages.html" %} + +
    + {{ page.intro|richtext }} +
    + {% csrf_token %} + {% dsfr_form %} + +
    {% endblock content %} diff --git a/forms/templates/forms/form_page_landing.html b/forms/templates/forms/form_page_landing.html index 94c8b281..87c9111b 100644 --- a/forms/templates/forms/form_page_landing.html +++ b/forms/templates/forms/form_page_landing.html @@ -2,9 +2,20 @@ {% load i18n wagtailcore_tags %} +{% block title %} + {{ page.seo_title|default:page.title }} — {{ settings.content_manager.CmsDsfrConfig.site_title }} +{% endblock title %} + {% block content %} -
    +
    + {% include "content_manager/blocks/breadcrumbs.html" %}

    {{ page.title }}

    + +
    + + {% include "content_manager/blocks/messages.html" %} + +
    {% if page.thank_you_text %} {{ page.thank_you_text|richtext }} {% else %} From 8876da3b0ee4d57d64449d82b85b4d6abe224cf0 Mon Sep 17 00:00:00 2001 From: Sylvain Boissel Date: Thu, 27 Jun 2024 16:25:43 +0200 Subject: [PATCH 8/9] Update content creation scripts --- .../management/commands/create_demo_pages.py | 156 +++++++++++++++++- .../commands/create_starter_pages.py | 105 +++++++++++- content_manager/tests/test_views.py | 4 +- content_manager/utils.py | 34 +++- 4 files changed, 289 insertions(+), 10 deletions(-) diff --git a/content_manager/management/commands/create_demo_pages.py b/content_manager/management/commands/create_demo_pages.py index 63635ca7..df6b0250 100644 --- a/content_manager/management/commands/create_demo_pages.py +++ b/content_manager/management/commands/create_demo_pages.py @@ -8,8 +8,9 @@ from blog.models import BlogIndexPage from content_manager.models import ContentPage, MegaMenu, MegaMenuCategory +from forms.models import FormField, FormPage -ALL_ALLOWED_SLUGS = ["blog_index", "publications"] +ALL_ALLOWED_SLUGS = ["blog_index", "publications", "menu_page", "form"] fake = Faker("fr_FR") @@ -51,6 +52,22 @@ def handle(self, *args, **kwargs): elif slug == "publications": self.create_publication_pages(site, home_page, main_menu) + elif slug == "menu_page": + # A blank page that is just destined to have a list of its subpages. + body = [("subpageslist", None)] + menu_page = self.create_content_page(slug, title="Pages d’exemple", body=body, parent_page=home_page) + + # Inserts it right before the last entry + contact_menu_entry = MainMenuItem.objects.filter(menu=main_menu).last() + MainMenuItem.objects.update_or_create( + link_page=menu_page, menu=main_menu, defaults={"sort_order": contact_menu_entry.sort_order} + ) + contact_menu_entry.sort_order += 1 + contact_menu_entry.save() + + elif slug == "form": + menu_page = ContentPage.objects.get(slug="menu_page") + self.create_form_page("form_with_all_fields", parent_page=menu_page) else: raise ValueError(f"Valeur inconnue : {slug}") @@ -151,3 +168,140 @@ def create_publication_pages(self, site, home_page, main_menu): publications_mega_menu.categories.add(menu_category) publications_mega_menu.save() + + def create_form_page(self, slug: str, parent_page: ContentPage) -> None: + """ + Creates a form page with all the different forms + """ + + # Don't replace a manually created page + already_exists = ContentPage.objects.filter(slug=slug).first() + if already_exists: + self.stdout.write(f"The page seem to already exist with id {already_exists.id}") + return + + # Create the form page + title = "Formulaire avec tous les champs" + intro = RichText("

    Texte d’introduction

    ") + + thank_you_text = RichText("

    Merci pour votre message !

    ") + + form_page = parent_page.add_child( + instance=FormPage(title=title, slug=slug, intro=intro, thank_you_text=thank_you_text, show_in_menus=True) + ) + + # Create the form fields + fields = [ + { + "sort_order": 0, + "clean_name": "champ_texte", + "label": "Champ texte", + "required": True, + "choices": "", + "default_value": "", + "help_text": "", + "page": form_page, + "field_type": "singleline", + }, + { + "sort_order": 1, + "clean_name": "zone_de_texte", + "label": "Zone de texte", + "required": True, + "page": form_page, + "field_type": "multiline", + }, + { + "sort_order": 2, + "clean_name": "adresse_email", + "label": "Adresse email", + "required": True, + "page": form_page, + "field_type": "email", + }, + { + "sort_order": 3, + "clean_name": "nombre", + "label": "Nombre", + "default_value": 42, + "required": True, + "page": form_page, + "field_type": "number", + }, + { + "sort_order": 4, + "clean_name": "url", + "label": "URL", + "required": True, + "page": form_page, + "field_type": "url", + }, + { + "sort_order": 5, + "clean_name": "case_a_cocher", + "label": "Case à cocher", + "required": True, + "page": form_page, + "field_type": "checkbox", + }, + { + "sort_order": 6, + "clean_name": "cases_a_cocher", + "label": "Cases à cocher", + "required": True, + "choices": "1\r\n2\r\n3", + "default_value": "", + "help_text": "", + "page": form_page, + "field_type": "checkboxes", + }, + { + "sort_order": 7, + "clean_name": "liste_deroulante", + "label": "Liste déroulante", + "required": True, + "choices": "4\r\n5\r\n6", + "default_value": "", + "help_text": "", + "page": form_page, + "field_type": "dropdown", + }, + { + "sort_order": 8, + "clean_name": "boutons_radio", + "label": "Boutons radio", + "required": True, + "choices": "7\r\n8\r\n9", + "default_value": "", + "help_text": "", + "page": form_page, + "field_type": "radio", + }, + { + "sort_order": 9, + "clean_name": "date", + "label": "Date", + "required": True, + "choices": "", + "default_value": "", + "help_text": "", + "page": form_page, + "field_type": "date", + }, + { + "sort_order": 10, + "clean_name": "champ_cache", + "label": "Champ caché", + "required": True, + "choices": "", + "default_value": "valeur", + "help_text": "", + "page": form_page, + "field_type": "hidden", + }, + ] + + for field_data in fields: + FormField.objects.create(**field_data) + + self.stdout.write(self.style.SUCCESS(f"Page {slug} created with id {form_page.id}")) diff --git a/content_manager/management/commands/create_starter_pages.py b/content_manager/management/commands/create_starter_pages.py index a02b90dc..980261d9 100644 --- a/content_manager/management/commands/create_starter_pages.py +++ b/content_manager/management/commands/create_starter_pages.py @@ -5,12 +5,13 @@ from wagtail.images.models import Image from wagtail.models import Page, Site from wagtail.rich_text import RichText -from wagtailmenus.models.menuitems import FlatMenuItem +from wagtailmenus.models.menuitems import FlatMenuItem, MainMenuItem from content_manager.models import ContentPage -from content_manager.utils import get_or_create_footer_menu +from content_manager.utils import get_or_create_footer_menu, get_or_create_main_menu +from forms.models import FormField, FormPage -ALL_ALLOWED_SLUGS = ["home", "mentions-legales", "accessibilite"] +ALL_ALLOWED_SLUGS = ["home", "mentions-legales", "accessibilite", "contact"] class Command(BaseCommand): @@ -80,6 +81,8 @@ def handle(self, *args, **kwargs): body.append(("alert", alert_block)) self.create_page(slug=slug, title=title, body=body, footer_label="Accessibilité : non conforme") + elif slug == "contact": + self.create_contact_page(slug) else: raise ValueError(f"Valeur inconnue : {slug}") @@ -171,3 +174,99 @@ def create_page(self, slug: str, title: str, body: list, footer_label: str = "") FlatMenuItem.objects.create(**footer_item) self.stdout.write(self.style.SUCCESS(f"Page {slug} created with id {new_page.id}")) + + def create_contact_page(self, slug: str = "contact") -> None: + """ + Creates a contact page for the site and adds it the main menu + """ + + # Don't replace a manually created page + already_exists = ContentPage.objects.filter(slug=slug).first() + if already_exists: + self.stdout.write(f"The contact page seem to already exist with id {already_exists.id}") + return + + # Create the form page + title = "Contact" + intro = RichText( + """ +

    Bonjour, n’hésitez pas à nous contacter via le formulaire ci-dessous.

    +

    +

    Vous pouvez également nous contacter via <autres méthodes>.

    +

    +

    Les champs marqués d’une astérisque (*) sont obligatoires.

    """ + ) + + thank_you_text = RichText("

    Merci pour votre message ! Nous reviendrons vers vous rapidement.

    ") + + default_site = Site.objects.filter(is_default_site=True).first() + home_page = default_site.root_page + contact_page = home_page.add_child( + instance=FormPage(title=title, slug=slug, intro=intro, thank_you_text=thank_you_text, show_in_menus=True) + ) + + # Create the form fields + fields = [ + { + "sort_order": 0, + "clean_name": "votre_nom_complet", + "label": "Votre nom complet", + "required": True, + "page": contact_page, + "field_type": "singleline", + }, + { + "sort_order": 1, + "clean_name": "votre_adresse_electronique", + "label": "Votre adresse électronique", + "required": True, + "choices": "", + "default_value": "", + "help_text": "Format attendu : nom@domaine.fr", + "page": contact_page, + "field_type": "email", + }, + { + "sort_order": 2, + "clean_name": "votre_numero_de_telephone", + "label": "Votre numéro de téléphone", + "required": False, + "page": contact_page, + "field_type": "singleline", + }, + { + "sort_order": 3, + "clean_name": "titre_de_votre_message", + "label": "Titre de votre message", + "required": True, + "page": contact_page, + "field_type": "singleline", + }, + { + "sort_order": 4, + "clean_name": "votre_message", + "label": "Votre message", + "required": True, + "choices": "", + "default_value": "", + "help_text": "", + "page": contact_page, + "field_type": "multiline", + }, + ] + + for field_data in fields: + FormField.objects.create(**field_data) + + # Menu item + main_menu = get_or_create_main_menu() + + menu_item = { + "sort_order": MainMenuItem.objects.filter(menu=main_menu).count(), + "link_page": contact_page, + "link_text": title, + "menu": main_menu, + } + MainMenuItem.objects.create(**menu_item) + + self.stdout.write(self.style.SUCCESS(f"Form page {slug} created with id {contact_page.id}")) diff --git a/content_manager/tests/test_views.py b/content_manager/tests/test_views.py index e3c1f57e..f442546f 100644 --- a/content_manager/tests/test_views.py +++ b/content_manager/tests/test_views.py @@ -178,9 +178,7 @@ def setUp(self) -> None: self.site = Site.objects.filter(is_default_site=True).first() self.home_page = self.site.root_page - self.main_menu = MainMenu.objects.create(site=self.site) - - MainMenuItem.objects.create(link_page=self.home_page, menu=self.main_menu, link_text="Accueil", sort_order=0) + self.main_menu = MainMenu.objects.first() body = [] diff --git a/content_manager/utils.py b/content_manager/utils.py index 9b66a6b0..f94e23b1 100644 --- a/content_manager/utils.py +++ b/content_manager/utils.py @@ -6,7 +6,8 @@ from django.core.files.images import ImageFile from wagtail.images.models import Image from wagtail.models import Collection, Site -from wagtailmenus.models.menus import FlatMenu +from wagtailmenus.models.menuitems import MainMenuItem +from wagtailmenus.models.menus import FlatMenu, MainMenu def import_image(full_path: str, title: str) -> Image: @@ -39,15 +40,42 @@ def get_or_create_footer_menu() -> FlatMenu: In any case, return it. """ - footer_menu = FlatMenu.objects.filter(handle="footer").first() + default_site = Site.objects.filter(is_default_site=True).first() + footer_menu = FlatMenu.objects.filter(handle="footer", site=default_site).first() if not footer_menu: - default_site = Site.objects.filter(is_default_site=True).first() footer_menu = FlatMenu.objects.create(title="Pied de page", handle="footer", site=default_site) return footer_menu +def get_or_create_main_menu() -> MainMenu: + """ + Get the main menu or create it if it doesn't already exist + + In any case, return it. + """ + + default_site = Site.objects.filter(is_default_site=True).first() + main_menu = MainMenu.objects.filter(site=default_site).first() + + if not main_menu: + main_menu = MainMenu.objects.create(site=default_site, max_levels=2) + + # Init the main menu with the home page + home_page = default_site.root_page + + menu_item = { + "sort_order": 0, + "link_page": home_page, + "link_text": "Accueil", + "menu": main_menu, + } + MainMenuItem.objects.create(**menu_item) + + return main_menu + + def get_streamblock_raw_text(block) -> str: """ Get the raw text of a streamblock. From 2c8bfdeaa74fce8221559ca7be5e22591d27cfdf Mon Sep 17 00:00:00 2001 From: Sylvain Boissel Date: Thu, 27 Jun 2024 17:32:24 +0200 Subject: [PATCH 9/9] Update margin --- forms/templates/forms/form_page_landing.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forms/templates/forms/form_page_landing.html b/forms/templates/forms/form_page_landing.html index 87c9111b..353fe978 100644 --- a/forms/templates/forms/form_page_landing.html +++ b/forms/templates/forms/form_page_landing.html @@ -15,7 +15,7 @@

    {{ page.title }}

    {% include "content_manager/blocks/messages.html" %} -
    +
    {% if page.thank_you_text %} {{ page.thank_you_text|richtext }} {% else %}