def get_advanced_settings_model():
    """
    Returns the advanced form settings model if one is defined
    """

    model = get_setting("ADVANCED_SETTINGS_MODEL")

    if not model:
        return

    def raise_error(msg):
        setting = "WAGTAILSTREAMFORMS_ADVANCED_SETTINGS_MODEL"
        raise ImproperlyConfigured("%s %s" % (setting, msg))

    try:
        model_class = apps.get_model(model, require_ready=False)
        if issubclass(model_class, AbstractFormSetting):
            return model_class
        raise_error(
            "must inherit from 'wagtailstreamforms.models.AbstractFormSetting'"
        )
    except ValueError:
        raise_error("must be of the form 'app_label.model_name'")
    except LookupError:
        raise_error("refers to model '%s' that has not been installed" % model)
class FormModelAdmin(ModelAdmin):
    model = Form
    list_display = ("title", "slug", "latest_submission", "saved_submissions")
    list_filter = None
    menu_label = _(get_setting("ADMIN_MENU_LABEL"))
    menu_order = get_setting("ADMIN_MENU_ORDER")
    menu_icon = "icon icon-form"
    search_fields = ("title", "slug")
    button_helper_class = FormButtonHelper
    inspect_view_class = InspectFormView
    create_view_class = CreateFormView
    edit_view_class = EditFormView
    delete_view_class = DeleteFormView
    url_helper_class = FormURLHelper

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        for fn in form_hooks.get_hooks("construct_form_queryset"):
            qs = fn(qs, request)
        return qs

    def get_list_display(self, request):
        list_display = self.list_display
        for fn in form_hooks.get_hooks("construct_form_list_display"):
            list_display = fn(list_display, request)
        return list_display

    def get_list_filter(self, request):
        list_filter = self.list_filter
        for fn in form_hooks.get_hooks("construct_form_list_filter"):
            list_filter = fn(list_filter, request)
        return list_filter

    def latest_submission(self, obj):
        submission_class = obj.get_submission_class()
        return (submission_class._default_manager.filter(
            form=obj).latest("submit_time").submit_time)

    latest_submission.short_description = _("Latest submission")

    def saved_submissions(self, obj):
        submission_class = obj.get_submission_class()
        return submission_class._default_manager.filter(form=obj).count()

    saved_submissions.short_description = _("Saved submissions")
def process_form(page, request, *args, **kwargs):
    """ Process the form if there is one, if not just continue. """

    # only process if settings.WAGTAILSTREAMFORMS_ENABLE_FORM_PROCESSING is True
    if not get_setting("ENABLE_FORM_PROCESSING"):
        return

    if request.method == "POST":
        form_def = get_form_instance_from_request(request)

        if form_def:
            form = form_def.get_form(request.POST,
                                     request.FILES,
                                     page=page,
                                     user=request.user)
            context = page.get_context(request, *args, **kwargs)

            if form.is_valid():
                # process the form submission
                form_def.process_form_submission(form)

                # create success message
                if form_def.success_message:
                    messages.success(request,
                                     form_def.success_message,
                                     fail_silently=True)

                # redirect to the page defined in the form
                # or the current page as a fallback - this will avoid refreshing and submitting again
                redirect_page = form_def.post_redirect_page or page

                return redirect(redirect_page.get_url(request),
                                context=context)

            else:
                # update the context with the invalid form and serve the page
                context.update({
                    "invalid_stream_form_reference":
                    form.data.get("form_reference"),
                    "invalid_stream_form":
                    form,
                })

                # create error message
                if form_def.error_message:
                    messages.error(request,
                                   form_def.error_message,
                                   fail_silently=True)

                return TemplateResponse(
                    request, page.get_template(request, *args, **kwargs),
                    context)
Exemple #4
0
class FormModelAdmin(ModelAdmin):
    model = Form
    list_display = ('title', 'slug', 'latest_submission', 'saved_submissions')
    menu_label = _(get_setting('ADMIN_MENU_LABEL'))
    menu_order = get_setting('ADMIN_MENU_ORDER')
    menu_icon = 'icon icon-form'
    search_fields = ('title', 'slug')
    button_helper_class = FormButtonHelper
    url_helper_class = FormURLHelper

    def latest_submission(self, obj):
        submission_class = obj.get_submission_class()
        return submission_class._default_manager.filter(
            form=obj).latest('submit_time').submit_time

    latest_submission.short_description = _('Latest submission')

    def saved_submissions(self, obj):
        submission_class = obj.get_submission_class()
        return submission_class._default_manager.filter(form=obj).count()

    saved_submissions.short_description = _('Saved submissions')
