{ "info": { "author": "Artur Barseghyan", "author_email": "artur.barseghyan@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", "License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7" ], "description": "===========\ndjango-fobi\n===========\n`django-fobi` (or just `fobi`) is a customisable, modular, user- and developer-\nfriendly form generator/builder application for Django. With `fobi` you can\nbuild Django forms using an intuitive GUI, save or mail posted form data or\neven export forms into JSON format and import them on other instances. API\nallows you to build your own form elements and form handlers (mechanisms for\nhandling the submitted form data).\n\n.. image:: https://img.shields.io/pypi/v/django-fobi.svg\n :target: https://pypi.python.org/pypi/django-fobi\n :alt: PyPI Version\n\n.. image:: https://img.shields.io/travis/barseghyanartur/django-fobi/master.svg\n :target: http://travis-ci.org/barseghyanartur/django-fobi\n :alt: Build Status\n\n.. image:: https://img.shields.io/badge/license-GPL--2.0--only%20OR%20LGPL--2.1--or--later-blue.svg\n :target: https://github.com/barseghyanartur/django-fobi/#License\n :alt: GPL-2.0-only OR LGPL-2.1-or-later\n\nPrerequisites\n=============\n- Django 1.8, 1.9, 1.10, 1.11, 2.0, 2.1 and 2.2.\n- Python 2.7, 3.4, 3.5, 3.6 and 3.7.\n\nKey concepts\n============\n- Each form consists of elements. Form elements are divided into groups:\n\n (a) form fields (input field, textarea, hidden field, file field, etc.).\n (b) content (presentational) elements (text, image, embed video, etc.).\n (c) security elements (captcha, etc).\n\n- Number of form elements is not limited.\n- Each form may contain handlers. Handler processes the form data (for example,\n saves it or mails it). Number of the handlers is not limited.\n- Both form elements and form handlers are made with Django permission system\n in mind.\n- As an addition to form handlers, form callbacks are implemented. Form\n callbacks are fired on various stages of pre- and post-processing the form\n data (on POST). Form callbacks do not make use of permission system (unless\n you intentionally do so in the code of your callback) and are fired for all\n forms (unlike form handlers, that are executed only if assigned).\n- Each plugin (form element or form handler) or a callback - is a Django\n micro-app.\n- In addition for form element and form handler plugins, integration form\n element and integration form handler plugins are implemented for integration\n with diverse third-party apps and frameworks (such as Django REST framework).\n\nNote, that `django-fobi` does not require django-admin and administrative\nrights/permissions to access the UI, although almost seamless integration with\ndjango-admin is implemented through the ``simple`` theme.\n\nMain features and highlights\n============================\n- User-friendly GUI to quickly build forms.\n- Large variety of `Bundled form element plugins`_. Most of the Django fields\n are supported. `HTML5 fields`_ are supported as well.\n- `Form wizards`_. Combine your forms into wizards. Form wizards may contain\n handlers. Handler processes the form wizard data (for example, saves it or\n mails it). Number of the form wizard handlers is not limited.\n- Forms can be automatically enabled/disabled based on dates (start date, end\n date).\n- Anti-spam solutions like `CAPTCHA\n `_,\n `ReCAPTCHA\n `_,\n `Honeypot\n `_\n or `Invisible reCAPTCHA\n `__\n come out of the box (CAPTCHA and ReCAPTCHA do require additional third-party\n apps to be installed; Invisible reCAPTCHA doesn't).\n- In addition to standard form elements, there are cosmetic (presentational)\n form elements (for adding a piece of text, image or a embed video)\n alongside standard form elements.\n- Data handling in plugins (form handlers). Save the data, mail it to some\n address or re-post it to some other endpoint. See the\n `Bundled form handler plugins`_ for more information.\n- Developer-friendly API, which allows to edit existing or build new form\n fields and handlers without touching the core.\n- Support for custom user model.\n- `Theming`_. There are 4 ready to use `Bundled themes`_: \"Bootstrap 3\",\n \"Foundation 5\", \"Simple\" (with editing interface in style of Django admin)\n and \"DjangoCMS admin style\" theme (which is another simple theme with editing\n interface in style of `djangocms-admin-style\n `_).\n- Implemented `integration with Django REST framework\n `_.\n- Implemented `integration with Wagtail\n `_\n (in a form of a Wagtail page).\n- Implemented `integration with FeinCMS\n `_\n (in a form of a FeinCMS page widget).\n- Implemented `integration with DjangoCMS\n `_\n (in a form of a DjangoCMS page plugin).\n- Implemented `integration with Mezzanine\n `_\n (in a form of a Mezzanine page).\n- Reordering of form elements using drag-n-drop.\n- Data export (`DB store\n `_\n form handler plugin) into XLS/CSV format.\n- `Dynamic initial values`_ for form elements.\n- Import/export forms to/from JSON format.\n- Import forms from MailChimp using `mailchimp importer\n `_.\n\nRoadmap\n=======\nSome of the upcoming/in-development features/improvements are:\n\n- Implement disabling forms based on dates.\n- Class based views.\n- Cloning of forms.\n- JSON schema support.\n- Webpack integration.\n- Improved Django REST framework OPTIONS.\n- Bootstrap 4 support.\n- Foundation 6 support.\n\nSee the `TODOS\n`_\nfor the full list of planned-, pending- in-development- or to-be-implemented\nfeatures.\n\nSome screenshots\n================\nSee the documentation for some screen shots:\n\n- `ReadTheDocs `_\n\nDemo\n====\nLive demo\n---------\nSee the `live demo app `_ on Heroku.\nAdditionally, see the `Django REST framework integration demo\n`_.\n\nCredentials:\n\n- username: test_user\n- password: test_user\n\nRun demo locally\n----------------\nIn order to be able to quickly evaluate the ``django-fobi``, a demo app (with a\nquick installer) has been created (works on Ubuntu/Debian, may work on other\nLinux systems as well, although not guaranteed). Follow the instructions below\nfor having the demo running within a minute.\n\nGrab the latest ``django_fobi_example_app_installer.sh``:\n\n.. code-block:: sh\n\n wget https://raw.github.com/barseghyanartur/django-fobi/stable/examples/django_fobi_example_app_installer.sh\n\nAssign execute rights to the installer and run the\n`django_fobi_example_app_installer.sh`:\n\n.. code-block:: sh\n\n chmod +x django_fobi_example_app_installer.sh\n ./django_fobi_example_app_installer.sh\n\nOpen your browser and test the app.\n\nDashboard:\n\n- URL: http://127.0.0.1:8001/fobi/\n- Admin username: test_admin\n- Admin password: test\n\nDjango admin interface:\n\n- URL: http://127.0.0.1:8001/admin/\n- Admin username: test_admin\n- Admin password: test\n\nIf quick installer doesn't work for you, see the manual steps on running the\n`example project\n`_.\n\nQuick start\n===========\nSee the `quick start `_.\n\nInstallation\n============\n\n(1) Install latest stable version from PyPI:\n\n.. code-block:: sh\n\n pip install django-fobi\n\nOr latest stable version from GitHub:\n\n.. code-block:: sh\n\n pip install https://github.com/barseghyanartur/django-fobi/archive/stable.tar.gz\n\nOr latest stable version from BitBucket:\n\n.. code-block:: sh\n\n pip install https://bitbucket.org/barseghyanartur/django-fobi/get/stable.tar.gz\n\n(2) Add `fobi` to ``INSTALLED_APPS`` of the your projects' Django settings.\n Furthermore, all themes and plugins to be used, shall be added to the\n ``INSTALLED_APPS`` as well. Note, that if a plugin has additional\n dependencies, you should be mentioning those in the ``INSTALLED_APPS``\n as well.\n\n.. code-block:: python\n\n INSTALLED_APPS = (\n # Used by fobi\n 'django.contrib.auth',\n 'django.contrib.contenttypes',\n 'django.contrib.sessions',\n 'django.contrib.sites',\n 'django.contrib.messages',\n 'django.contrib.staticfiles',\n 'django.contrib.admin',\n\n # ...\n # `django-fobi` core\n 'fobi',\n\n # `django-fobi` themes\n 'fobi.contrib.themes.bootstrap3', # Bootstrap 3 theme\n 'fobi.contrib.themes.foundation5', # Foundation 5 theme\n 'fobi.contrib.themes.simple', # Simple theme\n\n # `django-fobi` form elements - fields\n 'fobi.contrib.plugins.form_elements.fields.boolean',\n 'fobi.contrib.plugins.form_elements.fields.checkbox_select_multiple',\n 'fobi.contrib.plugins.form_elements.fields.date',\n 'fobi.contrib.plugins.form_elements.fields.date_drop_down',\n 'fobi.contrib.plugins.form_elements.fields.datetime',\n 'fobi.contrib.plugins.form_elements.fields.decimal',\n 'fobi.contrib.plugins.form_elements.fields.email',\n 'fobi.contrib.plugins.form_elements.fields.file',\n 'fobi.contrib.plugins.form_elements.fields.float',\n 'fobi.contrib.plugins.form_elements.fields.hidden',\n 'fobi.contrib.plugins.form_elements.fields.input',\n 'fobi.contrib.plugins.form_elements.fields.integer',\n 'fobi.contrib.plugins.form_elements.fields.ip_address',\n 'fobi.contrib.plugins.form_elements.fields.null_boolean',\n 'fobi.contrib.plugins.form_elements.fields.password',\n 'fobi.contrib.plugins.form_elements.fields.radio',\n 'fobi.contrib.plugins.form_elements.fields.regex',\n 'fobi.contrib.plugins.form_elements.fields.select',\n 'fobi.contrib.plugins.form_elements.fields.select_model_object',\n 'fobi.contrib.plugins.form_elements.fields.select_multiple',\n 'fobi.contrib.plugins.form_elements.fields.select_multiple_model_objects',\n 'fobi.contrib.plugins.form_elements.fields.slug',\n 'fobi.contrib.plugins.form_elements.fields.text',\n 'fobi.contrib.plugins.form_elements.fields.textarea',\n 'fobi.contrib.plugins.form_elements.fields.time',\n 'fobi.contrib.plugins.form_elements.fields.url',\n\n # `django-fobi` form elements - content elements\n 'fobi.contrib.plugins.form_elements.test.dummy',\n 'easy_thumbnails', # Required by `content_image` plugin\n 'fobi.contrib.plugins.form_elements.content.content_image',\n 'fobi.contrib.plugins.form_elements.content.content_image_url',\n 'fobi.contrib.plugins.form_elements.content.content_text',\n 'fobi.contrib.plugins.form_elements.content.content_video',\n\n # `django-fobi` form handlers\n 'fobi.contrib.plugins.form_handlers.db_store',\n 'fobi.contrib.plugins.form_handlers.http_repost',\n 'fobi.contrib.plugins.form_handlers.mail',\n 'fobi.contrib.plugins.form_handlers.mail_sender',\n\n # Other project specific apps\n 'foo', # Test app\n # ...\n )\n\n(3) Make appropriate changes to the ``TEMPLATES`` of the your projects'\n Django settings.\n\nAnd ``fobi.context_processors.theme`` and\n``fobi.context_processors.dynamic_values``. See the following example.\n\n.. code-block:: python\n\n TEMPLATES = [\n {\n 'BACKEND': 'django.template.backends.django.DjangoTemplates',\n 'DIRS': [(os.path.join('path', 'to', 'your', 'templates'))],\n 'OPTIONS': {\n 'context_processors': [\n \"django.template.context_processors.debug\",\n 'django.template.context_processors.request',\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n \"fobi.context_processors.theme\", # Important!\n \"fobi.context_processors.dynamic_values\", # Optional\n ],\n 'loaders': [\n 'django.template.loaders.filesystem.Loader',\n 'django.template.loaders.app_directories.Loader',\n 'admin_tools.template_loaders.Loader',\n ],\n 'debug': DEBUG_TEMPLATE,\n }\n },\n ]\n\nMake sure that ``django.core.context_processors.request`` is in\n``context_processors`` too.\n\n(4) Configure URLs\n\nAdd the following line to urlpatterns of your `urls` module.\n\n.. code-block:: python\n\n # View URLs\n url(r'^fobi/', include('fobi.urls.view')),\n\n # Edit URLs\n url(r'^fobi/', include('fobi.urls.edit')),\n\nNote, that some plugins require additional URL includes. For instance, if you\nlisted the ``fobi.contrib.plugins.form_handlers.db_store`` form handler plugin\nin the ``INSTALLED_APPS``, you should mention the following in ``urls``\nmodule.\n\n.. code-block:: python\n\n # DB Store plugin URLs\n url(r'^fobi/plugins/form-handlers/db-store/',\n include('fobi.contrib.plugins.form_handlers.db_store.urls')),\n\nView URLs are put separately from edit URLs in order to make it possible\nto prefix the edit URLs differently. For example, if you're using the\n\"Simple\" theme, you would likely want to prefix the edit URLs with \"admin/\"\nso that it looks more like django-admin.\n\nCreating a new form element plugin\n==================================\nForm element plugins represent the elements of which the forms is made:\nInputs, checkboxes, textareas, files, hidden fields, as well as pure\npresentational elements (text or image). Number of form elements in a form\nis not limited.\n\nPresentational form elements are inherited from ``fobi.base.FormElementPlugin``.\n\nThe rest (real form elements, that are supposed to have a value)\nare inherited from ``fobi.base.FormFieldPlugin``.\n\nYou should see a form element plugin as a Django micro app, which could have\nits' own models, admin interface, etc.\n\n`django-fobi` comes with several bundled form element plugins. Do check the\nsource code as example.\n\nLet's say, you want to create a textarea form element plugin.\n\nThere are several properties, each textarea should have. They are:\n\n- `label` (string): HTML label of the textarea.\n- `name` (string): HTML name of the textarea.\n- `initial` (string): Initial value of the textarea.\n- `required` (bool): Flag, which tells us whether the field is required or\n optional.\n\nLet's name that plugin ``sample_textarea``. The plugin directory should then\nhave the following structure.\n\n.. code-block:: sh\n\n path/to/sample_textarea/\n \u251c\u2500\u2500 __init__.py\n \u251c\u2500\u2500 fobi_form_elements.py # Where plugins are defined and registered\n \u251c\u2500\u2500 forms.py # Plugin configuration form\n \u2514\u2500\u2500 widgets.py # Where plugins widgets are defined\n\nForm element plugins should be registered in \"fobi_form_elements.py\" file. Each\nplugin module should be put into the ``INSTALLED_APPS`` of your Django\nprojects' settings.\n\nIn some cases, you would need plugin specific overridable settings (see\n``fobi.contrib.form_elements.fields.content.content_image`` plugin as an\nexample). You are advised to write your settings in such a way, that variables\nof your Django project settings module would have `FOBI_PLUGIN_` prefix.\n\nDefine and register the form element plugin\n-------------------------------------------\nStep by step review of a how to create and register a plugin and plugin\nwidgets. Note, that `django-fobi` auto-discovers your plugins if you place\nthem into a file named ``fobi_form_elements.py`` of any Django app listed in\n``INSTALLED_APPS`` of your Django projects' settings module.\n\npath/to/sample_textarea/fobi_form_elements.py\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nA single form element plugin is registered by its' UID.\n\nRequired imports.\n\n.. code-block:: python\n\n from django import forms\n from fobi.base import FormFieldPlugin, form_element_plugin_registry\n from path.to.sample_textarea.forms import SampleTextareaForm\n\nDefining the Sample textarea plugin.\n\n.. code-block:: python\n\n class SampleTextareaPlugin(FormFieldPlugin):\n \"\"\"Sample textarea plugin.\"\"\"\n\n uid = \"sample_textarea\"\n name = \"Sample Textarea\"\n form = SampleTextareaForm\n group = \"Samples\" # Group to which the plugin belongs to\n\n def get_form_field_instances(self,\n request=None,\n form_entry=None,\n form_element_entries=None,\n **kwargs):\n kwargs = {\n 'required': self.data.required,\n 'label': self.data.label,\n 'initial': self.data.initial,\n 'widget': forms.widgets.Textarea(attrs={})\n }\n\n return [(self.data.name, forms.CharField, kwargs),]\n\nRegistering the ``SampleTextareaPlugin`` plugin.\n\n.. code-block:: python\n\n form_element_plugin_registry.register(SampleTextareaPlugin)\n\nNote, that in case you want to define a pure presentational element, make use\nof ``fobi.base.FormElementPlugin`` for subclassing, instead of\n``fobi.base.FormFieldPlugin``.\nSee the source of the content plugins\n(fobi.contrib.plugins.form_elements.content) as a an example.\n\nFor instance, the ``captcha`` and ``honeypot`` fields are implemented\nas form elements (subclasses the ``fobi.base.FormElementPlugin``). The\n``db_store`` form handler plugin does not save the form data of\nthose elements. If you want the form element data to be saved, do inherit\nfrom ``fobi.base.FormFieldPlugin``.\n\nHidden form element plugins, should be also having set the ``is_hidden``\nproperty to True. By default it's set to False. That makes the hidden\nform elements to be rendered using as ``django.forms.widgets.TextInput``\nwidget in edit mode. In the view mode, the original widget that you\nassigned in your form element plugin would be used.\n\nThere might be cases, when you need to do additional handling of the data upon\nthe successful form submission. In such cases, you will need to define a\n``submit_plugin_form_data`` method in the plugin, which accepts the\nfollowing arguments:\n\n- `form_entry` (fobi.models.FormEntry): Form entry, which is being submitted.\n- `request` (django.http.HttpRequest): The Django HTTP request.\n- `form` (django.forms.Form): Form object (a valid one, which contains\n the ``cleaned_data`` attribute).\n- `form_element_entries` (fobi.models.FormElementEntry): Form element entries\n for the `form_entry` given.\n- (**)kwargs : Additional arguments.\n\nExample (taken from fobi.contrib.plugins.form_elements.fields.file):\n\n.. code-block:: python\n\n def submit_plugin_form_data(self,\n form_entry,\n request,\n form,\n form_element_entries=None,\n **kwargs):\n \"\"\"Submit plugin form data.\"\"\"\n # Get the file path\n file_path = form.cleaned_data.get(self.data.name, None)\n if file_path:\n # Handle the upload\n saved_file = handle_uploaded_file(FILES_UPLOAD_DIR, file_path)\n # Overwrite ``cleaned_data`` of the ``form`` with path to moved\n # file.\n form.cleaned_data[self.data.name] = \"{0}{1}\".format(\n settings.MEDIA_URL, saved_file\n )\n\n # It's critically important to return the ``form`` with updated\n # ``cleaned_data``\n return form\n\nIn the example below, the original form is being modified. If you don't want\nthe original form to be modified, do not return anything.\n\nCheck the file form element plugin\n(fobi.contrib.plugins.form_elements.fields.file) for complete example.\n\npath/to/sample_textarea/forms.py\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nWhy to have another file for defining forms? Just to keep the code clean and\nless messy, although you could perfectly define all your plugin forms in the\nmodule ``fobi_form_elements.py``, it's recommended to keep it separate.\n\nTake into consideration, that ``forms.py`` is not an auto-discovered file\npattern. All your form element plugins should be registered in modules named\n``fobi_form_elements.py``.\n\nRequired imports.\n\n.. code-block:: python\n\n from django import forms\n from fobi.base import BasePluginForm\n\nForm for for ``SampleTextareaPlugin`` form element plugin.\n\n.. code-block:: python\n\n class SampleTextareaForm(forms.Form, BasePluginForm):\n \"\"\"Sample textarea form.\"\"\"\n\n plugin_data_fields = [\n (\"name\", \"\"),\n (\"label\", \"\"),\n (\"initial\", \"\"),\n (\"required\", False)\n ]\n\n name = forms.CharField(label=\"Name\", required=True)\n label = forms.CharField(label=\"Label\", required=True)\n initial = forms.CharField(label=\"Initial\", required=False)\n required = forms.BooleanField(label=\"Required\", required=False)\n\nNote that although it's not being checked in the code, but for form\nfield plugins the following fields should be present in the plugin\nform (``BasePluginForm``) and the form plugin (``FormFieldPlugin``):\n\n- name\n\nIn some cases, you might want to do something with the data\nbefore it gets saved. For that purpose, ``save_plugin_data`` method\nhas been introduced.\n\nSee the following `example\n`_.\n\n.. code-block:: python\n\n def save_plugin_data(self, request=None):\n \"\"\"Saving the plugin data and moving the file.\"\"\"\n file_path = self.cleaned_data.get('file', None)\n if file_path:\n saved_image = handle_uploaded_file(IMAGES_UPLOAD_DIR, file_path)\n self.cleaned_data['file'] = saved_image\n\npath/to/sample_textarea/widgets.py\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nRequired imports.\n\n.. code-block:: python\n\n from fobi.base import FormElementPluginWidget\n\nDefining the base plugin widget.\n\n.. code-block:: python\n\n class BaseSampleTextareaPluginWidget(FormElementPluginWidget):\n \"\"\"Base sample textarea plugin widget.\"\"\"\n\n # Same as ``uid`` value of the ``SampleTextareaPlugin``.\n plugin_uid = \"sample_textarea\"\n\npath/to/sample_layout/fobi_form_elements.py\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nRegister in the registry (in some module which is for sure to be loaded; it's\nhandy to do it in the theme module).\n\nRequired imports.\n\n.. code-block:: python\n\n from fobi.base import form_element_plugin_widget_registry\n from path.to.sample_textarea.widgets import BaseSampleTextareaPluginWidget\n\nDefine the theme specific plugin.\n\n.. code-block:: python\n\n class SampleTextareaPluginWidget(BaseSampleTextareaPluginWidget):\n \"\"\"Sample textarea plugin widget.\"\"\"\n\n theme_uid = 'bootstrap3' # Theme for which the widget is loaded\n media_js = [\n 'sample_layout/js/fobi.plugins.form_elements.sample_textarea.js',\n ]\n media_css = [\n 'sample_layout/css/fobi.plugins.form_elements.sample_textarea.css',\n ]\n\nRegister the widget.\n\n.. code-block:: python\n\n form_element_plugin_widget_registry.register(SampleTextareaPluginWidget)\n\nForm element plugin final steps\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nNow, that everything is ready, make sure your plugin module is added to\n``INSTALLED_APPS``.\n\n.. code-block:: python\n\n INSTALLED_APPS = (\n # ...\n 'path.to.sample_textarea',\n # ...\n )\n\nAfterwards, go to terminal and type the following command.\n\n.. code-block:: sh\n\n ./manage.py fobi_sync_plugins\n\nIf your HTTP server is running, you would then be able to see the new plugin\nin the edit form interface.\n\nDashboard URL: http://127.0.0.1:8000/fobi/\n\nNote, that you have to be logged in, in order to use the dashboard. If your\nnew plugin doesn't appear, set the ``FOBI_DEBUG`` to True in your Django's\nlocal settings module, re-run your code and check console for error\nnotifications.\n\nCreating a new form handler plugin\n==================================\nForm handler plugins handle the form data. `django-fobi` comes with several\nbundled form handler plugins, among which is the ``db_store`` and ``mail``\nplugins, which are responsible for saving the submitted form data into the\ndatabase and mailing the data to recipients specified. Number of form handlers\nin a form is not limited. Certain form handlers are not configurable (for\nexample the ``db_store`` form handler isn't), while others are (``mail``,\n``http_repost``).\n\nYou should see a form handler as a Django micro app, which could have its' own\nmodels, admin interface, etc.\n\nBy default, it's possible to use a form handler plugin multiple times per form.\nIf you wish to allow form handler plugin to be used only once in a form,\nset the ``allow_multiple`` property of the plugin to False.\n\nAs said above, `django-fobi` comes with several bundled form handler plugins.\nDo check the source code as example.\n\nDefine and register the form handler plugin\n-------------------------------------------\nLet's name that plugin ``sample_mail``. The plugin directory should then have\nthe following structure.\n\n.. code-block:: text\n\n path/to/sample_mail/\n \u251c\u2500\u2500 __init__.py\n \u251c\u2500\u2500 fobi_form_handlers.py # Where plugins are defined and registered\n \u2514\u2500\u2500 forms.py # Plugin configuration form\n\nForm handler plugins should be registered in \"fobi_form_handlers.py\" file.\nEach plugin module should be put into the ``INSTALLED_APPS`` of your Django\nprojects' settings.\n\npath/to/sample_mail/fobi_form_handlers.py\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nA single form handler plugin is registered by its' UID.\n\nRequired imports.\n\n.. code-block:: python\n\n import json\n from django.core.mail import send_mail\n from fobi.base import FormHandlerPlugin, form_handler_plugin_registry\n from path.to.sample_mail.forms import SampleMailForm\n\nDefining the Sample mail handler plugin.\n\n.. code-block:: python\n\n class SampleMailHandlerPlugin(FormHandlerPlugin):\n \"\"\"Sample mail handler plugin.\"\"\"\n\n uid = \"sample_mail\"\n name = _(\"Sample mail\")\n form = SampleMailForm\n\n def run(self, form_entry, request, form, form_element_entries=None):\n \"\"\"To be executed by handler.\"\"\"\n send_mail(\n self.data.subject,\n json.dumps(form.cleaned_data),\n self.data.from_email,\n [self.data.to_email],\n fail_silently=True\n )\n\nRegister the plugin\n\n.. code-block:: python\n\n form_handler_plugin_registry.register(SampleMailHandlerPlugin)\n\nSome form handlers are configurable, some others not. In order to\nhave a user friendly way of showing the form handler settings, what's\nsometimes needed, a ``plugin_data_repr`` method has been introduced.\nSimplest implementation of it would look as follows:\n\n.. code-block:: python\n\n def plugin_data_repr(self):\n \"\"\"Human readable representation of plugin data.\n\n :return string:\n \"\"\"\n return self.data.__dict__\n\npath/to/sample_mail/forms.py\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nIf plugin is configurable, it has configuration data. A single form may have\nunlimited number of same plugins. Imagine, you want to have different subjects\nand additional body texts for different user groups. You could then assign two\nform handler ``mail`` plugins to the form. Of course, saving the posted form\ndata many times does not make sense, but it's up to the user. So, in case if\nplugin is configurable, it should have a form.\n\nWhy to have another file for defining forms? Just to keep the code clean and\nless messy, although you could perfectly define all your plugin forms in the\nmodule ``fobi_form_handlers.py``, it's recommended to keep it separate.\n\nTake into consideration, that ``forms.py`` is not an auto-discovered file\npattern. All your form handler plugins should be registered in modules named\n``fobi_form_handlers.py``.\n\nRequired imports.\n\n.. code-block:: python\n\n from django import forms\n from django.utils.translation import ugettext_lazy as _\n from fobi.base import BasePluginForm\n\nDefining the form for Sample mail handler plugin.\n\n.. code-block:: python\n\n class MailForm(forms.Form, BasePluginForm):\n \"\"\"Mail form.\"\"\"\n\n plugin_data_fields = [\n (\"from_name\", \"\"),\n (\"from_email\", \"\"),\n (\"to_name\", \"\"),\n (\"to_email\", \"\"),\n (\"subject\", \"\"),\n (\"body\", \"\"),\n ]\n\n from_name = forms.CharField(label=_(\"From name\"), required=True)\n from_email = forms.EmailField(label=_(\"From email\"), required=True)\n to_name = forms.CharField(label=_(\"To name\"), required=True)\n to_email = forms.EmailField(label=_(\"To email\"), required=True)\n subject = forms.CharField(label=_(\"Subject\"), required=True)\n body = forms.CharField(\n label=_(\"Body\"),\n required=False,\n widget=forms.widgets.Textarea\n )\n\nAfter the plugin has been processed, all its' data is available in a\n``plugin_instance.data`` container (for example,\n``plugin_instance.data.subject`` or ``plugin_instance.data.from_name``).\n\nPrioritise the execution order\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nSome form handlers shall be executed prior others. A good example of such, is\na combination of \"mail\" and \"db_save\" form handlers for the form. In case if\nlarge files are posted, submission of form data would fail if \"mail\" plugin\nwould be executed after \"db_save\" has been executed. That's why it's possible\nto prioritise that ordering in a ``FOBI_FORM_HANDLER_PLUGINS_EXECUTION_ORDER``\nsetting variable.\n\nIf not specified or left empty, form handler plugins would be ran in the order\nof discovery. All form handler plugins that are not listed in the\n``FORM_HANDLER_PLUGINS_EXECUTION_ORDER``, would be ran after the plugins that\nare mentioned there.\n\n.. code-block:: python\n\n FORM_HANDLER_PLUGINS_EXECUTION_ORDER = (\n 'http_repost',\n 'mail',\n # The 'db_store' is left out intentionally, since it should\n # be the last plugin to be executed.\n )\n\nForm handler plugin custom actions\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nBy default, a single form handler plugin has at least a \"delete\" action.\nIf plugin is configurable, it gets an \"edit\" action as well.\n\nFor some of your plugins, you may want to register a custom action. For\nexample, the \"db_store\" plugin does have one, for showing a link to\na listing page with saved form data for the form given.\n\nFor such cases, define a ``custom_actions`` method in your form handler\nplugin. That method shall return a list of triples. In each triple,\nfirst value is the URL, second value is the title and the third value\nis the icon of the URL.\n\nThe following example is taken from the \"db_store\" plugin.\n\n.. code-block:: python\n\n def custom_actions(self):\n \"\"\"Adding a link to view the saved form entries.\n\n :return iterable:\n \"\"\"\n return (\n (\n reverse('fobi.contrib.plugins.form_handlers.db_store.view_saved_form_data_entries'),\n _(\"View entries\"),\n 'glyphicon glyphicon-list'\n ),\n )\n\nForm handler plugin final steps\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nDo not forget to add the form handler plugin module to ``INSTALLED_APPS``.\n\n.. code-block:: python\n\n INSTALLED_APPS = (\n # ...\n 'path.to.sample_mail',\n # ...\n )\n\nAfterwards, go to terminal and type the following command.\n\n.. code-block:: sh\n\n ./manage.py fobi_sync_plugins\n\nIf your HTTP server is running, you would then be able to see the new plugin\nin the edit form interface.\n\nCreating a new form importer plugin\n===================================\nForm importer plugins import the forms from some external data source into\n`django-fobi` form format. Number of form importers is not limited. Form\nimporters are implemented in forms of wizards (since they may contain several\nsteps).\n\nYou should see a form importer as a Django micro app, which could have its' own\nmodels, admin interface, etc.\n\nAt the moment `django-fobi` comes with only one bundled form handler plugin,\nwhich is the ``mailchimp_importer``, which is responsible for importing\nexisting MailChimp forms into `django-fobi`.\n\nDefine and register the form importer plugin\n--------------------------------------------\nLet's name that plugin ``sample_importer``. The plugin directory should then\nhave the following structure.\n\n.. code-block:: text\n\n path/to/sample_importer/\n \u251c\u2500\u2500 templates\n \u2502 \u2514\u2500\u2500 sample_importer\n \u2502 \u251c\u2500\u2500 0.html\n \u2502 \u2514\u2500\u2500 1.html\n \u251c\u2500\u2500 __init__.py\n \u251c\u2500\u2500 fobi_form_importers.py # Where plugins are defined and registered\n \u251c\u2500\u2500 forms.py # Wizard forms\n \u2514\u2500\u2500 views.py # Wizard views\n\nForm importer plugins should be registered in \"fobi_form_importers.py\" file.\nEach plugin module should be put into the ``INSTALLED_APPS`` of your Django\nprojects' settings.\n\npath/to/sample_importer/fobi_form_importers.py\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nA single form importer plugin is registered by its' UID.\n\nRequired imports.\n\n.. code-block:: python\n\n from django.utils.translation import ugettext_lazy as _\n from fobi.form_importers import BaseFormImporter, form_importer_plugin_registry\n from fobi.contrib.plugins.form_elements import fields\n from path.to.sample_importer.views import SampleImporterWizardView\n\nDefining the Sample importer plugin.\n\n.. code-block:: python\n\n class SampleImporterPlugin(FormHandlerPlugin):\n \"\"\"Sample importer plugin.\"\"\"\n\n uid = 'sample_importer'\n name = _(\"Sample importer\")\n wizard = SampleImporterWizardView\n templates = [\n 'sample_importer/0.html',\n 'sample_importer/1.html',\n ]\n\n # field_type (at importer): uid (django-fobi)\n fields_mapping = {\n # Implemented\n 'email': fields.email.UID,\n 'text': fields.text.UID,\n 'number': fields.integer.UID,\n 'dropdown': fields.select.UID,\n 'date': fields.date.UID,\n 'url': fields.url.UID,\n 'radio': fields.radio.UID,\n\n # Transformed into something else\n 'address': fields.text.UID,\n 'zip': fields.text.UID,\n 'phone': fields.text.UID,\n }\n\n # Django standard: remote\n field_properties_mapping = {\n 'label': 'name',\n 'name': 'tag',\n 'help_text': 'helptext',\n 'initial': 'default',\n 'required': 'req',\n 'choices': 'choices',\n }\n\n field_type_prop_name = 'field_type'\n position_prop_name = 'order'\n\n def extract_field_properties(self, field_data):\n field_properties = {}\n for prop, val in self.field_properties_mapping.items():\n if val in field_data:\n if 'choices' == val:\n field_properties[prop] = \"\\n\".join(field_data[val])\n else:\n field_properties[prop] = field_data[val]\n return field_properties\n\n\n form_importer_plugin_registry.register(SampleImporter)\n\npath/to/sample_importer/forms.py\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nAs mentioned above, form importers are implemented in form of wizards. The\nforms are the wizard steps.\n\nRequired imports.\n\n.. code-block:: python\n\n from django import forms\n from django.utils.translation import ugettext_lazy as _\n from sample_service_api import sample_api # Just an imaginary API client\n\nDefining the form for Sample importer plugin.\n\n.. code-block:: python\n\n class SampleImporterStep1Form(forms.Form):\n \"\"\"First form the the wizard.\"\"\"\n\n api_key = forms.CharField(required=True)\n\n\n class SampleImporterStep2Form(forms.Form):\n \"\"\"Second form of the wizard.\"\"\"\n\n list_id = forms.ChoiceField(required=True, choices=[])\n\n def __init__(self, *args, **kwargs):\n self._api_key = None\n\n if 'api_key' in kwargs:\n self._api_key = kwargs.pop('api_key', None)\n\n super(SampleImporterStep2Form, self).__init__(*args, **kwargs)\n\n if self._api_key:\n client = sample_api.Api(self._api_key)\n lists = client.lists.list()\n choices = [(l['id'], l['name']) for l in lists['data']]\n self.fields['list_id'].choices = choices\n\npath/to/sample_importer/views.py\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nThe wizard views.\n\nRequired imports.\n\n.. code-block:: python\n\n from sample_service_api import sample_api # Just an imaginary API client\n\n from django.shortcuts import redirect\n from django.core.urlresolvers import reverse\n from django.contrib import messages\n from django.utils.translation import ugettext_lazy as _\n\n # For django LTE 1.8 import from `django.contrib.formtools.wizard.views`\n from formtools.wizard.views import SessionWizardView\n\n from path.to.sample_importer.forms import (\n SampleImporterStep1Form,\n SampleImporterStep2Form,\n )\n\nDefining the wizard view for Sample importer plugin.\n\n.. code-block:: python\n\n class SampleImporterWizardView(SessionWizardView):\n \"\"\"Sample importer wizard view.\"\"\"\n\n form_list = [SampleImporterStep1Form, SampleImporterStep2Form]\n\n def get_form_kwargs(self, step):\n \"\"\"Get form kwargs (to be used internally).\"\"\"\n if '1' == step:\n data = self.get_cleaned_data_for_step('0') or {}\n api_key = data.get('api_key', None)\n return {'api_key': api_key}\n return {}\n\n def done(self, form_list, **kwargs):\n \"\"\"After all forms are submitted.\"\"\"\n # Merging cleaned data into one dict\n cleaned_data = {}\n for form in form_list:\n cleaned_data.update(form.cleaned_data)\n\n # Connecting to sample client API\n client = sample_client.Api(cleaned_data['api_key'])\n\n # Fetching the form data\n form_data = client.lists.merge_vars(\n id={'list_id': cleaned_data['list_id']}\n )\n\n # We need the first form only\n try:\n form_data = form_data['data'][0]\n except Exception as err:\n messages.warning(\n self.request,\n _('Selected form could not be imported due errors.')\n )\n return redirect(reverse('fobi.dashboard'))\n\n # Actually, import the form\n form_entry = self._form_importer.import_data(\n {'name': form_data['name'], 'user': self.request.user},\n form_data['merge_vars']\n )\n\n redirect_url = reverse(\n 'fobi.edit_form_entry',\n kwargs={'form_entry_id': form_entry.pk}\n )\n\n messages.info(\n self.request,\n _('Form {0} imported successfully.').format(form_data['name'])\n )\n\n return redirect(\"{0}\".format(redirect_url))\n\nForm importer plugin final steps\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nDo not forget to add the form importer plugin module to ``INSTALLED_APPS``.\n\n.. code-block:: python\n\n INSTALLED_APPS = (\n # ...\n 'path.to.sample_importer',\n # ...\n )\n\nAfterwards, go to terminal and type the following command.\n\n.. code-block:: sh\n\n ./manage.py fobi_sync_plugins\n\nIf your HTTP server is running, you would then be able to see the new plugin\nin the dashboard form interface (implemented in all bundled themes).\n\nCreating a form callback\n========================\nForm callbacks are additional hooks, that are executed on various stages of\nthe form submission.\n\nLet's place the callback in the ``foo`` module. The plugin directory should\nthen have the following structure.\n\n.. code-block:: text\n\n path/to/foo/\n \u251c\u2500\u2500 __init__.py\n \u2514\u2500\u2500 fobi_form_callbacks.py # Where callbacks are defined and registered\n\nSee the callback example below.\n\nRequired imports.\n\n.. code-block:: python\n\n from fobi.constants import (\n CALLBACK_BEFORE_FORM_VALIDATION,\n CALLBACK_FORM_VALID_BEFORE_SUBMIT_PLUGIN_FORM_DATA,\n CALLBACK_FORM_VALID, CALLBACK_FORM_VALID_AFTER_FORM_HANDLERS,\n CALLBACK_FORM_INVALID\n )\n from fobi.base import FormCallback, form_callback_registry\n\nDefine and register the callback\n\n.. code-block:: python\n\n class SampleFooCallback(FormCallback):\n \"\"\"Sample foo callback.\"\"\"\n\n stage = CALLBACK_FORM_VALID\n\n def callback(self, form_entry, request, form):\n \"\"\"Define your callback code here.\"\"\"\n print(\"Great! Your form is valid!\")\n\n form_callback_registry.register(SampleFooCallback)\n\nAdd the callback module to ``INSTALLED_APPS``.\n\n.. code-block:: python\n\n INSTALLED_APPS = (\n # ...\n 'path.to.foo',\n # ...\n )\n\nSuggestions\n===========\nCustom action for the form\n--------------------------\nSometimes, you would want to specify a different action for the form.\nAlthough it's possible to define a custom form action (``action`` field\nin the \"Form properties\" tab), you're advised to use the ``http_repost``\nplugin instead, since then the form would be still validated locally\nand only then the valid data, as is, would be sent to the desired\nendpoint.\n\nTake in mind, that if both cases, if CSRF protection is enabled on\nthe endpoint, your post request would result an error.\n\nWhen you want to customise too many things\n------------------------------------------\n`django-fobi`, with its' flexible form elements, form handlers and form\ncallbacks is very customisable. However, there might be cases when you need to\noverride entire view to fit your needs. Take a look at the\n`FeinCMS integration\n`_\nor `DjangoCMS integration\n`_\nas a good example of such. You may also want to compare the code from original\nview ``fobi.views.view_form_entry`` with the code from the widget to get a\nbetter idea of what could be changed in your case. If need a good advice,\njust ask me.\n\nTheming\n=======\n`django-fobi` comes with theming API. While there are several ready-to-use\nthemes:\n\n- \"Bootstrap 3\" theme\n- \"Foundation 5\" theme\n- \"Simple\" theme in (with editing interface in style of the Django admin)\n- \"DjangoCMS admin style\" theme (which is another simple theme with editing\n interface in style of ``djangocms-admin-style``)\n\nObviously, there are two sorts of views when it comes to editing and viewing\nthe form.\n\n- The \"view-view\", when the form as it has been made is exposed to the\n site end- users/visitors.\n- The \"edit-view\" (builder view), where the authorised users build their forms.\n\nBoth \"Bootstrap 3\" and \"Foundation 5\" themes are making use of the same style\nfor both \"view-view\" and \"edit-view\" views.\n\nBoth \"Simple\" and \"DjangoCMS admin style\" themes are styling for the\n\"edit-view\" only. The \"view-view\" is pretty much blank, as shown on the one\nof the screenshots [2.6]_.\n\nHave in mind, that creating a brand new theme could be time consuming.\nInstead, you are advised to extend existing themes or in the worst case,\nif too much customisation required, create your own themes based on\nexisting ones (just copy the desired theme to your project directory and\nwork it out further).\n\nIt's possible to use different templates for all \"view\" and \"edit\"\nactions (see the source code of the \"simple\" theme). Both \"Bootstrap 3\" and\n\"Foundation 5\" themes look great. Although if you can't use any of those,\nthe \"Simple\" theme is the best start, since it looks just like django-admin.\n\nCreate a new theme\n------------------\n\nLet's place the theme in the ``sample_theme`` module. The theme directory\nshould then have the following structure.\n\n.. code-block:: text\n\n path/to/sample_theme/\n \u251c\u2500\u2500 static\n \u2502 \u251c\u2500\u2500 css\n \u2502 \u2502 \u2514\u2500\u2500 sample_theme.css\n \u2502 \u2514\u2500\u2500 js\n \u2502 \u2514\u2500\u2500 sample_theme.js\n \u251c\u2500\u2500 templates\n \u2502 \u2514\u2500\u2500 sample_theme\n \u2502 \u251c\u2500\u2500 _base.html\n \u2502 \u251c\u2500\u2500 add_form_element_entry.html\n \u2502 \u251c\u2500\u2500 ...\n \u2502 \u2514\u2500\u2500 view_form_entry_ajax.html\n \u251c\u2500\u2500 __init__.py\n \u251c\u2500\u2500 fobi_form_elements.py\n \u2514\u2500\u2500 fobi_themes.py # Where themes are defined and registered\n\nSee the theme example below.\n\n.. code-block:: python\n\n from django.utils.translation import ugettext_lazy as _\n\n from fobi.base import BaseTheme, theme_registry\n\n class SampleTheme(BaseTheme):\n \"\"\"Sample theme.\"\"\"\n\n uid = 'sample'\n name = _(\"Sample\")\n\n media_css = (\n 'sample_theme/css/sample_theme.css',\n 'css/fobi.core.css',\n )\n\n media_js = (\n 'js/jquery-1.10.2.min.js',\n 'jquery-ui/js/jquery-ui-1.10.3.custom.min.js',\n 'js/jquery.slugify.js',\n 'js/fobi.core.js',\n 'sample_theme/js/sample_theme.js',\n )\n\n # Form element specific\n form_element_html_class = 'form-control'\n form_radio_element_html_class = 'radio'\n form_element_checkbox_html_class = 'checkbox'\n\n form_edit_form_entry_option_class = 'glyphicon glyphicon-edit'\n form_delete_form_entry_option_class = 'glyphicon glyphicon-remove'\n form_list_container_class = 'list-inline'\n\n # Templates\n master_base_template = 'sample_theme/_base.html'\n base_template = 'sample_theme/base.html'\n\n form_ajax = 'sample_theme/snippets/form_ajax.html'\n form_snippet_template_name = 'sample_theme/snippets/form_snippet.html'\n form_properties_snippet_template_name = 'sample_theme/snippets/form_properties_snippet.html'\n messages_snippet_template_name = 'sample_theme/snippets/messages_snippet.html'\n\n add_form_element_entry_template = 'sample_theme/add_form_element_entry.html'\n add_form_element_entry_ajax_template = 'sample_theme/add_form_element_entry_ajax.html'\n\n add_form_handler_entry_template = 'sample_theme/add_form_handler_entry.html'\n add_form_handler_entry_ajax_template = 'sample_theme/add_form_handler_entry_ajax.html'\n\n create_form_entry_template = 'sample_theme/create_form_entry.html'\n create_form_entry_ajax_template = 'bootstrap3/create_form_entry_ajax.html'\n\n dashboard_template = 'sample_theme/dashboard.html'\n\n edit_form_element_entry_template = 'sample_theme/edit_form_element_entry.html'\n edit_form_element_entry_ajax_template = 'sample_theme/edit_form_element_entry_ajax.html'\n\n edit_form_entry_template = 'sample_theme/edit_form_entry.html'\n edit_form_entry_ajax_template = 'sample_theme/edit_form_entry_ajax.html'\n\n edit_form_handler_entry_template = 'sample_theme/edit_form_handler_entry.html'\n edit_form_handler_entry_ajax_template = 'sample_theme/edit_form_handler_entry_ajax.html'\n\n form_entry_submitted_template = 'sample_theme/form_entry_submitted.html'\n form_entry_submitted_ajax_template = 'sample_theme/form_entry_submitted_ajax.html'\n\n view_form_entry_template = 'sample_theme/view_form_entry.html'\n view_form_entry_ajax_template = 'sample_theme/view_form_entry_ajax.html'\n\nRegistering the ``SampleTheme`` plugin.\n\n.. code-block:: python\n\n theme_registry.register(SampleTheme)\n\nSometimes you would want to attach additional properties to the theme\nin order to use them later in templates (remember, current theme object\nis always available in templates under name ``fobi_theme``).\n\nFor such cases you would need to define a variable in your project's settings\nmodule, called ``FOBI_CUSTOM_THEME_DATA``. See the following code as example:\n\n.. code-block:: python\n\n # `django-fobi` custom theme data for to be displayed in third party apps\n # like `django-registraton`.\n FOBI_CUSTOM_THEME_DATA = {\n 'bootstrap3': {\n 'page_header_html_class': '',\n 'form_html_class': 'form-horizontal',\n 'form_button_outer_wrapper_html_class': 'control-group',\n 'form_button_wrapper_html_class': 'controls',\n 'form_button_html_class': 'btn',\n 'form_primary_button_html_class': 'btn-primary pull-right',\n },\n 'foundation5': {\n 'page_header_html_class': '',\n 'form_html_class': 'form-horizontal',\n 'form_button_outer_wrapper_html_class': 'control-group',\n 'form_button_wrapper_html_class': 'controls',\n 'form_button_html_class': 'radius button',\n 'form_primary_button_html_class': 'btn-primary',\n },\n 'simple': {\n 'page_header_html_class': '',\n 'form_html_class': 'form-horizontal',\n 'form_button_outer_wrapper_html_class': 'control-group',\n 'form_button_wrapper_html_class': 'submit-row',\n 'form_button_html_class': 'btn',\n 'form_primary_button_html_class': 'btn-primary',\n }\n }\n\nYou would now be able to access the defined extra properties in templates\nas shown below.\n\n.. code-block:: html\n\n
\n\nYou likely would want to either remove the footer text or change it. Define\na variable in your project's settings module, called ``FOBI_THEME_FOOTER_TEXT``.\nSee the following code as example:\n\n.. code-block:: python\n\n FOBI_THEME_FOOTER_TEXT = gettext('© django-fobi example site 2014')\n\nBelow follow the properties of the theme:\n\n- ``base_edit``\n- ``base_view``\n\nThere are generic templates made in order to simplify theming. Some\nof them you would never need to override. Some others, you would likely\nwant to.\n\nTemplates that you likely would want to re-write in your custom\ntheme implementation are marked with three asterisks (\\*\\*\\*):\n\n.. code-block:: text\n\n generic\n \u251c\u2500\u2500 snippets\n \u2502 \u251c\u2500\u2500 form_ajax.html\n \u2502 \u251c\u2500\u2500 form_edit_ajax.html\n \u2502 \u251c\u2500\u2500 *** form_properties_snippet.html\n \u2502 \u251c\u2500\u2500 *** form_snippet.html\n \u2502 \u251c\u2500\u2500 --- form_edit_snippet.html (does not exist in generic templates)\n \u2502 \u251c\u2500\u2500 --- form_view_snippet.html (does not exist in generic templates)\n \u2502 \u251c\u2500\u2500 form_view_ajax.html\n \u2502 \u2514\u2500\u2500 messages_snippet.html\n \u2502\n \u251c\u2500\u2500 _base.html\n \u251c\u2500\u2500 add_form_element_entry.html\n \u251c\u2500\u2500 add_form_element_entry_ajax.html\n \u251c\u2500\u2500 add_form_handler_entry.html\n \u251c\u2500\u2500 add_form_handler_entry_ajax.html\n \u251c\u2500\u2500 base.html\n \u251c\u2500\u2500 create_form_entry.html\n \u251c\u2500\u2500 create_form_entry_ajax.html\n \u251c\u2500\u2500 *** dashboard.html\n \u251c\u2500\u2500 edit_form_element_entry.html\n \u251c\u2500\u2500 edit_form_element_entry_ajax.html\n \u251c\u2500\u2500 edit_form_entry.html\n \u251c\u2500\u2500 *** edit_form_entry_ajax.html\n \u251c\u2500\u2500 edit_form_handler_entry.html\n \u251c\u2500\u2500 edit_form_handler_entry_ajax.html\n \u251c\u2500\u2500 form_entry_submitted.html\n \u251c\u2500\u2500 *** form_entry_submitted_ajax.html\n \u251c\u2500\u2500 *** theme.html\n \u251c\u2500\u2500 view_form_entry.html\n \u2514\u2500\u2500 view_form_entry_ajax.html\n\nFrom all of the templates listed above, the _base.html template is\nthe most influenced by the Bootstrap 3 theme.\n\nMake changes to an existing theme\n---------------------------------\nAs said above, making your own theme from scratch could be costly. Instead,\nyou can override/reuse an existing one and change it to your needs with\nminimal efforts. See the `override simple theme\n`_\nexample. In order to see it in action, run the project with\n`settings_override_simple_theme\n`_\noption:\n\n.. code-block:: sh\n\n ./manage.py runserver --settings=settings_override_simple_theme\n\nDetails explained below.\n\nDirectory structure\n~~~~~~~~~~~~~~~~~~~\n.. code-block:: text\n\n override_simple_theme/\n \u251c\u2500\u2500 static\n \u2502 \u2514\u2500\u2500 override_simple_theme\n \u2502 \u251c\u2500\u2500 css\n \u2502 \u2502 \u2514\u2500\u2500 override-simple-theme.css\n \u2502 \u2514\u2500\u2500 js\n \u2502 \u2514\u2500\u2500 override-simple-theme.js\n \u2502\n \u251c\u2500\u2500 templates\n \u2502 \u2514\u2500\u2500 override_simple_theme\n \u2502 \u251c\u2500\u2500 snippets\n \u2502 \u2502 \u2514\u2500\u2500 form_ajax.html\n \u2502 \u2514\u2500\u2500 base_view.html\n \u251c\u2500\u2500 __init__.py\n \u2514\u2500\u2500 fobi_themes.py # Where themes are defined and registered\n\nfobi_themes.py\n~~~~~~~~~~~~~~\nOverriding the \"simple\" theme.\n\n.. code-block:: python\n\n __all__ = ('MySimpleTheme',)\n\n from fobi.base import theme_registry\n\n from fobi.contrib.themes.simple.fobi_themes import SimpleTheme\n\n class MySimpleTheme(SimpleTheme):\n \"\"\"My simple theme, inherited from `SimpleTheme` theme.\"\"\"\n\n html_classes = ['my-simple-theme',]\n base_view_template = 'override_simple_theme/base_view.html'\n form_ajax = 'override_simple_theme/snippets/form_ajax.html'\n\nRegister the overridden theme. Note, that it's important to set the `force`\nargument to True, in order to override the original theme. Force can be\napplied only once (for an overridden element).\n\n.. code-block:: python\n\n theme_registry.register(MySimpleTheme, force=True)\n\ntemplates/override_simple_theme/base_view.html\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n.. code-block:: html\n\n {% extends \"simple/base_view.html\" %}\n\n {% load static %}\n\n {% block stylesheets %}\n \n {% endblock stylesheets %}\n\n {% block main-wrapper %}\n
\n

It's easy to override a theme!

\n
\n\n {{ block.super }}\n {% endblock main-wrapper %}\n\ntemplates/override_simple_theme/snippets/form_ajax.html\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n.. code-block:: html\n\n {% extends \"fobi/generic/snippets/form_ajax.html\" %}\n\n {% block form_html_class %}basic-grey{% endblock %}\n\nForm wizards\n============\nBasics\n------\nWith form wizards you can split forms across multiple pages. State is\nmaintained in one of the backends (at the moment the Session backend). Data\nprocessing is delayed until the submission of the final form.\n\nIn `django-fobi` wizards work in the following way:\n\n- Number of forms in a form wizard is not limited.\n- Form callbacks, handlers are totally ignored in form wizards. Instead,\n the form-wizard specific handlers (form wizard handlers) take over handling\n of the form data on the final step.\n\nBundled form wizard handler plugins\n-----------------------------------\nBelow a short overview of the form wizard handler plugins. See the\nREADME.rst file in directory of each plugin for details.\n\n- `DB store\n `__:\n Stores form data in a database.\n- `HTTP repost\n `__:\n Repost the POST request to another endpoint.\n- `Mail\n `__:\n Send the form data by email.\n- `Mail the sender\n `__:\n Send the form data by email to the sender (submitter) of the form.\n\nIntegration with third-party apps and frameworks\n================================================\n`django-fobi` has been successfully integrated into a number of diverse\nthird-party apps and frameworks, such as: Django REST framework, Django CMS,\nFeinCMS, Mezzanine and Wagtail.\n\nCertainly, integration into CMS is one case, integration into REST framework -\ntotally another. In REST frameworks we no longer have forms as such. Context\nis very different. Handling of form data should obviously happen in a\ndifferent way. Assembling of the form class isn't enough (in case of Django\nREST framework we assemble the serializer class).\n\nIn order to handle such level of integration, two additional sort of plugins\nhave been introduced:\n\n- IntegrationFormElementPlugin\n- IntegrationFormHandlerPlugin\n\nThese plugins are in charge of representation of the form elements in a\nproper way for the package to be integrated and handling the submitted form\ndata.\n\n`Additional documentation\n`_\nis available in the sub-package.\n\nSample `IntegrationFormElementPlugin`\n-------------------------------------\nSample is taken from `here\n`__.\n\nbase.py\n~~~~~~~\nDefine the form element plugin.\n\n.. code-block:: python\n\n from django.utils.translation import ugettext_lazy as _\n\n from rest_framework.fields import EmailField\n\n from fobi.base import IntegrationFormFieldPlugin\n from fobi.contrib.apps.drf_integration import UID as INTEGRATE_WITH_UID\n from fobi.contrib.apps.drf_integration.base import (\n DRFIntegrationFormElementPluginProcessor,\n DRFSubmitPluginFormDataMixin,\n )\n from fobi.contrib.apps.drf_integration.form_elements.fields.email import UID\n\n\n class EmailInputPlugin(IntegrationFormFieldPlugin,\n DRFSubmitPluginFormDataMixin):\n \"\"\"EmailField plugin.\"\"\"\n\n uid = UID\n integrate_with = INTEGRATE_WITH_UID\n name = _(\"Decimal\")\n group = _(\"Fields\")\n\n def get_custom_field_instances(self,\n form_element_plugin,\n request=None,\n form_entry=None,\n form_element_entries=None,\n **kwargs):\n \"\"\"Get form field instances.\"\"\"\n field_kwargs = {\n 'required': form_element_plugin.data.required,\n 'initial': form_element_plugin.data.initial,\n 'label': form_element_plugin.data.label,\n 'help_text': form_element_plugin.data.help_text,\n 'max_length': form_element_plugin.data.max_length,\n }\n return [\n DRFIntegrationFormElementPluginProcessor(\n field_class=EmailField,\n field_kwargs=field_kwargs\n )\n ]\n\nfobi_integration_form_elements.py\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nRegister the plugin. Note the name pattern `fobi_integration_form_elements`.\n\n.. code-block:: python\n\n from fobi.base import integration_form_element_plugin_registry\n from .base import EmailInputPlugin\n\n integration_form_element_plugin_registry.register(EmailInputPlugin)\n\nDon't forget to list your plugin in the ``INSTALLED_APPS`` afterwards.\n\nSample `IntegrationFormHandlerPlugin`\n-------------------------------------\nSample is taken from `here\n`__.\n\nbase.py\n~~~~~~~\nDefine the form handler plugin.\n\n.. code-block:: python\n\n import logging\n from mimetypes import guess_type\n import os\n\n from django.conf import settings\n from django.utils.translation import ugettext_lazy as _\n\n from fobi.base import IntegrationFormHandlerPlugin\n from fobi.helpers import extract_file_path\n\n from fobi.contrib.apps.drf_integration import UID as INTEGRATE_WITH_UID\n from fobi.contrib.apps.drf_integration.base import get_processed_serializer_data\n\n from . import UID\n\n\n class MailHandlerPlugin(IntegrationFormHandlerPlugin):\n \"\"\"Mail handler form handler plugin.\n\n Can be used only once per form.\n \"\"\"\n\n uid = UID\n name = _(\"Mail\")\n integrate_with = INTEGRATE_WITH_UID\n\n def run(self,\n form_handler_plugin,\n form_entry,\n request,\n form_element_entries=None,\n **kwargs):\n \"\"\"Run.\"\"\"\n base_url = form_handler_plugin.get_base_url(request)\n\n serializer = kwargs['serializer']\n\n # Clean up the values, leave our content fields and empty values.\n field_name_to_label_map, cleaned_data = get_processed_serializer_data(\n serializer,\n form_element_entries\n )\n\n rendered_data = form_handler_plugin.get_rendered_data(\n serializer.validated_data,\n field_name_to_label_map,\n base_url\n )\n\n files = self._prepare_files(request, serializer)\n\n form_handler_plugin.send_email(rendered_data, files)\n\n def _prepare_files(self, request, serializer):\n \"\"\"Prepares the files for being attached to the mail message.\"\"\"\n files = {}\n\n def process_path(file_path, imf):\n \"\"\"Processes the file path and the file.\"\"\"\n if file_path:\n file_path = file_path.replace(\n settings.MEDIA_URL,\n os.path.join(settings.MEDIA_ROOT, '')\n )\n mime_type = guess_type(imf.name)\n files[field_name] = (\n imf.name,\n ''.join([c for c in imf.chunks()]),\n mime_type[0] if mime_type else ''\n )\n\n for field_name, imf in request.FILES.items():\n try:\n file_path = serializer.validated_data.get(field_name, '')\n process_path(file_path, imf)\n except Exception as err:\n file_path = extract_file_path(imf.name)\n process_path(file_path, imf)\n\n return files\n\nfobi_integration_form_handlers.py\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nRegister the plugin. Note the name pattern `fobi_integration_form_handlers`.\n\n.. code-block:: python\n\n from fobi.base import integration_form_handler_plugin_registry\n from .base import MailHandlerPlugin\n\n integration_form_handler_plugin_registry.register(MailHandlerPlugin)\n\nDon't forget to list your plugin in the ``INSTALLED_APPS`` afterwards.\n\nPermissions\n===========\nPlugin system allows administrators to specify the access rights to every\nplugin. `django-fobi` permissions are based on Django Users and User Groups.\nAccess rights are manageable via Django admin (\"/admin/fobi/formelement/\",\n\"/admin/fobi/formhandler/\"). If user doesn't have the rights to access plugin,\nit doesn't appear on his form even if has been added to it (imagine, you have\nonce granted the right to use the news plugin to all users, but later on\ndecided to limit it to Staff members group only). Note, that superusers have\naccess to all plugins.\n\n.. code-block:: text\n\n Plugin access rights management interface in Django admin\n\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 `Plugin` \u2502 `Users` \u2502 `Groups` \u2502\n \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 Text \u2502 John Doe \u2502 Form builder users \u2502\n \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 Textarea \u2502 \u2502 Form builder users \u2502\n \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 File \u2502 Oscar, John Doe \u2502 Staff members \u2502\n \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 URL \u2502 \u2502 Form builder users \u2502\n \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 Hidden \u2502 \u2502 Form builder users \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\nManagement commands\n===================\nThere are several management commands available.\n\n- `fobi_find_broken_entries`. Find broken form element/handler entries that\n occur when some plugin which did exist in the system, no longer exists.\n- `fobi_sync_plugins`. Should be ran each time a new plugin is being added to\n the `django-fobi`.\n- `fobi_update_plugin_data`. A mechanism to update existing plugin data in\n case if it had become invalid after a change in a plugin. In order for it\n to work, each plugin should implement and ``update`` method, in which the\n data update happens.\n\nTuning\n======\nThere are number of `django-fobi` settings you can override in the settings\nmodule of your Django project:\n\n- `FOBI_RESTRICT_PLUGIN_ACCESS` (bool): If set to True, (Django) permission\n system for dash plugins is enabled. Defaults to True. Setting this to False\n makes all plugins available for all users.\n- `FOBI_DEFAULT_THEME` (str): Active (default) theme UID. Defaults to\n \"bootstrap3\".\n- `FORM_HANDLER_PLUGINS_EXECUTION_ORDER` (list of tuples): Order in which the\n form handlers are executed. See the \"Prioritise the execution order\"\n section for details.\n\nFor tuning of specific contrib plugin, see the docs in the plugin directory.\n\nBundled plugins and themes\n==========================\n`django-fobi` ships with number of bundled form element- and form handler-\nplugins, as well as themes which are ready to be used as is.\n\nBundled form element plugins\n----------------------------\nBelow a short overview of the form element plugins. See the README.rst file\nin directory of each plugin for details.\n\nFields\n~~~~~~\nFields marked with asterisk (*) fall under the definition of text elements.\nIt's possible to provide `Dynamic initial values`_ for text elements.\n\n- `Boolean (checkbox)\n `_\n- `Date\n `_\n- `DateTime\n `_\n- `Date drop down (year, month, day selection drop-downs)\n `_\n- `Decimal\n `_\n- `Duration\n `_\n- `Email*\n `_\n- `File\n `_\n- `Float\n `_\n- `Hidden*\n