├── .coveragerc ├── .editorconfig ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codecov.yml │ ├── codeql-analysis.yml │ ├── docs.yml │ ├── lint-pr.yml │ ├── lint.yml │ ├── publish-to-live-pypi.yml │ └── publish-to-test-pypi.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .stylelintrc.js ├── .toml ├── .tx ├── config └── transifex.yaml ├── CHANGELOG.rst ├── LICENSE ├── README.rst ├── browserslist ├── djangocms_form_builder ├── __init__.py ├── actions.py ├── admin.py ├── apps.py ├── cms_plugins │ ├── __init__.py │ ├── ajax_plugins.py │ ├── form_plugins.py │ └── legacy.py ├── constants.py ├── entry_model.py ├── fields.py ├── forms.py ├── frontends │ ├── __init__.py │ ├── bootstrap5.py │ └── foundation6.py ├── helpers.py ├── locale │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── nl │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── sq │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── migrations │ ├── 0001_initial.py │ ├── 0002_alter_form_cmsplugin_ptr_and_more.py │ ├── 0003_auto_20230129_1950.py │ └── __init__.py ├── models.py ├── recaptcha.py ├── settings.py ├── static │ └── djangocms_form_builder │ │ ├── css │ │ ├── actions_form.css │ │ └── button_group.css │ │ └── js │ │ └── actions_form.js ├── templates │ ├── captcha │ │ └── includes │ │ │ ├── js_v2_checkbox.html │ │ │ └── js_v2_invisible.html │ ├── djangocms_form_builder │ │ ├── actions │ │ │ └── submit_message.html │ │ ├── admin │ │ │ └── widgets │ │ │ │ ├── button_group.html │ │ │ │ ├── button_group_color_option.html │ │ │ │ ├── button_group_option.html │ │ │ │ ├── icon_group_option.html │ │ │ │ └── select.html │ │ ├── ajax_base.html │ │ ├── ajax_form.html │ │ ├── bootstrap5 │ │ │ ├── form.html │ │ │ ├── render │ │ │ │ ├── field.html │ │ │ │ ├── form.html │ │ │ │ └── section.html │ │ │ └── widgets │ │ │ │ ├── base.html │ │ │ │ └── submit.html │ │ ├── mails │ │ │ └── default │ │ │ │ └── mail_html.html │ │ └── widgets │ │ │ ├── input_option.html │ │ │ └── mutliple_input.html │ └── djangocms_frontend │ │ └── admin │ │ └── base.html ├── templatetags │ ├── __init__.py │ └── form_builder_tags.py ├── urls.py └── views.py ├── docs ├── Makefile ├── requirements.in ├── requirements.txt └── source │ ├── conf.py │ ├── forms.rst │ ├── index.rst │ ├── screenshots │ ├── accordion-example.png │ ├── accordion-plugins.png │ ├── add_plugin.png │ ├── adv-settings-active.png │ ├── alert-example.png │ ├── alert-plugins.png │ ├── badge-example.png │ ├── card-example.png │ ├── card-inner.png │ ├── card-overlay-example.png │ ├── card-plugins.png │ ├── card.png │ ├── col.png │ ├── container.png │ ├── form-plugin.png │ ├── row.png │ ├── tab-error-indicator.png │ ├── tabs-advanced.png │ ├── tabs-background.png │ ├── tabs-main.png │ ├── tabs-spacing.png │ └── tabs-visibility.png │ └── spelling_wordlist.txt ├── pyproject.toml ├── run_tests.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── fixtures.py ├── helpers.py ├── requirements ├── base.txt ├── dj32_cms310.txt ├── dj32_cms311.txt ├── dj42_cms311.txt ├── dj42_cms41.txt ├── dj50_cms41.txt └── dj51_cms41.txt ├── templates └── page.html ├── test_actions.py ├── test_app ├── cms_plugins.py └── templates │ └── test_app │ └── container.html ├── test_fields.py ├── test_form_registry.py ├── test_formeditor.py ├── test_migrations.py ├── test_models.py ├── test_settings.py └── urls.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = djangocms_form_builder 4 | omit = 5 | djangocms_form_builder/migrations/* 6 | djangocms_form_builder/management/* 7 | tests/* 8 | 9 | [report] 10 | exclude_lines = 11 | pragma: no cover 12 | def __repr__ 13 | if self.debug: 14 | if settings.DEBUG 15 | raise AssertionError 16 | raise NotImplementedError 17 | if 0: 18 | if __name__ == .__main__.: 19 | ignore_errors = True 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | max_line_length = 80 13 | 14 | [*.py] 15 | max_line_length = 120 16 | quote_type = single 17 | 18 | [*.{scss,js,html}] 19 | max_line_length = 120 20 | indent_style = space 21 | quote_type = double 22 | 23 | [*.js] 24 | max_line_length = 120 25 | quote_type = single 26 | 27 | [*.rst] 28 | max_line_length = 80 29 | 30 | [*.yml] 31 | indent_size = 2 32 | 33 | [*plugins/code.html] 34 | insert_final_newline = false 35 | 36 | [*plugins/responsive.html] 37 | insert_final_newline = false 38 | 39 | [*plugins/image.html] 40 | insert_final_newline = false 41 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | // documentation about rules can be found on http://eslint.org/docs/user-guide/configuring 5 | // based on http://eslint.org/docs/user-guide/configuring 6 | 'extends': 'eslint:recommended', 7 | // http://eslint.org/docs/user-guide/configuring.html#specifying-environments 8 | 'env': { 9 | 'browser': true, 10 | 'node': true, 11 | 'es6': true, 12 | }, 13 | 'parser': 'babel-eslint', 14 | 'parserOptions': { 15 | 'sourceType': 'module', 16 | }, 17 | 'rules': { 18 | // 0 = ignore, 1 = warning, 2 = error 19 | 'indent': [2, 4], 20 | 'quotes': [1, 'single'], 21 | 'comma-dangle': [1, 'always-multiline'], 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: CodeCov 2 | 3 | on: [push, pull_request] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | coverage: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | python-version: [ 3.9, "3.10", "3.11", "3.12"] # latest release minus two 16 | requirements-file: [ 17 | dj42_cms311.txt, 18 | dj42_cms41.txt, 19 | dj50_cms41.txt, 20 | dj51_cms41.txt, 21 | ] 22 | os: [ 23 | ubuntu-latest, 24 | ] 25 | exclude: 26 | - python-version: 3.9 27 | requirements-file: dj50_cms41.txt 28 | - python-version: 3.9 29 | requirements-file: dj51_cms41.txt 30 | 31 | steps: 32 | - uses: actions/checkout@v3 33 | with: 34 | fetch-depth: '2' 35 | 36 | - name: Setup Python 37 | uses: actions/setup-python@master 38 | with: 39 | python-version: ${{ matrix.python-version }} 40 | - name: Generate Report 41 | run: | 42 | pip install -r tests/requirements/${{ matrix.requirements-file }} 43 | pip install -e . 44 | coverage run ./run_tests.py 45 | - name: Upload Coverage to Codecov 46 | uses: codecov/codecov-action@v3 47 | 48 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '27 18 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript', 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v2 71 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: [push, pull_request] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | name: build 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: '3.9' 20 | cache: 'pip' 21 | - name: Cache dependencies 22 | uses: actions/cache@v3 23 | with: 24 | path: ~/.cache/pip 25 | key: ${{ runner.os }}-pip-${{ hashFiles('docs/requirements.txt') }} 26 | restore-keys: | 27 | ${{ runner.os }}-pip- 28 | - run: python -m pip install -r docs/requirements.txt 29 | - name: Build docs 30 | run: | 31 | cd docs 32 | make install 33 | make html 34 | 35 | spelling: 36 | runs-on: ubuntu-latest 37 | name: spelling 38 | needs: build 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v3 42 | - name: Set up Python 43 | uses: actions/setup-python@v4 44 | with: 45 | python-version: '3.9' 46 | cache: 'pip' 47 | - name: Cache dependencies 48 | uses: actions/cache@v3 49 | with: 50 | path: ~/.cache/pip 51 | key: ${{ runner.os }}-pip-${{ hashFiles('docs/requirements.txt') }} 52 | restore-keys: | 53 | ${{ runner.os }}-pip- 54 | - run: python -m pip install -r docs/requirements.txt 55 | - name: Check spelling 56 | run: | 57 | cd docs 58 | make spelling 59 | 60 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Lint PR" 2 | 3 | # Validates PR titles against the conventional commit spec 4 | # https://github.com/commitizen/conventional-commit-types 5 | 6 | on: 7 | pull_request_target: 8 | types: 9 | - opened 10 | - edited 11 | - synchronize 12 | 13 | jobs: 14 | main: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@v4 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | flake8: 11 | name: flake8 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: 3.9 20 | - name: Install flake8 21 | run: pip install --upgrade flake8 22 | - name: Run flake8 23 | uses: liskin/gh-problem-matcher-wrap@v1 24 | with: 25 | linters: flake8 26 | run: flake8 27 | 28 | isort: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Set up Python 34 | uses: actions/setup-python@v4 35 | with: 36 | python-version: 3.9 37 | - run: python -m pip install isort 38 | - name: isort 39 | uses: liskin/gh-problem-matcher-wrap@v1 40 | with: 41 | linters: isort 42 | run: isort --check --diff djangocms_form_builder 43 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-live-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to pypi 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | build-n-publish: 10 | name: Build and publish Python 🐍 distributions 📦 to pypi 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Set up Python 3.10 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: '3.10' 18 | 19 | - name: Install pypa/build 20 | run: >- 21 | python -m 22 | pip install 23 | build 24 | --user 25 | - name: Build a binary wheel and a source tarball 26 | run: >- 27 | python -m 28 | build 29 | --sdist 30 | --wheel 31 | --outdir dist/ 32 | . 33 | 34 | - name: Publish distribution 📦 to PyPI 35 | if: startsWith(github.ref, 'refs/tags') 36 | uses: pypa/gh-action-pypi-publish@release/v1 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-test-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to TestPyPI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build-n-publish: 13 | name: Build and publish Python 🐍 distributions 📦 to TestPyPI 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@master 17 | - name: Set up Python 3.10 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.10' 21 | 22 | - name: Install pypa/build 23 | run: >- 24 | python -m 25 | pip install 26 | build 27 | --user 28 | - name: Build a binary wheel and a source tarball 29 | run: >- 30 | python -m 31 | build 32 | --sdist 33 | --wheel 34 | --outdir dist/ 35 | . 36 | 37 | - name: Publish distribution 📦 to Test PyPI 38 | uses: pypa/gh-action-pypi-publish@release/v1 39 | with: 40 | user: __token__ 41 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 42 | repository_url: https://test.pypi.org/legacy/ 43 | skip_existing: true 44 | verbose: true 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *$py.class 3 | *.egg-info 4 | *.log 5 | *.pot 6 | .DS_Store 7 | .coverage 8 | .coverage/ 9 | .eggs/ 10 | .idea/ 11 | .project/ 12 | .pydevproject/ 13 | .vscode/ 14 | .settings/ 15 | .tox/ 16 | __pycache__/ 17 | build/ 18 | dist/ 19 | env/ 20 | .venv/ 21 | venv/ 22 | 23 | /~ 24 | /node_modules 25 | .sass-cache 26 | *.css.map 27 | npm-debug.log 28 | package-lock.json 29 | 30 | local.sqlite 31 | /docs/_build/ 32 | 33 | /filer_public/ 34 | /filer_public_thumbnails/ 35 | /private/sass/bootstrap/ 36 | /htmlcov/ 37 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autofix_prs: false 3 | 4 | repos: 5 | - repo: https://github.com/asottile/pyupgrade 6 | rev: v2.32.1 7 | hooks: 8 | - id: pyupgrade 9 | args: [ "--py38-plus" ] 10 | - repo: https://github.com/adamchainz/django-upgrade 11 | rev: '1.16.0' 12 | hooks: 13 | - id: django-upgrade 14 | args: [ --target-version, "4.2" ] 15 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/source/conf.py 5 | fail_on_warning: false 6 | 7 | formats: 8 | - epub 9 | - pdf 10 | 11 | python: 12 | version: 3.8 13 | 14 | install: 15 | - requirements: docs/requirements.txt 16 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // documentation about rules can be found on https://stylelint.io/user-guide/rules/ 3 | // based on https://github.com/stylelint/stylelint-config-standard/blob/master/index.js 4 | // configurator avaialble https://maximgatilin.github.io/stylelint-config/ 5 | 'rules': { 6 | 'at-rule-empty-line-before': [ 'always', { 7 | except: [ 8 | 'blockless-after-same-name-blockless', 9 | 'first-nested', 10 | ], 11 | ignore: ['after-comment', 'inside-block'], 12 | } ], 13 | 'at-rule-name-case': 'lower', 14 | 'at-rule-name-space-after': 'always-single-line', 15 | 'at-rule-semicolon-newline-after': 'always', 16 | 'block-closing-brace-empty-line-before': 'never', 17 | 'block-closing-brace-newline-after': 'always', 18 | 'block-closing-brace-newline-before': 'always-multi-line', 19 | 'block-closing-brace-space-before': 'always-single-line', 20 | 'block-no-empty': true, 21 | 'block-opening-brace-newline-after': 'always-multi-line', 22 | 'block-opening-brace-space-after': 'always-single-line', 23 | 'block-opening-brace-space-before': 'always', 24 | 'color-hex-case': 'lower', 25 | 'color-hex-length': 'short', 26 | 'color-no-invalid-hex': true, 27 | // 'comment-empty-line-before': ['never'], 28 | 'comment-no-empty': true, 29 | 'comment-whitespace-inside': 'always', 30 | 'custom-property-empty-line-before': [ 'always', { 31 | except: [ 32 | 'after-custom-property', 33 | 'first-nested', 34 | ], 35 | ignore: [ 36 | 'after-comment', 37 | 'inside-single-line-block', 38 | ], 39 | } ], 40 | 'declaration-bang-space-after': 'never', 41 | 'declaration-bang-space-before': 'always', 42 | 'declaration-block-no-duplicate-properties': [ true, { 43 | ignore: ['consecutive-duplicates-with-different-values'], 44 | } ], 45 | 'declaration-block-no-redundant-longhand-properties': true, 46 | 'declaration-block-no-shorthand-property-overrides': true, 47 | 'declaration-block-semicolon-newline-after': 'always-multi-line', 48 | 'declaration-block-semicolon-space-after': 'always-single-line', 49 | 'declaration-block-semicolon-space-before': 'never', 50 | 'declaration-block-single-line-max-declarations': 1, 51 | 'declaration-block-trailing-semicolon': 'always', 52 | 'declaration-colon-newline-after': 'always-multi-line', 53 | 'declaration-colon-space-after': 'always-single-line', 54 | 'declaration-colon-space-before': 'never', 55 | 'declaration-empty-line-before': [ 'always', { 56 | except: [ 57 | 'after-declaration', 58 | 'first-nested', 59 | ], 60 | ignore: [ 61 | 'after-comment', 62 | 'inside-single-line-block', 63 | ], 64 | } ], 65 | 'font-family-no-duplicate-names': true, 66 | 'function-calc-no-unspaced-operator': true, 67 | 'function-comma-newline-after': 'always-multi-line', 68 | 'function-comma-space-after': 'always-single-line', 69 | 'function-comma-space-before': 'never', 70 | 'function-linear-gradient-no-nonstandard-direction': true, 71 | 'function-max-empty-lines': 0, 72 | 'function-name-case': 'lower', 73 | 'function-parentheses-newline-inside': 'always-multi-line', 74 | 'function-parentheses-space-inside': 'never-single-line', 75 | 'function-whitespace-after': 'always', 76 | 'indentation': 4, 77 | 'keyframe-declaration-no-important': true, 78 | 'length-zero-no-unit': true, 79 | 'max-empty-lines': 1, 80 | 'media-feature-colon-space-after': 'always', 81 | 'media-feature-colon-space-before': 'never', 82 | 'media-feature-name-case': 'lower', 83 | 'media-feature-name-no-unknown': true, 84 | 'media-feature-parentheses-space-inside': 'never', 85 | 'media-feature-range-operator-space-after': 'always', 86 | 'media-feature-range-operator-space-before': 'always', 87 | 'media-query-list-comma-newline-after': 'always-multi-line', 88 | 'media-query-list-comma-space-after': 'always-single-line', 89 | 'media-query-list-comma-space-before': 'never', 90 | 'no-empty-source': true, 91 | 'no-eol-whitespace': true, 92 | 'no-extra-semicolons': true, 93 | 'no-invalid-double-slash-comments': true, 94 | 'no-missing-end-of-source-newline': true, 95 | 'number-leading-zero': 'always', 96 | 'number-no-trailing-zeros': true, 97 | 'property-case': 'lower', 98 | 'property-no-unknown': true, 99 | 'rule-empty-line-before': [ 'always-multi-line', { 100 | except: ['first-nested'], 101 | ignore: ['after-comment'], 102 | } ], 103 | 'selector-attribute-brackets-space-inside': 'never', 104 | 'selector-attribute-operator-space-after': 'never', 105 | 'selector-attribute-operator-space-before': 'never', 106 | 'selector-combinator-space-after': 'always', 107 | 'selector-combinator-space-before': 'always', 108 | 'selector-descendant-combinator-no-non-space': true, 109 | 'selector-list-comma-newline-after': 'always', 110 | 'selector-list-comma-space-before': 'never', 111 | 'selector-max-empty-lines': 0, 112 | 'selector-pseudo-class-case': 'lower', 113 | 'selector-pseudo-class-no-unknown': true, 114 | 'selector-pseudo-class-parentheses-space-inside': 'never', 115 | 'selector-pseudo-element-case': 'lower', 116 | 'selector-pseudo-element-colon-notation': 'single', 117 | 'selector-pseudo-element-no-unknown': true, 118 | // 'selector-no-type': true, 119 | 'selector-no-vendor-prefix': true, 120 | // 'selector-no-universal': true, 121 | 'selector-type-case': 'lower', 122 | 'selector-type-no-unknown': true, 123 | 'shorthand-property-no-redundant-values': true, 124 | 'string-no-newline': true, 125 | 'unit-case': 'lower', 126 | 'unit-no-unknown': true, 127 | 'value-list-comma-newline-after': 'always-multi-line', 128 | 'value-list-comma-space-after': 'always-single-line', 129 | 'value-list-comma-space-before': 'never', 130 | 'value-list-max-empty-lines': 0, 131 | }, 132 | }; 133 | -------------------------------------------------------------------------------- /.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | exclude = ''' 3 | /( 4 | \.git 5 | | \.hg 6 | | \.tox 7 | | \venv 8 | | _build 9 | | build 10 | | dist 11 | )/ 12 | ''' 13 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [o:divio:p:django-cms-form-builder:r:djangopo] 5 | file_filter = djangocms_form_builder/locale//LC_MESSAGES/django.po 6 | source_file = djangocms_form_builder/locale/en/LC_MESSAGES/django.po 7 | type = PO 8 | minimum_perc = 0 9 | resource_name = django.po 10 | 11 | -------------------------------------------------------------------------------- /.tx/transifex.yaml: -------------------------------------------------------------------------------- 1 | git: 2 | filters: 3 | - filter_type: file 4 | file_format: PO 5 | source_file: djangocms_form_builder/locale/en/LC_MESSAGES/django.po 6 | source_language: en 7 | translation_files_expression: 'djangocms_form_builder/locale//LC_MESSAGES/django.po' 8 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | 0.3.2 (2025-03-04) 6 | ================== 7 | 8 | * fix: Action tab failed form validation if djangocms-link was installed 9 | 10 | 0.3.1 (2025-03-03) 11 | ================== 12 | 13 | * fix: Send mail action failed by @fsbraun in https://github.com/django-cms/djangocms-form-builder/pull/21 14 | * fix: Correct send_mail recipients parameter in action.py by @fdik in https://github.com/django-cms/djangocms-form-builder/pull/22 15 | * docs: Update Codecov link in README.rst by @fsbraun in https://github.com/django-cms/djangocms-form-builder/pull/23 16 | * fix: prevent duplicate submit buttons in forms by @earthcomfy in https://github.com/django-cms/djangocms-form-builder/pull/27 17 | 18 | * @fdik made their first contribution in https://github.com/django-cms/djangocms-form-builder/pull/22 19 | * @earthcomfy made their first contribution in https://github.com/django-cms/djangocms-form-builder/pull/27 20 | 21 | 22 | 0.3.0 (2025-01-07) 23 | ================== 24 | 25 | * feat: Success message and redirect action by @fsbraun 26 | * fix: forms did not redirect to same page if sent from alias by @fsbraun 27 | 28 | 0.2.0 (2025-01-06) 29 | ================== 30 | 31 | * fix: github coverage action by @fsbraun in https://github.com/django-cms/djangocms-form-builder/pull/12 32 | * fix: an error when an anonymous user fills the form by @arunk in https://github.com/django-cms/djangocms-form-builder/pull/13 33 | * fix: Add support for Django-entangled 0.6+ by @fsbraun in https://github.com/django-cms/djangocms-form-builder/pull/19 34 | * docs: Updated README.rst to show where to add actions by @arunk in https://github.com/django-cms/djangocms-form-builder/pull/14 35 | * chore: Added venv/ directory to .gitignore by @arunk in https://github.com/django-cms/djangocms-form-builder/pull/15 36 | 37 | **New Contributors** 38 | * @arunk made their first contribution in https://github.com/django-cms/djangocms-form-builder/pull/13 39 | 40 | 41 | 0.1.1 (2021-09-14) 42 | ================== 43 | 44 | * feat: updated captcha optional til active by @svandeneertwegh in https://github.com/fsbraun/djangocms-form-builder/pull/4 45 | * feat: Allow actions to add form fields for configuration by @fsbraun in https://github.com/fsbraun/djangocms-form-builder/pull/6 46 | * fix: Update converage action by @fsbraun in https://github.com/fsbraun/djangocms-form-builder/pull/10 47 | * feat: move to hatch build process by @fsbraun 48 | * ci: Add tests for registry by @fsbraun in https://github.com/fsbraun/djangocms-form-builder/pull/5 49 | 50 | New Contributors 51 | 52 | * @svandeneertwegh made their first contribution in https://github.com/fsbraun/djangocms-form-builder/pull/4 53 | 54 | 0.1.0 (unreleased) 55 | ================== 56 | 57 | * Set ``default_auto_field`` to ``BigAutoField`` to ensure projects don't try to create a migration if they still use ``AutoField`` 58 | * Transfer of forms app from djangocms-frontend 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, Fabian Braun 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Fabian Braun, the django CMS association nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL DIVIO AG BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ######################## 2 | django CMS form builder 3 | ######################## 4 | 5 | |pypi| |coverage| |python| |django| |djangocms| |djangocms4| 6 | 7 | **djangocms-form-builder** supports rendering of styled forms. The objective is to tightly integrate forms in the website design. **djangocms-form-builder** allows as many forms as you wish on one page. All forms are **xhr-based**. To this end, **djangocms-form-builder** extends the django CMS plugin model allowing a form plugin to receive xhr post requests. 8 | 9 | There are two different ways to manage forms with **djangocms-form-builder**: 10 | 11 | 1. **Building a form with django CMS' powerful structure board.** This is fast an easy. It integrates smoothly with other design elements, especially the grid elements allowing to design simple responsive forms. 12 | 13 | Form actions can be configured by form. Built in actions include saving the results in the database for later evaluation and mailing submitted forms to the site admins. Other form actions can be registered. 14 | 15 | 2. Works with **django CMS v4** and **djangocms-alias** to manage your forms centrally. Djangocms-alias becomes your form editor and forms can be placed on pages by referring to them with their alias. 16 | 17 | 3. **Registering an application-specific form with djangocms-form-builder.** If you already have forms you may register them with djangocms-form-builder and allow editors to use them in the form plugin. If you only have simpler design requirements, **djangocms-form-builder** allows you to use fieldsets as with admin forms. 18 | 19 | ************** 20 | Key features 21 | ************** 22 | 23 | - Supports `Bootstrap 5 `_. 24 | 25 | - Open architecture to support other css frameworks. 26 | 27 | - Integrates with `django-crispy-forms `_ 28 | 29 | - Integrates with `djangocms-frontend `_ 30 | 31 | 32 | Feedback 33 | ======== 34 | 35 | This project is in a early stage. All feedback is welcome! Please mail me at fsbraun(at)gmx.de 36 | 37 | Also, all contributions are welcome. 38 | 39 | Contributing 40 | ============ 41 | 42 | This is a an open-source project. We'll be delighted to receive your feedback in the form of issues and pull requests. Before submitting your pull request, please review our `contribution guidelines `_. 43 | 44 | We're grateful to all contributors who have helped create and maintain this package. Contributors are listed at the `contributors `_ section. 45 | 46 | 47 | ************ 48 | Installation 49 | ************ 50 | 51 | For a manual install: 52 | 53 | - run ``pip install djangocms-form-builder``, **or** 54 | 55 | - run ``pip install git+https://github.com/fsbraun/djangocms-form-builder@master#egg=djangocms-form-builder`` 56 | 57 | - add ``djangocms_form_builder`` to your ``INSTALLED_APPS``. (If you are using both djangocms-frontend and djangocms-form-builder, add it **after** djangocms-frontend 58 | 59 | - run ``python manage.py migrate`` 60 | 61 | ***** 62 | Usage 63 | ***** 64 | 65 | Creating forms using django CMS' structure board 66 | ================================================ 67 | 68 | First create a ``Form`` plugin to add a form. Each form created with help of the structure board needs a unique identifier (formatted as a slug). 69 | 70 | Add form fields by adding child classes to the form plugin. Child classes can be form fields but also any other CMS Plugin. CMS Plugins may, e.g., be used to add custom formatting or additional help texts to a form. 71 | 72 | Form fields 73 | ----------- 74 | 75 | Currently the following form fields are supported: 76 | 77 | * CharField, EmailField, URLField 78 | * DecimalField, IntegerField 79 | * Textarea 80 | * DateField, DateTimeField, TimeField 81 | * SelectField 82 | * BooleanField 83 | 84 | A Form plugin must not be used within another Form plugin. 85 | 86 | Actions 87 | ------- 88 | 89 | Upon submission of a valid form actions can be performed. 90 | 91 | Four actions come with djangocms-form-builder comes with four actions built-in 92 | 93 | * **Save form submission** - Saves each form submission to the database. See the 94 | results in the admin interface. 95 | * **Send email** - Sends an email to the site admins with the form data. 96 | * **Success message** - Specify a message to be shown to the user upon 97 | successful form submission. 98 | * **Redirect after submission** - Specify a link to a page where the user is 99 | redirected after successful form submission. 100 | 101 | Actions can be configured in the form plugin. 102 | 103 | A project can register as many actions as it likes:: 104 | 105 | from djangocms_form_builder import actions 106 | 107 | @actions.register 108 | class MyAction(actions.FormAction): 109 | verbose_name = _("Everything included action") 110 | 111 | def execute(self, form, request): 112 | ... # This method is run upon successful submission of the form 113 | 114 | 115 | To add this action, might need to be added to your project only after all Django apps have loaded at startup. 116 | You can put these actions in your apps models.py file. Another options is your apps, apps.py file:: 117 | 118 | from django.apps import AppConfig 119 | 120 | class MyAppConfig(AppConfig): 121 | default_auto_field = 'django.db.models.BigAutoField' 122 | name = 'myapp' 123 | label = 'myapp' 124 | verbose_name = _("My App") 125 | 126 | def ready(self): 127 | super().ready() 128 | 129 | from djangocms_form_builder import actions 130 | 131 | @actions.register 132 | class MyAction(actions.FormAction): # Or import from within the ready method 133 | verbose_name = _("Everything included action") 134 | 135 | def execute(self, form, request): 136 | ... # This method is run upon successful submission of the form 137 | # Process form and request data, you can send an email to the person who filled the form 138 | # Or admins though that functionality is available from the default SendMailAction 139 | 140 | 141 | 142 | Using (existing) Django forms with djangocms-form-builder 143 | ========================================================= 144 | 145 | The ``Form`` plugin also provides access to Django forms if they are registered with djangocms-form-builder:: 146 | 147 | from djangocms_form_builder import register_with_form_builder 148 | 149 | @register_with_form_builder 150 | class MyGreatForm(forms.Form): 151 | ... 152 | 153 | Alternatively you can also register at any other place in the code by running ``register_with_form_builder(AnotherGreatForm)``. 154 | 155 | By default the class name is translated to a human readable form (``MyGreatForm`` -> ``"My Great Form"``). Additional information may be added using Meta classes:: 156 | 157 | @register_with_form_builder 158 | class MyGreatForm(forms.Form): 159 | class Meta: 160 | verbose_name = _("My great form") # can be localized 161 | redirect = "https://somewhere.org" # string or object with get_absolute_url() method 162 | floating_labels = True # switch on floating labels 163 | field_sep = "mb-3" # separator used between fields (depends on css framework) 164 | 165 | The verbose name will be shown in a Select field of the Form plugin. 166 | 167 | Upon form submission a ``save()`` method of the form (if it has one). After executing the ``save()`` method the user is redirected to the url given in the ``redirect`` attribute. 168 | 169 | Actions are not available for Django forms. Any actions to be performed upon submission should reside in its ``save()`` method. 170 | 171 | 172 | .. |pypi| image:: https://badge.fury.io/py/djangocms-form-builder.svg 173 | :target: http://badge.fury.io/py/djangocms-form-builder 174 | 175 | .. |coverage| image:: https://codecov.io/gh/django-cms/djangocms-form-builder/branch/main/graph/badge.svg 176 | :target: https://codecov.io/gh/django-cms/djangocms-form-builder 177 | 178 | .. |python| image:: https://img.shields.io/badge/python-3.7+-blue.svg 179 | :target: https://pypi.org/project/djangocms-form-builder/ 180 | 181 | .. |django| image:: https://img.shields.io/badge/django-3.2-blue.svg 182 | :target: https://www.djangoproject.com/ 183 | 184 | .. |djangocms| image:: https://img.shields.io/badge/django%20CMS-3.8%2B-blue.svg 185 | :target: https://www.django-cms.org/ 186 | 187 | .. |djangocms4| image:: https://img.shields.io/badge/django%20CMS-4-blue.svg 188 | :target: https://www.django-cms.org/ 189 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | last 2 versions 4 | ie >= 11 5 | -------------------------------------------------------------------------------- /djangocms_form_builder/__init__.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | try: 4 | from django.utils.translation import gettext_lazy as _ 5 | except ModuleNotFoundError: 6 | _ = lambda x: x # noqa: E731 7 | 8 | 9 | __version__ = "0.3.2" 10 | 11 | _form_registry = {} 12 | 13 | 14 | def verbose_name(form_class): 15 | """returns the verbose_name property of a Meta class if present or else 16 | splits the camel-cased form class name""" 17 | if hasattr(form_class, "Meta") and hasattr(form_class.Meta, "verbose_name"): 18 | return getattr(form_class.Meta, "verbose_name") # noqa 19 | class_name = form_class.__name__.rsplit(".", 1)[-1] 20 | from re import finditer 21 | 22 | matches = finditer( 23 | ".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)", class_name 24 | ) 25 | return " ".join(m.group(0) for m in matches) 26 | 27 | 28 | def get_registered_forms(): 29 | """Creates a tuple for a ChoiceField to select form""" 30 | result = tuple( 31 | (hash, verbose_name(form_class)) for hash, form_class in _form_registry.items() 32 | ) 33 | return result if result else ((_("No forms registered"), ()),) 34 | 35 | 36 | def register_with_form_builder(form_class): 37 | """Function to call or decorator for a Form class to make it available for the plugin""" 38 | hash = hashlib.sha1(form_class.__name__.encode("utf-8")).hexdigest() 39 | _form_registry.update({hash: form_class}) 40 | return form_class 41 | 42 | 43 | __all__ = ["register_with_form_builder", "get_registered_forms"] 44 | -------------------------------------------------------------------------------- /djangocms_form_builder/actions.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from django import forms 4 | from django.apps import apps 5 | from django.core.exceptions import ImproperlyConfigured 6 | from django.core.validators import EmailValidator 7 | from django.template import TemplateDoesNotExist 8 | from django.template.loader import render_to_string 9 | from django.utils.html import strip_tags 10 | from django.utils.translation import gettext_lazy as _ 11 | from djangocms_text_ckeditor.fields import HTMLFormField 12 | from entangled.forms import EntangledModelFormMixin 13 | 14 | from . import models 15 | from .entry_model import FormEntry 16 | from .helpers import get_option, insert_fields 17 | from .settings import MAIL_TEMPLATE_SETS 18 | 19 | _action_registry = {} 20 | 21 | 22 | def get_registered_actions(): 23 | """Creates a tuple for a ChoiceField to select form""" 24 | result = tuple( 25 | (hash, action_class.verbose_name) 26 | for hash, action_class in _action_registry.items() 27 | ) 28 | return result if result else ((_("No actions registered"), ()),) 29 | 30 | 31 | def register(action_class): 32 | """Function to call or decorator for an Action class to make it available for the plugin""" 33 | 34 | if not issubclass(action_class, FormAction): 35 | raise ImproperlyConfigured( 36 | "djangocms_form_builder.actions.register only " 37 | "accepts subclasses of djangocms_form_builder.actions.FormAction" 38 | ) 39 | if not action_class.verbose_name: 40 | raise ImproperlyConfigured( 41 | "FormActions need to have a verbose_name property to be registered", 42 | ) 43 | hash = hashlib.sha1(action_class.__name__.encode("utf-8")).hexdigest() 44 | _action_registry.update({hash: action_class}) 45 | return action_class 46 | 47 | 48 | def unregister(action_class): 49 | hash = hashlib.sha1(action_class.__name__.encode("utf-8")).hexdigest() 50 | if hash in _action_registry: 51 | del _action_registry[hash] 52 | return action_class 53 | 54 | 55 | def get_action_class(action): 56 | return _action_registry.get(action, None) 57 | 58 | 59 | def get_hash(action_class): 60 | return hashlib.sha1(action_class.__name__.encode("utf-8")).hexdigest() 61 | 62 | 63 | class ActionMixin: 64 | """Adds action form elements to Form plugin admin""" 65 | 66 | def get_form(self, request, *args, **kwargs): 67 | """Creates new form class based adding the actions as mixins""" 68 | return type("FormActionAdminForm", (self.form, *_action_registry.values()), {}) 69 | 70 | def get_fieldsets(self, request, obj=None): 71 | fieldsets = super().get_fieldsets(request, obj) 72 | for action in _action_registry.values(): 73 | new_fields = list(action.declared_fields.keys()) 74 | if new_fields: 75 | hash = hashlib.sha1(action.__name__.encode("utf-8")).hexdigest() 76 | fieldsets = insert_fields( 77 | fieldsets, 78 | new_fields, 79 | block=None, 80 | position=-1, 81 | blockname=action.verbose_name, 82 | blockattrs=dict(classes=(f"c{hash}", "action-hide")), 83 | ) 84 | return fieldsets 85 | 86 | 87 | class FormAction(EntangledModelFormMixin): 88 | class Meta: 89 | entangled_fields = {"action_parameters": []} 90 | model = models.Form 91 | exclude = () 92 | 93 | class Media: 94 | js = ("djangocms_form_builder/js/actions_form.js",) 95 | css = {"all": ("djangocms_form_builder/css/actions_form.css",)} 96 | 97 | verbose_name = None 98 | 99 | def execute(self, form, request): 100 | raise NotImplementedError() 101 | 102 | @staticmethod 103 | def get_parameter(form, param): 104 | return (get_option(form, "form_parameters") or {}).get(param, None) 105 | 106 | 107 | @register 108 | class SaveToDBAction(FormAction): 109 | verbose_name = _("Save form submission") 110 | 111 | def execute(self, form, request): 112 | if get_option(form, "unique", False) and get_option( 113 | form, "login_required", False 114 | ): 115 | keys = { 116 | "form_name": get_option(form, "form_name"), 117 | "form_user": request.user, 118 | } 119 | defaults = {} 120 | else: 121 | keys = {} 122 | defaults = { 123 | "form_name": get_option(form, "form_name"), 124 | "form_user": None if request.user.is_anonymous else request.user, 125 | } 126 | defaults.update( 127 | { 128 | "entry_data": form.cleaned_data, 129 | "html_headers": dict( 130 | user_agent=request.headers["User-Agent"], 131 | referer=request.headers["Referer"], 132 | ), 133 | } 134 | ) 135 | if keys: # update_or_create only works if at least one key is given 136 | try: 137 | FormEntry.objects.update_or_create(**keys, defaults=defaults) 138 | except FormEntry.MultipleObjectsReturned: # Delete outdated objects 139 | FormEntry.objects.filter(**keys).delete() 140 | FormEntry.objects.create(**keys, **defaults) 141 | else: 142 | FormEntry.objects.create(**defaults), True 143 | 144 | 145 | SAVE_TO_DB_ACTION = next(iter(_action_registry)) if _action_registry else None 146 | 147 | 148 | def validate_recipients(value): 149 | recipients = value.split() 150 | for recipient in recipients: 151 | EmailValidator( 152 | message=_('Please replace "%s" by a valid email address.') % recipient 153 | )(recipient) 154 | 155 | 156 | @register 157 | class SendMailAction(FormAction): 158 | class Meta: 159 | entangled_fields = { 160 | "action_parameters": [ 161 | "sendemail_recipients", 162 | "sendemail_template", 163 | ] 164 | } 165 | 166 | verbose_name = _("Send email") 167 | from_mail = None 168 | template = "djangocms_form_builder/actions/mail.html" 169 | subject = _("%(form_name)s form submission") 170 | 171 | sendemail_recipients = forms.CharField( 172 | label=_("Mail recipients"), 173 | required=False, 174 | initial="", 175 | validators=[ 176 | validate_recipients, 177 | ], 178 | help_text=_("Space or newline separated list of email addresses."), 179 | widget=forms.Textarea, 180 | ) 181 | 182 | sendemail_template = forms.ChoiceField( 183 | label=_("Mail template set"), 184 | required=True, 185 | initial=MAIL_TEMPLATE_SETS[0][0], 186 | choices=MAIL_TEMPLATE_SETS, 187 | widget=forms.Select if len(MAIL_TEMPLATE_SETS) > 1 else forms.HiddenInput, 188 | ) 189 | 190 | def execute(self, form, request): 191 | from django.core.mail import mail_admins, send_mail 192 | 193 | recipients = self.get_parameter(form, "sendemail_recipients") or "" 194 | template_set = self.get_parameter(form, "sendemail_template") or "default" 195 | context = dict( 196 | cleaned_data=form.cleaned_data, 197 | form_name=getattr(form.Meta, "verbose_name", ""), 198 | user=request.user, 199 | user_agent=request.headers["User-Agent"] 200 | if "User-Agent" in request.headers 201 | else "", 202 | referer=request.headers["Referer"] if "Referer" in request.headers else "", 203 | ) 204 | 205 | html_message = render_to_string( 206 | f"djangocms_form_builder/mails/{template_set}/mail_html.html", context 207 | ) 208 | try: 209 | message = render_to_string( 210 | f"djangocms_form_builder/mails/{template_set}/mail.txt", context 211 | ) 212 | except TemplateDoesNotExist: 213 | message = strip_tags(html_message) 214 | try: 215 | subject = render_to_string( 216 | f"djangocms_form_builder/mails/{template_set}/subject.txt", context 217 | ) 218 | except TemplateDoesNotExist: 219 | subject = self.subject % dict(form_name=context["form_name"]) 220 | 221 | if not recipients: 222 | return mail_admins( 223 | subject, 224 | message, 225 | fail_silently=True, 226 | html_message=html_message, 227 | ) 228 | else: 229 | return send_mail( 230 | subject, 231 | message, 232 | self.from_mail, 233 | recipients.split(), 234 | fail_silently=True, 235 | html_message=html_message, 236 | ) 237 | 238 | 239 | @register 240 | class SuccessMessageAction(FormAction): 241 | verbose_name = _("Success message") 242 | 243 | class Meta: 244 | entangled_fields = { 245 | "action_parameters": [ 246 | "submitmessage_message", 247 | ] 248 | } 249 | 250 | submitmessage_message = HTMLFormField( 251 | label=_("Message"), 252 | required=True, 253 | initial=_("

Thank you for your submission.

"), 254 | ) 255 | 256 | def execute(self, form, request): 257 | from .cms_plugins.ajax_plugins import SAME_PAGE_REDIRECT 258 | 259 | message = self.get_parameter(form, "submitmessage_message") 260 | # Overwrite the success context and render template 261 | form.get_success_context = lambda *args, **kwargs: {"message": message} 262 | form.Meta.options["render_success"] = ( 263 | "djangocms_form_builder/actions/submit_message.html" 264 | ) 265 | # Overwrite the default redirect to same page 266 | if form.Meta.options.get("redirect") == SAME_PAGE_REDIRECT: 267 | form.Meta.options["redirect"] = None 268 | 269 | 270 | if apps.is_installed("djangocms_link"): 271 | from djangocms_link.fields import LinkFormField 272 | from djangocms_link.helpers import get_link 273 | 274 | @register 275 | class RedirectAction(FormAction): 276 | verbose_name = _("Redirect after submission") 277 | 278 | class Meta: 279 | entangled_fields = { 280 | "action_parameters": [ 281 | "redirect_link", 282 | ] 283 | } 284 | 285 | redirect_link = LinkFormField( 286 | label=_("Link"), 287 | required=True, 288 | ) 289 | 290 | def __init__(self, *args, **kwargs): 291 | super().__init__(*args, **kwargs) 292 | if args: 293 | self.fields["redirect_link"].required = get_hash(RedirectAction) in args[0].get("form_actions", []) 294 | 295 | def execute(self, form, request): 296 | form.Meta.options["redirect"] = get_link( 297 | self.get_parameter(form, "redirect_link") 298 | ) 299 | -------------------------------------------------------------------------------- /djangocms_form_builder/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import FormEntry 4 | 5 | 6 | @admin.register(FormEntry) 7 | class FormEntryAdmin(admin.ModelAdmin): 8 | date_hierarchy = "entry_created_at" 9 | list_display = ("__str__", "form_user", "entry_created_at") 10 | list_filter = ("form_name", "form_user", "entry_created_at") 11 | readonly_fields = ["form_name", "form_user"] 12 | 13 | def has_add_permission(self, request): 14 | return False 15 | 16 | def get_form(self, request, obj=None, **kwargs): 17 | if obj: 18 | kwargs["form"] = obj.get_admin_form() 19 | return super().get_form(request, obj, **kwargs) 20 | 21 | def get_fieldsets(self, request, obj=None): 22 | if obj: 23 | return obj.get_admin_fieldsets() 24 | return super().get_fieldsets(request, obj) 25 | -------------------------------------------------------------------------------- /djangocms_form_builder/apps.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | from django.apps import AppConfig 4 | from django.conf import settings 5 | from django.urls import NoReverseMatch, clear_url_caches, include, path, reverse 6 | from django.utils.translation import gettext_lazy as _ 7 | 8 | 9 | class FormsConfig(AppConfig): 10 | default_auto_field = "django.db.models.BigAutoField" 11 | name = "djangocms_form_builder" 12 | verbose_name = _("Form builder") 13 | 14 | def ready(self): 15 | """Install the URLs""" 16 | try: 17 | reverse("form_builder:ajaxview", args=(1,)) 18 | except NoReverseMatch: # Not installed yet 19 | urlconf_module = import_module(settings.ROOT_URLCONF) 20 | urlconf_module.urlpatterns = [ 21 | path( 22 | "@form-builder/", 23 | include( 24 | "djangocms_form_builder.urls", 25 | namespace="form_builder", 26 | ), 27 | ) 28 | ] + urlconf_module.urlpatterns 29 | clear_url_caches() 30 | reverse("form_builder:ajaxview", args=(1,)) 31 | -------------------------------------------------------------------------------- /djangocms_form_builder/cms_plugins/__init__.py: -------------------------------------------------------------------------------- 1 | from . import legacy # noqa F401 2 | from .ajax_plugins import FormPlugin 3 | from .form_plugins import ( 4 | BooleanFieldPlugin, 5 | CharFieldPlugin, 6 | ChoicePlugin, 7 | DateFieldPlugin, 8 | DateTimeFieldPlugin, 9 | DecimalFieldPlugin, 10 | EmailFieldPlugin, 11 | IntegerFieldPlugin, 12 | SelectPlugin, 13 | SubmitPlugin, 14 | TextareaPlugin, 15 | TimeFieldPlugin, 16 | URLFieldPlugin, 17 | ) 18 | 19 | __all__ = [ 20 | "FormPlugin", 21 | "BooleanFieldPlugin", 22 | "CharFieldPlugin", 23 | "ChoicePlugin", 24 | "DateFieldPlugin", 25 | "DateTimeFieldPlugin", 26 | "DecimalFieldPlugin", 27 | "EmailFieldPlugin", 28 | "IntegerFieldPlugin", 29 | "SelectPlugin", 30 | "SubmitPlugin", 31 | "TextareaPlugin", 32 | "TimeFieldPlugin", 33 | "URLFieldPlugin", 34 | ] 35 | -------------------------------------------------------------------------------- /djangocms_form_builder/cms_plugins/form_plugins.py: -------------------------------------------------------------------------------- 1 | from cms.plugin_base import CMSPluginBase 2 | from cms.plugin_pool import plugin_pool 3 | from django.utils.encoding import force_str 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from .. import forms 7 | from .. import forms as forms_module 8 | from .. import models, settings 9 | from ..helpers import add_plugin, delete_plugin, insert_fields 10 | from .ajax_plugins import FormPlugin 11 | 12 | mixin_factory = settings.get_renderer(forms_module) 13 | 14 | 15 | class FormElementPlugin(CMSPluginBase): 16 | top_element = FormPlugin.__name__ 17 | module = _("Forms") 18 | render_template = f"djangocms_form_builder/{settings.framework}/widgets/base.html" 19 | change_form_template = "djangocms_frontend/admin/base.html" 20 | settings_name = _("Settings") 21 | # Form elements are rendered by the surrounding FormPlugin, hence any change has to be 22 | # propageted up to the FormPlugin: 23 | is_local = False 24 | 25 | fieldsets = ( 26 | ( 27 | None, 28 | { 29 | "fields": ( 30 | ("field_label", "field_name"), 31 | ("field_required", "field_placeholder"), 32 | ) 33 | }, 34 | ), 35 | ) 36 | 37 | @classmethod 38 | def get_parent_classes(cls, slot, page, instance=None): 39 | """Only valid as indirect child of the cls.top_element""" 40 | 41 | if instance is None: 42 | return [""] 43 | parent = instance 44 | while parent is not None: 45 | if parent.plugin_type == cls.top_element: 46 | return super().get_parent_classes(slot, page, instance) 47 | parent = parent.parent 48 | return [""] 49 | 50 | def get_fieldsets(self, request, obj=None): 51 | if hasattr(self, "settings_fields"): 52 | return insert_fields( 53 | super().get_fieldsets(request, obj), 54 | self.settings_fields, 55 | block=None, 56 | position=-1, 57 | blockname=self.settings_name, 58 | blockattrs=dict(classes=()), 59 | ) 60 | return super().get_fieldsets(request, obj) 61 | 62 | def render(self, context, instance, placeholder): 63 | # instance.add_classes("form-control") 64 | return super().render(context, instance, placeholder) 65 | 66 | def __str__(self): 67 | return force_str(super().__str__()) 68 | 69 | 70 | @plugin_pool.register_plugin 71 | class CharFieldPlugin(mixin_factory("CharField"), FormElementPlugin): 72 | name = _("Text") 73 | model = models.CharField 74 | form = forms.CharFieldForm 75 | settings_fields = (("min_length", "max_length"),) 76 | 77 | 78 | @plugin_pool.register_plugin 79 | class EmailFieldPlugin(mixin_factory("EmailField"), FormElementPlugin): 80 | name = _("Email") 81 | model = models.EmailField 82 | form = forms.EmailFieldForm 83 | 84 | 85 | @plugin_pool.register_plugin 86 | class URLFieldPlugin(mixin_factory("URLField"), FormElementPlugin): 87 | name = _("URL") 88 | model = models.UrlField 89 | form = forms.UrlFieldForm 90 | 91 | 92 | @plugin_pool.register_plugin 93 | class DecimalFieldPlugin(mixin_factory("DecimalField"), FormElementPlugin): 94 | name = _("Decimal") 95 | model = models.DecimalField 96 | form = forms.DecimalFieldForm 97 | settings_fields = ("decimal_places", ("min_value", "max_value")) 98 | 99 | 100 | @plugin_pool.register_plugin 101 | class IntegerFieldPlugin(mixin_factory("IntegerField"), FormElementPlugin): 102 | name = _("Integer") 103 | model = models.IntegerField 104 | form = forms.IntegerFieldForm 105 | settings_fields = (("min_value", "max_value"),) 106 | 107 | 108 | @plugin_pool.register_plugin 109 | class TextareaPlugin(FormElementPlugin): 110 | name = _("Textarea") 111 | model = models.TextareaField 112 | form = forms.TextareaFieldForm 113 | settings_fields = ( 114 | "field_rows", 115 | ("min_length", "max_length"), 116 | ) 117 | 118 | 119 | @plugin_pool.register_plugin 120 | class DateFieldPlugin(FormElementPlugin): 121 | name = _("Date") 122 | model = models.DateField 123 | form = forms.DateFieldForm 124 | 125 | 126 | @plugin_pool.register_plugin 127 | class DateTimeFieldPlugin(FormElementPlugin): 128 | name = _("Date and time") 129 | model = models.DateTimeField 130 | form = forms.DateTimeFieldForm 131 | 132 | 133 | @plugin_pool.register_plugin 134 | class TimeFieldPlugin(FormElementPlugin): 135 | name = _("Time") 136 | model = models.TimeField 137 | form = forms.TimeFieldForm 138 | 139 | 140 | @plugin_pool.register_plugin 141 | class SelectPlugin(mixin_factory("SelectField"), FormElementPlugin): 142 | name = _("Select") 143 | 144 | model = models.Select 145 | form = forms.SelectFieldForm 146 | allow_children = True 147 | child_classes = ["ChoicePlugin"] 148 | 149 | fieldsets = ( 150 | ( 151 | None, 152 | { 153 | "fields": ( 154 | ("field_label", "field_name"), 155 | "field_select", 156 | "field_required", 157 | ) 158 | }, 159 | ), 160 | ( 161 | _("Choices"), 162 | { 163 | "classes": ("collapse",), 164 | "description": _( 165 | "Use this field to quick edit choices. Choices can be added (+), deleted " 166 | "(×) and updated. On the left side enter the value to be stored in the database. " 167 | "On the right side enter the text to be shown to the user. The order of choices can be adjusted " 168 | "in the structure tree after saving the edits." 169 | ), 170 | "fields": ("field_choices",), 171 | }, 172 | ), 173 | ) 174 | 175 | def save_model(self, request, obj, form, change): 176 | """Reflects the quick edit changes in the plugin tree""" 177 | super().save_model(request, obj, form, change) 178 | child_plugins = obj.get_children() 179 | children = {} 180 | for child in child_plugins: 181 | child_ui = child.djangocms_form_builder_formfield 182 | children[child_ui.config["value"]] = child_ui 183 | position = len(child_plugins) 184 | data = form.cleaned_data 185 | for value, verbose in data["field_choices"]: 186 | child = children.pop(value, None) 187 | if child is not None: # Need to update? 188 | if verbose != child.config["verbose"]: 189 | child.config["verbose"] = verbose 190 | child.save() 191 | else: # Not in there, add it! 192 | add_plugin( 193 | obj.placeholder, 194 | models.Choice( 195 | parent=obj, 196 | placeholder=obj.placeholder, 197 | position=obj.position + position + 1, 198 | language=obj.language, 199 | plugin_type=ChoicePlugin.__name__, 200 | ui_item=models.Choice.__class__.__name__, 201 | config=dict(value=value, verbose=verbose), 202 | ), 203 | ) 204 | position += 1 205 | for _key, child in children.items(): # Delete remaining 206 | delete_plugin(child) 207 | 208 | 209 | @plugin_pool.register_plugin 210 | class ChoicePlugin(mixin_factory("ChoiceField"), FormElementPlugin): 211 | name = _("Choice") 212 | module = _("Forms") 213 | fieldsets = ((None, {"fields": (("value", "verbose"),)}),) 214 | model = models.Choice 215 | form = forms.ChoiceForm 216 | require_parent = True 217 | parent_classes = ["SelectPlugin"] 218 | 219 | 220 | @plugin_pool.register_plugin 221 | class BooleanFieldPlugin(mixin_factory("BooleanField"), FormElementPlugin): 222 | name = _("Boolean") 223 | model = models.BooleanField 224 | form = forms.BooleanFieldForm 225 | 226 | fieldsets = ( 227 | ( 228 | None, 229 | { 230 | "fields": ( 231 | ("field_label", "field_name"), 232 | "field_as_switch", 233 | "field_required", 234 | ) 235 | }, 236 | ), 237 | ) 238 | 239 | 240 | @plugin_pool.register_plugin 241 | class SubmitPlugin(mixin_factory("SubmitButton"), FormElementPlugin): 242 | name = _("Submit button") 243 | module = _("Forms") 244 | 245 | fieldsets = ( 246 | ( 247 | None, 248 | { 249 | "fields": ( 250 | ("field_name", "field_label"), 251 | ("submit_cta",), 252 | ) 253 | }, 254 | ), 255 | ) 256 | model = models.SubmitButton 257 | form = forms.SubmitButtonForm 258 | 259 | render_template = f"djangocms_form_builder/{settings.framework}/widgets/submit.html" 260 | -------------------------------------------------------------------------------- /djangocms_form_builder/cms_plugins/legacy.py: -------------------------------------------------------------------------------- 1 | from cms.plugin_pool import plugin_pool 2 | from django.conf import settings 3 | 4 | if "djangocms_frontend.contrib.frontend_forms" in settings.INSTALLED_APPS: 5 | from djangocms_frontend.contrib.frontend_forms.cms_plugins import ( 6 | BooleanFieldPlugin, 7 | CharFieldPlugin, 8 | ChoicePlugin, 9 | DateFieldPlugin, 10 | DateTimeFieldPlugin, 11 | DecimalFieldPlugin, 12 | EmailFieldPlugin, 13 | FormPlugin, 14 | IntegerFieldPlugin, 15 | SelectPlugin, 16 | TextareaPlugin, 17 | TimeFieldPlugin, 18 | URLFieldPlugin, 19 | ) 20 | 21 | for plugin in ( 22 | FormPlugin, 23 | BooleanFieldPlugin, 24 | CharFieldPlugin, 25 | ChoicePlugin, 26 | DateFieldPlugin, 27 | DateTimeFieldPlugin, 28 | DecimalFieldPlugin, 29 | EmailFieldPlugin, 30 | IntegerFieldPlugin, 31 | SelectPlugin, 32 | TextareaPlugin, 33 | TimeFieldPlugin, 34 | URLFieldPlugin, 35 | ): 36 | plugin_pool.unregister_plugin(plugin) 37 | -------------------------------------------------------------------------------- /djangocms_form_builder/constants.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from . import settings 6 | 7 | framework = importlib.import_module( 8 | f"djangocms_form_builder.frontends.{settings.framework}", # TODO 9 | ) 10 | 11 | default_attr = framework.default_attr # NOQA 12 | attr_dict = framework.attr_dict # NOQA 13 | DEFAULT_FIELD_SEP = framework.DEFAULT_FIELD_SEP # NOQA 14 | 15 | # default_attr = settings.default_attr # NOQA 16 | # attr_dict = settings.attr_dict # NOQA 17 | # DEFAULT_FIELD_SEP = settings.DEFAULT_FIELD_SEP # NOQA 18 | 19 | CHOICE_FIELDS = ( 20 | ( 21 | _("Single choice"), 22 | ( 23 | ("select", _("Drop down")), 24 | ("radio", _("Radio buttons")), 25 | ), 26 | ), 27 | ( 28 | _("Multiple choice"), 29 | ( 30 | ("checkbox", _("Checkboxes")), 31 | ("multiselect", _("List")), 32 | ), 33 | ), 34 | ) 35 | -------------------------------------------------------------------------------- /djangocms_form_builder/entry_model.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | 3 | from django import forms 4 | from django.conf import settings 5 | from django.core.serializers.json import DjangoJSONEncoder 6 | from django.db import models 7 | from django.utils.translation import gettext_lazy as _ 8 | from entangled.forms import EntangledModelForm 9 | 10 | 11 | class CSValues(forms.CharField): 12 | class CSVWidget(forms.TextInput): 13 | def format_value(self, value): 14 | return ", ".join(value) 15 | 16 | def __init__(self, *args, **kwargs): 17 | kwargs.setdefault("widget", CSValues.CSVWidget()) 18 | super().__init__(*args, **kwargs) 19 | 20 | def to_python(self, value): 21 | value = value.split(",") 22 | value = list(map(lambda x: x.strip(), value)) 23 | return value 24 | 25 | 26 | class FormEntry(models.Model): 27 | class Meta: 28 | verbose_name = _("Form entry") 29 | verbose_name_plural = _("Form entries") 30 | 31 | form_name = models.SlugField( 32 | verbose_name=_("Form"), 33 | blank=False, 34 | ) 35 | form_user = models.ForeignKey( 36 | settings.AUTH_USER_MODEL, 37 | verbose_name=_("User"), 38 | null=True, 39 | blank=True, 40 | on_delete=models.CASCADE, 41 | ) 42 | entry_data = models.JSONField( 43 | default=dict, 44 | blank=True, 45 | encoder=DjangoJSONEncoder, 46 | ) 47 | html_headers = models.JSONField( 48 | default=dict, 49 | blank=True, 50 | ) 51 | entry_created_at = models.DateTimeField(auto_now_add=True) 52 | entry_updated_at = models.DateTimeField(auto_now=True) 53 | 54 | def get_admin_form(self): 55 | entangled_fields = [] 56 | fields = {} 57 | for key, value in self.entry_data.items(): 58 | if isinstance(value, str): 59 | entangled_fields.append(key) 60 | fields[key] = forms.CharField( 61 | label=key, 62 | widget=forms.TextInput if len(value) < 80 else forms.Textarea, 63 | required=False, 64 | ) 65 | elif isinstance(value, (list, tuple)): 66 | entangled_fields.append(key) 67 | fields[key] = CSValues( 68 | label=key, 69 | required=False, 70 | ) 71 | elif isinstance(value, bool): 72 | entangled_fields.append(key) 73 | fields[key] = forms.BooleanField( 74 | label=key, 75 | required=False, 76 | ) 77 | elif isinstance(value, decimal.Decimal): 78 | entangled_fields.append(key) 79 | fields[key] = forms.DecimalField( 80 | label=key, 81 | required=False, 82 | ) 83 | 84 | fields["Meta"] = type( 85 | "Meta", 86 | (), 87 | { 88 | "model": FormEntry, 89 | "exclude": None, 90 | "entangled_fields": {"entry_data": entangled_fields}, 91 | "untangled_fields": [ 92 | "form_name", 93 | "form_user", 94 | ], 95 | }, 96 | ) 97 | return type("DynamicFormEntryForm", (EntangledModelForm,), fields) 98 | 99 | def get_admin_fieldsets(self): 100 | return ( 101 | ( 102 | None, 103 | { 104 | "fields": (("form_name", "form_user"),), 105 | }, 106 | ), 107 | ( 108 | _("User-entered data"), 109 | { 110 | "fields": tuple( 111 | key 112 | for key, value in self.entry_data.items() 113 | if isinstance(value, (str, tuple, list, bool)) 114 | ) 115 | }, 116 | ), 117 | ) 118 | 119 | def __str__(self): 120 | return f"{self.form_name} ({self.pk})" 121 | -------------------------------------------------------------------------------- /djangocms_form_builder/fields.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.core.exceptions import ValidationError 3 | from django.db import models 4 | from django.utils.safestring import mark_safe 5 | from django.utils.translation import gettext_lazy as _ 6 | from djangocms_attributes_field import fields 7 | 8 | from . import settings 9 | from .helpers import first_choice 10 | 11 | 12 | class TemplateChoiceMixin: 13 | """Mixin that hides the template field if only one template is available and is selected""" 14 | 15 | def __init__(self, *args, **kwargs): 16 | super().__init__(*args, **kwargs) 17 | if "template" in self.fields: 18 | template_field = self.fields["template"] 19 | choices = template_field.choices 20 | instance = kwargs.get("instance", None) 21 | if len(choices) == 1 and ( 22 | instance is None or instance.config.get("template", "") == choices[0][0] 23 | ): 24 | template_field.widget = forms.HiddenInput() 25 | 26 | 27 | class ButtonGroup(forms.RadioSelect): 28 | template_name = "djangocms_form_builder/admin/widgets/button_group.html" 29 | option_template_name = ( 30 | "djangocms_form_builder/admin/widgets/button_group_option.html" 31 | ) 32 | 33 | class Media: 34 | css = {"all": ("djangocms_form_builder/css/button_group.css",)} 35 | 36 | 37 | class ColoredButtonGroup(ButtonGroup): 38 | option_template_name = ( 39 | "djangocms_form_builder/admin/widgets/button_group_color_option.html" 40 | ) 41 | 42 | class Media: 43 | css = settings.ADMIN_CSS 44 | 45 | def __init__(self, *args, **kwargs): 46 | kwargs.update({"attrs": {**kwargs.get("attrs", {}), **dict(property="color")}}) 47 | super().__init__(*args, **kwargs) 48 | 49 | 50 | class IconGroup(ButtonGroup): 51 | option_template_name = "djangocms_form_builder/admin/widgets/icon_group_option.html" 52 | 53 | def __init__(self, *args, **kwargs): 54 | kwargs.update({"attrs": {**dict(property="icon"), **kwargs.get("attrs", {})}}) 55 | super().__init__(*args, **kwargs) 56 | 57 | 58 | class IconMultiselect(forms.CheckboxSelectMultiple): 59 | template_name = "djangocms_form_builder/admin/widgets/button_group.html" 60 | option_template_name = "djangocms_form_builder/admin/widgets/icon_group_option.html" 61 | 62 | class Media: 63 | css = {"all": ("djangocms_form_builder/css/button_group.css",)} 64 | 65 | def __init__(self, *args, **kwargs): 66 | kwargs.update({"attrs": {**kwargs.get("attrs", {}), **dict(property="icon")}}) 67 | super().__init__(*args, **kwargs) 68 | 69 | 70 | class OptionalDeviceChoiceField(forms.MultipleChoiceField): 71 | def __init__(self, **kwargs): 72 | kwargs.setdefault("choices", settings.DEVICE_CHOICES) 73 | kwargs.setdefault("initial", None) 74 | kwargs.setdefault("widget", IconMultiselect()) 75 | super().__init__(**kwargs) 76 | 77 | def prepare_value(self, value): 78 | if value is None: 79 | value = [size for size, _ in settings.DEVICE_CHOICES] 80 | return super().prepare_value(value) 81 | 82 | def clean(self, value): 83 | value = super().clean(value) 84 | if len(value) == len(settings.DEVICE_CHOICES): 85 | return None 86 | return value 87 | 88 | 89 | class DeviceChoiceField(OptionalDeviceChoiceField): 90 | def clean(self, value): 91 | value = super().clean(value) 92 | if isinstance(value, list) and len(value) == 0: 93 | raise ValidationError( 94 | _("Please select at least one device size"), code="invalid" 95 | ) 96 | return value 97 | 98 | 99 | class AttributesField(fields.AttributesField): 100 | def __init__(self, *args, **kwargs): 101 | if "verbose_name" not in kwargs: 102 | kwargs["verbose_name"] = _("Attributes") 103 | if "blank" not in kwargs: 104 | kwargs["blank"] = True 105 | super().__init__(*args, **kwargs) 106 | 107 | 108 | class AttributesFormField(fields.AttributesFormField): 109 | def __init__(self, *args, **kwargs): 110 | kwargs.setdefault("label", _("Attributes")) 111 | kwargs.setdefault("required", False) 112 | kwargs.setdefault("widget", fields.AttributesWidget) 113 | self.excluded_keys = kwargs.pop("excluded_keys", []) 114 | super().__init__(*args, **kwargs) 115 | 116 | 117 | try: 118 | fields.AttributesWidget( 119 | sorted=True 120 | ) # does djangocms-attributes-field support sorted param? 121 | CHOICESWIDGETPARAMS = dict(sorted=False) # use unsorted variant 122 | except TypeError: 123 | CHOICESWIDGETPARAMS = dict() # Fallback for djangocms-attributes-field < 2.1 124 | 125 | 126 | class ChoicesFormField(fields.AttributesFormField): 127 | """Simple choices field based on attributes field. Needs to be extended to 128 | allow to sort choices""" 129 | 130 | def __init__(self, *args, **kwargs): 131 | kwargs.setdefault("label", _("Choices")) 132 | kwargs.setdefault("required", True) 133 | kwargs.setdefault("widget", fields.AttributesWidget(**CHOICESWIDGETPARAMS)) 134 | self.excluded_keys = kwargs.pop("excluded_keys", []) 135 | super().__init__(*args, **kwargs) 136 | 137 | def clean(self, value): 138 | if not value: 139 | raise ValidationError( 140 | mark_safe( 141 | _( 142 | "Please enter at least one choice. Use the + symbol to add a choice." 143 | ) 144 | ), 145 | code="empty", 146 | ) 147 | return [(key, value) for key, value in value.items()] 148 | 149 | def prepare_value(self, value): 150 | if not value: 151 | return {} 152 | if isinstance(value, dict): # Already dict? OK! 153 | return super().prepare_value(value) 154 | # Turn items into dict 155 | return super().prepare_value({key: value for key, value in value}) 156 | 157 | 158 | class TagTypeField(models.CharField): 159 | def __init__(self, *args, **kwargs): 160 | if "verbose_name" not in kwargs: 161 | kwargs["verbose_name"] = _("Tag type") 162 | if "choices" not in kwargs: 163 | kwargs["choices"] = settings.TAG_CHOICES 164 | if "default" not in kwargs: 165 | kwargs["default"] = first_choice(settings.TAG_CHOICES) 166 | if "max_length" not in kwargs: 167 | kwargs["max_length"] = 255 168 | if "help_text" not in kwargs: 169 | kwargs["help_text"] = _("Select the HTML tag to be used.") 170 | super().__init__(*args, **kwargs) 171 | 172 | 173 | class TagTypeFormField(forms.ChoiceField): 174 | def __init__(self, *args, **kwargs): 175 | kwargs.setdefault("label", _("Tag type")) 176 | kwargs.setdefault("choices", settings.TAG_CHOICES) 177 | kwargs.setdefault("initial", first_choice(settings.TAG_CHOICES)) 178 | kwargs.setdefault("required", False) 179 | kwargs.setdefault("widget", ButtonGroup(attrs=dict(property="text"))) 180 | super().__init__(*args, **kwargs) 181 | -------------------------------------------------------------------------------- /djangocms_form_builder/frontends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/djangocms_form_builder/frontends/__init__.py -------------------------------------------------------------------------------- /djangocms_form_builder/frontends/bootstrap5.py: -------------------------------------------------------------------------------- 1 | default_attr = dict( 2 | input="form-control", 3 | label="form-label", 4 | div="", 5 | group="", 6 | ) 7 | 8 | attr_dict = dict( 9 | Select=dict(input="form-select"), 10 | SelectMultiple=dict(input="form-select"), 11 | NullBooleanSelect=dict(input="form-select"), 12 | RadioSelect=dict( 13 | input="form-check-input", label="form-check-label", group="form-check" 14 | ), 15 | CheckboxInput=dict( 16 | input="form-check-input", label="form-check-label", div="form-check" 17 | ), 18 | SwitchInput=dict( 19 | input="form-check-input", label="form-check-label", div="form-check form-switch" 20 | ), 21 | CheckboxSelectMultiple=dict( 22 | input="form-check-input", label="form-check-label", group="form-check" 23 | ), 24 | ButtonRadio=dict(input="btn-check", label="btn btn-outline-primary"), 25 | ButtonCheckbox=dict(input="btn-check", label="btn btn-outline-primary"), 26 | ) 27 | 28 | DEFAULT_FIELD_SEP = "mb-3" 29 | 30 | 31 | class FormRenderMixin: 32 | render_template = "djangocms_form_builder/bootstrap5/form.html" 33 | -------------------------------------------------------------------------------- /djangocms_form_builder/frontends/foundation6.py: -------------------------------------------------------------------------------- 1 | default_attr = dict( 2 | input="form-control", 3 | label="form-label", 4 | div="", 5 | ) 6 | 7 | attr_dict = dict( 8 | Select=dict(input="form-select"), 9 | SelectMultiple=dict(input="form-select"), 10 | NullBooleanSelect=dict(input="form-select"), 11 | RadioSelect=dict( 12 | input="form-check-input", label="form-check-label", div="form-check" 13 | ), 14 | CheckboxInput=dict( 15 | input="form-check-input", label="form-check-label", div="form-check" 16 | ), 17 | ButtonRadio=dict(input="btn-check", label="btn btn-outline-primary"), 18 | ButtonCheckbox=dict(input="btn-check", label="btn btn-outline-primary"), 19 | ) 20 | 21 | DEFAULT_FIELD_SEP = "mb-3" 22 | -------------------------------------------------------------------------------- /djangocms_form_builder/helpers.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import decimal 3 | 4 | from django.apps import apps 5 | from django.db.models import ObjectDoesNotExist 6 | from django.template.exceptions import TemplateDoesNotExist 7 | from django.template.loader import select_template 8 | from django.utils.functional import lazy 9 | from django.utils.safestring import mark_safe 10 | 11 | from . import settings 12 | 13 | global_options = settings.FORM_OPTIONS 14 | 15 | 16 | def get_option(form, option, default=None): 17 | form_options = getattr(getattr(form, "Meta", None), "options", {}) 18 | return form_options.get(option, global_options.get(option, default)) 19 | 20 | 21 | def get_related_object(scope, field_name): 22 | """ 23 | Returns the related field, referenced by the content of a ModelChoiceField. 24 | """ 25 | try: 26 | Model = apps.get_model(scope[field_name]["model"]) 27 | relobj = Model.objects.get(pk=scope[field_name]["pk"]) 28 | except (ObjectDoesNotExist, LookupError): 29 | relobj = None 30 | return relobj 31 | 32 | 33 | def insert_fields( 34 | fieldsets, new_fields, block=None, position=-1, blockname=None, blockattrs=None 35 | ): 36 | """ 37 | creates a copy of fieldsets inserting the new fields either in the indexed block at the position, 38 | or - if no block is given - at the end 39 | """ 40 | if blockattrs is None: 41 | blockattrs = dict() 42 | if block is None: 43 | fs = ( 44 | list(fieldsets[:position] if position != -1 else fieldsets) 45 | + [ 46 | ( 47 | blockname, 48 | { 49 | "classes": ("collapse",) if len(fieldsets) > 0 else (), 50 | "fields": list(new_fields), 51 | **blockattrs, 52 | }, 53 | ) 54 | ] 55 | + list(fieldsets[position:] if position != -1 else []) 56 | ) 57 | return fs 58 | modify = copy.deepcopy(fieldsets[block]) 59 | fields = modify[1]["fields"] 60 | if position >= 0: 61 | modify[1]["fields"] = ( 62 | list(fields[:position]) + list(new_fields) + list(fields[position:]) 63 | ) 64 | else: 65 | modify[1]["fields"] = ( 66 | list(fields[: position + 1] if position != -1 else fields) 67 | + list(new_fields) 68 | + list(fields[position + 1 :] if position != -1 else []) 69 | ) 70 | fs = ( 71 | list(fieldsets[:block] if block != -1 else fieldsets) 72 | + [modify] 73 | + list(fieldsets[block + 1 :] if block != -1 else []) 74 | ) 75 | return fs 76 | 77 | 78 | def first_choice(choices): 79 | for value, verbose in choices: 80 | if not isinstance(verbose, (tuple, list)): 81 | return value 82 | else: 83 | first = first_choice(verbose) 84 | if first is not None: 85 | return first 86 | return None 87 | 88 | 89 | def get_template_path(prefix, template, name): 90 | return ( 91 | f"djangocms_form_builder/{settings.framework}/{prefix}/{template}/{name}.html" 92 | ) 93 | 94 | 95 | def get_plugin_template(instance, prefix, name, templates): 96 | template = getattr(instance, "template", first_choice(templates)) 97 | template_path = get_template_path(prefix, template, name) 98 | 99 | try: 100 | select_template([template_path]) 101 | except TemplateDoesNotExist: 102 | # TODO render a warning inside the template 103 | template_path = get_template_path(prefix, "default", name) 104 | 105 | return template_path 106 | 107 | 108 | # use mark_safe_lazy to delay the translation when using mark_safe 109 | # otherwise they will not be added to /locale/ 110 | # https://docs.djangoproject.com/en/1.11/topics/i18n/translation/#other-uses-of-lazy-in-delayed-translations 111 | mark_safe_lazy = lazy(mark_safe, str) 112 | 113 | 114 | def add_plugin(placeholder, plugin): 115 | """CMS version save function to add a plugin to a placeholder""" 116 | if hasattr(placeholder, "add_plugin"): # CMS v4? 117 | placeholder.add_plugin(plugin) 118 | else: # CMS < v4 119 | if plugin.parent: 120 | plugin.position -= plugin.parent.position + 1 121 | else: 122 | plugin.position = 0 123 | plugin.save() 124 | 125 | 126 | def delete_plugin(plugin): 127 | """CMS version save function to delete a plugin (and its descendants) from a placeholder""" 128 | return plugin.placeholder.delete_plugin(plugin) 129 | 130 | 131 | def coerce_decimal(value): 132 | try: 133 | return decimal.Decimal(value) 134 | except TypeError: 135 | return None 136 | -------------------------------------------------------------------------------- /djangocms_form_builder/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/djangocms_form_builder/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /djangocms_form_builder/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/djangocms_form_builder/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /djangocms_form_builder/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-01-24 14:41+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=n != 1;\n" 20 | 21 | #: __init__.py:33 22 | msgid "No forms registered" 23 | msgstr "" 24 | 25 | #: actions.py:21 26 | msgid "No actions registered" 27 | msgstr "" 28 | 29 | #: actions.py:54 30 | msgid "Save form submission" 31 | msgstr "" 32 | 33 | #: actions.py:95 34 | msgid "Send email to administrators" 35 | msgstr "" 36 | 37 | #: actions.py:99 38 | #, python-format 39 | msgid "%(form_name)s form submission" 40 | msgstr "" 41 | 42 | #: apps.py:12 43 | msgid "django CMS form builder" 44 | msgstr "" 45 | 46 | #: cms_plugins/ajax_plugins.py:239 entry_model.py:32 forms.py:91 models.py:23 47 | #: models.py:26 48 | msgid "Form" 49 | msgstr "" 50 | 51 | #: cms_plugins/ajax_plugins.py:264 52 | msgid "Actions" 53 | msgstr "" 54 | 55 | #: cms_plugins/ajax_plugins.py:289 56 | msgid "reCaptcha" 57 | msgstr "" 58 | 59 | #: cms_plugins/ajax_plugins.py:295 60 | #, python-brace-format 61 | msgid "" 62 | "
Please get a public and secret " 63 | "key from Google and ensure that " 64 | "they are available through django settings RECAPTCHA_PUBLIC_KEY " 65 | "and RECAPTCHA_PRIVATE_KEY. Without these keys captcha " 66 | "protection will not work.
" 67 | msgstr "" 68 | 69 | #: cms_plugins/form_plugins.py:17 cms_plugins/form_plugins.py:209 70 | #: cms_plugins/form_plugins.py:240 71 | msgid "Forms" 72 | msgstr "" 73 | 74 | #: cms_plugins/form_plugins.py:20 75 | msgid "Settings" 76 | msgstr "" 77 | 78 | #: cms_plugins/form_plugins.py:69 79 | msgid "Text" 80 | msgstr "" 81 | 82 | #: cms_plugins/form_plugins.py:77 83 | msgid "Email" 84 | msgstr "" 85 | 86 | #: cms_plugins/form_plugins.py:84 87 | msgid "URL" 88 | msgstr "" 89 | 90 | #: cms_plugins/form_plugins.py:91 91 | msgid "Decimal" 92 | msgstr "" 93 | 94 | #: cms_plugins/form_plugins.py:99 95 | msgid "Integer" 96 | msgstr "" 97 | 98 | #: cms_plugins/form_plugins.py:107 99 | msgid "Textarea" 100 | msgstr "" 101 | 102 | #: cms_plugins/form_plugins.py:118 103 | msgid "Date" 104 | msgstr "" 105 | 106 | #: cms_plugins/form_plugins.py:125 107 | msgid "Date and time" 108 | msgstr "" 109 | 110 | #: cms_plugins/form_plugins.py:132 111 | msgid "Time" 112 | msgstr "" 113 | 114 | #: cms_plugins/form_plugins.py:139 models.py:384 115 | msgid "Select" 116 | msgstr "" 117 | 118 | #: cms_plugins/form_plugins.py:158 fields.py:131 119 | msgid "Choices" 120 | msgstr "" 121 | 122 | #: cms_plugins/form_plugins.py:162 123 | msgid "" 124 | "Use this field to quick edit choices. Choices can be added (+), " 125 | "deleted (×) and updated. On the left side enter the value " 126 | "to be stored in the database. On the right side enter the text to be shown " 127 | "to the user. The order of choices can be adjusted in the structure tree " 128 | "after saving the edits." 129 | msgstr "" 130 | 131 | #: cms_plugins/form_plugins.py:208 models.py:431 132 | msgid "Choice" 133 | msgstr "" 134 | 135 | #: cms_plugins/form_plugins.py:219 136 | msgid "Boolean" 137 | msgstr "" 138 | 139 | #: cms_plugins/form_plugins.py:239 models.py:463 140 | msgid "Submit button" 141 | msgstr "" 142 | 143 | #: constants.py:21 144 | msgid "Single choice" 145 | msgstr "" 146 | 147 | #: constants.py:23 148 | msgid "Drop down" 149 | msgstr "" 150 | 151 | #: constants.py:24 152 | msgid "Radio buttons" 153 | msgstr "" 154 | 155 | #: constants.py:28 156 | msgid "Multiple choice" 157 | msgstr "" 158 | 159 | #: constants.py:30 160 | msgid "Checkboxes" 161 | msgstr "" 162 | 163 | #: constants.py:31 164 | msgid "List" 165 | msgstr "" 166 | 167 | #: entry_model.py:28 168 | msgid "Form entry" 169 | msgstr "" 170 | 171 | #: entry_model.py:29 172 | msgid "Form entries" 173 | msgstr "" 174 | 175 | #: entry_model.py:37 176 | msgid "User" 177 | msgstr "" 178 | 179 | #: entry_model.py:108 180 | msgid "User-entered data" 181 | msgstr "" 182 | 183 | #: fields.py:94 184 | msgid "Please select at least one device size" 185 | msgstr "" 186 | 187 | #: fields.py:102 fields.py:110 188 | msgid "Attributes" 189 | msgstr "" 190 | 191 | #: fields.py:142 192 | msgid "" 193 | "Please enter at least one choice. Use the + symbol to add a " 194 | "choice." 195 | msgstr "" 196 | 197 | #: fields.py:161 fields.py:175 198 | msgid "Tag type" 199 | msgstr "" 200 | 201 | #: fields.py:169 202 | msgid "Select the HTML tag to be used." 203 | msgstr "" 204 | 205 | #: forms.py:47 206 | msgid "Please login before submitting this form." 207 | msgstr "" 208 | 209 | #: forms.py:59 210 | msgid "Action not available any more" 211 | msgstr "" 212 | 213 | #: forms.py:61 214 | msgid "No action registered" 215 | msgstr "" 216 | 217 | #: forms.py:96 models.py:32 218 | msgid "Form name" 219 | msgstr "" 220 | 221 | #: forms.py:104 models.py:41 222 | msgid "Login required to submit form" 223 | msgstr "" 224 | 225 | #: forms.py:108 models.py:45 226 | msgid "" 227 | "To avoid issues with user experience use this type of form only on pages, " 228 | "which require login." 229 | msgstr "" 230 | 231 | #: forms.py:114 models.py:51 232 | msgid "User can reopen form" 233 | msgstr "" 234 | 235 | #: forms.py:117 models.py:53 236 | msgid "Requires \"Login required\" to be checked to work." 237 | msgstr "" 238 | 239 | #: forms.py:121 models.py:61 240 | msgid "Margin between fields" 241 | msgstr "" 242 | 243 | #: forms.py:127 models.py:66 244 | msgid "Actions to be taken after form submission" 245 | msgstr "" 246 | 247 | #: forms.py:135 models.py:75 248 | msgid "reCaptcha widget" 249 | msgstr "" 250 | 251 | #: forms.py:141 models.py:82 252 | #, python-brace-format 253 | msgid "" 254 | "Read more in the documentation." 255 | msgstr "" 256 | 257 | #: forms.py:146 models.py:87 258 | msgid "Minimum score requirement" 259 | msgstr "" 260 | 261 | #: forms.py:152 models.py:93 262 | msgid "Only for reCaptcha v3: Minimum score required to accept challenge." 263 | msgstr "" 264 | 265 | #: forms.py:156 models.py:97 266 | msgid "Recaptcha configuration parameters" 267 | msgstr "" 268 | 269 | #: forms.py:159 models.py:100 270 | #, python-brace-format 271 | msgid "" 272 | "The reCAPTCHA widget supports several data attributes that customize the behaviour of the " 274 | "widget, such as data-theme, data-size. The " 275 | "reCAPTCHA api supports several parameters. Add these api parameters as attributes, e." 277 | "g. hl to set the language." 278 | msgstr "" 279 | 280 | #: forms.py:188 281 | msgid "Please provide a form name to be able to evaluate form submissions." 282 | msgstr "" 283 | 284 | #: forms.py:203 285 | msgid "Please select \"Save form submission\" to allow users to reopen forms." 286 | msgstr "" 287 | 288 | #: forms.py:206 289 | msgid "" 290 | "Please select the action \"Save form submission\" to allow users to reopen " 291 | "forms." 292 | msgstr "" 293 | 294 | #: forms.py:214 295 | msgid "" 296 | "No form action to save form contents available. Users will not be able to " 297 | "reopen a form." 298 | msgstr "" 299 | 300 | #: forms.py:223 301 | msgid "" 302 | "At least one action needs to be selected for the form to have an effect." 303 | msgstr "" 304 | 305 | #: forms.py:232 306 | #, python-format 307 | msgid "Users can only reopen forms if they are logged in. %(remedy)s" 308 | msgstr "" 309 | 310 | #: forms.py:236 311 | msgid "Either enable this." 312 | msgstr "" 313 | 314 | #: forms.py:237 315 | msgid "Or disable this." 316 | msgstr "" 317 | 318 | #: forms.py:261 319 | msgid "This name is reserved. Please chose a different one." 320 | msgstr "" 321 | 322 | #: forms.py:282 323 | msgid "Field name" 324 | msgstr "" 325 | 326 | #: forms.py:284 327 | msgid "" 328 | "Internal field name consisting of letters, numbers, underscores or hyphens" 329 | msgstr "" 330 | 331 | #: forms.py:290 332 | msgid "Label" 333 | msgstr "" 334 | 335 | #: forms.py:292 336 | msgid "Field label shown to the user describing the entity to be entered" 337 | msgstr "" 338 | 339 | #: forms.py:297 340 | msgid "Placeholder" 341 | msgstr "" 342 | 343 | #: forms.py:298 344 | msgid "Example input shown muted in an empty field" 345 | msgstr "" 346 | 347 | #: forms.py:302 348 | msgid "Required" 349 | msgstr "" 350 | 351 | #: forms.py:306 352 | msgid "If selected form will not accept submissions with with empty data" 353 | msgstr "" 354 | 355 | #: forms.py:322 forms.py:327 forms.py:417 356 | msgid "Minimum text length" 357 | msgstr "" 358 | 359 | #: forms.py:359 forms.py:387 360 | msgid "Minimum value" 361 | msgstr "" 362 | 363 | #: forms.py:364 forms.py:392 364 | msgid "Maximum value" 365 | msgstr "" 366 | 367 | #: forms.py:367 368 | msgid "Decimal places" 369 | msgstr "" 370 | 371 | #: forms.py:410 372 | msgid "Rows" 373 | msgstr "" 374 | 375 | #: forms.py:414 376 | msgid "Defines the vertical size of the text area in number of rows." 377 | msgstr "" 378 | 379 | #: forms.py:422 380 | msgid "Maximum text length" 381 | msgstr "" 382 | 383 | #: forms.py:435 forms.py:447 forms.py:457 384 | msgid "Not visible on most browsers." 385 | msgstr "" 386 | 387 | #: forms.py:476 388 | msgid "Selection type" 389 | msgstr "" 390 | 391 | #: forms.py:496 392 | msgid "" 393 | "For a required multiple choice fild select the list selection type." 394 | msgstr "" 395 | 396 | #: forms.py:500 397 | msgid "Checkbox multiple choice field must not be required." 398 | msgstr "" 399 | 400 | #: forms.py:518 templates/djangocms_form_builder/actions/mail.html:8 401 | msgid "Value" 402 | msgstr "" 403 | 404 | #: forms.py:520 405 | msgid "Stored in database if the choice is selected." 406 | msgstr "" 407 | 408 | #: forms.py:523 409 | msgid "Display text" 410 | msgstr "" 411 | 412 | #: forms.py:525 413 | msgid "Representation of choice displayed to the user." 414 | msgstr "" 415 | 416 | #: forms.py:541 417 | msgid "Layout" 418 | msgstr "" 419 | 420 | #: forms.py:545 421 | msgid "Checkbox" 422 | msgstr "" 423 | 424 | #: forms.py:545 425 | msgid "Switch" 426 | msgstr "" 427 | 428 | #: forms.py:553 429 | msgid "" 430 | "If checked, the form can only be submitted if the checkbox is checked or the " 431 | "switch set to on." 432 | msgstr "" 433 | 434 | #: forms.py:570 435 | msgid "Button label" 436 | msgstr "" 437 | 438 | #: forms.py:571 templates/djangocms_form_builder/bootstrap5/form.html:10 439 | #: templates/djangocms_form_builder/bootstrap5/widgets/submit.html:1 440 | msgid "Submit" 441 | msgstr "" 442 | 443 | #: models.py:57 444 | msgid "Floating labels" 445 | msgstr "" 446 | 447 | #: models.py:127 448 | msgid "Form field item" 449 | msgstr "" 450 | 451 | #: models.py:199 452 | msgid "Character field" 453 | msgstr "" 454 | 455 | #: models.py:214 456 | msgid "Email field" 457 | msgstr "" 458 | 459 | #: models.py:229 460 | msgid "URL field" 461 | msgstr "" 462 | 463 | #: models.py:244 464 | msgid "Decimal field" 465 | msgstr "" 466 | 467 | #: models.py:288 468 | msgid "Integer field" 469 | msgstr "" 470 | 471 | #: models.py:303 472 | msgid "Text field" 473 | msgstr "" 474 | 475 | #: models.py:322 models.py:340 models.py:366 476 | msgid "Date field" 477 | msgstr "" 478 | 479 | #: models.py:387 480 | msgid "No selection" 481 | msgstr "" 482 | 483 | #: models.py:444 484 | msgid "Boolean field" 485 | msgstr "" 486 | 487 | #: recaptcha.py:38 488 | msgid "v2 checkbox" 489 | msgstr "" 490 | 491 | #: recaptcha.py:39 492 | msgid "v2 invisible" 493 | msgstr "" 494 | 495 | #: templates/djangocms_form_builder/actions/mail.html:2 496 | msgid "Form submission" 497 | msgstr "" 498 | 499 | #: templates/djangocms_form_builder/actions/mail.html:3 500 | msgid "by" 501 | msgstr "" 502 | 503 | #: templates/djangocms_form_builder/actions/mail.html:3 504 | msgid "by anonymous" 505 | msgstr "" 506 | 507 | #: templates/djangocms_form_builder/actions/mail.html:7 508 | msgid "Field" 509 | msgstr "" 510 | 511 | #: templates/djangocms_form_builder/actions/mail.html:19 512 | msgid "time" 513 | msgstr "" 514 | 515 | #: templates/djangocms_form_builder/actions/mail.html:23 516 | msgid "user agent" 517 | msgstr "" 518 | 519 | #: templates/djangocms_form_builder/actions/mail.html:27 520 | msgid "referer" 521 | msgstr "" 522 | 523 | #: templates/djangocms_form_builder/ajax_form.html:118 524 | msgid "" 525 | "Network connection or server error. Please try again later. We apologize for " 526 | "the inconvenience." 527 | msgstr "" 528 | 529 | #: views.py:25 530 | msgid "Only unique slugs accepted for form views" 531 | msgstr "" 532 | -------------------------------------------------------------------------------- /djangocms_form_builder/locale/nl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/djangocms_form_builder/locale/nl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /djangocms_form_builder/locale/sq/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/djangocms_form_builder/locale/sq/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /djangocms_form_builder/locale/sq/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/djangocms_form_builder/locale/sq/LC_MESSAGES/django.po -------------------------------------------------------------------------------- /djangocms_form_builder/migrations/0002_alter_form_cmsplugin_ptr_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2022-10-19 10:08 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("cms", "0022_auto_20180620_1551"), 10 | ("djangocms_form_builder", "0001_initial"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="form", 16 | name="cmsplugin_ptr", 17 | field=models.OneToOneField( 18 | auto_created=True, 19 | on_delete=django.db.models.deletion.CASCADE, 20 | parent_link=True, 21 | primary_key=True, 22 | related_name="%(app_label)s_%(class)s", 23 | serialize=False, 24 | to="cms.cmsplugin", 25 | ), 26 | ), 27 | migrations.AlterField( 28 | model_name="formfield", 29 | name="cmsplugin_ptr", 30 | field=models.OneToOneField( 31 | auto_created=True, 32 | on_delete=django.db.models.deletion.CASCADE, 33 | parent_link=True, 34 | primary_key=True, 35 | related_name="%(app_label)s_%(class)s", 36 | serialize=False, 37 | to="cms.cmsplugin", 38 | ), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /djangocms_form_builder/migrations/0003_auto_20230129_1950.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2023-01-29 19:50 2 | 3 | import django.core.serializers.json 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("djangocms_form_builder", "0002_alter_form_cmsplugin_ptr_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="form", 15 | name="tag_type", 16 | ), 17 | migrations.RemoveField( 18 | model_name="formfield", 19 | name="tag_type", 20 | ), 21 | migrations.AddField( 22 | model_name="form", 23 | name="action_parameters", 24 | field=models.JSONField( 25 | blank=True, 26 | default=dict, 27 | encoder=django.core.serializers.json.DjangoJSONEncoder, 28 | null=True, 29 | ), 30 | ), 31 | migrations.AlterField( 32 | model_name="form", 33 | name="captcha_widget", 34 | field=models.CharField( 35 | blank=True, 36 | choices=[("", "-----")], 37 | default="", 38 | help_text='Read more in the documentation.', 39 | max_length=16, 40 | verbose_name="captcha widget", 41 | ), 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /djangocms_form_builder/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/djangocms_form_builder/migrations/__init__.py -------------------------------------------------------------------------------- /djangocms_form_builder/recaptcha.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | from django.conf import settings 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from .helpers import coerce_decimal 6 | 7 | CAPTCHA_WIDGETS = {} 8 | CAPTCHA_FIELDS = {} 9 | CAPTCHA_CHOICES = () 10 | 11 | if apps.is_installed("captcha"): 12 | """Soft dependency on django-captcha for reCaptchaField""" 13 | 14 | from captcha.fields import ReCaptchaField # NOQA 15 | from captcha.widgets import ReCaptchaV2Checkbox, ReCaptchaV2Invisible # NOQA 16 | 17 | CAPTCHA_WIDGETS["v2-checkbox"] = ReCaptchaV2Checkbox 18 | CAPTCHA_WIDGETS["v2-invisible"] = ReCaptchaV2Invisible 19 | 20 | CAPTCHA_FIELDS["v2-checkbox"] = ReCaptchaField 21 | CAPTCHA_FIELDS["v2-invisible"] = ReCaptchaField 22 | 23 | CAPTCHA_CHOICES += ( 24 | ("v2-checkbox", f"reCaptcha - {_('v2 checkbox')}"), 25 | ("v2-invisible", f"reCaptcha - {_('v2 invisible')}"), 26 | ) 27 | 28 | if apps.is_installed("hcaptcha"): 29 | """Soft dependency on django-hcaptcha for hcaptcha""" 30 | 31 | from hcaptcha.fields import hCaptchaField # NOQA 32 | from hcaptcha.widgets import hCaptchaWidget # NOQA 33 | 34 | CAPTCHA_FIELDS["hcaptcha"] = hCaptchaField 35 | CAPTCHA_WIDGETS["hcaptcha"] = hCaptchaWidget 36 | 37 | CAPTCHA_CHOICES += (("hcaptcha", _("hCaptcha")),) 38 | 39 | if len(CAPTCHA_CHOICES) > 0: 40 | installed = True 41 | else: 42 | installed = False 43 | 44 | 45 | def get_recaptcha_field(instance): 46 | config = instance.captcha_config 47 | widget_params = { 48 | "attrs": { 49 | key: value 50 | for key, value in config.get("captcha_config", {}).items() 51 | if key.startswith("data-") 52 | }, 53 | "api_params": { 54 | key: value 55 | for key, value in config.get("captcha_config", {}).items() 56 | if not key.startswith("data-") 57 | }, 58 | } 59 | widget_params["attrs"]["no_field_sep"] = True 60 | if config.get("captcha_widget", "") == "v3": 61 | widget_params["attrs"]["required_score"] = coerce_decimal( 62 | config.get("captcha_requirement", 0.5) 63 | ) # installing recaptcha 3 ? 64 | if not widget_params["api_params"]: 65 | del widget_params["api_params"] 66 | return CAPTCHA_FIELDS[instance.captcha_widget]( 67 | widget=CAPTCHA_WIDGETS[instance.captcha_widget](**widget_params), label="" 68 | ) 69 | 70 | 71 | keys_available = installed and ( 72 | hasattr(settings, "RECAPTCHA_PUBLIC_KEY") 73 | and hasattr(settings, "RECAPTCHA_PRIVATE_KEY") 74 | ) 75 | 76 | field_name = "captcha_field" 77 | RECAPTCHA_PUBLIC_KEY = getattr(settings, "RECAPTCHA_PUBLIC_KEY", "") 78 | -------------------------------------------------------------------------------- /djangocms_form_builder/settings.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | from django.conf import settings as django_settings 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | EMPTY_CHOICE = (("", "-----"),) 7 | 8 | ADMIN_CSS = getattr( 9 | django_settings, 10 | "DJANGOCMS_FRONTEND_ADMIN_CSS", 11 | {}, 12 | ) 13 | 14 | 15 | FORM_OPTIONS = getattr(django_settings, "DJANGOCMS_FORMS_OPTIONS", {}) 16 | MAIL_TEMPLATE_SETS = getattr( 17 | django_settings, "DJANGOCMS_MAIL_TEMPLATE_SETS", (("default", _("Default")),) 18 | ) 19 | 20 | framework = getattr(django_settings, "DJANGOCMS_FRONTEND_FRAMEWORK", "bootstrap5") 21 | theme = getattr(django_settings, "DJANGOCMS_FRONTEND_THEME", "djangocms_frontend") 22 | 23 | DEFAULT_SPACER_SIZE_CHOICES = (("mb-3", "Default"),) 24 | TAG_CHOICES = (("div", "div"),) 25 | FORM_TEMPLATE = getattr( 26 | django_settings, 27 | "FORM_TEMPLATE", 28 | f"djangocms_form_builder/{framework}/render/form.html", 29 | ) 30 | 31 | theme_render_path = f"{theme}.frameworks.{framework}" 32 | theme_forms_path = f"{theme}.forms" 33 | 34 | if not getattr(django_settings, "DJANGO_FORM_BUILDER_SPACER_CHOICES", False): 35 | if not getattr(django_settings, "DJANGOCMS_FRONTEND_SPACER_SIZES", False): 36 | SPACER_SIZE_CHOICES = DEFAULT_SPACER_SIZE_CHOICES 37 | else: 38 | SPACER_SIZE_CHOICES = [ 39 | (f"mb-{key}", value) 40 | for key, value in django_settings.DJANGOCMS_FRONTEND_SPACER_SIZES 41 | ] 42 | else: 43 | SPACER_SIZE_CHOICES = django_settings.DJANGO_FORM_BUILDER_SPACER_CHOICES 44 | 45 | 46 | def render_factory(cls, theme_module, render_module): 47 | parents = tuple( 48 | getattr(module, cls, None) 49 | for module in (theme_module, render_module) 50 | if module is not None and getattr(module, cls, None) is not None 51 | ) 52 | return type(cls, parents, dict()) # Empty Mix 53 | 54 | 55 | def get_mixins(naming, theme_path, mixin_path): 56 | try: 57 | theme_module = importlib.import_module(theme_path) if theme_path else None 58 | except ModuleNotFoundError: 59 | theme_module = None 60 | try: 61 | render_module = importlib.import_module(mixin_path) if mixin_path else None 62 | except ModuleNotFoundError: 63 | render_module = None 64 | 65 | return lambda name: render_factory( 66 | naming.format(name=name), theme_module, render_module 67 | ) 68 | 69 | 70 | def get_renderer(my_module): 71 | if not isinstance(my_module, str): 72 | my_module = my_module.__name__ 73 | return get_mixins( 74 | "{name}RenderMixin", theme_render_path, f"{my_module}.frameworks.{framework}" 75 | ) 76 | 77 | 78 | def get_forms(my_module): 79 | if not isinstance(my_module, str): 80 | my_module = my_module.__name__ 81 | return get_mixins( 82 | "{name}FormMixin", theme_forms_path, f"{my_module}.frameworks.{framework}" 83 | ) 84 | -------------------------------------------------------------------------------- /djangocms_form_builder/static/djangocms_form_builder/css/actions_form.css: -------------------------------------------------------------------------------- 1 | fieldset.action-hide, fieldset.empty { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /djangocms_form_builder/static/djangocms_form_builder/css/button_group.css: -------------------------------------------------------------------------------- 1 | form .form-row div.frontend-button-group.frontend-button-group-block{display:flex;flex-wrap:wrap}form .form-row div.frontend-button-group.frontend-button-group-block label{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;flex-basis:calc(25% - 6px)}@media(min-width: 820px){form .form-row div.frontend-button-group.frontend-button-group-block label{flex-basis:calc(20% - 6px)}}form .form-row div.frontend-button-group label.btn-grp{margin:3px;padding:4px 7px;text-transform:none;text-align:center !important;outline:2px solid transparent;width:auto !important;font-weight:normal !important}form .form-row div.frontend-button-group input[property=color]+label.btn-grp[style="--fe-value: ;"]{color:var(--dca-black, #333) !important}@media(prefers-color-scheme: dark){form .form-row div.frontend-button-group input[property=color]+label.btn-grp[style="--fe-value: ;"]{color:var(--dca-black, #eee) !important}}form .form-row div.frontend-button-group input[property=color][value=transparent]+label.btn-grp{color:var(--dca-black, #333) !important}@media(prefers-color-scheme: dark){form .form-row div.frontend-button-group input[property=color][value=transparent]+label.btn-grp{color:var(--dca-black, #eee) !important}}form .form-row div.frontend-button-group input[property=text]+label.btn-grp{color:var(--dca-black, #333) !important}@media(prefers-color-scheme: dark){form .form-row div.frontend-button-group input[property=text]+label.btn-grp{color:var(--dca-black, #eee) !important}}form .form-row div.frontend-button-group input[type=radio]:checked+label.btn-grp,form .form-row div.frontend-button-group input[type=checkbox]:checked+label.btn-grp{outline:2px solid #0bf;border-color:#fff;border-radius:0}form .form-row div.frontend-button-group input[property=text]:checked+label.btn-grp,form .form-row div.frontend-button-group input[property=icon]:checked+label.btn-grp{background:#0bf}form .form-row div.frontend-button-group input[property=opacity]+label.btn-grp{width:3.5em !important;padding-left:0 !important;padding-right:0 !important;color:var(--dca-black, #eee) !important;overflow:hidden;white-space:nowrap;background:rgba(var(--bs-secondary-rgb), calc(var(--fe-value) / 100))}@media(prefers-color-scheme: light){form .form-row div.frontend-button-group input[property=opacity][value="50"]+label.btn-grp,form .form-row div.frontend-button-group input[property=opacity][value="25"]+label.btn-grp,form .form-row div.frontend-button-group input[property=opacity][value="10"]+label.btn-grp{color:#000 !important}}form .form-row div.frontend-button-group input[property=link-size][value=btn-lg]+label.btn-grp{padding:.5rem 1rem !important;font-size:1.25rem;border-radius:.3rem}form .form-row div.frontend-button-group input[property=link-size][value=btn-sm]+label.btn-grp{padding:.25rem .5rem !important;font-size:.875rem;border-radius:.2rem}form .form-row div.frontend-button-group input[property=list_state]+label.btn-grp[style="--fe-value: ;"]{color:var(--dca-black, #333) !important}@media(prefers-color-scheme: dark){form .form-row div.frontend-button-group input[property=list_state]+label.btn-grp[style="--fe-value: ;"]{color:var(--dca-black, #eee)}}form .form-row div.frontend-button-group input[property=list_state][value=active]+label.btn-grp{background:var(--bs-primary);color:#fff}form .form-row div.frontend-button-group input[property=list_state][value=disabled]+label.btn-grp{color:var(--bs-gray-600) !important;background:#fff}form .form-row div.frontend-button-group input[property=shadow]+label.btn-grp{margin-right:1em;width:3.5em !important;padding-left:0 !important;padding-right:0 !important;background:var(--bs-secondary);color:#fff !important;text-align:center !important;border:var(--bs-secondary) 1px solid !important;overflow:hidden;white-space:nowrap}form .form-row div.frontend-button-group input[property=shadow][value=lg]+label.btn-grp{box-shadow:0 1rem 3rem rgba(0,0,0,.175) !important}form .form-row div.frontend-button-group input[property=shadow][value=reg]+label.btn-grp{box-shadow:0 .5rem 1rem rgba(0,0,0,.15) !important}form .form-row div.frontend-button-group input[property=shadow][value=sm]+label.btn-grp{box-shadow:0 .125rem .25rem rgba(0,0,0,.075) !important}form .form-row div.frontend-button-group input[property=nav-design][value=light]+label.btn-grp{background:var(--bs-light);color:var(--bs-gray-900) !important}form .form-row div.frontend-button-group input[property=nav-design][value=dark]+label.btn-grp{background:var(--bs-dark);color:var(--bs-white)}form .form-row div.frontend-button-group .btn-white{background:#fff;color:#000 !important}form .form-row div.frontend-button-group .btn-light{color:#000 !important}form .form-row div.frontend-button-group .btn-dark{color:#fff !important}form .form-row div.frontend-button-group .btn-transparent{color:var(--dca-black, var(--body-fg, #000))}body:not(.djangocms-admin-style) .frontend-button-group .optgroup{clear:left}/*# sourceMappingURL=button_group.css.map */ 2 | -------------------------------------------------------------------------------- /djangocms_form_builder/static/djangocms_form_builder/js/actions_form.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function () { 2 | 'use strict'; 3 | 4 | for (const element of document.querySelectorAll('fieldset.action-auto-hide input[type="checkbox"][name="form_actions"]')) { 5 | const getByClass = (className) => (document.getElementsByClassName('c' + className) || [undefined])[0]; 6 | const target = getByClass(element.value); 7 | 8 | if (target) { 9 | if (element.checked) { 10 | target.classList.remove("action-hide"); 11 | } 12 | if (!target.querySelector('.form-row:not(.hidden)')) { 13 | target.classList.add("empty"); 14 | } 15 | element.addEventListener('change', function (event) { 16 | if (event.target.checked) { 17 | getByClass(event.target.value)?.classList.remove("action-hide"); 18 | } else { 19 | getByClass(event.target.value)?.classList.add("action-hide"); 20 | } 21 | }); 22 | } 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/captcha/includes/js_v2_checkbox.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/captcha/includes/js_v2_invisible.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/actions/submit_message.html: -------------------------------------------------------------------------------- 1 | {{ message|safe }} 2 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/admin/widgets/button_group.html: -------------------------------------------------------------------------------- 1 | {% for group, options, index in widget.optgroups %}{% if group %} 2 |
{% endif %}{% for option in options %}{% include option.template_name with widget=option %}{% endfor %}{% if group %} 3 |
{% endif %}{% endfor %} 4 | 5 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/admin/widgets/button_group_color_option.html: -------------------------------------------------------------------------------- 1 | {% if widget.wrap_label %}{% endif %}{% if widget.wrap_label %} {{ widget.label }}{% endif %} 2 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/admin/widgets/button_group_option.html: -------------------------------------------------------------------------------- 1 | {% if widget.wrap_label %}{% endif %}{% if widget.wrap_label %} {{ widget.label }}{% endif %} 2 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/admin/widgets/icon_group_option.html: -------------------------------------------------------------------------------- 1 | {% if widget.wrap_label %}{% endif %} 2 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/admin/widgets/select.html: -------------------------------------------------------------------------------- 1 |
{% include "django/forms/widgets/select.html" %}
2 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/ajax_base.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %}{% block content %}{% endblock content %}{% render_block 'js' %} 2 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/ajax_form.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags i18n form_builder_tags cms_tags %}{% spaceless %} 2 |
3 |
4 |
    5 |
