From f0c078f59530bb4d5eb73870ce86ba22ed474ebd Mon Sep 17 00:00:00 2001 From: Sylvain Boissel Date: Mon, 24 Jun 2024 17:30:20 +0200 Subject: [PATCH] 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 %}