Exemple #5
0
def get_hooks(hook_name):
    """ Return the hooks function sorted by their order. """

    search_for_hooks()
    hooks = _hooks.get(hook_name, [])
    hooks = sorted(hooks, key=itemgetter(1))
    fncs = []
    builtin_hook_modules = ['wagtailstreamforms.wagtailstreamforms_hooks']
    builtin_enabled = get_setting('ENABLE_BUILTIN_HOOKS')

    for fn, _ in hooks:
        # dont add the hooks if they have been disabled via the setting
        # this is so that they can be overridden
        if fn.__module__ in builtin_hook_modules and not builtin_enabled:
            continue
        fncs.append(fn)

    return fncs
Exemple #6
0
class FormGroup(ModelAdminGroup):
    menu_label = _(get_setting('ADMIN_MENU_LABEL'))
    menu_order = get_setting('ADMIN_MENU_ORDER')
    menu_icon = 'icon icon-form'
    items = form_admins + [RegexFieldValidatorModelAdmin]
            ],
            icon=self.icon,
            label=self.label,
        )


FIELD_MAPPING = {
    "singleline": SingleLineTextField,
    "multiline": MultiLineTextField,
    "date": DateField,
    "datetime": DateTimeField,
    "email": EmailField,
    "url": URLField,
    "number": NumberField,
    "dropdown": DropdownField,
    "radio": RadioField,
    "checkboxes": CheckboxesField,
    "checkbox": CheckboxField,
    "hidden": HiddenField,
    "singlefile": SingleFileField,
    "multifile": MultiFileField,
}

enabled_fields = get_setting("ENABLED_FIELDS")

for field_name in enabled_fields:
    cls = FIELD_MAPPING.get(field_name, None)
    if not cls:
        raise KeyError("Field with name '%s' does not exist" % field_name)
    register(field_name, cls)
Exemple #8
0
class Form(models.Model):
    """ The form class. """

    title = models.CharField(
        _('Title'),
        max_length=255
    )
    slug = models.SlugField(
        _('Slug'),
        allow_unicode=True,
        max_length=255,
        unique=True,
        help_text=_('Used to identify the form in template tags')
    )
    template_name = models.CharField(
        _('Template'),
        max_length=255,
        choices=get_setting('FORM_TEMPLATES')
    )
    fields = FormFieldsStreamField(
        [],
        verbose_name=_('Fields')
    )
    submit_button_text = models.CharField(
        _('Submit button text'),
        max_length=100,
        default='Submit'
    )
    success_message = models.CharField(
        _('Success message'),
        blank=True,
        max_length=255,
        help_text=_('An optional success message to show when the form has been successfully submitted')
    )
    error_message = models.CharField(
        _('Error message'),
        blank=True,
        max_length=255,
        help_text=_('An optional error message to show when the form has validation errors')
    )
    post_redirect_page = models.ForeignKey(
        'wagtailcore.Page',
        verbose_name=_('Post redirect page'),
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='+',
        help_text=_('The page to redirect to after a successful submission')
    )
    process_form_submission_hooks = HookSelectField(
        verbose_name=_('Submission hooks'),
        blank=True
    )

    settings_panels = [
        FieldPanel('title', classname='full'),
        FieldPanel('slug'),
        FieldPanel('template_name'),
        FieldPanel('submit_button_text'),
        MultiFieldPanel([
            FieldPanel('success_message'),
            FieldPanel('error_message'),
        ], _('Messages')),
        FieldPanel('process_form_submission_hooks', classname='choice_field'),
        PageChooserPanel('post_redirect_page')
    ]

    field_panels = [
        StreamFieldPanel('fields'),
    ]

    edit_handler = TabbedInterface([
        ObjectList(settings_panels, heading=_('General')),
        ObjectList(field_panels, heading=_('Fields')),
    ])

    def __str__(self):
        return self.title

    class Meta:
        ordering = ['title', ]
        verbose_name = _('Form')
        verbose_name_plural = _('Forms')

    def copy(self):
        """ Copy this form and its fields. """

        form_copy = Form(
            title=self.title,
            slug=uuid.uuid4(),
            template_name=self.template_name,
            fields=self.fields,
            submit_button_text=self.submit_button_text,
            success_message=self.success_message,
            error_message=self.error_message,
            post_redirect_page=self.post_redirect_page,
            process_form_submission_hooks=self.process_form_submission_hooks
        )
        form_copy.save()

        # additionally copy the advanced settings if they exist
        SettingsModel = get_advanced_settings_model()

        if SettingsModel:
            try:
                advanced = SettingsModel.objects.get(form=self)
                advanced.pk = None
                advanced.form = form_copy
                advanced.save()
            except SettingsModel.DoesNotExist:
                pass

        return form_copy

    copy.alters_data = True

    def get_data_fields(self):
        """ Returns a list of tuples with (field_name, field_label). """

        data_fields = [
            ('submit_time', _('Submission date')),
        ]
        data_fields += [
            (get_slug_from_string(field['value']['label']), field['value']['label'])
            for field in self.get_form_fields()
        ]

        return data_fields

    def get_form(self, *args, **kwargs):
        """ Returns the form. """

        form_class = self.get_form_class()
        return form_class(*args, **kwargs)

    def get_form_class(self):
        """ Returns the form class. """

        return FormBuilder(self.get_form_fields()).get_form_class()

    def get_form_fields(self):
        """ Returns the form fields stream_data. """

        return self.fields.stream_data

    def get_submission_class(self):
        """ Returns submission class. """

        return FormSubmission

    def process_form_submission(self, form):
        """ Runs each hook if selected in the form. """

        for fn in hooks.get_hooks('process_form_submission'):
            if fn.__name__ in self.process_form_submission_hooks:
                fn(self, form)
 def test_template_name(self):
     field = self.get_field(BaseForm, 'template_name')
     self.assertModelField(field, models.CharField)
     self.assertEqual(field.max_length, 255)
     self.assertEqual(field.choices, get_setting('FORM_TEMPLATES'))