6 |
7 | {% for plugin in instance.child_plugin_instances %} 8 | {% render_plugin plugin %} 9 | {% empty %} 10 | {% render_form form %} 11 | {% endfor %} 12 | {% if instance.child_plugin_instances %} 13 | {% render_recaptcha_widget form %} 14 | {% endif %} 15 |
{% endspaceless %} 16 | {% if instance.config.captcha_widget %} 17 | {% addtoblock 'js' %}{% spaceless %} 18 | 23 | 24 | {% endspaceless %}{% endaddtoblock %} 25 | {% endif %} 26 | {% addtoblock 'js' %}{% spaceless %} 27 | 159 | {% endspaceless %}{% endaddtoblock %} 160 | {% addtoblock 'js' %}{% endaddtoblock %} 174 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/bootstrap5/form.html: -------------------------------------------------------------------------------- 1 | {% load static cms_tags sekizai_tags i18n %} 2 | {% spaceless %} 3 | {% if form %} 4 |
8 | {% csrf_token %} 9 | {% include 'djangocms_form_builder/ajax_form.html' with form=form instance=instance tracking=instance.tracking_code RECAPTCHA_PUBLIC_KEY=RECAPTCHA_PUBLIC_KEY %} 10 | {% if not has_submit_button %} 11 | 13 | {% endif %} 14 |
15 | {% endif %} 16 | {% endspaceless %} 17 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/bootstrap5/render/field.html: -------------------------------------------------------------------------------- 1 | {% load form_builder_tags %}{% spaceless %} 2 | {% if form_field|stringformat:"s" == form_field %} 3 | {% render_widget form form_field %} 4 | {% else %} 5 |
6 | {% for form_field in form_field %} 7 |
8 | {% include "djangocms_form_builder/bootstrap5/render/field.html" with form=form form_field=form_field %} 9 |
10 | {% endfor %} 11 |
12 | {% endif %} 13 | {% endspaceless %} 14 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/bootstrap5/render/form.html: -------------------------------------------------------------------------------- 1 | {% load form_builder_tags %}{% spaceless %} 2 | 9 | {% for hidden_field in form.hidden_fields %} 10 | {{ hidden_field }} 11 | {% endfor %} 12 | {% block form_fields %} 13 | {% for title, prop in form|get_fieldset %} 14 | {% include "djangocms_form_builder/bootstrap5/render/section.html" with form=form title=title prop=prop section=forloop.counter %} 15 | {% endfor %} 16 | {% endblock %} 17 | {% endspaceless %} 18 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/bootstrap5/render/section.html: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 | {% with prop.classes as classes %} 3 | {% if title %}
{% if "collapse" in classes %}{% endif %}{{ title }}{% if "collapse" in classes %}{% endif %}
{% endif %} 4 | {% if prop.desription %}

