Merge branch 'master' into wagtail/integration

This commit is contained in:
Artur Barseghyan 2017-03-16 22:13:10 +01:00
commit d84977fe96
25 changed files with 351 additions and 219 deletions

View file

@ -15,6 +15,22 @@ are used for versioning (schema follows below):
0.3.4 to 0.4).
- All backwards incompatible changes are mentioned in this document.
0.10.8
------
yyyy-mm-dd (not yet released)
- Documentation fixes.
- PEP8 code fixes.
- Minor setup fixes related to moved screen-shots file.
- Added helper scripts to test with Firefox in headless mode. Describe
testing with Firefox in headless mode in documentation.
0.10.7
------
2017-03-13
- Several Django deprecation/moves fixes for better future compatibility.
0.10.6
------
2017-02-14

View file

@ -16,14 +16,14 @@ Installation and configuration
------------------------------
Install the package in your environment.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: none
.. code-block:: sh
pip install django-fobi
INSTALLED_APPS
^^^^^^^^^^^^^^
Add ``fobi`` core and the plugins to the ``INSTALLED_APPS`` of the your
`settings` module.
``settings`` module.
1. The core.
@ -47,7 +47,7 @@ Add ``fobi`` core and the plugins to the ``INSTALLED_APPS`` of the your
<https://github.com/barseghyanartur/django-fobi/tree/stable/src/fobi/contrib/plugins/form_elements/security/recaptcha/>`_
would require additional packages to be installed. If so, make sure to have
installed and configured those dependencies prior adding the dependant
add-ons to the `settings` module.
add-ons to the ``settings`` module.
.. code-block:: python
@ -82,7 +82,7 @@ Add ``fobi`` core and the plugins to the ``INSTALLED_APPS`` of the your
.. code-block:: python
'easy_thumbnails', # Required by `content_image` plugin
'easy_thumbnails', # Required by `content_image` plugin
'fobi.contrib.plugins.form_elements.content.content_image',
'fobi.contrib.plugins.form_elements.content.content_text',
'fobi.contrib.plugins.form_elements.content.content_video',
@ -145,7 +145,7 @@ Putting all together, you would have something like this.
'fobi.contrib.plugins.form_elements.fields.url',
# Form element plugins
'easy_thumbnails', # Required by `content_image` plugin
'easy_thumbnails', # Required by ``content_image`` plugin
'fobi.contrib.plugins.form_elements.content.content_image',
'fobi.contrib.plugins.form_elements.content.content_text',
'fobi.contrib.plugins.form_elements.content.content_video',
@ -162,21 +162,21 @@ TEMPLATE_CONTEXT_PROCESSORS
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Add ``django.core.context_processors.request`` and
``fobi.context_processors.theme`` to ``TEMPLATE_CONTEXT_PROCESSORS`` of
your `settings` module.
your ``settings`` module.
.. code-block:: python
TEMPLATE_CONTEXT_PROCESSORS = (
# ...
"django.core.context_processors.request",
"fobi.context_processors.theme", # Obligatory
"fobi.context_processors.dynamic_values", # Optional
"fobi.context_processors.theme", # Obligatory
"fobi.context_processors.dynamic_values", # Optional
# ...
)
urlpatterns
^^^^^^^^^^^
Add the following line to ``urlpatterns`` of your `urls` module.
Add the following line to ``urlpatterns`` of your ``urls`` module.
.. code-block:: python
@ -201,21 +201,21 @@ Update the database
1. First you should be syncing/migrating the database. Depending on your
Django version and migration app, this step may vary. Typically as follows:
.. code-block:: none
.. code-block:: sh
$ ./manage.py syncdb
$ ./manage.py migrate --fake-initial
./manage.py syncdb
./manage.py migrate --fake-initial
2. Sync installed ``fobi`` plugins. Go to terminal and type the following
command.
.. code-block:: none
.. code-block:: sh
$ ./manage.py fobi_sync_plugins
./manage.py fobi_sync_plugins
Specify the active theme
^^^^^^^^^^^^^^^^^^^^^^^^
Specify the default theme in your `settings` module.
Specify the default theme in your ``settings`` module.
.. code-block:: python

View file

