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)
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')
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
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)
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'))
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)
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()