{{ prop.description }}

{% endif %} 5 |
6 | {% for form_field in prop.fields %} 7 | {% include "djangocms_form_builder/bootstrap5/render/field.html" with form=form form_field=form_field %} 8 | {% endfor %} 9 |
10 | {% endwith %} 11 | {% endspaceless %} 12 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/bootstrap5/widgets/base.html: -------------------------------------------------------------------------------- 1 | {% load form_builder_tags %}{% render_widget form instance.field_name %} 2 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/bootstrap5/widgets/submit.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/mails/default/mail_html.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |

{% trans "Form submission" %}

3 |

{% if user %}{% trans "by" %} {{ user.firstname }} {{ user.lastname }} ({{ user.username }}){% else %}{% trans "by anonymous" %}{% endif %}

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% for field, value in cleaned_data.items %} 13 | 14 | 15 | 16 | 17 | {% endfor %} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
{% trans "Field" %}{% trans "Value" %}
{{ field }}{{ value }}
{% trans "time" %}{% now "H:i d/m/Y" %}
{% trans "user agent" %}{{ user_agent }}
{% trans "referer" %}{{ referer }}
32 | 33 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/widgets/input_option.html: -------------------------------------------------------------------------------- 1 | {% if widget.wrap_label %}{% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.wrap_label %} {{ widget.label }}{% endif %} 2 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_form_builder/widgets/mutliple_input.html: -------------------------------------------------------------------------------- 1 | {% with id=widget.attrs.id %}{% for group, options, index in widget.optgroups %}{% if group %} 2 |
{{ group }}{% endif %}{% for option in options %} 3 | {% include option.template_name with widget=option %}
{% endfor %}{% if group %} 4 | {% endif %}{% endfor %} 5 | {% endwith %} 6 | -------------------------------------------------------------------------------- /djangocms_form_builder/templates/djangocms_frontend/admin/base.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/cms/page/plugin/change_form.html" %} 2 | -------------------------------------------------------------------------------- /djangocms_form_builder/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/djangocms_form_builder/templatetags/__init__.py -------------------------------------------------------------------------------- /djangocms_form_builder/templatetags/form_builder_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.apps import apps 3 | from django.template.loader import render_to_string 4 | from django.utils.html import mark_safe 5 | 6 | from .. import constants, recaptcha 7 | from ..helpers import get_option 8 | from ..settings import FORM_TEMPLATE 9 | 10 | register = template.Library() 11 | attr_dict = constants.attr_dict 12 | default_attr = constants.default_attr 13 | 14 | 15 | if apps.is_installed("crispy_forms"): 16 | from crispy_forms.helper import FormHelper 17 | from crispy_forms.utils import render_crispy_form as render_form_implementation 18 | 19 | crispy_forms_installed = True 20 | else: 21 | crispy_forms_installed = False 22 | 23 | def render_form_implementation(form, helper=None, context=None): 24 | return str(form) 25 | 26 | class FormHelper: 27 | def __init__(self, form): 28 | self.form = form 29 | 30 | 31 | @register.filter 32 | def add_placeholder(form): 33 | """Adds placeholder based on a form field's title""" 34 | for field_name, _ in form.fields.items(): 35 | form.fields[field_name].widget.attrs["placeholder"] = form.fields[ 36 | field_name 37 | ].label 38 | return form 39 | 40 | 41 | @register.simple_tag() 42 | def render_form(form, **kwargs): 43 | """Renders form either with crispy_forms if installed and form has helper or with 44 | django-formset's means""" 45 | if crispy_forms_installed: 46 | helper = kwargs.pop("helper", None) or getattr(form, "helper", None) 47 | if helper is None and get_option(form, "crispy_form"): 48 | helper = FormHelper(form=form) 49 | if helper is not None: 50 | helper.form_tag = False 51 | helper.disable_csrf = True 52 | return mark_safe(render_form_implementation(form, helper, None)) 53 | template = kwargs.pop("template", FORM_TEMPLATE) 54 | return render_to_string(template, {"form": form, **kwargs}) 55 | 56 | 57 | def get_bound_field(form, formfield): 58 | if form: 59 | for field in form.visible_fields(): 60 | if field.name == formfield: 61 | return field 62 | return None 63 | 64 | 65 | def attrs_for_widget(widget, item, additional_classes=None): 66 | if widget.__class__.__name__ in constants.attr_dict: 67 | cls = attr_dict[widget.__class__.__name__].get(item, default_attr[item]) 68 | else: 69 | cls = default_attr[item] 70 | if cls: 71 | if additional_classes: 72 | cls += " " + additional_classes 73 | else: 74 | cls = additional_classes or "" 75 | return {"class": cls} 76 | 77 | 78 | @register.simple_tag(takes_context=False) 79 | def render_widget(form, form_field, **kwargs): 80 | field = get_bound_field(form, form_field) 81 | if field is None: 82 | return "" 83 | floating_labels = get_option(form, "floating_labels") 84 | if field.field.widget.attrs.pop("no_field_sep", False): 85 | field_sep = "" 86 | else: 87 | field_sep = get_option(form, "field_sep", constants.DEFAULT_FIELD_SEP) 88 | widget_attr = kwargs 89 | if form.is_bound: 90 | add_classes = "is_invalid" if field.errors else "is_valid" 91 | else: 92 | add_classes = None 93 | widget_attr.update( 94 | attrs_for_widget(field.field.widget, "input", additional_classes=add_classes) 95 | ) 96 | label_attr = attrs_for_widget(field.field.widget, "label") 97 | if field.help_text: 98 | widget_attr.update({"aria-describedby": f"hints_{field.id_for_label}"}) 99 | help_text = f'
{field.help_text}
' 100 | else: 101 | help_text = "" 102 | input_type = getattr(field.field.widget, "input_type", None) 103 | if floating_labels: 104 | widget_attr.setdefault("placeholder", "-") 105 | if input_type not in ("checkbox", "radio"): 106 | field_sep += " form-floating" # TODO: Only true for Bootstrap5 107 | div_attrs = attrs_for_widget(field.field.widget, "div", field_sep) 108 | div_attrs = " ".join([f'{key}="{value}"' for key, value in div_attrs.items()]) 109 | grp_attrs = attrs_for_widget(field.field.widget, "group") 110 | errors = "".join( 111 | f'
{error}
' for error in field.errors 112 | ) 113 | if field.field.widget.template_name.rsplit("/", 1)[-1] in ( 114 | "radio.html", 115 | "checkbox_select.html", 116 | ): 117 | """For multi-valued widgets use own templates to ensure classes appear at the right nesting""" 118 | field.field.widget.template_name = ( 119 | "djangocms_form_builder/widgets/mutliple_input.html" 120 | ) 121 | field.field.widget.option_template_name = ( 122 | "djangocms_form_builder/widgets/input_option.html" 123 | ) 124 | widget_attr["label_class"] = label_attr.pop( 125 | "class", None 126 | ) # pass through label classes 127 | widget_attr["div_class"] = grp_attrs.pop( 128 | "class", None 129 | ) # pass through div classes 130 | input_first = False 131 | else: 132 | input_first = ( 133 | floating_labels 134 | or input_type == "checkbox" 135 | or input_type == "select" 136 | and floating_labels 137 | ) 138 | widget = field.as_widget(attrs=widget_attr) 139 | label = field.label_tag(attrs=label_attr) if field.label else "" 140 | if input_first: 141 | render = f"
{widget}{label}{errors}{help_text}
" 142 | else: 143 | render = f"
{label}{widget}{errors}{help_text}
" 144 | return mark_safe(render) 145 | 146 | 147 | @register.simple_tag(takes_context=False) 148 | def render_recaptcha_widget(form): 149 | if recaptcha.installed: 150 | return render_widget(form, recaptcha.field_name) 151 | return "" 152 | 153 | 154 | @register.filter_function 155 | def get_fieldset(form): 156 | """returns the fieldsets of a form if available or generates a fieldset as a 157 | list of all fields""" 158 | if hasattr(form, "get_fieldsets") and callable(form.get_fieldsets): 159 | return form.get_fieldsets() 160 | elif hasattr(form, "Meta") and hasattr(form.Meta, "fieldsets"): 161 | return form.Meta.fieldsets 162 | return ((None, {"fields": [field.name for field in form.visible_fields()]}),) 163 | -------------------------------------------------------------------------------- /djangocms_form_builder/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = "djangocms_form_builder" 6 | 7 | urlpatterns = [ 8 | path("f", views.AjaxView.as_view(), name="ajaxformbuilder"), 9 | path( 10 | "/", 11 | views.AjaxView.as_view(), 12 | name="ajaxview", 13 | ), 14 | path( 15 | "", 16 | views.AjaxView.as_view(), 17 | name="ajaxview", 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /djangocms_form_builder/views.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from cms.models import CMSPlugin 4 | from django.core.exceptions import ValidationError 5 | from django.http import Http404, JsonResponse, QueryDict 6 | from django.shortcuts import get_object_or_404 7 | from django.utils.crypto import get_random_string 8 | from django.utils.translation import gettext as _ 9 | from django.views import View 10 | 11 | _formview_pool = {} 12 | 13 | 14 | def register_form_view(cls, slug=None): 15 | """ 16 | Registers a Widget (with type defined by cls) and slug 17 | :type cls: class 18 | :type slug: string to instantiate dashboard_widget 19 | """ 20 | if not slug: 21 | slug = get_random_string(length=12) 22 | key = hashlib.sha384(slug.encode("utf-8")).hexdigest() 23 | if key in _formview_pool: 24 | assert _formview_pool[key][0] == cls, _( 25 | "Only unique slugs accepted for form views" 26 | ) 27 | _formview_pool[key] = (cls, slug, key) 28 | return key 29 | 30 | 31 | class AjaxView(View): 32 | """ 33 | A Django view to handle AJAX requests for GET and POST methods for django CMS Form Builder forms. 34 | this view allows django CMS plugins to receive ajax requests if they implement the `ajax_get` and 35 | `ajax_post` methods. The form plugin implements the `ajax_post` method to handle form submissions. 36 | 37 | Methods 38 | ------- 39 | 40 | dispatch(request, \*args, \*\*kwargs) 41 | Overrides the default dispatch method to handle AJAX requests. 42 | 43 | decode_path(path) 44 | Decodes a URL path into a dictionary of parameters. 45 | 46 | plugin_instance(pk) 47 | Retrieves the plugin instance and its associated model instance by primary key. 48 | 49 | ajax_post(request, \*args, \*\*kwargs) 50 | Handles AJAX POST requests. Calls the `ajax_post` method of the plugin or form instance if available. 51 | 52 | ajax_get(request, \*args, \*\*kwargs) 53 | Handles AJAX GET requests. Calls the `ajax_get` method of the plugin or form instance if available. 54 | """ 55 | def dispatch(self, request, *args, **kwargs): 56 | if request.accepts("application/json"): 57 | if request.method == "GET" and "get" in self.http_method_names: 58 | return self.ajax_get(request, *args, **kwargs) 59 | elif request.method == "POST" and "post" in self.http_method_names: 60 | return self.ajax_post(request, *args, **kwargs) 61 | return super().dispatch(request, *args, **kwargs) 62 | 63 | @staticmethod 64 | def decode_path(path): 65 | params = {} 66 | for element in path.split(","): 67 | if "=" in element: 68 | params[element.split("=", 1)[0]] = element.split("=", 1)[1] 69 | elif "%3D" in element: 70 | params[element.split("%3D", 1)[0]] = element.split("%3D", 1)[1] 71 | else: 72 | params[element] = True 73 | return params 74 | 75 | @staticmethod 76 | def plugin_instance(pk): 77 | plugin = get_object_or_404(CMSPlugin, pk=pk) 78 | plugin.__class__ = plugin.get_plugin_class() 79 | instance = ( 80 | plugin.model.objects.get(cmsplugin_ptr=plugin.id) 81 | if hasattr(plugin.model, "cmsplugin_ptr") 82 | else plugin 83 | ) 84 | return plugin, instance 85 | 86 | def ajax_post(self, request, *args, **kwargs): 87 | """ 88 | Handles AJAX POST requests for the form builder. 89 | 90 | This method processes AJAX POST requests by determining the appropriate 91 | plugin or form instance to handle the request based on the provided 92 | keyword arguments. 93 | 94 | Args: 95 | request (HttpRequest): The HTTP request object. 96 | \*args: Additional positional arguments. 97 | \*\*kwargs: Additional keyword arguments, which may include: 98 | - instance_id (int): The ID of the plugin instance. 99 | - parameter (str): Optional parameter for decoding. 100 | - form_id (str): The ID of the form instance. 101 | 102 | Returns: 103 | JsonResponse: A JSON response with the result of the AJAX POST request. 104 | Http404: If the plugin or form instance cannot be found or does not 105 | support AJAX POST requests. 106 | 107 | Raises: 108 | Http404: If the plugin or form instance cannot be found or does not 109 | support AJAX POST requests. 110 | ValidationError: If there is a validation error during the request 111 | processing. 112 | """ 113 | if "instance_id" in kwargs: 114 | plugin, instance = self.plugin_instance(kwargs["instance_id"]) 115 | if hasattr(plugin, "ajax_post"): 116 | request.POST = QueryDict(request.body) 117 | try: 118 | params = ( 119 | self.decode_path(kwargs["parameter"]) 120 | if "parameter" in kwargs 121 | else {} 122 | ) 123 | return plugin.ajax_post(request, instance, params) 124 | except ValidationError as error: 125 | return JsonResponse({"result": "error", "msg": str(error.args[0])}) 126 | else: 127 | raise Http404() 128 | elif "form_id" in kwargs: 129 | if kwargs["form_id"] in _formview_pool: 130 | form_id = kwargs.pop("form_id") 131 | instance = _formview_pool[form_id][0](*args, **kwargs) 132 | if hasattr(instance, "ajax_post"): 133 | return instance.ajax_post(request, *args, **kwargs) 134 | elif hasattr(instance, "post"): 135 | return instance.post(request, *args, **kwargs) 136 | raise Http404() 137 | raise Http404() 138 | 139 | def ajax_get(self, request, *args, **kwargs): 140 | """ 141 | Handles AJAX GET requests. 142 | 143 | This method processes AJAX GET requests by checking for specific keys in the 144 | `kwargs` and delegating the request to the appropriate handler. 145 | 146 | Args: 147 | request (HttpRequest): The HTTP request object. 148 | \*args: Additional positional arguments. 149 | \*\*kwargs: Additional keyword arguments. 150 | 151 | Returns: 152 | JsonResponse: A JSON response with the result of the AJAX request. 153 | Http404: If the required keys are not found in `kwargs` or the handler is not available. 154 | 155 | Raises: 156 | ValidationError: If there is an error during the processing of the AJAX request. 157 | 158 | Notes: 159 | - If "instance_id" is present in `kwargs`, it attempts to retrieve the plugin instance 160 | and calls its `ajax_get` method if available. 161 | - If "form_id" is present in `kwargs`, it attempts to retrieve the form instance from 162 | the `_formview_pool` and calls its `ajax_get` or `get` method if available. 163 | """ 164 | if "instance_id" in kwargs: 165 | plugin, instance = self.plugin_instance(kwargs["instance_id"]) 166 | if hasattr(plugin, "ajax_get"): 167 | request.GET = QueryDict(request.body) 168 | try: 169 | params = ( 170 | self.decode_path(kwargs["parameter"]) 171 | if "parameter" in kwargs 172 | else {} 173 | ) 174 | return plugin.ajax_get(request, instance, params) 175 | except ValidationError as error: 176 | return JsonResponse({"result": "error", "msg": str(error.args[0])}) 177 | elif "form_id" in kwargs: 178 | if kwargs["form_id"] in _formview_pool: 179 | form_id = kwargs.pop("form_id") 180 | instance = _formview_pool[form_id][0](**kwargs) 181 | if hasattr(instance, "ajax_get"): 182 | return instance.ajax_get(request, *args, **kwargs) 183 | elif hasattr(instance, "get"): 184 | return instance.get(request, *args, **kwargs) 185 | raise Http404() 186 | raise Http404() 187 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | SOURCEDIR = source 10 | VENV = env/bin/activate 11 | PORT = 8001 12 | 13 | # Internal variables. 14 | PAPEROPT_a4 = -D latex_paper_size=a4 15 | PAPEROPT_letter = -D latex_paper_size=letter 16 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SOURCEDIR) 17 | # the i18n builder cannot share the environment and doctrees with the others 18 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | 20 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 21 | 22 | help: 23 | @echo "Please use \`make ' where is one of" 24 | @echo " install to create a virtualenv and install the requirements" 25 | @echo " clean to delete the build directory" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " run to build the docs, watch for changes, and serve them at http://0.0.0.0:8001" 51 | 52 | install: 53 | @echo "... setting up docs virtualenv" 54 | python3 -m venv env 55 | . $(VENV); pip install -r requirements.txt 56 | @echo "\n" \ 57 | "-------------------------------------------------------------------------------------------------- \n" \ 58 | "* to build and serve the documentation: make run \n" 59 | 60 | clean: 61 | rm -rf $(BUILDDIR)/* 62 | 63 | html: 64 | . $(VENV) 65 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 66 | @echo 67 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 68 | 69 | dirhtml: 70 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 71 | @echo 72 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 73 | 74 | singlehtml: 75 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 76 | @echo 77 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 78 | 79 | pickle: 80 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 81 | @echo 82 | @echo "Build finished; now you can process the pickle files." 83 | 84 | json: 85 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 86 | @echo 87 | @echo "Build finished; now you can process the JSON files." 88 | 89 | htmlhelp: 90 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 91 | @echo 92 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 93 | ".hhp project file in $(BUILDDIR)/htmlhelp." 94 | 95 | qthelp: 96 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 97 | @echo 98 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 99 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 100 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/djangocms-blog.qhcp" 101 | @echo "To view the help file:" 102 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/djangocms-blog.qhc" 103 | 104 | applehelp: 105 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 106 | @echo 107 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 108 | @echo "N.B. You won't be able to view it unless you put it in" \ 109 | "~/Library/Documentation/Help or install it in your application" \ 110 | "bundle." 111 | 112 | devhelp: 113 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 114 | @echo 115 | @echo "Build finished." 116 | @echo "To view the help file:" 117 | @echo "# mkdir -p $$HOME/.local/share/devhelp/djangocms-blog" 118 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/djangocms-blog" 119 | @echo "# devhelp" 120 | 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | latex: 127 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 128 | @echo 129 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 130 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 131 | "(use \`make latexpdf' here to do that automatically)." 132 | 133 | latexpdf: 134 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 135 | @echo "Running LaTeX files through pdflatex..." 136 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 137 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 138 | 139 | latexpdfja: 140 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 141 | @echo "Running LaTeX files through platex and dvipdfmx..." 142 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 143 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 144 | 145 | text: 146 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 147 | @echo 148 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 149 | 150 | man: 151 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 152 | @echo 153 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 154 | 155 | texinfo: 156 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 157 | @echo 158 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 159 | @echo "Run \`make' in that directory to run these through makeinfo" \ 160 | "(use \`make info' here to do that automatically)." 161 | 162 | info: 163 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 164 | @echo "Running Texinfo files through makeinfo..." 165 | make -C $(BUILDDIR)/texinfo info 166 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 167 | 168 | gettext: 169 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 170 | @echo 171 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 172 | 173 | changes: 174 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 175 | @echo 176 | @echo "The overview file is in $(BUILDDIR)/changes." 177 | 178 | linkcheck: 179 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 180 | @echo 181 | @echo "Link check complete; look for any errors in the above output " \ 182 | "or in $(BUILDDIR)/linkcheck/output.txt." 183 | 184 | doctest: 185 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 186 | @echo "Testing of doctests in the sources finished, look at the " \ 187 | "results in $(BUILDDIR)/doctest/output.txt." 188 | 189 | coverage: 190 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 191 | @echo "Testing of coverage in the sources finished, look at the " \ 192 | "results in $(BUILDDIR)/coverage/python.txt." 193 | 194 | xml: 195 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 196 | @echo 197 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 198 | 199 | pseudoxml: 200 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 201 | @echo 202 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 203 | 204 | spelling: 205 | $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling 206 | 207 | run: 208 | . $(VENV); sphinx-autobuild $(ALLSPHINXOPTS) $(BUILDDIR)/html --host 0.0.0.0 --port $(PORT) 209 | -------------------------------------------------------------------------------- /docs/requirements.in: -------------------------------------------------------------------------------- 1 | furo 2 | Sphinx 3 | sphinx-autobuild 4 | sphinx-copybutton 5 | sphinxcontrib-spelling 6 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=requirements.txt requirements.in 6 | # 7 | alabaster==0.7.16 8 | # via sphinx 9 | babel==2.14.0 10 | # via sphinx 11 | beautifulsoup4==4.12.3 12 | # via furo 13 | certifi==2024.2.2 14 | # via requests 15 | charset-normalizer==3.3.2 16 | # via requests 17 | colorama==0.4.6 18 | # via sphinx-autobuild 19 | docutils==0.20.1 20 | # via sphinx 21 | furo==2024.1.29 22 | # via -r requirements.in 23 | idna==3.6 24 | # via requests 25 | imagesize==1.4.1 26 | # via sphinx 27 | jinja2==3.1.3 28 | # via sphinx 29 | livereload==2.6.3 30 | # via sphinx-autobuild 31 | markupsafe==2.1.5 32 | # via jinja2 33 | packaging==24.0 34 | # via sphinx 35 | pyenchant==3.2.2 36 | # via sphinxcontrib-spelling 37 | pygments==2.17.2 38 | # via 39 | # furo 40 | # sphinx 41 | requests==2.31.0 42 | # via sphinx 43 | six==1.16.0 44 | # via livereload 45 | snowballstemmer==2.2.0 46 | # via sphinx 47 | soupsieve==2.5 48 | # via beautifulsoup4 49 | sphinx==7.2.6 50 | # via 51 | # -r requirements.in 52 | # furo 53 | # sphinx-autobuild 54 | # sphinx-basic-ng 55 | # sphinx-copybutton 56 | # sphinxcontrib-spelling 57 | sphinx-autobuild==2024.2.4 58 | # via -r requirements.in 59 | sphinx-basic-ng==1.0.0b2 60 | # via furo 61 | sphinx-copybutton==0.5.2 62 | # via -r requirements.in 63 | sphinxcontrib-applehelp==1.0.8 64 | # via sphinx 65 | sphinxcontrib-devhelp==1.0.6 66 | # via sphinx 67 | sphinxcontrib-htmlhelp==2.0.5 68 | # via sphinx 69 | sphinxcontrib-jsmath==1.0.1 70 | # via sphinx 71 | sphinxcontrib-qthelp==1.0.7 72 | # via sphinx 73 | sphinxcontrib-serializinghtml==1.1.10 74 | # via sphinx 75 | sphinxcontrib-spelling==8.0.0 76 | # via -r requirements.in 77 | tornado==6.4 78 | # via livereload 79 | urllib3==2.2.1 80 | # via requests 81 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # djangocms-blog documentation build configuration file, created by 3 | # sphinx-quickstart on Sun Jun 5 23:27:04 2016. 4 | # 5 | # This file is execfile()d with the current directory set to its 6 | # containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | # fmt: off 15 | import sys 16 | from pathlib import Path 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | HERE = Path(__file__).parent 23 | sys.path.insert(0, str(HERE.parent.parent)) # this way, we don't have to install the app 24 | 25 | #import cms_helper # isort:skip # noqa 26 | import djangocms_form_builder # isort:skip # noqa 27 | # fmt: on 28 | 29 | # cms_helper.setup() 30 | 31 | 32 | # -- General configuration ------------------------------------------------ 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # needs_sphinx = '1.0' 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | "sphinx.ext.autodoc", 42 | "sphinx.ext.doctest", 43 | "sphinx.ext.intersphinx", 44 | "sphinx.ext.todo", 45 | "sphinx.ext.coverage", 46 | "sphinx_copybutton", 47 | "sphinxcontrib.spelling", 48 | ] 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ["_templates"] 52 | 53 | # The suffix(es) of source filenames. 54 | # You can specify multiple suffix as a list of string: 55 | # source_suffix = ['.rst', '.md'] 56 | source_suffix = ".rst" 57 | 58 | # The encoding of source files. 59 | # source_encoding = 'utf-8-sig' 60 | 61 | # The master toctree document. 62 | master_doc = "index" 63 | 64 | # General information about the project. 65 | project = "djangocms-frontend" 66 | author = "Fabian Braun" 67 | 68 | # The version info for the project you're documenting, acts as replacement for 69 | # |version| and |release|, also used in various other places throughout the 70 | # built documents. 71 | # 72 | # The short X.Y version. 73 | version = djangocms_form_builder.__version__ 74 | # The full version, including alpha/beta/rc tags. 75 | release = djangocms_form_builder.__version__ 76 | 77 | # The language for content autogenerated by Sphinx. Refer to documentation 78 | # for a list of supported languages. 79 | # 80 | # This is also used if you do content translation via gettext catalogs. 81 | # Usually you set "language" from the command line for these cases. 82 | language = None 83 | 84 | # There are two options for replacing |today|: either, you set today to some 85 | # non-false value, then it is used: 86 | # today = '' 87 | # Else, today_fmt is used as the format for a strftime call. 88 | # today_fmt = '%B %d, %Y' 89 | 90 | # List of patterns, relative to source directory, that match files and 91 | # directories to ignore when looking for source files. 92 | exclude_patterns = ["_build"] 93 | 94 | # The reST default role (used for this markup: `text`) to use for all 95 | # documents. 96 | # default_role = None 97 | 98 | # If true, '()' will be appended to :func: etc. cross-reference text. 99 | # add_function_parentheses = True 100 | 101 | # If true, the current module name will be prepended to all description 102 | # unit titles (such as .. function::). 103 | # add_module_names = True 104 | 105 | # If true, sectionauthor and moduleauthor directives will be shown in the 106 | # output. They are ignored by default. 107 | # show_authors = False 108 | 109 | # The name of the Pygments (syntax highlighting) style to use. 110 | pygments_style = "sphinx" 111 | 112 | # A list of ignored prefixes for module index sorting. 113 | # modindex_common_prefix = [] 114 | 115 | # If true, keep warnings as "system message" paragraphs in the built documents. 116 | # keep_warnings = False 117 | 118 | # If true, `todo` and `todoList` produce output, else they produce nothing. 119 | todo_include_todos = True 120 | 121 | 122 | # -- Options for HTML output ---------------------------------------------- 123 | 124 | 125 | html_theme = "furo" 126 | html_theme_options = { 127 | "navigation_with_keys": True, 128 | } 129 | 130 | 131 | # Theme options are theme-specific and customize the look and feel of a theme 132 | # further. For a list of options available for each theme, see the 133 | # documentation. 134 | # html_theme_options = {} 135 | 136 | # Add any paths that contain custom themes here, relative to this directory. 137 | # html_theme_path = [] 138 | 139 | # The name for this set of Sphinx documents. If None, it defaults to 140 | # " v documentation". 141 | # html_title = None 142 | 143 | # A shorter title for the navigation bar. Default is the same as html_title. 144 | # html_short_title = None 145 | 146 | # The name of an image file (relative to this directory) to place at the top 147 | # of the sidebar. 148 | # html_logo = None 149 | 150 | # The name of an image file (within the static path) to use as favicon of the 151 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 152 | # pixels large. 153 | # html_favicon = None 154 | 155 | # Add any paths that contain custom static files (such as style sheets) here, 156 | # relative to this directory. They are copied after the builtin static files, 157 | # so a file named "default.css" will overwrite the builtin "default.css". 158 | # html_static_path = ["_static"] 159 | 160 | # Add any extra paths that contain custom files (such as robots.txt or 161 | # .htaccess) here, relative to this directory. These files are copied 162 | # directly to the root of the documentation. 163 | # html_extra_path = [] 164 | 165 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 166 | # using the given strftime format. 167 | # html_last_updated_fmt = '%b %d, %Y' 168 | 169 | # If true, SmartyPants will be used to convert quotes and dashes to 170 | # typographically correct entities. 171 | # html_use_smartypants = True 172 | 173 | # Custom sidebar templates, maps document names to template names. 174 | # html_sidebars = {} 175 | 176 | # Additional templates that should be rendered to pages, maps page names to 177 | # template names. 178 | # html_additional_pages = {} 179 | 180 | # If false, no module index is generated. 181 | # html_domain_indices = True 182 | 183 | # If false, no index is generated. 184 | # html_use_index = True 185 | 186 | # If true, the index is split into individual pages for each letter. 187 | # html_split_index = False 188 | 189 | # If true, links to the reST sources are added to the pages. 190 | # html_show_sourcelink = True 191 | 192 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 193 | # html_show_sphinx = True 194 | 195 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 196 | # html_show_copyright = True 197 | 198 | # If true, an OpenSearch description file will be output, and all pages will 199 | # contain a tag referring to it. The value of this option must be the 200 | # base URL from which the finished HTML is served. 201 | # html_use_opensearch = '' 202 | 203 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 204 | # html_file_suffix = None 205 | 206 | # Language to be used for generating the HTML full-text search index. 207 | # Sphinx supports the following languages: 208 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 209 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 210 | # html_search_language = 'en' 211 | 212 | # A dictionary with options for the search language support, empty by default. 213 | # Now only 'ja' uses this config value 214 | # html_search_options = {'type': 'default'} 215 | 216 | # The name of a javascript file (relative to the configuration directory) that 217 | # implements a search results scorer. If empty, the default will be used. 218 | # html_search_scorer = 'scorer.js' 219 | 220 | # Output file base name for HTML help builder. 221 | htmlhelp_basename = "djangocms-frontenddoc" 222 | 223 | # -- Options for LaTeX output --------------------------------------------- 224 | 225 | latex_elements = { 226 | # The paper size ('letterpaper' or 'a4paper'). 227 | # 'papersize': 'letterpaper', 228 | # The font size ('10pt', '11pt' or '12pt'). 229 | # 'pointsize': '10pt', 230 | # Additional stuff for the LaTeX preamble. 231 | # 'preamble': '', 232 | # Latex figure (float) alignment 233 | # 'figure_align': 'htbp', 234 | } 235 | 236 | # Grouping the document tree into LaTeX files. List of tuples 237 | # (source start file, target name, title, 238 | # author, documentclass [howto, manual, or own class]). 239 | latex_documents = [ 240 | ( 241 | master_doc, 242 | "djangocms-frontend.tex", 243 | "djangocms-frontend Documentation", 244 | author, 245 | "manual", 246 | ), 247 | ] 248 | 249 | # The name of an image file (relative to this directory) to place at the top of 250 | # the title page. 251 | # latex_logo = None 252 | 253 | # For "manual" documents, if this is true, then toplevel headings are parts, 254 | # not chapters. 255 | # latex_use_parts = False 256 | 257 | # If true, show page references after internal links. 258 | # latex_show_pagerefs = False 259 | 260 | # If true, show URL addresses after external links. 261 | # latex_show_urls = False 262 | 263 | # Documents to append as an appendix to all manuals. 264 | # latex_appendices = [] 265 | 266 | # If false, no module index is generated. 267 | # latex_domain_indices = True 268 | 269 | 270 | # -- Options for manual page output --------------------------------------- 271 | 272 | # One entry per manual page. List of tuples 273 | # (source start file, name, description, authors, manual section). 274 | man_pages = [ 275 | (master_doc, "djangocms-frontend", "djangocms-frontend Documentation", [author], 1) 276 | ] 277 | 278 | # If true, show URL addresses after external links. 279 | # man_show_urls = False 280 | 281 | 282 | # -- Options for Texinfo output ------------------------------------------- 283 | 284 | # Grouping the document tree into Texinfo files. List of tuples 285 | # (source start file, target name, title, author, 286 | # dir menu entry, description, category) 287 | texinfo_documents = [ 288 | ( 289 | master_doc, 290 | "djangocms-frontend", 291 | "djangocms-frontend Documentation", 292 | author, 293 | "djangocms-frontend", 294 | "One line description of project.", 295 | "Miscellaneous", 296 | ), 297 | ] 298 | 299 | # Documents to append as an appendix to all manuals. 300 | # texinfo_appendices = [] 301 | 302 | # If false, no module index is generated. 303 | # texinfo_domain_indices = True 304 | 305 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 306 | # texinfo_show_urls = 'footnote' 307 | 308 | # If true, do not generate a @detailmenu in the "Top" node's menu. 309 | # texinfo_no_detailmenu = False 310 | 311 | 312 | # Example configuration for intersphinx: refer to the Python standard library. 313 | intersphinx_mapping = {"https://docs.python.org/": None} 314 | -------------------------------------------------------------------------------- /docs/source/forms.rst: -------------------------------------------------------------------------------- 1 | ####### 2 | Forms 3 | ####### 4 | 5 | 6 | 7 | **djangocms-form-builder** supports rendering of styled forms which is part of 8 | all major frontend frameworks, like Bootstrap 5. The objective is to tightly 9 | integrate forms in the website design. djangocms-form-builder allows as many forms 10 | as you wish on one page. All forms are **ajax/xhr-based**. To this end, 11 | djangocms-form-builder extends the django CMS plugin model allowing a form plugin 12 | to receive ajax post requests. 13 | 14 | There are two different ways to manage forms with **djangocms-form-builder**: 15 | 16 | 1. **Building a form with django CMS' powerful structure board.** This is 17 | fast an easy. It integrates smoothly with other design elements, especially 18 | the grid elements allowing to design simple responsive forms. 19 | 20 | Form actions can be configured by form. Built in actions include saving the 21 | results in the database for later evaluation and mailing submitted forms to 22 | the site admins. Other form actions can be registered. 23 | 24 | If you prefer to have a central form repository, we suggest 25 | **djangocms-alias** to manage your forms centrally. Djangocms-alias becomes 26 | your form editors and forms can be placed on pages by refering to them with 27 | their alias. 28 | 29 | 2. **Registering an application-specific form with djangocms-form-builder.** If you 30 | already have forms you may register them with djangocms-form-builder and allow 31 | editors to use them in the form plugin. If you use 32 | `django-crispy-forms `_ 33 | all form layouts will be retained. If you only have simpler design 34 | requirements, **djangocms-form-builder** allows you to use fieldsets as with 35 | admin forms. 36 | 37 | ************** 38 | Building forms 39 | ************** 40 | 41 | Form plugin 42 | =========== 43 | 44 | All forms live in the Form plugin. A form plugin can be positioned everywhere 45 | except inside another form plugin. 46 | 47 | If you want to use the structure board to build your form you will have to add 48 | the form components as child plugins to a form plugin. If you have registiered 49 | an application-specific form with djangocms-form-builder you will be able to select 50 | one of the registered forms for be shown by the form plugin. (If you do both, 51 | the selected form takes precedence over the child plugins.) 52 | 53 | .. image:: screenshots/form-plugin.png 54 | :width: 720 55 | 56 | In the tab "Submit button" the name and appearance of the submit button is 57 | configured. 58 | 59 | 60 | Form fields 61 | =========== 62 | 63 | The form plugin accepts all regular plugins **plus** special plugins that 64 | represent form fields. These are: 65 | 66 | * Text 67 | * Textarea 68 | * Integer 69 | * Decimal 70 | * Boolean 71 | * Date 72 | * Time 73 | * Date and Time 74 | * Select/Choice 75 | * URL 76 | * Email 77 | 78 | Each field requires an input of then specific form. Some fields (e.g., Boolean 79 | or Select/Choice) offer options on the specific input widget. 80 | 81 | djangocms-form-builder will use framework specific widgets or fall back to standard 82 | widgets browsers offer (e.g., date picker). 83 | 84 | *************************** 85 | Using forms from other apps 86 | *************************** 87 | 88 | Other apps can register forms to be used by **djangocms-form-builder**. Once at 89 | least one form is registered, the form will appear as a option in the Form 90 | plugin. 91 | 92 | Registration is can simply be done by a decorator or function call: 93 | 94 | .. code:: python 95 | 96 | from django import forms 97 | from djangocms_form_builder import register_with_form_builder 98 | 99 | @register_with_form_builder 100 | class MyCoolForm(forms.Form): 101 | ... 102 | 103 | class MyOtherCoolForm(forms.Form): 104 | ... 105 | 106 | register_with_form_builder(MyOtherCoolForm) 107 | 108 | 109 | 110 | There are three ways **djangocms-form-builder** can render registered forms: 111 | 112 | 1. **Regular form rendering**: all fields a shown below one another. This is 113 | only advisable for very simple forms (e.g. a contact form with name, email, 114 | and text body). 115 | 116 | 2. **Adding a fieldsets argument to the form**: The ``fieldsets`` work as you 117 | know them from ``ModelAdmin``. See `Django documentation 118 | `_. 119 | This may be the most convenient way of building not-too-complex forms. 120 | **djangocms-form-builder** uses the grid system to generate the form layout. 121 | 122 | 3. **Using the third party package** `django-crispy-forms `_: 123 | If installed and the form has a property ``helper`` the form is automatically 124 | rendered using **django-crispy-forms**. Note, however, that the submit button 125 | is rendered by the plugin. Hence do not include it into the form (which is 126 | possible with **django-crispy-forms**). 127 | 128 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. 2 | djangocms-blog documentation master file, created by 3 | sphinx-quickstart on Sun Jun 5 23:27:04 2016. 4 | You can adapt this file completely to your liking, but it should at least 5 | contain the root `toctree` directive. 6 | 7 | ################################################### 8 | Welcome to djangocms-form-builder's documentation! 9 | ################################################### 10 | 11 | *********************** 12 | djangocms-form-builder 13 | *********************** 14 | 15 | **djangocms-form-builder**'s objective is to 16 | provide a set of popular frontend components independent of the 17 | currently used frontend framework such as Bootstrap, or its specific 18 | version. 19 | 20 | .. image:: ../../preview.png 21 | 22 | ************** 23 | Key features 24 | ************** 25 | 26 | - Support of `Bootstrap 5 `_. 27 | 28 | - **Separation of plugins from css framework**, i.e., no need to 29 | rebuild you site's plugin tree if css framework is changed in the 30 | future, e.g., from Bootstrap 5 to a future version. 31 | 32 | - **New link plugin** allowing to link to internal pages provided by 33 | other applications, such as `djangocms-blog 34 | `_. 35 | 36 | - **Nice and well-arranged admin frontend** of `djangocms-bootstrap4 37 | `_ 38 | 39 | - Management command to **migrate from djangocms-bootstrap4**. This 40 | command automatically migrates all djangocms-bootstrap4 plugins to 41 | djangocms-frontend. 42 | 43 | - **Extensible** within the project and with separate project (e.g., a 44 | theme app) 45 | 46 | - **Accordion** plugin and simple **forms** plugin w/ Bootstrap-styled 47 | forms on your cms page. 48 | 49 | ************* 50 | Description 51 | ************* 52 | 53 | The plugins are framework agnostic and the framework can be changed by 54 | adapting your project's settings. Also, it is designed to avoid having 55 | to rebuild your CMS plugin tree when upgrading e.g. from one version of 56 | your frontend framework to the next. 57 | 58 | django CMS Frontend uses `django-entangled 59 | `_ by Jacob Rief to avoid 60 | bloating your project's database with css framework-dependent tables. 61 | Instead all design parameters are stored in a common JSON field and 62 | future releases of improved frontend features will not require to 63 | rebuild your full plugin tree. 64 | 65 | The link plugin has been rewritten to not allow internal links to other 66 | CMS pages, but also to other django models such as, e.g., posts of 67 | `djangocms-blog `_. 68 | 69 | **djangocms-frontend** provides a set of plugins to structure your 70 | layout. This includes three basic elements 71 | 72 | The grid 73 | The grid is the basis for responsive page design. It splits the page 74 | into containers, rows and columns. Depending on the device, columns 75 | are shown next to each other (larger screens) or one below the other 76 | (smaller screens). 77 | 78 | Components 79 | Components structure information on your site by giving them an easy 80 | to grasp and easy to use look. Alerts or cards are examples of 81 | components. 82 | 83 | Forms (work in progress) 84 | Finally, djangocms-frontend lets you display forms in a nice way. 85 | Also, it handles form submit actions, validation etc. Forms can be 86 | easily structured using fieldsets known from django's admin app. But 87 | djangocms-frontend also works with third-party apps like 88 | `django-crispy-forms 89 | `_ for 90 | even more complex layouts. 91 | 92 | Contents 93 | ======== 94 | 95 | .. toctree:: 96 | :maxdepth: 3 97 | 98 | forms 99 | howto_guides 100 | reference 101 | 102 | .. toctree:: 103 | :hidden: 104 | 105 | genindex 106 | 107 | Indices and tables 108 | ================== 109 | 110 | - :ref:`genindex` 111 | - :ref:`search` 112 | -------------------------------------------------------------------------------- /docs/source/screenshots/accordion-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/accordion-example.png -------------------------------------------------------------------------------- /docs/source/screenshots/accordion-plugins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/accordion-plugins.png -------------------------------------------------------------------------------- /docs/source/screenshots/add_plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/add_plugin.png -------------------------------------------------------------------------------- /docs/source/screenshots/adv-settings-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/adv-settings-active.png -------------------------------------------------------------------------------- /docs/source/screenshots/alert-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/alert-example.png -------------------------------------------------------------------------------- /docs/source/screenshots/alert-plugins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/alert-plugins.png -------------------------------------------------------------------------------- /docs/source/screenshots/badge-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/badge-example.png -------------------------------------------------------------------------------- /docs/source/screenshots/card-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/card-example.png -------------------------------------------------------------------------------- /docs/source/screenshots/card-inner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/card-inner.png -------------------------------------------------------------------------------- /docs/source/screenshots/card-overlay-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/card-overlay-example.png -------------------------------------------------------------------------------- /docs/source/screenshots/card-plugins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/card-plugins.png -------------------------------------------------------------------------------- /docs/source/screenshots/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/card.png -------------------------------------------------------------------------------- /docs/source/screenshots/col.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/col.png -------------------------------------------------------------------------------- /docs/source/screenshots/container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/container.png -------------------------------------------------------------------------------- /docs/source/screenshots/form-plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/form-plugin.png -------------------------------------------------------------------------------- /docs/source/screenshots/row.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/row.png -------------------------------------------------------------------------------- /docs/source/screenshots/tab-error-indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/tab-error-indicator.png -------------------------------------------------------------------------------- /docs/source/screenshots/tabs-advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/tabs-advanced.png -------------------------------------------------------------------------------- /docs/source/screenshots/tabs-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/tabs-background.png -------------------------------------------------------------------------------- /docs/source/screenshots/tabs-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/tabs-main.png -------------------------------------------------------------------------------- /docs/source/screenshots/tabs-spacing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/tabs-spacing.png -------------------------------------------------------------------------------- /docs/source/screenshots/tabs-visibility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/docs/source/screenshots/tabs-visibility.png -------------------------------------------------------------------------------- /docs/source/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | ajax 2 | autosizing 3 | blockquote 4 | checkboxes 5 | ckeditor 6 | cms 7 | collapsable 8 | config 9 | css 10 | customisable 11 | dismissible 12 | django 13 | djangocms 14 | dropdown 15 | fieldsets 16 | frontend 17 | ish 18 | javascript 19 | Javascript 20 | Jumbotron 21 | jumbotron 22 | Jumbotron 23 | Jumbotrons 24 | kwargs 25 | LinkPlugin 26 | listgroup 27 | mixin 28 | mixins 29 | mkoisten 30 | nav 31 | proxied 32 | queryset 33 | Rief 34 | rtl 35 | styledlink 36 | subclasses 37 | subclassing 38 | themable 39 | un 40 | url 41 | viewport 42 | xl 43 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "djangocms-form-builder" 7 | dynamic = ["version"] 8 | description = "Adds a form editor to the structure board of django CMS." 9 | readme = "README.rst" 10 | license.text = "BSD-3-Clause" 11 | requires-python = ">=3.9" 12 | authors = [ 13 | { name = "fsbraun", email = "fsbraun@gmx.de" }, 14 | ] 15 | classifiers = [ 16 | "Development Status :: 4 - Beta", 17 | "Environment :: Web Environment", 18 | "Framework :: Django", 19 | "Framework :: Django :: 4.2", 20 | "Framework :: Django :: 5.0", 21 | "Framework :: Django :: 5.1", 22 | "Framework :: Django CMS", 23 | "Framework :: Django CMS :: 3.9", 24 | "Framework :: Django CMS :: 3.10", 25 | "Framework :: Django CMS :: 3.11", 26 | "Framework :: Django CMS :: 4.0", 27 | "Framework :: Django CMS :: 4.1", 28 | "Intended Audience :: Developers", 29 | "License :: OSI Approved :: BSD License", 30 | "Operating System :: OS Independent", 31 | "Programming Language :: Python", 32 | "Programming Language :: Python :: 3", 33 | "Programming Language :: Python :: 3.9", 34 | "Programming Language :: Python :: 3.10", 35 | "Programming Language :: Python :: 3.11", 36 | "Programming Language :: Python :: 3.12", 37 | "Topic :: Internet :: WWW/HTTP", 38 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 39 | "Topic :: Software Development", 40 | "Topic :: Software Development :: Libraries", 41 | ] 42 | dependencies = [ 43 | "django-cms>=3.10", 44 | "django-entangled", 45 | "Django>=3.2", 46 | "djangocms-attributes-field>=1", 47 | ] 48 | 49 | [project.optional-dependencies] 50 | reCaptcha = [ 51 | "django-recaptcha", 52 | ] 53 | 54 | [project.urls] 55 | Homepage = "https://github.com/fsbraun/djangocms-form-builder" 56 | 57 | [tool.hatch.version] 58 | path = "djangocms_form_builder/__init__.py" 59 | 60 | [tool.hatch.build.targets.sdist] 61 | include = [ 62 | "/djangocms_form_builder", 63 | ] 64 | -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | import django 6 | from django.conf import settings 7 | from django.test.utils import get_runner 8 | 9 | 10 | def run(): 11 | os.environ["DJANGO_SETTINGS_MODULE"] = "tests.test_settings" 12 | django.setup() 13 | TestRunner = get_runner(settings) 14 | test_runner = TestRunner() 15 | failures = test_runner.run_tests(["tests"]) 16 | sys.exit(bool(failures)) 17 | 18 | 19 | if __name__ == "__main__": 20 | run() 21 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | dictionaries=en_US,python,technical,django 3 | ignore = D203, W503 4 | select = C,E,F,W,B,B950 5 | extend-ignore = E203, E501, E731 6 | max-line-length = 119 7 | exclude = 8 | *.egg-info, 9 | .eggs, 10 | .git, 11 | .settings, 12 | .tox, 13 | build, 14 | data, 15 | dist, 16 | docs, 17 | *migrations*, 18 | requirements, 19 | tmp 20 | 21 | [isort] 22 | profile = black 23 | ;line_length = 119 24 | ;skip = manage.py, *migrations*, .tox, .eggs, data 25 | ;include_trailing_comma = true 26 | ;multi_line_output = 5 27 | ;not_skip = __init__.py 28 | ;lines_after_imports = 2 29 | ;default_section = THIRDPARTY 30 | ;sections = FUTURE, STDLIB, DJANGO, CMS, THIRDPARTY, FIRSTPARTY, LIB, LOCALFOLDER 31 | ;known_first_party = djangocms_frontend 32 | ;known_cms = cms, menus 33 | ;known_django = django 34 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-form-builder/0b037a0745d92e27085dfe0a0c8d179342fba73c/tests/__init__.py -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | from cms.api import create_page 2 | from django.apps import apps 3 | from django.contrib.contenttypes.models import ContentType 4 | from django.contrib.sites.models import Site 5 | 6 | DJANGO_CMS4 = apps.is_installed("djangocms_versioning") 7 | 8 | 9 | class TestFixture: 10 | """Sets up generic setUp and tearDown methods for tests.""" 11 | 12 | def setUp(self): 13 | self.language = "en" 14 | self.superuser = self.get_superuser() 15 | self.default_site = Site.objects.first() 16 | self.home = self.create_page( 17 | title="home", 18 | template="page.html", 19 | ) 20 | self.publish(self.home, self.language) 21 | self.page = self.create_page( 22 | title="content", 23 | template="page.html", 24 | ) 25 | self.publish(self.page, self.language) 26 | self.placeholder = self.get_placeholders(self.page).get(slot="content") 27 | self.request_url = ( 28 | self.page.get_absolute_url(self.language) + "?toolbar_off=true" 29 | ) 30 | return super().setUp() 31 | 32 | def tearDown(self): 33 | self.page.delete() 34 | self.home.delete() 35 | if DJANGO_CMS4: 36 | from djangocms_versioning.models import Version 37 | 38 | Version.objects.all().delete() 39 | self.superuser.delete() 40 | 41 | return super().tearDown() 42 | 43 | if DJANGO_CMS4: # CMS V4 44 | 45 | def _get_version(self, grouper, version_state, language=None): 46 | language = language or self.language 47 | 48 | from djangocms_versioning.models import Version 49 | 50 | versions = Version.objects.filter_by_grouper(grouper).filter( 51 | state=version_state 52 | ) 53 | for version in versions: 54 | if ( 55 | hasattr(version.content, "language") 56 | and version.content.language == language 57 | ): 58 | return version 59 | 60 | def publish(self, grouper, language=None): 61 | from djangocms_versioning.constants import DRAFT 62 | 63 | version = self._get_version(grouper, DRAFT, language) 64 | if version is not None: 65 | version.publish(self.superuser) 66 | 67 | def unpublish(self, grouper, language=None): 68 | from djangocms_versioning.constants import PUBLISHED 69 | 70 | version = self._get_version(grouper, PUBLISHED, language) 71 | if version is not None: 72 | version.unpublish(self.superuser) 73 | 74 | def create_page(self, title, **kwargs): 75 | kwargs.setdefault("language", self.language) 76 | kwargs.setdefault("created_by", self.superuser) 77 | kwargs.setdefault("in_navigation", True) 78 | kwargs.setdefault("limit_visibility_in_menu", None) 79 | kwargs.setdefault("menu_title", title) 80 | return create_page(title=title, **kwargs) 81 | 82 | def get_placeholders(self, page): 83 | return page.get_placeholders(self.language) 84 | 85 | def create_url( 86 | self, 87 | site=None, 88 | content_object=None, 89 | manual_url="", 90 | relative_path="", 91 | phone="", 92 | mailto="", 93 | anchor="", 94 | ): 95 | from djangocms_url_manager.models import Url, UrlGrouper 96 | from djangocms_url_manager.utils import is_versioning_enabled 97 | from djangocms_versioning.constants import DRAFT 98 | from djangocms_versioning.models import Version 99 | 100 | if site is None: 101 | site = self.default_site 102 | 103 | url = Url.objects.create( 104 | site=site, 105 | content_object=content_object, 106 | manual_url=manual_url, 107 | relative_path=relative_path, 108 | phone=phone, 109 | mailto=mailto, 110 | anchor=anchor, 111 | url_grouper=UrlGrouper.objects.create(), 112 | ) 113 | if is_versioning_enabled(): 114 | Version.objects.create( 115 | content=url, 116 | created_by=self.superuser, 117 | state=DRAFT, 118 | content_type_id=ContentType.objects.get_for_model(Url).id, 119 | ) 120 | 121 | return url 122 | 123 | def delete_urls(self): 124 | from djangocms_url_manager.models import Url 125 | 126 | Url.objects.all().delete() 127 | 128 | else: # CMS V3 129 | 130 | def publish(self, page, language=None): 131 | page.publish(language) 132 | 133 | def unpublish(self, page, language=None): 134 | page.unpublish(language) 135 | 136 | def create_page(self, title, **kwargs): 137 | kwargs.setdefault("language", self.language) 138 | kwargs.setdefault("menu_title", title) 139 | return create_page(title=title, **kwargs) 140 | 141 | def get_placeholders(self, page): 142 | return page.get_placeholders() 143 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | from tempfile import mkdtemp 3 | 4 | from django.core.files import File 5 | from filer.models.filemodels import File as FilerFile 6 | from filer.models.foldermodels import Folder as FilerFolder 7 | from filer.models.imagemodels import Image as FilerImage 8 | from filer.utils.compatibility import PILImage, PILImageDraw 9 | 10 | 11 | # from https://github.com/divio/django-filer/blob/develop/tests/helpers.py#L46-L52 12 | def create_image(mode="RGB", size=(800, 600)): 13 | """ 14 | Creates a usable image file using PIL 15 | :returns: PIL Image instance 16 | """ 17 | image = PILImage.new(mode, size) 18 | draw = PILImageDraw.Draw(image) 19 | x_bit, y_bit = size[0] // 10, size[1] // 10 20 | draw.rectangle((x_bit, y_bit * 2, x_bit * 7, y_bit * 3), "red") 21 | draw.rectangle((x_bit * 2, y_bit, x_bit * 3, y_bit * 8), "red") 22 | 23 | return image 24 | 25 | 26 | def get_image(image_name="test_file.jpg"): 27 | """ 28 | Creates and stores an image to the file system using PILImage 29 | 30 | :param image_name: the name for the file (default "test_file.jpg") 31 | :returns: dict {name, image, path} 32 | """ 33 | image = create_image() 34 | image_path = os.path.join( 35 | mkdtemp(), 36 | image_name, 37 | ) 38 | image.save(image_path, "JPEG") 39 | 40 | return { 41 | "name": image_name, 42 | "image": image, 43 | "path": image_path, 44 | } 45 | 46 | 47 | def get_file(file_name="test_file.pdf"): 48 | """ 49 | Creates and stores an arbitrary file into a temporary dir 50 | 51 | :param file_name: the name for the file (default "test_file.pdf") 52 | :returns: dict {name, image, path} 53 | """ 54 | file_path = os.path.join( 55 | mkdtemp(), 56 | file_name, 57 | ) 58 | data = open(file_path, "a") 59 | 60 | return { 61 | "name": file_name, 62 | "file": data, 63 | "path": file_path, 64 | } 65 | 66 | 67 | def get_filer_image(image_name="test_file.jpg", name="", original_filename=True): 68 | """ 69 | Creates and stores an image to filer and returns it 70 | 71 | :param image_name: the name for the file (default "test_file.jpg") 72 | :returns: filer image instance 73 | """ 74 | image = get_image(image_name) 75 | filename = None 76 | if original_filename: 77 | filename = image.get("name") 78 | filer_file = File( 79 | open(image.get("path"), "rb"), 80 | name=image.get("name"), 81 | ) 82 | filer_object = FilerImage.objects.create( 83 | original_filename=filename, 84 | file=filer_file, 85 | name=name, 86 | ) 87 | 88 | return filer_object 89 | 90 | 91 | def get_filer_file(file_name="test_file.pdf", folder=None): 92 | """ 93 | Creates and stores a file to filer and returns it 94 | 95 | :param file_name: the name for the file (default "test_file.pdf") 96 | :param folder: optionally provide a folder instance 97 | :returns: filer file instance 98 | """ 99 | data = get_file(file_name) 100 | filer_file = File( 101 | open(data.get("path"), "rb"), 102 | name=data.get("name"), 103 | ) 104 | filer_object = FilerFile.objects.create( 105 | original_filename=data.get("name"), 106 | file=filer_file, 107 | folder=folder, 108 | ) 109 | 110 | return filer_object 111 | 112 | 113 | def get_filer_folder(folder_name="test_folder", parent=None): 114 | """ 115 | Creates and returns a filer folder 116 | 117 | :param folder_name: the name of the folder to be used (default "test_folder") 118 | :param parent: optionally provide a parent folder 119 | :returns: filer folder instance 120 | """ 121 | filer_object = FilerFolder.objects.create( 122 | parent=parent, 123 | name=folder_name, 124 | ) 125 | 126 | return filer_object 127 | -------------------------------------------------------------------------------- /tests/requirements/base.txt: -------------------------------------------------------------------------------- 1 | django-filer 2 | easy-thumbnails 3 | djangocms-text-ckeditor 4 | coverage 5 | django-app-helper 6 | django-treebeard<4.5 7 | tox 8 | coverage 9 | isort 10 | flake8 11 | pyflakes>=2.1 12 | wheel 13 | black 14 | pre-commit 15 | -------------------------------------------------------------------------------- /tests/requirements/dj32_cms310.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Django>=3.2,<3.3 4 | django-cms>=3.10,<3.11 5 | -------------------------------------------------------------------------------- /tests/requirements/dj32_cms311.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Django>=3.2,<3.3 4 | django-cms>=3.11,<3.12 5 | -------------------------------------------------------------------------------- /tests/requirements/dj42_cms311.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | django-filer>=2.2 4 | Django>=4.2,<4.3 5 | django-cms>=3.11,<3.12 6 | -------------------------------------------------------------------------------- /tests/requirements/dj42_cms41.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Django>=4.2,<4.3 4 | django-cms>=4.1,<4.2 5 | djangocms-text 6 | djangocms-versioning 7 | -------------------------------------------------------------------------------- /tests/requirements/dj50_cms41.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Django>=5.0,<5.1 4 | django-cms>=4.1,<4.2 5 | djangocms-text 6 | djangocms-versioning 7 | -------------------------------------------------------------------------------- /tests/requirements/dj51_cms41.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Django>=5.1,<5.2 4 | django-cms>=4.1,<4.2 5 | djangocms-text 6 | djangocms-versioning 7 | -------------------------------------------------------------------------------- /tests/templates/page.html: -------------------------------------------------------------------------------- 1 | {% load cms_tags %}{% placeholder "content" %} 2 | -------------------------------------------------------------------------------- /tests/test_actions.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from cms.api import add_plugin 4 | from cms.test_utils.testcases import CMSTestCase 5 | 6 | from djangocms_form_builder.actions import get_registered_actions 7 | 8 | from .fixtures import TestFixture 9 | 10 | 11 | class ActionTestCase(TestFixture, CMSTestCase): 12 | def setUp(self): 13 | super().setUp() 14 | self.actions = get_registered_actions() 15 | self.save_action = [key for key, value in self.actions if value == "Save form submission"][0] 16 | self.send_mail_action = [key for key, value in self.actions if value == "Send email"][0] 17 | 18 | def test_send_mail_action(self): 19 | plugin_instance = add_plugin( 20 | placeholder=self.placeholder, 21 | plugin_type="FormPlugin", 22 | language=self.language, 23 | form_name="test_form", 24 | ) 25 | plugin_instance.action_parameters = {"sendemail_recipients": "a@b.c d@e.f", "sendemail_template": "default"} 26 | plugin_instance.form_actions = f"[\"{self.send_mail_action}\"]" 27 | plugin_instance.save() 28 | 29 | child_plugin = add_plugin( 30 | placeholder=self.placeholder, 31 | plugin_type="CharFieldPlugin", 32 | language=self.language, 33 | target=plugin_instance, 34 | config={"field_name": "field1"} 35 | ) 36 | child_plugin.save() 37 | plugin_instance.child_plugin_instances = [child_plugin] 38 | child_plugin.child_plugin_instances = [] 39 | 40 | plugin = plugin_instance.get_plugin_class_instance() 41 | plugin.instance = plugin_instance 42 | 43 | # Simulate form submission 44 | with patch("django.core.mail.send_mail") as mock_send_mail: 45 | form = plugin.get_form_class()({}, request=self.get_request("/")) 46 | form.cleaned_data = {"field1": "value1", "field2": "value2"} 47 | form.save() 48 | 49 | # Validate send_mail call 50 | mock_send_mail.assert_called_once() 51 | args, kwargs = mock_send_mail.call_args 52 | self.assertEqual(args[0], 'Test form form submission') 53 | self.assertIn('Form submission', args[1]) 54 | self.assertEqual(args[3], ['a@b.c', 'd@e.f']) 55 | 56 | # Test with no recipients 57 | plugin_instance.action_parameters = {"sendemail_recipients": "", "sendemail_template": "default"} 58 | plugin_instance.save() 59 | 60 | with patch("django.core.mail.mail_admins") as mock_mail_admins: 61 | form = plugin.get_form_class()({}, request=self.get_request("/")) 62 | form.cleaned_data = {"field1": "value1", "field2": "value2"} 63 | form.save() 64 | 65 | # Validate mail_admins call 66 | mock_mail_admins.assert_called_once() 67 | args, kwargs = mock_mail_admins.call_args 68 | self.assertEqual(args[0], 'Test form form submission') 69 | self.assertIn('Form submission', args[1]) 70 | -------------------------------------------------------------------------------- /tests/test_app/cms_plugins.py: -------------------------------------------------------------------------------- 1 | from cms.plugin_base import CMSPluginBase 2 | from cms.plugin_pool import plugin_pool 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | 6 | @plugin_pool.register_plugin 7 | class ContainerPlugin(CMSPluginBase): 8 | name = _("Container") 9 | render_template = "test_app/container.html" 10 | allow_children = True 11 | -------------------------------------------------------------------------------- /tests/test_app/templates/test_app/container.html: -------------------------------------------------------------------------------- 1 | {% load cms_tags %} 2 |
3 | {% for plugin in instance.child_plugin_instances %} 4 | {% render_plugin plugin %} 5 | {% endfor %} 6 |
7 | -------------------------------------------------------------------------------- /tests/test_fields.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from djangocms_form_builder.fields import AttributesField, TagTypeField 4 | 5 | 6 | class FieldsTestCase(TestCase): 7 | def test_attributes_field(self): 8 | field = AttributesField() 9 | self.assertEqual(field.verbose_name, "Attributes") 10 | self.assertEqual(field.blank, True) 11 | 12 | def test_tag_type_field(self): 13 | field = TagTypeField() 14 | self.assertEqual(field.verbose_name, "Tag type") 15 | self.assertEqual( 16 | list(field.choices), 17 | list((("div", "div"),)), 18 | ) 19 | self.assertEqual(field.default, "div") 20 | self.assertEqual(field.max_length, 255) 21 | self.assertEqual( 22 | field.help_text, 23 | "Select the HTML tag to be used.", 24 | ) 25 | -------------------------------------------------------------------------------- /tests/test_form_registry.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.test import TestCase 3 | 4 | from djangocms_form_builder.forms import FormsForm 5 | 6 | 7 | class MyTestForm(forms.Form): 8 | field = forms.TextInput() 9 | 10 | 11 | class NamedForm(MyTestForm): 12 | class Meta: 13 | verbose_name = "This form has a custom name" 14 | 15 | 16 | class TestRegistry(TestCase): 17 | def test_registry(self): 18 | from djangocms_form_builder import ( 19 | get_registered_forms, 20 | register_with_form_builder, 21 | ) 22 | 23 | registered_forms = get_registered_forms() 24 | # no forms registered yet 25 | self.assertEqual(registered_forms[0][0], "No forms registered") 26 | forms_form = FormsForm() 27 | # No forms to select: hide field in form plugin admin form 28 | self.assertIsInstance(forms_form.fields["form_selection"].widget, forms.HiddenInput) 29 | 30 | register_with_form_builder(MyTestForm) 31 | 32 | registered_forms = get_registered_forms() 33 | self.assertEqual(len(registered_forms), 1) # one form 34 | self.assertEqual(registered_forms[0][1], "My Test Form") # derived from the class name 35 | forms_form = FormsForm() 36 | # Form registered: Select widget in form plugin admin form 37 | self.assertIsInstance(forms_form.fields["form_selection"].widget, forms.Select) 38 | 39 | register_with_form_builder(NamedForm) 40 | 41 | registered_forms = get_registered_forms() 42 | self.assertEqual(len(registered_forms), 2) # second form 43 | self.assertEqual(registered_forms[1][1], "This form has a custom name") # attribute-driven 44 | -------------------------------------------------------------------------------- /tests/test_formeditor.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from cms.api import add_plugin 4 | from cms.test_utils.testcases import CMSTestCase 5 | 6 | from djangocms_form_builder import cms_plugins 7 | from djangocms_form_builder.cms_plugins.form_plugins import FormElementPlugin 8 | from tests.test_app.cms_plugins import ContainerPlugin 9 | 10 | from .fixtures import TestFixture 11 | 12 | 13 | class FormEditorTestCase(TestFixture, CMSTestCase): 14 | def test_form_editor(self): 15 | form = add_plugin( 16 | placeholder=self.placeholder, 17 | plugin_type=cms_plugins.FormPlugin.__name__, 18 | language=self.language, 19 | form_selection="", 20 | form_name="my-test-form", 21 | ) 22 | 23 | for item, cls in cms_plugins.__dict__.items(): 24 | if ( 25 | inspect.isclass(cls) 26 | and issubclass(cls, FormElementPlugin) 27 | and not issubclass(cls, cms_plugins.ChoicePlugin) 28 | and cls is not cms_plugins.SubmitPlugin 29 | ): 30 | field = add_plugin( 31 | placeholder=self.placeholder, 32 | plugin_type=cls.__name__, 33 | target=form, 34 | language=self.language, 35 | config=dict( 36 | field_name="field_" + item, 37 | ), 38 | ) 39 | field.initialize_from_form() 40 | 41 | self.publish(self.page, self.language) 42 | 43 | with self.login_user_context(self.superuser): 44 | response = self.client.get(self.request_url) 45 | self.assertEqual(response.status_code, 200) 46 | self.assertContains(response, 'action="/@form-builder/1"') 47 | self.assertContains(response, '") 12 | instance.form_name = "my-test-form" 13 | self.assertEqual(instance.get_short_description(), "(my-test-form)") 14 | 15 | entry = FormEntry.objects.create(form_name=instance.form_name) 16 | self.assertEqual(str(entry), "my-test-form (1)") 17 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from cms.utils.compat import DJANGO_3_1 4 | 5 | 6 | class DisableMigrations(dict): 7 | def __contains__(self, item): 8 | return True 9 | 10 | def __getitem__(self, item): 11 | return None 12 | 13 | 14 | MIGRATION_MODULES = DisableMigrations() 15 | 16 | INSTALLED_APPS = [ 17 | "django.contrib.contenttypes", 18 | "django.contrib.auth", 19 | "django.contrib.sites", 20 | "django.contrib.sessions", 21 | "django.contrib.admin", 22 | "django.contrib.messages", 23 | "easy_thumbnails", 24 | "filer", 25 | "cms", 26 | "menus", 27 | "treebeard", 28 | "djangocms_text_ckeditor", 29 | "djangocms_form_builder", 30 | "sekizai", 31 | "tests.test_app", 32 | ] 33 | 34 | if DJANGO_3_1: 35 | INSTALLED_APPS += ["django_jsonfield_backport"] 36 | 37 | try: # V4 test? 38 | import djangocms_versioning # noqa 39 | 40 | INSTALLED_APPS += [ 41 | "djangocms_versioning", 42 | ] 43 | CMS_CONFIRM_VERSION4 = True 44 | except ImportError: # Nope 45 | pass 46 | 47 | MIDDLEWARE = [ 48 | "django.contrib.sessions.middleware.SessionMiddleware", 49 | "django.contrib.auth.middleware.AuthenticationMiddleware", 50 | "django.contrib.messages.middleware.MessageMiddleware", 51 | "cms.middleware.user.CurrentUserMiddleware", 52 | "cms.middleware.page.CurrentPageMiddleware", 53 | "cms.middleware.toolbar.ToolbarMiddleware", 54 | "cms.middleware.language.LanguageCookieMiddleware", 55 | ] 56 | CMS_LANGUAGES = { 57 | 1: [ 58 | { 59 | "code": "en", 60 | "name": "English", 61 | } 62 | ] 63 | } 64 | 65 | # required otherwise subject_location would throw an error in the template 66 | THUMBNAIL_PROCESSORS = ( 67 | "easy_thumbnails.processors.colorspace", 68 | "easy_thumbnails.processors.autocrop", 69 | "filer.thumbnail_processors.scale_and_crop_with_subject_location", 70 | "easy_thumbnails.processors.filters", 71 | ) 72 | 73 | LANGUAGE_CODE = "en" 74 | ALLOWED_HOSTS = ["localhost"] 75 | DJANGOCMS_PICTURE_RESPONSIVE_IMAGES = False 76 | DJANGOCMS_PICTURE_RESPONSIVE_IMAGES_VIEWPORT_BREAKPOINTS = [576, 768, 992] 77 | 78 | SECRET_KEY = "fake-key" 79 | 80 | TEMPLATES = [ 81 | { 82 | "BACKEND": "django.template.backends.django.DjangoTemplates", 83 | "DIRS": [ 84 | os.path.join(os.path.dirname(__file__), "templates"), 85 | # insert your TEMPLATE_DIRS here 86 | ], 87 | "APP_DIRS": True, 88 | "OPTIONS": { 89 | "context_processors": [ 90 | "django.template.context_processors.debug", 91 | "django.template.context_processors.request", 92 | "django.contrib.auth.context_processors.auth", 93 | "django.contrib.messages.context_processors.messages", 94 | ], 95 | }, 96 | }, 97 | ] 98 | 99 | DATABASES = { 100 | "default": { 101 | "ENGINE": "django.db.backends.sqlite3", 102 | "NAME": "mydatabase", # This is where you put the name of the db file. 103 | # If one doesn't exist, it will be created at migration time. 104 | } 105 | } 106 | 107 | CMS_TEMPLATES = (("page.html", "Page"),) 108 | 109 | SITE_ID = 1 110 | 111 | ROOT_URLCONF = "tests.urls" 112 | 113 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 114 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | urlpatterns = [ 5 | path("admin/", admin.site.urls), 6 | path("", include("cms.urls")), 7 | ] 8 | --------------------------------------------------------------------------------