diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a791ff582..5b625e7e2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,61 @@ are used for versioning (schema follows below): 0.3.4 to 0.4). - All backwards incompatible changes are mentioned in this document. +0.19 +---- +2022-07-11 + +- Introduce class based views. Function based views are still supported + and will be supported until at least 0.23. + + Migration to class based views is simple. Only your project's ``urls.py`` + would change: + + .. code-block:: python + + urlpatterns = [ + # ... + url(r'^fobi/', include('fobi.urls.class_based.view')), + url(r'^fobi/', include('fobi.urls.class_based.edit')), + # ... + ] + + To use function based views, simply replace the previous line with: + + .. code-block:: python + + urlpatterns = [ + # ... + url(r'^fobi/', include('fobi.urls.view')), + url(r'^fobi/', include('fobi.urls.edit')), + # ... + ] + +- Class-based permissions (work only in combination with class-based views). + + Example: + + .. code-block:: python + + from fobi.permissions.definitions import edit_form_entry_permissions + from fobi.permissions.generic import BasePermission + from fobi.permissions.helpers import ( + any_permission_required_func, login_required, + ) + + class EditFormEntryPermission(BasePermission): + """Permission to edit form entries.""" + + def has_permission(self, request, view) -> bool: + return login_required(request) and any_permission_required_func( + edit_form_entry_permissions + )(request.user) + + def has_object_permission(self, request, view, obj) -> bool: + return login_required(request) and any_permission_required_func( + edit_form_entry_permissions + )(request.user) and obj.user == request.user + 0.18 ---- 2022-06-23 diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..72829e42f --- /dev/null +++ b/Makefile @@ -0,0 +1,107 @@ +.PHONY: help clean + +define BROWSER_PYSCRIPT +import os, webbrowser, sys + +from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + +BROWSER := python -c "$$BROWSER_PYSCRIPT" + +help: + @echo "clean | Remove all build, test, coverage and Python artifacts" + @echo "clean-build | Remove build artifacts" + @echo "clean-pyc | Remove Python file artifacts" + @echo "clean-test | Remove test and coverage artifacts" + @echo "run | Run the project in Docker" + +clean: clean-build clean-pyc clean-test + +clean-build: + rm -rf build/ + rm -rf dist/ + rm -rf **/*.egg-info + rm -rf static/CACHE + +clean-pyc: + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: + rm -rf .pytest_cache; \ + rm -rf .ipython/profile_default; \ + rm -rf htmlcov; \ + rm -rf build; \ + rm -f .coverage; \ + rm -f coverage.xml; \ + rm -f junit.xml; \ + rm -rf .hypothesis; \ + find . -name '*.py,cover' -exec rm -f {} + + +fix-file-permissions: + sudo chown $$USER:$$USER src/fobi/migrations/ -R || true + sudo chown $$USER:$$USER src/fobi/contrib/apps/djangocms_integration/migrations/ -R || true + sudo chown $$USER:$$USER src/fobi/contrib/apps/wagtail_integration/migrations/ -R || true + sudo chown $$USER:$$USER src/fobi/contrib/form_handlers/db_store/migrations/ -R || true + sudo chown $$USER:$$USER examples/simple/page/migrations/ -R || true + sudo chown $$USER:$$USER tmp/ -R || true + +run: prepare-required-files + docker-compose -f docker-compose.yml up --remove-orphans; + +build: prepare-required-files + docker-compose -f docker-compose.yml build; + +build-%: prepare-required-files + docker-compose -f docker-compose.yml build $*; + +stop: + docker-compose -f docker-compose.yml stop; + +make-migrations: + docker-compose -f docker-compose.yml exec backend ./manage.py makemigrations $(APP); + +migrate: + docker-compose -f docker-compose.yml exec backend ./manage.py migrate $(APP); + +test: + docker-compose -f docker-compose.yml exec backend pytest /backend/src/$(TEST_PATH); + +show-migrations: + docker-compose -f docker-compose.yml exec backend ./manage.py showmigrations + +show-urls: + docker-compose -f docker-compose.yml exec backend ./manage.py show_urls + +shell: + docker-compose -f docker-compose.yml exec backend python examples/simple/manage.py shell + +create-superuser: + docker-compose -f docker-compose.yml exec backend python examples/simple/manage.py createsuperuser + +fobi-sync-plugins: + docker-compose -f docker-compose.yml exec backend ./manage.py fobi_sync_plugins + +pip-install: + docker-compose -f docker-compose.yml exec backend pip install -r requirements/local.txt + +pip-list: + docker-compose -f docker-compose.yml exec backend pip list + +black: + docker-compose -f docker-compose.yml exec backend black . + +isort: + docker-compose -f docker-compose.yml exec backend isort . --overwrite-in-place + +bash: + docker-compose -f docker-compose.yml run backend /bin/bash + +prepare-required-files: + mkdir -p examples/logs examples/db examples/media examples/media/static examples/media/fobi_plugins/content_image + mkdir -p examples/media/fobi_plugins/file diff --git a/README.rst b/README.rst index 47829e49e..4f4556892 100644 --- a/README.rst +++ b/README.rst @@ -94,6 +94,9 @@ Main features and highlights - Developer-friendly API, which allows to edit existing or build new form fields and handlers without touching the core. - Support for custom user model. +- Class based views (and class-based permissions). Forms have an + owner (``auth.User``). Default permissions are set for the owner, but + class-based views provide a lot of freedom and can be easily customized. - `Theming`_. There are 4 ready to use `Bundled themes`_: "Bootstrap 3", "Foundation 5", "Simple" (with editing interface in style of Django admin) and "DjangoCMS admin style" theme (which is another simple theme with editing @@ -127,7 +130,6 @@ Roadmap Some of the upcoming/in-development features/improvements are: - Implement disabling forms based on dates. -- Class based views. - Cloning of forms. - JSON schema support. - Webpack integration. @@ -1179,6 +1181,61 @@ Add the callback module to ``INSTALLED_APPS``. # ... ) +Class-based views +================= +Views +----- +Migration to class based views is simple. Only your project's ``urls.py`` +would change: + +.. code-block:: python + + urlpatterns = [ + # ... + url(r'^fobi/', include('fobi.urls.class_based.view')), + url(r'^fobi/', include('fobi.urls.class_based.edit')), + # ... + ] + +To use function based views, simply replace the previous line with: + +.. code-block:: python + + urlpatterns = [ + # ... + url(r'^fobi/', include('fobi.urls.view')), + url(r'^fobi/', include('fobi.urls.edit')), + # ... + ] + +Permissions +----------- + +Class-based permissions work only in combination with class-based views. + +Example: + +.. code-block:: python + + from fobi.permissions.definitions import edit_form_entry_permissions + from fobi.permissions.generic import BasePermission + from fobi.permissions.helpers import ( + any_permission_required_func, login_required, + ) + + class EditFormEntryPermission(BasePermission): + """Permission to edit form entries.""" + + def has_permission(self, request, view) -> bool: + return login_required(request) and any_permission_required_func( + edit_form_entry_permissions + )(request.user) + + def has_object_permission(self, request, view, obj) -> bool: + return login_required(request) and any_permission_required_func( + edit_form_entry_permissions + )(request.user) and obj.user == request.user + Suggestions =========== Custom action for the form diff --git a/docs/changelog.rst b/docs/changelog.rst index bc62e44d0..5b625e7e2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,71 @@ are used for versioning (schema follows below): 0.3.4 to 0.4). - All backwards incompatible changes are mentioned in this document. +0.19 +---- +2022-07-11 + +- Introduce class based views. Function based views are still supported + and will be supported until at least 0.23. + + Migration to class based views is simple. Only your project's ``urls.py`` + would change: + + .. code-block:: python + + urlpatterns = [ + # ... + url(r'^fobi/', include('fobi.urls.class_based.view')), + url(r'^fobi/', include('fobi.urls.class_based.edit')), + # ... + ] + + To use function based views, simply replace the previous line with: + + .. code-block:: python + + urlpatterns = [ + # ... + url(r'^fobi/', include('fobi.urls.view')), + url(r'^fobi/', include('fobi.urls.edit')), + # ... + ] + +- Class-based permissions (work only in combination with class-based views). + + Example: + + .. code-block:: python + + from fobi.permissions.definitions import edit_form_entry_permissions + from fobi.permissions.generic import BasePermission + from fobi.permissions.helpers import ( + any_permission_required_func, login_required, + ) + + class EditFormEntryPermission(BasePermission): + """Permission to edit form entries.""" + + def has_permission(self, request, view) -> bool: + return login_required(request) and any_permission_required_func( + edit_form_entry_permissions + )(request.user) + + def has_object_permission(self, request, view, obj) -> bool: + return login_required(request) and any_permission_required_func( + edit_form_entry_permissions + )(request.user) and obj.user == request.user + +0.18 +---- +2022-06-23 + +- Tested against Python 3.9, Django 3.2 and 4.0. + +.. note:: + + Release dedicated to my dear son, Tigran, who turned 10 recently. + 0.17.1 ------ 2021-01-25 diff --git a/docs/index.rst b/docs/index.rst index cb94b2602..d3f9b2afb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -34,8 +34,8 @@ handling the submitted form data). Prerequisites ============= -- Django 2.2, 3.0 and 3.1. -- Python 3.5, 3.6, 3.7, 3.8 and 3.9. +- Django 2.2, 3.0, 3.1, 3.2 and 4.0. +- Python 3.6, 3.7, 3.8 and 3.9. Key concepts ============ @@ -94,6 +94,9 @@ Main features and highlights - Developer-friendly API, which allows to edit existing or build new form fields and handlers without touching the core. - Support for custom user model. +- Class based views (and class-based permissions). Forms have an + owner (``auth.User``). Default permissions are set for the owner, but + class-based views provide a lot of freedom and can be easily customized. - `Theming`_. There are 4 ready to use `Bundled themes`_: "Bootstrap 3", "Foundation 5", "Simple" (with editing interface in style of Django admin) and "DjangoCMS admin style" theme (which is another simple theme with editing @@ -127,7 +130,6 @@ Roadmap Some of the upcoming/in-development features/improvements are: - Implement disabling forms based on dates. -- Class based views. - Cloning of forms. - JSON schema support. - Webpack integration. @@ -207,15 +209,15 @@ Installation (1) Install latest stable version from PyPI: -.. code-block:: sh + .. code-block:: sh - pip install django-fobi + pip install django-fobi -Or latest stable version from GitHub: + Or latest stable version from GitHub: -.. code-block:: sh + .. code-block:: sh - pip install https://github.com/barseghyanartur/django-fobi/archive/stable.tar.gz + pip install https://github.com/barseghyanartur/django-fobi/archive/stable.tar.gz (2) Add `fobi` to ``INSTALLED_APPS`` of the your projects' Django settings. Furthermore, all themes and plugins to be used, shall be added to the @@ -223,130 +225,130 @@ Or latest stable version from GitHub: dependencies, you should be mentioning those in the ``INSTALLED_APPS`` as well. -.. code-block:: python + .. code-block:: python - INSTALLED_APPS = ( - # Used by fobi - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.admin', + INSTALLED_APPS = ( + # Used by fobi + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', - # ... - # `django-fobi` core - 'fobi', - - # `django-fobi` themes - 'fobi.contrib.themes.bootstrap3', # Bootstrap 3 theme - 'fobi.contrib.themes.foundation5', # Foundation 5 theme - 'fobi.contrib.themes.simple', # Simple theme - - # `django-fobi` form elements - fields - 'fobi.contrib.plugins.form_elements.fields.boolean', - 'fobi.contrib.plugins.form_elements.fields.checkbox_select_multiple', - 'fobi.contrib.plugins.form_elements.fields.date', - 'fobi.contrib.plugins.form_elements.fields.date_drop_down', - 'fobi.contrib.plugins.form_elements.fields.datetime', - 'fobi.contrib.plugins.form_elements.fields.decimal', - 'fobi.contrib.plugins.form_elements.fields.email', - 'fobi.contrib.plugins.form_elements.fields.file', - 'fobi.contrib.plugins.form_elements.fields.float', - 'fobi.contrib.plugins.form_elements.fields.hidden', - 'fobi.contrib.plugins.form_elements.fields.input', - 'fobi.contrib.plugins.form_elements.fields.integer', - 'fobi.contrib.plugins.form_elements.fields.ip_address', - 'fobi.contrib.plugins.form_elements.fields.null_boolean', - 'fobi.contrib.plugins.form_elements.fields.password', - 'fobi.contrib.plugins.form_elements.fields.radio', - 'fobi.contrib.plugins.form_elements.fields.regex', - 'fobi.contrib.plugins.form_elements.fields.select', - 'fobi.contrib.plugins.form_elements.fields.select_model_object', - 'fobi.contrib.plugins.form_elements.fields.select_multiple', - 'fobi.contrib.plugins.form_elements.fields.select_multiple_model_objects', - 'fobi.contrib.plugins.form_elements.fields.slug', - 'fobi.contrib.plugins.form_elements.fields.text', - 'fobi.contrib.plugins.form_elements.fields.textarea', - 'fobi.contrib.plugins.form_elements.fields.time', - 'fobi.contrib.plugins.form_elements.fields.url', - - # `django-fobi` form elements - content elements - 'fobi.contrib.plugins.form_elements.test.dummy', - 'easy_thumbnails', # Required by `content_image` plugin - 'fobi.contrib.plugins.form_elements.content.content_image', - 'fobi.contrib.plugins.form_elements.content.content_image_url', - 'fobi.contrib.plugins.form_elements.content.content_text', - 'fobi.contrib.plugins.form_elements.content.content_video', - - # `django-fobi` form handlers - 'fobi.contrib.plugins.form_handlers.db_store', - 'fobi.contrib.plugins.form_handlers.http_repost', - 'fobi.contrib.plugins.form_handlers.mail', - 'fobi.contrib.plugins.form_handlers.mail_sender', - - # Other project specific apps - 'foo', # Test app - # ... - ) + # ... + # `django-fobi` core + 'fobi', + + # `django-fobi` themes + 'fobi.contrib.themes.bootstrap3', # Bootstrap 3 theme + 'fobi.contrib.themes.foundation5', # Foundation 5 theme + 'fobi.contrib.themes.simple', # Simple theme + + # `django-fobi` form elements - fields + 'fobi.contrib.plugins.form_elements.fields.boolean', + 'fobi.contrib.plugins.form_elements.fields.checkbox_select_multiple', + 'fobi.contrib.plugins.form_elements.fields.date', + 'fobi.contrib.plugins.form_elements.fields.date_drop_down', + 'fobi.contrib.plugins.form_elements.fields.datetime', + 'fobi.contrib.plugins.form_elements.fields.decimal', + 'fobi.contrib.plugins.form_elements.fields.email', + 'fobi.contrib.plugins.form_elements.fields.file', + 'fobi.contrib.plugins.form_elements.fields.float', + 'fobi.contrib.plugins.form_elements.fields.hidden', + 'fobi.contrib.plugins.form_elements.fields.input', + 'fobi.contrib.plugins.form_elements.fields.integer', + 'fobi.contrib.plugins.form_elements.fields.ip_address', + 'fobi.contrib.plugins.form_elements.fields.null_boolean', + 'fobi.contrib.plugins.form_elements.fields.password', + 'fobi.contrib.plugins.form_elements.fields.radio', + 'fobi.contrib.plugins.form_elements.fields.regex', + 'fobi.contrib.plugins.form_elements.fields.select', + 'fobi.contrib.plugins.form_elements.fields.select_model_object', + 'fobi.contrib.plugins.form_elements.fields.select_multiple', + 'fobi.contrib.plugins.form_elements.fields.select_multiple_model_objects', + 'fobi.contrib.plugins.form_elements.fields.slug', + 'fobi.contrib.plugins.form_elements.fields.text', + 'fobi.contrib.plugins.form_elements.fields.textarea', + 'fobi.contrib.plugins.form_elements.fields.time', + 'fobi.contrib.plugins.form_elements.fields.url', + + # `django-fobi` form elements - content elements + 'fobi.contrib.plugins.form_elements.test.dummy', + 'easy_thumbnails', # Required by `content_image` plugin + 'fobi.contrib.plugins.form_elements.content.content_image', + 'fobi.contrib.plugins.form_elements.content.content_image_url', + 'fobi.contrib.plugins.form_elements.content.content_text', + 'fobi.contrib.plugins.form_elements.content.content_video', + + # `django-fobi` form handlers + 'fobi.contrib.plugins.form_handlers.db_store', + 'fobi.contrib.plugins.form_handlers.http_repost', + 'fobi.contrib.plugins.form_handlers.mail', + 'fobi.contrib.plugins.form_handlers.mail_sender', + + # Other project specific apps + 'foo', # Test app + # ... + ) (3) Make appropriate changes to the ``TEMPLATES`` of the your projects' Django settings. -And ``fobi.context_processors.theme`` and -``fobi.context_processors.dynamic_values``. See the following example. + And ``fobi.context_processors.theme`` and + ``fobi.context_processors.dynamic_values``. See the following example. -.. code-block:: python + .. code-block:: python - TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [(os.path.join('path', 'to', 'your', 'templates'))], - 'OPTIONS': { - 'context_processors': [ - "django.template.context_processors.debug", - 'django.template.context_processors.request', - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - "fobi.context_processors.theme", # Important! - "fobi.context_processors.dynamic_values", # Optional - ], - 'loaders': [ - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - 'admin_tools.template_loaders.Loader', - ], - 'debug': DEBUG_TEMPLATE, - } - }, - ] + TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [(os.path.join('path', 'to', 'your', 'templates'))], + 'OPTIONS': { + 'context_processors': [ + "django.template.context_processors.debug", + 'django.template.context_processors.request', + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "fobi.context_processors.theme", # Important! + "fobi.context_processors.dynamic_values", # Optional + ], + 'loaders': [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + 'admin_tools.template_loaders.Loader', + ], + 'debug': DEBUG_TEMPLATE, + } + }, + ] -Make sure that ``django.core.context_processors.request`` is in -``context_processors`` too. + Make sure that ``django.core.context_processors.request`` is in + ``context_processors`` too. (4) Configure URLs -Add the following line to urlpatterns of your `urls` module. + Add the following line to urlpatterns of your `urls` module. -.. code-block:: python + .. code-block:: python - # View URLs - url(r'^fobi/', include('fobi.urls.view')), + # View URLs + url(r'^fobi/', include('fobi.urls.view')), - # Edit URLs - url(r'^fobi/', include('fobi.urls.edit')), + # Edit URLs + url(r'^fobi/', include('fobi.urls.edit')), -Note, that some plugins require additional URL includes. For instance, if you -listed the ``fobi.contrib.plugins.form_handlers.db_store`` form handler plugin -in the ``INSTALLED_APPS``, you should mention the following in ``urls`` -module. + Note, that some plugins require additional URL includes. For instance, if + you listed the ``fobi.contrib.plugins.form_handlers.db_store`` form handler + plugin in the ``INSTALLED_APPS``, you should mention the following in + ``urls`` module. -.. code-block:: python + .. code-block:: python - # DB Store plugin URLs - url(r'^fobi/plugins/form-handlers/db-store/', - include('fobi.contrib.plugins.form_handlers.db_store.urls')), + # DB Store plugin URLs + url(r'^fobi/plugins/form-handlers/db-store/', + include('fobi.contrib.plugins.form_handlers.db_store.urls')), View URLs are put separately from edit URLs in order to make it possible to prefix the edit URLs differently. For example, if you're using the @@ -1179,6 +1181,61 @@ Add the callback module to ``INSTALLED_APPS``. # ... ) +Class-based views +================= +Views +----- +Migration to class based views is simple. Only your project's ``urls.py`` +would change: + +.. code-block:: python + + urlpatterns = [ + # ... + url(r'^fobi/', include('fobi.urls.class_based.view')), + url(r'^fobi/', include('fobi.urls.class_based.edit')), + # ... + ] + +To use function based views, simply replace the previous line with: + +.. code-block:: python + + urlpatterns = [ + # ... + url(r'^fobi/', include('fobi.urls.view')), + url(r'^fobi/', include('fobi.urls.edit')), + # ... + ] + +Permissions +----------- + +Class-based permissions work only in combination with class-based views. + +Example: + +.. code-block:: python + + from fobi.permissions.definitions import edit_form_entry_permissions + from fobi.permissions.generic import BasePermission + from fobi.permissions.helpers import ( + any_permission_required_func, login_required, + ) + + class EditFormEntryPermission(BasePermission): + """Permission to edit form entries.""" + + def has_permission(self, request, view) -> bool: + return login_required(request) and any_permission_required_func( + edit_form_entry_permissions + )(request.user) + + def has_object_permission(self, request, view, obj) -> bool: + return login_required(request) and any_permission_required_func( + edit_form_entry_permissions + )(request.user) and obj.user == request.user + Suggestions =========== Custom action for the form diff --git a/examples/simple/urls.py b/examples/simple/urls.py index 0d88b1bf3..e6c8064fb 100644 --- a/examples/simple/urls.py +++ b/examples/simple/urls.py @@ -43,10 +43,10 @@ # django-fobi URLs: # namespace='fobi' - url(r'^fobi/', include('fobi.urls.view')), + url(r'^fobi/', include('fobi.urls.class_based.view')), # namespace='fobi' url(r'^{0}fobi/'.format(FOBI_EDIT_URLS_PREFIX), - include('fobi.urls.edit')), + include('fobi.urls.class_based.edit')), url(r'^admin_tools/', include('admin_tools.urls')), diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..088642f98 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +# Example configuration for Black. + +# NOTE: you have to use single-quoted strings in TOML for regular expressions. +# It's the equivalent of r-strings in Python. Multiline strings are treated as +# verbose regular expressions by Black. Use [ ] to denote a significant space +# character. + +[tool.black] +line-length = 80 +target-version = ['py39'] +include = '\.pyi?$' +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | migrations + # The following are specific to Black, you probably don't want those. + | blib2to3 + | tests/data + | profiling +)/ +''' diff --git a/setup.cfg b/setup.cfg index d64948832..4cfb6351c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,6 +25,7 @@ ignore = max-line-length = 80 [isort] +line_length = 80 atomic = true combine_as_imports = true default_section = THIRDPARTY @@ -34,7 +35,7 @@ known_first_party = fobi known_third_party = django factory -multi_line_output = 5 +multi_line_output = 3 skip = docs, fabfile.py @@ -42,6 +43,9 @@ skip = wsgi.py, ./src/fobi/migrations/*.py, ./src/fobi/south_migrations/*.py +skip_glob = **/migrations/*.py +force_grid_wrap = 0 +use_parentheses = true [metadata] license-file = LICENSE_GPL2.0.txt diff --git a/setup.py b/setup.py index f2db0d891..a9c401d20 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from distutils.version import LooseVersion from setuptools import setup, find_packages -version = '0.18' +version = '0.19' # *************************************************************************** # ************************** Django version ********************************* diff --git a/src/fobi/__init__.py b/src/fobi/__init__.py index 16ac919c6..3c6b808b5 100644 --- a/src/fobi/__init__.py +++ b/src/fobi/__init__.py @@ -1,5 +1,5 @@ __title__ = 'django-fobi' -__version__ = '0.18' +__version__ = '0.19' __author__ = 'Artur Barseghyan ' __copyright__ = '2014-2022 Artur Barseghyan' __license__ = 'GPL 2.0/LGPL 2.1' diff --git a/src/fobi/permissions/__init__.py b/src/fobi/permissions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/fobi/permissions/default.py b/src/fobi/permissions/default.py new file mode 100644 index 000000000..8cca9ad2f --- /dev/null +++ b/src/fobi/permissions/default.py @@ -0,0 +1,193 @@ +from .definitions import ( + add_form_element_entry_permission, + add_form_handler_entry_permission, + add_form_wizard_form_entry_permission, + add_form_wizard_handler_entry_permission, + create_form_entry_permissions, + create_form_wizard_entry_permissions, + dashboard_permissions, + delete_form_element_entry_permission, + delete_form_entry_permissions, + delete_form_handler_entry_permission, + delete_form_wizard_entry_permissions, + delete_form_wizard_form_entry_permission, + delete_form_wizard_handler_entry_permission, + edit_form_element_entry_permission, + edit_form_entry_permissions, + edit_form_handler_entry_permission, + edit_form_wizard_entry_permissions, + edit_form_wizard_handler_entry_permission, + wizards_dashboard_permissions, +) +from .generic import AllowAnyPermission, BasePermission +from .helpers import ( + all_permissions_required_func, + any_permission_required_func, + login_required, + permissions_required_func, +) + +__title__ = "fobi.permissions.default" +__author__ = "Artur Barseghyan " +__copyright__ = "2022 Artur Barseghyan" +__license__ = "GPL 2.0/LGPL 2.1" +__all__ = ( + "CreateFormEntryPermission", + "EditFormEntryPermission", + "DeleteFormEntryPermission", + "AddFormElementEntryPermission", + "EditFormElementEntryPermission", + "DeleteFormElementEntryPermission", + "AddFormHandlerEntryPermission", + "EditFormHandlerEntryPermission", + "DeleteFormHandlerEntryPermission", + "ViewFormEntryPermission", +) + + +class CreateFormEntryPermission(BasePermission): + """Permission to create form entries.""" + + def has_permission(self, request, view) -> bool: + return login_required(request) and all_permissions_required_func( + create_form_entry_permissions + )(request.user) + + def has_object_permission(self, request, view, obj) -> bool: + return self.has_permission(request, view) + + +class EditFormEntryPermission(BasePermission): + """Permission to edit form entries.""" + + def has_permission(self, request, view) -> bool: + return login_required(request) and any_permission_required_func( + edit_form_entry_permissions + )(request.user) + + def has_object_permission(self, request, view, obj) -> bool: + return ( + login_required(request) + and any_permission_required_func(edit_form_entry_permissions)( + request.user + ) + and obj.user == request.user + ) + + +class DeleteFormEntryPermission(BasePermission): + """Permission to delete form entries.""" + + def has_permission(self, request, view) -> bool: + return login_required(request) and all_permissions_required_func( + delete_form_entry_permissions + )(request.user) + + def has_object_permission(self, request, view, obj) -> bool: + return ( + login_required(request) + and any_permission_required_func(delete_form_entry_permissions)( + request.user + ) + and obj.user == request.user + ) + + +class AddFormElementEntryPermission(BasePermission): + """Permission to add form element entries.""" + + def has_permission(self, request, view) -> bool: + return login_required(request) and permissions_required_func( + add_form_element_entry_permission + )(request.user) + + def has_object_permission(self, request, view, obj) -> bool: + return self.has_permission(request, view) + + +class EditFormElementEntryPermission(BasePermission): + """Permission to edit form element entries.""" + + def has_permission(self, request, view) -> bool: + return login_required(request) and permissions_required_func( + edit_form_element_entry_permission + )(request.user) + + def has_object_permission(self, request, view, obj) -> bool: + return ( + login_required(request) + and permissions_required_func(edit_form_element_entry_permission)( + request.user + ) + and obj.form_entry.user == request.user + ) + + +class DeleteFormElementEntryPermission(BasePermission): + """Permission to delete form element entries.""" + + def has_permission(self, request, view) -> bool: + return login_required(request) and permissions_required_func( + delete_form_element_entry_permission + )(request.user) + + def has_object_permission(self, request, view, obj) -> bool: + return ( + login_required(request) + and permissions_required_func(delete_form_element_entry_permission)( + request.user + ) + and obj.form_entry.user == request.user + ) + + +class AddFormHandlerEntryPermission(BasePermission): + """Permission to add form handler entries.""" + + def has_permission(self, request, view) -> bool: + return login_required(request) and permissions_required_func( + add_form_handler_entry_permission + )(request.user) + + def has_object_permission(self, request, view, obj) -> bool: + return self.has_permission(request, view) + + +class EditFormHandlerEntryPermission(BasePermission): + """Permission to edit form handler entries.""" + + def has_permission(self, request, view) -> bool: + return login_required(request) and permissions_required_func( + edit_form_handler_entry_permission + )(request.user) + + def has_object_permission(self, request, view, obj) -> bool: + return ( + login_required(request) + and permissions_required_func(edit_form_handler_entry_permission)( + request.user + ) + and obj.form_entry.user == request.user + ) + + +class DeleteFormHandlerEntryPermission(BasePermission): + """Permission to delete form handler entries.""" + + def has_permission(self, request, view) -> bool: + return login_required(request) and permissions_required_func( + delete_form_handler_entry_permission + )(request.user) + + def has_object_permission(self, request, view, obj) -> bool: + return ( + login_required(request) + and permissions_required_func(delete_form_handler_entry_permission)( + request.user + ) + and obj.form_entry.user == request.user + ) + + +class ViewFormEntryPermission(AllowAnyPermission): + """Permission to view form entries.""" diff --git a/src/fobi/permissions/definitions.py b/src/fobi/permissions/definitions.py new file mode 100644 index 000000000..01b897a76 --- /dev/null +++ b/src/fobi/permissions/definitions.py @@ -0,0 +1,125 @@ +__title__ = "fobi.permissions.definitions" +__author__ = "Artur Barseghyan " +__copyright__ = "2022 Artur Barseghyan" +__license__ = "GPL 2.0/LGPL 2.1" +__all__ = ( + "dashboard_permissions", + "wizards_dashboard_permissions", + "create_form_entry_permissions", + "edit_form_entry_permissions", + "delete_form_entry_permissions", + "add_form_element_entry_permission", + "edit_form_element_entry_permission", + "delete_form_element_entry_permission", + "add_form_handler_entry_permission", + "edit_form_handler_entry_permission", + "delete_form_handler_entry_permission", + "create_form_wizard_entry_permissions", + "edit_form_wizard_entry_permissions", + "delete_form_wizard_entry_permissions", + "add_form_wizard_form_entry_permission", + "delete_form_wizard_form_entry_permission", + "add_form_wizard_handler_entry_permission", + "edit_form_wizard_handler_entry_permission", + "delete_form_wizard_handler_entry_permission", +) + +# Used in `dashboard` view. +dashboard_permissions = [ + # Form + "fobi.add_formentry", + "fobi.change_formentry", + "fobi.delete_formentry", +] + +# Used in `form_wizards_dashboard` view. +wizards_dashboard_permissions = [ + # Form wizard + "fobi.add_formwizardentry", + "fobi.change_formwizardentry", + "fobi.delete_formwizardentry", +] + +# Used in `create_form_entry` view. +create_form_entry_permissions = [ + "fobi.add_formentry", + "fobi.add_formelemententry", + "fobi.add_formhandlerentry", +] + +# Used in `edit_form_entry` view. +edit_form_entry_permissions = [ + "fobi.change_formentry", + "fobi.change_formelemententry", + "fobi.change_formhandlerentry", + "fobi.add_formelemententry", + "fobi.add_formhandlerentry", + "fobi.delete_formelemententry", + "fobi.delete_formhandlerentry", +] + +# Used in `delete_form_entry` view. +delete_form_entry_permissions = [ + "fobi.delete_formentry", + "fobi.delete_formelemententry", + "fobi.delete_formhandlerentry", +] + +# Used in `add_form_element_entry` view. +add_form_element_entry_permission = "fobi.add_formelemententry" + +# Used in `edit_form_element_entry` view. +edit_form_element_entry_permission = "fobi.change_formelemententry" + +# Used in `delete_form_element_entry` view. +delete_form_element_entry_permission = "fobi.delete_formelemententry" + +# Used in `add_form_handler_entry` view. +add_form_handler_entry_permission = "fobi.add_formhandlerentry" + +# Used in `edit_form_handler_entry` view. +edit_form_handler_entry_permission = "fobi.change_formhandlerentry" + +# Used in `delete_form_handler_entry` view. +delete_form_handler_entry_permission = "fobi.delete_formhandlerentry" + +# Used in `create_form_wizard_entry` view. +create_form_wizard_entry_permissions = [ + "fobi.add_formwizardentry", + "fobi.add_formwizardformentry", + "fobi.add_formhandlerentry", +] + +# Used in `edit_form_wizard_entry` view. +edit_form_wizard_entry_permissions = [ + "fobi.change_formwizardentry", + "fobi.add_formwizardformentry", + "fobi.delete_formewizardformentry", + "fobi.add_formhandlerentry", + "fobi.change_formhandlerentry", + "fobi.delete_formhandlerentry", +] + +# Used in `delete_form_wizard_entry` view. +delete_form_wizard_entry_permissions = [ + "fobi.delete_formwizardentry", + "fobi.delete_formwizardformentry", + "fobi.delete_formwizardhandlerentry", +] + +# Used in `add_form_wizard_form_entry` view. +add_form_wizard_form_entry_permission = "fobi.add_formwizardformentry" + +# Used in `delete_form_wizard_form_entry` view. +delete_form_wizard_form_entry_permission = "fobi.delete_formwizardformentry" + +# Used in `add_form_wizard_handler_entry` view. +add_form_wizard_handler_entry_permission = "fobi.add_formwizardhandlerentry" + +# Used in `edit_form_wizard_handler_entry` view. +edit_form_wizard_handler_entry_permission = "fobi.change_formwizardhandlerentry" + +# Used in `delete_form_wizard_handler_entry` view. +delete_form_wizard_handler_entry_permission = ( + "fobi.delete_formwizardhandlerentry" +) diff --git a/src/fobi/permissions/generic.py b/src/fobi/permissions/generic.py new file mode 100644 index 000000000..f4e195182 --- /dev/null +++ b/src/fobi/permissions/generic.py @@ -0,0 +1,219 @@ +""" +Provides a set of pluggable permission policies. + +Inspired by Django REST Framework class-based permissions. + +`BasePermissions` forbids everything by default. +""" + +__title__ = "fobi.permissions.generic" +__author__ = "Artur Barseghyan " +__copyright__ = "2022 Artur Barseghyan" +__license__ = "GPL 2.0/LGPL 2.1" +__all__ = ( + "SAFE_METHODS", + "OperationHolderMixin", + "SingleOperandHolder", + "AND", + "OR", + "NOT", + "BasePermissionMetaclass", + "BasePermission", + "AllowAnyPermission", + "DenyAnyPermission", + "IsAuthenticatedPermission", + "IsAdminUserPermission", + "IsAuthenticatedOrReadOnlyPermission", + "IsSuperUserPermission", +) + +SAFE_METHODS = ("GET", "HEAD", "OPTIONS") + + +class OperationHolderMixin: + def __and__(self, other): + return OperandHolder(AND, self, other) + + def __or__(self, other): + return OperandHolder(OR, self, other) + + def __rand__(self, other): + return OperandHolder(AND, other, self) + + def __ror__(self, other): + return OperandHolder(OR, other, self) + + def __invert__(self): + return SingleOperandHolder(NOT, self) + + +class SingleOperandHolder(OperationHolderMixin): + def __init__(self, operator_class, op1_class): + self.operator_class = operator_class + self.op1_class = op1_class + + def __call__(self, *args, **kwargs): + op1 = self.op1_class(*args, **kwargs) + return self.operator_class(op1) + + +class OperandHolder(OperationHolderMixin): + def __init__(self, operator_class, op1_class, op2_class): + self.operator_class = operator_class + self.op1_class = op1_class + self.op2_class = op2_class + + def __call__(self, *args, **kwargs): + op1 = self.op1_class(*args, **kwargs) + op2 = self.op2_class(*args, **kwargs) + return self.operator_class(op1, op2) + + +class AND: + def __init__(self, op1, op2): + self.op1 = op1 + self.op2 = op2 + + def has_permission(self, request, view): + return self.op1.has_permission( + request, view + ) and self.op2.has_permission(request, view) + + def has_object_permission(self, request, view, obj): + return self.op1.has_object_permission( + request, view, obj + ) and self.op2.has_object_permission(request, view, obj) + + +class OR: + def __init__(self, op1, op2): + self.op1 = op1 + self.op2 = op2 + + def has_permission(self, request, view): + return self.op1.has_permission( + request, view + ) or self.op2.has_permission(request, view) + + def has_object_permission(self, request, view, obj): + return self.op1.has_object_permission( + request, view, obj + ) or self.op2.has_object_permission(request, view, obj) + + +class NOT: + def __init__(self, op1): + self.op1 = op1 + + def has_permission(self, request, view): + return not self.op1.has_permission(request, view) + + def has_object_permission(self, request, view, obj): + return not self.op1.has_object_permission(request, view, obj) + + +class BasePermissionMetaclass(OperationHolderMixin, type): + pass + + +class BasePermission(metaclass=BasePermissionMetaclass): + """ + A base class from which all permission classes should inherit. + """ + + def has_permission(self, request, view): + """ + Return `True` if permission is granted, `False` otherwise. + """ + return False + + def has_object_permission(self, request, view, obj): + """ + Return `True` if permission is granted, `False` otherwise. + """ + return False + + +class AllowAnyPermission(BasePermission): + """ + Allow any access. + This isn't strictly required, since you could use an empty + permission_classes list, but it's useful because it makes the intention + more explicit. + """ + + def has_permission(self, request, view): + return True + + def has_object_permission(self, request, view, obj): + """ + Return `True` if permission is granted, `False` otherwise. + """ + return True + + +class DenyAnyPermission(BasePermission): + """ + Deny any access. + This isn't strictly required, since you could use an empty + permission_classes list, but it's useful because it makes the intention + more explicit. + """ + + def has_permission(self, request, view): + return False + + def has_object_permission(self, request, view, obj): + return False + + +class IsAuthenticatedPermission(BasePermission): + """ + Allows access only to authenticated users. + """ + + def has_permission(self, request, view): + return bool(request.user and request.user.is_authenticated) + + def has_object_permission(self, request, view, obj): + return self.has_permission(request, view) + + +class IsAdminUserPermission(BasePermission): + """ + Allows access only to admin users. + """ + + def has_permission(self, request, view): + return bool(request.user and request.user.is_staff) + + def has_object_permission(self, request, view, obj): + return self.has_permission(request, view) + + +class IsSuperUserPermission(BasePermission): + """ + Allows access only to super-users. + """ + + def has_permission(self, request, view): + return bool(request.user and request.user.is_superuser) + + def has_object_permission(self, request, view, obj): + return self.has_permission(request, view) + + +class IsAuthenticatedOrReadOnlyPermission(BasePermission): + """ + The request is authenticated as a user, or is a read-only request. + """ + + def has_permission(self, request, view): + return bool( + request.method in SAFE_METHODS + or request.user + and request.user.is_authenticated + ) + + def has_object_permission(self, request, view, obj): + return self.has_permission(request, view) diff --git a/src/fobi/permissions/helpers.py b/src/fobi/permissions/helpers.py new file mode 100644 index 000000000..d5b108b6b --- /dev/null +++ b/src/fobi/permissions/helpers.py @@ -0,0 +1,63 @@ +from ..decorators import DEFAULT_SATISFY, SATISFY_ALL, SATISFY_ANY + +__title__ = "fobi.permissions.helpers" +__author__ = "Artur Barseghyan " +__copyright__ = "2022 Artur Barseghyan" +__license__ = "GPL 2.0/LGPL 2.1" +__all__ = ( + "login_required", + "permissions_required_func", + "all_permissions_required_func", + "any_permission_required_func", +) + + +def login_required(request) -> bool: + return bool(request.user and request.user.is_authenticated) + + +def permissions_required_func(perms, satisfy=DEFAULT_SATISFY) -> callable: + assert satisfy in (SATISFY_ANY, SATISFY_ALL) + + if isinstance(perms, str): + perms = (perms,) + + if SATISFY_ALL == satisfy: + # ``SATISFY_ALL`` case + def check_perms(user): + # First check if the user has the permission (even anon users) + if user.has_perms(perms): + return True + # As the last resort, show the login form + return False + + else: + # ``SATISFY_ANY`` case + def check_perms(user): + # First check if the user has the permission (even anon users) + for perm in perms: + if user.has_perm(perm): + return True + + # As the last resort, show the login form + return False + + return check_perms + + +def all_permissions_required_func(perms) -> callable: + """Check for the permissions given based on the strategy chosen. + + :param iterable perms: + :return bool: + """ + return permissions_required_func(perms, satisfy=SATISFY_ALL) + + +def any_permission_required_func(perms) -> callable: + """Check for the permissions given based on the strategy chosen. + + :param iterable perms: + :return bool: + """ + return permissions_required_func(perms, satisfy=SATISFY_ANY) diff --git a/src/fobi/tests/base.py b/src/fobi/tests/base.py index 42037b2cb..8df068705 100644 --- a/src/fobi/tests/base.py +++ b/src/fobi/tests/base.py @@ -1,4 +1,6 @@ +import os import logging +from datetime import datetime from selenium import webdriver from selenium.webdriver.common.action_chains import ActionChains @@ -219,3 +221,13 @@ def _scroll_page_bottom(self): """Scroll to the page bottom.""" html = self.driver.find_element_by_tag_name('html') html.send_keys(Keys.END) + + def take_screenshot(self, name="screenshot"): + now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + filename = os.path.join( + settings.STATIC_ROOT, + f"{name}-{now}.png" + ) + self.driver.get_screenshot_as_file(filename) + logger.debug(f"Screenshot saved in: {filename}") + return filename diff --git a/src/fobi/tests/data.py b/src/fobi/tests/data.py index 1d0d3248b..2d51691b4 100644 --- a/src/fobi/tests/data.py +++ b/src/fobi/tests/data.py @@ -249,7 +249,7 @@ TEST_FORM_FIELD_DATA = { 'test_boolean': True, # 'test_checkbox_select_multiple_input': '', - 'test_date_input': datetime.date.today().strftime("%Y-%m-%d"), + 'test_date_input': datetime.date.today().strftime("%d-%m-%Y"), 'test_datetime_input': datetime.datetime.now().strftime( "%Y-%m-%d %H:%M:%S" ), diff --git a/src/fobi/tests/test_browser_build_dynamic_forms.py b/src/fobi/tests/test_browser_build_dynamic_forms.py index 7296bf492..5f689788c 100644 --- a/src/fobi/tests/test_browser_build_dynamic_forms.py +++ b/src/fobi/tests/test_browser_build_dynamic_forms.py @@ -1,5 +1,7 @@ +import os import logging import unittest +from datetime import datetime # from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.wait import WebDriverWait @@ -7,6 +9,7 @@ from fobi.models import FormEntry +from django.conf import settings from django.urls import reverse from . import constants @@ -566,7 +569,7 @@ def test_2004_submit_form(self, wait=WAIT_FOR): for field_name, field_value in TEST_FORM_FIELD_DATA.items(): field_input = self.driver.find_element_by_name(field_name) field_input.send_keys(field_value) - + self.take_screenshot("filled_form_page") self._sleep(2) footer = self.driver.find_element_by_xpath('//footer') @@ -588,6 +591,8 @@ def test_2004_submit_form(self, wait=WAIT_FOR): self._sleep(2) + self.take_screenshot("filled_form_page_scroll") + submit_button.click() try: @@ -596,6 +601,11 @@ def test_2004_submit_form(self, wait=WAIT_FOR): pass # Wait until the submit success page opens a clear success message. + # TODO: This is really weird. Somehow it takes longer time to render + # the class based views than the correspondent function based views. + # find out why and fix. As temporary workaround, we're waiting + # twice as long as the normal timeout. + self.take_screenshot("submit_success_page") WebDriverWait(self.driver, timeout=TIMEOUT).until( lambda driver: driver.find_element_by_xpath( """//div[contains(text(), 'Form {0} was submitted """ diff --git a/src/fobi/urls/class_based/__init__.py b/src/fobi/urls/class_based/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/fobi/urls/class_based/edit.py b/src/fobi/urls/class_based/edit.py new file mode 100644 index 000000000..9e54ef736 --- /dev/null +++ b/src/fobi/urls/class_based/edit.py @@ -0,0 +1,240 @@ +from django.urls import re_path as url +from django.utils.translation import gettext_lazy as _ + +from ...views import ( + add_form_element_entry, + add_form_handler_entry, + add_form_wizard_form_entry, + add_form_wizard_handler_entry, + create_form_entry, + create_form_wizard_entry, + dashboard, + delete_form_element_entry, + delete_form_entry, + delete_form_handler_entry, + delete_form_wizard_entry, + delete_form_wizard_form_entry, + delete_form_wizard_handler_entry, + edit_form_element_entry, + edit_form_entry, + edit_form_handler_entry, + edit_form_wizard_entry, + edit_form_wizard_handler_entry, + export_form_entry, + export_form_wizard_entry, + form_importer, + form_wizards_dashboard, + import_form_entry, + import_form_wizard_entry, +) +from ...views.class_based import ( + AddFormElementEntryView, + AddFormHandlerEntryView, + CreateFormEntryView, + DeleteFormElementEntryView, + DeleteFormEntryView, + DeleteFormHandlerEntryView, + EditFormElementEntryView, + EditFormEntryView, + EditFormHandlerEntryView, +) + +__title__ = "fobi.urls.class_based.edit" +__author__ = "Artur Barseghyan " +__copyright__ = "2014-2022 Artur Barseghyan" +__license__ = "GPL 2.0/LGPL 2.1" +__all__ = ("urlpatterns",) + + +urlpatterns = [ + # *********************************************************************** + # **************************** Form entry CUD *************************** + # *********************************************************************** + # Create form entry + url( + _(r"^forms/create/$"), + # view=create_form_entry, + view=CreateFormEntryView.as_view(), + name="fobi.create_form_entry", + ), + # Edit form entry + url( + _(r"^forms/edit/(?P\d+)/$"), + # edit_form_entry, + view=EditFormEntryView.as_view(), + name="fobi.edit_form_entry", + ), + # Delete form entry + url( + _(r"^forms/delete/(?P\d+)/$"), + view=DeleteFormEntryView.as_view(), + name="fobi.delete_form_entry", + ), + # *********************************************************************** + # ************************ Form entry add-ons *************************** + # *********************************************************************** + # Export form entry + url( + _(r"^forms/export/(?P\d+)/$"), + export_form_entry, + name="fobi.export_form_entry", + ), + # Import form entry + url( + _(r"^forms/import/$"), import_form_entry, name="fobi.import_form_entry" + ), + # Form importers + url( + _(r"^forms/importer/(?P[\w_\-]+)/$"), + form_importer, + name="fobi.form_importer", + ), + # *********************************************************************** + # *********************** Form element entry CUD ************************ + # *********************************************************************** + # Add form element entry + url( + _( + r"^forms/elements/add/(?P\d+)/" + r"(?P[\w_\-]+)/$" + ), + # view=add_form_element_entry, + view=AddFormElementEntryView.as_view(), + name="fobi.add_form_element_entry", + ), + # Edit form element entry + url( + _(r"^forms/elements/edit/(?P\d+)/$"), + # view=edit_form_element_entry, + view=EditFormElementEntryView.as_view(), + name="fobi.edit_form_element_entry", + ), + # Delete form element entry + url( + _(r"^forms/elements/delete/(?P\d+)/$"), + # view=delete_form_element_entry, + view=DeleteFormElementEntryView.as_view(), + name="fobi.delete_form_element_entry", + ), + # *********************************************************************** + # *********************** Form handler entry CUD ************************ + # *********************************************************************** + # Add form handler entry + url( + _( + r"^forms/handlers/add/(?P\d+)/" + r"(?P[\w_\-]+)/$" + ), + # view=add_form_handler_entry, + view=AddFormHandlerEntryView.as_view(), + name="fobi.add_form_handler_entry", + ), + # Edit form handler entry + url( + _(r"^forms/handlers/edit/(?P\d+)/$"), + # view=edit_form_handler_entry, + view=EditFormHandlerEntryView.as_view(), + name="fobi.edit_form_handler_entry", + ), + # Delete form handler entry + url( + _(r"^forms/handlers/delete/(?P\d+)/$"), + # view=delete_form_handler_entry, + view=DeleteFormHandlerEntryView.as_view(), + name="fobi.delete_form_handler_entry", + ), + # *********************************************************************** + # ************************ Form wizard entry CUD ************************ + # *********************************************************************** + # Create form wizard entry + url( + _(r"^wizard/create/$"), + view=create_form_wizard_entry, + name="fobi.create_form_wizard_entry", + ), + # Edit form wizard entry + url( + _(r"^wizard/edit/(?P\d+)/$"), + edit_form_wizard_entry, + name="fobi.edit_form_wizard_entry", + ), + # Delete form wizard entry + url( + _(r"^wizard/delete/(?P\d+)/$"), + delete_form_wizard_entry, + name="fobi.delete_form_wizard_entry", + ), + # *********************************************************************** + # ******************** Form wizard form entry CUD *********************** + # *********************************************************************** + # Add form wizard form entry + url( + _( + r"^wizard/forms/add/(?P\d+)/" + r"(?P[\w_\-]+)/$" + ), + add_form_wizard_form_entry, + name="fobi.add_form_wizard_form_entry", + ), + # # Edit form wizard form entry + # url(_(r'^wizard/forms/edit/(?P\d+)/$'), + # edit_form_wizard_form_entry, + # name='fobi.edit_form_wizard_form_entry'), + # + # Delete form wizard form entry + url( + _(r"^wizard/elements/delete/(?P\d+)/$"), + delete_form_wizard_form_entry, + name="fobi.delete_form_wizard_form_entry", + ), + # *********************************************************************** + # ******************* Form wizard handler entry CUD ********************* + # *********************************************************************** + # Add form wizard handler entry + url( + _( + r"^wizard/handlers/add/(?P\d+)/" + r"(?P[\w_\-]+)/$" + ), + add_form_wizard_handler_entry, + name="fobi.add_form_wizard_handler_entry", + ), + # Edit form wizard handler entry + url( + _(r"^wizard/handlers/edit/(?P\d+)/$"), + edit_form_wizard_handler_entry, + name="fobi.edit_form_wizard_handler_entry", + ), + # Delete form wizard handler entry + url( + _(r"^wizard/handlers/delete/(?P\d+)/$"), + delete_form_wizard_handler_entry, + name="fobi.delete_form_wizard_handler_entry", + ), + # *********************************************************************** + # *********************** Form wizard entry add-ons ********************* + # *********************************************************************** + # Export form wizard entry + url( + _(r"^wizard/export/(?P\d+)/$"), + export_form_wizard_entry, + name="fobi.export_form_wizard_entry", + ), + # Import form wizard entry + url( + _(r"^wizard/import/$"), + import_form_wizard_entry, + name="fobi.import_form_wizard_entry", + ), + # *********************************************************************** + # ****************************** Dashboard ****************************** + # *********************************************************************** + # Forms dashboard + url(_(r"^$"), view=dashboard, name="fobi.dashboard"), + # Form wizards dashboard + url( + _(r"^wizards/$"), + view=form_wizards_dashboard, + name="fobi.form_wizards_dashboard", + ), +] diff --git a/src/fobi/urls/class_based/view.py b/src/fobi/urls/class_based/view.py new file mode 100644 index 000000000..b092eb628 --- /dev/null +++ b/src/fobi/urls/class_based/view.py @@ -0,0 +1,64 @@ +from django.urls import re_path as url +from django.utils.translation import gettext_lazy as _ + +from ...views import ( + FormWizardView, + form_entry_submitted, + form_wizard_entry_submitted, + view_form_entry, +) +from ...views.class_based import ViewFormEntrySubmittedView, ViewFormEntryView + +__title__ = "fobi.urls.class_based.view" +__author__ = "Artur Barseghyan " +__copyright__ = "2014-2022 Artur Barseghyan" +__license__ = "GPL 2.0/LGPL 2.1" +__all__ = ("urlpatterns",) + +urlpatterns = [ + # *********************************************************************** + # ****************************** Form entry ***************************** + # *********************************************************************** + # Form submitted success page + url( + _(r"^view/submitted/$"), + # view=form_entry_submitted, + view=ViewFormEntrySubmittedView.as_view(), + name="fobi.form_entry_submitted", + ), + # View form entry + url( + _(r"^view/(?P[\w_\-]+)/$"), + # view=view_form_entry, + view=ViewFormEntryView.as_view(), + name="fobi.view_form_entry", + ), + # Form submitted success page + url( + _(r"^view/(?P[\w_\-]+)/submitted/$"), + # view=form_entry_submitted, + view=ViewFormEntrySubmittedView.as_view(), + name="fobi.form_entry_submitted", + ), + # *********************************************************************** + # *************************** Form wizard entry ************************* + # *********************************************************************** + # Form wizard submitted success page + url( + _(r"^wizard-view/submitted/$"), + view=form_wizard_entry_submitted, + name="fobi.form_wizard_entry_submitted", + ), + # View form wizard entry + url( + _(r"^wizard-view/(?P[\w_\-]+)/$"), + FormWizardView.as_view(), + name="fobi.view_form_wizard_entry", + ), + # Form wizard submitted success page + url( + _(r"^wizard-view/(?P[\w_\-]+)/submitted/$"), + view=form_wizard_entry_submitted, + name="fobi.form_wizard_entry_submitted", + ), +] diff --git a/src/fobi/views/__init__.py b/src/fobi/views/__init__.py new file mode 100644 index 000000000..e17400acd --- /dev/null +++ b/src/fobi/views/__init__.py @@ -0,0 +1 @@ +from .function_based import * diff --git a/src/fobi/views/class_based.py b/src/fobi/views/class_based.py new file mode 100644 index 000000000..138e0e163 --- /dev/null +++ b/src/fobi/views/class_based.py @@ -0,0 +1,1772 @@ +import datetime +import json +import logging + +from django.contrib import messages +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.db import IntegrityError, models +from django.http import Http404 +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse_lazy +from django.utils.datastructures import MultiValueDictKeyError +from django.utils.translation import gettext_lazy as _ +from django.views import View +from django.views.generic import CreateView, DeleteView, DetailView, UpdateView +from django.views.generic.edit import DeletionMixin + +from ..base import ( # get_registered_form_handler_plugins + fire_form_callbacks, + form_element_plugin_registry, + form_handler_plugin_registry, + form_wizard_handler_plugin_registry, + get_theme, + run_form_handlers, + run_form_wizard_handlers, + submit_plugin_form_data, +) +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 ..dynamic import assemble_form_class +from ..forms import ( + FormElementEntryFormSet, + FormEntryForm, + FormWizardEntryForm, + FormWizardFormEntryFormSet, + ImportFormEntryForm, + ImportFormWizardEntryForm, +) +from ..models import ( + FormElementEntry, + FormEntry, + FormHandlerEntry, + FormWizardEntry, + FormWizardFormEntry, + FormWizardHandlerEntry, +) +from ..permissions.default import ( + AddFormElementEntryPermission, + AddFormHandlerEntryPermission, + CreateFormEntryPermission, + DeleteFormElementEntryPermission, + DeleteFormEntryPermission, + DeleteFormHandlerEntryPermission, + EditFormElementEntryPermission, + EditFormEntryPermission, + EditFormHandlerEntryPermission, + ViewFormEntryPermission, +) +from ..settings import DEBUG, GET_PARAM_INITIAL_DATA, SORT_PLUGINS_BY_VALUE +from ..utils import ( + append_edit_and_delete_links_to_field, + get_user_form_element_plugins_grouped, + get_user_form_field_plugin_uids, + get_user_form_handler_plugin_uids, + get_user_form_handler_plugins, + get_user_form_wizard_handler_plugin_uids, + get_user_form_wizard_handler_plugins, + get_wizard_files_upload_dir, + perform_form_entry_import, + prepare_form_entry_export_data, +) + +__all__ = ( + "PermissionMixin", + "AbstractDeletePluginEntryView", + "CreateFormEntryView", + "EditFormEntryView", + "DeleteFormEntryView", + "AddFormElementEntryView", + "EditFormElementEntryView", + "DeleteFormElementEntryView", + "AddFormHandlerEntryView", + "EditFormHandlerEntryView", + "DeleteFormHandlerEntryView", + "ViewFormEntryView", + "ViewFormEntrySubmittedView", +) + +logger = logging.getLogger(__name__) + + +# ***************************************************************************** +# ***************************************************************************** +# *********************************** Generic ********************************* +# ***************************************************************************** +# ***************************************************************************** + + +class PermissionMixin(View): + """Mixin for permission-based views.""" + + permission_classes: tuple = () + + def permission_denied(self, request, message=None, code=None): + """If request is not permitted, raise.""" + raise PermissionDenied() + + def get_permissions(self): + """Return initialized list of permissions required by this view.""" + return [permission() for permission in self.permission_classes] + + def dispatch(self, request, *args, **kwargs): + """Dispatch the request.""" + self.check_permissions(request) + return super(PermissionMixin, self).dispatch(request, *args, **kwargs) + + def check_permissions(self, request): + """Check if the request should be permitted. + + Raises an appropriate exception if the request is not permitted. + """ + for permission in self.get_permissions(): + if not permission.has_permission(request, self): + self.permission_denied( + request, + message=getattr(permission, "message", None), + code=getattr(permission, "code", None), + ) + + def check_object_permissions(self, request, obj): + """Check if the request should be permitted for a given object. + + Raises an appropriate exception if the request is not permitted. + """ + for permission in self.get_permissions(): + if not permission.has_object_permission(request, self, obj): + self.permission_denied( + request, + message=getattr(permission, "message", None), + code=getattr(permission, "code", None), + ) + + +class AbstractDeletePluginEntryView(PermissionMixin, DeleteView): + """Abstract delete view for plugin entries.""" + + pk_url_kwarg: str + get_user_plugin_uids_func: callable + message: str + html_anchor: str + + def _get_queryset(self, request): + """Get queryset.""" + return self.model._default_manager.select_related("form_entry").filter( + form_entry__user__pk=request.user.pk + ) + + def get_object(self, queryset=None): + """Get object.""" + # TODO: There's a tiny deviation from `_delete_plugin_entry` + # implementation. The message in the latter is fully custom, while + # in this case we're stuck to Django's own implementation. + # Comment added on 2022-07-10. + obj = get_object_or_404( + self._get_queryset(self.request), + pk=self.kwargs.get(self.pk_url_kwarg), + ) + self.check_object_permissions(self.request, obj) + return obj + + # Add support for browsers which only accept GET and POST for now. + def get(self, request, *args, **kwargs): + return self.delete(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + """Delete.""" + self.object = self.get_object() + self._run_before_plugin_entry_delete(request, self.object) + form_entry = self.object.form_entry + plugin = self.object.get_plugin(request=request) + plugin.request = request + + plugin._delete_plugin_data() + + self.object.delete() + + self._run_after_plugin_entry_delete( + request, self.kwargs.get("form_entry_id") + ) + messages.info(request, self.message.format(plugin.name)) + redirect_url = reverse_lazy( + "fobi.edit_form_entry", kwargs={"form_entry_id": form_entry.pk} + ) + return redirect("{0}{1}".format(redirect_url, self.html_anchor)) + + def _run_before_plugin_entry_delete(self, request, form_entry): + """Run just before plugin entry has been deleted.""" + try: + self.run_before_plugin_entry_delete(request, form_entry) + return True + except: + return False + + def run_before_plugin_entry_delete(self, request, form_entry): + """Run just before plugin entry has been deleted.""" + + def _run_after_plugin_entry_delete(self, request, form_entry_id): + """Run after plugin entry has been deleted.""" + try: + self.run_after_plugin_entry_delete(request, form_entry_id) + return True + except: + return False + + def run_after_plugin_entry_delete(self, request, form_entry_id): + """Run after plugin entry has been deleted.""" + + +# ***************************************************************************** +# ***************************************************************************** +# ********************************** Builder ********************************** +# ***************************************************************************** +# ***************************************************************************** + +# ***************************************************************************** +# **************************** Create form entry ****************************** +# ***************************************************************************** + + +class CreateFormEntryView(PermissionMixin, CreateView): + """Create form entry.""" + + template_name = None + form_class = FormEntryForm + theme = None + permission_classes = (CreateFormEntryPermission,) + + def get_context_data(self, **kwargs): + """Get context data.""" + context = super(CreateFormEntryView, self).get_context_data(**kwargs) + context["form"] = self.get_form() + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + + if theme: + context.update({"fobi_theme": theme}) + return context + + def get_template_names(self): + """Get template names.""" + template_name = self.template_name + if not template_name: + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + template_name = theme.create_form_entry_template + return [template_name] + + def get_form_kwargs(self): + kwargs = super(CreateFormEntryView, self).get_form_kwargs() + kwargs["request"] = self.request + return kwargs + + def get(self, request, *args, **kwargs): + """Handle GET requests: instantiate a blank version of the form.""" + self.object = None + return self.render_to_response(self.get_context_data()) + + def post(self, request, *args, **kwargs): + """ + Handle POST requests: instantiate a form instance with the passed + POST variables and then check if it's valid. + """ + self.object = None + form = self.get_form() + if form.is_valid(): + form_entry = form.save(commit=False) + form_entry.user = request.user + self._run_before_form_create(request, form_entry) + try: + form_entry.save() + self._run_after_form_create(request, form_entry) + messages.info( + request, + _("Form {0} was created successfully.").format( + form_entry.name + ), + ) + return redirect( + "fobi.edit_form_entry", form_entry_id=form_entry.pk + ) + except IntegrityError as err: + messages.info( + request, + _("Errors occurred while saving the form: {0}.").format( + str(err) + ), + ) + + return self.render_to_response(self.get_context_data()) + + def _run_before_form_create(self, request, form_entry): + """Run just before form_entry has been created/saved.""" + try: + self.run_before_form_create(request, form_entry) + return True + except: + return False + + def run_before_form_create(self, request, form_entry): + """Run just before form_entry has been created/saved.""" + + def _run_after_form_create(self, request, form_entry): + """Run after form_entry has been created/saved.""" + try: + self.run_after_form_create(request, form_entry) + return True + except: + return False + + def run_after_form_create(self, request, form_entry): + """Run after the form_entry has been created/saved.""" + + +# ************************************************************************** +# ******************************* Edit form entry ************************** +# ************************************************************************** + + +class EditFormEntryView(PermissionMixin, UpdateView): + """Edit form entry.""" + + template_name = None + form_class = FormEntryForm + theme = None + pk_url_kwarg = "form_entry_id" + permission_classes = (EditFormEntryPermission,) + + def get_context_data(self, **kwargs): + """Get context data.""" + context = super(EditFormEntryView, self).get_context_data(**kwargs) + + # In case of success, we don't need this (since redirect would happen). + # Thus, fetch only if needed. + form_elements = self.object.formelemententry_set.all() + form_handlers = self.object.formhandlerentry_set.all()[:] + used_form_handler_uids = [ + form_handler.plugin_uid for form_handler in form_handlers + ] + + # The code below (two lines below) is not really used at the moment, + # thus - comment out, but do not remove, as we might need it later on. + # all_form_entries = FormEntry._default_manager \ + # .only('id', 'name', 'slug') \ + # .filter(user__pk=request.user.pk) + + # List of form element plugins allowed to user + user_form_element_plugins = get_user_form_element_plugins_grouped( + self.request.user, sort_by_value=SORT_PLUGINS_BY_VALUE + ) + # List of form handler plugins allowed to user + user_form_handler_plugins = get_user_form_handler_plugins( + self.request.user, + exclude_used_singles=True, + used_form_handler_plugin_uids=used_form_handler_uids, + ) + + # Assembling the form for preview + form_cls = assemble_form_class( + self.object, + origin="edit_form_entry", + origin_kwargs_update_func=append_edit_and_delete_links_to_field, + request=self.request, + ) + + assembled_form = form_cls() + + # In debug mode, try to identify possible problems. + if DEBUG: + assembled_form.as_p() + else: + try: + assembled_form.as_p() + except Exception as err: + logger.error(err) + + # If no theme provided, pick a default one. + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + + if theme: + context.update({"fobi_theme": theme}) + + theme.collect_plugin_media(form_elements) + + context.update( + { + "form": self.get_form(), + "form_entry": self.object, + "form_elements": form_elements, + "form_handlers": form_handlers, + "user_form_element_plugins": user_form_element_plugins, + "user_form_handler_plugins": user_form_handler_plugins, + "assembled_form": assembled_form, + "fobi_theme": theme, + } + ) + + return context + + def get_template_names(self): + """Get template names.""" + template_name = self.template_name + if not template_name: + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + template_name = theme.edit_form_entry_template + return [template_name] + + def get_form_kwargs(self): + kwargs = super(EditFormEntryView, self).get_form_kwargs() + kwargs["request"] = self.request + return kwargs + + def _get_queryset(self, request): + """Get queryset.""" + return ( + FormEntry._default_manager.select_related("user") + .prefetch_related("formelemententry_set") + .filter(user__pk=request.user.pk) + ) + + def get_object(self, queryset=None): + """Get object.""" + obj = super(EditFormEntryView, self).get_object(queryset) + self.check_object_permissions(self.request, obj) + return obj + + def get(self, request, *args, **kwargs): + self.object = self.get_object(queryset=self._get_queryset(request)) + """Handle GET requests: instantiate a blank version of the form.""" + form_element_entry_formset = FormElementEntryFormSet( + queryset=self.object.formelemententry_set.all(), + # prefix='form_element' + ) + return self.render_to_response( + self.get_context_data( + form_element_entry_formset=form_element_entry_formset, + ) + ) + + def post(self, request, *args, **kwargs): + """ + Handle POST requests: instantiate a form instance with the passed + POST variables and then check if it's valid. + """ + self.object = self.get_object(queryset=self._get_queryset(request)) + form = self.get_form() + + # This is where we save ordering if it has been changed. + # The `FormElementEntryFormSet` contain ids and positions only. + if "ordering" in request.POST: + form_element_entry_formset = FormElementEntryFormSet( + request.POST, + request.FILES, + queryset=self.object.formelemententry_set.all(), + # prefix = 'form_element' + ) + # If form elements aren't properly made (developers's fault) + # there might be problems with saving the ordering - likely + # in case of hidden elements only. Thus, we want to avoid + # errors here. + try: + if form_element_entry_formset.is_valid(): + form_element_entry_formset.save() + messages.info( + request, _("Elements ordering edited successfully.") + ) + return redirect( + reverse_lazy( + "fobi.edit_form_entry", + kwargs={"form_entry_id": self.object.pk}, + ) + ) + except MultiValueDictKeyError as err: + messages.error( + request, + _( + "Errors occurred while trying to change the " + "elements ordering!" + ), + ) + return redirect( + reverse_lazy( + "fobi.edit_form_entry", + kwargs={"form_entry_id": self.object.pk}, + ) + ) + else: + form_element_entry_formset = FormElementEntryFormSet( + queryset=self.object.formelemententry_set.all(), + # prefix='form_element' + ) + + if form.is_valid(): + obj = form.save(commit=False) + obj.user = request.user + try: + obj.save() + messages.info( + request, + _("Form {0} was edited successfully.").format(obj.name), + ) + return redirect( + reverse_lazy( + "fobi.edit_form_entry", kwargs={"form_entry_id": obj.pk} + ) + ) + except IntegrityError as err: + messages.info( + request, + _("Errors occurred while saving the form: {0}.").format( + str(err) + ), + ) + + return self.render_to_response( + self.get_context_data( + form_element_entry_formset=form_element_entry_formset, + ) + ) + + +# ***************************************************************************** +# ********************************* Delete form entry ************************* +# ***************************************************************************** + + +class DeleteFormEntryView(PermissionMixin, DeletionMixin): + """Delete form entry.""" + + model = FormEntry + success_url = reverse_lazy("fobi.dashboard") + permission_classes = (DeleteFormEntryPermission,) + + def get_object(self, queryset=None): + """Get object.""" + obj = get_object_or_404( + FormEntry._default_manager.all(), + pk=self.kwargs.get("form_entry_id"), + user__pk=self.request.user.pk, + ) + self.check_object_permissions(self.request, obj) + return obj + + # Add support for browsers which only accept GET and POST for now. + def get(self, request, *args, **kwargs): + return self.delete(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + """Delete.""" + self.object = self.get_object() + success_url = self.get_success_url() + self._run_before_form_delete(request, self.object) + self.object.delete() + self._run_after_form_delete(request, self.kwargs.get("form_entry_id")) + messages.info( + request, + _("Form {0} was deleted successfully.").format(self.object.name), + ) + return redirect(success_url) + + def _run_before_form_delete(self, request, form_entry): + """Run just before form_entry has been deleted.""" + try: + self.run_before_form_delete(request, form_entry) + return True + except: + return False + + def run_before_form_delete(self, request, form_entry): + """Run just before form_entry has been deleted.""" + + def _run_after_form_delete(self, request, form_entry_id): + """Run after form_entry has been deleted.""" + try: + self.run_after_form_delete(request, form_entry_id) + return True + except: + return False + + def run_after_form_delete(self, request, form_entry_id): + """Run after form_entry has been deleted.""" + + +# ***************************************************************************** +# **************************** Add form element entry ************************* +# ***************************************************************************** + + +class AddFormElementEntryView(PermissionMixin, CreateView): + """Add form element entry.""" + + template_name = None + form_class = None + theme = None + permission_classes = (AddFormElementEntryPermission,) + + def get_essential_objects( + self, + form_entry_id, + form_element_plugin_uid, + request, + ): + """Get essential objects.""" + try: + form_entry = FormEntry._default_manager.prefetch_related( + "formelemententry_set" + ).get(pk=form_entry_id) + except ObjectDoesNotExist as err: + raise Http404(_("Form entry not found.")) + + form_elements = form_entry.formelemententry_set.all() + + user_form_element_plugin_uids = get_user_form_field_plugin_uids( + request.user + ) + + if form_element_plugin_uid not in user_form_element_plugin_uids: + raise Http404( + _( + "Plugin does not exist or you are not allowed " + "to use this plugin!" + ) + ) + + form_element_plugin_cls = form_element_plugin_registry.get( + form_element_plugin_uid + ) + form_element_plugin = form_element_plugin_cls(user=request.user) + form_element_plugin.request = request + + form_element_plugin_form_cls = form_element_plugin.get_form() + # form = None + + obj = FormElementEntry() + obj.form_entry = form_entry + obj.plugin_uid = form_element_plugin_uid + obj.user = request.user + + return ( + form_entry, + form_elements, + form_element_plugin_cls, + form_element_plugin, + form_element_plugin_form_cls, + user_form_element_plugin_uids, + obj, + ) + + def do_save_object( + self, form_entry_id, form_entry, obj, form_element_plugin, request + ): + """Do save object.""" + # Handling the position + position = 1 + records = FormElementEntry.objects.filter( + form_entry=form_entry + ).aggregate(models.Max("position")) + if records: + try: + position = records["{0}__max".format("position")] + 1 + + except TypeError as err: + pass + + obj.position = position + + # Save the object. + obj.save() + + messages.info( + request, + _( + 'The form element plugin "{0}" was added ' "successfully." + ).format(form_element_plugin.name), + ) + return redirect( + "{0}?active_tab=tab-form-elements".format( + reverse_lazy( + "fobi.edit_form_entry", + kwargs={"form_entry_id": form_entry_id}, + ) + ) + ) + + def get_context_data(self, **kwargs): + """Get context data.""" + context = super(AddFormElementEntryView, self).get_context_data( + **kwargs + ) + + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + + if theme: + context.update({"fobi_theme": theme}) + return context + + def get_template_names(self): + """Get template names.""" + template_name = self.template_name + if not template_name: + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + template_name = theme.add_form_element_entry_template + return [template_name] + + def get(self, request, *args, **kwargs): + """Handle GET requests: instantiate a blank version of the form.""" + self.object = None + ( + form_entry, + form_elements, + form_element_plugin_cls, + form_element_plugin, + form_element_plugin_form_cls, + user_form_element_plugin_uids, + obj, + ) = self.get_essential_objects( + self.kwargs.get("form_entry_id"), + self.kwargs.get("form_element_plugin_uid"), + self.request, + ) + + save_object = False + if not form_element_plugin_form_cls: + save_object = True + + if not save_object: + form = form_element_plugin.get_initialised_create_form_or_404() + + if save_object: + return self.do_save_object( + self.kwargs.get("form_entry_id"), + form_entry, + obj, + form_element_plugin, + request, + ) + + return self.render_to_response( + self.get_context_data( + form=form, + form_entry=form_entry, + form_element_plugin=form_element_plugin, + ) + ) + + def post(self, request, *args, **kwargs): + """ + Handle POST requests: instantiate a form instance with the passed + POST variables and then check if it's valid. + """ + self.object = None + ( + form_entry, + form_elements, + form_element_plugin_cls, + form_element_plugin, + form_element_plugin_form_cls, + user_form_element_plugin_uids, + obj, + ) = self.get_essential_objects( + self.kwargs.get("form_entry_id"), + self.kwargs.get("form_element_plugin_uid"), + self.request, + ) + + save_object = False + if not form_element_plugin_form_cls: + save_object = True + + if not save_object: + form = form_element_plugin.get_initialised_create_form_or_404( + data=request.POST, files=request.FILES + ) + form.validate_plugin_data(form_elements, request=request) + if form.is_valid(): + # Saving the plugin form data. + form.save_plugin_data(request=request) + + # Getting the plugin data. + obj.plugin_data = form.get_plugin_data(request=request) + save_object = True + + if save_object: + return self.do_save_object( + self.kwargs.get("form_entry_id"), + form_entry, + obj, + form_element_plugin, + request, + ) + + return self.render_to_response( + self.get_context_data( + form=form, + form_entry=form_entry, + form_element_plugin=form_element_plugin, + ) + ) + + +# ***************************************************************************** +# **************************** Edit form element entry ************************ +# ***************************************************************************** + + +class EditFormElementEntryView(PermissionMixin, UpdateView): + """Edit form element entry view.""" + + template_name = None + form_class = None + theme = None + pk_url_kwarg = "form_element_entry_id" + permission_classes = (EditFormElementEntryPermission,) + + def _get_queryset(self, request): + """Get queryset.""" + return FormElementEntry._default_manager.select_related( + "form_entry", "form_entry__user" + ).filter(form_entry__user__pk=request.user.pk) + + def get_object(self, queryset=None): + """Get object.""" + obj = super(EditFormElementEntryView, self).get_object(queryset) + self.check_object_permissions(self.request, obj) + return obj + + def get_essential_objects( + self, + form_element_entry_id, + request, + ): + """Get essential objects.""" + try: + obj = self.get_object(queryset=self._get_queryset(request)) + except ObjectDoesNotExist as err: + raise Http404(_("Form element entry not found.")) + + form_entry = obj.form_entry + form_element_plugin = obj.get_plugin(request=request) + form_element_plugin.request = request + + form_element_plugin_form_cls = form_element_plugin.get_form() + + return ( + form_entry, + form_element_entry_id, + form_element_plugin, + form_element_plugin_form_cls, + obj, + ) + + def get_context_data(self, **kwargs): + """Get context data.""" + context = super(EditFormElementEntryView, self).get_context_data( + **kwargs + ) + + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + + if theme: + context.update({"fobi_theme": theme}) + return context + + def get_template_names(self): + """Get template names.""" + template_name = self.template_name + if not template_name: + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + template_name = theme.edit_form_element_entry_template + return [template_name] + + def get(self, request, *args, **kwargs): + """Handle GET requests: instantiate a blank version of the form.""" + ( + form_entry, + form_element_entry_id, + form_element_plugin, + form_element_plugin_form_cls, + obj, + ) = self.get_essential_objects( + self.kwargs.get("form_element_entry_id"), + self.request, + ) + self.object = obj + form = None + + if not form_element_plugin_form_cls: + messages.info( + request, + _( + 'The form element plugin "{0}" ' "is not configurable!" + ).format(form_element_plugin.name), + ) + return redirect("fobi.edit_form_entry", form_entry_id=form_entry.pk) + + else: + form = form_element_plugin.get_initialised_edit_form_or_404() + + return self.render_to_response( + self.get_context_data( + form=form, + form_entry=form_entry, + form_element_plugin=form_element_plugin, + ) + ) + + def post(self, request, *args, **kwargs): + """ + Handle POST requests: instantiate a form instance with the passed + POST variables and then check if it's valid. + """ + self.object = None + ( + form_entry, + form_element_entry_id, + form_element_plugin, + form_element_plugin_form_cls, + obj, + ) = self.get_essential_objects( + self.kwargs.get("form_element_entry_id"), + self.request, + ) + self.object = obj + + if not form_element_plugin_form_cls: + messages.info( + request, + _( + 'The form element plugin "{0}" ' "is not configurable!" + ).format(form_element_plugin.name), + ) + return redirect("fobi.edit_form_entry", form_entry_id=form_entry.pk) + + form = form_element_plugin.get_initialised_edit_form_or_404( + data=request.POST, files=request.FILES + ) + + form_elements = ( + FormElementEntry._default_manager.select_related( + "form_entry", "form_entry__user" + ) + .exclude(pk=form_element_entry_id) + .filter(form_entry=form_entry) + ) + + form.validate_plugin_data(form_elements, request=request) + + if form.is_valid(): + # Saving the plugin form data. + form.save_plugin_data(request=request) + + # Getting the plugin data. + obj.plugin_data = form.get_plugin_data(request=request) + + # Save the object. + obj.save() + + messages.info( + request, + _( + 'The form element plugin "{0}" was edited ' "successfully." + ).format(form_element_plugin.name), + ) + + return redirect("fobi.edit_form_entry", form_entry_id=form_entry.pk) + + return self.render_to_response( + self.get_context_data( + form=form, + form_entry=form_entry, + form_element_plugin=form_element_plugin, + ) + ) + + +# ***************************************************************************** +# **************************** Delete form element entry ********************** +# ***************************************************************************** + + +class DeleteFormElementEntryView(AbstractDeletePluginEntryView): + """Delete form element entry.""" + + model = FormElementEntry + permission_classes = (DeleteFormElementEntryPermission,) + pk_url_kwarg = "form_element_entry_id" + get_user_plugin_uids_func = get_user_form_field_plugin_uids + message = _('The form element plugin "{0}" was deleted successfully.') + html_anchor = "?active_tab=tab-form-elements" + + +# ***************************************************************************** +# **************************** Add form handler entry ************************* +# ***************************************************************************** + + +class AddFormHandlerEntryView(PermissionMixin, CreateView): + """Add form handler entry.""" + + template_name = None + form_class = None + theme = None + permission_classes = (AddFormHandlerEntryPermission,) + + def get_essential_objects( + self, + form_entry_id, + form_handler_plugin_uid, + request, + ): + """Get essential objects.""" + try: + form_entry = FormEntry._default_manager.get(pk=form_entry_id) + except ObjectDoesNotExist as err: + raise Http404(_("Form entry not found.")) + + # TODO: Form handlers don't have this, while form elements do. + # Find out whether including this improves performance. + # Comment added on 2022-07-10. + # form_elements = form_entry.formelemententry_set.all() + + user_form_handler_plugin_uids = get_user_form_handler_plugin_uids( + request.user + ) + + if form_handler_plugin_uid not in user_form_handler_plugin_uids: + raise Http404( + _( + "Plugin does not exist or you are not allowed " + "to use this plugin!" + ) + ) + + form_handler_plugin_cls = form_handler_plugin_registry.get( + form_handler_plugin_uid + ) + + # Check if we deal with form handler plugin that is only allowed to be + # used once. In that case, check if it has been used already in the + # current form entry. + if not form_handler_plugin_cls.allow_multiple: + times_used = FormHandlerEntry._default_manager.filter( + form_entry__id=form_entry_id, + plugin_uid=form_handler_plugin_cls.uid, + ).count() + if times_used > 0: + raise Http404( + _( + "The {0} plugin can be used only once in a " "form." + ).format(form_handler_plugin_cls.name) + ) + + form_handler_plugin = form_handler_plugin_cls(user=request.user) + form_handler_plugin.request = request + + form_handler_plugin_form_cls = form_handler_plugin.get_form() + + obj = FormHandlerEntry() + obj.form_entry = form_entry + obj.plugin_uid = form_handler_plugin_uid + obj.user = request.user + + return ( + form_entry, + # form_elements, + form_handler_plugin_cls, + form_handler_plugin, + form_handler_plugin_form_cls, + user_form_handler_plugin_uids, + obj, + ) + + def do_save_object( + self, form_entry_id, form_entry, obj, form_handler_plugin, request + ): + """Do save object.""" + # Save the object. + obj.save() + + messages.info( + request, + _( + 'The form handler plugin "{0}" was added ' "successfully." + ).format(form_handler_plugin.name), + ) + return redirect( + "{0}?active_tab=tab-form-handlers".format( + reverse_lazy( + "fobi.edit_form_entry", + kwargs={"form_entry_id": form_entry_id}, + ) + ) + ) + + def get_context_data(self, **kwargs): + """Get context data.""" + context = super(AddFormHandlerEntryView, self).get_context_data( + **kwargs + ) + + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + + if theme: + context.update({"fobi_theme": theme}) + return context + + def get_template_names(self): + """Get template names.""" + template_name = self.template_name + if not template_name: + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + template_name = theme.add_form_handler_entry_template + return [template_name] + + def get(self, request, *args, **kwargs): + """Handle GET requests: instantiate a blank version of the form.""" + self.object = None + ( + form_entry, + # form_elements, + form_handler_plugin_cls, + form_handler_plugin, + form_handler_plugin_form_cls, + user_form_handler_plugin_uids, + obj, + ) = self.get_essential_objects( + self.kwargs.get("form_entry_id"), + self.kwargs.get("form_handler_plugin_uid"), + self.request, + ) + + save_object = False + if not form_handler_plugin_form_cls: + save_object = True + + if not save_object: + form = form_handler_plugin.get_initialised_create_form_or_404() + + if save_object: + return self.do_save_object( + self.kwargs.get("form_entry_id"), + form_entry, + obj, + form_handler_plugin, + request, + ) + + return self.render_to_response( + self.get_context_data( + form=form, + form_entry=form_entry, + form_handler_plugin=form_handler_plugin, + ) + ) + + def post(self, request, *args, **kwargs): + """ + Handle POST requests: instantiate a form instance with the passed + POST variables and then check if it's valid. + """ + self.object = None + ( + form_entry, + # form_elements, + form_handler_plugin_cls, + form_handler_plugin, + form_handler_plugin_form_cls, + user_form_handler_plugin_uids, + obj, + ) = self.get_essential_objects( + self.kwargs.get("form_entry_id"), + self.kwargs.get("form_handler_plugin_uid"), + self.request, + ) + + save_object = False + if not form_handler_plugin_form_cls: + save_object = True + + if not save_object: + form = form_handler_plugin.get_initialised_create_form_or_404( + data=request.POST, files=request.FILES + ) + # TODO: Form handlers don't have this, while form elements do. + # Find out whether this is something that could be correct + # for form handlers. + # form.validate_plugin_data(form_elements, request=request) + if form.is_valid(): + # Saving the plugin form data. + form.save_plugin_data(request=request) + + # Getting the plugin data. + obj.plugin_data = form.get_plugin_data(request=request) + save_object = True + + if save_object: + return self.do_save_object( + self.kwargs.get("form_entry_id"), + form_entry, + obj, + form_handler_plugin, + request, + ) + + return self.render_to_response( + self.get_context_data( + form=form, + form_entry=form_entry, + form_handler_plugin=form_handler_plugin, + ) + ) + + +# ***************************************************************************** +# **************************** Edit form handler entry ************************ +# ***************************************************************************** + + +class EditFormHandlerEntryView(PermissionMixin, UpdateView): + """Edit form handler entry view.""" + + template_name = None + form_class = None + theme = None + pk_url_kwarg = "form_handler_entry_id" + permission_classes = (EditFormHandlerEntryPermission,) + + def _get_queryset(self, request): + """Get queryset.""" + # TODO: The form element entry has also `form_entry__user` in + # `seleect_related`. Find out if that something that could + # be also be applied here to improve the performance. + return FormHandlerEntry._default_manager.select_related( + "form_entry" + ).filter(form_entry__user__pk=request.user.pk) + + def get_object(self, queryset=None): + """Get object.""" + obj = super(EditFormHandlerEntryView, self).get_object(queryset) + self.check_object_permissions(self.request, obj) + return obj + + def get_essential_objects( + self, + form_handler_entry_id, + request, + ): + """Get essential objects.""" + try: + obj = self.get_object(queryset=self._get_queryset(request)) + except ObjectDoesNotExist as err: + raise Http404(_("Form element entry not found.")) + + form_entry = obj.form_entry + form_handler_plugin = obj.get_plugin(request=request) + form_handler_plugin.request = request + + form_handler_plugin_form_cls = form_handler_plugin.get_form() + + return ( + form_entry, + form_handler_entry_id, + form_handler_plugin, + form_handler_plugin_form_cls, + obj, + ) + + def get_context_data(self, **kwargs): + """Get context data.""" + context = super(EditFormHandlerEntryView, self).get_context_data( + **kwargs + ) + + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + + if theme: + context.update({"fobi_theme": theme}) + return context + + def get_template_names(self): + """Get template names.""" + template_name = self.template_name + if not template_name: + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + template_name = theme.edit_form_handler_entry_template + return [template_name] + + def get(self, request, *args, **kwargs): + """Handle GET requests: instantiate a blank version of the form.""" + ( + form_entry, + form_handler_entry_id, + form_handler_plugin, + form_handler_plugin_form_cls, + obj, + ) = self.get_essential_objects( + self.kwargs.get("form_handler_entry_id"), + self.request, + ) + self.object = obj + form = None + + if not form_handler_plugin_form_cls: + messages.info( + request, + _( + 'The form handler plugin "{0}" is not ' "configurable!" + ).format(form_handler_plugin.name), + ) + return redirect("fobi.edit_form_entry", form_entry_id=form_entry.pk) + + else: + form = form_handler_plugin.get_initialised_edit_form_or_404() + + return self.render_to_response( + self.get_context_data( + form=form, + form_entry=form_entry, + form_handler_plugin=form_handler_plugin, + ) + ) + + def post(self, request, *args, **kwargs): + """ + Handle POST requests: instantiate a form instance with the passed + POST variables and then check if it's valid. + """ + self.object = None + ( + form_entry, + form_handler_entry_id, + form_handler_plugin, + form_handler_plugin_form_cls, + obj, + ) = self.get_essential_objects( + self.kwargs.get("form_handler_entry_id"), + self.request, + ) + self.object = obj + + if not form_handler_plugin_form_cls: + messages.info( + request, + _( + 'The form handler plugin "{0}" is not ' "configurable!" + ).format(form_handler_plugin.name), + ) + return redirect("fobi.edit_form_entry", form_entry_id=form_entry.pk) + + form = form_handler_plugin.get_initialised_edit_form_or_404( + data=request.POST, files=request.FILES + ) + + if form.is_valid(): + # Saving the plugin form data. + form.save_plugin_data(request=request) + + # Getting the plugin data. + obj.plugin_data = form.get_plugin_data(request=request) + + # Save the object. + obj.save() + + messages.info( + request, + _( + 'The form handler plugin "{0}" was edited ' "successfully." + ).format(form_handler_plugin.name), + ) + + return redirect( + "{0}?active_tab=tab-form-handlers".format( + reverse_lazy( + "fobi.edit_form_entry", + kwargs={"form_entry_id": form_entry.pk}, + ) + ) + ) + + return self.render_to_response( + self.get_context_data( + form=form, + form_entry=form_entry, + form_handler_plugin=form_handler_plugin, + ) + ) + + +# ***************************************************************************** +# **************************** Delete form handler entry ********************** +# ***************************************************************************** + + +class DeleteFormHandlerEntryView(AbstractDeletePluginEntryView): + """Delete form handler entry.""" + + model = FormHandlerEntry + permission_classes = (DeleteFormHandlerEntryPermission,) + pk_url_kwarg = "form_handler_entry_id" + get_user_plugin_uids_func = get_user_form_handler_plugin_uids + message = _('The form handler plugin "{0}" was deleted successfully.') + html_anchor = "?active_tab=tab-form-handlers" + + +# ***************************************************************************** +# ***************************************************************************** +# ******************************** View form entry **************************** +# ***************************************************************************** +# ***************************************************************************** + +# ***************************************************************************** +# ******************************** View form entry **************************** +# ***************************************************************************** + + +class AbstractViewFormEntryView(PermissionMixin, DetailView): + """Abstract view form entry.""" + + model = FormEntry + slug_url_kwarg = "form_entry_slug" + template_name = None + theme = None + permission_classes = (ViewFormEntryPermission,) + + def get_object(self, queryset=None): + """Get object.""" + if queryset is None: + queryset = self._get_queryset(request=self.request) + obj = super(AbstractViewFormEntryView, self).get_object( + queryset=queryset, + ) + self.check_object_permissions(self.request, obj) + return obj + + def _get_queryset(self, request): + """Get queryset.""" + queryset = FormEntry._default_manager.all().select_related("user") + if not request.user.is_authenticated: + queryset = queryset.filter(is_public=True) + return queryset + + +class ViewFormEntryView(AbstractViewFormEntryView): + """View created form.""" + + def get_context_data(self, **kwargs): + """Get context data.""" + context = super(ViewFormEntryView, self).get_context_data(**kwargs) + + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + + if theme: + context.update({"fobi_theme": theme}) + return context + + def get_template_names(self): + """Get template names.""" + template_name = self.template_name + if not template_name: + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + template_name = theme.view_form_entry_template + return [template_name] + + def get_essential_objects( + self, + form_entry, + request, + ): + """Get essential objects.""" + form_element_entries = form_entry.formelemententry_set.all()[:] + + # This is where the most of the magic happens. Our form is being built + # dynamically. + form_cls = assemble_form_class( + form_entry, + form_element_entries=form_element_entries, + request=request, + ) + + return ( + form_element_entries, + form_cls, + ) + + def inactive_form_response(self, request, form_entry): + context = { + "form_entry": form_entry, + "page_header": ( + form_entry.inactive_page_title + or form_entry.title + or form_entry.name + ), + } + + if not self.template_name: + theme = get_theme(request=request, as_instance=True) + template_name = theme.form_entry_inactive_template + else: + template_name = self.template_name + + return render(request, template_name, context) + + def get_object(self, queryset=None): + """Get object.""" + obj = super(ViewFormEntryView, self).get_object(queryset=queryset) + self.check_object_permissions(self.request, obj) + return obj + + def get(self, request, *args, **kwargs): + """Handle GET requests: instantiate a blank version of the form.""" + try: + form_entry = self.get_object() + except ObjectDoesNotExist as err: + raise Http404(_("Form entry not found.")) + + self.object = form_entry + + if not form_entry.is_active: + return self.inactive_form_response(request, form_entry) + + form_element_entries, form_cls = self.get_essential_objects( + form_entry, + request, + ) + + # Providing initial form data by feeding entire GET dictionary + # to the form, if ``GET_PARAM_INITIAL_DATA`` is present in the + # GET. + kwargs = {} + if GET_PARAM_INITIAL_DATA in request.GET: + kwargs = {"initial": request.GET} + form = form_cls(**kwargs) + + # In debug mode, try to identify possible problems. + if DEBUG: + form.as_p() + else: + try: + form.as_p() + except Exception as err: + logger.error(err) + + theme = get_theme(request=request, as_instance=True) + theme.collect_plugin_media(form_element_entries) + + return self.render_to_response( + self.get_context_data( + form=form, + form_entry=form_entry, + fobi_form_title=form_entry.title, + ) + ) + + def post(self, request, *args, **kwargs): + """ + Handle POST requests: instantiate a form instance with the passed + POST variables and then check if it's valid. + """ + try: + form_entry = self.get_object() + except ObjectDoesNotExist as err: + raise Http404(_("Form entry not found.")) + + self.object = form_entry + + if not form_entry.is_active: + return self.inactive_form_response(request, form_entry) + + form_element_entries, form_cls = self.get_essential_objects( + form_entry, + request, + ) + + form = form_cls(request.POST, request.FILES) + + # Fire pre form validation callbacks + fire_form_callbacks( + form_entry=form_entry, + request=request, + form=form, + stage=CALLBACK_BEFORE_FORM_VALIDATION, + ) + + if form.is_valid(): + # Fire form valid callbacks, before handling submitted plugin + # form data. + form = fire_form_callbacks( + form_entry=form_entry, + request=request, + form=form, + stage=CALLBACK_FORM_VALID_BEFORE_SUBMIT_PLUGIN_FORM_DATA, + ) + + # Fire plugin processors + form = submit_plugin_form_data( + form_entry=form_entry, request=request, form=form + ) + + # Fire form valid callbacks + form = fire_form_callbacks( + form_entry=form_entry, + request=request, + form=form, + stage=CALLBACK_FORM_VALID, + ) + + # Run all handlers + handler_responses, handler_errors = run_form_handlers( + form_entry=form_entry, + request=request, + form=form, + form_element_entries=form_element_entries, + ) + + # Warning that not everything went ok. + if handler_errors: + for handler_error in handler_errors: + messages.warning( + request, _("Error occurred: {0}.").format(handler_error) + ) + + # Fire post handler callbacks + fire_form_callbacks( + form_entry=form_entry, + request=request, + form=form, + stage=CALLBACK_FORM_VALID_AFTER_FORM_HANDLERS, + ) + + messages.info( + request, + _("Form {0} was submitted successfully.").format( + form_entry.name + ), + ) + return redirect( + reverse_lazy( + "fobi.form_entry_submitted", args=[form_entry.slug] + ) + ) + else: + # Fire post form validation callbacks + fire_form_callbacks( + form_entry=form_entry, + request=request, + form=form, + stage=CALLBACK_FORM_INVALID, + ) + + # In debug mode, try to identify possible problems. + if DEBUG: + form.as_p() + else: + try: + form.as_p() + except Exception as err: + logger.error(err) + + theme = get_theme(request=request, as_instance=True) + theme.collect_plugin_media(form_element_entries) + + return self.render_to_response( + self.get_context_data( + form=form, + form_entry=form_entry, + fobi_form_title=form_entry.title, + ) + ) + + +# ***************************************************************************** +# **************************** View form entry success ************************ +# ***************************************************************************** + + +class ViewFormEntrySubmittedView(AbstractViewFormEntryView): + """View form entry submitted.""" + + def get(self, request, *args, **kwargs): + """Handle GET requests: instantiate a blank version of the form.""" + try: + form_entry = self.get_object() + except ObjectDoesNotExist as err: + raise Http404(_("Form entry not found.")) + + self.object = form_entry + + return self.render_to_response( + self.get_context_data( + form_entry_slug=self.kwargs.get(self.slug_url_kwarg), + form_entry=form_entry, + ) + ) + + def get_context_data(self, **kwargs): + """Get context data.""" + context = super(ViewFormEntrySubmittedView, self).get_context_data( + **kwargs + ) + + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + + if theme: + context.update({"fobi_theme": theme}) + return context + + def get_template_names(self): + """Get template names.""" + template_name = self.template_name + if not template_name: + if not self.theme: + theme = get_theme(request=self.request, as_instance=True) + else: + theme = self.theme + template_name = theme.form_entry_submitted_template + return [template_name] diff --git a/src/fobi/views.py b/src/fobi/views/function_based.py similarity index 95% rename from src/fobi/views.py rename to src/fobi/views/function_based.py index 4faf366b7..ec2a9a251 100644 --- a/src/fobi/views.py +++ b/src/fobi/views/function_based.py @@ -29,7 +29,7 @@ from django_nine import versions -from .base import ( +from ..base import ( fire_form_callbacks, run_form_handlers, run_form_wizard_handlers, @@ -40,20 +40,20 @@ get_theme, # get_registered_form_handler_plugins ) -from .constants import ( +from ..constants import ( CALLBACK_BEFORE_FORM_VALIDATION, CALLBACK_FORM_VALID_BEFORE_SUBMIT_PLUGIN_FORM_DATA, CALLBACK_FORM_VALID, CALLBACK_FORM_VALID_AFTER_FORM_HANDLERS, CALLBACK_FORM_INVALID ) -from .decorators import permissions_required, SATISFY_ALL, SATISFY_ANY -from .dynamic import assemble_form_class -from .form_importers import ( +from ..decorators import permissions_required, SATISFY_ALL, SATISFY_ANY +from ..dynamic import assemble_form_class +from ..form_importers import ( ensure_autodiscover as ensure_importers_autodiscover, form_importer_plugin_registry, get_form_importer_plugin_urls ) -from .forms import ( +from ..forms import ( FormEntryForm, FormElementEntryFormSet, ImportFormEntryForm, @@ -63,8 +63,8 @@ FormWizardFormEntryFormSet, # FormWizardFormEntryForm ) -from .helpers import JSONDataExporter -from .models import ( +from ..helpers import JSONDataExporter +from ..models import ( FormEntry, FormElementEntry, FormHandlerEntry, @@ -72,12 +72,33 @@ FormWizardFormEntry, FormWizardHandlerEntry ) -from .settings import ( +from ..permissions.definitions import ( + dashboard_permissions, + wizards_dashboard_permissions, + create_form_entry_permissions, + edit_form_entry_permissions, + delete_form_entry_permissions, + add_form_element_entry_permission, + edit_form_element_entry_permission, + delete_form_element_entry_permission, + add_form_handler_entry_permission, + edit_form_handler_entry_permission, + delete_form_handler_entry_permission, + create_form_wizard_entry_permissions, + edit_form_wizard_entry_permissions, + delete_form_wizard_entry_permissions, + add_form_wizard_form_entry_permission, + delete_form_wizard_form_entry_permission, + add_form_wizard_handler_entry_permission, + edit_form_wizard_handler_entry_permission, + delete_form_wizard_handler_entry_permission, +) +from ..settings import ( GET_PARAM_INITIAL_DATA, DEBUG, SORT_PLUGINS_BY_VALUE, ) -from .utils import ( +from ..utils import ( append_edit_and_delete_links_to_field, get_user_form_element_plugins_grouped, get_user_form_field_plugin_uids, @@ -91,14 +112,14 @@ perform_form_entry_import, prepare_form_entry_export_data ) -from .wizard import ( +from ..wizard import ( # DynamicCookieWizardView, DynamicSessionWizardView, ) -__title__ = 'fobi.views' +__title__ = 'fobi.views.function_based' __author__ = 'Artur Barseghyan ' -__copyright__ = '2014-2019 Artur Barseghyan' +__copyright__ = '2014-2022 Artur Barseghyan' __license__ = 'GPL 2.0/LGPL 2.1' __all__ = ( 'add_form_element_entry', @@ -113,11 +134,14 @@ 'delete_form_handler_entry', 'delete_form_wizard_entry', 'delete_form_wizard_form_entry', + 'delete_form_wizard_handler_entry', 'edit_form_element_entry', 'edit_form_entry', 'edit_form_handler_entry', + 'edit_form_wizard_entry', 'edit_form_wizard_handler_entry', 'export_form_entry', + 'export_form_wizard_entry', 'form_entry_submitted', 'form_importer', 'form_wizard_entry_submitted', @@ -130,6 +154,7 @@ logger = logging.getLogger(__name__) + # ***************************************************************************** # ***************************************************************************** # *********************************** Generic ********************************* @@ -236,12 +261,12 @@ def _delete_wizard_plugin_entry(request, # ********************************** Forms ************************************ # ***************************************************************************** -dashboard_permissions = [ - # Form - 'fobi.add_formentry', - 'fobi.change_formentry', - 'fobi.delete_formentry', -] +# dashboard_permissions = [ +# # Form +# 'fobi.add_formentry', +# 'fobi.change_formentry', +# 'fobi.delete_formentry', +# ] @login_required @@ -279,12 +304,12 @@ def dashboard(request, theme=None, template_name=None): # ****************************** Form wizards ********************************* # ***************************************************************************** -wizards_dashboard_permissions = [ - # Form wizard - 'fobi.add_formwizardentry', - 'fobi.change_formwizardentry', - 'fobi.delete_formwizardentry', -] +# wizards_dashboard_permissions = [ +# # Form wizard +# 'fobi.add_formwizardentry', +# 'fobi.change_formwizardentry', +# 'fobi.delete_formwizardentry', +# ] @login_required @@ -328,11 +353,11 @@ def form_wizards_dashboard(request, theme=None, template_name=None): # **************************** Create form entry ****************************** # ***************************************************************************** -create_form_entry_permissions = [ - 'fobi.add_formentry', - 'fobi.add_formelemententry', - 'fobi.add_formhandlerentry', -] +# create_form_entry_permissions = [ +# 'fobi.add_formentry', +# 'fobi.add_formelemententry', +# 'fobi.add_formhandlerentry', +# ] @login_required @@ -365,7 +390,7 @@ def create_form_entry(request, theme=None, template_name=None): messages.info( request, gettext('Errors occurred while saving ' - 'the form: {0}.').format(str(err)) + 'the form: {0}.').format(str(err)) ) else: @@ -390,12 +415,12 @@ def create_form_entry(request, theme=None, template_name=None): # ******************************* Edit form entry ************************** # ************************************************************************** -edit_form_entry_permissions = [ - 'fobi.change_formentry', 'fobi.change_formelemententry', - 'fobi.change_formhandlerentry', - 'fobi.add_formelemententry', 'fobi.add_formhandlerentry', - 'fobi.delete_formelemententry', 'fobi.delete_formhandlerentry', -] +# edit_form_entry_permissions = [ +# 'fobi.change_formentry', 'fobi.change_formelemententry', +# 'fobi.change_formhandlerentry', +# 'fobi.add_formelemententry', 'fobi.add_formhandlerentry', +# 'fobi.delete_formelemententry', 'fobi.delete_formhandlerentry', +# ] @login_required @@ -570,10 +595,10 @@ def edit_form_entry(request, form_entry_id, theme=None, template_name=None): # ********************************* Delete form entry ************************* # ***************************************************************************** -delete_form_entry_permissions = [ - 'fobi.delete_formentry', 'fobi.delete_formelemententry', - 'fobi.delete_formhandlerentry', -] +# delete_form_entry_permissions = [ +# 'fobi.delete_formentry', 'fobi.delete_formelemententry', +# 'fobi.delete_formhandlerentry', +# ] @login_required @@ -606,9 +631,9 @@ def delete_form_entry(request, form_entry_id, template_name=None): # **************************** Add form element entry ************************* # ***************************************************************************** - +# 'fobi.add_formelemententry' @login_required -@permission_required('fobi.add_formelemententry') +@permission_required(add_form_element_entry_permission) def add_form_element_entry(request, form_entry_id, form_element_plugin_uid, @@ -638,7 +663,7 @@ def add_form_element_entry(request, if form_element_plugin_uid not in user_form_element_plugin_uids: raise Http404(gettext("Plugin does not exist or you are not allowed " - "to use this plugin!")) + "to use this plugin!")) form_element_plugin_cls = form_element_plugin_registry.get( form_element_plugin_uid @@ -732,9 +757,10 @@ def add_form_element_entry(request, # **************************** Edit form element entry ************************ # ***************************************************************************** +# 'fobi.change_formelemententry' @login_required -@permission_required('fobi.change_formelemententry') +@permission_required(edit_form_element_entry_permission) def edit_form_element_entry(request, form_element_entry_id, theme=None, @@ -760,10 +786,10 @@ def edit_form_element_entry(request, form_element_plugin = obj.get_plugin(request=request) form_element_plugin.request = request - FormElementPluginForm = form_element_plugin.get_form() + form_element_plugin_form_cls = form_element_plugin.get_form() form = None - if not FormElementPluginForm: + if not form_element_plugin_form_cls: messages.info( request, gettext('The form element plugin "{0}" ' @@ -798,7 +824,7 @@ def edit_form_element_entry(request, messages.info( request, gettext('The form element plugin "{0}" was edited ' - 'successfully.').format(form_element_plugin.name) + 'successfully.').format(form_element_plugin.name) ) return redirect('fobi.edit_form_entry', @@ -807,8 +833,10 @@ def edit_form_element_entry(request, else: form = form_element_plugin.get_initialised_edit_form_or_404() - form_element_plugin = obj.get_plugin(request=request) - form_element_plugin.request = request + # TODO: Commented out on 2022-07-09 during refactoring. If something + # goes wrong, uncomment it. + # form_element_plugin = obj.get_plugin(request=request) + # form_element_plugin.request = request context = { 'form': form, @@ -817,7 +845,7 @@ def edit_form_element_entry(request, } # If given, pass to the template (and override the value set by - # the context processor. + # the context processor). if theme: context.update({'fobi_theme': theme}) @@ -832,9 +860,10 @@ def edit_form_element_entry(request, # **************************** Delete form element entry ********************** # ***************************************************************************** +# 'fobi.delete_formelemententry' @login_required -@permission_required('fobi.delete_formelemententry') +@permission_required(delete_form_element_entry_permission) def delete_form_element_entry(request, form_element_entry_id): """Delete form element entry. @@ -857,9 +886,10 @@ def delete_form_element_entry(request, form_element_entry_id): # **************************** Add form handler entry ************************* # ***************************************************************************** +# 'fobi.add_formhandlerentry' @login_required -@permission_required('fobi.add_formhandlerentry') +@permission_required(add_form_handler_entry_permission) def add_form_handler_entry(request, form_entry_id, form_handler_plugin_uid, @@ -978,9 +1008,10 @@ def add_form_handler_entry(request, # **************************** Edit form handler entry ************************ # ***************************************************************************** +# 'fobi.change_formhandlerentry' @login_required -@permission_required('fobi.change_formhandlerentry') +@permission_required(edit_form_handler_entry_permission) def edit_form_handler_entry(request, form_handler_entry_id, theme=None, @@ -1001,18 +1032,17 @@ def edit_form_handler_entry(request, raise Http404(gettext("Form handler entry not found.")) form_entry = obj.form_entry - form_handler_plugin = obj.get_plugin(request=request) form_handler_plugin.request = request - FormHandlerPluginForm = form_handler_plugin.get_form() + form_handler_plugin_form_cls = form_handler_plugin.get_form() form = None - if not FormHandlerPluginForm: + if not form_handler_plugin_form_cls: messages.info( request, gettext('The form handler plugin "{0}" is not ' - 'configurable!').format(form_handler_plugin.name) + 'configurable!').format(form_handler_plugin.name) ) return redirect('fobi.edit_form_entry', form_entry_id=form_entry.pk) @@ -1035,11 +1065,17 @@ def edit_form_handler_entry(request, messages.info( request, gettext('The form handler plugin "{0}" was edited ' - 'successfully.').format(form_handler_plugin.name) + 'successfully.').format(form_handler_plugin.name) ) - return redirect('fobi.edit_form_entry', - form_entry_id=form_entry.pk) + return redirect( + "{0}?active_tab=tab-form-handlers".format( + reverse( + 'fobi.edit_form_entry', + kwargs={"form_entry_id": form_entry.pk}, + ) + ) + ) else: form = form_handler_plugin.get_initialised_edit_form_or_404() @@ -1066,9 +1102,10 @@ def edit_form_handler_entry(request, # **************************** Delete form handler entry ********************** # ***************************************************************************** +# 'fobi.delete_formhandlerentry' @login_required -@permission_required('fobi.delete_formhandlerentry') +@permission_required(delete_form_handler_entry_permission) def delete_form_handler_entry(request, form_handler_entry_id): """Delete form handler entry. @@ -1098,11 +1135,11 @@ def delete_form_handler_entry(request, form_handler_entry_id): # ************************* Create form wizard entry ************************** # ***************************************************************************** -create_form_wizard_entry_permissions = [ - 'fobi.add_formwizardentry', - 'fobi.add_formwizardformentry', - 'fobi.add_formhandlerentry', -] +# create_form_wizard_entry_permissions = [ +# 'fobi.add_formwizardentry', +# 'fobi.add_formwizardformentry', +# 'fobi.add_formhandlerentry', +# ] @login_required @@ -1163,16 +1200,16 @@ def create_form_wizard_entry(request, theme=None, template_name=None): # *************************** Edit form wizard entry *********************** # ************************************************************************** -edit_form_wizard_entry_permissions = [ - 'fobi.change_formwizardentry', - - 'fobi.add_formwizardformentry', - 'fobi.delete_formewizardformentry', - - 'fobi.add_formhandlerentry', - 'fobi.change_formhandlerentry', - 'fobi.delete_formhandlerentry', -] +# edit_form_wizard_entry_permissions = [ +# 'fobi.change_formwizardentry', +# +# 'fobi.add_formwizardformentry', +# 'fobi.delete_formewizardformentry', +# +# 'fobi.add_formhandlerentry', +# 'fobi.change_formhandlerentry', +# 'fobi.delete_formhandlerentry', +# ] @login_required @@ -1332,11 +1369,11 @@ def edit_form_wizard_entry(request, form_wizard_entry_id, theme=None, # **************************** Delete form wizard entry *********************** # ***************************************************************************** -delete_form_wizard_entry_permissions = [ - 'fobi.delete_formwizardentry', - 'fobi.delete_formwizardformentry', - 'fobi.delete_formwizardhandlerentry', -] +# delete_form_wizard_entry_permissions = [ +# 'fobi.delete_formwizardentry', +# 'fobi.delete_formwizardformentry', +# 'fobi.delete_formwizardhandlerentry', +# ] @login_required @@ -1703,9 +1740,10 @@ def form_wizard_entry_submitted(request, form_wizard_entry_slug=None, # ************************* Add form wizard form entry ************************ # ***************************************************************************** +# 'fobi.add_formwizardformentry' @login_required -@permission_required('fobi.add_formwizardformentry') +@permission_required(add_form_wizard_form_entry_permission) def add_form_wizard_form_entry(request, form_wizard_entry_id, form_entry_id, @@ -1799,9 +1837,10 @@ def add_form_wizard_form_entry(request, # ************************** Delete form wizard form entry ******************** # ***************************************************************************** +# 'fobi.delete_formwizardformentry' @login_required -@permission_required('fobi.delete_formwizardformentry') +@permission_required(delete_form_wizard_form_entry_permission) def delete_form_wizard_form_entry(request, form_wizard_form_entry_id): """Delete form wizard form entry. @@ -1850,9 +1889,10 @@ def delete_form_wizard_form_entry(request, form_wizard_form_entry_id): # **************************** Add form handler entry ************************* # ***************************************************************************** +# 'fobi.add_formwizardhandlerentry' @login_required -@permission_required('fobi.add_formwizardhandlerentry') +@permission_required(add_form_wizard_handler_entry_permission) def add_form_wizard_handler_entry(request, form_wizard_entry_id, form_wizard_handler_plugin_uid, @@ -1979,9 +2019,10 @@ def add_form_wizard_handler_entry(request, # ************************ Edit form wizard handler entry ********************* # ***************************************************************************** +# 'fobi.change_formwizardhandlerentry' @login_required -@permission_required('fobi.change_formwizardhandlerentry') +@permission_required(edit_form_wizard_handler_entry_permission) def edit_form_wizard_handler_entry(request, form_wizard_handler_entry_id, theme=None, @@ -2074,9 +2115,10 @@ def edit_form_wizard_handler_entry(request, # *********************** Delete form wizard handler entry ******************** # ***************************************************************************** +# 'fobi.delete_formwizardhandlerentry' @login_required -@permission_required('fobi.delete_formwizardhandlerentry') +@permission_required(delete_form_wizard_handler_entry_permission) def delete_form_wizard_handler_entry(request, form_wizard_handler_entry_id): """Delete form handler entry.