=========== django-fobi =========== `django-fobi` (or just `fobi`) is a customisable, modular, user- and developer- friendly form generator/builder application for Django. With `fobi` you 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). Prerequisites ============= Present ------- - Django 1.8, 1.9, 1.10 and 1.11. - Python 2.7, 3.4, 3.5, 3.6 and PyPy. Past ---- - Dropping support of Django 1.5, 1.6 has been announced in version 0.9.13. Dropping support of Django 1.7 has been announced in version 0.9.17. As of 0.9.17 everything is still backwards compatible with versions 1.5, 1.6 and 1.7, but in future versions compatibility with these versions will be wiped out. - Dropping support of Python 2.6 has been announced in version 0.9.17. As of 0.9.17 everything is still backwards compatible with Python 2.6, but in future versions compatibility with it will be wiped out. - Since version 0.10.4 support for Python 3.3 has been dropped. Key concepts ============ - Each form consists of elements. Form elements are divided into two groups: (a) form fields (input field, textarea, hidden field, file field, etc.). (b) content (presentational) elements (text, image, embed video, 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, that `django-fobi` does not require django-admin and administrative rights/permissions to access the UI, although almost seamless integration with django-admin is implemented through the ``simple`` theme. Main features and highlights ============================ - User-friendly GUI to quickly build forms. - Large variety of `Bundled form element plugins`_. Most of the Django fields are supported. `HTML5 fields`_ are 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. - Anti-spam solutions like `CAPTCHA `_, `ReCAPTCHA `_ or `Honeypot `_ come out of the box (CAPTCHA and ReCAPTCHA do require additional third-party apps to be installed). - 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 the `Bundled form handler plugins`_ for 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. - `Theming`_. There are 4 ready to use `Bundled 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 of `djangocms-admin-style `_). - Implemented `integration with Django REST framework `_. - Implemented `integration with FeinCMS `_ (in a form of a FeinCMS page widget). - Implemented `integration with DjangoCMS `_ (in a form of a DjangoCMS page plugin). - Implemented `integration with Mezzanine `_ (in a form of a Mezzanine page). - Reordering of form elements using drag-n-drop. - Data export (`DB store `_ form handler plugin) into XLS/CSV format. - `Dynamic initial values`_ for form elements. - Import/export forms to/from JSON format. - Import forms from MailChimp using `mailchimp importer `_. Roadmap ======= Some of the upcoming/in-development features/improvements are: - Wagtail integration (in version 0.12). - Bootstrap 4 and Foundation 6 support (in version 0.13). See the `TODOS `_ for the full list of planned-, pending- in-development- or to-be-implemented features. Some screenshots ================ See the documentation for some screen shots: - `PythonHosted `_ - `ReadTheDocs `_ Demo ==== Live demo --------- See the `live demo app `_ on Heroku. Additionally, see the `Django REST framework integration demo `_. Credentials: - username: test_user - password: test_user Run demo locally ---------------- In order to be able to quickly evaluate the ``django-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 latest ``django_fobi_example_app_installer.sh``: .. code-block:: sh wget https://raw.github.com/barseghyanartur/django-fobi/stable/examples/django_fobi_example_app_installer.sh Assign execute rights to the installer and run the `django_fobi_example_app_installer.sh`: .. code-block:: sh chmod +x django_fobi_example_app_installer.sh ./django_fobi_example_app_installer.sh Open your browser and test the app. Dashboard: - URL: http://127.0.0.1:8001/fobi/ - Admin username: test_admin - Admin password: test Django admin interface: - URL: http://127.0.0.1:8001/admin/ - Admin username: test_admin - Admin password: test If quick installer doesn't work for you, see the manual steps on running the `example project `_. Quick start =========== See the `quick start `_. Installation ============ (1) Install latest stable version from PyPI: .. code-block:: sh pip install django-fobi Or latest stable version from GitHub: .. code-block:: sh pip install https://github.com/barseghyanartur/django-fobi/archive/stable.tar.gz Or latest stable version from BitBucket: .. code-block:: sh pip install https://bitbucket.org/barseghyanartur/django-fobi/get/stable.tar.gz (2) Add `fobi` to ``INSTALLED_APPS`` of the your projects' Django settings. Furthermore, all themes and plugins to be used, shall be added to the ``INSTALLED_APPS`` as well. Note, that if a plugin has additional dependencies, you should be mentioning those in the ``INSTALLED_APPS`` as well. .. code-block:: python 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', # Other project specific apps 'foo', # Test app # ... ) (3) Make appropriate changes to the ``TEMPLATE_CONTEXT_PROCESSORS`` of the your projects' Django settings. And the following to the context processors. .. code-block:: python TEMPLATE_CONTEXT_PROCESSORS = ( # ... "fobi.context_processors.theme", # ... ) Make sure that ``django.core.context_processors.request`` is in ``TEMPLATE_CONTEXT_PROCESSORS`` too. (4) Configure URLs Add the following line to urlpatterns of your `urls` module. .. code-block:: python # View URLs url(r'^fobi/', include('fobi.urls.view')), # Edit URLs url(r'^fobi/', include('fobi.urls.edit')), Note, that some plugins require additional URL includes. For instance, if you listed the ``fobi.contrib.plugins.form_handlers.db_store`` form handler plugin in the ``INSTALLED_APPS``, you should mention the following in ``urls`` module. .. code-block:: python # DB Store plugin URLs url(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 plugin ================================== Form 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 from ``fobi.base.FormElementPlugin``. The rest (real form elements, that are supposed to have a value) are inherited from ``fobi.base.FormFieldPlugin``. You should see a form element plugin as a Django micro app, which could have its' own models, admin interface, etc. `django-fobi` comes 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 plugin ``sample_textarea``. The plugin directory should then have the following structure. .. code-block:: sh 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 defined Form element plugins should be registered in "fobi_form_elements.py" file. Each plugin module should be put into the ``INSTALLED_APPS`` of your Django projects' settings. In some cases, you would need plugin specific overridable settings (see ``fobi.contrib.form_elements.fields.content.content_image`` plugin as an example). You are advised to write your settings in such a way, that variables of your Django project settings module would have `FOBI_PLUGIN_` prefix. Define and register the form element plugin ------------------------------------------- Step by step review of a how to create and register a plugin and plugin widgets. Note, that `django-fobi` auto-discovers your plugins if you place them into a file named ``fobi_form_elements.py`` of any Django app listed in ``INSTALLED_APPS`` of your Django projects' settings module. path/to/sample_textarea/fobi_form_elements.py ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A single form element plugin is registered by its' UID. Required imports. .. code-block:: python from django import forms from fobi.base import FormFieldPlugin, form_element_plugin_registry from path.to.sample_textarea.forms import SampleTextareaForm Defining the Sample textarea plugin. .. code-block:: python class SampleTextareaPlugin(FormFieldPlugin): """Sample textarea plugin.""" uid = "sample_textarea" name = "Sample Textarea" form = SampleTextareaForm group = "Samples" # Group to which the plugin belongs to def get_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 the ``SampleTextareaPlugin`` plugin. .. code-block:: python form_element_plugin_registry.register(SampleTextareaPlugin) Note, that in case you want to define a pure presentational element, make use of ``fobi.base.FormElementPlugin`` for subclassing, instead of ``fobi.base.FormFieldPlugin``. See the source of the content plugins (fobi.contrib.plugins.form_elements.content) as a an example. For instance, the ``captcha`` and ``honeypot`` fields are implemented as form elements (subclasses the ``fobi.base.FormElementPlugin``). The ``db_store`` form handler plugin does not save the form data of those elements. If you want the form element data to be saved, do inherit from ``fobi.base.FormFieldPlugin``. Hidden form element plugins, should be also having set the ``is_hidden`` property to True. By default it's set to False. That makes the hidden form elements to be rendered using as ``django.forms.widgets.TextInput`` widget 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 a ``submit_plugin_form_data`` method 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 the ``cleaned_data`` attribute). - `form_element_entries` (fobi.models.FormElementEntry): Form element entries for the `form_entry` given. - (**)kwargs : Additional arguments. Example (taken from fobi.contrib.plugins.form_elements.fields.file): .. code-block:: python def submit_plugin_form_data(self, form_entry, request, form, form_element_entries=None, **kwargs): """Submit plugin form data.""" # Get the file path file_path = form.cleaned_data.get(self.data.name, None) if file_path: # Handle the upload saved_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`` return form In 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.py ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 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 module ``fobi_form_elements.py``, it's recommended to keep it separate. Take into consideration, that ``forms.py`` is not an auto-discovered file pattern. All your form element plugins should be registered in modules named ``fobi_form_elements.py``. Required imports. .. code-block:: python from django import forms from fobi.base import BasePluginForm Form for for ``SampleTextareaPlugin`` form element plugin. .. code-block:: python class SampleTextareaForm(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``): - name In some cases, you might want to do something with the data before it gets saved. For that purpose, ``save_plugin_data`` method has been introduced. See the following `example `_. .. code-block:: python def save_plugin_data(self, request=None): """Saving the plugin data and moving the file.""" file_path = self.cleaned_data.get('file', None) if file_path: saved_image = handle_uploaded_file(IMAGES_UPLOAD_DIR, file_path) self.cleaned_data['file'] = saved_image path/to/sample_textarea/widgets.py ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Required imports. .. code-block:: python from fobi.base import FormElementPluginWidget Defining the base plugin widget. .. code-block:: python class BaseSampleTextareaPluginWidget(FormElementPluginWidget): """Base sample textarea plugin widget.""" # Same as ``uid`` value of the ``SampleTextareaPlugin``. plugin_uid = "sample_textarea" path/to/sample_layout/fobi_form_elements.py ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Register 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. .. code-block:: python from fobi.base import form_element_plugin_widget_registry from path.to.sample_textarea.widgets import BaseSampleTextareaPluginWidget Define the theme specific plugin. .. code-block:: python class SampleTextareaPluginWidget(BaseSampleTextareaPluginWidget): """Sample textarea plugin widget.""" theme_uid = 'bootstrap3' # Theme for which the widget is loaded media_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. .. code-block:: python form_element_plugin_widget_registry.register(SampleTextareaPluginWidget) Form element plugin final steps ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now, that everything is ready, make sure your plugin module is added to ``INSTALLED_APPS``. .. code-block:: python INSTALLED_APPS = ( # ... 'path.to.sample_textarea', # ... ) Afterwards, go to terminal and type the following command. .. code-block:: sh ./manage.py fobi_sync_plugins If 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 the ``FOBI_DEBUG`` to True in your Django's local settings module, re-run your code and check console for error notifications. Creating a new form handler plugin ================================== Form handler plugins handle the form data. `django-fobi` comes with several bundled form handler plugins, among which is the ``db_store`` and ``mail`` plugins, 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 the ``db_store`` form 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 the ``allow_multiple`` property of the plugin to False. As said above, `django-fobi` comes with several bundled form handler plugins. Do check the source code as example. Define and register the form handler plugin ------------------------------------------- Let's name that plugin ``sample_mail``. The plugin directory should then have the following structure. .. code-block:: text path/to/sample_mail/ ├── __init__.py ├── fobi_form_handlers.py # Where plugins are defined and registered └── forms.py # Plugin configuration form Form handler plugins should be registered in "fobi_form_handlers.py" file. Each plugin module should be put into the ``INSTALLED_APPS`` of your Django projects' settings. path/to/sample_mail/fobi_form_handlers.py ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A single form handler plugin is registered by its' UID. Required imports. .. code-block:: python import json from django.core.mail import send_mail from fobi.base import FormHandlerPlugin, form_handler_plugin_registry from path.to.sample_mail.forms import SampleMailForm Defining the Sample mail handler plugin. .. code-block:: python class SampleMailHandlerPlugin(FormHandlerPlugin): """Sample mail handler plugin.""" uid = "sample_mail" name = _("Sample mail") form = SampleMailForm def run(self, form_entry, request, form): """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 ) 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, a ``plugin_data_repr`` method has been introduced. Simplest implementation of it would look as follows: .. code-block:: python def plugin_data_repr(self): """Human readable representation of plugin data. :return string: """ return self.data.__dict__ path/to/sample_mail/forms.py ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If 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 handler ``mail`` plugins 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 module ``fobi_form_handlers.py``, it's recommended to keep it separate. Take into consideration, that ``forms.py`` is not an auto-discovered file pattern. All your form handler plugins should be registered in modules named ``fobi_form_handlers.py``. Required imports. .. code-block:: python from django import forms from django.utils.translation import ugettext_lazy as _ from fobi.base import BasePluginForm Defining the form for Sample mail handler plugin. .. code-block:: python class MailForm(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 a ``plugin_instance.data`` container (for example, ``plugin_instance.data.subject`` or ``plugin_instance.data.from_name``). Prioritise the execution order ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Some 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 a ``FOBI_FORM_HANDLER_PLUGINS_EXECUTION_ORDER`` setting 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 the ``FORM_HANDLER_PLUGINS_EXECUTION_ORDER``, would be ran after the plugins that are mentioned there. .. code-block:: python 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 actions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By 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 a ``custom_actions`` method 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. .. code-block:: python def custom_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 steps ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Do not forget to add the form handler plugin module to ``INSTALLED_APPS``. .. code-block:: python INSTALLED_APPS = ( # ... 'path.to.sample_mail', # ... ) Afterwards, go to terminal and type the following command. .. code-block:: sh ./manage.py fobi_sync_plugins If 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 plugin =================================== Form importer plugins import the forms from some external data source into `django-fobi` form 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 moment `django-fobi` comes with only one bundled form handler plugin, which is the ``mailchimp_importer``, which is responsible for importing existing MailChimp forms into `django-fobi`. Define and register the form importer plugin -------------------------------------------- Let's name that plugin ``sample_importer``. The plugin directory should then have the following structure. .. code-block:: text 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 views Form importer plugins should be registered in "fobi_form_importers.py" file. Each plugin module should be put into the ``INSTALLED_APPS`` of your Django projects' settings. path/to/sample_importer/fobi_form_importers.py ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A single form importer plugin is registered by its' UID. Required imports. .. code-block:: python from django.utils.translation import ugettext_lazy as _ from fobi.form_importers import BaseFormImporter, form_importer_plugin_registry from fobi.contrib.plugins.form_elements import fields from path.to.sample_importer.views import SampleImporterWizardView Defining the Sample importer plugin. .. code-block:: python class SampleImporterPlugin(FormHandlerPlugin): """Sample importer plugin.""" uid = 'sample_importer' name = _("Sample importer) wizard = SampleImporterWizardView templates = [ '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: remote field_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' def extract_field_properties(self, field_data): field_properties = {} for prop, val in self.field_properties_mapping.items(): if val in field_data: if 'choices' == val: field_properties[prop] = "\n".join(field_data[val]) else: field_properties[prop] = field_data[val] return field_properties form_importer_plugin_registry.register(SampleImporter) path/to/sample_importer/forms.py ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As mentioned above, form importers are implemented in form of wizards. The forms are the wizard steps. Required imports. .. code-block:: python from django import forms from django.utils.translation import ugettext_lazy as _ from sample_service_api import sample_api # Just an imaginary API client Defining the form for Sample importer plugin. .. code-block:: python class SampleImporterStep1Form(forms.Form): """First form the the wizard.""" api_key = forms.CharField(required=True) class SampleImporterStep2Form(forms.Form): """Second form of the wizard.""" list_id = forms.ChoiceField(required=True, choices=[]) def __init__(self, *args, **kwargs): self._api_key = None if 'api_key' in kwargs: self._api_key = kwargs.pop('api_key', None) super(SampleImporterStep2Form, self).__init__(*args, **kwargs) if self._api_key: client = sample_api.Api(self._api_key) lists = client.lists.list() choices = [(l['id'], l['name']) for l in lists['data']] self.fields['list_id'].choices = choices path/to/sample_importer/views.py ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The wizard views. Required imports. .. code-block:: python from sample_service_api import sample_api # Just an imaginary API client from django.shortcuts import redirect from django.core.urlresolvers import reverse from django.contrib import messages from django.utils.translation import ugettext_lazy as _ # For django LTE 1.8 import from `django.contrib.formtools.wizard.views` from formtools.wizard.views import SessionWizardView from path.to.sample_importer.forms import ( SampleImporterStep1Form, SampleImporterStep2Form, ) Defining the wizard view for Sample importer plugin. .. code-block:: python class SampleImporterWizardView(SessionWizardView): """Sample importer wizard view.""" form_list = [SampleImporterStep1Form, SampleImporterStep2Form] def get_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 {} def done(self, form_list, **kwargs): """After all forms are submitted.""" # Merging cleaned data into one dict cleaned_data = {} for form in form_list: cleaned_data.update(form.cleaned_data) # Connecting to sample client API client = sample_client.Api(cleaned_data['api_key']) # Fetching the form data form_data = client.lists.merge_vars( id={'list_id': cleaned_data['list_id']} ) # We need the first form only try: form_data = form_data['data'][0] except Exception as err: messages.warning( self.request, _('Selected form could not be imported due errors.') ) return redirect(reverse('fobi.dashboard')) # Actually, import the form form_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']) ) return redirect("{0}".format(redirect_url)) Form importer plugin final steps ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Do not forget to add the form importer plugin module to ``INSTALLED_APPS``. .. code-block:: python INSTALLED_APPS = ( # ... 'path.to.sample_importer', # ... ) Afterwards, go to terminal and type the following command. .. code-block:: sh ./manage.py fobi_sync_plugins If 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 callback ======================== Form callbacks are additional hooks, that are executed on various stages of the form submission. Let's place the callback in the ``foo`` module. The plugin directory should then have the following structure. .. code-block:: text path/to/foo/ ├── __init__.py └── fobi_form_callbacks.py # Where callbacks are defined and registered See the callback example below. Required imports. .. code-block:: python from fobi.constants import ( CALLBACK_BEFORE_FORM_VALIDATION, CALLBACK_FORM_VALID_BEFORE_SUBMIT_PLUGIN_FORM_DATA, CALLBACK_FORM_VALID, CALLBACK_FORM_VALID_AFTER_FORM_HANDLERS, CALLBACK_FORM_INVALID ) from fobi.base import FormCallback, form_callback_registry Define and register the callback .. code-block:: python class SampleFooCallback(FormCallback): """Sample foo callback.""" stage = CALLBACK_FORM_VALID def callback(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 to ``INSTALLED_APPS``. .. code-block:: python INSTALLED_APPS = ( # ... 'path.to.foo', # ... ) Suggestions =========== Custom action for the form -------------------------- Sometimes, you would want to specify a different action for the form. Although it's possible to define a custom form action (``action`` field in the "Form properties" tab), you're advised to use the ``http_repost`` plugin 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 things ------------------------------------------ `django-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 the `FeinCMS integration `_ or `DjangoCMS integration `_ as a good example of such. You may also want to compare the code from original view ``fobi.views.view_form_entry`` with 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. Theming ======= `django-fobi` comes 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 of ``djangocms-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 theme ------------------ Let's place the theme in the ``sample_theme`` module. The theme directory should then have the following structure. .. code-block:: text 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 registered See the theme example below. .. code-block:: python from django.utils.translation import ugettext_lazy as _ from fobi.base import BaseTheme, theme_registry class SampleTheme(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 specific form_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' # Templates master_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 the ``SampleTheme`` plugin. .. code-block:: python 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 name ``fobi_theme``). For such cases you would need to define a variable in your project's settings module, called ``FOBI_CUSTOM_THEME_DATA``. See the following code as example: .. code-block:: python # `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. .. code-block:: html
You likely would want to either remove the footer text or change it. Define a variable in your project's settings module, called ``FOBI_THEME_FOOTER_TEXT``. See the following code as example: .. code-block:: python FOBI_THEME_FOOTER_TEXT = gettext('© django-fobi example site 2014') Below follow the properties of the theme: - ``base_edit`` - ``base_view`` There 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 (\*\*\*): .. code-block:: text 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.html From all of the templates listed above, the _base.html template is the most influenced by the Bootstrap 3 theme. Make changes to an existing theme --------------------------------- As 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 the `override simple theme `_ example. In order to see it in action, run the project with `settings_override_simple_theme `_ option: .. code-block:: sh ./manage.py runserver --settings=settings_override_simple_theme Details explained below. Directory structure ~~~~~~~~~~~~~~~~~~~ .. code-block:: text override_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 registered fobi_themes.py ~~~~~~~~~~~~~~ Overriding the "simple" theme. .. code-block:: python __all__ = ('MySimpleTheme',) from fobi.base import theme_registry from fobi.contrib.themes.simple.fobi_themes import SimpleTheme class MySimpleTheme(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 the `force` argument to True, in order to override the original theme. Force can be applied only once (for an overridden element). .. code-block:: python theme_registry.register(MySimpleTheme, force=True) templates/override_simple_theme/base_view.html ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: html {% extends "simple/base_view.html" %} {% load static %} {% block stylesheets %} {% endblock stylesheets %} {% block main-wrapper %} {{ block.super }} {% endblock main-wrapper %} templates/override_simple_theme/snippets/form_ajax.html ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: html {% extends "fobi/generic/snippets/form_ajax.html" %} {% block form_html_class %}basic-grey{% endblock %} Form wizards ============ Basics ------ With 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. In `django-fobi` wizards 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 plugins ----------------------------------- Below 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. Integration with third-party apps and frameworks ================================================ `django-fobi` has been successfully integrated into a number of diverse third-party apps and frameworks, such as: Django REST framework, Django CMS, FeinCMS and Mezzanine. 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: - IntegrationFormElementPlugin - IntegrationFormHandlerPlugin These 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 documentation `_ is available in the sub-package. Sample `IntegrationFormElementPlugin` ------------------------------------- Sample is taken from `here `__. base.py ~~~~~~~ Define the form element plugin. .. code-block:: python from django.utils.translation import ugettext_lazy as _ from rest_framework.fields import EmailField from fobi.base import IntegrationFormFieldPlugin from fobi.contrib.apps.drf_integration import UID as INTEGRATE_WITH_UID from fobi.contrib.apps.drf_integration.base import ( DRFIntegrationFormElementPluginProcessor, DRFSubmitPluginFormDataMixin, ) from fobi.contrib.apps.drf_integration.form_elements.fields.email import UID class EmailInputPlugin(IntegrationFormFieldPlugin, DRFSubmitPluginFormDataMixin): """EmailField plugin.""" uid = UID integrate_with = INTEGRATE_WITH_UID name = _("Decimal") group = _("Fields") def get_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.py ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Register the plugin. Note the name pattern `fobi_integration_form_elements`. .. code-block:: python from fobi.base import integration_form_element_plugin_registry from .base import EmailInputPlugin integration_form_element_plugin_registry.register(EmailInputPlugin) Don't forget to list your plugin in the ``INSTALLED_APPS`` afterwards. Sample `IntegrationFormHandlerPlugin` ------------------------------------- Sample is taken from `here `__. base.py ~~~~~~~ Define the form handler plugin. .. code-block:: python import logging from mimetypes import guess_type import os from django.conf import settings from django.utils.translation import ugettext_lazy as _ from fobi.base import IntegrationFormHandlerPlugin from fobi.helpers import extract_file_path from fobi.contrib.apps.drf_integration import UID as INTEGRATE_WITH_UID from fobi.contrib.apps.drf_integration.base import get_processed_serializer_data from . import UID class MailHandlerPlugin(IntegrationFormHandlerPlugin): """Mail handler form handler plugin. Can be used only once per form. """ uid = UID name = _("Mail") integrate_with = INTEGRATE_WITH_UID def run(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 = {} def process_path(file_path, imf): """Processes the file path and the file.""" if file_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([c for c in imf.chunks()]), mime_type[0] if mime_type else '' ) for field_name, imf in request.FILES.items(): try: file_path = serializer.validated_data.get(field_name, '') process_path(file_path, imf) except Exception as err: file_path = extract_file_path(imf.name) process_path(file_path, imf) return files fobi_integration_form_handlers.py ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Register the plugin. Note the name pattern `fobi_integration_form_handlers`. .. code-block:: python from fobi.base import integration_form_handler_plugin_registry from .base import MailHandlerPlugin integration_form_handler_plugin_registry.register(MailHandlerPlugin) Don't forget to list your plugin in the ``INSTALLED_APPS`` afterwards. Permissions =========== Plugin system allows administrators to specify the access rights to every plugin. `django-fobi` permissions 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. .. code-block:: text 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 commands =================== There 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 the `django-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 and ``update`` method, in which the data update happens. Tuning ====== There are number of `django-fobi` settings 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 themes ========================== `django-fobi` ships 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 plugins ---------------------------- Below a short overview of the form element plugins. See the README.rst file in directory of each plugin for details. Fields ~~~~~~ Fields marked with asterisk (*) fall under the definition of text elements. It's possible to provide `Dynamic initial values`_ for text elements. - `Boolean (checkbox) `_ - `Date `_ - `DateTime `_ - `Date drop down (year, month, day selection drop-downs) `_ - `Decimal `_ - `Duration `_ - `Email* `_ - `File `_ - `Float `_ - `Hidden*