@ -302,8 +302,9 @@ Add the following line to urlpatterns of your `urls` module.
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.
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
@ -344,8 +345,8 @@ There are several properties, each textarea should have. They are:
- `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.
Let's name that plugin ``sample_textarea``. The plugin directory should then
have the following structure.
.. code-block:: sh
@ -368,7 +369,7 @@ 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
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
@ -475,11 +476,11 @@ 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.
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`.
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.
@ -631,7 +632,7 @@ 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
Let's name that plugin ``sample_mail``. The plugin directory should then have
the following structure.
.. code-block:: text
@ -704,11 +705,11 @@ 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.
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`.
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.
@ -837,8 +838,8 @@ 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.
Let's name that plugin ``sample_importer``. The plugin directory should then
have the following structure.
.. code-block:: text
@ -938,7 +939,7 @@ Required imports.
from django import forms
from django.utils.translation import ugettext_lazy as _
from sample_service_api import sample_api # Just an imaginary API client
from sample_service_api import sample_api # Just an imaginary API client
Defining the form for Sample importer plugin.
@ -988,7 +989,8 @@ Required imports.
from formtools.wizard.views import SessionWizardView
from path.to.sample_importer.forms import (
SampleImporterStep1Form, SampleImporterStep2Form
SampleImporterStep1Form,
SampleImporterStep2Form,
)
Defining the wizard view for Sample importer plugin.
@ -1076,9 +1078,8 @@ 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.
Let's place the callback in the ``foo`` module. The plugin directory should
then have the following structure.
.. code-block:: text
@ -1162,7 +1163,7 @@ themes:
- "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`)
interface in style of ``djangocms-admin-style``)
Obviously, there are two sorts of views when it comes to editing and viewing
the form.
@ -1192,7 +1193,7 @@ 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
Let's place the theme in the ``sample_theme`` module. The theme directory
should then have the following structure.
.. code-block:: text
@ -1292,7 +1293,7 @@ Registering the ``SampleTheme`` plugin.
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`).
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:
@ -1742,7 +1743,7 @@ The following HTML5 fields are supported in corresponding bundled plugins:
- placeholder
- type
With the `fobi.contrib.plugins.form_elements.fields.input` support for
With the ``fobi.contrib.plugins.form_elements.fields.input`` support for
HTML5 fields is extended to the following fields:
- autocomplete
@ -1769,7 +1770,7 @@ Dynamic initial values
It's possible to provide a dynamic initial value for any of the text elements.
In order to do that, you should use the build-in context processor or make
your own one. The only requirement is that you should store all values that
should be exposes in the form as a dict for `fobi_dynamic_values` dictionary
should be exposes in the form as a dict for ``fobi_dynamic_values`` dictionary
key. Beware, that passing the original request object might be unsafe in
many ways. Currently, a stripped down version of the request object is being
passed as a context variable.
@ -1793,7 +1794,8 @@ passed as a context variable.
}
}
In your GUI, you should be refering to the initial values in the following way:
In your GUI, you should be referring to the initial values in the following
way:
.. code-block:: html
@ -1807,21 +1809,21 @@ Currently, the following variables are available in the
- request: Stripped HttpRequest object.
- request.path: A string representing the full path to the requested page,
not including the scheme or domain.
- request.path: A string representing the full path to the requested
page, not including the scheme or domain.
- request.get_full_path(): Returns the path, plus an appended query string,
if applicable.
- request.get_full_path(): Returns the path, plus an appended query
string, if applicable.
- request.is_secure(): Returns True if the request is secure; that is, if
it was made with HTTPS.
- request.is_secure(): Returns True if the request is secure; that
is, if it was made with HTTPS.
- request.is_ajax(): Returns True if the request was made via an
XMLHttpRequest, by checking the HTTP_X_REQUESTED_WITH header for the
string 'XMLHttpRequest'.
- request.META: A stripped down standard Python dictionary containing the
available HTTP headers.
- request.META: A stripped down standard Python dictionary containing
the available HTTP headers.
- HTTP_ACCEPT_ENCODING: Acceptable encodings for the response.
@ -1833,7 +1835,7 @@ Currently, the following variables are available in the
- HTTP_USER_AGENT: The clients user-agent string.
- QUERY_STRING: The query string, as a single (unparsed) string.
- QUERY_STRING: The query string, as a single (un-parsed) string.
- REMOTE_ADDR: The IP address of the client.
@ -1841,9 +1843,9 @@ Currently, the following variables are available in the
- request.user.email:
- request.user.get_username(): Returns the username for the user. Since
the User model can be swapped out, you should use this method
instead of referencing the username attribute directly.
- request.user.get_username(): Returns the username for the user.
Since the User model can be swapped out, you should use this
method instead of referencing the username attribute directly.
- request.user.get_full_name(): Returns the first_name plus the
last_name, with a space in between.
@ -2038,10 +2040,11 @@ install the test requirements:
Browser tests
-------------
For browser tests you may choose between Firefox and PhantomJS. PhantomJS is
faster, Firefox tests tell you more. Both cases require some effort and both
have disadvantages regarding the installation (although once you have them
installed they work perfect).
For browser tests you may choose between Firefox, headless Firefox and
PhantomJS. PhantomJS is faster, headless Firefox is fast as well, but
normal Firefox tests tell you more (as you see what exactly happens on the
screen). Both cases require some effort and both have disadvantages regarding
the installation (although once you have them installed they work perfect).
Latest versions of Firefox are often not supported by Selenium. Current
version of the Selenium for Python (2.53.6) works fine with Firefox 47.
@ -2063,10 +2066,31 @@ Set up Firefox 47
FIREFOX_BIN_PATH = '/usr/lib/firefox47/firefox'
If you set ``FIREFOX_BIN_PATH`` to None, system Firefox would be used.
If you set to use system Firefox, remove or comment-out the
``FIREFOX_BIN_PATH`` setting.
After that your Selenium tests would work.
Set up headless Firefox
~~~~~~~~~~~~~~~~~~~~~~~
1. Install ``xvfb`` package which is used to start Firefox in headless mode.
.. code-block:: sh
sudo apt-get install xvfb
2. Run the tests using headless Firefox.
.. code-block:: sh
./scripts/runtests.sh
Or run tox tests using headless Firefox.
.. code-block:: sh
./scripts/tox.sh
Setup PhantomJS
~~~~~~~~~~~~~~~
You could also run tests in headless mode (faster). For that you will need
@ -2088,14 +2112,14 @@ PhantomJS.
PHANTOM_JS_EXECUTABLE_PATH = ""
If you want to use Firefox for testing, set
``PHANTOM_JS_EXECUTABLE_PATH`` to None.
If you want to use Firefox for testing, remove or comment-out the
``PHANTOM_JS_EXECUTABLE_PATH`` setting.
Troubleshooting
===============
If you get a ``FormElementPluginDoesNotExist`` or a
``FormHandlerPluginDoesNotExist`` exception, make sure you have listed your
plugin in the `settings` module of your project.
plugin in the ``settings`` module of your project.
License
=======

View file

@ -302,8 +302,9 @@ Add the following line to urlpatterns of your `urls` module.
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.
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
@ -344,8 +345,8 @@ There are several properties, each textarea should have. They are:
- `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.
Let's name that plugin ``sample_textarea``. The plugin directory should then
have the following structure.
.. code-block:: sh
@ -368,7 +369,7 @@ 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
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
@ -475,11 +476,11 @@ 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.
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`.
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.
@ -631,7 +632,7 @@ 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
Let's name that plugin ``sample_mail``. The plugin directory should then have
the following structure.
.. code-block:: text
@ -704,11 +705,11 @@ 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.
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`.
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.
@ -837,8 +838,8 @@ 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.
Let's name that plugin ``sample_importer``. The plugin directory should then
have the following structure.
.. code-block:: text
@ -938,7 +939,7 @@ Required imports.
from django import forms
from django.utils.translation import ugettext_lazy as _
from sample_service_api import sample_api # Just an imaginary API client
from sample_service_api import sample_api # Just an imaginary API client
Defining the form for Sample importer plugin.
@ -988,7 +989,8 @@ Required imports.
from formtools.wizard.views import SessionWizardView
from path.to.sample_importer.forms import (
SampleImporterStep1Form, SampleImporterStep2Form
SampleImporterStep1Form,
SampleImporterStep2Form,
)
Defining the wizard view for Sample importer plugin.
@ -1076,9 +1078,8 @@ 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.
Let's place the callback in the ``foo`` module. The plugin directory should
then have the following structure.
.. code-block:: text
@ -1162,7 +1163,7 @@ themes:
- "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`)
interface in style of ``djangocms-admin-style``)
Obviously, there are two sorts of views when it comes to editing and viewing
the form.
@ -1192,7 +1193,7 @@ 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
Let's place the theme in the ``sample_theme`` module. The theme directory
should then have the following structure.
.. code-block:: text
@ -1292,7 +1293,7 @@ Registering the ``SampleTheme`` plugin.
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`).
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:
@ -1742,7 +1743,7 @@ The following HTML5 fields are supported in corresponding bundled plugins:
- placeholder
- type
With the `fobi.contrib.plugins.form_elements.fields.input` support for
With the ``fobi.contrib.plugins.form_elements.fields.input`` support for
HTML5 fields is extended to the following fields:
- autocomplete
@ -1769,7 +1770,7 @@ Dynamic initial values
It's possible to provide a dynamic initial value for any of the text elements.
In order to do that, you should use the build-in context processor or make
your own one. The only requirement is that you should store all values that
should be exposes in the form as a dict for `fobi_dynamic_values` dictionary
should be exposes in the form as a dict for ``fobi_dynamic_values`` dictionary
key. Beware, that passing the original request object might be unsafe in
many ways. Currently, a stripped down version of the request object is being
passed as a context variable.
@ -1793,7 +1794,8 @@ passed as a context variable.
}
}
In your GUI, you should be refering to the initial values in the following way:
In your GUI, you should be referring to the initial values in the following
way:
.. code-block:: html
@ -1807,21 +1809,21 @@ Currently, the following variables are available in the
- request: Stripped HttpRequest object.
- request.path: A string representing the full path to the requested page,
not including the scheme or domain.
- request.path: A string representing the full path to the requested
page, not including the scheme or domain.
- request.get_full_path(): Returns the path, plus an appended query string,
if applicable.
- request.get_full_path(): Returns the path, plus an appended query
string, if applicable.
- request.is_secure(): Returns True if the request is secure; that is, if
it was made with HTTPS.
- request.is_secure(): Returns True if the request is secure; that
is, if it was made with HTTPS.
- request.is_ajax(): Returns True if the request was made via an
XMLHttpRequest, by checking the HTTP_X_REQUESTED_WITH header for the
string 'XMLHttpRequest'.
- request.META: A stripped down standard Python dictionary containing the
available HTTP headers.
- request.META: A stripped down standard Python dictionary containing
the available HTTP headers.
- HTTP_ACCEPT_ENCODING: Acceptable encodings for the response.
@ -1833,7 +1835,7 @@ Currently, the following variables are available in the
- HTTP_USER_AGENT: The clients user-agent string.
- QUERY_STRING: The query string, as a single (unparsed) string.
- QUERY_STRING: The query string, as a single (un-parsed) string.
- REMOTE_ADDR: The IP address of the client.
@ -1841,9 +1843,9 @@ Currently, the following variables are available in the
- request.user.email:
- request.user.get_username(): Returns the username for the user. Since
the User model can be swapped out, you should use this method
instead of referencing the username attribute directly.
- request.user.get_username(): Returns the username for the user.
Since the User model can be swapped out, you should use this
method instead of referencing the username attribute directly.
- request.user.get_full_name(): Returns the first_name plus the
last_name, with a space in between.
@ -2038,10 +2040,11 @@ install the test requirements:
Browser tests
-------------
For browser tests you may choose between Firefox and PhantomJS. PhantomJS is
faster, Firefox tests tell you more. Both cases require some effort and both
have disadvantages regarding the installation (although once you have them
installed they work perfect).
For browser tests you may choose between Firefox, headless Firefox and
PhantomJS. PhantomJS is faster, headless Firefox is fast as well, but
normal Firefox tests tell you more (as you see what exactly happens on the
screen). Both cases require some effort and both have disadvantages regarding
the installation (although once you have them installed they work perfect).
Latest versions of Firefox are often not supported by Selenium. Current
version of the Selenium for Python (2.53.6) works fine with Firefox 47.
@ -2063,10 +2066,31 @@ Set up Firefox 47
FIREFOX_BIN_PATH = '/usr/lib/firefox47/firefox'
If you set ``FIREFOX_BIN_PATH`` to None, system Firefox would be used.
If you set to use system Firefox, remove or comment-out the
``FIREFOX_BIN_PATH`` setting.
After that your Selenium tests would work.
Set up headless Firefox
~~~~~~~~~~~~~~~~~~~~~~~
1. Install ``xvfb`` package which is used to start Firefox in headless mode.
.. code-block:: sh
sudo apt-get install xvfb
2. Run the tests using headless Firefox.
.. code-block:: sh
./scripts/runtests.sh
Or run tox tests using headless Firefox.
.. code-block:: sh
./scripts/tox.sh
Setup PhantomJS
~~~~~~~~~~~~~~~
You could also run tests in headless mode (faster). For that you will need
@ -2088,14 +2112,14 @@ PhantomJS.
PHANTOM_JS_EXECUTABLE_PATH = ""
If you want to use Firefox for testing, set
``PHANTOM_JS_EXECUTABLE_PATH`` to None.
If you want to use Firefox for testing, remove or comment-out the
``PHANTOM_JS_EXECUTABLE_PATH`` setting.
Troubleshooting
===============
If you get a ``FormElementPluginDoesNotExist`` or a
``FormHandlerPluginDoesNotExist`` exception, make sure you have listed your
plugin in the `settings` module of your project.
plugin in the ``settings`` module of your project.
License
=======

View file

@ -16,14 +16,14 @@ Installation and configuration
------------------------------
Install the package in your environment.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: none
.. code-block:: sh
pip install django-fobi
INSTALLED_APPS
^^^^^^^^^^^^^^
Add ``fobi`` core and the plugins to the ``INSTALLED_APPS`` of the your
`settings` module.
``settings`` module.
1. The core.
@ -47,7 +47,7 @@ Add ``fobi`` core and the plugins to the ``INSTALLED_APPS`` of the your
<https://github.com/barseghyanartur/django-fobi/tree/stable/src/fobi/contrib/plugins/form_elements/security/recaptcha/>`_
would require additional packages to be installed. If so, make sure to have
installed and configured those dependencies prior adding the dependant
add-ons to the `settings` module.
add-ons to the ``settings`` module.
.. code-block:: python
@ -82,7 +82,7 @@ Add ``fobi`` core and the plugins to the ``INSTALLED_APPS`` of the your
.. code-block:: python
'easy_thumbnails', # Required by `content_image` plugin
'easy_thumbnails', # Required by `content_image` plugin
'fobi.contrib.plugins.form_elements.content.content_image',
'fobi.contrib.plugins.form_elements.content.content_text',
'fobi.contrib.plugins.form_elements.content.content_video',
@ -145,7 +145,7 @@ Putting all together, you would have something like this.
'fobi.contrib.plugins.form_elements.fields.url',
# Form element plugins
'easy_thumbnails', # Required by `content_image` plugin
'easy_thumbnails', # Required by ``content_image`` plugin
'fobi.contrib.plugins.form_elements.content.content_image',
'fobi.contrib.plugins.form_elements.content.content_text',
'fobi.contrib.plugins.form_elements.content.content_video',
@ -162,21 +162,21 @@ TEMPLATE_CONTEXT_PROCESSORS
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Add ``django.core.context_processors.request`` and
``fobi.context_processors.theme`` to ``TEMPLATE_CONTEXT_PROCESSORS`` of
your `settings` module.
your ``settings`` module.
.. code-block:: python
TEMPLATE_CONTEXT_PROCESSORS = (
# ...
"django.core.context_processors.request",
"fobi.context_processors.theme", # Obligatory
"fobi.context_processors.dynamic_values", # Optional
"fobi.context_processors.theme", # Obligatory
"fobi.context_processors.dynamic_values", # Optional
# ...
)
urlpatterns
^^^^^^^^^^^
Add the following line to ``urlpatterns`` of your `urls` module.
Add the following line to ``urlpatterns`` of your ``urls`` module.
.. code-block:: python
@ -201,21 +201,21 @@ Update the database
1. First you should be syncing/migrating the database. Depending on your
Django version and migration app, this step may vary. Typically as follows:
.. code-block:: none
.. code-block:: sh
$ ./manage.py syncdb
$ ./manage.py migrate --fake-initial
./manage.py syncdb
./manage.py migrate --fake-initial
2. Sync installed ``fobi`` plugins. Go to terminal and type the following
command.
.. code-block:: none
.. code-block:: sh
$ ./manage.py fobi_sync_plugins
./manage.py fobi_sync_plugins
Specify the active theme
^^^^^^^^^^^^^^^^^^^^^^^^
Specify the default theme in your `settings` module.
Specify the default theme in your ``settings`` module.
.. code-block:: python