Exemple #10
0
class AbstractForm(models.Model):
    site = models.ForeignKey(Site, on_delete=models.SET_NULL, null=True, blank=True)
    title = models.CharField(_("Title"), max_length=255)
    slug = models.SlugField(
        _("Slug"),
        allow_unicode=True,
        max_length=255,
        unique=True,
        help_text=_("Used to identify the form in template tags"),
    )
    template_name = models.CharField(
        _("Template"), max_length=255, choices=get_setting("FORM_TEMPLATES")
    )
    fields = FormFieldsStreamField([], verbose_name=_("Fields"))
    submit_button_text = models.CharField(
        _("Submit button text"), max_length=100, default="Submit"
    )
    success_message = models.CharField(
        _("Success message"),
        blank=True,
        max_length=255,
        help_text=_(
            "An optional success message to show when the form has been successfully submitted"
        ),
    )
    error_message = models.CharField(
        _("Error message"),
        blank=True,
        max_length=255,
        help_text=_(
            "An optional error message to show when the form has validation errors"
        ),
    )
    post_redirect_page = models.ForeignKey(
        "wagtailcore.Page",
        verbose_name=_("Post redirect page"),
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        help_text=_("The page to redirect to after a successful submission"),
    )
    process_form_submission_hooks = HookSelectField(
        verbose_name=_("Submission hooks"), blank=True
    )

    objects = FormQuerySet.as_manager()

    settings_panels = [
        FieldPanel("title", classname="full"),
        FieldPanel("slug"),
        FieldPanel("template_name"),
        FieldPanel("submit_button_text"),
        MultiFieldPanel(
            [FieldPanel("success_message"), FieldPanel("error_message")], _("Messages")
        ),
        FieldPanel("process_form_submission_hooks", classname="choice_field"),
        PageChooserPanel("post_redirect_page"),
    ]

    field_panels = [StreamFieldPanel("fields")]

    edit_handler = TabbedInterface(
        [
            ObjectList(settings_panels, heading=_("General")),
            ObjectList(field_panels, heading=_("Fields")),
        ]
    )

    def __str__(self):
        return self.title

    class Meta:
        abstract = True
        ordering = ["title"]
        verbose_name = _("Form")
        verbose_name_plural = _("Forms")

    def copy(self):
        """Copy this form and its fields."""

        form_copy = Form(
            site=self.site,
            title=self.title,
            slug=uuid.uuid4(),
            template_name=self.template_name,
            fields=self.fields,
            submit_button_text=self.submit_button_text,
            success_message=self.success_message,
            error_message=self.error_message,
            post_redirect_page=self.post_redirect_page,
            process_form_submission_hooks=self.process_form_submission_hooks,
        )
        form_copy.save()

        # additionally copy the advanced settings if they exist
        SettingsModel = get_advanced_settings_model()

        if SettingsModel:
            try:
                advanced = SettingsModel.objects.get(form=self)
                advanced.pk = None
                advanced.form = form_copy
                advanced.save()
            except SettingsModel.DoesNotExist:
                pass

        return form_copy

    copy.alters_data = True

    def get_data_fields(self):
        """Returns a list of tuples with (field_name, field_label)."""

        data_fields = [("submit_time", _("Submission date"))]
        data_fields += [
            (get_slug_from_string(field["value"]["label"]), field["value"]["label"])
            for field in self.get_form_fields()
        ]

        return data_fields

    def get_form(self, *args, **kwargs):
        """Returns the form."""

        form_class = self.get_form_class()
        return form_class(*args, **kwargs)

    def get_form_class(self):
        """Returns the form class."""

        return FormBuilder(self.get_form_fields()).get_form_class()

    def get_form_fields(self):
        """Returns the form field's stream data."""

        if WAGTAIL_VERSION >= (2, 12):
            form_fields = self.fields.raw_data
        else:
            form_fields = self.fields.stream_data
        for fn in hooks.get_hooks("construct_submission_form_fields"):
            form_fields = fn(form_fields)
        return form_fields

    def get_submission_class(self):
        """Returns submission class."""

        return FormSubmission

    def process_form_submission(self, form):
        """Runs each hook if selected in the form."""

        for fn in hooks.get_hooks("process_form_submission"):
            if fn.__name__ in self.process_form_submission_hooks:
                fn(self, form)
