├── .editorconfig ├── .github ├── dependabot.yml ├── stale.yml └── workflows │ ├── dependabot-auto-approve-and-merge.yml │ └── test.yml ├── .gitignore ├── .readthedocs.yaml ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── changelog.md ├── conf.py ├── example_template.rst ├── index.rst ├── installation.rst ├── quickstart.rst ├── settings.rst ├── templates.rst └── templatetags.rst ├── example ├── app │ ├── __init__.py │ ├── forms.py │ ├── templates │ │ └── app │ │ │ ├── base.html │ │ │ ├── bootstrap.html │ │ │ ├── form.html │ │ │ ├── form_by_field.html │ │ │ ├── form_horizontal.html │ │ │ ├── form_inline.html │ │ │ ├── form_with_files.html │ │ │ ├── formset.html │ │ │ ├── home.html │ │ │ ├── misc.html │ │ │ └── pagination.html │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── manage.py └── settings.py ├── justfile ├── manage.py ├── pyproject.toml ├── src └── bootstrap3 │ ├── __about__.py │ ├── __init__.py │ ├── bootstrap.py │ ├── components.py │ ├── exceptions.py │ ├── forms.py │ ├── models.py │ ├── renderers.py │ ├── templates │ └── bootstrap3 │ │ ├── bootstrap3.html │ │ ├── field_error.html │ │ ├── field_help_text.html │ │ ├── field_help_text_and_errors.html │ │ ├── form_errors.html │ │ ├── messages.html │ │ └── pagination.html │ ├── templatetags │ ├── __init__.py │ └── bootstrap3.py │ ├── text.py │ └── utils.py ├── tests ├── __init__.py ├── app │ ├── __init__.py │ ├── forms.py │ ├── settings.py │ └── urls.py ├── test_settings.py ├── test_templates.py ├── test_templatetags.py └── test_version.py ├── tox.ini └── uv.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | 13 | [*.py] 14 | max_line_length = 88 15 | 16 | [*.toml] 17 | indent_size = 2 18 | 19 | [docs/**.rst] 20 | max_line_length = 79 21 | 22 | [*.{yaml,yml}] 23 | indent_size = 2 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration 2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | 7 | # Maintain dependencies for GitHub Actions 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | 13 | # Maintain dependencies for pip 14 | - package-ecosystem: "pip" 15 | directory: "/" 16 | schedule: 17 | interval: "daily" 18 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - accepted 10 | # Label to use when marking an issue as stale 11 | staleLabel: wontfix 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. Thank you 16 | for your contributions. 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: false 19 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-approve-and-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot PR Approve and Merge 2 | # Original: https://blog.somewhatabstract.com/2021/10/11/setting-up-dependabot-with-github-actions-to-approve-and-merge/?utm_source=pocket_mylist 3 | # See also: https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/automating-dependabot-with-github-actions#approve-a-pull-request 4 | 5 | on: pull_request_target 6 | 7 | permissions: 8 | pull-requests: write 9 | contents: write 10 | 11 | jobs: 12 | dependabot: 13 | runs-on: ubuntu-latest 14 | # Check the actor, only run for Dependabot PRs, prevent failing on non-Dependabot PRs. 15 | if: ${{ github.actor == 'dependabot[bot]' }} 16 | steps: 17 | # This step will fail (without approval) if there's no metadata. 18 | - name: Dependabot metadata 19 | id: dependabot-metadata 20 | uses: dependabot/fetch-metadata@v2.4.0 21 | with: 22 | github-token: "${{ secrets.GITHUB_TOKEN }}" 23 | # Approve the PR. 24 | - name: Approve a PR 25 | run: gh pr review --approve "$PR_URL" 26 | env: 27 | PR_URL: ${{ github.event.pull_request.html_url }} 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | # Allow auto-merging for patch and minor updates if all checks pass. 30 | - name: Enable auto-merge for Dependabot PRs 31 | if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }} 32 | run: gh pr merge --auto --squash "$PR_URL" 33 | env: 34 | PR_URL: ${{ github.event.pull_request.html_url }} 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | concurrency: 9 | group: test-${{ github.head_ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | ruff: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: chartboost/ruff-action@v1 18 | 19 | tests_matrix: 20 | runs-on: ubuntu-latest 21 | needs: [ ruff ] 22 | strategy: 23 | matrix: 24 | python-version: [3.9, "3.10", 3.11, 3.12, 3.13] 25 | django-version: [4.2, 5.1, 5.2, "main"] 26 | exclude: 27 | # Django 5.1 28 | - python-version: 3.9 29 | django-version: 5.1 30 | 31 | # Django 5.2 32 | - python-version: 3.9 33 | django-version: 5.2 34 | 35 | # Django main (6.0) 36 | - python-version: 3.9 37 | django-version: "main" 38 | - python-version: "3.10" 39 | django-version: "main" 40 | - python-version: 3.11 41 | django-version: "main" 42 | 43 | steps: 44 | - uses: actions/checkout@v4 45 | 46 | - name: Update repositories 47 | run: sudo apt-get update 48 | 49 | - name: Install GDAL binaries 50 | run: sudo apt-get install binutils libproj-dev gdal-bin 51 | 52 | - name: Set up Python with uv 53 | uses: drivendataorg/setup-python-uv-action@main 54 | with: 55 | python-version: ${{ matrix.python-version }} 56 | cache: packages 57 | cache-dependency-path: >- 58 | pyproject.toml 59 | 60 | - name: Install dependencies 61 | run: uv sync --all-extras --all-groups --upgrade 62 | 63 | - name: Install Django ${{ matrix.django-version }} 64 | run: uv pip install Django==${{ matrix.django-version }} 65 | if: matrix.django-version != 'main' 66 | - name: Install Django main branch 67 | run: uv pip install -U https://github.com/django/django/archive/master.tar.gz 68 | if: matrix.django-version == 'main' 69 | 70 | - name: Run tests 71 | run: | 72 | uv run coverage run manage.py test 73 | uv run coverage report 74 | 75 | - name: Upload coveralls (parallel) 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | COVERALLS_PARALLEL: true 79 | run: | 80 | uv pip install coveralls 81 | uv run coveralls --service=github 82 | 83 | 84 | docs: 85 | runs-on: ubuntu-latest 86 | needs: [ tests_matrix ] 87 | steps: 88 | - uses: actions/checkout@v4 89 | 90 | - name: Set up Python with uv 91 | uses: drivendataorg/setup-python-uv-action@main 92 | with: 93 | python-version: 3.12 94 | cache: packages 95 | cache-dependency-path: >- 96 | pyproject.toml 97 | 98 | - name: Set up just 99 | uses: extractions/setup-just@v3 100 | 101 | - name: Build documentation 102 | run: just docs 103 | 104 | build: 105 | runs-on: ubuntu-latest 106 | needs: [ tests_matrix ] 107 | steps: 108 | - uses: actions/checkout@v4 109 | 110 | - name: Set up Python with uv 111 | uses: drivendataorg/setup-python-uv-action@main 112 | with: 113 | python-version: 3.12 114 | cache: packages 115 | cache-dependency-path: >- 116 | pyproject.toml 117 | 118 | - name: Set up just 119 | uses: extractions/setup-just@v3 120 | 121 | - name: Build package 122 | run: just build 123 | 124 | tests: 125 | if: always() 126 | runs-on: ubuntu-latest 127 | needs: [ tests_matrix, ruff, docs, build ] 128 | steps: 129 | - name: Check tests matrix status 130 | if: needs.tests_matrix.result != 'success' 131 | run: exit 1 132 | - name: Finish parallel build 133 | uses: coverallsapp/github-action@v2 134 | with: 135 | parallel-finished: true 136 | github-token: ${{ secrets.GITHUB_TOKEN }} 137 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Editors 3 | .vscode/ 4 | .idea/ 5 | 6 | # Local development 7 | .python-version 8 | .env* 9 | .venv 10 | 11 | # Testing 12 | .coverage* 13 | .tox/ 14 | 15 | # Vagrant 16 | .vagrant/ 17 | 18 | # Mac/OSX 19 | .DS_Store 20 | 21 | # Windows 22 | Thumbs.db 23 | 24 | # Byte-compiled / optimized / DLL files 25 | __pycache__/ 26 | *.py[cod] 27 | *$py.class 28 | 29 | # Distribution / packaging 30 | *.egg-info 31 | docs/_build/ 32 | dist/ 33 | build/ 34 | build-check-description/ 35 | 36 | # Example database 37 | example/db.sqlite3 38 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the version of Python and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | commands: 13 | - asdf plugin add uv 14 | - asdf install uv latest 15 | - asdf global uv latest 16 | - uv sync --only-group docs --frozen 17 | - uv run -m sphinx -T -b html -d docs/_build/doctrees -D language=en docs $READTHEDOCS_OUTPUT/html 18 | 19 | # Build documentation in the docs/ directory with Sphinx 20 | sphinx: 21 | configuration: docs/conf.py 22 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | This application is developed and maintained by `Zostera `_. 4 | 5 | ## Original author 6 | 7 | * Dylan Verheul 8 | 9 | ## Contributors 10 | 11 | * Allard Stijnman 12 | * Austin Whittier 13 | * Caio Ariede 14 | * Fabio C. Barrionuevo da Luz 15 | * Fabio Perfetti 16 | * Irving Ckam 17 | * Jay Pipes 18 | * Jieter Waagmeester 19 | * Jonas Hagstedt 20 | * Jordan Starcher 21 | * Juan Carlos 22 | * Markus Holtermann 23 | * Martin Koistinen 24 | * Nick S 25 | * Owais Lone 26 | * pmav99 27 | * Richard Hajdu 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## In development 4 | 5 | - Add support for Django 5.2 (#). 6 | - Symlink CHANGELOG.md into docs for Sphinx (#1076). 7 | - Drop support for Django 5.0 (EOL) (#1075). 8 | 9 | ## 25.1 (2025-02-28) 10 | 11 | - Add support for Python 3.13 (#1064, #1065). 12 | - Use uv and just for package management (#1064). 13 | - Drop support for Python 3.8 (EOL) (#1061). 14 | 15 | ## 24.3 (2024-09-18) 16 | 17 | - Add support for Django 5.1 (#1013). 18 | 19 | ## 24.2 (2024-04-17) 20 | 21 | - Reinstate setuptools_scm for build (#965). 22 | 23 | ## 24.1 (2024-04-16) 24 | 25 | - Remove support for Django 3.2 (EOL) (#962). 26 | - Remove setuptools_scm (#961). 27 | - Fix Read the Docs (#958). 28 | 29 | ## 23.6 (2023-12-28) 30 | 31 | - Use setuptools_scm to build package content (#920). 32 | 33 | ## 23.5 (2023-12-24) 34 | 35 | - Fix Django versions in test matrix (#900). 36 | - Use ruff instead of black for formatting (#901). 37 | - Add support for Python 3.12 (#905). 38 | - Add support for Django 5.0 (#904, #906). 39 | - Revert packaging tools to setuptools, build, tox and twine (#908). 40 | 41 | ## 23.4 (2023-06-28) 42 | 43 | - Fix inline form spacing (#892). 44 | 45 | ## 23.3 (2023-06-27) 46 | 47 | - Fix example (#886). 48 | - Remove support for Python 3.7 (EOL) (#889). 49 | - Fix radio buttons in Django 4 (#887). 50 | - Fix check order of CheckboxSelectMultiple and RadioSelect (#859). 51 | 52 | ## 23.2 (2023-06-08) 53 | 54 | - Switch to Hatch (#880). 55 | - Reinstate coveralls (#880). 56 | - Fix readthedocs config (#844). 57 | - Remove version restriction on importlib-metadata (#843). 58 | - Replace m2r2 with sphinx-mdinclude (#842). 59 | - Update packaging, reduce dependencies (#849). 60 | - Drop support for Django 4.0 (#849). 61 | - Fix example (#853). 62 | 63 | ## 23.1 (2023-04-02) 64 | 65 | - Add support for Django 4.2 (#828). 66 | - Update requirements and packages (#828). 67 | - Stop using coveralls (#829). 68 | 69 | ## 22.2 (2022-11-22) 70 | 71 | - Add support Python 3.11 (#775). 72 | 73 | ## 22.1 (2022-08-08) 74 | 75 | - Add support for Django 4.1 (#718). 76 | - Drop support for Django 2.2 (EOL) (#718). 77 | 78 | ## 21.2 (2021-12-27) 79 | 80 | - Drop support for Django 3.1 (EOL, #632). 81 | - Drop support for Python 3.6 (EOL, #632). 82 | - Fix CI (#632). 83 | 84 | ## 21.1 (2021-11-03) 85 | 86 | - Switch to a [CalVer](https://calver.org) YY.MINOR versioning scheme. MINOR is the number of the release in the given year. This is the first release in 2021 using this scheme, so its version is 21.1. The next version this year will be 21.2. The first version in 2022 will be 22.1. 87 | - Add support for Django 4 and Python 3.10 (#579). 88 | 89 | ## 15.0.0 (2021-04-10) 90 | 91 | - Drop support for Django 3.0, extended support stopped on 2021-04-01). 92 | - Add support for Django 3.2. 93 | - Fix `render_alert` (#488) 94 | - Rename AUTHORS.md to AUTHORS, remove authors section from documentation. 95 | - Revert to setuptools for packaging. 96 | - Add docs and tests to sdist (#494). 97 | - Use GitHub Actions for CI. 98 | 99 | ## 14.2.0 (2020-10-13) 100 | 101 | - Reformat CHANGELOG. 102 | - Fix Django 3.1 warning in test app settings. 103 | - Update black. 104 | - Replace m2r with m2r2 to support Sphinx3. 105 | - Add Python 3.9 to test matrix. 106 | 107 | ## 14.1.0 (2020-07-02) 108 | 109 | - Fix coveralls. 110 | - Explicitly support Django 3.1 in tox matrix. 111 | 112 | ## 14.0.0 (2020-06-22) 113 | 114 | - Drop support for Python 3.5 and Django 2.1. 115 | - Use Poetry () for dependency management and packaging. 116 | - Change documentation to support main branch rename to 'main'. 117 | - Fix settings override bug (fixes #388). 118 | - Use Markdown for README. 119 | - Fix Travis, ReadTheDocs and tox configurations. 120 | - Update Makefile with lessons learned from other packages. 121 | 122 | ## 12.1.0 (2020-05-01) 123 | 124 | - Distinguish between help text and errors (fixes #479) 125 | 126 | ## 12.0.3 (2019-12-21) 127 | 128 | - Update changelog 129 | 130 | ## 12.0.2 (2019-12-21) 131 | 132 | - Revert of #453, which turned out to break checkboxes (fixes #467) 133 | - Update requirements and fix `make docs` 134 | - Replace `force_text` with `force_str`, removes warnings 135 | 136 | ## 12.0.1 (2019-12-12) 137 | 138 | - Reinstate ``bootstrap3.__version__`` (fixes #486) 139 | - Update Makefile, travis and tox configuration (#470) 140 | 141 | ## 12.0.0 (2019-12-04) 142 | 143 | - Drop support for Python 2.7, Django 1.11 and Django 2.0 (#456) 144 | - Fix Deprecationwarning in Python 3.7 (#455) 145 | - Add label class support to form field checkboxes (#453) 146 | - Move development tasks from `setup.py` to `Makefile` 147 | - Fix compatibility with Django 3.0 and master 148 | - Add Django 3.0 to `tox.ini` 149 | - Update versions in `requirements.txt` 150 | - Use Makefile for common tasks 151 | - Drop `MANIFEST.in`, use `setuptools_scm` 152 | - Drop `_version.py`, use version from git tag 153 | 154 | ## 11.1.0 (2019-08-09) 155 | 156 | - Update Bootstrap to 3.4.1 (#459) 157 | - **NOTE** Version 12 will drop support for Python 2.x.x and Django 1.x.x 158 | 159 | ## 11.0.0 (2018-08-30) 160 | 161 | - Support `crossorigin` and `integrity` in urls (#443) 162 | - Switch to explicit Travis tests (#444) 163 | - Fix PyPI classifiers 164 | - Remove obsolete code for Django <= 1.8 (#446) 165 | - Remove obsolete settings `set_required` and `set_disabled` (#445) 166 | - Remove setting `base_url` (#443) 167 | 168 | ## 10.0.1 (2018-05-02) 169 | 170 | - Fix PyPI classifiers 171 | 172 | ## 10.0.0 (2018-05-01) 173 | 174 | - Drop support for Django 1.8 (#434) 175 | - Fix bug in demo app (#430) 176 | - Remove unnecessary `len` call (#424) 177 | - Switched to master as main branch, deleted other branches 178 | - Switched to twine for publication on PyPI 179 | 180 | ## 9.1.0 (2017-10-27) 181 | 182 | - Mention django-bootstrap4 () in README 183 | - Rewrite `tox` test matrix to focus on Django releases rather than Python versions 184 | - Add tests for Django master branch (>= 2) 185 | - Add `label` override for `{% bootstrap_field %}` 186 | 187 | ## 9.0.0 (2017-07-11) 188 | 189 | - Renamed requirements-dev.txt back to requirements.txt because that suits ReadTheDocs better 190 | - Added `error_types` support on `bootstrap3_form` (thanks @mkoistinen and @ickam) 191 | - **BREAKING** Default setting of `error_types` to `non_field_errors` is different from behavior in versions < 9 192 | 193 | ## 8.2.3 (2017-05-05) 194 | 195 | - Renamed requirements.txt to requirements-dev.txt 196 | - Tweaks to tests and CI (see #400) 197 | - Prepared test for geometry fields (disabled, blocked by Django update, see #392) 198 | - Bug fixes for add ons and placeholders (thanks @jaimesanz, @cybojenix and @marc-gist) 199 | - Improve documentation for pagination with GET parameters (thanks @nspo) 200 | - Add unicode test for help_text 201 | - Removed tests for Python 3.2 from tox and Travis CI (no longer supported by Django 1.8) 202 | 203 | ## 8.2.2 (2017-04-03) 204 | 205 | - Fix invalid HTML in help texts (thanks @luksen) 206 | - Added `mark_safe` to placeholder (thanks @ppo) 207 | - Fix DateWidget import for newer Django versions (thanks @clokep) 208 | 209 | ## 8.2.1 (2017-02-23) 210 | 211 | - Support for local languages in `url_replace_param` on Python 2 (#362, thanks @aamalev) 212 | - Correct checking Mapping instance (#363, thanks @aamalev) 213 | - Fix Django 1.11 import bug (see #369) 214 | - Add Django 1.11 and Python 3.6 to tests 215 | - Fix sdist issue with .pyc files 216 | 217 | ## 8.1.0 (2017-01-12) 218 | 219 | - Rolled back subresource integrity (see #353) 220 | - Documentation fix (thanks @clokep) 221 | 222 | ## 8.0.0 (2017-01-06) 223 | 224 | - **BREAKING** For Django >= 1.10 Remove everything to do with setting HTML attributes `required` (#337) and `disabled` (#345) 225 | - Add `id` parameter to bootstrap_button (#214) 226 | - Add `set_placeholder` to field and form renderers (#339, thanks @predatell) 227 | - Default button type to `btn-default` 228 | - Add `addon_before_class` and `addon_after_class` (#295, thanks @DanWright91 and others) 229 | - Fix handling of error class (#170) 230 | - No size class for checkboxes (#318, thanks @cybojenix) 231 | - Fix warnings during install (thanks @mfcovington) 232 | - Fix rare RunTimeError when working without database (#346, thanks @Mactory) 233 | - Add subresource integrity to external components (thanks @mfcovington and @Alex131089) 234 | - Several improvements to documentation, tests, and comments. Thanks all! 235 | 236 | ## 7.1.0 (2016-09-16) 237 | 238 | - Print help text and errors in their own block (#329, thanks @Matoking) 239 | - Improved page urls in pagination (fixes #323) 240 | - Changed setup.py to allow `setup.py test` run tests 241 | - Removed link target from active page in pagination (fixes #328) 242 | - Fixed example for bootstrap_label (fixed #332) 243 | - Fixed tests to support Django 1.10 handling of required attribute, see #337 (needs fixing) 244 | - Added tests for Django 1.10 245 | - Bootstrap to 3.3.7 246 | 247 | ## 7.0.1 (2016-03-23) 248 | 249 | - Fixed bug with widget attrs consistency (@onysos) 250 | 251 | ## 7.0.0 (2016-02-24) 252 | 253 | - Dropped support for Django < 1.8 254 | - Dropped support for Python < 2.7 255 | - Fix page number bug (thanks @frewsxcv) 256 | - Fix template context warning (thanks @jieter and @jonashaag) 257 | - Update to Bootstrap 3.3.6 (@nikolas) 258 | - Show links and newlines in messages (@jakub3279) 259 | - CSS classes arguments passed to the bootstrap_form are now working (@gordon) 260 | - Support for Django 1.9/Python 3.5 (@jieter and @jonashaag) 261 | - Better Travis CI Django versions (thanks @jonashaag) 262 | - Improved handling of messages in `bootstrap_messages` (thanks @frewsxcv and @rjsparks) 263 | 264 | ## 6.2.2 (2015-08-20) 265 | 266 | - Bug fix for escaped icons in buttons (reported by @jlec) 267 | 268 | ## 6.2.1 (2015-08-19) 269 | 270 | - Bug fix for whitespace in label placeholders (@Grelek) 271 | 272 | ## 6.2.0 (2015-08-15) 273 | 274 | - Improved tests 275 | - Make simple_tag output safe in Django 1.9 276 | - Better support for MultiWidgets (@xrmx) 277 | - Better documentation (@Moustacha) 278 | 279 | ## 6.1.0 (2015-06-25) 280 | 281 | - Upgrade to Bootstrap 3.3.5 282 | - Properly quote help text (@joshkel) 283 | 284 | ## 6.0.0 (2015-04-21) 285 | 286 | - No more media="screen" in CSS tags, complying to Bootstraps examples 287 | 288 | ## 5.4.0 (2015-04-21) 289 | 290 | - No more forcing btn-primary when another button class is specified (@takuchanno2) 291 | - Added value option to buttons (@TyVik) 292 | - Switched CDN to //maxcdn.bootstrapcdn.com/bootstrap/3.3.4/ (@djangoic) 293 | 294 | ## 5.3.1 (2015-04-08) 295 | 296 | - Fix Django 1.8 importlib warnings 297 | - Set defaults for horizontal-form to col-md-3 for label, col-md-9 for field 298 | - Various bug fixes 299 | - Fix version number typo 300 | 301 | ## 5.2.0 (2015-03-25) 302 | 303 | - Upgrade to Bootstrap 3.3.4 304 | - Fix required bug for checkboxes 305 | - Various bug fixes 306 | 307 | ## 5.1.1 (2015-01-22) 308 | 309 | - Fix checkbox display bug 310 | 311 | ## 5.1.0 (2015-01-22) 312 | 313 | - Make Bootstrap 3.3.2 default 314 | - Fix issue #140 (bad behaviour in Python 3) 315 | 316 | ## 5.0.3 (2014-12-02) 317 | 318 | - Fixing tests for older Django and Python versions 319 | 320 | ## 5.0.2 (2014-11-24) 321 | 322 | - Cleaning up some mess in 5.0.1 created by PyPI malfunction 323 | 324 | ## 5.0.1 (2014-11-21) 325 | 326 | - Bug fixes and update to Bootstrap 3.3.1 327 | 328 | ## 4.11.0 (2014-08-19) 329 | 330 | - Improved handling and control of form classes for error and success 331 | 332 | ## 4.10.1 (2014-08-18) 333 | 334 | - Bug fixes, test fixes, documentation fixes 335 | 336 | ## 4.10.0 (2014-08-12) 337 | 338 | - Template tag `bootstrap_icon` now supports a `title` parameter 339 | 340 | ## 4.9.2 (2014-08-11) 341 | 342 | - Fixed bug causing problems with setting classes for horizontal forms 343 | 344 | ## 4.9.1 (2014-08-10) 345 | 346 | - Fixed test for Django 1.4 347 | 348 | ## 4.9.0 (2014-08-09) 349 | 350 | - New parameter `href` for `bootstrap_button`, if provided will render `a` tag instead of `button` tag 351 | 352 | ## 4.8.2 (2014-07-10) 353 | 354 | - Internal fixes to master branch 355 | 356 | ## 4.8.1 (2014-07-10) 357 | 358 | - Make extra classes override bootstrap defaults 359 | 360 | ## 4.8.0 (2014-07-10) 361 | 362 | - Introduced new setting `set_placeholder`, default True 363 | 364 | ## 4.7.1 (2014-07-07) 365 | 366 | - Fixed rendering of various sizes (as introduced in 4.7.0) 367 | - Upgrade to Bootstrap 3.2.0 as default version 368 | 369 | ## 4.7.0 (2014-06-04) 370 | 371 | - `size` option added to formsets, forms, fields and buttons 372 | 373 | ## 4.6.0 (2014-05-22) 374 | 375 | - new `bootstrap_formset_errors` tag 376 | 377 | ## 4.5.0 (2014-05-21) 378 | 379 | - bug fixes in formsets 380 | - new formset renderer 381 | - new `bootstrap_form_errors` tag 382 | 383 | ## 4.4.2 (2014-05-20) 384 | 385 | - documentation now mentions templates 386 | 387 | ## 4.4.1 (2014-05-08) 388 | 389 | - bug fixes 390 | - documentation fixes 391 | - test coverage on coveralls.io 392 | 393 | ## 4.4.0 (2014-05-01) 394 | 395 | - added `bootstrap_alert` template tag 396 | 397 | ## 4.3.0 (2014-04-25) 398 | 399 | - added `required_css_class` and `error_css_class` as optional settings (global) and parameters (form and field rendering) 400 | 401 | ## 4.2.0 (2014-04-06) 402 | 403 | - moved styling of form level errors to template 404 | - bug fixes 405 | 406 | ## 4.1.1 (2014-04-06) 407 | 408 | - moved all text conversions to text_value 409 | 410 | ## 4.1.0 (2014-04-05) 411 | 412 | - typo fix and internal branching changes 413 | 414 | ## 4.0.3 (2014-04-03) 415 | 416 | - fixed checkbox label bug in vertical and inline forms 417 | 418 | ## 4.0.2 (2014-04-02) 419 | 420 | - fixed bug in vertical form rendering 421 | 422 | ## 4.0.1 (2014-03-29) 423 | 424 | - fixed unicode bug and added unicode label to tests 425 | 426 | ## 4.0.0 (2014-03-28) 427 | 428 | - use renderer classes for generating HTML 429 | - several bug fixes 430 | 431 | ## 3.3.0 (2014-03-19) 432 | 433 | - use Django forms css classes for indicating required and error on fields 434 | 435 | ## 3.2.1 (2014-03-16) 436 | 437 | - improved form rendering 438 | 439 | ## 3.2.0 (2014-03-11) 440 | 441 | - support for addons 442 | 443 | ## 3.1.0 (2014-03-03) 444 | 445 | - improve compatibility with Django < 1.5 446 | 447 | ## 3.0.0 (2014-02-28) 448 | 449 | - added support for themes (fix issue #74) 450 | - show inline form errors in field title (fix issue #81) 451 | - fixed bugs in demo application 452 | - update to newest Bootstrap (fix issue #83) 453 | 454 | ## 2.6.0 (2014-02-20) 455 | 456 | - new setting `set_required` to control setting of HTML `required` attribute (fix issue #76) 457 | 458 | ## 2.5.6 (2014-01-23) 459 | 460 | - project refactored 461 | - added skeleton for creating documentation (fix issue #30) 462 | - fixed `FileField` issues 463 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, and they are greatly appreciated! Every 4 | little bit helps, and credit will always be given. 5 | 6 | You can contribute in many ways: 7 | 8 | ## Types of Contributions 9 | 10 | ### Report Bugs 11 | 12 | Report bugs at . 13 | 14 | If you are reporting a bug, please include: 15 | 16 | - Your operating system name and version. 17 | - Any details about your local setup that might be helpful in troubleshooting. 18 | - Detailed steps to reproduce the bug. 19 | 20 | ### Fix Bugs 21 | 22 | Look through the GitHub issues for bugs. Anything tagged with \"bug\" is open to whoever wants to implement it. 23 | 24 | ### Implement Features 25 | 26 | Look through the GitHub issues for features. Anything tagged with \"feature\" is open to whoever wants to implement it. 27 | 28 | ### Write Documentation 29 | 30 | `django-bootstrap3` could always use more documentation, whether as part of the official django-bootstrap3 docs, in docstrings, or even on the web in blog posts, articles, and such. 31 | 32 | ### Submit Feedback 33 | 34 | The best way to send feedback is to file an issue at 35 | . 36 | 37 | If you are proposing a feature: 38 | 39 | - Explain in detail how it would work. 40 | - Keep the scope as narrow as possible, to make it easier to implement. 41 | 42 | ## Get Started! 43 | 44 | Ready to contribute? Here\'s how to set up `django-bootstrap3` for local development. 45 | 46 | You will need some knowledge of git, github, and Python/Django development. Using a Python virtual environment is advised. 47 | 48 | ### Local installation 49 | 50 | This package uses [uv](https://github.com/astral-sh/uv) and [just](https://github.com/casey/just). 51 | 52 | After installing both, check out this repository and type `just bootstrap` to bootstrap a development environment. 53 | 54 | ```console 55 | git clone git://github.com/zostera/django-bootstrap3.git 56 | cd django-bootstrap3 57 | just bootstrap 58 | ``` 59 | 60 | ### Running the tests 61 | 62 | To run the tests: 63 | 64 | ```console 65 | just test 66 | ``` 67 | 68 | To run the tests on all supported Python/Django combinations: 69 | 70 | ```console 71 | just tests 72 | ``` 73 | 74 | ## Pull Request Guidelines 75 | 76 | Before you submit a pull request, check that it meets these guidelines: 77 | 78 | 1. The pull request should include tests for new or changed functionality, and pass all tests. 79 | 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in CHANGELOG.md. 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Dylan Verheul and individual contributors 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include MANIFEST.in 3 | include .editorconfig 4 | include *.yaml 5 | include pyproject.toml 6 | include *.md 7 | include *.py 8 | include *.txt 9 | include Makefile 10 | include tox.ini 11 | include docs/*.rst docs/*.txt docs/*.py docs/Makefile 12 | graft src 13 | graft tests 14 | graft example 15 | prune docs/_build 16 | exclude example/db.sqlite3 17 | global-exclude *.py[cod] 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-bootstrap3 2 | 3 | [![CI](https://github.com/zostera/django-bootstrap3/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/zostera/django-bootstrap3/actions?workflow=test) 4 | [![Coverage Status](https://coveralls.io/repos/github/zostera/django-bootstrap3/badge.svg?branch=main)](https://coveralls.io/github/zostera/django-bootstrap3?branch=main) 5 | [![Latest PyPI version](https://img.shields.io/pypi/v/django-bootstrap3.svg)](https://pypi.python.org/pypi/django-bootstrap3) 6 | 7 | Bootstrap 3 for Django. 8 | 9 | ## Goal 10 | 11 | The goal of this project is to seamlessly blend Django and Bootstrap 3. 12 | 13 | ## Maintenance Mode 14 | 15 | Bootstrap 3 has been superseded by Bootstrap 4, which has since been superseded by Bootstrap 5. As a result, this package is now in **maintenance mode** and will only receive bug fixes and security updates. No new features or enhancements will be added. We recommend that new projects use Bootstrap 5 and encourage existing projects to consider migrating when feasible. 16 | 17 | For Bootstrap 4, please refer to our dedicated package: [django-bootstrap4](https://github.com/zostera/django-bootstrap4). 18 | 19 | For Bootstrap 5, please refer to our dedicated package: [django-bootstrap5](https://github.com/zostera/django-bootstrap5). 20 | 21 | For icons, we recommend our dedicated package: [django-icons](https://github.com/zostera/django-icons). 22 | 23 | ## Requirements 24 | 25 | This package requires a combination of Python and Django that is currently supported. 26 | 27 | See "Supported Versions" on https://www.djangoproject.com/download/. 28 | 29 | ## Documentation 30 | 31 | The full documentation is at https://django-bootstrap3.readthedocs.io/ 32 | 33 | ## Installation 34 | 35 | 1. Install using pip: 36 | 37 | ```shell script 38 | pip install django-bootstrap3 39 | ``` 40 | 41 | Alternatively, you can install download or clone this repo and call ``pip install -e .``. 42 | 43 | 2. Add to `INSTALLED_APPS` in your `settings.py`: 44 | 45 | ```python 46 | INSTALLED_APPS = ( 47 | # ... 48 | "bootstrap3", 49 | # ... 50 | ) 51 | ```` 52 | 53 | 3. In your templates, load the `bootstrap3` library and use the `bootstrap_*` tags: 54 | 55 | ## Example template 56 | 57 | ```djangotemplate 58 | {% load bootstrap3 %} 59 | 60 | {# Display a form #} 61 | 62 |
63 | {% csrf_token %} 64 | {% bootstrap_form form %} 65 | {% buttons %} 66 | 67 | {% endbuttons %} 68 |
69 | ``` 70 | 71 | Example app 72 | ----------- 73 | 74 | An example app is provided in `example`. You can run it with `make example`. 75 | 76 | 77 | Bugs and suggestions 78 | -------------------- 79 | 80 | If you have found a bug or if you have a request for additional functionality, please use the issue tracker on GitHub. 81 | 82 | https://github.com/zostera/django-bootstrap3/issues 83 | 84 | 85 | License 86 | ------- 87 | 88 | You can use this under BSD-3-Clause. See [LICENSE](LICENSE) file for details. 89 | 90 | 91 | Author 92 | ------ 93 | 94 | Developed and maintained by [Zostera](https://zostera.nl). 95 | 96 | Original author: [Dylan Verheul](https://github.com/dyve). 97 | 98 | Thanks to everybody that has contributed pull requests, ideas, issues, comments and kind words. 99 | 100 | Please see [AUTHORS](AUTHORS) for a list of contributors. 101 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import tomllib 4 | 5 | with open("../pyproject.toml", "rb") as f: 6 | pyproject = tomllib.load(f) 7 | 8 | project = pyproject["project"]["name"] 9 | release = pyproject["project"]["version"] 10 | version = ".".join(release.split(".")[:2]) 11 | author = ", ".join(author["name"] for author in pyproject["project"]["authors"]) 12 | year = datetime.now().year 13 | copyright = f"{year}, {author}" 14 | 15 | extensions = [ 16 | "sphinx.ext.autodoc", 17 | "sphinx.ext.viewcode", 18 | "myst_parser", 19 | ] 20 | 21 | htmlhelp_basename = f"{project}-doc" 22 | html_theme = "furo" 23 | pygments_style = "sphinx" 24 | -------------------------------------------------------------------------------- /docs/example_template.rst: -------------------------------------------------------------------------------- 1 | .. code:: django 2 | 3 | {# Load the tag library #} 4 | {% load bootstrap3 %} 5 | 6 | {# Load CSS and JavaScript #} 7 | {% bootstrap_css %} 8 | {% bootstrap_javascript %} 9 | 10 | {# Display django.contrib.messages as Bootstrap alerts #} 11 | {% bootstrap_messages %} 12 | 13 | {# Display a form #} 14 |
15 | {% csrf_token %} 16 | {% bootstrap_form form %} 17 | {% buttons %} 18 | 21 | {% endbuttons %} 22 |
23 | 24 | {# Read the documentation for more information #} 25 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to django-bootstrap3's documentation! 2 | ============================================= 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | installation 10 | quickstart 11 | templatetags 12 | settings 13 | templates 14 | changelog 15 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | The preferred way to install ``django-bootstrap3`` is ``pip``:: 6 | 7 | $ pip install django-bootstrap3 8 | 9 | Alternatively, you can install download or clone this repo and install from its folder with:: 10 | 11 | $ pip install -e . 12 | 13 | In your project, you should add ``django-bootstrap3`` to your ``requirements.txt``. 14 | 15 | Be sure to use ``virtualenv`` if you develop python projects. 16 | 17 | Add to INSTALLED_APPS in your ``settings.py``: 18 | 19 | ``'bootstrap3',`` 20 | 21 | After installation, the :doc:`quickstart` will get you on your way to using ``django-bootstrap3``. 22 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Quickstart 3 | ========== 4 | 5 | After :doc:`installation`, you can use ``django-bootstrap3`` in your templates.: 6 | 7 | Load the ``bootstrap3`` library and use the ``bootstrap_*`` tags: 8 | 9 | 10 | Example template 11 | ---------------- 12 | 13 | .. include:: example_template.rst 14 | 15 | 16 | Template tags and filters 17 | ------------------------- 18 | 19 | Refer to :doc:`templatetags` for more information. 20 | 21 | 22 | Settings 23 | -------- 24 | 25 | You can set defaults for ``django-bootstrap3`` in your settings file. Refer to :doc:`settings` for more information. 26 | 27 | 28 | Example application 29 | ------------------- 30 | 31 | The example application provides a number of useful examples. 32 | 33 | https://github.com/dyve/django-bootstrap3/tree/master/example 34 | 35 | -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Settings 3 | ======== 4 | 5 | The django-bootstrap3 has some pre-configured settings. 6 | 7 | They can be modified by adding a dict variable called ``BOOTSTRAP3`` in your ``settings.py`` and customizing the values ​​you want; 8 | 9 | The ``BOOTSTRAP3`` dict variable contains these settings and defaults: 10 | 11 | 12 | .. code:: django 13 | 14 | # Default settings 15 | BOOTSTRAP3 = { 16 | 17 | # The complete URL to the Bootstrap CSS file 18 | # Note that a URL can be either 19 | # - a string, e.g. "//code.jquery.com/jquery.min.js" 20 | # - a dict like the default value below (use key "url" for the actual link) 21 | "css_url": { 22 | "url": "https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css", 23 | "integrity": "sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu", 24 | "crossorigin": "anonymous", 25 | }, 26 | 27 | # The complete URL to the Bootstrap CSS file (None means no theme) 28 | "theme_url": None, 29 | 30 | # The complete URL to the Bootstrap JavaScript file 31 | "javascript_url": { 32 | "url": "https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js", 33 | "integrity": "sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd", 34 | "crossorigin": "anonymous", 35 | }, 36 | 37 | # The URL to the jQuery JavaScript file 38 | "jquery_url": "//code.jquery.com/jquery.min.js", 39 | 40 | # Put JavaScript in the HEAD section of the HTML document (only relevant if you use bootstrap3.html) 41 | "javascript_in_head": False, 42 | 43 | # Include jQuery with Bootstrap JavaScript (affects django-bootstrap3 template tags) 44 | "include_jquery": False, 45 | 46 | # Label class to use in horizontal forms 47 | "horizontal_label_class": "col-md-3", 48 | 49 | # Field class to use in horizontal forms 50 | "horizontal_field_class": "col-md-9", 51 | 52 | # Set placeholder attributes to label if no placeholder is provided. 53 | # This also considers the "label" option of {% bootstrap_field %} tags. 54 | "set_placeholder": True, 55 | 56 | # Class to indicate required (better to set this in your Django form) 57 | "required_css_class": "", 58 | 59 | # Class to indicate error (better to set this in your Django form) 60 | "error_css_class": "has-error", 61 | 62 | # Class to indicate success, meaning the field has valid input (better to set this in your Django form) 63 | "success_css_class": "has-success", 64 | 65 | # Renderers (only set these if you have studied the source and understand the inner workings) 66 | "formset_renderers":{ 67 | "default": "bootstrap3.renderers.FormsetRenderer", 68 | }, 69 | "form_renderers": { 70 | "default": "bootstrap3.renderers.FormRenderer", 71 | }, 72 | "field_renderers": { 73 | "default": "bootstrap3.renderers.FieldRenderer", 74 | "inline": "bootstrap3.renderers.InlineFieldRenderer", 75 | }, 76 | } 77 | -------------------------------------------------------------------------------- /docs/templates.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Templates 3 | ========= 4 | 5 | You can customize the output of ``django-bootstrap3`` by writing your own templates. These templates are available: 6 | 7 | 8 | bootstrap3/field_help_text_and_errors.html 9 | ------------------------------------------ 10 | 11 | This renders the help text and error of each field. 12 | 13 | Variable ``help_text_and_errors`` contains an array of strings. 14 | 15 | 16 | bootstrap3/form_errors.html 17 | --------------------------- 18 | 19 | This renders the non field errors of a form. 20 | 21 | Variable ``errors`` contains an array of strings. 22 | 23 | 24 | bootstrap3/messages.html 25 | ------------------------ 26 | 27 | This renders the Django messages variable. 28 | 29 | Variable ``messages`` contains the messages as described in https://docs.djangoproject.com/en/dev/ref/contrib/messages/#displaying-messages 30 | 31 | ``messages`` is passed through three built-in filters 32 | 33 | `safe ` 34 | 35 | `urlize ` 36 | 37 | `linebreaksbr ` 38 | 39 | Other 40 | ----- 41 | 42 | There are two more templates, ``bootstrap3/bootstrap3.html`` and ``bootstrap3/pagination.html``. You should consider these private for now, meaning you can use them but not modify them. 43 | -------------------------------------------------------------------------------- /docs/templatetags.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Template tags and filters 3 | ========================= 4 | 5 | 6 | .. note:: 7 | 8 | All the following examples it is understood that you have already loaded the ``bootstrap3`` 9 | template tag library, placing the code below in the beginning that each template that ``bootstrap3`` 10 | template tag library will be used. Read the :doc:`installation` and :doc:`quickstart` sections on how 11 | to accomplish this. 12 | 13 | 14 | bootstrap_form 15 | ~~~~~~~~~~~~~~ 16 | 17 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_form 18 | 19 | 20 | bootstrap_form_errors 21 | ~~~~~~~~~~~~~~~~~~~~~ 22 | 23 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_form_errors 24 | 25 | 26 | bootstrap_formset 27 | ~~~~~~~~~~~~~~~~~ 28 | 29 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_formset 30 | 31 | 32 | bootstrap_formset_errors 33 | ~~~~~~~~~~~~~~~~~~~~~~~~ 34 | 35 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_formset_errors 36 | 37 | 38 | bootstrap_field 39 | ~~~~~~~~~~~~~~~ 40 | 41 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_field 42 | 43 | 44 | bootstrap_label 45 | ~~~~~~~~~~~~~~~ 46 | 47 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_label 48 | 49 | 50 | bootstrap_button 51 | ~~~~~~~~~~~~~~~~ 52 | 53 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_button 54 | 55 | 56 | bootstrap_icon 57 | ~~~~~~~~~~~~~~ 58 | 59 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_icon 60 | 61 | bootstrap_alert 62 | ~~~~~~~~~~~~~~~ 63 | 64 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_alert 65 | 66 | buttons 67 | ~~~~~~~ 68 | 69 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_buttons 70 | 71 | 72 | bootstrap_messages 73 | ~~~~~~~~~~~~~~~~~~ 74 | 75 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_messages 76 | 77 | 78 | bootstrap_pagination 79 | ~~~~~~~~~~~~~~~~~~~~ 80 | 81 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_pagination 82 | 83 | 84 | bootstrap_jquery_url 85 | ~~~~~~~~~~~~~~~~~~~~ 86 | 87 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_jquery_url 88 | 89 | 90 | bootstrap_javascript_url 91 | ~~~~~~~~~~~~~~~~~~~~~~~~ 92 | 93 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_javascript_url 94 | 95 | 96 | bootstrap_css_url 97 | ~~~~~~~~~~~~~~~~~ 98 | 99 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_css_url 100 | 101 | 102 | bootstrap_css 103 | ~~~~~~~~~~~~~ 104 | 105 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_css 106 | 107 | 108 | bootstrap_javascript 109 | ~~~~~~~~~~~~~~~~~~~~ 110 | 111 | .. autofunction:: bootstrap3.templatetags.bootstrap3.bootstrap_javascript 112 | -------------------------------------------------------------------------------- /example/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zostera/django-bootstrap3/77345eb7f75d05fb701266c44462021fc60f0c22/example/app/__init__.py -------------------------------------------------------------------------------- /example/app/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.admin.widgets import AdminSplitDateTime 3 | from django.forms import BaseFormSet, formset_factory 4 | 5 | RADIO_CHOICES = (("1", "Radio 1"), ("2", "Radio 2")) 6 | 7 | MEDIA_CHOICES = ( 8 | ("Audio", (("vinyl", "Vinyl"), ("cd", "CD"))), 9 | ("Video", (("vhs", "VHS Tape"), ("dvd", "DVD"))), 10 | ("unknown", "Unknown"), 11 | ) 12 | 13 | 14 | class SmallTestForm(forms.Form): 15 | sender = forms.EmailField(label="Sender © unicode", help_text='E.g., "me@example.com"') 16 | subject = forms.CharField( 17 | max_length=100, 18 | help_text="my_help_text", 19 | required=True, 20 | widget=forms.TextInput(attrs={"placeholder": "placeholdertest"}), 21 | ) 22 | 23 | def clean(self): 24 | cleaned_data = super().clean() 25 | raise forms.ValidationError("This error was added to show the non field errors styling.") 26 | return cleaned_data 27 | 28 | 29 | class TestForm(forms.Form): 30 | """Form with a variety of widgets to test bootstrap3 rendering.""" 31 | 32 | date = forms.DateField(required=False) 33 | datetime = forms.SplitDateTimeField(widget=AdminSplitDateTime(), required=False) 34 | subject = forms.CharField( 35 | max_length=100, 36 | help_text="my_help_text", 37 | required=True, 38 | widget=forms.TextInput(attrs={"placeholder": "placeholdertest"}), 39 | ) 40 | password = forms.CharField(widget=forms.PasswordInput) 41 | message = forms.CharField(required=False, help_text="my_help_text") 42 | sender = forms.EmailField(label="Sender © unicode", help_text='E.g., "me@example.com"') 43 | secret = forms.CharField(initial=42, widget=forms.HiddenInput) 44 | weird = forms.CharField(help_text="strings are now utf-8 \u03bcnico\u0394é!") 45 | cc_myself = forms.BooleanField( 46 | required=False, help_text='cc stands for "carbon copy." You will get a copy in your mailbox.' 47 | ) 48 | select1 = forms.ChoiceField(choices=RADIO_CHOICES) 49 | select2 = forms.MultipleChoiceField(choices=RADIO_CHOICES, help_text="Check as many as you like.") 50 | select3 = forms.ChoiceField(choices=MEDIA_CHOICES) 51 | select4 = forms.MultipleChoiceField(choices=MEDIA_CHOICES, help_text="Check as many as you like.") 52 | category1 = forms.ChoiceField(choices=RADIO_CHOICES, widget=forms.RadioSelect) 53 | category2 = forms.MultipleChoiceField( 54 | choices=RADIO_CHOICES, widget=forms.CheckboxSelectMultiple, help_text="Check as many as you like." 55 | ) 56 | category3 = forms.ChoiceField(widget=forms.RadioSelect, choices=MEDIA_CHOICES) 57 | category4 = forms.MultipleChoiceField( 58 | choices=MEDIA_CHOICES, widget=forms.CheckboxSelectMultiple, help_text="Check as many as you like." 59 | ) 60 | number = forms.FloatField() 61 | url = forms.URLField() 62 | addon = forms.CharField(widget=forms.TextInput(attrs={"addon_before": "before", "addon_after": "after"})) 63 | 64 | # TODO: Re-enable this after Django 1.11 #28105 is available 65 | # polygon = gisforms.PointField() 66 | 67 | required_css_class = "bootstrap3-req" 68 | 69 | # Set this to allow tests to work properly in Django 1.10+ 70 | # More information, see issue #337 71 | use_required_attribute = False 72 | 73 | def clean(self): 74 | cleaned_data = super().clean() 75 | raise forms.ValidationError("This error was added to show the non field errors styling.") 76 | return cleaned_data 77 | 78 | 79 | class ContactForm(TestForm): 80 | pass 81 | 82 | 83 | class ContactBaseFormSet(BaseFormSet): 84 | def add_fields(self, form, index): 85 | super().add_fields(form, index) 86 | 87 | def clean(self): 88 | super().clean() 89 | raise forms.ValidationError("This error was added to show the non form errors styling") 90 | 91 | 92 | ContactFormSet = formset_factory(TestForm, formset=ContactBaseFormSet, extra=2, max_num=4, validate_max=True) 93 | 94 | 95 | class FilesForm(forms.Form): 96 | text1 = forms.CharField() 97 | file1 = forms.FileField() 98 | file2 = forms.FileField(required=False) 99 | file3 = forms.FileField(widget=forms.ClearableFileInput) 100 | file5 = forms.ImageField() 101 | file4 = forms.FileField(required=False, widget=forms.ClearableFileInput) 102 | 103 | 104 | class ArticleForm(forms.Form): 105 | title = forms.CharField() 106 | pub_date = forms.DateField() 107 | 108 | def clean(self): 109 | cleaned_data = super().clean() 110 | raise forms.ValidationError("This error was added to show the non field errors styling.") 111 | return cleaned_data 112 | -------------------------------------------------------------------------------- /example/app/templates/app/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/bootstrap.html' %} 2 | 3 | {% load bootstrap3 %} 4 | 5 | {% block bootstrap3_content %} 6 |
7 |

{% block title %}(no title){% endblock %}

8 | 9 |

10 | home 11 | formset 12 | form 13 | form_by_field 14 | form_horizontal 15 | form_inline 16 | form_with_files 17 | pagination 18 | miscellaneous 19 |

20 | 21 | {% autoescape off %}{% bootstrap_messages %}{% endautoescape %} 22 | 23 | {% block content %}(no content){% endblock %} 24 |
25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /example/app/templates/app/bootstrap.html: -------------------------------------------------------------------------------- 1 | {% extends 'bootstrap3/bootstrap3.html' %} 2 | 3 | {% block bootstrap3_title %}{% block title %}{% endblock %}{% endblock %} 4 | -------------------------------------------------------------------------------- /example/app/templates/app/form.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap3 %} 4 | 5 | {% block title %} 6 | Forms 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 |
12 | {% csrf_token %} 13 | {% bootstrap_form form %} 14 | {% buttons submit='OK' reset="Cancel" %}{% endbuttons %} 15 |
16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /example/app/templates/app/form_by_field.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap3 %} 4 | 5 | {% block title %} 6 | Forms 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 |
12 | {% csrf_token %} 13 | {% bootstrap_form_errors form type='non_fields' %} 14 | {% bootstrap_field form.subject layout='horizontal' size='sm' %} 15 | {% bootstrap_field form.message placeholder='bonkers' %} 16 | {% buttons submit='OK' reset="Cancel" %}{% endbuttons %} 17 |
18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /example/app/templates/app/form_horizontal.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap3 %} 4 | 5 | {% block title %} 6 | Forms 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 |
12 | {% csrf_token %} 13 | {% bootstrap_form form layout="horizontal" %} 14 | {% buttons submit='OK' reset='Cancel' layout='horizontal' %}{% endbuttons %} 15 |
16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /example/app/templates/app/form_inline.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap3 %} 4 | 5 | {% block title %} 6 | Forms 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 |
12 | {% csrf_token %} 13 | {% bootstrap_form form layout='inline' %} 14 | {% buttons submit='OK' reset='Cancel' layout='inline' %}{% endbuttons %} 15 |
16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /example/app/templates/app/form_with_files.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap3 %} 4 | 5 | {% block title %} 6 | Forms 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 |
12 | {% csrf_token %} 13 | {% bootstrap_form form layout=layout %} 14 | {% buttons submit='OK' reset="Cancel" %}{% endbuttons %} 15 |
16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /example/app/templates/app/formset.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap3 %} 4 | 5 | {% block title %} 6 | Formset 7 | {% endblock %} 8 | 9 | {% block content %} 10 | {% bootstrap_formset_errors form %} 11 |
12 | {% csrf_token %} 13 | {% bootstrap_formset form %} 14 | {% buttons submit='OK' reset="Cancel" %}{% endbuttons %} 15 |
16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /example/app/templates/app/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | {% load bootstrap3 %} 3 | 4 | {% block title %}django-bootstrap3{% endblock %} 5 | 6 | {% block content %} 7 | This is bootstrap3 for Django. 8 | {% endblock %} -------------------------------------------------------------------------------- /example/app/templates/app/misc.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap3 %} 4 | 5 | {% block title %} 6 | Miscellaneous 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 | {% bootstrap_button 'button' size='lg' %} 12 | 13 | {% bootstrap_button 'button' size='lg' icon='info-sign' %} 14 | 15 | {% bootstrap_alert "Something went wrong" alert_type='danger' %} 16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /example/app/templates/app/pagination.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap3 %} 4 | 5 | {% block title %} 6 | Pagination 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 | 12 | {% for line in lines %} 13 | 14 | 15 | 16 | {% endfor %} 17 |
{{ line }}
18 | 19 |
20 | 21 | {% bootstrap_pagination lines url="/pagination?page=1&flop=flip" extra="q=foo" size="small" %} 22 | 23 | {% bootstrap_pagination lines url="/pagination?page=1" size="large" %} 24 | 25 | {% endblock %} -------------------------------------------------------------------------------- /example/app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import ( 4 | DefaultFormByFieldView, 5 | DefaultFormsetView, 6 | DefaultFormView, 7 | FormHorizontalView, 8 | FormInlineView, 9 | FormWithFilesView, 10 | HomePageView, 11 | MiscView, 12 | PaginationView, 13 | ) 14 | 15 | urlpatterns = [ 16 | path("", HomePageView.as_view(), name="home"), 17 | path("formset", DefaultFormsetView.as_view(), name="formset_default"), 18 | path("form", DefaultFormView.as_view(), name="form_default"), 19 | path("form_by_field", DefaultFormByFieldView.as_view(), name="form_by_field"), 20 | path("form_horizontal", FormHorizontalView.as_view(), name="form_horizontal"), 21 | path("form_inline", FormInlineView.as_view(), name="form_inline"), 22 | path("form_with_files", FormWithFilesView.as_view(), name="form_with_files"), 23 | path("pagination", PaginationView.as_view(), name="pagination"), 24 | path("misc", MiscView.as_view(), name="misc"), 25 | ] 26 | -------------------------------------------------------------------------------- /example/app/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.core.files.storage import default_storage 3 | from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator 4 | from django.db.models.fields.files import FieldFile 5 | from django.views.generic import FormView 6 | from django.views.generic.base import TemplateView 7 | 8 | from .forms import ContactForm, ContactFormSet, FilesForm 9 | 10 | 11 | # http://yuji.wordpress.com/2013/01/30/django-form-field-in-initial-data-requires-a-fieldfile-instance/ 12 | class FakeField: 13 | storage = default_storage 14 | 15 | 16 | fieldfile = FieldFile(None, FakeField, "dummy.txt") 17 | 18 | 19 | class HomePageView(TemplateView): 20 | template_name = "app/home.html" 21 | 22 | def get_context_data(self, **kwargs): 23 | context = super().get_context_data(**kwargs) 24 | messages.info(self.request, "hello http://example.com") 25 | return context 26 | 27 | 28 | class DefaultFormsetView(FormView): 29 | template_name = "app/formset.html" 30 | form_class = ContactFormSet 31 | 32 | 33 | class DefaultFormView(FormView): 34 | template_name = "app/form.html" 35 | form_class = ContactForm 36 | 37 | 38 | class DefaultFormByFieldView(FormView): 39 | template_name = "app/form_by_field.html" 40 | form_class = ContactForm 41 | 42 | 43 | class FormHorizontalView(FormView): 44 | template_name = "app/form_horizontal.html" 45 | form_class = ContactForm 46 | 47 | 48 | class FormInlineView(FormView): 49 | template_name = "app/form_inline.html" 50 | form_class = ContactForm 51 | 52 | 53 | class FormWithFilesView(FormView): 54 | template_name = "app/form_with_files.html" 55 | form_class = FilesForm 56 | 57 | def get_context_data(self, **kwargs): 58 | context = super().get_context_data(**kwargs) 59 | context["layout"] = self.request.GET.get("layout", "vertical") 60 | return context 61 | 62 | def get_initial(self): 63 | return {"file4": fieldfile} 64 | 65 | 66 | class PaginationView(TemplateView): 67 | template_name = "app/pagination.html" 68 | 69 | def get_context_data(self, **kwargs): 70 | context = super().get_context_data(**kwargs) 71 | num_lines = 200 72 | lines = [] 73 | for i in range(1, num_lines + 1): 74 | lines.append(f"Line {i}") 75 | paginator = Paginator(lines, 10) 76 | page = self.request.GET.get("page") 77 | try: 78 | show_lines = paginator.page(page) 79 | except PageNotAnInteger: 80 | # If page is not an integer, deliver first page. 81 | show_lines = paginator.page(1) 82 | except EmptyPage: 83 | # If page is out of range (e.g. 9999), deliver last page of results. 84 | show_lines = paginator.page(paginator.num_pages) 85 | context["lines"] = show_lines 86 | return context 87 | 88 | 89 | class MiscView(TemplateView): 90 | template_name = "app/misc.html" 91 | -------------------------------------------------------------------------------- /example/app/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django-bootstrap3 project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | """ 15 | 16 | import os 17 | 18 | from django.core.wsgi import get_wsgi_application 19 | 20 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 21 | # if running multiple sites in the same mod_wsgi process. To fix this, use 22 | # mod_wsgi daemon mode with each site in its own daemon process, or use 23 | # os.environ["DJANGO_SETTINGS_MODULE"] = "app.settings" 24 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") 25 | 26 | # This application object is used by any WSGI server configured to use this 27 | # file. This includes Django's development server, if the WSGI_APPLICATION 28 | # setting points here. 29 | 30 | application = get_wsgi_application() 31 | 32 | # Apply WSGI middleware here. 33 | # from helloworld.wsgi import HelloWorldApplication 34 | # application = HelloWorldApplication(application) 35 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__))) 4 | 5 | DEBUG = True 6 | 7 | ADMINS = () 8 | 9 | DATABASES = { 10 | "default": { 11 | "ENGINE": "django.db.backends.sqlite3", 12 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 13 | } 14 | } 15 | 16 | # Hosts/domain names that are valid for this site; required if DEBUG is False 17 | # See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts 18 | ALLOWED_HOSTS = ["localhost", "127.0.0.1"] 19 | 20 | # Local time zone for this installation. Choices can be found here: 21 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 22 | # although not all choices may be available on all operating systems. 23 | # In a Windows environment this must be set to your system time zone. 24 | TIME_ZONE = "Europe/Amsterdam" 25 | 26 | # Language code for this installation. All choices can be found here: 27 | # http://www.i18nguy.com/unicode/language-identifiers.html 28 | LANGUAGE_CODE = "en-us" 29 | 30 | # If you set this to False, Django will make some optimizations so as not 31 | # to load the internationalization machinery. 32 | USE_I18N = True 33 | 34 | # If you set this to False, Django will not format dates, numbers and 35 | # calendars according to the current locale. 36 | USE_L10N = True 37 | 38 | # If you set this to False, Django will not use timezone-aware datetimes. 39 | USE_TZ = True 40 | 41 | # Absolute filesystem path to the directory that will hold user-uploaded files. 42 | # Example: "/var/www/example.com/media/" 43 | MEDIA_ROOT = "" 44 | 45 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 46 | # trailing slash. 47 | # Examples: "http://example.com/media/", "http://media.example.com/" 48 | MEDIA_URL = "" 49 | 50 | # Absolute path to the directory static files should be collected to. 51 | # Don't put anything in this directory yourself; store your static files 52 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 53 | # Example: "/var/www/example.com/static/" 54 | STATIC_ROOT = "" 55 | 56 | # URL prefix for static files. 57 | # Example: "http://example.com/static/", "http://static.example.com/" 58 | STATIC_URL = "/static/" 59 | 60 | # Additional locations of static files 61 | STATICFILES_DIRS = ( 62 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 63 | # Always use forward slashes, even on Windows. 64 | # Don't forget to use absolute paths, not relative paths. 65 | ) 66 | 67 | # List of finder classes that know how to find static files in 68 | # various locations. 69 | STATICFILES_FINDERS = ( 70 | "django.contrib.staticfiles.finders.FileSystemFinder", 71 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 72 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 73 | ) 74 | 75 | # Make this unique, and don't share it with anybody. 76 | SECRET_KEY = "8s)l4^2s&&0*31-)+6lethmfy3#r1egh^6y^=b9@g!q63r649_" 77 | 78 | MIDDLEWARE = ( 79 | "django.middleware.common.CommonMiddleware", 80 | "django.contrib.sessions.middleware.SessionMiddleware", 81 | "django.middleware.csrf.CsrfViewMiddleware", 82 | "django.contrib.auth.middleware.AuthenticationMiddleware", 83 | "django.contrib.messages.middleware.MessageMiddleware", 84 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 85 | ) 86 | 87 | ROOT_URLCONF = "app.urls" 88 | 89 | # Python dotted path to the WSGI application used by Django's runserver. 90 | WSGI_APPLICATION = "app.wsgi.application" 91 | 92 | TEMPLATES = [ 93 | { 94 | "BACKEND": "django.template.backends.django.DjangoTemplates", 95 | "APP_DIRS": True, 96 | "OPTIONS": { 97 | "context_processors": [ 98 | "django.contrib.auth.context_processors.auth", 99 | "django.template.context_processors.request", 100 | "django.template.context_processors.debug", 101 | "django.template.context_processors.i18n", 102 | "django.template.context_processors.media", 103 | "django.template.context_processors.static", 104 | "django.template.context_processors.tz", 105 | "django.contrib.messages.context_processors.messages", 106 | ] 107 | }, 108 | } 109 | ] 110 | 111 | INSTALLED_APPS = ( 112 | "django.contrib.auth", 113 | "django.contrib.contenttypes", 114 | "django.contrib.sessions", 115 | "django.contrib.sites", 116 | "django.contrib.messages", 117 | "django.contrib.staticfiles", 118 | "django.contrib.admin", 119 | "bootstrap3", 120 | "app", 121 | ) 122 | 123 | # A sample logging configuration. The only tangible logging 124 | # performed by this configuration is to send an email to 125 | # the site admins on every HTTP 500 error when DEBUG=False. 126 | # See http://docs.djangoproject.com/en/dev/topics/logging for 127 | # more details on how to customize your logging configuration. 128 | LOGGING = { 129 | "version": 1, 130 | "disable_existing_loggers": False, 131 | "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, 132 | "handlers": { 133 | "mail_admins": { 134 | "level": "ERROR", 135 | "filters": ["require_debug_false"], 136 | "class": "django.utils.log.AdminEmailHandler", 137 | } 138 | }, 139 | "loggers": {"django.request": {"handlers": ["mail_admins"], "level": "ERROR", "propagate": True}}, 140 | } 141 | 142 | # Settings for django-bootstrap3 143 | BOOTSTRAP3 = { 144 | "error_css_class": "bootstrap3-error", 145 | "required_css_class": "bootstrap3-required", 146 | "javascript_in_head": True, 147 | } 148 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set export := true 2 | set dotenv-load := true 3 | 4 | EXAMPLE_DIRNAME := "example" 5 | VENV_DIRNAME := ".venv" 6 | VERSION := `sed -n 's/^ *version.*=.*"\([^"]*\)".*/\1/p' pyproject.toml` 7 | 8 | # default recipe 9 | default: 10 | just --list 11 | 12 | [private] 13 | @uv: 14 | if ! command -v uv >/dev/null; then \ 15 | echo "Error: 'uv' command is not available"; \ 16 | exit 1; \ 17 | fi 18 | 19 | # Set up development environment 20 | @bootstrap: uv 21 | if test ! -e {{ VENV_DIRNAME }}; then \ 22 | uv python install; \ 23 | fi 24 | just update 25 | 26 | # Install and/or update all dependencies defined in pyproject.toml 27 | @update: uv 28 | uv sync --all-extras --all-groups --upgrade 29 | 30 | # Format 31 | @format: bootstrap 32 | ruff format 33 | ruff check --fix 34 | 35 | # Lint 36 | @lint: bootstrap 37 | ruff format --check 38 | ruff check 39 | 40 | # Test 41 | @test: bootstrap 42 | coverage run manage.py test 43 | coverage report 44 | 45 | # Test 46 | @tests: bootstrap 47 | tox 48 | 49 | # Build 50 | @build: bootstrap 51 | uv build 52 | uvx twine check dist/* 53 | uvx check-manifest 54 | uvx pyroma . 55 | uvx check-wheel-contents dist 56 | 57 | # Clean 58 | @clean: 59 | rm -rf build dist src/*.egg-info .coverage* 60 | 61 | # Check if the current Git branch is 'main' 62 | @branch: 63 | if [ "`git rev-parse --abbrev-ref HEAD`" = "main" ]; then \ 64 | echo "On branch main."; \ 65 | else \ 66 | echo "Error - Not on branch main."; \ 67 | exit 1; \ 68 | fi 69 | 70 | # Check if the working directory is clean 71 | @porcelain: 72 | if [ -z "`git status --porcelain`" ]; then \ 73 | echo "Working directory is clean."; \ 74 | else \ 75 | echo "Error - working directory is dirty. Commit your changes."; \ 76 | exit 1; \ 77 | fi 78 | 79 | @docs: bootstrap clean 80 | uv run -m sphinx -T -b html -d docs/_build/doctrees -D language=en docs docs/_build/html 81 | 82 | @example: 83 | if test -e {{ EXAMPLE_DIRNAME }}; then \ 84 | cd "{{ EXAMPLE_DIRNAME }}" && python manage.py runserver; \ 85 | else \ 86 | echo "Example not found."; \ 87 | fi 88 | 89 | @publish: porcelain branch docs build 90 | uvx uv-publish 91 | git tag -a v${VERSION} -m "Release {{ VERSION }}" 92 | git push origin --tags 93 | 94 | # Version number 95 | @version: 96 | echo "{{ VERSION }}" 97 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.app.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = ["hatchling"] 4 | 5 | [project] 6 | authors = [ 7 | {name = "Dylan Verheul", email = "dylan@dyve.net"}, 8 | ] 9 | classifiers = [ 10 | "Development Status :: 5 - Production/Stable", 11 | "Environment :: Web Environment", 12 | "Framework :: Django", 13 | "Framework :: Django :: 4.2", 14 | "Framework :: Django :: 5.1", 15 | "Framework :: Django :: 5.2", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: BSD License", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Programming Language :: Python :: 3.12", 24 | "Programming Language :: Python :: 3.13", 25 | "Topic :: Software Development :: Libraries", 26 | "Topic :: Utilities", 27 | ] 28 | dependencies = ["Django>=4.2"] 29 | description = "Bootstrap 3 for Django" 30 | keywords = ["django", "bootstrap", "bootstrap3"] 31 | license = {file = "LICENSE"} 32 | name = "django-bootstrap3" 33 | readme = "README.md" 34 | requires-python = ">=3.9" 35 | version = "25.1" 36 | 37 | [project.urls] 38 | Changelog = "https://github.com/zostera/django-bootstrap3/blob/main/CHANGELOG.md" 39 | Documentation = "https://django-bootstrap3.readthedocs.io/" 40 | Homepage = "https://github.com/zostera/django-bootstrap3" 41 | Issues = "https://github.com/zostera/django-bootstrap3/issues" 42 | Source = "https://github.com/zostera/django-bootstrap3" 43 | 44 | [tool.ruff] 45 | fix = false 46 | line-length = 120 47 | src = ["src"] 48 | target-version = "py39" 49 | 50 | [tool.ruff.lint] 51 | fixable = [ 52 | "I001", # isort (sorting) 53 | "F", # flake8 54 | "D", # docformatter 55 | "UP", # pyupgrade 56 | ] 57 | ignore = [ 58 | "D1", # D1: Missing docstring error codes (because not every function and class has a docstring) 59 | "D203", # D203: 1 blank line required before class docstring (conflicts with D211 and should be disabled, see https://github.com/PyCQA/pydocstyle/pull/91) 60 | "D212", # D212: Multi-line docstring summary should start at the first line 61 | "D301", # D301: Use r”“” if any backslashes in a docstring (unclear how else to handle backslashes in docstrings) 62 | ] 63 | select = [ 64 | "D", # pydocstyle 65 | "E", # pycodestyle 66 | "F", # flake8 67 | "I", # isort 68 | "UP", # pyupgrade 69 | ] 70 | unfixable = [ 71 | "F8", # names in flake8, such as defined but unused variables 72 | ] 73 | 74 | [tool.ruff.lint.isort] 75 | known-first-party = ["bootstrap3", "app"] 76 | known-third-party = ["django"] 77 | 78 | [tool.coverage.run] 79 | branch = true 80 | source = ["src", "tests"] 81 | 82 | [tool.coverage.paths] 83 | package = ["src/bootstrap3", "*/django_bootstrap3/src/bootstrap3"] 84 | 85 | [tool.coverage.report] 86 | show_missing = true 87 | skip_covered = true 88 | 89 | [dependency-groups] 90 | dev = [ 91 | "check-manifest>=0.50", 92 | "check-wheel-contents>=0.6.0", 93 | "coverage[toml]>=7.6.1", 94 | "pyroma>=4.2", 95 | "ruff>=0.7.1", 96 | "tox-uv>=1.13.1", 97 | "twine>=5.1.1", 98 | ] 99 | docs = [ 100 | "furo>=2024.8.6", 101 | "myst-parser>=3.0.1", 102 | "sphinx>=7.1.2", 103 | ] 104 | 105 | [tool.hatch.build.targets.wheel] 106 | packages = ["src/bootstrap3"] 107 | -------------------------------------------------------------------------------- /src/bootstrap3/__about__.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | __version__ = importlib.metadata.version("django-bootstrap3") 4 | -------------------------------------------------------------------------------- /src/bootstrap3/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "__version__", 3 | ] 4 | 5 | from .__about__ import __version__ 6 | -------------------------------------------------------------------------------- /src/bootstrap3/bootstrap.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | from django.conf import settings 4 | 5 | # Default settings 6 | BOOTSTRAP3_DEFAULTS = { 7 | "css_url": { 8 | "url": "https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css", 9 | "integrity": "sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu", 10 | "crossorigin": "anonymous", 11 | }, 12 | "theme_url": None, 13 | "javascript_url": { 14 | "url": "https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js", 15 | "integrity": "sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd", 16 | "crossorigin": "anonymous", 17 | }, 18 | "jquery_url": "//code.jquery.com/jquery.min.js", 19 | "javascript_in_head": False, 20 | "include_jquery": False, 21 | "horizontal_label_class": "col-md-3", 22 | "horizontal_field_class": "col-md-9", 23 | "set_placeholder": True, 24 | "required_css_class": "", 25 | "error_css_class": "has-error", 26 | "success_css_class": "has-success", 27 | "formset_renderers": {"default": "bootstrap3.renderers.FormsetRenderer"}, 28 | "form_renderers": {"default": "bootstrap3.renderers.FormRenderer"}, 29 | "field_renderers": { 30 | "default": "bootstrap3.renderers.FieldRenderer", 31 | "inline": "bootstrap3.renderers.InlineFieldRenderer", 32 | }, 33 | } 34 | 35 | 36 | def get_bootstrap_setting(name, default=None): 37 | """Read a setting.""" 38 | # Start with a copy of default settings 39 | bootstrap3 = BOOTSTRAP3_DEFAULTS.copy() 40 | 41 | # Override with user settings from settings.py 42 | bootstrap3.update(getattr(settings, "BOOTSTRAP3", {})) 43 | 44 | # Update use_i18n 45 | bootstrap3["use_i18n"] = i18n_enabled() 46 | 47 | return bootstrap3.get(name, default) 48 | 49 | 50 | def jquery_url(): 51 | """Return the full url to jQuery file to use.""" 52 | return get_bootstrap_setting("jquery_url") 53 | 54 | 55 | def javascript_url(): 56 | """Return the full url to the Bootstrap JavaScript file.""" 57 | return get_bootstrap_setting("javascript_url") 58 | 59 | 60 | def css_url(): 61 | """Return the full url to the Bootstrap CSS file.""" 62 | return get_bootstrap_setting("css_url") 63 | 64 | 65 | def theme_url(): 66 | """Return the full url to the theme CSS file.""" 67 | return get_bootstrap_setting("theme_url") 68 | 69 | 70 | def i18n_enabled(): 71 | """Return the projects i18n setting.""" 72 | return getattr(settings, "USE_I18N", False) 73 | 74 | 75 | def get_renderer(renderers, **kwargs): 76 | layout = kwargs.get("layout", "") 77 | path = renderers.get(layout, renderers["default"]) 78 | mod, cls = path.rsplit(".", 1) 79 | return getattr(import_module(mod), cls) 80 | 81 | 82 | def get_formset_renderer(**kwargs): 83 | renderers = get_bootstrap_setting("formset_renderers") 84 | return get_renderer(renderers, **kwargs) 85 | 86 | 87 | def get_form_renderer(**kwargs): 88 | renderers = get_bootstrap_setting("form_renderers") 89 | return get_renderer(renderers, **kwargs) 90 | 91 | 92 | def get_field_renderer(**kwargs): 93 | renderers = get_bootstrap_setting("field_renderers") 94 | return get_renderer(renderers, **kwargs) 95 | -------------------------------------------------------------------------------- /src/bootstrap3/components.py: -------------------------------------------------------------------------------- 1 | from django.utils.safestring import mark_safe 2 | 3 | from bootstrap3.utils import add_css_class, render_tag 4 | 5 | from .text import text_value 6 | 7 | 8 | def render_icon(icon, **kwargs): 9 | """Render a Bootstrap glyphicon icon.""" 10 | attrs = {"class": add_css_class(f"glyphicon glyphicon-{icon}", kwargs.get("extra_classes", ""))} 11 | title = kwargs.get("title") 12 | if title: 13 | attrs["title"] = title 14 | return render_tag("span", attrs=attrs) 15 | 16 | 17 | def render_alert(content, alert_type=None, dismissable=True): 18 | """Render a Bootstrap alert.""" 19 | button = "" 20 | if not alert_type: 21 | alert_type = "info" 22 | css_classes = ["alert", "alert-" + text_value(alert_type)] 23 | if dismissable: 24 | css_classes.append("alert-dismissable") 25 | button = '' 26 | button_placeholder = "__BUTTON__" 27 | return mark_safe( 28 | render_tag( 29 | "div", attrs={"class": " ".join(css_classes)}, content=mark_safe(button_placeholder) + text_value(content) 30 | ).replace(button_placeholder, button) 31 | ) 32 | -------------------------------------------------------------------------------- /src/bootstrap3/exceptions.py: -------------------------------------------------------------------------------- 1 | class BootstrapException(Exception): 2 | """Any exception from this package.""" 3 | 4 | 5 | class BootstrapError(BootstrapException): 6 | """Any exception that is an error.""" 7 | -------------------------------------------------------------------------------- /src/bootstrap3/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.widgets import AdminFileWidget 2 | from django.forms import ( 3 | CheckboxSelectMultiple, 4 | EmailInput, 5 | FileInput, 6 | HiddenInput, 7 | NumberInput, 8 | PasswordInput, 9 | Textarea, 10 | TextInput, 11 | URLInput, 12 | ) 13 | from django.forms.widgets import CheckboxInput 14 | from django.utils.safestring import mark_safe 15 | 16 | from .bootstrap import ( 17 | get_bootstrap_setting, 18 | get_field_renderer, 19 | get_form_renderer, 20 | get_formset_renderer, 21 | ) 22 | from .components import render_icon 23 | from .exceptions import BootstrapError 24 | from .text import text_concat, text_value 25 | from .utils import add_css_class, render_tag 26 | 27 | FORM_GROUP_CLASS = "form-group" 28 | 29 | WIDGETS_NO_REQUIRED = ( 30 | AdminFileWidget, 31 | HiddenInput, 32 | FileInput, 33 | CheckboxInput, 34 | CheckboxSelectMultiple, 35 | ) 36 | 37 | 38 | def render_formset(formset, **kwargs): 39 | """Render a formset to a Bootstrap layout.""" 40 | renderer_cls = get_formset_renderer(**kwargs) 41 | return renderer_cls(formset, **kwargs).render() 42 | 43 | 44 | def render_formset_errors(formset, **kwargs): 45 | """Render formset errors to a Bootstrap layout.""" 46 | renderer_cls = get_formset_renderer(**kwargs) 47 | return renderer_cls(formset, **kwargs).render_errors() 48 | 49 | 50 | def render_form(form, **kwargs): 51 | """Render a form to a Bootstrap layout.""" 52 | renderer_cls = get_form_renderer(**kwargs) 53 | return renderer_cls(form, **kwargs).render() 54 | 55 | 56 | def render_form_errors(form, error_types="non_field_errors", **kwargs): 57 | """Render form errors to a Bootstrap layout.""" 58 | renderer_cls = get_form_renderer(**kwargs) 59 | return renderer_cls(form, **kwargs).render_errors(error_types) 60 | 61 | 62 | def render_field(field, **kwargs): 63 | """Render a field to a Bootstrap layout.""" 64 | renderer_cls = get_field_renderer(**kwargs) 65 | return renderer_cls(field, **kwargs).render() 66 | 67 | 68 | def render_label(content, label_for=None, label_class=None, label_title=""): 69 | """Render a label with content.""" 70 | attrs = {} 71 | if label_for: 72 | attrs["for"] = label_for 73 | if label_class: 74 | attrs["class"] = label_class 75 | if label_title: 76 | attrs["title"] = label_title 77 | return render_tag("label", attrs=attrs, content=content) 78 | 79 | 80 | def render_button( 81 | content, 82 | button_type=None, 83 | icon=None, 84 | button_class="btn-default", 85 | size="", 86 | href="", 87 | name=None, 88 | value=None, 89 | title=None, 90 | extra_classes="", 91 | id="", 92 | ): 93 | """Render a button with content.""" 94 | attrs = {} 95 | classes = add_css_class("btn", button_class) 96 | size = text_value(size).lower().strip() 97 | if size == "xs": 98 | classes = add_css_class(classes, "btn-xs") 99 | elif size == "sm" or size == "small": 100 | classes = add_css_class(classes, "btn-sm") 101 | elif size == "lg" or size == "large": 102 | classes = add_css_class(classes, "btn-lg") 103 | elif size == "md" or size == "medium": 104 | pass 105 | elif size: 106 | raise BootstrapError(f'Parameter "size" should be "xs", "sm", "lg" or empty ("{size}" given).') 107 | if button_type: 108 | if button_type not in ("submit", "reset", "button", "link"): 109 | raise BootstrapError( 110 | 'Parameter "button_type" should be "submit", "reset", "button", "link" or empty ' 111 | + f'("{button_type}" given).' 112 | ) 113 | attrs["type"] = button_type 114 | classes = add_css_class(classes, extra_classes) 115 | attrs["class"] = classes 116 | icon_content = render_icon(icon) if icon else "" 117 | if href: 118 | attrs["href"] = href 119 | tag = "a" 120 | else: 121 | tag = "button" 122 | if id: 123 | attrs["id"] = id 124 | if name: 125 | attrs["name"] = name 126 | if value: 127 | attrs["value"] = value 128 | if title: 129 | attrs["title"] = title 130 | return render_tag( 131 | tag, 132 | attrs=attrs, 133 | content=mark_safe(text_concat(icon_content, content, separator=" ")), 134 | ) 135 | 136 | 137 | def render_field_and_label(field, label, field_class="", label_for=None, label_class="", layout="", **kwargs): 138 | """Render a field with its label.""" 139 | if layout == "horizontal": 140 | if not label_class: 141 | label_class = get_bootstrap_setting("horizontal_label_class") 142 | if not field_class: 143 | field_class = get_bootstrap_setting("horizontal_field_class") 144 | if not label: 145 | label = mark_safe(" ") 146 | label_class = add_css_class(label_class, "control-label") 147 | html = field 148 | if field_class: 149 | html = f'
{html}
' 150 | if label: 151 | html = render_label(label, label_for=label_for, label_class=label_class) + html 152 | return html 153 | 154 | 155 | def render_form_group(content, css_class=FORM_GROUP_CLASS): 156 | """Render a Bootstrap form group.""" 157 | return f'
{content}
' 158 | 159 | 160 | def is_widget_required_attribute(widget): 161 | """Return whether this widget is required.""" 162 | if not widget.is_required: 163 | return False 164 | if isinstance(widget, WIDGETS_NO_REQUIRED): 165 | return False 166 | return True 167 | 168 | 169 | def is_widget_with_placeholder(widget): 170 | """ 171 | Return whether this a widget that should have a placeholder. 172 | 173 | Only text, search, url, tel, e-mail, password, number have placeholders 174 | """ 175 | return isinstance(widget, (TextInput, Textarea, NumberInput, EmailInput, URLInput, PasswordInput)) 176 | -------------------------------------------------------------------------------- /src/bootstrap3/models.py: -------------------------------------------------------------------------------- 1 | # Empty models.py, required file for Django tests 2 | -------------------------------------------------------------------------------- /src/bootstrap3/renderers.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.forms import ( 4 | BaseForm, 5 | BaseFormSet, 6 | BoundField, 7 | CheckboxInput, 8 | CheckboxSelectMultiple, 9 | ClearableFileInput, 10 | DateInput, 11 | EmailInput, 12 | FileInput, 13 | MultiWidget, 14 | NumberInput, 15 | PasswordInput, 16 | RadioSelect, 17 | Select, 18 | SelectDateWidget, 19 | TextInput, 20 | URLInput, 21 | ) 22 | from django.utils.html import conditional_escape, escape, strip_tags 23 | from django.utils.safestring import mark_safe 24 | 25 | from .bootstrap import get_bootstrap_setting 26 | from .exceptions import BootstrapError 27 | from .forms import ( 28 | FORM_GROUP_CLASS, 29 | is_widget_with_placeholder, 30 | render_field, 31 | render_form, 32 | render_form_group, 33 | render_label, 34 | ) 35 | from .text import text_value 36 | from .utils import add_css_class, render_template_file 37 | 38 | try: 39 | # If Django is set up without a database, importing this widget gives RuntimeError 40 | from django.contrib.auth.forms import ReadOnlyPasswordHashWidget 41 | except RuntimeError: 42 | ReadOnlyPasswordHashWidget = None 43 | 44 | 45 | class BaseRenderer: 46 | """A content renderer.""" 47 | 48 | def __init__(self, *args, **kwargs): 49 | self.layout = kwargs.get("layout", "") 50 | self.form_group_class = kwargs.get("form_group_class", FORM_GROUP_CLASS) 51 | self.field_class = kwargs.get("field_class", "") 52 | self.label_class = kwargs.get("label_class", "") 53 | self.show_help = kwargs.get("show_help", True) 54 | self.show_label = kwargs.get("show_label", True) 55 | self.exclude = kwargs.get("exclude", "") 56 | 57 | self.set_placeholder = kwargs.get("set_placeholder", True) 58 | self.size = self.parse_size(kwargs.get("size", "")) 59 | self.horizontal_label_class = kwargs.get( 60 | "horizontal_label_class", get_bootstrap_setting("horizontal_label_class") 61 | ) 62 | self.horizontal_field_class = kwargs.get( 63 | "horizontal_field_class", get_bootstrap_setting("horizontal_field_class") 64 | ) 65 | 66 | def parse_size(self, size): 67 | size = text_value(size).lower().strip() 68 | if size in ("sm", "small"): 69 | return "small" 70 | if size in ("lg", "large"): 71 | return "large" 72 | if size in ("md", "medium", ""): 73 | return "medium" 74 | raise BootstrapError(f'Invalid value "{size}" for parameter "size" (expected "sm", "md", "lg" or "").') 75 | 76 | def get_size_class(self, prefix="input"): 77 | if self.size == "small": 78 | return prefix + "-sm" 79 | if self.size == "large": 80 | return prefix + "-lg" 81 | return "" 82 | 83 | def _render(self): 84 | return "" 85 | 86 | def render(self): 87 | return mark_safe(self._render()) 88 | 89 | 90 | class FormsetRenderer(BaseRenderer): 91 | """Default formset renderer.""" 92 | 93 | def __init__(self, formset, *args, **kwargs): 94 | if not isinstance(formset, BaseFormSet): 95 | raise BootstrapError('Parameter "formset" should contain a valid Django Formset.') 96 | self.formset = formset 97 | super().__init__(*args, **kwargs) 98 | 99 | def render_management_form(self): 100 | return text_value(self.formset.management_form) 101 | 102 | def render_form(self, form, **kwargs): 103 | return render_form(form, **kwargs) 104 | 105 | def render_forms(self): 106 | rendered_forms = [] 107 | for form in self.formset.forms: 108 | rendered_forms.append( 109 | self.render_form( 110 | form, 111 | layout=self.layout, 112 | form_group_class=self.form_group_class, 113 | field_class=self.field_class, 114 | label_class=self.label_class, 115 | show_label=self.show_label, 116 | show_help=self.show_help, 117 | exclude=self.exclude, 118 | set_placeholder=self.set_placeholder, 119 | size=self.size, 120 | horizontal_label_class=self.horizontal_label_class, 121 | horizontal_field_class=self.horizontal_field_class, 122 | ) 123 | ) 124 | return "\n".join(rendered_forms) 125 | 126 | def get_formset_errors(self): 127 | return self.formset.non_form_errors() 128 | 129 | def render_errors(self): 130 | formset_errors = self.get_formset_errors() 131 | if formset_errors: 132 | return render_template_file( 133 | "bootstrap3/form_errors.html", 134 | context={"errors": formset_errors, "form": self.formset, "layout": self.layout}, 135 | ) 136 | return "" 137 | 138 | def _render(self): 139 | return f"{self.render_errors()}{self.render_management_form()}{self.render_forms()}" 140 | 141 | 142 | class FormRenderer(BaseRenderer): 143 | """Default form renderer.""" 144 | 145 | def __init__(self, form, *args, **kwargs): 146 | if not isinstance(form, BaseForm): 147 | raise BootstrapError('Parameter "form" should contain a valid Django Form.') 148 | self.form = form 149 | super().__init__(*args, **kwargs) 150 | self.error_types = kwargs.get("error_types", "non_field_errors") 151 | self.error_css_class = kwargs.get("error_css_class", None) 152 | self.required_css_class = kwargs.get("required_css_class", None) 153 | self.bound_css_class = kwargs.get("bound_css_class", None) 154 | 155 | def render_fields(self): 156 | rendered_fields = [] 157 | for field in self.form: 158 | rendered_fields.append( 159 | render_field( 160 | field, 161 | layout=self.layout, 162 | form_group_class=self.form_group_class, 163 | field_class=self.field_class, 164 | label_class=self.label_class, 165 | show_label=self.show_label, 166 | show_help=self.show_help, 167 | exclude=self.exclude, 168 | set_placeholder=self.set_placeholder, 169 | size=self.size, 170 | horizontal_label_class=self.horizontal_label_class, 171 | horizontal_field_class=self.horizontal_field_class, 172 | error_css_class=self.error_css_class, 173 | required_css_class=self.required_css_class, 174 | bound_css_class=self.bound_css_class, 175 | ) 176 | ) 177 | return "\n".join(rendered_fields) 178 | 179 | def get_fields_errors(self): 180 | form_errors = [] 181 | for field in self.form: 182 | if not field.is_hidden and field.errors: 183 | form_errors += field.errors 184 | return form_errors 185 | 186 | def render_errors(self, error_types="all"): 187 | form_errors = [] 188 | if error_types == "all": 189 | form_errors = self.get_fields_errors() + self.form.non_field_errors() 190 | elif error_types == "field_errors": 191 | form_errors = self.get_fields_errors() 192 | elif error_types == "non_field_errors": 193 | form_errors = self.form.non_field_errors() 194 | elif error_types and error_types != "none": 195 | raise Exception('Illegal value "{}" for error_types.') 196 | 197 | if form_errors: 198 | return render_template_file( 199 | "bootstrap3/form_errors.html", 200 | context={"errors": form_errors, "form": self.form, "layout": self.layout, "error_types": error_types}, 201 | ) 202 | 203 | return "" 204 | 205 | def _render(self): 206 | return self.render_errors(self.error_types) + self.render_fields() 207 | 208 | 209 | class FieldRenderer(BaseRenderer): 210 | """Default field renderer.""" 211 | 212 | # These widgets will not be wrapped in a form-control class 213 | WIDGETS_NO_FORM_CONTROL = (CheckboxInput, RadioSelect, CheckboxSelectMultiple, FileInput) 214 | 215 | def __init__(self, field, *args, **kwargs): 216 | if not isinstance(field, BoundField): 217 | raise BootstrapError('Parameter "field" should contain a valid Django BoundField.') 218 | self.field = field 219 | super().__init__(*args, **kwargs) 220 | 221 | self.widget = field.field.widget 222 | self.is_multi_widget = isinstance(field.field.widget, MultiWidget) 223 | self.initial_attrs = self.widget.attrs.copy() 224 | self.field_help = text_value(mark_safe(field.help_text)) if self.show_help and field.help_text else "" 225 | self.field_errors = [conditional_escape(text_value(error)) for error in field.errors] 226 | 227 | self.label = kwargs.get("label", field.label) 228 | 229 | if "placeholder" in kwargs: 230 | # Find the placeholder in kwargs, even if it's empty 231 | self.placeholder = kwargs["placeholder"] 232 | elif get_bootstrap_setting("set_placeholder"): 233 | # If not found, see if we set the label 234 | self.placeholder = self.label 235 | else: 236 | # Or just set it to empty 237 | self.placeholder = "" 238 | if self.placeholder: 239 | self.placeholder = text_value(mark_safe(self.placeholder)) 240 | 241 | self.addon_before = kwargs.get("addon_before", self.widget.attrs.pop("addon_before", "")) 242 | self.addon_after = kwargs.get("addon_after", self.widget.attrs.pop("addon_after", "")) 243 | self.addon_before_class = kwargs.get( 244 | "addon_before_class", self.widget.attrs.pop("addon_before_class", "input-group-addon") 245 | ) 246 | self.addon_after_class = kwargs.get( 247 | "addon_after_class", self.widget.attrs.pop("addon_after_class", "input-group-addon") 248 | ) 249 | 250 | # These are set in Django or in the global BOOTSTRAP3 settings, and 251 | # they can be overwritten in the template 252 | error_css_class = kwargs.get("error_css_class", None) 253 | required_css_class = kwargs.get("required_css_class", None) 254 | bound_css_class = kwargs.get("bound_css_class", None) 255 | if error_css_class is not None: 256 | self.error_css_class = error_css_class 257 | else: 258 | self.error_css_class = getattr(field.form, "error_css_class", get_bootstrap_setting("error_css_class")) 259 | if required_css_class is not None: 260 | self.required_css_class = required_css_class 261 | else: 262 | self.required_css_class = getattr( 263 | field.form, "required_css_class", get_bootstrap_setting("required_css_class") 264 | ) 265 | if bound_css_class is not None: 266 | self.success_css_class = bound_css_class 267 | else: 268 | self.success_css_class = getattr(field.form, "bound_css_class", get_bootstrap_setting("success_css_class")) 269 | 270 | # If the form is marked as form.empty_permitted, do not set required class 271 | if self.field.form.empty_permitted: 272 | self.required_css_class = "" 273 | 274 | def restore_widget_attrs(self): 275 | self.widget.attrs = self.initial_attrs.copy() 276 | 277 | def add_class_attrs(self, widget=None): 278 | if widget is None: 279 | widget = self.widget 280 | classes = widget.attrs.get("class", "") 281 | if ReadOnlyPasswordHashWidget is not None and isinstance(widget, ReadOnlyPasswordHashWidget): 282 | # Render this is a static control 283 | classes = add_css_class(classes, "form-control-static", prepend=True) 284 | elif not isinstance(widget, self.WIDGETS_NO_FORM_CONTROL): 285 | classes = add_css_class(classes, "form-control", prepend=True) 286 | # For these widget types, add the size class here 287 | classes = add_css_class(classes, self.get_size_class()) 288 | widget.attrs["class"] = classes 289 | 290 | def add_placeholder_attrs(self, widget=None): 291 | if widget is None: 292 | widget = self.widget 293 | placeholder = widget.attrs.get("placeholder", self.placeholder) 294 | if placeholder and self.set_placeholder and is_widget_with_placeholder(widget): 295 | # TODO: Should this be stripped and/or escaped? 296 | widget.attrs["placeholder"] = placeholder 297 | 298 | def add_help_attrs(self, widget=None): 299 | if widget is None: 300 | widget = self.widget 301 | if not isinstance(widget, CheckboxInput): 302 | widget.attrs["title"] = widget.attrs.get("title", escape(strip_tags(self.field_help))) 303 | 304 | def add_widget_attrs(self): 305 | if self.is_multi_widget: 306 | widgets = self.widget.widgets 307 | else: 308 | widgets = [self.widget] 309 | for widget in widgets: 310 | self.add_class_attrs(widget) 311 | self.add_placeholder_attrs(widget) 312 | self.add_help_attrs(widget) 313 | 314 | def list_to_class(self, html, klass): 315 | classes = add_css_class(klass, self.get_size_class()) 316 | return re.sub(r"\s*
\s*", "" + div2) 331 | return '
' + html + "
" 332 | 333 | def fix_clearable_file_input(self, html): 334 | """ 335 | Fix a clearable file input. 336 | 337 | TODO: This needs improvement 338 | 339 | Currently Django returns 340 | Currently: 341 | dummy.txt 342 | 343 |
344 | Change: 345 | 346 |
347 | """ 348 | # TODO This needs improvement 349 | return f'
{html}
' 350 | 351 | def post_widget_render(self, html): 352 | if isinstance(self.widget, CheckboxSelectMultiple): 353 | html = self.list_to_class(html, "checkbox") 354 | elif isinstance(self.widget, RadioSelect): 355 | html = self.list_to_class(html, "radio") 356 | elif isinstance(self.widget, SelectDateWidget): 357 | html = self.fix_date_select_input(html) 358 | elif isinstance(self.widget, ClearableFileInput): 359 | html = self.fix_clearable_file_input(html) 360 | elif isinstance(self.widget, CheckboxInput): 361 | html = self.put_inside_label(html) 362 | return html 363 | 364 | def wrap_widget(self, html): 365 | if isinstance(self.widget, CheckboxInput): 366 | # Wrap checkboxes 367 | # Note checkboxes do not get size classes, see #318 368 | html = f'
{html}
' 369 | return html 370 | 371 | def make_input_group(self, html): 372 | if (self.addon_before or self.addon_after) and isinstance( 373 | self.widget, (TextInput, NumberInput, EmailInput, URLInput, DateInput, Select, PasswordInput) 374 | ): 375 | before = f'{self.addon_before}' if self.addon_before else "" 376 | after = f'{self.addon_after}' if self.addon_after else "" 377 | html = f'
{before}{html}{after}
' 378 | return html 379 | 380 | def append_to_field(self, html): 381 | help_text_and_errors = [] 382 | if self.field_help: 383 | help_text_and_errors.append(self.field_help) 384 | help_text_and_errors += self.field_errors 385 | if help_text_and_errors: 386 | help_html = render_template_file( 387 | "bootstrap3/field_help_text_and_errors.html", 388 | context={ 389 | "field": self.field, 390 | "errors": self.field_errors, 391 | "help_text": self.field_help, 392 | "help_text_and_errors": help_text_and_errors, 393 | "layout": self.layout, 394 | "show_help": self.show_help, 395 | }, 396 | ) 397 | html += help_html 398 | return html 399 | 400 | def get_field_class(self): 401 | field_class = self.field_class 402 | if not field_class and self.layout == "horizontal": 403 | field_class = self.horizontal_field_class 404 | return field_class 405 | 406 | def wrap_field(self, html): 407 | field_class = self.get_field_class() 408 | if field_class: 409 | html = f'
{html}
' 410 | return html 411 | 412 | def get_label_class(self): 413 | label_class = self.label_class 414 | if not label_class and self.layout == "horizontal": 415 | label_class = self.horizontal_label_class 416 | label_class = text_value(label_class) 417 | if not self.show_label: 418 | label_class = add_css_class(label_class, "sr-only") 419 | return add_css_class(label_class, "control-label") 420 | 421 | def get_label(self): 422 | if isinstance(self.widget, CheckboxInput): 423 | label = None 424 | else: 425 | label = self.label 426 | if self.layout == "horizontal" and not label: 427 | return mark_safe(" ") 428 | return label 429 | 430 | def add_label(self, html): 431 | label = self.get_label() 432 | if label: 433 | html = render_label(label, label_for=self.field.id_for_label, label_class=self.get_label_class()) + html 434 | return html 435 | 436 | def get_form_group_class(self): 437 | form_group_class = self.form_group_class 438 | if self.field.errors: 439 | if self.error_css_class: 440 | form_group_class = add_css_class(form_group_class, self.error_css_class) 441 | else: 442 | if self.field.form.is_bound: 443 | form_group_class = add_css_class(form_group_class, self.success_css_class) 444 | if self.field.field.required and self.required_css_class: 445 | form_group_class = add_css_class(form_group_class, self.required_css_class) 446 | if self.layout == "horizontal": 447 | form_group_class = add_css_class(form_group_class, self.get_size_class(prefix="form-group")) 448 | return form_group_class 449 | 450 | def wrap_label_and_field(self, html): 451 | return render_form_group(html, self.get_form_group_class()) 452 | 453 | def _render(self): 454 | # See if we're not excluded 455 | if self.field.name in self.exclude.replace(" ", "").split(","): 456 | return "" 457 | # Hidden input requires no special treatment 458 | if self.field.is_hidden: 459 | return text_value(self.field) 460 | # Render the widget 461 | self.add_widget_attrs() 462 | html = self.field.as_widget(attrs=self.widget.attrs) 463 | self.restore_widget_attrs() 464 | # Start post render 465 | html = self.post_widget_render(html) 466 | html = self.wrap_widget(html) 467 | html = self.make_input_group(html) 468 | html = self.append_to_field(html) 469 | html = self.wrap_field(html) 470 | html = self.add_label(html) 471 | html = self.wrap_label_and_field(html) 472 | return html 473 | 474 | 475 | class InlineFieldRenderer(FieldRenderer): 476 | """Inline field renderer.""" 477 | 478 | def add_error_attrs(self): 479 | field_title = self.widget.attrs.get("title", "") 480 | field_title += " " + " ".join([strip_tags(e) for e in self.field_errors]) 481 | self.widget.attrs["title"] = field_title.strip() 482 | 483 | def add_widget_attrs(self): 484 | super().add_widget_attrs() 485 | self.add_error_attrs() 486 | 487 | def append_to_field(self, html): 488 | return html 489 | 490 | def get_field_class(self): 491 | return self.field_class 492 | 493 | def get_label_class(self): 494 | return add_css_class(self.label_class, "sr-only") 495 | -------------------------------------------------------------------------------- /src/bootstrap3/templates/bootstrap3/bootstrap3.html: -------------------------------------------------------------------------------- 1 | 2 | {% load bootstrap3 %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block bootstrap3_title %}django-bootstrap3 template title{% endblock %} 10 | {% bootstrap_css %} 11 | 12 | 13 | 17 | {% if 'javascript_in_head'|bootstrap_setting %}{% bootstrap_javascript jquery=True %}{% endif %} 18 | {% block bootstrap3_extra_head %}{% endblock %} 19 | 20 | 21 | 22 | {% block bootstrap3_content %}django-bootstrap3 template content{% endblock %} 23 | {% if not 'javascript_in_head'|bootstrap_setting %}{% bootstrap_javascript jquery=True %}{% endif %} 24 | {% block bootstrap3_extra_script %}{% endblock %} 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/bootstrap3/templates/bootstrap3/field_error.html: -------------------------------------------------------------------------------- 1 |
{{ error }}
2 | -------------------------------------------------------------------------------- /src/bootstrap3/templates/bootstrap3/field_help_text.html: -------------------------------------------------------------------------------- 1 |
{{ help_text }}
2 | -------------------------------------------------------------------------------- /src/bootstrap3/templates/bootstrap3/field_help_text_and_errors.html: -------------------------------------------------------------------------------- 1 | {% for error in errors %} 2 | {% include "bootstrap3/field_error.html" with field=field error=error only %} 3 | {% endfor %} 4 | {% if help_text %} 5 | {% include "bootstrap3/field_help_text.html" with field=field help_text=help_text only %} 6 | {% endif %} -------------------------------------------------------------------------------- /src/bootstrap3/templates/bootstrap3/form_errors.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/bootstrap3/templates/bootstrap3/messages.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap3 %} 2 | {% for message in messages %} 3 |
4 | 5 | {{ message }} 6 |
7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /src/bootstrap3/templates/bootstrap3/pagination.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap3 %} 2 | {% with bpurl=bootstrap_pagination_url|default:"" %} 3 | 4 |
    5 | 6 | {% if current_page == 1 %} 7 | 8 | {% else %} 9 | 12 | {% endif %} 13 | 14 | {% if pages_back %} 15 |
  • 16 | 17 |
  • 18 | {% endif %} 19 | 20 | {% for p in pages_shown %} 21 | {% if current_page == p %} 22 |
  • 23 | {{ p }} 24 |
  • 25 | {% else %} 26 |
  • 27 | {{ p }} 28 |
  • 29 | {% endif %} 30 | {% endfor %} 31 | 32 | {% if pages_forward %} 33 |
  • 34 | 35 |
  • 36 | {% endif %} 37 | 38 | {% if current_page == num_pages %} 39 |
  • 40 | » 41 |
  • 42 | {% else %} 43 |
  • 44 | » 45 |
  • 46 | {% endif %} 47 | 48 |
49 | 50 | {% endwith %} 51 | -------------------------------------------------------------------------------- /src/bootstrap3/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zostera/django-bootstrap3/77345eb7f75d05fb701266c44462021fc60f0c22/src/bootstrap3/templatetags/__init__.py -------------------------------------------------------------------------------- /src/bootstrap3/templatetags/bootstrap3.py: -------------------------------------------------------------------------------- 1 | import re 2 | from math import floor 3 | 4 | from django import template 5 | from django.contrib.messages import constants as message_constants 6 | from django.template import Context 7 | from django.utils.encoding import force_str 8 | from django.utils.safestring import mark_safe 9 | 10 | from ..bootstrap import css_url, get_bootstrap_setting, javascript_url, jquery_url, theme_url 11 | from ..components import render_alert, render_icon 12 | from ..forms import ( 13 | render_button, 14 | render_field, 15 | render_field_and_label, 16 | render_form, 17 | render_form_errors, 18 | render_form_group, 19 | render_formset, 20 | render_formset_errors, 21 | render_label, 22 | ) 23 | from ..utils import ( 24 | handle_var, 25 | parse_token_contents, 26 | render_link_tag, 27 | render_script_tag, 28 | render_template_file, 29 | url_replace_param, 30 | ) 31 | 32 | MESSAGE_LEVEL_CLASSES = { 33 | message_constants.DEBUG: "alert alert-warning", 34 | message_constants.INFO: "alert alert-info", 35 | message_constants.SUCCESS: "alert alert-success", 36 | message_constants.WARNING: "alert alert-warning", 37 | message_constants.ERROR: "alert alert-danger", 38 | } 39 | 40 | register = template.Library() 41 | 42 | 43 | @register.filter 44 | def bootstrap_setting(value): 45 | """ 46 | Return value of a setting. 47 | 48 | Please consider this filter private for now, do not use it in your own templates. 49 | """ 50 | return get_bootstrap_setting(value) 51 | 52 | 53 | @register.filter 54 | def bootstrap_message_classes(message): 55 | """Return the message classes for a message.""" 56 | extra_tags = None 57 | try: 58 | extra_tags = message.extra_tags 59 | except AttributeError: 60 | pass 61 | if not extra_tags: 62 | extra_tags = "" 63 | classes = [extra_tags] 64 | try: 65 | level = message.level 66 | except AttributeError: 67 | pass 68 | else: 69 | try: 70 | classes.append(MESSAGE_LEVEL_CLASSES[level]) 71 | except KeyError: 72 | classes.append("alert alert-danger") 73 | return " ".join(classes).strip() 74 | 75 | 76 | @register.simple_tag 77 | def bootstrap_jquery_url(): 78 | """ 79 | Return url to jquery resource. 80 | 81 | **Tag name**:: 82 | 83 | bootstrap_jquery_url 84 | 85 | Return the full url to jQuery file to use 86 | 87 | Default value: ``//code.jquery.com/jquery.min.js`` 88 | 89 | This value is configurable, see Settings section 90 | 91 | **Usage**:: 92 | 93 | {% bootstrap_jquery_url %} 94 | 95 | **Example**:: 96 | 97 | {% bootstrap_jquery_url %} 98 | """ 99 | return jquery_url() 100 | 101 | 102 | @register.simple_tag 103 | def bootstrap_javascript_url(): 104 | """ 105 | Return the full url to the Bootstrap JavaScript library. 106 | 107 | Default value: ``None`` 108 | 109 | This value is configurable, see Settings section 110 | 111 | **Tag name**:: 112 | 113 | bootstrap_javascript_url 114 | 115 | **Usage**:: 116 | 117 | {% bootstrap_javascript_url %} 118 | 119 | **Example**:: 120 | 121 | {% bootstrap_javascript_url %} 122 | """ 123 | return javascript_url() 124 | 125 | 126 | @register.simple_tag 127 | def bootstrap_css_url(): 128 | """ 129 | Return the full url to the Bootstrap CSS library. 130 | 131 | Default value: ``None`` 132 | 133 | This value is configurable, see Settings section 134 | 135 | **Tag name**:: 136 | 137 | bootstrap_css_url 138 | 139 | **Usage**:: 140 | 141 | {% bootstrap_css_url %} 142 | 143 | **Example**:: 144 | 145 | {% bootstrap_css_url %} 146 | """ 147 | return css_url() 148 | 149 | 150 | @register.simple_tag 151 | def bootstrap_theme_url(): 152 | """ 153 | Return the full url to a Bootstrap theme CSS library. 154 | 155 | Default value: ``None`` 156 | 157 | This value is configurable, see Settings section 158 | 159 | **Tag name**:: 160 | 161 | bootstrap_theme_url 162 | 163 | **Usage**:: 164 | 165 | {% bootstrap_theme_url %} 166 | 167 | **Example**:: 168 | 169 | {% bootstrap_theme_url %} 170 | """ 171 | return theme_url() 172 | 173 | 174 | @register.simple_tag 175 | def bootstrap_css(): 176 | """ 177 | Return HTML for Bootstrap CSS. Adjust url in settings. 178 | 179 | If no url is returned, we don't want this statement to return any HTML. This is intended behavior. 180 | 181 | Default value: ``None`` 182 | 183 | This value is configurable, see Settings section 184 | 185 | **Tag name**:: 186 | 187 | bootstrap_css 188 | 189 | **Usage**:: 190 | 191 | {% bootstrap_css %} 192 | 193 | **Example**:: 194 | 195 | {% bootstrap_css %} 196 | """ 197 | rendered_urls = [render_link_tag(bootstrap_css_url())] 198 | if bootstrap_theme_url(): 199 | rendered_urls.append(render_link_tag(bootstrap_theme_url())) 200 | return mark_safe("".join([url for url in rendered_urls])) 201 | 202 | 203 | @register.simple_tag 204 | def bootstrap_javascript(jquery=None): 205 | """ 206 | Return HTML for Bootstrap JavaScript. 207 | 208 | Adjust url in settings. If no url is returned, we don't want this 209 | statement to return any HTML. 210 | This is intended behavior. 211 | 212 | Default value: ``None`` 213 | 214 | This value is configurable, see Settings section 215 | 216 | **Tag name**:: 217 | 218 | bootstrap_javascript 219 | 220 | **Parameters**: 221 | 222 | :jquery: Truthy to include jQuery as well as Bootstrap 223 | 224 | **Usage**:: 225 | 226 | {% bootstrap_javascript %} 227 | 228 | **Example**:: 229 | 230 | {% bootstrap_javascript jquery=1 %} 231 | """ 232 | javascript = "" 233 | # See if we have to include jQuery 234 | if jquery is None: 235 | jquery = get_bootstrap_setting("include_jquery", False) 236 | # NOTE: No async on scripts, not mature enough. See issue #52 and #56 237 | if jquery: 238 | url = bootstrap_jquery_url() 239 | if url: 240 | javascript += render_script_tag(url) 241 | url = bootstrap_javascript_url() 242 | if url: 243 | javascript += render_script_tag(url) 244 | return mark_safe(javascript) 245 | 246 | 247 | @register.simple_tag 248 | def bootstrap_formset(*args, **kwargs): 249 | """ 250 | Render a formset. 251 | 252 | **Tag name**:: 253 | 254 | bootstrap_formset 255 | 256 | **Parameters**: 257 | 258 | formset 259 | The formset that is being rendered 260 | 261 | 262 | See bootstrap_field_ for other arguments 263 | 264 | **Usage**:: 265 | 266 | {% bootstrap_formset formset %} 267 | 268 | **Example**:: 269 | 270 | {% bootstrap_formset formset layout='horizontal' %} 271 | """ 272 | return render_formset(*args, **kwargs) 273 | 274 | 275 | @register.simple_tag 276 | def bootstrap_formset_errors(*args, **kwargs): 277 | """ 278 | Render formset errors. 279 | 280 | **Tag name**:: 281 | 282 | bootstrap_formset_errors 283 | 284 | **Parameters**: 285 | 286 | formset 287 | The formset that is being rendered 288 | 289 | layout 290 | Context value that is available in the template ``bootstrap3/form_errors.html`` 291 | as ``layout``. 292 | 293 | **Usage**:: 294 | 295 | {% bootstrap_formset_errors formset %} 296 | 297 | **Example**:: 298 | 299 | {% bootstrap_formset_errors formset layout='inline' %} 300 | """ 301 | return render_formset_errors(*args, **kwargs) 302 | 303 | 304 | @register.simple_tag 305 | def bootstrap_form(*args, **kwargs): 306 | """ 307 | Render a form. 308 | 309 | **Tag name**:: 310 | 311 | bootstrap_form 312 | 313 | **Parameters**: 314 | 315 | form 316 | The form that is to be rendered 317 | 318 | exclude 319 | A list of field names (comma separated) that should not be rendered 320 | E.g. exclude=subject,bcc 321 | 322 | error_types 323 | This controls the types of errors that are rendered above the form. 324 | Choices are: "all", "field_errors", "non_field_errors" or "none". This will not 325 | affect the display of errors on the fields themselves. 326 | 327 | Default is "non_field_errors". 328 | 329 | See bootstrap_field_ for other arguments 330 | 331 | **Usage**:: 332 | 333 | {% bootstrap_form form %} 334 | 335 | **Example**:: 336 | 337 | {% bootstrap_form form layout='inline' %} 338 | """ 339 | return render_form(*args, **kwargs) 340 | 341 | 342 | @register.simple_tag 343 | def bootstrap_form_errors(*args, **kwargs): 344 | """ 345 | Render form errors. 346 | 347 | **Tag name**:: 348 | 349 | bootstrap_form_errors 350 | 351 | **Parameters**: 352 | 353 | form 354 | The form that is to be rendered 355 | 356 | error_types 357 | Control which type of errors should be rendered. 358 | 359 | One of the following values: 360 | 361 | * ``'all'`` 362 | * ``'field_errors'`` 363 | * ``'non_field_errors'`` 364 | 365 | :default: ``'non_field_errors'`` 366 | 367 | layout 368 | Context value that is available in the template ``bootstrap3/form_errors.html`` as ``layout``. 369 | 370 | **Usage**:: 371 | 372 | {% bootstrap_form_errors form %} 373 | 374 | **Example**:: 375 | 376 | {% bootstrap_form_errors form layout='inline' %} 377 | """ 378 | return render_form_errors(*args, **kwargs) 379 | 380 | 381 | @register.simple_tag 382 | def bootstrap_field(*args, **kwargs): 383 | """ 384 | Render a field. 385 | 386 | **Tag name**:: 387 | 388 | bootstrap_field 389 | 390 | **Parameters**: 391 | 392 | 393 | field 394 | The form field to be rendered 395 | 396 | layout 397 | If set to ``'horizontal'`` then the field and label will be rendered side-by-side, as long as there 398 | is no ``field_class`` set as well. 399 | 400 | form_group_class 401 | CSS class of the ``div`` that wraps the field and label. 402 | 403 | :default: ``'form-group'`` 404 | 405 | field_class 406 | CSS class of the ``div`` that wraps the field. 407 | 408 | label_class 409 | CSS class of the ``label`` element. Will always have ``control-label`` as the last CSS class. 410 | 411 | show_help 412 | Show the field's help text, if the field has help text. 413 | 414 | :default: ``True`` 415 | 416 | show_label 417 | Whether the show the label of the field. 418 | 419 | :default: ``True`` 420 | 421 | exclude 422 | A list of field names that should not be rendered 423 | 424 | size 425 | Controls the size of the rendered ``div.form-group`` through the use of CSS classes. 426 | 427 | One of the following values: 428 | 429 | * ``'small'`` 430 | * ``'medium'`` 431 | * ``'large'`` 432 | 433 | placeholder 434 | Set/overwrite the field's placeholder. 435 | 436 | label 437 | Overwrite the field's label. 438 | 439 | horizontal_label_class 440 | Class used on the label when the ``layout`` is set to ``horizontal``. 441 | 442 | :default: ``'col-md-3'``. Can be changed in :doc:`settings` 443 | 444 | horizontal_field_class 445 | Class used on the field when the ``layout`` is set to ``horizontal``. 446 | 447 | :default: ``'col-md-9'``. Can be changed in :doc:`settings` 448 | 449 | addon_before 450 | Text that should be prepended to the form field. Can also be an icon, e.g. 451 | ``''`` 452 | 453 | See the `Bootstrap docs ` for more examples. 454 | 455 | addon_after 456 | Text that should be appended to the form field. Can also be an icon, e.g. 457 | ``''`` 458 | 459 | See the `Bootstrap docs ` for more examples. 460 | 461 | addon_before_class 462 | Class used on the span when ``addon_before`` is used. 463 | 464 | One of the following values: 465 | 466 | * ``'input-group-addon'`` 467 | * ``'input-group-btn'`` 468 | 469 | :default: ``input-group-addon`` 470 | 471 | addon_after_class 472 | Class used on the span when ``addon_after`` is used. 473 | 474 | One of the following values: 475 | 476 | * ``'input-group-addon'`` 477 | * ``'input-group-btn'`` 478 | 479 | :default: ``input-group-addon`` 480 | 481 | error_css_class 482 | CSS class used when the field has an error 483 | 484 | :default: ``'has-error'``. Can be changed :doc:`settings` 485 | 486 | required_css_class 487 | CSS class used on the ``div.form-group`` to indicate a field is required 488 | 489 | :default: ``''``. Can be changed :doc:`settings` 490 | 491 | bound_css_class 492 | CSS class used when the field is bound 493 | 494 | :default: ``'has-success'``. Can be changed :doc:`settings` 495 | 496 | **Usage**:: 497 | 498 | {% bootstrap_field field %} 499 | 500 | **Example**:: 501 | 502 | {% bootstrap_field field show_label=False %} 503 | """ 504 | return render_field(*args, **kwargs) 505 | 506 | 507 | @register.simple_tag() 508 | def bootstrap_label(*args, **kwargs): 509 | """ 510 | Render a label. 511 | 512 | **Tag name**:: 513 | 514 | bootstrap_label 515 | 516 | **Parameters**: 517 | 518 | content 519 | The label's text 520 | 521 | label_for 522 | The value that will be in the ``for`` attribute of the rendered ``