1
scripts/runtests.sh Executable file
View file

@ -0,0 +1 @@
xvfb-run python runtests.py

1
scripts/tox.sh Executable file
View file

@ -0,0 +1 @@
xvfb-run python toxtests.py

View file

@ -4,7 +4,7 @@ import sys
from distutils.version import LooseVersion
from setuptools import setup, find_packages
version = '0.10.6'
version = '0.10.7'
# ***************************************************************************
# ************************** Python version *********************************
@ -78,7 +78,7 @@ except Exception as err:
try:
readme = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read()
screenshots = open(
os.path.join(os.path.dirname(__file__), 'SCREENSHOTS.rst')
os.path.join(os.path.dirname(__file__), 'docs/screenshots.rst.distrib')
).read()
screenshots = screenshots.replace(
'.. image:: _static',

View file

@ -1,6 +1,6 @@
__title__ = 'django-fobi'
__version__ = '0.10.6'
__build__ = 0x000079
__version__ = '0.10.7'
__build__ = 0x00007a
__author__ = 'Artur Barseghyan <artur.barseghyan@gmail.com>'
__copyright__ = '2014-2017 Artur Barseghyan'
__license__ = 'GPL 2.0/LGPL 2.1'

View file

@ -81,7 +81,7 @@ def base_bulk_change_plugins(PluginForm, named_url, modeladmin, request,
post = dict(request.POST)
if selected:
post['selected_plugins'] = ','.join(selected)
if 'POST' == request.method:
if request.method == 'POST':
form = PluginForm(
data=post,
files=request.FILES,
@ -455,7 +455,7 @@ class BasePluginModelAdmin(admin.ModelAdmin):
This is where the data is actually processed.
"""
changelist_named_url = self._get_changelist_named_url()
if 'POST' == request.method:
if request.method == 'POST':
form_cls = self._get_bulk_change_form_class()
form = form_cls(
data=request.POST,

View file

@ -2,9 +2,10 @@ import datetime
import simplejson as json
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from nine.versions import DJANGO_GTE_1_10
from fobi.base import (
# General
get_processed_form_data,
@ -24,6 +25,11 @@ from fobi.helpers import get_form_element_entries_for_form_wizard_entry
from . import UID
from .models import SavedFormDataEntry, SavedFormWizardDataEntry
if DJANGO_GTE_1_10:
from django.urls import reverse
else:
from django.core.urlresolvers import reverse
__title__ = 'fobi.contrib.plugins.form_handlers.db_store.fobi_form_handlers'
__author__ = 'Artur Barseghyan <artur.barseghyan@gmail.com>'
__copyright__ = '2014-2017 Artur Barseghyan'

View file

@ -1,16 +1,22 @@
import logging
import mailchimp
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
import mailchimp
from fobi.wizard import SessionWizardView
from nine.versions import DJANGO_GTE_1_10
from .forms import MailchimpAPIKeyForm, MailchimpListIDForm
if DJANGO_GTE_1_10:
from django.urls import reverse
else:
from django.core.urlresolvers import reverse
__title__ = 'fobi.contrib.plugins.form_importers.mailchimp_importer.views'
__author__ = 'Artur Barseghyan <artur.barseghyan@gmail.com>'
__copyright__ = '2014-2017 Artur Barseghyan'

View file

@ -1,21 +1,28 @@
from six import with_metaclass
from django.core.urlresolvers import reverse
from django.forms.forms import BaseForm
from django.forms.widgets import media_property
from django.http import HttpResponseRedirect
from nine.versions import DJANGO_GTE_1_7, DJANGO_GTE_1_8
from nine.versions import (
DJANGO_GTE_1_7,
DJANGO_GTE_1_8,
DJANGO_GTE_1_10,
)
from six import with_metaclass
from .constants import WIZARD_TYPE_COOKIE, WIZARD_TYPE_SESSION
if DJANGO_GTE_1_8:
from formtools.wizard.views import (
WizardView, SessionWizardView, CookieWizardView
CookieWizardView,
SessionWizardView,
WizardView,
)
else:
from django.contrib.formtools.wizard.views import (
WizardView, SessionWizardView, CookieWizardView
CookieWizardView,
SessionWizardView,
WizardView,
)
if DJANGO_GTE_1_7:
@ -23,6 +30,11 @@ if DJANGO_GTE_1_7:
else:
from django.utils.datastructures import SortedDict as OrderedDict
if DJANGO_GTE_1_10:
from django.urls import reverse
else:
from django.core.urlresolvers import reverse
__title__ = 'fobi.dynamic'
__author__ = 'Artur Barseghyan <artur.barseghyan@gmail.com>'
__copyright__ = '2014-2017 Artur Barseghyan'

View file

@ -1,12 +1,17 @@
from six import text_type
from nine.versions import DJANGO_GTE_1_10
import simplejson as json
from django.core.urlresolvers import reverse
from six import text_type
from .base import BaseRegistry
from .discover import autodiscover
if DJANGO_GTE_1_10:
from django.urls import reverse
else:
from django.core.urlresolvers import reverse
__title__ = 'fobi.form_importers'
__author__ = 'Artur Barseghyan <artur.barseghyan@gmail.com>'
__copyright__ = '2014-2017 Artur Barseghyan'

View file

@ -3,47 +3,50 @@ Helpers module. This module can be safely imported from any fobi (sub)module,
since it never imports from any of the fobi (sub)modules (except for the
`fobi.constants` and `fobi.exceptions` modules).
"""
import os
import glob
import logging
import uuid
import os
import shutil
from six import text_type, PY3
import simplejson as json
from django.conf import settings
from django.core.urlresolvers import reverse
from django.core.files.base import File
from django.contrib.contenttypes.models import ContentType
from django.db.utils import DatabaseError
from django.utils.encoding import force_text
from django.utils.html import format_html_join
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import AnonymousUser
from django.test.client import RequestFactory
from django.http import HttpResponse
import uuid
from autoslug.settings import slugify
from nine.user import User
from nine.versions import DJANGO_GTE_1_7
from django import forms
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
# from django.contrib.contenttypes.models import ContentType
from django.core.files.base import File
# from django.db.utils import DatabaseError
from django.http import HttpResponse
from django.test.client import RequestFactory
from django.utils.encoding import force_text
from django.utils.html import format_html_join
from django.utils.translation import ugettext_lazy as _
from fobi.constants import (
SUBMIT_VALUE_AS_VAL,
from nine.user import User
from nine.versions import DJANGO_GTE_1_7, DJANGO_GTE_1_10
import simplejson as json
from six import text_type, PY3
from .constants import (
SUBMIT_VALUE_AS_MIX,
SUBMIT_VALUE_AS_REPR,
SUBMIT_VALUE_AS_MIX
SUBMIT_VALUE_AS_VAL,
)
from fobi.exceptions import ImproperlyConfigured
from .exceptions import ImproperlyConfigured
if DJANGO_GTE_1_7:
import django.apps
else:
from django.db import models
if DJANGO_GTE_1_10:
from django.urls import reverse
else:
from django.core.urlresolvers import reverse
__title__ = 'fobi.helpers'
__author__ = 'Artur Barseghyan <artur.barseghyan@gmail.com>'
__copyright__ = '2014-2017 Artur Barseghyan'
@ -873,7 +876,7 @@ def get_wizard_form_field_value_from_request(request,
return value
# Then try POST
if 'POST' == request.method:
if request.method == 'POST':
value = get_wizard_form_field_value_from_post(
request,
wizard_view_name,
@ -884,7 +887,7 @@ def get_wizard_form_field_value_from_request(request,
else:
# First try POST
if 'POST' == request.method:
if request.method == 'POST':
value = get_wizard_form_field_value_from_post(
request,
wizard_view_name,

View file

@ -1,4 +1,4 @@
from fobi.base import get_theme
from ..base import get_theme
__title__ = 'fobi.integration.helpers'
__author__ = 'Artur Barseghyan <artur.barseghyan@gmail.com>'

View file

@ -5,18 +5,22 @@ from django.template import RequestContext
from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _
from fobi.base import (
fire_form_callbacks, run_form_handlers,
submit_plugin_form_data, get_theme
from ..base import (
fire_form_callbacks,
get_theme,
run_form_handlers,
submit_plugin_form_data,
)
from fobi.constants import (
CALLBACK_BEFORE_FORM_VALIDATION, CALLBACK_FORM_INVALID,
CALLBACK_FORM_VALID_BEFORE_SUBMIT_PLUGIN_FORM_DATA, CALLBACK_FORM_VALID,
CALLBACK_FORM_VALID_AFTER_FORM_HANDLERS
from ..constants import (
CALLBACK_BEFORE_FORM_VALIDATION,
CALLBACK_FORM_INVALID,
CALLBACK_FORM_VALID,
CALLBACK_FORM_VALID_AFTER_FORM_HANDLERS,
CALLBACK_FORM_VALID_BEFORE_SUBMIT_PLUGIN_FORM_DATA,
)
from fobi.dynamic import assemble_form_class
from fobi.exceptions import ImproperlyConfigured
from fobi.settings import GET_PARAM_INITIAL_DATA
from ..dynamic import assemble_form_class
from ..exceptions import ImproperlyConfigured
from ..settings import GET_PARAM_INITIAL_DATA
__title__ = 'fobi.integration.processors'
__author__ = 'Artur Barseghyan <artur.barseghyan@gmail.com>'
@ -70,6 +74,7 @@ class IntegrationProcessor(object):
)
def get_context_data(self, request, instance, **kwargs):
"""Get context data."""
context = {
'form_entry': instance.form_entry,
}
@ -77,12 +82,15 @@ class IntegrationProcessor(object):
return context
def get_form_template_name(self, request, instance):
"""Get form template name."""
return instance.form_template_name or None
def get_success_page_template_name(self, request, instance):
"""Get succes page template name."""
return instance.success_page_template_name or None
def get_login_required_template_name(self, request, instance):
"""Get login required template name."""
return self.login_required_template_name or None
def _process_form(self, request, instance, **kwargs):
@ -121,7 +129,7 @@ class IntegrationProcessor(object):
request=request
)
if 'POST' == request.method:
if request.method == 'POST':
form = FormClass(request.POST, request.FILES)
# Fire pre form validation callbacks
@ -133,8 +141,7 @@ class IntegrationProcessor(object):
)
if form.is_valid():
# Fire form valid callbacks, before handling sufrom
# django.http import HttpResponseRedirectbmitted plugin
# Fire form valid callbacks, before handling submitted plugin
# form data
form = fire_form_callbacks(
form_entry=instance.form_entry,

View file

@ -2,13 +2,14 @@ from __future__ import absolute_import
import logging
from autoslug import AutoSlugField
from django.conf import settings
from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.translation import ugettext_lazy as _
from autoslug import AutoSlugField
from nine.versions import DJANGO_GTE_1_10
from six import python_2_unicode_compatible
@ -18,10 +19,15 @@ from .base import (
form_wizard_handler_plugin_registry,
get_registered_form_element_plugins,
get_registered_form_handler_plugins,
get_registered_form_wizard_handler_plugins
get_registered_form_wizard_handler_plugins,
)
from .constants import WIZARD_TYPES, DEFAULT_WIZARD_TYPE
if DJANGO_GTE_1_10:
from django.urls import reverse
else:
from django.core.urlresolvers import reverse
__title__ = 'fobi.models'
__author__ = 'Artur Barseghyan <artur.barseghyan@gmail.com>'
__copyright__ = '2014-2017 Artur Barseghyan'

View file

@ -30,7 +30,7 @@ __all__ = (
THEME = get_theme(request=None, as_instance=True)
REGISTER = Library()
register = Library()
# *****************************************************************************
# *****************************************************************************
@ -63,7 +63,7 @@ class GetFobiPluginNode(Node):
return ''
@REGISTER.tag
@register.tag
def get_fobi_plugin(parser, token):
"""Get the plugin.
@ -126,7 +126,7 @@ class GetFobiFormHandlerPluginCustomActionsNode(Node):
return ''
@REGISTER.tag
@register.tag
def get_fobi_form_handler_plugin_custom_actions(parser, token):
"""Get the form handler plugin custom actions.
@ -192,7 +192,7 @@ class GetFobiFormWizardHandlerPluginCustomActionsNode(Node):
return ''
@REGISTER.tag
@register.tag
def get_fobi_form_wizard_handler_plugin_custom_actions(parser, token):
"""Get the form wizard handler plugin custom actions.
@ -275,12 +275,12 @@ def render_auth_link(context):
}
REGISTER.inclusion_tag(
register.inclusion_tag(
'fobi/snippets/render_auth_link.html', takes_context=True
)(render_auth_link)
@REGISTER.inclusion_tag(THEME.forms_list_template, takes_context=True)
@register.inclusion_tag(THEME.forms_list_template, takes_context=True)
def render_fobi_forms_list(context, queryset, *args, **kwargs):
"""Render the list of fobi forms.
@ -366,7 +366,7 @@ class HasEditFormEntryPermissionsNode(Node):
return False
@REGISTER.tag
@register.tag
def has_edit_form_entry_permissions(parser, token):
"""Checks the permissions
@ -456,7 +456,7 @@ class GetFormFieldTypeNode(Node):
return ''
@REGISTER.tag
@register.tag
def get_form_field_type(parser, token):
"""Get form field type.
@ -515,7 +515,7 @@ class GetFormHiddenFieldsErrorsNode(Node):
return ''
@REGISTER.tag
@register.tag
def get_form_hidden_fields_errors(parser, token):
"""Get form hidden fields errors.

View file

@ -20,7 +20,7 @@ except ImportError:
from django.utils.safestring import mark_safe, EscapeData, SafeData
from django.utils.timezone import template_localtime
REGISTER = Library()
register = Library()
def render_value_in_context(value, context):
"""Render value in context.
@ -56,7 +56,7 @@ except ImportError:
return render_value_in_context(value, context)
return ''
@REGISTER.tag
@register.tag
def firstof(parser, token, escape=False):
"""Outputs the first variable passed that is not False.

View file

@ -11,12 +11,13 @@ from selenium.webdriver.support.wait import WebDriverWait
from selenium.common.exceptions import WebDriverException
from django.core.management import call_command
from django.core.urlresolvers import reverse
from django.test import LiveServerTestCase
from django.conf import settings
from fobi.models import FormEntry
from nine.versions import DJANGO_GTE_1_10
from . import constants
from .base import print_info, skip
from .data import (
@ -31,6 +32,10 @@ from .helpers import (
phantom_js_clean_up
)
if DJANGO_GTE_1_10:
from django.urls import reverse
else:
from django.core.urlresolvers import reverse
__title__ = 'fobi.tests.test_browser_build_dynamic_forms'
__author__ = 'Artur Barseghyan <artur.barseghyan@gmail.com>'

View file

@ -3,18 +3,19 @@ Another helper module. This module can NOT be safely imported from any fobi
(sub)module - thus should be imported carefully.
"""
import datetime
import os
import logging
from six import PY3
import os
from django.conf import settings
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.forms.widgets import TextInput
from django.utils.encoding import force_text
from django.utils.translation import ugettext, ugettext_lazy as _
from nine.versions import DJANGO_GTE_1_10
from six import PY3
from .base import (
form_element_plugin_registry,
form_handler_plugin_registry,
@ -42,6 +43,11 @@ from .models import (
)
from .settings import RESTRICT_PLUGIN_ACCESS, DEBUG, WIZARD_FILES_UPLOAD_DIR
if DJANGO_GTE_1_10:
from django.urls import reverse
else:
from django.core.urlresolvers import reverse
__title__ = 'fobi.utils'
__author__ = 'Artur Barseghyan <artur.barseghyan@gmail.com>'
__copyright__ = '2014-2017 Artur Barseghyan'

View file

@ -15,7 +15,6 @@ from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.core.exceptions import ObjectDoesNotExist
from django.core.files.storage import FileSystemStorage
from django.core.urlresolvers import reverse
from django.forms import ValidationError
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import redirect
@ -87,7 +86,9 @@ from .wizard import DynamicSessionWizardView, DynamicCookieWizardView
if versions.DJANGO_GTE_1_10:
from django.shortcuts import render
from django.urls import reverse
else:
from django.core.urlresolvers import reverse
from django.shortcuts import render_to_response
if versions.DJANGO_GTE_1_8:
@ -352,7 +353,7 @@ def create_form_entry(request, theme=None, template_name=None):
:param str template_name:
:return django.http.HttpResponse:
"""
if 'POST' == request.method:
if request.method == 'POST':
form = FormEntryForm(request.POST, request.FILES, request=request)
if form.is_valid():
form_entry = form.save(commit=False)
@ -429,7 +430,7 @@ def edit_form_entry(request, form_entry_id, theme=None, template_name=None):
except ObjectDoesNotExist as err:
raise Http404(ugettext("Form entry not found."))
if 'POST' == request.method:
if request.method == 'POST':
# The form entry form (does not contain form elements)
form = FormEntryForm(request.POST, request.FILES, instance=form_entry,
request=request)
@ -672,7 +673,7 @@ def add_form_element_entry(request,
save_object = True
# If POST
elif 'POST' == request.method:
elif request.method == 'POST':
# If element has a form
form = form_element_plugin.get_initialised_create_form_or_404(
data=request.POST,
@ -759,7 +760,7 @@ def edit_form_element_entry(request,
:param django.http.HttpRequest request:
:param int form_element_entry_id:
:param fobi.base.BaseTheme: Theme instance.
:param fobi.base.BaseTheme theme: Theme instance.
:param string template_name:
:return django.http.HttpResponse:
"""
@ -787,7 +788,7 @@ def edit_form_element_entry(request,
)
return redirect('fobi.edit_form_entry', form_entry_id=form_entry.pk)
elif 'POST' == request.method:
elif request.method == 'POST':
form = form_element_plugin.get_initialised_edit_form_or_404(
data=request.POST,
files=request.FILES
@ -942,7 +943,7 @@ def add_form_handler_entry(request,
if not form_handler_plugin_form_cls:
save_object = True
elif 'POST' == request.method:
elif request.method == 'POST':
form = form_handler_plugin.get_initialised_create_form_or_404(
data=request.POST,
files=request.FILES
@ -1042,7 +1043,7 @@ def edit_form_handler_entry(request,
)
return redirect('fobi.edit_form_entry', form_entry_id=form_entry.pk)
elif 'POST' == request.method:
elif request.method == 'POST':
form = form_handler_plugin.get_initialised_edit_form_or_404(
data=request.POST,
files=request.FILES
@ -1146,7 +1147,7 @@ def create_form_wizard_entry(request, theme=None, template_name=None):
:param str template_name:
:return django.http.HttpResponse:
"""
if 'POST' == request.method:
if request.method == 'POST':
form = FormWizardEntryForm(request.POST,
request.FILES,
request=request)
@ -1231,7 +1232,7 @@ def edit_form_wizard_entry(request, form_wizard_entry_id, theme=None,
except ObjectDoesNotExist as err:
raise Http404(ugettext("Form wizard entry not found."))
if 'POST' == request.method:
if request.method == 'POST':
# The form entry form (does not contain form elements)
form = FormWizardEntryForm(request.POST, request.FILES,
instance=form_wizard_entry,
@ -1952,7 +1953,7 @@ def add_form_wizard_handler_entry(request,
if not form_wizard_handler_plugin_form_cls:
save_object = True
elif 'POST' == request.method:
elif request.method == 'POST':
form = form_wizard_handler_plugin.get_initialised_create_form_or_404(
data=request.POST,
files=request.FILES
@ -2059,7 +2060,7 @@ def edit_form_wizard_handler_entry(request,
form_wizard_entry_id=form_wizard_entry.pk
)
elif 'POST' == request.method:
elif request.method == 'POST':
form = form_wizard_handler_plugin.get_initialised_edit_form_or_404(
data=request.POST,
files=request.FILES
@ -2176,7 +2177,7 @@ def view_form_entry(request, form_entry_slug, theme=None, template_name=None):
request=request
)
if 'POST' == request.method:
if request.method == 'POST':
form = form_cls(request.POST, request.FILES)
# Fire pre form validation callbacks
@ -2405,7 +2406,7 @@ def import_form_entry(request, template_name=None):
:param string template_name:
:return django.http.HttpResponse:
"""
if 'POST' == request.method:
if request.method == 'POST':
form = ImportFormEntryForm(request.POST, request.FILES)
if form.is_valid():
@ -2610,7 +2611,7 @@ def import_form_wizard_entry(request, template_name=None):
:param string template_name:
:return django.http.HttpResponse:
"""
if 'POST' == request.method:
if request.method == 'POST':
form = ImportFormWizardEntryForm(request.POST, request.FILES)
if form.is_valid():

View file

@ -4,7 +4,6 @@ import re
from django import forms
from django.shortcuts import redirect
from django.core.urlresolvers import reverse
from django.forms import formsets, ValidationError
from django.views.generic import TemplateView
from django.utils.decorators import classonlymethod
@ -24,6 +23,11 @@ else:
)
from django.contrib.formtools.wizard.forms import ManagementForm
if versions.DJANGO_GTE_1_10:
from django.urls import reverse
else:
from django.core.urlresolvers import reverse
logger = logging.getLogger(__name__)
__all__ = (

5
toxtests.py Normal file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env python
from tox.session import main
if __name__ == "__main__":
main()