Package: django-fobi
django-fobi(or justfobi) is a customisable, modular, user- and developer-
friendly form generator/builder application for Django. Withfobiyou can
build Django forms using an intuitive GUI, save or mail posted form data or
even export forms into JSON format and import them on other instances. API
allows you to build your own form elements and form handlers (mechanisms for
handling the submitted form data).PrerequisitesDjango 2.2, 3.0, 3.1, 3.2, 4.0 and 4.1.Python 3.6, 3.7, 3.8, 3.9, 3.10 and 3.11.Key conceptsEach form consists of elements. Form elements are divided into groups:form fields (input field, textarea, hidden field, file field, etc.).content (presentational) elements (text, image, embed video, etc.).security elements (captcha, etc).Number of form elements is not limited.Each form may contain handlers. Handler processes the form data (for example,
saves it or mails it). Number of the handlers is not limited.Both form elements and form handlers are made with Django permission system
in mind.As an addition to form handlers, form callbacks are implemented. Form
callbacks are fired on various stages of pre- and post-processing the form
data (on POST). Form callbacks do not make use of permission system (unless
you intentionally do so in the code of your callback) and are fired for all
forms (unlike form handlers, that are executed only if assigned).Each plugin (form element or form handler) or a callback - is a Django
micro-app.In addition for form element and form handler plugins, integration form
element and integration form handler plugins are implemented for integration
with diverse third-party apps and frameworks (such as Django REST framework).Note, thatdjango-fobidoes not require django-admin and administrative
rights/permissions to access the UI, although almost seamless integration with
django-admin is implemented through thesimpletheme.Main features and highlightsUser-friendly GUI to quickly build forms.Large variety ofBundled form element plugins. Most of the Django fields
are supported.HTML5 fieldsare supported as well.Form wizards. Combine your forms into wizards. Form wizards may contain
handlers. Handler processes the form wizard data (for example, saves it or
mails it). Number of the form wizard handlers is not limited.Forms can be automatically enabled/disabled based on dates (start date, end
date).Anti-spam solutions likeCAPTCHA,ReCAPTCHA,HoneypotorInvisible reCAPTCHAcome out of the box (CAPTCHA and ReCAPTCHA do require additional third-party
apps to be installed; Invisible reCAPTCHA doesn’t).In addition to standard form elements, there are cosmetic (presentational)
form elements (for adding a piece of text, image or a embed video)
alongside standard form elements.Data handling in plugins (form handlers). Save the data, mail it to some
address or re-post it to some other endpoint. See theBundled form handler pluginsfor more information.Developer-friendly API, which allows to edit existing or build new form
fields and handlers without touching the core.Support for custom user model.Class based views (and class-based permissions). Forms have an
owner (auth.User). Default permissions are set for the owner, but
class-based views provide a lot of freedom and can be easily customized.Theming. There are 4 ready to useBundled themes: “Bootstrap 3”,
“Foundation 5”, “Simple” (with editing interface in style of Django admin)
and “DjangoCMS admin style” theme (which is another simple theme with editing
interface in style ofdjangocms-admin-style).Implementedintegration with Django REST framework.Implementedintegration with Wagtail(in a form of a Wagtail page).Implementedintegration with FeinCMS(in a form of a FeinCMS page widget).Implementedintegration with DjangoCMS(in a form of a DjangoCMS page plugin).Implementedintegration with Mezzanine(in a form of a Mezzanine page).Reordering of form elements using drag-n-drop.Data export (DB storeform handler plugin) into XLS/CSV format.Dynamic initial valuesfor form elements.Import/export forms to/from JSON format.Import forms from MailChimp usingmailchimp importer.RoadmapSome of the upcoming/in-development features/improvements are:Implement disabling forms based on dates.Cloning of forms.JSON schema support.Webpack integration.Improved Django REST framework OPTIONS.Bootstrap 5 support.Foundation 6 support.See theTODOSfor the full list of planned-, pending- in-development- or to-be-implemented
features.Some screenshotsSee the documentation for some screen shots:ReadTheDocsDemoLive demoSee thelive demo appon Heroku.
Additionally, see theDjango REST framework integration demo.Credentials:username: test_userpassword: test_userRun demo locallyIn order to be able to quickly evaluate thedjango-fobi, a demo app (with a
quick installer) has been created (works on Ubuntu/Debian, may work on other
Linux systems as well, although not guaranteed). Follow the instructions below
for having the demo running within a minute.Grab the latestdjango_fobi_example_app_installer.sh:wgethttps://raw.github.com/barseghyanartur/django-fobi/stable/examples/django_fobi_example_app_installer.shAssign execute rights to the installer and run thedjango_fobi_example_app_installer.sh:chmod+xdjango_fobi_example_app_installer.sh./django_fobi_example_app_installer.shOpen your browser and test the app.Dashboard:URL:http://127.0.0.1:8001/fobi/Admin username: test_adminAdmin password: testDjango admin interface:URL:http://127.0.0.1:8001/admin/Admin username: test_adminAdmin password: testIf quick installer doesn’t work for you, see the manual steps on running theexample project.Quick startSee thequick start.InstallationInstall latest stable version from PyPI:pipinstalldjango-fobiOr latest stable version from GitHub:pipinstallhttps://github.com/barseghyanartur/django-fobi/archive/stable.tar.gzAddfobitoINSTALLED_APPSof the your projects’ Django settings.
Furthermore, all themes and plugins to be used, shall be added to theINSTALLED_APPSas well. Note, that if a plugin has additional
dependencies, you should be mentioning those in theINSTALLED_APPSas well.INSTALLED_APPS=(# Used by fobi'django.contrib.auth','django.contrib.contenttypes','django.contrib.sessions','django.contrib.sites','django.contrib.messages','django.contrib.staticfiles','django.contrib.admin',# ...# `django-fobi` core'fobi',# `django-fobi` themes'fobi.contrib.themes.bootstrap3',# Bootstrap 3 theme'fobi.contrib.themes.foundation5',# Foundation 5 theme'fobi.contrib.themes.simple',# Simple theme# `django-fobi` form elements - fields'fobi.contrib.plugins.form_elements.fields.boolean','fobi.contrib.plugins.form_elements.fields.checkbox_select_multiple','fobi.contrib.plugins.form_elements.fields.date','fobi.contrib.plugins.form_elements.fields.date_drop_down','fobi.contrib.plugins.form_elements.fields.datetime','fobi.contrib.plugins.form_elements.fields.decimal','fobi.contrib.plugins.form_elements.fields.email','fobi.contrib.plugins.form_elements.fields.file','fobi.contrib.plugins.form_elements.fields.float','fobi.contrib.plugins.form_elements.fields.hidden','fobi.contrib.plugins.form_elements.fields.input','fobi.contrib.plugins.form_elements.fields.integer','fobi.contrib.plugins.form_elements.fields.ip_address','fobi.contrib.plugins.form_elements.fields.null_boolean','fobi.contrib.plugins.form_elements.fields.password','fobi.contrib.plugins.form_elements.fields.radio','fobi.contrib.plugins.form_elements.fields.regex','fobi.contrib.plugins.form_elements.fields.select','fobi.contrib.plugins.form_elements.fields.select_model_object','fobi.contrib.plugins.form_elements.fields.select_multiple','fobi.contrib.plugins.form_elements.fields.select_multiple_model_objects','fobi.contrib.plugins.form_elements.fields.slug','fobi.contrib.plugins.form_elements.fields.text','fobi.contrib.plugins.form_elements.fields.textarea','fobi.contrib.plugins.form_elements.fields.time','fobi.contrib.plugins.form_elements.fields.url',# `django-fobi` form elements - content elements'fobi.contrib.plugins.form_elements.test.dummy','easy_thumbnails',# Required by `content_image` plugin'fobi.contrib.plugins.form_elements.content.content_image','fobi.contrib.plugins.form_elements.content.content_image_url','fobi.contrib.plugins.form_elements.content.content_text','fobi.contrib.plugins.form_elements.content.content_video',# `django-fobi` form handlers'fobi.contrib.plugins.form_handlers.db_store','fobi.contrib.plugins.form_handlers.http_repost','fobi.contrib.plugins.form_handlers.mail','fobi.contrib.plugins.form_handlers.mail_sender',# Other project specific apps'foo',# Test app# ...)Make appropriate changes to theTEMPLATESof the your projects’
Django settings.Andfobi.context_processors.themeandfobi.context_processors.dynamic_values. See the following example.TEMPLATES=[{'BACKEND':'django.template.backends.django.DjangoTemplates','DIRS':[(os.path.join('path','to','your','templates'))],'OPTIONS':{'context_processors':["django.template.context_processors.debug",'django.template.context_processors.request',"django.contrib.auth.context_processors.auth","django.contrib.messages.context_processors.messages","fobi.context_processors.theme",# Important!"fobi.context_processors.dynamic_values",# Optional],'loaders':['django.template.loaders.filesystem.Loader','django.template.loaders.app_directories.Loader','admin_tools.template_loaders.Loader',],'debug':DEBUG_TEMPLATE,}},]Make sure thatdjango.core.context_processors.requestis incontext_processorstoo.Configure URLsAdd the following line to urlpatterns of yoururlsmodule.# View URLsre_path(r'^fobi/',include('fobi.urls.class_based.view')),# Edit URLsre_path(r'^fobi/',include('fobi.urls.class_based.edit')),Note, that some plugins require additional URL includes. For instance, if
you listed thefobi.contrib.plugins.form_handlers.db_storeform handler
plugin in theINSTALLED_APPS, you should mention the following inurlsmodule.# DB Store plugin URLsre_path(r'^fobi/plugins/form-handlers/db-store/',include('fobi.contrib.plugins.form_handlers.db_store.urls')),View URLs are put separately from edit URLs in order to make it possible
to prefix the edit URLs differently. For example, if you’re using the
“Simple” theme, you would likely want to prefix the edit URLs with “admin/”
so that it looks more like django-admin.Creating a new form element pluginForm element plugins represent the elements of which the forms is made:
Inputs, checkboxes, textareas, files, hidden fields, as well as pure
presentational elements (text or image). Number of form elements in a form
is not limited.Presentational form elements are inherited fromfobi.base.FormElementPlugin.The rest (real form elements, that are supposed to have a value)
are inherited fromfobi.base.FormFieldPlugin.You should see a form element plugin as a Django micro app, which could have
its’ own models, admin interface, etc.django-fobicomes with several bundled form element plugins. Do check the
source code as example.Let’s say, you want to create a textarea form element plugin.There are several properties, each textarea should have. They are:label(string): HTML label of the textarea.name(string): HTML name of the textarea.initial(string): Initial value of the textarea.required(bool): Flag, which tells us whether the field is required or
optional.Let’s name that pluginsample_textarea. The plugin directory should then
have the following structure.path/to/sample_textarea/├──__init__.py├──fobi_form_elements.py# Where plugins are defined and registered├──forms.py# Plugin configuration form└──widgets.py# Where plugins widgets are definedForm element plugins should be registered in “fobi_form_elements.py” file. Each
plugin module should be put into theINSTALLED_APPSof your Django
projects’ settings.In some cases, you would need plugin specific overridable settings (seefobi.contrib.form_elements.fields.content.content_imageplugin as an
example). You are advised to write your settings in such a way, that variables
of your Django project settings module would haveFOBI_PLUGIN_prefix.Define and register the form element pluginStep by step review of a how to create and register a plugin and plugin
widgets. Note, thatdjango-fobiauto-discovers your plugins if you place
them into a file namedfobi_form_elements.pyof any Django app listed inINSTALLED_APPSof your Django projects’ settings module.path/to/sample_textarea/fobi_form_elements.pyA single form element plugin is registered by its’ UID.Required imports.fromdjangoimportformsfromfobi.baseimportFormFieldPlugin,form_element_plugin_registryfrompath.to.sample_textarea.formsimportSampleTextareaFormDefining the Sample textarea plugin.classSampleTextareaPlugin(FormFieldPlugin):"""Sample textarea plugin."""uid="sample_textarea"name="Sample Textarea"form=SampleTextareaFormgroup="Samples"# Group to which the plugin belongs todefget_form_field_instances(self,request=None,form_entry=None,form_element_entries=None,**kwargs):kwargs={'required':self.data.required,'label':self.data.label,'initial':self.data.initial,'widget':forms.widgets.Textarea(attrs={})}return[(self.data.name,forms.CharField,kwargs),]Registering theSampleTextareaPluginplugin.form_element_plugin_registry.register(SampleTextareaPlugin)Note, that in case you want to define a pure presentational element, make use
offobi.base.FormElementPluginfor subclassing, instead offobi.base.FormFieldPlugin.
See the source of the content plugins
(fobi.contrib.plugins.form_elements.content) as a an example.For instance, thecaptchaandhoneypotfields are implemented
as form elements (subclasses thefobi.base.FormElementPlugin). Thedb_storeform handler plugin does not save the form data of
those elements. If you want the form element data to be saved, do inherit
fromfobi.base.FormFieldPlugin.Hidden form element plugins, should be also having set theis_hiddenproperty to True. By default it’s set to False. That makes the hidden
form elements to be rendered using asdjango.forms.widgets.TextInputwidget in edit mode. In the view mode, the original widget that you
assigned in your form element plugin would be used.There might be cases, when you need to do additional handling of the data upon
the successful form submission. In such cases, you will need to define asubmit_plugin_form_datamethod in the plugin, which accepts the
following arguments:form_entry(fobi.models.FormEntry): Form entry, which is being submitted.request(django.http.HttpRequest): The Django HTTP request.form(django.forms.Form): Form object (a valid one, which contains
thecleaned_dataattribute).form_element_entries(fobi.models.FormElementEntry): Form element entries
for theform_entrygiven.(**)kwargs : Additional arguments.Example (taken from fobi.contrib.plugins.form_elements.fields.file):defsubmit_plugin_form_data(self,form_entry,request,form,form_element_entries=None,**kwargs):"""Submit plugin form data."""# Get the file pathfile_path=form.cleaned_data.get(self.data.name,None)iffile_path:# Handle the uploadsaved_file=handle_uploaded_file(FILES_UPLOAD_DIR,file_path)# Overwrite ``cleaned_data`` of the ``form`` with path to moved# file.form.cleaned_data[self.data.name]="{0}{1}".format(settings.MEDIA_URL,saved_file)# It's critically important to return the ``form`` with updated# ``cleaned_data``returnformIn the example below, the original form is being modified. If you don’t want
the original form to be modified, do not return anything.Check the file form element plugin
(fobi.contrib.plugins.form_elements.fields.file) for complete example.path/to/sample_textarea/forms.pyWhy to have another file for defining forms? Just to keep the code clean and
less messy, although you could perfectly define all your plugin forms in the
modulefobi_form_elements.py, it’s recommended to keep it separate.Take into consideration, thatforms.pyis not an auto-discovered file
pattern. All your form element plugins should be registered in modules namedfobi_form_elements.py.Required imports.fromdjangoimportformsfromfobi.baseimportBasePluginFormForm for forSampleTextareaPluginform element plugin.classSampleTextareaForm(forms.Form,BasePluginForm):"""Sample textarea form."""plugin_data_fields=[("name",""),("label",""),("initial",""),("required",False)]name=forms.CharField(label="Name",required=True)label=forms.CharField(label="Label",required=True)initial=forms.CharField(label="Initial",required=False)required=forms.BooleanField(label="Required",required=False)Note that although it’s not being checked in the code, but for form
field plugins the following fields should be present in the plugin
form (BasePluginForm) and the form plugin (FormFieldPlugin):nameIn some cases, you might want to do something with the data
before it gets saved. For that purpose,save_plugin_datamethod
has been introduced.See the followingexample.defsave_plugin_data(self,request=None):"""Saving the plugin data and moving the file."""file_path=self.cleaned_data.get('file',None)iffile_path:saved_image=handle_uploaded_file(IMAGES_UPLOAD_DIR,file_path)self.cleaned_data['file']=saved_imagepath/to/sample_textarea/widgets.pyRequired imports.fromfobi.baseimportFormElementPluginWidgetDefining the base plugin widget.classBaseSampleTextareaPluginWidget(FormElementPluginWidget):"""Base sample textarea plugin widget."""# Same as ``uid`` value of the ``SampleTextareaPlugin``.plugin_uid="sample_textarea"path/to/sample_layout/fobi_form_elements.pyRegister in the registry (in some module which is for sure to be loaded; it’s
handy to do it in the theme module).Required imports.fromfobi.baseimportform_element_plugin_widget_registryfrompath.to.sample_textarea.widgetsimportBaseSampleTextareaPluginWidgetDefine the theme specific plugin.classSampleTextareaPluginWidget(BaseSampleTextareaPluginWidget):"""Sample textarea plugin widget."""theme_uid='bootstrap3'# Theme for which the widget is loadedmedia_js=['sample_layout/js/fobi.plugins.form_elements.sample_textarea.js',]media_css=['sample_layout/css/fobi.plugins.form_elements.sample_textarea.css',]Register the widget.form_element_plugin_widget_registry.register(SampleTextareaPluginWidget)Form element plugin final stepsNow, that everything is ready, make sure your plugin module is added toINSTALLED_APPS.INSTALLED_APPS=(# ...'path.to.sample_textarea',# ...)Afterwards, go to terminal and type the following command../manage.pyfobi_sync_pluginsIf your HTTP server is running, you would then be able to see the new plugin
in the edit form interface.Dashboard URL:http://127.0.0.1:8000/fobi/Note, that you have to be logged in, in order to use the dashboard. If your
new plugin doesn’t appear, set theFOBI_DEBUGto True in your Django’s
local settings module, re-run your code and check console for error
notifications.Creating a new form handler pluginForm handler plugins handle the form data.django-fobicomes with several
bundled form handler plugins, among which is thedb_storeandmailplugins, which are responsible for saving the submitted form data into the
database and mailing the data to recipients specified. Number of form handlers
in a form is not limited. Certain form handlers are not configurable (for
example thedb_storeform handler isn’t), while others are (mail,http_repost).You should see a form handler as a Django micro app, which could have its’ own
models, admin interface, etc.By default, it’s possible to use a form handler plugin multiple times per form.
If you wish to allow form handler plugin to be used only once in a form,
set theallow_multipleproperty of the plugin to False.As said above,django-fobicomes with several bundled form handler plugins.
Do check the source code as example.Define and register the form handler pluginLet’s name that pluginsample_mail. The plugin directory should then have
the following structure.path/to/sample_mail/
├── __init__.py
├── fobi_form_handlers.py  # Where plugins are defined and registered
└── forms.py  # Plugin configuration formForm handler plugins should be registered in “fobi_form_handlers.py” file.
Each plugin module should be put into theINSTALLED_APPSof your Django
projects’ settings.path/to/sample_mail/fobi_form_handlers.pyA single form handler plugin is registered by its’ UID.Required imports.importjsonfromdjango.core.mailimportsend_mailfromfobi.baseimportFormHandlerPlugin,form_handler_plugin_registryfrompath.to.sample_mail.formsimportSampleMailFormDefining the Sample mail handler plugin.classSampleMailHandlerPlugin(FormHandlerPlugin):"""Sample mail handler plugin."""uid="sample_mail"name=_("Sample mail")form=SampleMailFormdefrun(self,form_entry,request,form,form_element_entries=None):"""To be executed by handler."""send_mail(self.data.subject,json.dumps(form.cleaned_data),self.data.from_email,[self.data.to_email],fail_silently=True)Register the pluginform_handler_plugin_registry.register(SampleMailHandlerPlugin)Some form handlers are configurable, some others not. In order to
have a user friendly way of showing the form handler settings, what’s
sometimes needed, aplugin_data_reprmethod has been introduced.
Simplest implementation of it would look as follows:defplugin_data_repr(self):"""Human readable representation of plugin data.

    :return string:
    """returnself.data.__dict__path/to/sample_mail/forms.pyIf plugin is configurable, it has configuration data. A single form may have
unlimited number of same plugins. Imagine, you want to have different subjects
and additional body texts for different user groups. You could then assign two
form handlermailplugins to the form. Of course, saving the posted form
data many times does not make sense, but it’s up to the user. So, in case if
plugin is configurable, it should have a form.Why to have another file for defining forms? Just to keep the code clean and
less messy, although you could perfectly define all your plugin forms in the
modulefobi_form_handlers.py, it’s recommended to keep it separate.Take into consideration, thatforms.pyis not an auto-discovered file
pattern. All your form handler plugins should be registered in modules namedfobi_form_handlers.py.Required imports.fromdjangoimportformsfromdjango.utils.translationimportgettext_lazyas_fromfobi.baseimportBasePluginFormDefining the form for Sample mail handler plugin.classMailForm(forms.Form,BasePluginForm):"""Mail form."""plugin_data_fields=[("from_name",""),("from_email",""),("to_name",""),("to_email",""),("subject",""),("body",""),]from_name=forms.CharField(label=_("From name"),required=True)from_email=forms.EmailField(label=_("From email"),required=True)to_name=forms.CharField(label=_("To name"),required=True)to_email=forms.EmailField(label=_("To email"),required=True)subject=forms.CharField(label=_("Subject"),required=True)body=forms.CharField(label=_("Body"),required=False,widget=forms.widgets.Textarea)After the plugin has been processed, all its’ data is available in aplugin_instance.datacontainer (for example,plugin_instance.data.subjectorplugin_instance.data.from_name).Prioritise the execution orderSome form handlers shall be executed prior others. A good example of such, is
a combination of “mail” and “db_save” form handlers for the form. In case if
large files are posted, submission of form data would fail if “mail” plugin
would be executed after “db_save” has been executed. That’s why it’s possible
to prioritise that ordering in aFOBI_FORM_HANDLER_PLUGINS_EXECUTION_ORDERsetting variable.If not specified or left empty, form handler plugins would be ran in the order
of discovery. All form handler plugins that are not listed in theFORM_HANDLER_PLUGINS_EXECUTION_ORDER, would be ran after the plugins that
are mentioned there.FORM_HANDLER_PLUGINS_EXECUTION_ORDER=('http_repost','mail',# The 'db_store' is left out intentionally, since it should# be the last plugin to be executed.)Form handler plugin custom actionsBy default, a single form handler plugin has at least a “delete” action.
If plugin is configurable, it gets an “edit” action as well.For some of your plugins, you may want to register a custom action. For
example, the “db_store” plugin does have one, for showing a link to
a listing page with saved form data for the form given.For such cases, define acustom_actionsmethod in your form handler
plugin. That method shall return a list of triples. In each triple,
first value is the URL, second value is the title and the third value
is the icon of the URL.The following example is taken from the “db_store” plugin.defcustom_actions(self):"""Adding a link to view the saved form entries.

    :return iterable:
    """return((reverse('fobi.contrib.plugins.form_handlers.db_store.view_saved_form_data_entries'),_("View entries"),'glyphicon glyphicon-list'),)Form handler plugin final stepsDo not forget to add the form handler plugin module toINSTALLED_APPS.INSTALLED_APPS=(# ...'path.to.sample_mail',# ...)Afterwards, go to terminal and type the following command../manage.pyfobi_sync_pluginsIf your HTTP server is running, you would then be able to see the new plugin
in the edit form interface.Creating a new form importer pluginForm importer plugins import the forms from some external data source intodjango-fobiform format. Number of form importers is not limited. Form
importers are implemented in forms of wizards (since they may contain several
steps).You should see a form importer as a Django micro app, which could have its’ own
models, admin interface, etc.At the momentdjango-fobicomes with only one bundled form handler plugin,
which is themailchimp_importer, which is responsible for importing
existing MailChimp forms intodjango-fobi.Define and register the form importer pluginLet’s name that pluginsample_importer. The plugin directory should then
have the following structure.path/to/sample_importer/
├── templates
│   └── sample_importer
│       ├── 0.html
│       └── 1.html
├── __init__.py
├── fobi_form_importers.py # Where plugins are defined and registered
├── forms.py # Wizard forms
└── views.py # Wizard viewsForm importer plugins should be registered in “fobi_form_importers.py” file.
Each plugin module should be put into theINSTALLED_APPSof your Django
projects’ settings.path/to/sample_importer/fobi_form_importers.pyA single form importer plugin is registered by its’ UID.Required imports.fromdjango.utils.translationimportgettext_lazyas_fromfobi.form_importersimportBaseFormImporter,form_importer_plugin_registryfromfobi.contrib.plugins.form_elementsimportfieldsfrompath.to.sample_importer.viewsimportSampleImporterWizardViewDefining the Sample importer plugin.classSampleImporterPlugin(FormHandlerPlugin):"""Sample importer plugin."""uid='sample_importer'name=_("Sample importer")wizard=SampleImporterWizardViewtemplates=['sample_importer/0.html','sample_importer/1.html',]# field_type (at importer): uid (django-fobi)fields_mapping={# Implemented'email':fields.email.UID,'text':fields.text.UID,'number':fields.integer.UID,'dropdown':fields.select.UID,'date':fields.date.UID,'url':fields.url.UID,'radio':fields.radio.UID,# Transformed into something else'address':fields.text.UID,'zip':fields.text.UID,'phone':fields.text.UID,}# Django standard: remotefield_properties_mapping={'label':'name','name':'tag','help_text':'helptext','initial':'default','required':'req','choices':'choices',}field_type_prop_name='field_type'position_prop_name='order'defextract_field_properties(self,field_data):field_properties={}forprop,valinself.field_properties_mapping.items():ifvalinfield_data:if'choices'==val:field_properties[prop]="\n".join(field_data[val])else:field_properties[prop]=field_data[val]returnfield_propertiesform_importer_plugin_registry.register(SampleImporter)path/to/sample_importer/forms.pyAs mentioned above, form importers are implemented in form of wizards. The
forms are the wizard steps.Required imports.fromdjangoimportformsfromdjango.utils.translationimportgettext_lazyas_fromsample_service_apiimportsample_api# Just an imaginary API clientDefining the form for Sample importer plugin.classSampleImporterStep1Form(forms.Form):"""First form the the wizard."""api_key=forms.CharField(required=True)classSampleImporterStep2Form(forms.Form):"""Second form of the wizard."""list_id=forms.ChoiceField(required=True,choices=[])def__init__(self,*args,**kwargs):self._api_key=Noneif'api_key'inkwargs:self._api_key=kwargs.pop('api_key',None)super(SampleImporterStep2Form,self).__init__(*args,**kwargs)ifself._api_key:client=sample_api.Api(self._api_key)lists=client.lists.list()choices=[(l['id'],l['name'])forlinlists['data']]self.fields['list_id'].choices=choicespath/to/sample_importer/views.pyThe wizard views.Required imports.fromsample_service_apiimportsample_api# Just an imaginary API clientfromdjango.shortcutsimportredirectfromdjango.urlsimportreversefromdjango.contribimportmessagesfromdjango.utils.translationimportgettext_lazyas_# For django LTE 1.8 import from `django.contrib.formtools.wizard.views`fromformtools.wizard.viewsimportSessionWizardViewfrompath.to.sample_importer.formsimport(SampleImporterStep1Form,SampleImporterStep2Form,)Defining the wizard view for Sample importer plugin.classSampleImporterWizardView(SessionWizardView):"""Sample importer wizard view."""form_list=[SampleImporterStep1Form,SampleImporterStep2Form]defget_form_kwargs(self,step):"""Get form kwargs (to be used internally)."""if'1'==step:data=self.get_cleaned_data_for_step('0')or{}api_key=data.get('api_key',None)return{'api_key':api_key}return{}defdone(self,form_list,**kwargs):"""After all forms are submitted."""# Merging cleaned data into one dictcleaned_data={}forforminform_list:cleaned_data.update(form.cleaned_data)# Connecting to sample client APIclient=sample_client.Api(cleaned_data['api_key'])# Fetching the form dataform_data=client.lists.merge_vars(id={'list_id':cleaned_data['list_id']})# We need the first form onlytry:form_data=form_data['data'][0]exceptExceptionaserr:messages.warning(self.request,_('Selected form could not be imported due errors.'))returnredirect(reverse('fobi.dashboard'))# Actually, import the formform_entry=self._form_importer.import_data({'name':form_data['name'],'user':self.request.user},form_data['merge_vars'])redirect_url=reverse('fobi.edit_form_entry',kwargs={'form_entry_id':form_entry.pk})messages.info(self.request,_('Form{0}imported successfully.').format(form_data['name']))returnredirect("{0}".format(redirect_url))Form importer plugin final stepsDo not forget to add the form importer plugin module toINSTALLED_APPS.INSTALLED_APPS=(# ...'path.to.sample_importer',# ...)Afterwards, go to terminal and type the following command../manage.pyfobi_sync_pluginsIf your HTTP server is running, you would then be able to see the new plugin
in the dashboard form interface (implemented in all bundled themes).Creating a form callbackForm callbacks are additional hooks, that are executed on various stages of
the form submission.Let’s place the callback in thefoomodule. The plugin directory should
then have the following structure.path/to/foo/
├── __init__.py
└── fobi_form_callbacks.py # Where callbacks are defined and registeredSee the callback example below.Required imports.fromfobi.constantsimport(CALLBACK_BEFORE_FORM_VALIDATION,CALLBACK_FORM_VALID_BEFORE_SUBMIT_PLUGIN_FORM_DATA,CALLBACK_FORM_VALID,CALLBACK_FORM_VALID_AFTER_FORM_HANDLERS,CALLBACK_FORM_INVALID,)fromfobi.baseimportFormCallback,form_callback_registryDefine and register the callbackclassSampleFooCallback(FormCallback):"""Sample foo callback."""stage=CALLBACK_FORM_VALIDdefcallback(self,form_entry,request,form):"""Define your callback code here."""print("Great! Your form is valid!")form_callback_registry.register(SampleFooCallback)Add the callback module toINSTALLED_APPS.INSTALLED_APPS=(# ...'path.to.foo',# ...)Class-based viewsViewsMigration to class based views is simple. Only your project’surls.pywould change:urlpatterns=[# ...re_path(r'^fobi/',include('fobi.urls.class_based.view')),re_path(r'^fobi/',include('fobi.urls.class_based.edit')),# ...]To use function based views, simply replace the previous line with:urlpatterns=[# ...re_path(r'^fobi/',include('fobi.urls.view')),re_path(r'^fobi/',include('fobi.urls.edit')),# ...]PermissionsClass-based permissions work only in combination with class-based views.Example:fromfobi.permissions.definitionsimportedit_form_entry_permissionsfromfobi.permissions.genericimportBasePermissionfromfobi.permissions.helpersimport(any_permission_required_func,login_required,)classEditFormEntryPermission(BasePermission):"""Permission to edit form entries."""defhas_permission(self,request,view)->bool:returnlogin_required(request)andany_permission_required_func(edit_form_entry_permissions)(request.user)defhas_object_permission(self,request,view,obj)->bool:returnlogin_required(request)andany_permission_required_func(edit_form_entry_permissions)(request.user)andobj.user==request.userAnd then in your view:fromfobi.views.class_basedimportEditFormEntryViewclassMyEditFormEntryView(EditFormEntryView):"""EditFormEntryView."""permission_classes=(EditFormEntryPermission,)SuggestionsCustom action for the formSometimes, you would want to specify a different action for the form.
Although it’s possible to define a custom form action (actionfield
in the “Form properties” tab), you’re advised to use thehttp_repostplugin instead, since then the form would be still validated locally
and only then the valid data, as is, would be sent to the desired
endpoint.Take in mind, that if both cases, if CSRF protection is enabled on
the endpoint, your post request would result an error.When you want to customise too many thingsdjango-fobi, with its’ flexible form elements, form handlers and form
callbacks is very customisable. However, there might be cases when you need to
override entire view to fit your needs. Take a look at theFeinCMS integrationorDjangoCMS integrationas a good example of such. You may also want to compare the code from original
viewfobi.views.view_form_entrywith the code from the widget to get a
better idea of what could be changed in your case. If need a good advice,
just ask me.Themingdjango-fobicomes with theming API. While there are several ready-to-use
themes:“Bootstrap 3” theme“Foundation 5” theme“Simple” theme in (with editing interface in style of the Django admin)“DjangoCMS admin style” theme (which is another simple theme with editing
interface in style ofdjangocms-admin-style)Obviously, there are two sorts of views when it comes to editing and viewing
the form.The “view-view”, when the form as it has been made is exposed to the
site end- users/visitors.The “edit-view” (builder view), where the authorised users build their forms.Both “Bootstrap 3” and “Foundation 5” themes are making use of the same style
for both “view-view” and “edit-view” views.Both “Simple” and “DjangoCMS admin style” themes are styling for the
“edit-view” only. The “view-view” is pretty much blank, as shown on the one
of the screenshots[2.6].Have in mind, that creating a brand new theme could be time consuming.
Instead, you are advised to extend existing themes or in the worst case,
if too much customisation required, create your own themes based on
existing ones (just copy the desired theme to your project directory and
work it out further).It’s possible to use different templates for all “view” and “edit”
actions (see the source code of the “simple” theme). Both “Bootstrap 3” and
“Foundation 5” themes look great. Although if you can’t use any of those,
the “Simple” theme is the best start, since it looks just like django-admin.Create a new themeLet’s place the theme in thesample_thememodule. The theme directory
should then have the following structure.path/to/sample_theme/
├── static
│   ├── css
│   │   └── sample_theme.css
│   └── js
│       └── sample_theme.js
├── templates
│   └── sample_theme
│       ├── _base.html
│       ├── add_form_element_entry.html
│       ├── ...
│       └── view_form_entry_ajax.html
├── __init__.py
├── fobi_form_elements.py
└── fobi_themes.py # Where themes are defined and registeredSee the theme example below.fromdjango.utils.translationimportgettext_lazyas_fromfobi.baseimportBaseTheme,theme_registryclassSampleTheme(BaseTheme):"""Sample theme."""uid='sample'name=_("Sample")media_css=('sample_theme/css/sample_theme.css','css/fobi.core.css',)media_js=('js/jquery-1.10.2.min.js','jquery-ui/js/jquery-ui-1.10.3.custom.min.js','js/jquery.slugify.js','js/fobi.core.js','sample_theme/js/sample_theme.js',)# Form element specificform_element_html_class='form-control'form_radio_element_html_class='radio'form_element_checkbox_html_class='checkbox'form_edit_form_entry_option_class='glyphicon glyphicon-edit'form_delete_form_entry_option_class='glyphicon glyphicon-remove'form_list_container_class='list-inline'# Templatesmaster_base_template='sample_theme/_base.html'base_template='sample_theme/base.html'form_ajax='sample_theme/snippets/form_ajax.html'form_snippet_template_name='sample_theme/snippets/form_snippet.html'form_properties_snippet_template_name='sample_theme/snippets/form_properties_snippet.html'messages_snippet_template_name='sample_theme/snippets/messages_snippet.html'add_form_element_entry_template='sample_theme/add_form_element_entry.html'add_form_element_entry_ajax_template='sample_theme/add_form_element_entry_ajax.html'add_form_handler_entry_template='sample_theme/add_form_handler_entry.html'add_form_handler_entry_ajax_template='sample_theme/add_form_handler_entry_ajax.html'create_form_entry_template='sample_theme/create_form_entry.html'create_form_entry_ajax_template='bootstrap3/create_form_entry_ajax.html'dashboard_template='sample_theme/dashboard.html'edit_form_element_entry_template='sample_theme/edit_form_element_entry.html'edit_form_element_entry_ajax_template='sample_theme/edit_form_element_entry_ajax.html'edit_form_entry_template='sample_theme/edit_form_entry.html'edit_form_entry_ajax_template='sample_theme/edit_form_entry_ajax.html'edit_form_handler_entry_template='sample_theme/edit_form_handler_entry.html'edit_form_handler_entry_ajax_template='sample_theme/edit_form_handler_entry_ajax.html'form_entry_submitted_template='sample_theme/form_entry_submitted.html'form_entry_submitted_ajax_template='sample_theme/form_entry_submitted_ajax.html'view_form_entry_template='sample_theme/view_form_entry.html'view_form_entry_ajax_template='sample_theme/view_form_entry_ajax.html'Registering theSampleThemeplugin.theme_registry.register(SampleTheme)Sometimes you would want to attach additional properties to the theme
in order to use them later in templates (remember, current theme object
is always available in templates under namefobi_theme).For such cases you would need to define a variable in your project’s settings
module, calledFOBI_CUSTOM_THEME_DATA. See the following code as example:# `django-fobi` custom theme data for to be displayed in third party apps# like `django-registraton`.FOBI_CUSTOM_THEME_DATA={'bootstrap3':{'page_header_html_class':'','form_html_class':'form-horizontal','form_button_outer_wrapper_html_class':'control-group','form_button_wrapper_html_class':'controls','form_button_html_class':'btn','form_primary_button_html_class':'btn-primary pull-right',},'foundation5':{'page_header_html_class':'','form_html_class':'form-horizontal','form_button_outer_wrapper_html_class':'control-group','form_button_wrapper_html_class':'controls','form_button_html_class':'radius button','form_primary_button_html_class':'btn-primary',},'simple':{'page_header_html_class':'','form_html_class':'form-horizontal','form_button_outer_wrapper_html_class':'control-group','form_button_wrapper_html_class':'submit-row','form_button_html_class':'btn','form_primary_button_html_class':'btn-primary',}}You would now be able to access the defined extra properties in templates
as shown below.<divclass="{{ fobi_theme.custom_data.form_button_wrapper_html_class }}">You likely would want to either remove the footer text or change it. Define
a variable in your project’s settings module, calledFOBI_THEME_FOOTER_TEXT.
See the following code as example:FOBI_THEME_FOOTER_TEXT=gettext('&copy; django-fobi example site 2014')Below follow the properties of the theme:base_editbase_viewThere are generic templates made in order to simplify theming. Some
of them you would never need to override. Some others, you would likely
want to.Templates that you likely would want to re-write in your custom
theme implementation are marked with three asterisks (***):generic
├── snippets
│   ├── form_ajax.html
│   ├── form_edit_ajax.html
│   ├── *** form_properties_snippet.html
│   ├── *** form_snippet.html
│   ├── --- form_edit_snippet.html (does not exist in generic templates)
│   ├── --- form_view_snippet.html (does not exist in generic templates)
│   ├── form_view_ajax.html
│   └── messages_snippet.html
│
├── _base.html
├── add_form_element_entry.html
├── add_form_element_entry_ajax.html
├── add_form_handler_entry.html
├── add_form_handler_entry_ajax.html
├── base.html
├── create_form_entry.html
├── create_form_entry_ajax.html
├── *** dashboard.html
├── edit_form_element_entry.html
├── edit_form_element_entry_ajax.html
├── edit_form_entry.html
├── *** edit_form_entry_ajax.html
├── edit_form_handler_entry.html
├── edit_form_handler_entry_ajax.html
├── form_entry_submitted.html
├── *** form_entry_submitted_ajax.html
├── *** theme.html
├── view_form_entry.html
└── view_form_entry_ajax.htmlFrom all of the templates listed above, the _base.html template is
the most influenced by the Bootstrap 3 theme.Make changes to an existing themeAs said above, making your own theme from scratch could be costly. Instead,
you can override/reuse an existing one and change it to your needs with
minimal efforts. See theoverride simple themeexample. In order to see it in action, run the project withsettings_override_simple_themeoption:./manage.pyrunserver--settings=settings_override_simple_themeDetails explained below.Directory structureoverride_simple_theme/
├── static
│   └── override_simple_theme
│       ├── css
│       │   └── override-simple-theme.css
│       └── js
│           └── override-simple-theme.js
│
├── templates
│   └── override_simple_theme
│       ├── snippets
│       │   └── form_ajax.html
│       └── base_view.html
├── __init__.py
└── fobi_themes.py # Where themes are defined and registeredfobi_themes.pyOverriding the “simple” theme.__all__=('MySimpleTheme',)fromfobi.baseimporttheme_registryfromfobi.contrib.themes.simple.fobi_themesimportSimpleThemeclassMySimpleTheme(SimpleTheme):"""My simple theme, inherited from `SimpleTheme` theme."""html_classes=['my-simple-theme',]base_view_template='override_simple_theme/base_view.html'form_ajax='override_simple_theme/snippets/form_ajax.html'Register the overridden theme. Note, that it’s important to set theforceargument to True, in order to override the original theme. Force can be
applied only once (for an overridden element).theme_registry.register(MySimpleTheme,force=True)templates/override_simple_theme/base_view.html{% extends "simple/base_view.html" %}

{% load static %}

{% block stylesheets %}<linkhref="{% static 'override_simple_theme/css/override-simple-theme.css' %}"rel="stylesheet"media="all"/>{% endblock stylesheets %}

{% block main-wrapper %}<divid="sidebar"><h2>It's easy to override a theme!</h2></div>{{ block.super }}
{% endblock main-wrapper %}templates/override_simple_theme/snippets/form_ajax.html{% extends "fobi/generic/snippets/form_ajax.html" %}

{% block form_html_class %}basic-grey{% endblock %}Form wizardsBasicsWith form wizards you can split forms across multiple pages. State is
maintained in one of the backends (at the moment the Session backend). Data
processing is delayed until the submission of the final form.Indjango-fobiwizards work in the following way:Number of forms in a form wizard is not limited.Form callbacks, handlers are totally ignored in form wizards. Instead,
the form-wizard specific handlers (form wizard handlers) take over handling
of the form data on the final step.Bundled form wizard handler pluginsBelow a short overview of the form wizard handler plugins. See the
README.rst file in directory of each plugin for details.DB store:
Stores form data in a database.HTTP repost:
Repost the POST request to another endpoint.Mail:
Send the form data by email.Mail the sender:
Send the form data by email to the sender (submitter) of the form.Integration with third-party apps and frameworksdjango-fobihas been successfully integrated into a number of diverse
third-party apps and frameworks, such as: Django REST framework, Django CMS,
FeinCMS, Mezzanine and Wagtail.Certainly, integration into CMS is one case, integration into REST framework -
totally another. In REST frameworks we no longer have forms as such. Context
is very different. Handling of form data should obviously happen in a
different way. Assembling of the form class isn’t enough (in case of Django
REST framework we assemble the serializer class).In order to handle such level of integration, two additional sort of plugins
have been introduced:IntegrationFormElementPluginIntegrationFormHandlerPluginThese plugins are in charge of representation of the form elements in a
proper way for the package to be integrated and handling the submitted form
data.Additional documentationis available in the sub-package.SampleIntegrationFormElementPluginSample is taken fromhere.base.pyDefine the form element plugin.fromdjango.utils.translationimportgettext_lazyas_fromrest_framework.fieldsimportEmailFieldfromfobi.baseimportIntegrationFormFieldPluginfromfobi.contrib.apps.drf_integrationimportUIDasINTEGRATE_WITH_UIDfromfobi.contrib.apps.drf_integration.baseimport(DRFIntegrationFormElementPluginProcessor,DRFSubmitPluginFormDataMixin,)fromfobi.contrib.apps.drf_integration.form_elements.fields.emailimportUIDclassEmailInputPlugin(IntegrationFormFieldPlugin,DRFSubmitPluginFormDataMixin):"""EmailField plugin."""uid=UIDintegrate_with=INTEGRATE_WITH_UIDname=_("Decimal")group=_("Fields")defget_custom_field_instances(self,form_element_plugin,request=None,form_entry=None,form_element_entries=None,**kwargs):"""Get form field instances."""field_kwargs={'required':form_element_plugin.data.required,'initial':form_element_plugin.data.initial,'label':form_element_plugin.data.label,'help_text':form_element_plugin.data.help_text,'max_length':form_element_plugin.data.max_length,}return[DRFIntegrationFormElementPluginProcessor(field_class=EmailField,field_kwargs=field_kwargs)]fobi_integration_form_elements.pyRegister the plugin. Note the name patternfobi_integration_form_elements.fromfobi.baseimportintegration_form_element_plugin_registryfrom.baseimportEmailInputPluginintegration_form_element_plugin_registry.register(EmailInputPlugin)Don’t forget to list your plugin in theINSTALLED_APPSafterwards.SampleIntegrationFormHandlerPluginSample is taken fromhere.base.pyDefine the form handler plugin.importloggingfrommimetypesimportguess_typeimportosfromdjango.confimportsettingsfromdjango.utils.translationimportgettext_lazyas_fromfobi.baseimportIntegrationFormHandlerPluginfromfobi.helpersimportextract_file_pathfromfobi.contrib.apps.drf_integrationimportUIDasINTEGRATE_WITH_UIDfromfobi.contrib.apps.drf_integration.baseimportget_processed_serializer_datafrom.importUIDclassMailHandlerPlugin(IntegrationFormHandlerPlugin):"""Mail handler form handler plugin.

    Can be used only once per form.
    """uid=UIDname=_("Mail")integrate_with=INTEGRATE_WITH_UIDdefrun(self,form_handler_plugin,form_entry,request,form_element_entries=None,**kwargs):"""Run."""base_url=form_handler_plugin.get_base_url(request)serializer=kwargs['serializer']# Clean up the values, leave our content fields and empty values.field_name_to_label_map,cleaned_data=get_processed_serializer_data(serializer,form_element_entries)rendered_data=form_handler_plugin.get_rendered_data(serializer.validated_data,field_name_to_label_map,base_url)files=self._prepare_files(request,serializer)form_handler_plugin.send_email(rendered_data,files)def_prepare_files(self,request,serializer):"""Prepares the files for being attached to the mail message."""files={}defprocess_path(file_path,imf):"""Processes the file path and the file."""iffile_path:file_path=file_path.replace(settings.MEDIA_URL,os.path.join(settings.MEDIA_ROOT,''))mime_type=guess_type(imf.name)files[field_name]=(imf.name,''.join([cforcinimf.chunks()]),mime_type[0]ifmime_typeelse'')forfield_name,imfinrequest.FILES.items():try:file_path=serializer.validated_data.get(field_name,'')process_path(file_path,imf)exceptExceptionaserr:file_path=extract_file_path(imf.name)process_path(file_path,imf)returnfilesfobi_integration_form_handlers.pyRegister the plugin. Note the name patternfobi_integration_form_handlers.fromfobi.baseimportintegration_form_handler_plugin_registryfrom.baseimportMailHandlerPluginintegration_form_handler_plugin_registry.register(MailHandlerPlugin)Don’t forget to list your plugin in theINSTALLED_APPSafterwards.PermissionsPlugin system allows administrators to specify the access rights to every
plugin.django-fobipermissions are based on Django Users and User Groups.
Access rights are manageable via Django admin (“/admin/fobi/formelement/”,
“/admin/fobi/formhandler/”). If user doesn’t have the rights to access plugin,
it doesn’t appear on his form even if has been added to it (imagine, you have
once granted the right to use the news plugin to all users, but later on
decided to limit it to Staff members group only). Note, that superusers have
access to all plugins.Plugin access rights management interface in Django admin

┌──────────────────────────┬───────────────────────┬───────────────────────┐
│ `Plugin`                 │ `Users`               │ `Groups`              │
├──────────────────────────┼───────────────────────┼───────────────────────┤
│ Text                     │ John Doe              │ Form builder users    │
├──────────────────────────┼───────────────────────┼───────────────────────┤
│ Textarea                 │                       │ Form builder users    │
├──────────────────────────┼───────────────────────┼───────────────────────┤
│ File                     │ Oscar, John Doe       │ Staff members         │
├──────────────────────────┼───────────────────────┼───────────────────────┤
│ URL                      │                       │ Form builder users    │
├──────────────────────────┼───────────────────────┼───────────────────────┤
│ Hidden                   │                       │ Form builder users    │
└──────────────────────────┴───────────────────────┴───────────────────────┘Management commandsThere are several management commands available.fobi_find_broken_entries. Find broken form element/handler entries that
occur when some plugin which did exist in the system, no longer exists.fobi_sync_plugins. Should be ran each time a new plugin is being added to
thedjango-fobi.fobi_update_plugin_data. A mechanism to update existing plugin data in
case if it had become invalid after a change in a plugin. In order for it
to work, each plugin should implement andupdatemethod, in which the
data update happens.TuningThere are number ofdjango-fobisettings you can override in the settings
module of your Django project:FOBI_RESTRICT_PLUGIN_ACCESS(bool): If set to True, (Django) permission
system for dash plugins is enabled. Defaults to True. Setting this to False
makes all plugins available for all users.FOBI_DEFAULT_THEME(str): Active (default) theme UID. Defaults to
“bootstrap3”.FORM_HANDLER_PLUGINS_EXECUTION_ORDER(list of tuples): Order in which the
form handlers are executed. See the “Prioritise the execution order”
section for details.For tuning of specific contrib plugin, see the docs in the plugin directory.Bundled plugins and themesdjango-fobiships with number of bundled form element- and form handler-
plugins, as well as themes which are ready to be used as is.Bundled form element pluginsBelow a short overview of the form element plugins. See the README.rst file
in directory of each plugin for details.FieldsFields marked with asterisk (*) fall under the definition of text elements.
It’s possible to provideDynamic initial valuesfor text elements.Boolean (checkbox)Checkbox select multipleDateDate drop down (year, month, day selection drop-downs)DateTimeDecimalDurationEmail*Email repeat*FileFloatHidden*Hidden model objectInputIntegerIP address*Null booleanPassword*Radio select (radio button)Range selectRegexSelect (drop-down)Select model object (drop-down)Select multiple (drop-down)Select multiple model objects (drop-down)Select multiple with max (drop-down)SliderSlug*Text*Textarea*TimeURL*Content/presentationContent plugins are presentational plugins, that make your forms look more
complete and content rich.Content image:
Insert an image.Content image URL:
Insert an image URL.Content markdown:
Add markdown text.Content richtext:
Add rich text (based ondjango-ckeditorpackage).Content text:
Add text.Content video:
Add an embed YouTube or Vimeo video.SecurityCAPTCHA:
CAPTCHA integration, requiresdjango-simple-captchapackage.ReCAPTCHA:
CAPTCHA integration, requiresdjango-recaptchapackage.Invisible ReCAPTCHA:
Google invisible reCAPTCHA integration, with no additional dependencies.Honeypot:Anti-spam honeypotfield.MPTT fieldsSelect MPTT model object (drop-down)Select multiple MPTT model objects (drop-down)TestTest plugins are made for dev purposes only.Dummy:
Solely for dev purposes.Bundled form handler pluginsBelow a short overview of the form handler plugins. See the README.rst file
in directory of each plugin for details.DB store:
Stores form data in a database.HTTP repost:
Repost the POST request to another endpoint.Mail:
Send the form data by email.Mail the sender:
Send the form data by email to the sender.Bundled themesBelow a short overview of the themes. See the README.rst file in directory
of each theme for details.Bootstrap 3:
Bootstrap 3 theme.Foundation 5:
Foundation 5 theme.Simple:
Basic theme with form editing is in a style of Django admin.DjangoCMS admin style:
Basic theme with form editing is in a style ofdjangocms-admin-style.Third-party plugins and themesList of remarkable third-party plugins:fobi-phonenumber- A Fobi
PhoneNumber form field plugin. Makes use of thephonenumber_field.formfields.PhoneNumberFieldandphonenumber_field.widgets.PhoneNumberPrefixWidget.HTML5 fieldsThe following HTML5 fields are supported in corresponding bundled plugins:datedatetimeemailmaxminnumberurlplaceholdertypeWith thefobi.contrib.plugins.form_elements.fields.inputsupport for
HTML5 fields is extended to the following fields:autocompleteautofocuslistmultiplepatternstepLoading initial data using GET argumentsIt’s possible to provide initial data for the form using the GET arguments.In that case, along with the field values, you should be providing
an additional argument named “fobi_initial_data”, which doesn’t have to
hold a value. For example, if your form contains of fields named “email” and
“age” and you want to provide initial values for those using GET arguments, you
should be constructing your URL to the form as follows:http://127.0.0.1:8001/fobi/view/test-form/?fobi_initial_data&email=test@example.com&age=19Dynamic initial valuesIt’s possible to provide a dynamic initial value for any of the text elements.
In order to do that, you should use the build-in context processor or make
your own one. The only requirement is that you should store all values that
should be exposed in the form as a dict forfobi_dynamic_valuesdictionary
key. Beware, that passing the original request object might be unsafe in
many ways. Currently, a stripped down version of the request object is being
passed as a context variable.TEMPLATES=[{# ...'OPTIONS':{# ...'context_processors':[# ..."fobi.context_processors.theme",# Important!"fobi.context_processors.dynamic_values",# Optional]},},]defdynamic_values(request):return{'fobi_dynamic_values':{'request':StrippedRequest(request),'now':datetime.datetime.now(),'today':datetime.date.today(),}}In your GUI, you should be referring to the initial values in the following
way:{{ request.path }} {{ now }} {{ today }}Note, that you should not provide thefobi_dynamic_values.as a prefix.
Currently, the following variables are available in thefobi.context_processors.dynamic_valuescontext processor:- request: Stripped HttpRequest object.

    - request.path: A string representing the full path to the requested
      page, not including the scheme or domain.

    - request.get_full_path(): Returns the path, plus an appended query
      string, if applicable.

    - request.is_secure():  Returns True if the request is secure; that
      is, if it was made with HTTPS.

    - request.is_ajax(): Returns True if the request was made via an
      XMLHttpRequest, by checking the HTTP_X_REQUESTED_WITH header for the
      string 'XMLHttpRequest'.

    - request.META: A stripped down standard Python dictionary containing
      the available HTTP headers.

        - HTTP_ACCEPT_ENCODING: Acceptable encodings for the response.

        - HTTP_ACCEPT_LANGUAGE: Acceptable languages for the response.

        - HTTP_HOST: The HTTP Host header sent by the client.

        - HTTP_REFERER: The referring page, if any.

        - HTTP_USER_AGENT: The client’s user-agent string.

        - QUERY_STRING: The query string, as a single (un-parsed) string.

        - REMOTE_ADDR: The IP address of the client.

    - request.user: Authenticated user.

        - request.user.email:

        - request.user.get_username(): Returns the username for the user.
          Since the User model can be swapped out, you should use this
          method instead of referencing the username attribute directly.

        - request.user.get_full_name(): Returns the first_name plus the
          last_name, with a space in between.

        - request.user.get_short_name(): Returns the first_name.

        - request.user.is_anonymous():

- now: datetime.datetime.now()

- today: datetime.date.today()Submitted form element plugins valuesWhile some values of form element plugins are submitted as is, some others
need additional processing. There are 3 types of behaviour taken into
consideration:“val”: value is being sent as is.“repr”: (human readable) representation of the value is used.“mix”: mix of value as is and human readable representation.The following plugins have been made configurable in such a way, that
developers can choose the desired behaviour in projects’ settings:FOBI_FORM_ELEMENT_CHECKBOX_SELECT_MULTIPLE_SUBMIT_VALUE_ASFOBI_FORM_ELEMENT_RADIO_SUBMIT_VALUE_ASFOBI_FORM_ELEMENT_SELECT_SUBMIT_VALUE_ASFOBI_FORM_ELEMENT_SELECT_MULTIPLE_SUBMIT_VALUE_ASFOBI_FORM_ELEMENT_SELECT_MODEL_OBJECT_SUBMIT_VALUE_ASFOBI_FORM_ELEMENT_SELECT_MULTIPLE_MODEL_OBJECTS_SUBMIT_VALUE_ASSee the README.rst in each of the following plugins for more information.Checkbox select multiple (multiple checkboxes)Radio select (radio button)Select (drop-down)Select model object (drop-down)Select MPTT model object (drop-down)Select multiple (drop-down)Select multiple model objects (drop-down)Select multiple MPTT model objects (drop-down)Rendering forms using third-party librariesYou might want to render your forms using third-party libraries such asdjango-crispy-forms,django-floppyformsor
other alternatives.For that purpose you should override the “snippets/form_snippet.html” used
by the theme you have chosen. Your template would then look similar to the
one below (make sure to setup/configure your third-party form rendering library
prior doing this).Usingdjango-crispy-forms{% load crispy_forms_tags fobi_tags %}

{% block form_non_field_and_hidden_errors %}
    {% get_form_hidden_fields_errors form as form_hidden_fields_errors %}
    {% if form.non_field_errors or form_hidden_fields_errors %}
        {% include fobi_theme.form_non_field_and_hidden_errors_snippet_template %}
    {% endif %}
{% endblock form_non_field_and_hidden_errors %}

{% crispy form %}Usingdjango-floppyforms{% load floppyforms fobi_tags %}

{% block form_non_field_and_hidden_errors %}
    {% get_form_hidden_fields_errors form as form_hidden_fields_errors %}
    {% if form.non_field_errors or form_hidden_fields_errors %}
        {% include fobi_theme.form_non_field_and_hidden_errors_snippet_template %}
    {% endif %}
{% endblock form_non_field_and_hidden_errors %}

{% form form %}See how it’s done in theoverride simple themeexample.Import/export formsThere might be cases when you havedjango-fobirunning on multiple instances
and have already spend some time on making forms on one of the instances,
and want to reuse those forms on another. You could of course re-create entire
form in the GUI, but we can do better than that. It’s possible to export forms
into JSON format and import the exported forms again. It’s preferable that
you run both instances on the same versions ofdjango-fobi, otherwise imports
might break (although it might just work). There many ways to deal with
missing plugin errors, but the chosen strategy (which you don’t yet have full
control of) is safest (import everything possible, but warn user about errors).
If both instances have the same set of form element and form handler plugins
imports should go smoothly. It is though possible to make an import ignoring
missing form element and form handler plugins. You would get an appropriate
notice about that, but import will continue leaving the broken plugin data out.TranslationsAvailable translationsEnglish is the primary language. The following translations are
available (core and plugins)DutchGermanRussianFrenchOverriding translationsThere might be cases when you want to override certain translations. It’s
easily achievable with introduction of custom locale paths in your project.See the following as a good example of overriding some English plugin labels.custom settingscustom locales directoryRun the example project as follows:cdexamples/simple/./manage.pyrunserver--settings=settings.alternative_labelsIn the example given, “Boolean” and “Checkbox select multiple” plugin names
are renamed to “Checkbox” and “Multiple checkboxes” respectively.All built-in pluginnamevalues are almost equivalent to the pluginuidvalues. By default plugins are sorted byuidvalue. When you override thenameof the plugin, sorting breaks. Therefore, it’s recommended to
set theFOBI_SORT_PLUGINS_BY_VALUEvalue to True in your settings module.
Default value is False, which means that plugins are sorted by theiruidvalue.FOBI_SORT_PLUGINS_BY_VALUE=TrueDebuggingBy default debugging is turned off. It means that broken form entries, which
are entries with broken data, that are not possible to be shown, are just
skipped. That’s safe in production. Although, you for sure would want to
see the broken entries in development. Set theFOBI_DEBUGto True
in thesettings.pyof your project in order to do so.Most of the errors are logged (DEBUG). If you have written a plugin and it
somehow doesn’t appear in the list of available plugins, do run the following
management command since it not only syncs your plugins into the database,
but also is a great way of checking for possible errors../manage.pyfobi_sync_pluginsRun the following command in order to identify the broken plugins../manage.pyfobi_find_broken_entriesIf you have forms referring to form element- of form handler- plugins
that are currently missing (not registered, removed, failed to load - thus
there would be a risk that your form would’t be rendered properly/fully and
the necessary data handling wouldn’t happen either) you will get an
appropriate exception. Although it’s fine to get an instant error message about
such failures in development, in production is wouldn’t look appropriate.
Thus, there are two settings related to the non-existing (not-found) form
element- and form handler- plugins.FOBI_DEBUG: Set this to True in your development environment anyway. Watch
error logs closely.FOBI_FAIL_ON_MISSING_FORM_ELEMENT_PLUGINS: If you want no error to be
shown in case of missing form element plugins, set this to False in
your settings module. Default value is True.FOBI_FAIL_ON_MISSING_FORM_HANDLER_PLUGINS: If you want no error to be
shown in case of missing form element handlers, set this to False in
your settings module. Default value is True.TestingProject is covered by test (functional- and browser-tests).NoteYou are recommended to use Postgres or MySQL for testing. Tests
occasionally fail on SQLite due to very intensive IO and SQLite table
locking.To test with all supported Python/Django versions type:toxTo test against specific environment, type:tox-epy37-django21To test just your working environment type:./runtests.pyTo run a single test class in a given test module in your working environment type:./runtests.pysrc/fobi/tests/test_browser_build_dynamic_forms.py::FobiBrowserBuldDynamicFormsTest-k"test_2004_submit_form"It’s assumed that you have all the requirements installed. If not, first
install the test requirements:pipinstall-rexamples/requirements/test.txtBrowser testsFor browser tests you may choose between Firefox, headless Firefox and
PhantomJS. PhantomJS is faster, headless Firefox is fast as well, but
normal Firefox tests tell you more (as you see what exactly happens on the
screen). Both cases require some effort and both have disadvantages regarding
the installation (although once you have them installed they work perfect).Latest versions of Firefox are often not supported by Selenium. Current
version of the Selenium for Python (2.53.6) works fine with Firefox 47.
Thus, instead of using system Firefox you could better use a custom one.For PhantomJS you need to have NodeJS installed.Set up ChromeDriverDownload and install ChromeDriver version matching your browser:pipinstallget-chromedriver-pyget-chromedriver-pySpecify the full path to your ChromeDriver inCHROME_DRIVER_EXECUTABLE_PATHsetting. Example:fromchromedriver_pyimportbinary_pathasCHROME_DRIVER_EXECUTABLE_PATHAfter that your Selenium tests would work.Set up Firefox 47Download Firefox 47 fromthislocation and unzip it into/usr/lib/firefox47/Specify the full path to your Firefox inFIREFOX_BIN_PATHsetting. Example:FIREFOX_BIN_PATH='/usr/lib/firefox47/firefox'If you set to use system Firefox, remove or comment-out theFIREFOX_BIN_PATHsetting.After that your Selenium tests would work.Set up headless FirefoxInstallxvfbpackage which is used to start Firefox in headless mode.sudoapt-getinstallxvfbRun the tests using headless Firefox../scripts/runtests.shOr run tox tests using headless Firefox../scripts/tox.shOr run specific tox tests using headless Firefox../scripts/tox.sh-epy36-django111Setup PhantomJSYou could also run tests in headless mode (faster). For that you will need
PhantomJS.Install PhantomJS and dependencies.curl-sLhttps://deb.nodesource.com/setup_6.x-onodesource_setup.shsudobashnodesource_setup.shsudoapt-getinstallnodejssudoapt-getinstallbuild-essentiallibssl-devsudonpm-ginstallphantomjs-prebuiltSpecify thePHANTOM_JS_EXECUTABLE_PATHsetting. Example:PHANTOM_JS_EXECUTABLE_PATH=""If you want to use Firefox for testing, remove or comment-out thePHANTOM_JS_EXECUTABLE_PATHsetting.Writing documentationKeep the following hierarchy.=====
title
=====

header
======

sub-header
----------

sub-sub-header
~~~~~~~~~~~~~~

sub-sub-sub-header
##################

sub-sub-sub-sub-header
^^^^^^^^^^^^^^^^^^^^^^

sub-sub-sub-sub-sub-header
++++++++++++++++++++++++++TroubleshootingIf you get aFormElementPluginDoesNotExistor aFormHandlerPluginDoesNotExistexception, make sure you have listed your
plugin in thesettingsmodule of your project.ContributingIf you want to contribute to the library, but don’t know where to start,
do check theopen issues where help is appreciatedor ask theAuthorhow you could help.LicenseGPL-2.0-only OR LGPL-2.1-or-laterSupportFor any security issues contact me at the e-mail given in theAuthorsection.
For overall issues, go toGitHub.AuthorArtur Barseghyan <artur.barseghyan@gmail.com>ScreenshotsBootstrap3 themeDashboard[1.1]DashboardCreate a form[1.2]Create a formView/edit formForm elements[1.3]Edit form - form elements tab active, no elements yet[1.4]Edit form - form elements tab active, add a form element menu[1.5]Edit form - add a form element (URL plugin)[1.6]Edit form - form elements tab active, with form elementsForm handlers[1.7]Edit form - form handlers tab active, no handlers yet[1.8]Edit form - form handlers tab tactive, add form handler menu[1.9]Edit form - add a form handler (Mail plugin)[1.10]Edit form - form handlers tab active, with form handlers[1.11]Edit form - form properties tab active[1.12]View form[1.13]View form - form submitted (thanks page)[1.14]Edit form - add a form element (Video plugin)[1.15]Edit form - add a form element (Boolean plugin)[1.16]Edit form[1.17]View formSimple themeView/edit form[2.1]Edit form - form elements tab active, with form elements[2.2]Edit form - form elements tab active, add a form element menu[2.3]Edit form - add a form element (Hidden plugin)[2.4]Edit form - form handlers tab active, with form handlers[2.5]Edit form - form properties tab active[2.6]View form