Exemple #11
0
class BaseForm(ClusterableModel):
    """ A form base class, any form should inherit from this. """

    name = models.CharField(_('Name'), max_length=255)
    slug = models.SlugField(
        _('Slug'),
        allow_unicode=True,
        max_length=255,
        unique=True,
        help_text=_('Used to identify the form in template tags'))
    content_type = models.ForeignKey(
        'contenttypes.ContentType',
        verbose_name=_('Content type'),
        related_name='streamforms',
        on_delete=models.SET(get_default_form_content_type))
    template_name = models.CharField(_('Template'),
                                     max_length=255,
                                     choices=get_setting('FORM_TEMPLATES'))
    submit_button_text = models.CharField(_('Submit button text'),
                                          max_length=100,
                                          default='Submit')
    store_submission = models.BooleanField(_('Store submission'),
                                           default=False)
    add_recaptcha = models.BooleanField(
        _('Add recaptcha'),
        default=False,
        help_text=_('Add a reCapcha field to the form.'))
    success_message = models.CharField(
        _('Success message'),
        blank=True,
        max_length=255,
        help_text=
        _('An optional success message to show when the form has been successfully submitted'
          ))
    error_message = models.CharField(
        _('Error message'),
        blank=True,
        max_length=255,
        help_text=
        _('An optional error message to show when the form has validation errors'
          ))
    post_redirect_page = models.ForeignKey(
        'wagtailcore.Page',
        verbose_name=_('Post redirect page'),
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='+',
        help_text=_('The page to redirect to after a successful submission'))

    settings_panels = [
        FieldPanel('name', classname='full'),
        FieldPanel('slug'),
        FieldPanel('template_name'),
        FieldPanel('submit_button_text'),
        MultiFieldPanel([
            FieldPanel('success_message'),
            FieldPanel('error_message'),
        ], _('Messages')),
        FieldPanel('store_submission'),
        PageChooserPanel('post_redirect_page')
    ]

    field_panels = [
        InlinePanel('form_fields', label=_('Fields')),
    ]

    edit_handler = TabbedInterface([
        ObjectList(settings_panels, heading=_('General')),
        ObjectList(field_panels, heading=_('Fields')),
    ])

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not self.id:
            self.content_type = ContentType.objects.get_for_model(self)

    def __str__(self):
        return self.name

    class Meta:
        ordering = [
            'name',
        ]
        verbose_name = _('Form')
        verbose_name_plural = _('Forms')

    def copy(self):
        """ Copy this form and its fields. """

        exclude_fields = ['id', 'slug']
        specific_self = self.specific
        specific_dict = {}

        for field in specific_self._meta.get_fields():

            # ignore explicitly excluded fields
            if field.name in exclude_fields:
                continue  # pragma: no cover

            # ignore reverse relations
            if field.auto_created:
                continue  # pragma: no cover

            # ignore m2m relations - they will be copied as child objects
            # if modelcluster supports them at all (as it does for tags)
            if field.many_to_many:
                continue  # pragma: no cover

            # ignore parent links (baseform_ptr)
            if isinstance(field,
                          models.OneToOneField) and field.rel.parent_link:
                continue  # pragma: no cover

            specific_dict[field.name] = getattr(specific_self, field.name)

        # new instance from prepared dict values, in case the instance class implements multiple levels inheritance
        form_copy = self.specific_class(**specific_dict)

        # a dict that maps child objects to their new ids
        # used to remap child object ids in revisions
        child_object_id_map = defaultdict(dict)

        # create the slug - temp as will be changed from the copy form
        form_copy.slug = uuid.uuid4()

        form_copy.save()

        # copy child objects
        for child_relation in get_all_child_relations(specific_self):
            accessor_name = child_relation.get_accessor_name()
            parental_key_name = child_relation.field.attname
            child_objects = getattr(specific_self, accessor_name, None)

            if child_objects:
                for child_object in child_objects.all():
                    old_pk = child_object.pk
                    child_object.pk = None
                    setattr(child_object, parental_key_name, form_copy.id)
                    child_object.save()

                    # add mapping to new primary key (so we can apply this change to revisions)
                    child_object_id_map[accessor_name][
                        old_pk] = child_object.pk

            else:  # we should never get here as there is always a FormField child class
                pass  # pragma: no cover

        return form_copy

    copy.alters_data = True

    def get_data_fields(self):
        """
        Returns a list of tuples with (field_name, field_label).
        """

        data_fields = [
            ('submit_time', _('Submission date')),
        ]
        data_fields += [(field.clean_name, field.label)
                        for field in self.get_form_fields()]

        return data_fields

    def get_form(self, *args, **kwargs):
        form_class = self.get_form_class()
        form_params = self.get_form_parameters()
        form_params.update(kwargs)

        return form_class(*args, **form_params)

    def get_form_class(self):
        fb = FormBuilder(self.get_form_fields(),
                         add_recaptcha=self.add_recaptcha)
        return fb.get_form_class()

    def get_form_fields(self):
        """
        Form expects `form_fields` to be declared.
        If you want to change backwards relation name,
        you need to override this method.
        """

        return self.form_fields.all()

    def get_form_parameters(self):
        return {}

    def get_submission_class(self):
        """
        Returns submission class.

        You can override this method to provide custom submission class.
        Your class must be inherited from AbstractFormSubmission.
        """

        return FormSubmission

    def process_form_submission(self, form):
        """
        Accepts form instance with submitted data.
        Creates submission instance if self.store_submission = True.

        You can override this method if you want to have custom creation logic.
        For example, you want to additionally send an email.
        """

        if self.store_submission:
            return self.get_submission_class().objects.create(
                form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
                form=self)

    @cached_property
    def specific(self):
        """
        Return this form in its most specific subclassed form.
        """

        # the ContentType.objects manager keeps a cache, so this should potentially
        # avoid a database lookup over doing self.content_type. I think.
        content_type = ContentType.objects.get_for_id(self.content_type_id)
        model_class = content_type.model_class()
        if model_class is None:
            # Cannot locate a model class for this content type. This might happen
            # if the codebase and database are out of sync (e.g. the model exists
            # on a different git branch and we haven't rolled back migrations before
            # switching branches); if so, the best we can do is return the form
            # unchanged.
            return self  # pragma: no cover
        elif isinstance(self, model_class):
            # self is already the an instance of the most specific class
            return self
        else:
            return content_type.get_object_for_this_type(id=self.id)

    @cached_property
    def specific_class(self):
        """
        Return the class that this page would be if instantiated in its
        most specific form
        """

        content_type = ContentType.objects.get_for_id(self.content_type_id)
        return content_type.model_class()