├── .github └── workflows │ └── ci.yml ├── .gitignore ├── AUTHORS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── docs ├── authors.rst ├── changelog.rst ├── conf.py ├── example_template.rst ├── index.rst ├── installation.rst ├── migrate.rst ├── quickstart.rst ├── settings.rst ├── templates.rst ├── templatetags.rst └── widgets.rst ├── example ├── README.md ├── __init__.py ├── app │ ├── __init__.py │ ├── forms.py │ └── views.py ├── manage.py ├── requirements.txt ├── settings.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 ├── manage.py ├── poetry.lock ├── pyproject.toml ├── readthedocs.yml ├── src └── bootstrap5 │ ├── __init__.py │ ├── bootstrap.py │ ├── components.py │ ├── exceptions.py │ ├── forms.py │ ├── models.py │ ├── renderers.py │ ├── templates │ └── bootstrap5 │ │ ├── bootstrap5.html │ │ ├── field_errors.html │ │ ├── field_help_text.html │ │ ├── form_errors.html │ │ ├── messages.html │ │ ├── pagination.html │ │ └── widgets │ │ └── radio_select_button_group.html │ ├── templatetags │ ├── __init__.py │ └── bootstrap5.py │ ├── text.py │ ├── utils.py │ └── widgets.py ├── tests ├── __init__.py ├── app │ ├── __init__.py │ ├── settings.py │ └── urls.py ├── test_components.py ├── test_settings.py ├── test_templates.py ├── test_templatetags.py ├── test_version.py └── utils.py └── tox.ini /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | 7 | test: 8 | name: Python ${{ matrix.python-version }} 9 | runs-on: ubuntu-latest 10 | strategy: 11 | max-parallel: 5 12 | matrix: 13 | python-version: [3.6, 3.7, 3.8, 3.9] 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install GDAL binaries 25 | run: | 26 | sudo apt-get install binutils libproj-dev gdal-bin 27 | 28 | - run: pip install -U pip 29 | - run: pip install -U poetry tox coverage coveralls 30 | 31 | - name: Test with tox 32 | run: tox -e py 33 | 34 | - name: Coverage combine 35 | run: coverage combine 36 | 37 | - name: Coverage report 38 | run: coverage report 39 | 40 | - name: Upload coveralls 41 | env: 42 | COVERALLS_PARALLEL: true 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | run: coveralls 45 | 46 | coveralls: 47 | needs: test 48 | runs-on: ubuntu-latest 49 | 50 | steps: 51 | - name: Coveralls Finished 52 | uses: coverallsapp/github-action@master 53 | with: 54 | github-token: ${{ secrets.GITHUB_TOKEN }} 55 | parallel-finished: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .venv 3 | # Python 4 | *.py[cod] 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Packages 10 | .eggs 11 | *.egg 12 | *.egg-info 13 | dist 14 | build 15 | eggs 16 | parts 17 | bin 18 | var 19 | sdist 20 | develop-eggs 21 | .installed.cfg 22 | lib 23 | lib64 24 | pip-wheel-metadata/ 25 | 26 | # Installer logs 27 | pip-log.txt 28 | *.log 29 | 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .tox/ 33 | .coverage 34 | .coverage.* 35 | .cache 36 | nosetests.xml 37 | coverage.xml 38 | *,cover 39 | 40 | # Translations 41 | *.mo 42 | *.pot 43 | 44 | # Mr Developer 45 | .mr.developer.cfg 46 | .project 47 | .pydevproject 48 | 49 | # Complexity 50 | output/*.html 51 | output/*/index.html 52 | 53 | # Sphinx 54 | docs/_build 55 | 56 | # Pycharm 57 | .idea* 58 | 59 | # Django 60 | local_settings.py 61 | 62 | # pyenv 63 | .python-version 64 | reports 65 | 66 | # example database 67 | example/db.sqlite3 68 | 69 | *.swp* 70 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | This application is developed and maintained by [Zostera](https://zostera.nl). 4 | 5 | ## Development Lead 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 | - Jay Pipes 17 | - Jonas Hagstedt 18 | - Jordan Starcher 19 | - Juan Carlos 20 | - Markus Holtermann 21 | - Nick S 22 | - Owais Lone 23 | - pmav99 24 | - Richard Hajdu 25 | - Timothy Allen 26 | - Simon Berndtsson 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0.11] - Update dependencies 2022-02-10 4 | 5 | - Update to the latest Bootstrap 5.1.3 URLs 6 | 7 | ## [1.0.10] - Update dependencies 2022-02-05 8 | 9 | - Update Sphinx dependencies 10 | 11 | ## [1.0.9] - Update dependencies 2021-12-31 12 | 13 | - Fix Django 4 sample site 14 | 15 | ## [1.0.7] - Update dependencies 2021-11-05 16 | 17 | - Allow Django 4 18 | 19 | ## [1.0.6] - Update dependencies 2021-10-08 20 | 21 | - Update to the latest Bootstrap 5.1 URLs 22 | 23 | ## [1.0.5] - Update dependencies 2021-08-04 24 | 25 | - Update to the latest Bootstrap 5 URLs 26 | 27 | ## [1.0.4] - Update dependencies 2021-05-16 28 | 29 | - importlib-metadata version dependency 30 | 31 | ## [1.0.3] - Fix small bugs 2021-05-01 32 | 33 | - Fix inline file field display 34 | 35 | ## [1.0.2] - Fix small bugs 2021-04-08 36 | 37 | - Fix checkbox label display 38 | 39 | ## [1.0.1] - More Bootstrap 5 adjustments 2021-03-23 40 | 41 | - Form rendering is not adapted to v5 42 | 43 | ## [1.0.0] - Initial release 2020-12-12 44 | 45 | - Convert from bootstrap 4 to 5 46 | -------------------------------------------------------------------------------- /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-bootstrap-v5` could always use more documentation, whether as part of the official django-bootstrap-v5 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-bootstrap-v5` 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 | 1. Fork and clone `django-bootstrap-v5` repo on GitHub. There is an excellent guide at . 49 | 2. Install [poetry](https://python-poetry.org). 50 | 3. Inside your local `django-bootstrap-v5` folder, run 51 | ```shell script 52 | $ poetry install 53 | ``` 54 | 4. Create a branch for local development: 55 | ```shell script 56 | $ git checkout -b name-of-your-bugfix-or-feature 57 | ``` 58 | Now you can make your changes locally. 59 | 5. When you\'re done making changes, check that your changes pass the tests. 60 | Run the unit tests in your virtual environment with the `manage.py` command: 61 | ```shell script 62 | $ python manage.py test 63 | ```` 64 | Run the extended tests with `tox`: 65 | ```shell script 66 | $ make tox 67 | ``` 68 | 6. Commit your changes and push your branch to GitHub: 69 | ```shell script 70 | $ git add . 71 | $ git commit -m "Your detailed description of your changes." 72 | $ git push origin name-of-your-bugfix-or-feature 73 | ``` 74 | 7. Submit a pull request through the GitHub website. 75 | 76 | ## Pull Request Guidelines 77 | 78 | Before you submit a pull request, check that it meets these guidelines: 79 | 80 | 1. The pull request should include tests. 81 | 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. 82 | 3. The pull request should pass the Continuous Integration tests. Check and make sure that all tests pass. You can run the tests locally using `tox`. 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) Zostera B.V. 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test tox reformat lint docs porcelain branch build publish 2 | 3 | PROJECT_DIR=src/bootstrap5 4 | PYTHON_SOURCES=${PROJECT_DIR} tests *.py 5 | 6 | test: 7 | coverage run manage.py test 8 | coverage report 9 | 10 | tox: 11 | rm -rf .tox 12 | tox 13 | 14 | reformat: 15 | autoflake -ir --remove-all-unused-imports ${PYTHON_SOURCES} 16 | isort ${PYTHON_SOURCES} 17 | docformatter -ir --pre-summary-newline --wrap-summaries=0 --wrap-descriptions=0 ${PYTHON_SOURCES} 18 | black . 19 | 20 | lint: 21 | flake8 ${PYTHON_SOURCES} 22 | pydocstyle --add-ignore=D1,D202,D301,D413 ${PYTHON_SOURCES} 23 | 24 | docs: 25 | cd docs && sphinx-build -b html -d _build/doctrees . _build/html 26 | 27 | porcelain: 28 | ifeq ($(shell git status --porcelain),) 29 | @echo "Working directory is clean." 30 | else 31 | @echo "Error - working directory is dirty. Commit those changes!"; 32 | @exit 1; 33 | endif 34 | 35 | branch: 36 | ifeq ($(shell git rev-parse --abbrev-ref HEAD),main) 37 | @echo "On branch main." 38 | else 39 | @echo "Error - Not on branch main!" 40 | @exit 1; 41 | endif 42 | 43 | build: docs 44 | poetry build 45 | 46 | publish: porcelain branch build 47 | poetry publish 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-bootstrap v5 2 | 3 | **NB:** this project is not actively maintained at the moment. However, there is an alternative, which is quite up to date. See here: https://github.com/zostera/django-bootstrap5 4 | 5 | This package builds on top of the excellent [django-bootstrap4](https://github.com/zostera/django-bootstrap4) package. 6 | 7 | [![CI](https://github.com/zelenij/django-bootstrap-v5/workflows/CI/badge.svg?branch=main)](https://github.com/django-bootstrap-v5/actions?workflow=CI) 8 | [![Coverage Status](https://coveralls.io/repos/github/django-bootstrap-v5/badge.svg?branch=main)](https://coveralls.io/github/django-bootstrap-v5?branch=main) 9 | [![Latest PyPI version](https://img.shields.io/pypi/v/django-bootstrap-v5.svg)](https://pypi.python.org/pypi/django-bootstrap-v5) 10 | [![Any color you like](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 11 | 12 | Bootstrap 5 integration for Django. 13 | 14 | ## Package name 15 | 16 | Unfortunately, someone squatted on the django-bootstrap-v5 (as well as 6, 7, 8 etc) name in PyPi in 2013, so I had to modify the name of this package accordingly. 17 | 18 | ## Goal 19 | 20 | The goal of this project is to seamlessly blend Django and Bootstrap 5. 21 | 22 | ## Requirements 23 | 24 | Python 3.6 or newer with Django >= 2.2 or newer. 25 | 26 | ## Documentation 27 | 28 | The full documentation is at https://django-bootstrap-v5.readthedocs.io/ 29 | 30 | ## Installation 31 | 32 | 1. Install using pip: 33 | 34 | ```shell script 35 | pip install django-bootstrap-v5 36 | ``` 37 | 38 | Alternatively, you can install download or clone this repo and call ``pip install -e .``. 39 | 40 | 2. Add to `INSTALLED_APPS` in your `settings.py`: 41 | 42 | ```python 43 | INSTALLED_APPS = ( 44 | # ... 45 | "bootstrap5", 46 | # ... 47 | ) 48 | ```` 49 | 50 | 3. In your templates, load the `bootstrap5` library and use the `bootstrap_*` tags: 51 | 52 | ## Example template 53 | 54 | ```jinja 55 | {% load bootstrap5 %} 56 | 57 | {# Display a form #} 58 | 59 |
60 | {% csrf_token %} 61 | {% bootstrap_form form %} 62 | {% buttons %} 63 | 64 | {% endbuttons %} 65 |
66 | ``` 67 | 68 | Demo 69 | ---- 70 | 71 | A demo app is provided in `demo`. You can run it from your virtualenv with `python manage.py runserver`. 72 | 73 | 74 | Bugs and suggestions 75 | -------------------- 76 | 77 | If you have found a bug or if you have a request for additional functionality, please use the issue tracker on GitHub. 78 | 79 | https://github.com/zelenij/django-bootstrap-v5/issues 80 | 81 | 82 | License 83 | ------- 84 | 85 | You can use this under BSD-3-Clause. See [LICENSE](LICENSE) file for details. 86 | 87 | 88 | Author 89 | ------ 90 | 91 | Developed and maintained by [Andre Bar'yudin](https://www.baryudin.com) 92 | 93 | Original authors: 94 | 95 | * [Zostera](https://zostera.nl) 96 | * [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.md](AUTHORS.md) for a list of contributors. 101 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. mdinclude:: ../AUTHORS.md -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. mdinclude:: ../CHANGELOG.md -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | #try: 4 | # from importlib.metadata import metadata 5 | #except ImportError: 6 | # from importlib_metadata import metadata 7 | # 8 | #PROJECT_NAME = "django-bootstrap-v5" 9 | # 10 | #on_rtd = os.environ.get("READTHEDOCS", None) == "True" 11 | #project_metadata = metadata(PROJECT_NAME) 12 | #print(project_metadata) 13 | # 14 | #project = project_metadata["name"] 15 | #author = project_metadata["author"] 16 | #copyright = f"2020, {author}" 17 | # 18 | ## The full version, including alpha/beta/rc tags, in x.y.z.misc format 19 | #release = project_metadata["version"] 20 | ## The short X.Y version. 21 | #version = ".".join(release.split(".")[:2]) 22 | # 23 | #extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "m2r2"] 24 | #source_suffix = [".rst", ".md"] 25 | #pygments_style = "sphinx" 26 | #htmlhelp_basename = f"{PROJECT_NAME}-doc" 27 | # 28 | #if not on_rtd: # only import and set the theme if we're building docs locally 29 | # import sphinx_rtd_theme 30 | # 31 | # html_theme = "sphinx_rtd_theme" 32 | # html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 33 | -------------------------------------------------------------------------------- /docs/example_template.rst: -------------------------------------------------------------------------------- 1 | .. code:: django 2 | 3 | {# Load the tag library #} 4 | {% load bootstrap5 %} 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-bootstrap-v5's documentation! 2 | =============================================== 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | installation 10 | quickstart 11 | migrate 12 | templatetags 13 | settings 14 | templates 15 | widgets 16 | authors 17 | changelog 18 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | The preferred way to install ``django-bootstrap-v5`` is ``pip``:: 6 | 7 | $ pip install django-bootstrap-v5 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-bootstrap-v5`` 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 | ``'bootstrap5',`` 20 | 21 | After installation, the :doc:`quickstart` will get you on your way to using ``django-bootstrap-v5``. 22 | -------------------------------------------------------------------------------- /docs/migrate.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Migration 3 | ========= 4 | 5 | Below is a list of caveats when migrating from Bootstrap3/django-bootstrap3 to Bootstrap4/django-bootstrap4. 6 | 7 | This document only considers the differences between django-bootstrap4 and django-bootstrap-v5. For the migration 8 | guide from Bootstrap 4 to 5, please look at the Bootstrap docs, especially the `Migration section `_. 9 | 10 | Icons 11 | ----- 12 | 13 | Bootstrap 5 reversed the course on icons and they now publish their own extension pack. At the moment you will need to install it 14 | manually via npm. More details can be found on their web page: https://getbootstrap.com/docs/5.0/extend/icons/ 15 | 16 | JQuery 17 | ------ 18 | 19 | Bootstrap 5 does not need JQuery anymore. Therefore, django-bootstrap-v5 has removed all references to the library. It 20 | won't be included in the output from {% bootstrap_javascript %}. If your code relies on JQuery for other purposes, 21 | you will need to include it manually. -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Quickstart 3 | ========== 4 | 5 | After :doc:`installation`, you can use ``django-bootstrap-v5`` in your templates.: 6 | 7 | Load the ``bootstrap5`` 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-bootstrap-v5`` in your settings file. Refer to :doc:`settings` for more information. 26 | 27 | 28 | Demo application 29 | ---------------- 30 | 31 | The demo application provides a number of useful examples. 32 | 33 | https://github.com/zelenij/django-bootstrap-v5/tree/main/example 34 | 35 | -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Settings 3 | ======== 4 | 5 | The django-bootstrap-v5 has some pre-configured settings. 6 | 7 | They can be modified by adding a dict variable called ``BOOTSTRAP5`` in your ``settings.py`` and customizing the values ​​you want; 8 | 9 | The ``BOOTSTRAP5`` dict variable contains these settings and defaults: 10 | 11 | 12 | .. code:: django 13 | 14 | # Default settings 15 | BOOTSTRAP5 = { 16 | 17 | # The complete URL to the Bootstrap CSS file 18 | # Note that a URL can be either a string, 19 | # e.g. "https://stackpath.bootstrapcdn.com/bootstrap/5.1.1/css/bootstrap.min.css", 20 | # or a dict like the default value below. 21 | "css_url": { 22 | "href": "https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css", 23 | "integrity": "sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB", 24 | "crossorigin": "anonymous", 25 | }, 26 | 27 | # The complete URL to the Bootstrap JavaScript file 28 | "javascript_url": { 29 | "url": "https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js", 30 | "integrity": "sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T", 31 | "crossorigin": "anonymous", 32 | }, 33 | 34 | # The complete URL to the Bootstrap CSS file (None means no theme) 35 | "theme_url": None, 36 | 37 | # Put JavaScript in the HEAD section of the HTML document (only relevant if you use bootstrap5.html) 38 | 'javascript_in_head': False, 39 | 40 | # Label class to use in horizontal forms 41 | 'horizontal_label_class': 'col-md-3', 42 | 43 | # Field class to use in horizontal forms 44 | 'horizontal_field_class': 'col-md-9', 45 | 46 | # Set placeholder attributes to label if no placeholder is provided 47 | 'set_placeholder': True, 48 | 49 | # Class to indicate required (better to set this in your Django form) 50 | 'required_css_class': '', 51 | 52 | # Class to indicate error (better to set this in your Django form) 53 | 'error_css_class': 'is-invalid', 54 | 55 | # Class to indicate success, meaning the field has valid input (better to set this in your Django form) 56 | 'success_css_class': 'is-valid', 57 | 58 | # Renderers (only set these if you have studied the source and understand the inner workings) 59 | 'formset_renderers':{ 60 | 'default': 'bootstrap5.renderers.FormsetRenderer', 61 | }, 62 | 'form_renderers': { 63 | 'default': 'bootstrap5.renderers.FormRenderer', 64 | }, 65 | 'field_renderers': { 66 | 'default': 'bootstrap5.renderers.FieldRenderer', 67 | 'inline': 'bootstrap5.renderers.InlineFieldRenderer', 68 | }, 69 | } 70 | -------------------------------------------------------------------------------- /docs/templates.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Templates 3 | ========= 4 | 5 | You can customize the output of ``django-bootstrap-v5`` by writing your own templates. These templates are available: 6 | 7 | 8 | bootstrap5/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 | bootstrap5/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 | bootstrap5/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, ``bootstrap5/bootstrap5.html`` and ``bootstrap5/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 | In each of the following examples it is understood that you have already loaded the ``bootstrap5`` 9 | template tag library, placing the code below at the beginning of each template in which the ``bootstrap5`` 10 | template tag library will be used. Read the :doc:`installation` and :doc:`quickstart` sections to understand how 11 | to accomplish this. 12 | 13 | 14 | bootstrap_form 15 | ~~~~~~~~~~~~~~ 16 | 17 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_form 18 | 19 | 20 | bootstrap_form_errors 21 | ~~~~~~~~~~~~~~~~~~~~~ 22 | 23 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_form_errors 24 | 25 | 26 | bootstrap_formset 27 | ~~~~~~~~~~~~~~~~~ 28 | 29 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_formset 30 | 31 | 32 | bootstrap_formset_errors 33 | ~~~~~~~~~~~~~~~~~~~~~~~~ 34 | 35 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_formset_errors 36 | 37 | 38 | bootstrap_field 39 | ~~~~~~~~~~~~~~~ 40 | 41 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_field 42 | 43 | 44 | bootstrap_label 45 | ~~~~~~~~~~~~~~~ 46 | 47 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_label 48 | 49 | 50 | bootstrap_button 51 | ~~~~~~~~~~~~~~~~ 52 | 53 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_button 54 | 55 | 56 | bootstrap_alert 57 | ~~~~~~~~~~~~~~~ 58 | 59 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_alert 60 | 61 | buttons 62 | ~~~~~~~ 63 | 64 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_buttons 65 | 66 | 67 | bootstrap_messages 68 | ~~~~~~~~~~~~~~~~~~ 69 | 70 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_messages 71 | 72 | 73 | bootstrap_pagination 74 | ~~~~~~~~~~~~~~~~~~~~ 75 | 76 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_pagination 77 | 78 | 79 | bootstrap_javascript_url 80 | ~~~~~~~~~~~~~~~~~~~~~~~~ 81 | 82 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_javascript_url 83 | 84 | 85 | bootstrap_css_url 86 | ~~~~~~~~~~~~~~~~~ 87 | 88 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_css_url 89 | 90 | 91 | bootstrap_css 92 | ~~~~~~~~~~~~~ 93 | 94 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_css 95 | 96 | 97 | bootstrap_javascript 98 | ~~~~~~~~~~~~~~~~~~~~ 99 | 100 | .. autofunction:: bootstrap5.templatetags.bootstrap5.bootstrap_javascript 101 | -------------------------------------------------------------------------------- /docs/widgets.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Widgets 3 | ======= 4 | 5 | A form widget is available for displaying radio buttons as a Bootstrap 5 button group(https://getbootstrap.com/docs/5.2/components/button-group/#checkbox-and-radio-button-groups/). 6 | 7 | 8 | RadioSelectButtonGroup 9 | ~~~~~~~~~~~~~~~~~~~~~~ 10 | 11 | This renders a form ChoiceField as a Bootstrap 5 button group in the `primary` Bootstrap 5 color. 12 | 13 | .. code:: django 14 | 15 | from bootstrap5.widgets import RadioSelectButtonGroup 16 | 17 | class MyForm(forms.Form): 18 | media_type = forms.ChoiceField( 19 | help_text="Select the order type.", 20 | required=True, 21 | label="Order Type:", 22 | widget=RadioSelectButtonGroup, 23 | choices=((1, 'Vinyl'), (2, 'Compact Disc')), 24 | initial=1, 25 | ) 26 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example project for django-bootstrap-v5 2 | 3 | This example project only supports the latest version of Django. 4 | 5 | ## Instructions 6 | 7 | To run the example: 8 | 9 | ```bash 10 | git clone https://github.com/zelenij/django-bootstrap-v5.git 11 | 12 | cd django-bootstrap-v5/example 13 | pip install -r requirements.txt 14 | python manage.py migrate 15 | python manage.py runserver 16 | ``` 17 | 18 | Server should be live at http://127.0.0.1:8000/ now. 19 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zelenij/django-bootstrap-v5/cab866f2f0744245fdda86e2d555f373d4f4a081/example/__init__.py -------------------------------------------------------------------------------- /example/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zelenij/django-bootstrap-v5/cab866f2f0744245fdda86e2d555f373d4f4a081/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 | from bootstrap5.widgets import RadioSelectButtonGroup 6 | 7 | RADIO_CHOICES = (("1", "Radio 1"), ("2", "Radio 2")) 8 | 9 | 10 | MEDIA_CHOICES = ( 11 | ("Audio", (("vinyl", "Vinyl"), ("cd", "CD"))), 12 | ("Video", (("vhs", "VHS Tape"), ("dvd", "DVD"))), 13 | ("unknown", "Unknown"), 14 | ) 15 | 16 | 17 | class TestForm(forms.Form): 18 | """Form with a variety of widgets to test bootstrap5 rendering.""" 19 | 20 | date = forms.DateField(required=False) 21 | datetime = forms.SplitDateTimeField(widget=AdminSplitDateTime(), required=False) 22 | subject = forms.CharField( 23 | max_length=100, 24 | help_text="my_help_text", 25 | required=True, 26 | widget=forms.TextInput(attrs={"placeholder": "placeholdertest"}), 27 | ) 28 | xss_field = forms.CharField(label='XSS" onmouseover="alert(\'Hello, XSS\')" foo="', max_length=100) 29 | password = forms.CharField(widget=forms.PasswordInput) 30 | message = forms.CharField(required=False, help_text="my_help_text") 31 | sender = forms.EmailField(label="Sender © unicode", help_text='E.g., "me@example.com"') 32 | secret = forms.CharField(initial=42, widget=forms.HiddenInput) 33 | cc_myself = forms.BooleanField( 34 | required=False, help_text='cc stands for "carbon copy." You will get a copy in your mailbox.' 35 | ) 36 | select1 = forms.ChoiceField(choices=RADIO_CHOICES) 37 | select2 = forms.MultipleChoiceField(choices=RADIO_CHOICES, help_text="Check as many as you like.") 38 | select3 = forms.ChoiceField(choices=MEDIA_CHOICES) 39 | select4 = forms.MultipleChoiceField(choices=MEDIA_CHOICES, help_text="Check as many as you like.") 40 | category1 = forms.ChoiceField(choices=RADIO_CHOICES, widget=forms.RadioSelect) 41 | category2 = forms.MultipleChoiceField( 42 | choices=RADIO_CHOICES, widget=forms.CheckboxSelectMultiple, help_text="Check as many as you like." 43 | ) 44 | category3 = forms.ChoiceField(widget=forms.RadioSelect, choices=MEDIA_CHOICES) 45 | category4 = forms.MultipleChoiceField( 46 | choices=MEDIA_CHOICES, widget=forms.CheckboxSelectMultiple, help_text="Check as many as you like." 47 | ) 48 | category5 = forms.ChoiceField(widget=RadioSelectButtonGroup, choices=MEDIA_CHOICES) 49 | addon = forms.CharField(widget=forms.TextInput(attrs={"addon_before": "before", "addon_after": "after"})) 50 | 51 | required_css_class = "bootstrap5-req" 52 | 53 | # Set this to allow tests to work properly in Django 1.10+ 54 | # More information, see issue #337 55 | use_required_attribute = False 56 | 57 | def clean(self): 58 | cleaned_data = super().clean() 59 | raise forms.ValidationError("This error was added to show the non field errors styling.") 60 | return cleaned_data 61 | 62 | 63 | class ContactForm(TestForm): 64 | pass 65 | 66 | 67 | class ContactBaseFormSet(BaseFormSet): 68 | def add_fields(self, form, index): 69 | super().add_fields(form, index) 70 | 71 | def clean(self): 72 | super().clean() 73 | raise forms.ValidationError("This error was added to show the non form errors styling") 74 | 75 | 76 | ContactFormSet = formset_factory(TestForm, formset=ContactBaseFormSet, extra=2, max_num=4, validate_max=True) 77 | 78 | 79 | class FilesForm(forms.Form): 80 | text1 = forms.CharField() 81 | file1 = forms.FileField() 82 | file2 = forms.FileField(required=False) 83 | file3 = forms.FileField(widget=forms.ClearableFileInput) 84 | file5 = forms.ImageField() 85 | file4 = forms.FileField(required=False, widget=forms.ClearableFileInput) 86 | 87 | 88 | class ArticleForm(forms.Form): 89 | title = forms.CharField() 90 | pub_date = forms.DateField() 91 | 92 | def clean(self): 93 | cleaned_data = super().clean() 94 | raise forms.ValidationError("This error was added to show the non field errors styling.") 95 | return cleaned_data 96 | -------------------------------------------------------------------------------- /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 app.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(object): 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 | lines = [] 72 | for i in range(200): 73 | lines.append("Line %s" % (i + 1)) 74 | paginator = Paginator(lines, 10) 75 | page = self.request.GET.get("page") 76 | try: 77 | show_lines = paginator.page(page) 78 | except PageNotAnInteger: 79 | # If page is not an integer, deliver first page. 80 | show_lines = paginator.page(1) 81 | except EmptyPage: 82 | # If page is out of range (e.g. 9999), deliver last page of results. 83 | show_lines = paginator.page(paginator.num_pages) 84 | context["lines"] = show_lines 85 | return context 86 | 87 | 88 | class MiscView(TemplateView): 89 | template_name = "app/misc.html" 90 | -------------------------------------------------------------------------------- /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/requirements.txt: -------------------------------------------------------------------------------- 1 | .. 2 | django>=2.2 -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "")) 5 | 6 | # Include BOOTSTRAP5_FOLDER in path 7 | BOOTSTRAP5_FOLDER = os.path.abspath(os.path.join(BASE_DIR, "..", "bootstrap5")) 8 | if BOOTSTRAP5_FOLDER not in sys.path: 9 | sys.path.insert(0, BOOTSTRAP5_FOLDER) 10 | 11 | DEBUG = True 12 | 13 | ADMINS = () 14 | 15 | DATABASES = { 16 | "default": { 17 | "ENGINE": "django.db.backends.sqlite3", 18 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 19 | } 20 | } 21 | 22 | # Hosts/domain names that are valid for this site; required if DEBUG is False 23 | # See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts 24 | ALLOWED_HOSTS = ["localhost", "127.0.0.1"] 25 | 26 | # Local time zone for this installation. Choices can be found here: 27 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 28 | # although not all choices may be available on all operating systems. 29 | # In a Windows environment this must be set to your system time zone. 30 | TIME_ZONE = "Europe/Amsterdam" 31 | 32 | # Language code for this installation. All choices can be found here: 33 | # http://www.i18nguy.com/unicode/language-identifiers.html 34 | LANGUAGE_CODE = "en-us" 35 | 36 | # If you set this to False, Django will make some optimizations so as not 37 | # to load the internationalization machinery. 38 | USE_I18N = True 39 | 40 | # If you set this to False, Django will not format dates, numbers and 41 | # calendars according to the current locale. 42 | USE_L10N = True 43 | 44 | # If you set this to False, Django will not use timezone-aware datetimes. 45 | USE_TZ = True 46 | 47 | # Absolute filesystem path to the directory that will hold user-uploaded files. 48 | # Example: "/var/www/example.com/media/" 49 | MEDIA_ROOT = "" 50 | 51 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 52 | # trailing slash. 53 | # Examples: "http://example.com/media/", "http://media.example.com/" 54 | MEDIA_URL = "" 55 | 56 | # Absolute path to the directory static files should be collected to. 57 | # Don't put anything in this directory yourself; store your static files 58 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 59 | # Example: "/var/www/example.com/static/" 60 | STATIC_ROOT = "" 61 | 62 | # URL prefix for static files. 63 | # Example: "http://example.com/static/", "http://static.example.com/" 64 | STATIC_URL = "/static/" 65 | 66 | # Additional locations of static files 67 | STATICFILES_DIRS = ( 68 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 69 | # Always use forward slashes, even on Windows. 70 | # Don't forget to use absolute paths, not relative paths. 71 | ) 72 | 73 | # List of finder classes that know how to find static files in 74 | # various locations. 75 | STATICFILES_FINDERS = ( 76 | "django.contrib.staticfiles.finders.FileSystemFinder", 77 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 78 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 79 | ) 80 | 81 | # Make this unique, and don't share it with anybody. 82 | SECRET_KEY = "8s)l4^2s&&0*31-)+6lethmfy3#r1egh^6y^=b9@g!q63r649_" 83 | 84 | MIDDLEWARE = [ 85 | "django.middleware.security.SecurityMiddleware", 86 | "django.contrib.sessions.middleware.SessionMiddleware", 87 | "django.middleware.common.CommonMiddleware", 88 | "django.middleware.csrf.CsrfViewMiddleware", 89 | "django.contrib.auth.middleware.AuthenticationMiddleware", 90 | "django.contrib.messages.middleware.MessageMiddleware", 91 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 92 | ] 93 | 94 | ROOT_URLCONF = "urls" 95 | 96 | TEMPLATES = [ 97 | { 98 | "BACKEND": "django.template.backends.django.DjangoTemplates", 99 | "DIRS": ["templates"], 100 | "APP_DIRS": True, 101 | "OPTIONS": { 102 | "context_processors": [ 103 | "django.contrib.auth.context_processors.auth", 104 | "django.template.context_processors.debug", 105 | "django.template.context_processors.i18n", 106 | "django.template.context_processors.media", 107 | "django.template.context_processors.static", 108 | "django.template.context_processors.tz", 109 | "django.contrib.messages.context_processors.messages", 110 | ] 111 | }, 112 | } 113 | ] 114 | 115 | INSTALLED_APPS = ( 116 | "django.contrib.auth", 117 | "django.contrib.contenttypes", 118 | "django.contrib.sessions", 119 | "django.contrib.sites", 120 | "django.contrib.messages", 121 | "django.contrib.staticfiles", 122 | "django.contrib.admin", 123 | "bootstrap5", 124 | "app", 125 | ) 126 | 127 | # A sample logging configuration. The only tangible logging 128 | # performed by this configuration is to send an email to 129 | # the site admins on every HTTP 500 error when DEBUG=False. 130 | # See http://docs.djangoproject.com/en/dev/topics/logging for 131 | # more details on how to customize your logging configuration. 132 | LOGGING = { 133 | "version": 1, 134 | "disable_existing_loggers": False, 135 | "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, 136 | "handlers": { 137 | "mail_admins": { 138 | "level": "ERROR", 139 | "filters": ["require_debug_false"], 140 | "class": "django.utils.log.AdminEmailHandler", 141 | } 142 | }, 143 | "loggers": {"django.request": {"handlers": ["mail_admins"], "level": "ERROR", "propagate": True}}, 144 | } 145 | 146 | # Settings for django-bootstrap-v5 147 | BOOTSTRAP5 = { 148 | "error_css_class": "bootstrap5-error", 149 | "required_css_class": "bootstrap5-required", 150 | "javascript_in_head": True, 151 | } 152 | -------------------------------------------------------------------------------- /example/templates/app/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/bootstrap.html' %} 2 | 3 | {% load bootstrap5 %} 4 | 5 | {% block bootstrap5_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/templates/app/bootstrap.html: -------------------------------------------------------------------------------- 1 | {% extends 'bootstrap5/bootstrap5.html' %} 2 | 3 | {% block bootstrap5_title %}{% block title %}{% endblock %}{% endblock %} 4 | -------------------------------------------------------------------------------- /example/templates/app/form.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap5 %} 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/templates/app/form_by_field.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap5 %} 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/templates/app/form_horizontal.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap5 %} 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/templates/app/form_inline.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap5 %} 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/templates/app/form_with_files.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap5 %} 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/templates/app/formset.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap5 %} 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/templates/app/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | {% load bootstrap5 %} 3 | 4 | {% block title %}django-bootstrap-v5{% endblock %} 5 | 6 | {% block content %} 7 | This is bootstrap5 for Django. 8 | {% endblock %} -------------------------------------------------------------------------------- /example/templates/app/misc.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap5 %} 4 | 5 | {% block title %} 6 | Miscellaneous 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 | {% bootstrap_button 'button' size='lg' %} 12 |
13 | {% bootstrap_alert "Something went wrong" alert_type='danger' %} 14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /example/templates/app/pagination.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% load bootstrap5 %} 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/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from app.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 | -------------------------------------------------------------------------------- /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 | [tool.poetry] 2 | name = "django-bootstrap-v5" 3 | version = "1.0.11" 4 | description = "Bootstrap 5 support for Django projects" 5 | homepage = "https://github.com/zelenij/django-bootstrap-v5" 6 | repository = "https://github.com/zelenij/django-bootstrap-v5" 7 | documentation = "https://django-bootstrap-v5.readthedocs.io/" 8 | authors = ["Andre Bar'yudin "] 9 | license = "BSD-3-Clause" 10 | readme = "README.md" 11 | keywords= ["django", "django-bootstrap-v5", "bootstrap5", "bootstrap"] 12 | classifiers = [ 13 | "Development Status :: 5 - Production/Stable", 14 | "Environment :: Web Environment", 15 | "Framework :: Django :: 2.2", 16 | "Framework :: Django :: 3.0", 17 | "Framework :: Django", 18 | "Intended Audience :: Developers", 19 | "License :: OSI Approved :: BSD License", 20 | "Operating System :: OS Independent", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.6", 23 | "Programming Language :: Python :: 3.7", 24 | "Programming Language :: Python :: 3.8", 25 | "Topic :: Software Development :: Libraries", 26 | "Topic :: Utilities", 27 | ] 28 | packages = [ 29 | { include = "bootstrap5", from = "src" }, 30 | { include = "docs", format = "sdist" }, 31 | { include = "tests", format = "sdist" }, 32 | ] 33 | include = ["LICENSE"] 34 | 35 | [tool.poetry.dependencies] 36 | python = "^3.6" 37 | django = "^2.2 || ^3.0 || ^4.0" 38 | beautifulsoup4 = "^4.8.0" 39 | 40 | # docs 41 | sphinx = { version = "^4.4", optional = true } 42 | sphinx_rtd_theme = { version = "^1.0", optional = true } 43 | m2r2 = { version = "^0.2.5", optional = true } 44 | 45 | [tool.poetry.dev-dependencies] 46 | black = {version = "^20.8b1", allow-prereleases = true} 47 | isort = "^5.6.3" 48 | autoflake = "^1.4" 49 | flake8 = "^3.8.4" 50 | docformatter = "^1.3.1" 51 | pydocstyle = "^5.1.1" 52 | coverage = {extras = ["toml"], version = "^5.0.4"} 53 | tox = "^3.20.1" 54 | 55 | [tool.poetry.extras] 56 | docs = ["sphinx", "sphinx_rtd_theme", "m2r2"] 57 | 58 | [tool.black] 59 | line-length = 120 60 | target-version = ["py36"] 61 | 62 | [tool.coverage.run] 63 | branch = true 64 | source = ["bootstrap5"] 65 | 66 | [tool.coverage.paths] 67 | source = ["src", ".tox/*/site-packages"] 68 | 69 | [tool.coverage.report] 70 | show_missing = true 71 | skip_covered = true 72 | 73 | [build-system] 74 | requires = ["poetry_core>=1.0.0"] 75 | build-backend = "poetry.core.masonry.api" 76 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | python: 4 | version: 3.8 5 | install: 6 | - method: pip 7 | path: . 8 | extra_requirements: 9 | - docs 10 | 11 | -------------------------------------------------------------------------------- /src/bootstrap5/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "__version__", 3 | ] 4 | 5 | PACKAGE_NAME = "django-bootstrap-v5" 6 | 7 | try: 8 | from importlib.metadata import metadata 9 | except ImportError: 10 | from importlib_metadata import metadata 11 | 12 | package_metadata = metadata(PACKAGE_NAME) 13 | __version__ = package_metadata["Version"] 14 | -------------------------------------------------------------------------------- /src/bootstrap5/bootstrap.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | from django.conf import settings 4 | 5 | BOOTSTRAP5_DEFAULTS = { 6 | "css_url": { 7 | "href": "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css", 8 | "integrity": "sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3", 9 | "crossorigin": "anonymous", 10 | }, 11 | "javascript_url": { 12 | "url": "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js", 13 | "integrity": "sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p", 14 | "crossorigin": "anonymous", 15 | }, 16 | "theme_url": None, 17 | "javascript_in_head": False, 18 | "use_i18n": False, 19 | "horizontal_label_class": "col-md-3", 20 | "horizontal_field_class": "col-md-9", 21 | "set_placeholder": True, 22 | "required_css_class": "", 23 | "error_css_class": "is-invalid", 24 | "success_css_class": "is-valid", 25 | "formset_renderers": {"default": "bootstrap5.renderers.FormsetRenderer"}, 26 | "form_renderers": {"default": "bootstrap5.renderers.FormRenderer"}, 27 | "field_renderers": { 28 | "default": "bootstrap5.renderers.FieldRenderer", 29 | "inline": "bootstrap5.renderers.InlineFieldRenderer", 30 | "horizontal": "bootstrap5.renderers.HorizontalFieldRenderer", 31 | }, 32 | } 33 | 34 | 35 | def get_bootstrap_setting(name, default=None): 36 | """Read a setting.""" 37 | # Start with a copy of default settings 38 | BOOTSTRAP5 = BOOTSTRAP5_DEFAULTS.copy() 39 | 40 | # Override with user settings from settings.py 41 | BOOTSTRAP5.update(getattr(settings, "BOOTSTRAP5", {})) 42 | 43 | # Update use_i18n 44 | BOOTSTRAP5["use_i18n"] = i18n_enabled() 45 | 46 | return BOOTSTRAP5.get(name, default) 47 | 48 | 49 | def javascript_url(): 50 | """Return the full url to the Bootstrap JavaScript file.""" 51 | return get_bootstrap_setting("javascript_url") 52 | 53 | 54 | def css_url(): 55 | """Return the full url to the Bootstrap CSS file.""" 56 | return get_bootstrap_setting("css_url") 57 | 58 | 59 | def theme_url(): 60 | """Return the full url to the theme CSS file.""" 61 | return get_bootstrap_setting("theme_url") 62 | 63 | 64 | def i18n_enabled(): 65 | """Return the projects i18n setting.""" 66 | return getattr(settings, "USE_I18N", False) 67 | 68 | 69 | def get_renderer(renderers, **kwargs): 70 | layout = kwargs.get("layout", "") 71 | path = renderers.get(layout, renderers["default"]) 72 | mod, cls = path.rsplit(".", 1) 73 | return getattr(import_module(mod), cls) 74 | 75 | 76 | def get_formset_renderer(**kwargs): 77 | renderers = get_bootstrap_setting("formset_renderers") 78 | return get_renderer(renderers, **kwargs) 79 | 80 | 81 | def get_form_renderer(**kwargs): 82 | renderers = get_bootstrap_setting("form_renderers") 83 | return get_renderer(renderers, **kwargs) 84 | 85 | 86 | def get_field_renderer(**kwargs): 87 | renderers = get_bootstrap_setting("field_renderers") 88 | return get_renderer(renderers, **kwargs) 89 | -------------------------------------------------------------------------------- /src/bootstrap5/components.py: -------------------------------------------------------------------------------- 1 | from django.utils.safestring import mark_safe 2 | from django.utils.translation import gettext as _ 3 | 4 | from bootstrap5.utils import render_tag 5 | 6 | from .text import text_value 7 | 8 | 9 | def render_alert(content, alert_type=None, dismissible=True): 10 | """Render a Bootstrap alert.""" 11 | button = "" 12 | if not alert_type: 13 | alert_type = "info" 14 | css_classes = ["alert", "alert-" + text_value(alert_type)] 15 | if dismissible: 16 | css_classes.append("alert-dismissible") 17 | close = _("close") 18 | button = ( 19 | '' 20 | ).format(close=close) 21 | button_placeholder = "__BUTTON__" 22 | return mark_safe( 23 | render_tag( 24 | "div", 25 | attrs={"class": " ".join(css_classes), "role": "alert"}, 26 | content=mark_safe(button_placeholder) + text_value(content), 27 | ).replace(button_placeholder, button) 28 | ) 29 | -------------------------------------------------------------------------------- /src/bootstrap5/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/bootstrap5/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import EmailInput, NumberInput, PasswordInput, Textarea, TextInput, URLInput 2 | from django.utils.safestring import mark_safe 3 | 4 | from .bootstrap import get_bootstrap_setting, get_field_renderer, get_form_renderer, get_formset_renderer 5 | from .exceptions import BootstrapError 6 | from .text import text_value 7 | from .utils import add_css_class, render_tag 8 | 9 | FORM_GROUP_CLASS = "mb-3" 10 | 11 | 12 | def render_formset(formset, **kwargs): 13 | """Render a formset to a Bootstrap layout.""" 14 | renderer_cls = get_formset_renderer(**kwargs) 15 | return renderer_cls(formset, **kwargs).render() 16 | 17 | 18 | def render_formset_errors(formset, **kwargs): 19 | """Render formset errors to a Bootstrap layout.""" 20 | renderer_cls = get_formset_renderer(**kwargs) 21 | return renderer_cls(formset, **kwargs).render_errors() 22 | 23 | 24 | def render_form(form, **kwargs): 25 | """Render a form to a Bootstrap layout.""" 26 | renderer_cls = get_form_renderer(**kwargs) 27 | return renderer_cls(form, **kwargs).render() 28 | 29 | 30 | def render_form_errors(form, type="all", **kwargs): 31 | """Render form errors to a Bootstrap layout.""" 32 | renderer_cls = get_form_renderer(**kwargs) 33 | return renderer_cls(form, **kwargs).render_errors(type) 34 | 35 | 36 | def render_field(field, **kwargs): 37 | """Render a field to a Bootstrap layout.""" 38 | renderer_cls = get_field_renderer(**kwargs) 39 | return renderer_cls(field, **kwargs).render() 40 | 41 | 42 | def render_label(content, label_for=None, label_class="form-label", label_title=""): 43 | """Render a label with content.""" 44 | attrs = {} 45 | if label_for: 46 | attrs["for"] = label_for 47 | if label_class: 48 | attrs["class"] = label_class 49 | if label_title: 50 | attrs["title"] = label_title 51 | return render_tag("label", attrs=attrs, content=content) 52 | 53 | 54 | def render_button( 55 | content, 56 | button_type=None, 57 | button_class="btn-primary", 58 | size="", 59 | href="", 60 | name=None, 61 | value=None, 62 | title=None, 63 | extra_classes="", 64 | id="", 65 | ): 66 | """Render a button with content.""" 67 | attrs = {} 68 | classes = add_css_class("btn", button_class) 69 | size = text_value(size).lower().strip() 70 | if size == "xs": 71 | classes = add_css_class(classes, "btn-xs") 72 | elif size == "sm" or size == "small": 73 | classes = add_css_class(classes, "btn-sm") 74 | elif size == "lg" or size == "large": 75 | classes = add_css_class(classes, "btn-lg") 76 | elif size == "md" or size == "medium": 77 | pass 78 | elif size: 79 | raise BootstrapError('Parameter "size" should be "xs", "sm", "lg" or empty ("{size}" given).'.format(size=size)) 80 | 81 | if button_type: 82 | if button_type not in ("submit", "reset", "button", "link"): 83 | raise BootstrapError( 84 | ( 85 | 'Parameter "button_type" should be "submit", "reset", "button", "link" or empty ' 86 | '("{button_type}" given).' 87 | ).format(button_type=button_type) 88 | ) 89 | if button_type != "link": 90 | attrs["type"] = button_type 91 | 92 | classes = add_css_class(classes, extra_classes) 93 | attrs["class"] = classes 94 | 95 | if href: 96 | tag = "a" 97 | if button_type and button_type != "link": 98 | raise BootstrapError( 99 | 'Button of type "{button_type}" is not allowed a "href" parameter.'.format(button_type=button_type) 100 | ) 101 | attrs["href"] = href 102 | # Specify role for link with button appearance 103 | attrs.setdefault("role", "button") 104 | else: 105 | tag = "button" 106 | 107 | if id: 108 | attrs["id"] = id 109 | if name: 110 | attrs["name"] = name 111 | if value: 112 | attrs["value"] = value 113 | if title: 114 | attrs["title"] = title 115 | return render_tag(tag, attrs=attrs, content=mark_safe(content)) 116 | 117 | 118 | def render_field_and_label(field, label, field_class="", label_for=None, label_class="", layout="", **kwargs): 119 | """Render a field with its label.""" 120 | if layout == "horizontal": 121 | if not label_class: 122 | label_class = get_bootstrap_setting("horizontal_label_class") 123 | if not field_class: 124 | field_class = get_bootstrap_setting("horizontal_field_class") 125 | if not label: 126 | label = mark_safe(" ") 127 | label_class = add_css_class(label_class, "control-label") 128 | html = field 129 | if field_class: 130 | html = '
{html}
'.format(field_class=field_class, html=html) 131 | if label: 132 | html = render_label(label, label_for=label_for, label_class=label_class) + html 133 | return html 134 | 135 | 136 | def render_form_group(content, css_class=FORM_GROUP_CLASS): 137 | """Render a Bootstrap form group.""" 138 | return '
{content}
'.format(css_class=css_class, content=content) 139 | 140 | 141 | def is_widget_with_placeholder(widget): 142 | """ 143 | Return whether this widget should have a placeholder. 144 | 145 | Only text, text area, number, e-mail, url, password, number and derived inputs have placeholders. 146 | """ 147 | return isinstance(widget, (TextInput, Textarea, NumberInput, EmailInput, URLInput, PasswordInput)) 148 | -------------------------------------------------------------------------------- /src/bootstrap5/models.py: -------------------------------------------------------------------------------- 1 | # Empty models.py, required file for Django tests 2 | -------------------------------------------------------------------------------- /src/bootstrap5/renderers.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from django.forms import ( 3 | BaseForm, 4 | BaseFormSet, 5 | BoundField, 6 | CheckboxInput, 7 | CheckboxSelectMultiple, 8 | DateInput, 9 | EmailInput, 10 | FileInput, 11 | MultiWidget, 12 | NumberInput, 13 | PasswordInput, 14 | RadioSelect, 15 | Select, 16 | SelectDateWidget, 17 | TextInput, 18 | ) 19 | from django.utils.html import conditional_escape, escape, strip_tags 20 | from django.utils.safestring import mark_safe 21 | 22 | from .bootstrap import get_bootstrap_setting 23 | from .exceptions import BootstrapError 24 | from .forms import ( 25 | FORM_GROUP_CLASS, 26 | is_widget_with_placeholder, 27 | render_field, 28 | render_form, 29 | render_form_group, 30 | render_label, 31 | ) 32 | from .text import text_value 33 | from .utils import add_css_class, render_template_file 34 | 35 | try: 36 | # If Django is set up without a database, importing this widget gives RuntimeError 37 | from django.contrib.auth.forms import ReadOnlyPasswordHashWidget 38 | except RuntimeError: 39 | ReadOnlyPasswordHashWidget = None 40 | 41 | 42 | class BaseRenderer(object): 43 | """A content renderer.""" 44 | 45 | def __init__(self, *args, **kwargs): 46 | self.layout = kwargs.get("layout", "") 47 | self.form_group_class = kwargs.get("form_group_class", FORM_GROUP_CLASS) 48 | self.field_class = kwargs.get("field_class", "") 49 | self.label_class = kwargs.get("label_class", "") 50 | self.show_help = kwargs.get("show_help", True) 51 | self.show_label = kwargs.get("show_label", True) 52 | self.exclude = kwargs.get("exclude", "") 53 | 54 | self.set_placeholder = kwargs.get("set_placeholder", True) 55 | self.size = self.parse_size(kwargs.get("size", "")) 56 | self.horizontal_label_class = kwargs.get( 57 | "horizontal_label_class", get_bootstrap_setting("horizontal_label_class") 58 | ) 59 | self.horizontal_field_class = kwargs.get( 60 | "horizontal_field_class", get_bootstrap_setting("horizontal_field_class") 61 | ) 62 | 63 | def parse_size(self, size): 64 | size = text_value(size).lower().strip() 65 | if size in ("sm", "small"): 66 | return "small" 67 | if size in ("lg", "large"): 68 | return "large" 69 | if size in ("md", "medium", ""): 70 | return "medium" 71 | raise BootstrapError('Invalid value "%s" for parameter "size" (expected "sm", "md", "lg" or "").' % size) 72 | 73 | def get_size_class(self, prefix="form-control"): 74 | if self.size == "small": 75 | return prefix + "-sm" 76 | if self.size == "large": 77 | return prefix + "-lg" 78 | return "" 79 | 80 | def _render(self): 81 | return "" 82 | 83 | def render(self): 84 | return mark_safe(self._render()) 85 | 86 | 87 | class FormsetRenderer(BaseRenderer): 88 | """Default formset renderer.""" 89 | 90 | def __init__(self, formset, *args, **kwargs): 91 | if not isinstance(formset, BaseFormSet): 92 | raise BootstrapError('Parameter "formset" should contain a valid Django Formset.') 93 | self.formset = formset 94 | super().__init__(*args, **kwargs) 95 | 96 | def render_management_form(self): 97 | return text_value(self.formset.management_form) 98 | 99 | def render_form(self, form, **kwargs): 100 | return render_form(form, **kwargs) 101 | 102 | def render_forms(self): 103 | rendered_forms = [] 104 | for form in self.formset.forms: 105 | rendered_forms.append( 106 | self.render_form( 107 | form, 108 | layout=self.layout, 109 | form_group_class=self.form_group_class, 110 | field_class=self.field_class, 111 | label_class=self.label_class, 112 | show_label=self.show_label, 113 | show_help=self.show_help, 114 | exclude=self.exclude, 115 | set_placeholder=self.set_placeholder, 116 | size=self.size, 117 | horizontal_label_class=self.horizontal_label_class, 118 | horizontal_field_class=self.horizontal_field_class, 119 | ) 120 | ) 121 | return "\n".join(rendered_forms) 122 | 123 | def get_formset_errors(self): 124 | return self.formset.non_form_errors() 125 | 126 | def render_errors(self): 127 | formset_errors = self.get_formset_errors() 128 | if formset_errors: 129 | return render_template_file( 130 | "bootstrap5/form_errors.html", 131 | context={"errors": formset_errors, "form": self.formset, "layout": self.layout}, 132 | ) 133 | return "" 134 | 135 | def _render(self): 136 | return "".join([self.render_errors(), self.render_management_form(), self.render_forms()]) 137 | 138 | 139 | class FormRenderer(BaseRenderer): 140 | """Default form renderer.""" 141 | 142 | def __init__(self, form, *args, **kwargs): 143 | if not isinstance(form, BaseForm): 144 | raise BootstrapError('Parameter "form" should contain a valid Django Form.') 145 | self.form = form 146 | super().__init__(*args, **kwargs) 147 | self.error_css_class = kwargs.get("error_css_class", None) 148 | self.required_css_class = kwargs.get("required_css_class", None) 149 | self.bound_css_class = kwargs.get("bound_css_class", None) 150 | self.alert_error_type = kwargs.get("alert_error_type", "non_fields") 151 | self.form_check_class = kwargs.get("form_check_class", "form-check") 152 | 153 | def render_fields(self): 154 | rendered_fields = [] 155 | for field in self.form: 156 | rendered_fields.append( 157 | render_field( 158 | field, 159 | layout=self.layout, 160 | form_group_class=self.form_group_class, 161 | field_class=self.field_class, 162 | label_class=self.label_class, 163 | form_check_class=self.form_check_class, 164 | show_label=self.show_label, 165 | show_help=self.show_help, 166 | exclude=self.exclude, 167 | set_placeholder=self.set_placeholder, 168 | size=self.size, 169 | horizontal_label_class=self.horizontal_label_class, 170 | horizontal_field_class=self.horizontal_field_class, 171 | error_css_class=self.error_css_class, 172 | required_css_class=self.required_css_class, 173 | bound_css_class=self.bound_css_class, 174 | ) 175 | ) 176 | return "\n".join(rendered_fields) 177 | 178 | def get_fields_errors(self): 179 | form_errors = [] 180 | for field in self.form: 181 | if not field.is_hidden and field.errors: 182 | form_errors += field.errors 183 | return form_errors 184 | 185 | def render_errors(self, type="all"): 186 | form_errors = None 187 | if type == "all": 188 | form_errors = self.get_fields_errors() + self.form.non_field_errors() 189 | elif type == "fields": 190 | form_errors = self.get_fields_errors() 191 | elif type == "non_fields": 192 | form_errors = self.form.non_field_errors() 193 | 194 | if form_errors: 195 | return render_template_file( 196 | "bootstrap5/form_errors.html", 197 | context={"errors": form_errors, "form": self.form, "layout": self.layout, "type": type}, 198 | ) 199 | 200 | return "" 201 | 202 | def _render(self): 203 | return self.render_errors(self.alert_error_type) + self.render_fields() 204 | 205 | 206 | class FieldRenderer(BaseRenderer): 207 | """Default field renderer.""" 208 | 209 | # These widgets will not be wrapped in a form-control class 210 | WIDGETS_NO_FORM_CONTROL = (CheckboxInput, RadioSelect, CheckboxSelectMultiple, Select) 211 | 212 | def __init__(self, field, *args, **kwargs): 213 | if not isinstance(field, BoundField): 214 | raise BootstrapError('Parameter "field" should contain a valid Django BoundField.') 215 | self.field = field 216 | super().__init__(*args, **kwargs) 217 | 218 | self.widget = field.field.widget 219 | self.is_multi_widget = isinstance(field.field.widget, MultiWidget) 220 | self.initial_attrs = self.widget.attrs.copy() 221 | self.field_help = text_value(mark_safe(field.help_text)) if self.show_help and field.help_text else "" 222 | self.field_errors = [conditional_escape(text_value(error)) for error in field.errors] 223 | self.form_check_class = kwargs.get("form_check_class", "form-check") 224 | 225 | if "placeholder" in kwargs: 226 | # Find the placeholder in kwargs, even if it's empty 227 | self.placeholder = kwargs["placeholder"] 228 | elif get_bootstrap_setting("set_placeholder"): 229 | # If not found, see if we set the label 230 | self.placeholder = field.label 231 | else: 232 | # Or just set it to empty 233 | self.placeholder = "" 234 | if self.placeholder: 235 | self.placeholder = text_value(self.placeholder) 236 | 237 | self.addon_before = kwargs.get("addon_before", self.widget.attrs.pop("addon_before", "")) 238 | self.addon_after = kwargs.get("addon_after", self.widget.attrs.pop("addon_after", "")) 239 | self.addon_before_class = kwargs.get( 240 | "addon_before_class", self.widget.attrs.pop("addon_before_class", "input-group-text") 241 | ) 242 | self.addon_after_class = kwargs.get( 243 | "addon_after_class", self.widget.attrs.pop("addon_after_class", "input-group-text") 244 | ) 245 | 246 | # These are set in Django or in the global BOOTSTRAP5 settings, and 247 | # they can be overwritten in the template 248 | error_css_class = kwargs.get("error_css_class", None) 249 | required_css_class = kwargs.get("required_css_class", None) 250 | bound_css_class = kwargs.get("bound_css_class", None) 251 | if error_css_class is not None: 252 | self.error_css_class = error_css_class 253 | else: 254 | self.error_css_class = getattr(field.form, "error_css_class", get_bootstrap_setting("error_css_class")) 255 | if required_css_class is not None: 256 | self.required_css_class = required_css_class 257 | else: 258 | self.required_css_class = getattr( 259 | field.form, "required_css_class", get_bootstrap_setting("required_css_class") 260 | ) 261 | if bound_css_class is not None: 262 | self.success_css_class = bound_css_class 263 | else: 264 | self.success_css_class = getattr(field.form, "bound_css_class", get_bootstrap_setting("success_css_class")) 265 | 266 | # If the form is marked as form.empty_permitted, do not set required class 267 | if self.field.form.empty_permitted: 268 | self.required_css_class = "" 269 | 270 | def restore_widget_attrs(self): 271 | self.widget.attrs = self.initial_attrs.copy() 272 | 273 | def add_class_attrs(self, widget=None): 274 | if widget is None: 275 | widget = self.widget 276 | classes = widget.attrs.get("class", "") 277 | if ReadOnlyPasswordHashWidget is not None and isinstance(widget, ReadOnlyPasswordHashWidget): 278 | # Render this is a static control 279 | classes = add_css_class(classes, "form-control-static", prepend=True) 280 | elif not isinstance(widget, self.WIDGETS_NO_FORM_CONTROL): 281 | classes = add_css_class(classes, "form-control", prepend=True) 282 | # For these widget types, add the size class here 283 | classes = add_css_class(classes, self.get_size_class()) 284 | elif isinstance(widget, CheckboxInput): 285 | classes = add_css_class(classes, "form-check-input", prepend=True) 286 | elif isinstance(widget, Select): 287 | classes = add_css_class(classes, "form-select", prepend=True) 288 | 289 | if self.field.errors: 290 | if self.error_css_class: 291 | classes = add_css_class(classes, self.error_css_class) 292 | else: 293 | if self.field.form.is_bound: 294 | classes = add_css_class(classes, self.success_css_class) 295 | 296 | widget.attrs["class"] = classes 297 | 298 | def add_placeholder_attrs(self, widget=None): 299 | if widget is None: 300 | widget = self.widget 301 | placeholder = widget.attrs.get("placeholder", self.placeholder) 302 | if placeholder and self.set_placeholder and is_widget_with_placeholder(widget): 303 | # TODO: Should this be stripped and/or escaped? 304 | widget.attrs["placeholder"] = placeholder 305 | 306 | def add_help_attrs(self, widget=None): 307 | if widget is None: 308 | widget = self.widget 309 | if not isinstance(widget, CheckboxInput): 310 | widget.attrs["title"] = widget.attrs.get("title", escape(strip_tags(self.field_help))) 311 | 312 | def add_widget_attrs(self): 313 | if self.is_multi_widget: 314 | widgets = self.widget.widgets 315 | else: 316 | widgets = [self.widget] 317 | for widget in widgets: 318 | self.add_class_attrs(widget) 319 | self.add_placeholder_attrs(widget) 320 | self.add_help_attrs(widget) 321 | 322 | def list_to_class(self, html, klass): 323 | classes = add_css_class(klass, self.get_size_class()) 324 | mapping = [ 325 | ("", ""), 327 | ("", ""), 329 | ] 330 | for k, v in mapping: 331 | html = html.replace(k, v) 332 | 333 | # Apply bootstrap5 classes to labels and inputs. 334 | # A simple 'replace' isn't enough as we don't want to have several 'class' attr definition, which would happen 335 | # if we tried to 'html.replace("input", "input class=...")' 336 | soup = BeautifulSoup(html, features="html.parser") 337 | enclosing_div = soup.find("div", {"class": classes}) 338 | if enclosing_div: 339 | for label in enclosing_div.find_all("label"): 340 | label.attrs["class"] = label.attrs.get("class", []) + ["form-check-label"] 341 | try: 342 | label.input.attrs["class"] = label.input.attrs.get("class", []) + ["form-check-input"] 343 | except AttributeError: 344 | pass 345 | return str(soup) 346 | 347 | def add_checkbox_label(self, html): 348 | return html + render_label( 349 | content=self.field.label, 350 | label_for=self.field.id_for_label, 351 | label_title=escape(strip_tags(self.field_help)), 352 | label_class="form-check-label", 353 | ) 354 | 355 | def fix_date_select_input(self, html): 356 | div1 = '
' 357 | div2 = "
" 358 | html = html.replace("", "" + div2) 360 | return '
{html}
'.format(html=html) 361 | 362 | def fix_file_input_label(self, html): 363 | html = "
" + html 364 | return html 365 | 366 | def post_widget_render(self, html): 367 | if isinstance(self.widget, RadioSelect): 368 | html = self.list_to_class(html, "radio radio-success") 369 | elif isinstance(self.widget, CheckboxSelectMultiple): 370 | html = self.list_to_class(html, "checkbox") 371 | elif isinstance(self.widget, SelectDateWidget): 372 | html = self.fix_date_select_input(html) 373 | elif isinstance(self.widget, CheckboxInput) and self.show_label: 374 | html = self.add_checkbox_label(html) 375 | elif isinstance(self.widget, FileInput): 376 | html = self.fix_file_input_label(html) 377 | return html 378 | 379 | def wrap_widget(self, html): 380 | if isinstance(self.widget, CheckboxInput): 381 | # Wrap checkboxes 382 | # Note checkboxes do not get size classes, see #318 383 | html = '
{html}
'.format(html=html) 384 | return html 385 | 386 | def make_input_group_addon(self, inner_class, outer_class, content): 387 | if not content: 388 | return "" 389 | if inner_class: 390 | content = '{content}'.format(inner_class=inner_class, content=content) 391 | return '
{content}
'.format(outer_class=outer_class, content=content) 392 | 393 | @property 394 | def is_input_group(self): 395 | allowed_widget_types = (TextInput, PasswordInput, DateInput, NumberInput, Select, EmailInput) 396 | return (self.addon_before or self.addon_after) and isinstance(self.widget, allowed_widget_types) 397 | 398 | def make_input_group(self, html): 399 | if self.is_input_group: 400 | before = self.make_input_group_addon(self.addon_before_class, "input-group-prepend", self.addon_before) 401 | after = self.make_input_group_addon(self.addon_after_class, "input-group-append", self.addon_after) 402 | html = self.append_errors("{before}{html}{after}".format(before=before, html=html, after=after)) 403 | html = '
{html}
'.format(html=html) 404 | return html 405 | 406 | def append_help(self, html): 407 | field_help = self.field_help or None 408 | if field_help: 409 | help_html = render_template_file( 410 | "bootstrap5/field_help_text.html", 411 | context={ 412 | "field": self.field, 413 | "field_help": field_help, 414 | "layout": self.layout, 415 | "show_help": self.show_help, 416 | }, 417 | ) 418 | html += help_html 419 | return html 420 | 421 | def append_errors(self, html): 422 | field_errors = self.field_errors 423 | if field_errors: 424 | errors_html = render_template_file( 425 | "bootstrap5/field_errors.html", 426 | context={ 427 | "field": self.field, 428 | "field_errors": field_errors, 429 | "layout": self.layout, 430 | "show_help": self.show_help, 431 | }, 432 | ) 433 | html += errors_html 434 | return html 435 | 436 | def append_to_field(self, html): 437 | if isinstance(self.widget, CheckboxInput): 438 | # we have already appended errors and help to checkboxes 439 | # in append_to_checkbox_field 440 | return html 441 | 442 | if not self.is_input_group: 443 | # we already appended errors for input groups in make_input_group 444 | html = self.append_errors(html) 445 | 446 | return self.append_help(html) 447 | 448 | def append_to_checkbox_field(self, html): 449 | if not isinstance(self.widget, CheckboxInput): 450 | # we will append errors and help to normal fields later in append_to_field 451 | return html 452 | 453 | html = self.append_errors(html) 454 | return self.append_help(html) 455 | 456 | def get_field_class(self): 457 | field_class = self.field_class 458 | if not field_class and self.layout == "horizontal": 459 | field_class = self.horizontal_field_class 460 | return field_class 461 | 462 | def wrap_field(self, html): 463 | field_class = self.get_field_class() 464 | if field_class: 465 | html = '
{html}
'.format(field_class=field_class, html=html) 466 | return html 467 | 468 | def get_label_class(self): 469 | label_class = self.label_class 470 | if not label_class and self.layout == "horizontal": 471 | label_class = self.horizontal_label_class 472 | label_class = add_css_class(label_class, "col-form-label") 473 | label_class = text_value(label_class) 474 | if not label_class: 475 | label_class = "form-label" 476 | if not self.show_label or self.show_label == "visually-hidden": 477 | label_class = add_css_class(label_class, "visually-hidden") 478 | return label_class 479 | 480 | def get_label(self): 481 | if self.show_label == "skip": 482 | return None 483 | elif isinstance(self.widget, CheckboxInput): 484 | label = None 485 | else: 486 | label = self.field.label 487 | if self.layout == "horizontal" and not label: 488 | return mark_safe(" ") 489 | return label 490 | 491 | def add_label(self, html): 492 | label = self.get_label() 493 | if label: 494 | html = render_label(label, label_for=self.field.id_for_label, label_class=self.get_label_class()) + html 495 | return html 496 | 497 | def get_form_group_class(self): 498 | form_group_class = self.form_group_class 499 | if self.field.errors: 500 | if self.error_css_class: 501 | form_group_class = add_css_class(form_group_class, self.error_css_class) 502 | else: 503 | if self.field.form.is_bound: 504 | form_group_class = add_css_class(form_group_class, self.success_css_class) 505 | if self.field.field.required and self.required_css_class: 506 | form_group_class = add_css_class(form_group_class, self.required_css_class) 507 | if self.layout == "horizontal": 508 | form_group_class = add_css_class(form_group_class, "row") 509 | return form_group_class 510 | 511 | def wrap_label_and_field(self, html): 512 | return render_form_group(html, self.get_form_group_class()) 513 | 514 | def _render(self): 515 | # See if we're not excluded 516 | if self.field.name in self.exclude.replace(" ", "").split(","): 517 | return "" 518 | # Hidden input requires no special treatment 519 | if self.field.is_hidden: 520 | return text_value(self.field) 521 | # Render the widget 522 | self.add_widget_attrs() 523 | html = self.field.as_widget(attrs=self.widget.attrs) 524 | self.restore_widget_attrs() 525 | # Start post render 526 | html = self.post_widget_render(html) 527 | html = self.append_to_checkbox_field(html) 528 | html = self.wrap_widget(html) 529 | html = self.make_input_group(html) 530 | html = self.append_to_field(html) 531 | html = self.wrap_field(html) 532 | html = self.add_label(html) 533 | html = self.wrap_label_and_field(html) 534 | return html 535 | 536 | 537 | class InlineFieldRenderer(FieldRenderer): 538 | """Inline field renderer.""" 539 | 540 | def add_error_attrs(self): 541 | field_title = self.widget.attrs.get("title", "") 542 | field_title += " " + " ".join([strip_tags(e) for e in self.field_errors]) 543 | self.widget.attrs["title"] = field_title.strip() 544 | 545 | def add_widget_attrs(self): 546 | super().add_widget_attrs() 547 | self.add_error_attrs() 548 | 549 | def append_to_field(self, html): 550 | return html 551 | 552 | def get_field_class(self): 553 | return self.field_class 554 | 555 | def get_form_group_class(self): 556 | if self.form_group_class == FORM_GROUP_CLASS: 557 | self.form_group_class = "col-auto" 558 | return super().get_form_group_class() 559 | 560 | def get_label_class(self): 561 | return add_css_class(self.label_class, "visually-hidden") 562 | 563 | 564 | class HorizontalFieldRenderer(FieldRenderer): 565 | """Horizontal field renderer.""" 566 | 567 | # No-op the fix for normal form layout because it breaks the horizontal one. 568 | def fix_file_input_label(self, html): 569 | return html 570 | -------------------------------------------------------------------------------- /src/bootstrap5/templates/bootstrap5/bootstrap5.html: -------------------------------------------------------------------------------- 1 | 2 | {% load bootstrap5 %} 3 | {% if 'use_i18n'|bootstrap_setting %} 4 | {% load i18n %} 5 | {% get_current_language as LANGUAGE_CODE %} 6 | {% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% block bootstrap5_title %}django-bootstrap-v5 template title{% endblock %} 17 | 18 | 19 | {% bootstrap_css %} 20 | 21 | 22 | {% if 'javascript_in_head'|bootstrap_setting %} 23 | {% bootstrap_javascript %} 24 | {% endif %} 25 | 26 | {% block bootstrap5_extra_head %}{% endblock %} 27 | 28 | 29 | 30 | 31 | {% block bootstrap5_before_content %}{% endblock %} 32 | {% block bootstrap5_content %} CONTENT {% endblock %} 33 | {% block bootstrap5_after_content %}{% endblock %} 34 | 35 | 36 | {% if not 'javascript_in_head'|bootstrap_setting %} 37 | {% bootstrap_javascript %} 38 | {% endif %} 39 | 40 | {% block bootstrap5_extra_script %}{% endblock %} 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/bootstrap5/templates/bootstrap5/field_errors.html: -------------------------------------------------------------------------------- 1 | {% for text in field_errors %} 2 |
{{ text }}
3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /src/bootstrap5/templates/bootstrap5/field_help_text.html: -------------------------------------------------------------------------------- 1 | {% if field_help %} 2 | {{ field_help }} 3 | {% endif %} 4 | -------------------------------------------------------------------------------- /src/bootstrap5/templates/bootstrap5/form_errors.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 8 | -------------------------------------------------------------------------------- /src/bootstrap5/templates/bootstrap5/messages.html: -------------------------------------------------------------------------------- 1 | {% load i18n bootstrap5 %} 2 | {% for message in messages %} 3 | 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /src/bootstrap5/templates/bootstrap5/pagination.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap5 %} 2 | {% with bpurl=bootstrap_pagination_url|default:"" %} 3 | 4 |
    5 | 6 | 11 | 12 | {% if pages_back %} 13 |
  • 14 | 15 |
  • 16 | {% endif %} 17 | 18 | {% for p in pages_shown %} 19 |
  • 20 | 21 | {{ p }} 22 | 23 |
  • 24 | {% endfor %} 25 | 26 | {% if pages_forward %} 27 |
  • 28 | 29 |
  • 30 | {% endif %} 31 | 32 |
  • 33 | 34 | » 35 | 36 |
  • 37 | 38 |
39 | 40 | {% endwith %} 41 | -------------------------------------------------------------------------------- /src/bootstrap5/templates/bootstrap5/widgets/radio_select_button_group.html: -------------------------------------------------------------------------------- 1 | 2 | {% for group, options, index in widget.optgroups %} 3 | {% for option in options %} 4 | 5 | 6 | {% endfor %} 7 | {% endfor %} 8 | 9 | -------------------------------------------------------------------------------- /src/bootstrap5/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zelenij/django-bootstrap-v5/cab866f2f0744245fdda86e2d555f373d4f4a081/src/bootstrap5/templatetags/__init__.py -------------------------------------------------------------------------------- /src/bootstrap5/templatetags/bootstrap5.py: -------------------------------------------------------------------------------- 1 | from math import floor 2 | from urllib.parse import parse_qs, urlparse, urlunparse 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.http import urlencode 8 | from django.utils.safestring import mark_safe 9 | 10 | from ..bootstrap import ( 11 | css_url, 12 | get_bootstrap_setting, 13 | javascript_url, 14 | theme_url, 15 | ) 16 | from ..components import render_alert 17 | from ..forms import ( 18 | FORM_GROUP_CLASS, 19 | render_button, 20 | render_field, 21 | render_field_and_label, 22 | render_form, 23 | render_form_errors, 24 | render_form_group, 25 | render_formset, 26 | render_formset_errors, 27 | render_label, 28 | ) 29 | from ..utils import ( 30 | handle_var, 31 | parse_token_contents, 32 | render_link_tag, 33 | render_script_tag, 34 | render_tag, 35 | render_template_file, 36 | url_replace_param, 37 | ) 38 | 39 | MESSAGE_LEVEL_CLASSES = { 40 | message_constants.DEBUG: "alert alert-warning", 41 | message_constants.INFO: "alert alert-info", 42 | message_constants.SUCCESS: "alert alert-success", 43 | message_constants.WARNING: "alert alert-warning", 44 | message_constants.ERROR: "alert alert-danger", 45 | } 46 | 47 | register = template.Library() 48 | 49 | 50 | @register.filter 51 | def bootstrap_setting(value): 52 | """ 53 | Get a setting. 54 | 55 | A simple way to read bootstrap settings in a template. 56 | Please consider this filter private for now, do not use it in your own templates. 57 | """ 58 | return get_bootstrap_setting(value) 59 | 60 | 61 | @register.filter 62 | def bootstrap_message_classes(message): 63 | """Return the message classes for a message.""" 64 | extra_tags = None 65 | try: 66 | extra_tags = message.extra_tags 67 | except AttributeError: 68 | pass 69 | if not extra_tags: 70 | extra_tags = "" 71 | classes = [extra_tags] 72 | try: 73 | level = message.level 74 | except AttributeError: 75 | pass 76 | else: 77 | try: 78 | classes.append(MESSAGE_LEVEL_CLASSES[level]) 79 | except KeyError: 80 | classes.append("alert alert-danger") 81 | return " ".join(classes).strip() 82 | 83 | 84 | @register.simple_tag 85 | def bootstrap_javascript_url(): 86 | """ 87 | Return the full url to the Bootstrap JavaScript library. 88 | 89 | Default value: ``None`` 90 | 91 | This value is configurable, see Settings section 92 | 93 | **Tag name**:: 94 | 95 | bootstrap_javascript_url 96 | 97 | **Usage**:: 98 | 99 | {% bootstrap_javascript_url %} 100 | 101 | **Example**:: 102 | 103 | {% bootstrap_javascript_url %} 104 | """ 105 | return javascript_url() 106 | 107 | 108 | @register.simple_tag 109 | def bootstrap_css_url(): 110 | """ 111 | Return the full url to the Bootstrap CSS library. 112 | 113 | Default value: ``None`` 114 | 115 | This value is configurable, see Settings section 116 | 117 | **Tag name**:: 118 | 119 | bootstrap_css_url 120 | 121 | **Usage**:: 122 | 123 | {% bootstrap_css_url %} 124 | 125 | **Example**:: 126 | 127 | {% bootstrap_css_url %} 128 | """ 129 | return css_url() 130 | 131 | 132 | @register.simple_tag 133 | def bootstrap_theme_url(): 134 | """ 135 | Return the full url to a Bootstrap theme CSS library. 136 | 137 | Default value: ``None`` 138 | 139 | This value is configurable, see Settings section 140 | 141 | **Tag name**:: 142 | 143 | bootstrap_theme_url 144 | 145 | **Usage**:: 146 | 147 | {% bootstrap_theme_url %} 148 | 149 | **Example**:: 150 | 151 | {% bootstrap_theme_url %} 152 | """ 153 | return theme_url() 154 | 155 | 156 | @register.simple_tag 157 | def bootstrap_css(): 158 | """ 159 | Return HTML for Bootstrap CSS. If no CSS url is available, return empty string. 160 | 161 | Default value: ``None`` 162 | 163 | This value is configurable, see Settings section 164 | 165 | **Tag name**:: 166 | 167 | bootstrap_css 168 | 169 | **Usage**:: 170 | 171 | {% bootstrap_css %} 172 | 173 | **Example**:: 174 | 175 | {% bootstrap_css %} 176 | """ 177 | rendered_urls = [] 178 | if bootstrap_css_url(): 179 | rendered_urls.append(render_link_tag(bootstrap_css_url())) 180 | if bootstrap_theme_url(): 181 | rendered_urls.append(render_link_tag(bootstrap_theme_url())) 182 | return mark_safe("".join([url for url in rendered_urls])) 183 | 184 | 185 | @register.simple_tag 186 | def bootstrap_javascript(): 187 | """ 188 | Return HTML for Bootstrap JavaScript. 189 | 190 | Adjust url in settings. 191 | If no url is returned, we don't want this statement to return any HTML. This is intended behavior. 192 | 193 | Default value: False 194 | 195 | This value is configurable, see Settings section. Note that any value that evaluates to True and is 196 | not "slim" will be interpreted as True. 197 | 198 | **Tag name**:: 199 | 200 | bootstrap_javascript 201 | 202 | **Parameters**:: 203 | 204 | **Usage**:: 205 | 206 | {% bootstrap_javascript %} 207 | 208 | **Example**:: 209 | 210 | {% bootstrap_javascript %} 211 | """ 212 | # List of JS tags to include 213 | javascript_tags = [] 214 | 215 | # Bootstrap 4 JavaScript 216 | bootstrap_js_url = bootstrap_javascript_url() 217 | if bootstrap_js_url: 218 | javascript_tags.append(render_script_tag(bootstrap_js_url)) 219 | 220 | # Join and return 221 | return mark_safe("\n".join(javascript_tags)) 222 | 223 | 224 | @register.simple_tag 225 | def bootstrap_formset(*args, **kwargs): 226 | """ 227 | Render a formset. 228 | 229 | **Tag name**:: 230 | 231 | bootstrap_formset 232 | 233 | **Parameters**:: 234 | 235 | formset 236 | The formset that is being rendered 237 | 238 | 239 | See bootstrap_field_ for other arguments 240 | 241 | **Usage**:: 242 | 243 | {% bootstrap_formset formset %} 244 | 245 | **Example**:: 246 | 247 | {% bootstrap_formset formset layout='horizontal' %} 248 | """ 249 | return render_formset(*args, **kwargs) 250 | 251 | 252 | @register.simple_tag 253 | def bootstrap_formset_errors(*args, **kwargs): 254 | """ 255 | Render formset errors. 256 | 257 | **Tag name**:: 258 | 259 | bootstrap_formset_errors 260 | 261 | **Parameters**:: 262 | 263 | formset 264 | The formset that is being rendered 265 | 266 | layout 267 | Context value that is available in the template ``bootstrap5/form_errors.html`` as ``layout``. 268 | 269 | **Usage**:: 270 | 271 | {% bootstrap_formset_errors formset %} 272 | 273 | **Example**:: 274 | 275 | {% bootstrap_formset_errors formset layout='inline' %} 276 | """ 277 | return render_formset_errors(*args, **kwargs) 278 | 279 | 280 | @register.simple_tag 281 | def bootstrap_form(*args, **kwargs): 282 | """ 283 | Render a form. 284 | 285 | **Tag name**:: 286 | 287 | bootstrap_form 288 | 289 | **Parameters**:: 290 | 291 | form 292 | The form that is to be rendered 293 | 294 | exclude 295 | A list of field names (comma separated) that should not be rendered 296 | E.g. exclude=subject,bcc 297 | 298 | alert_error_type 299 | Control which type of errors should be rendered in global form alert. 300 | 301 | One of the following values: 302 | 303 | * ``'all'`` 304 | * ``'fields'`` 305 | * ``'non_fields'`` 306 | 307 | :default: ``'non_fields'`` 308 | 309 | See bootstrap_field_ for other arguments 310 | 311 | **Usage**:: 312 | 313 | {% bootstrap_form form %} 314 | 315 | **Example**:: 316 | 317 | {% bootstrap_form form layout='inline' %} 318 | """ 319 | return render_form(*args, **kwargs) 320 | 321 | 322 | @register.simple_tag 323 | def bootstrap_form_errors(*args, **kwargs): 324 | """ 325 | Render form errors. 326 | 327 | **Tag name**:: 328 | 329 | bootstrap_form_errors 330 | 331 | **Parameters**:: 332 | 333 | form 334 | The form that is to be rendered 335 | 336 | type 337 | Control which type of errors should be rendered. 338 | 339 | One of the following values: 340 | 341 | * ``'all'`` 342 | * ``'fields'`` 343 | * ``'non_fields'`` 344 | 345 | :default: ``'all'`` 346 | 347 | layout 348 | Context value that is available in the template ``bootstrap5/form_errors.html`` as ``layout``. 349 | 350 | **Usage**:: 351 | 352 | {% bootstrap_form_errors form %} 353 | 354 | **Example**:: 355 | 356 | {% bootstrap_form_errors form layout='inline' %} 357 | """ 358 | return render_form_errors(*args, **kwargs) 359 | 360 | 361 | @register.simple_tag 362 | def bootstrap_field(*args, **kwargs): 363 | """ 364 | Render a field. 365 | 366 | **Tag name**:: 367 | 368 | bootstrap_field 369 | 370 | **Parameters**:: 371 | 372 | 373 | field 374 | The form field to be rendered 375 | 376 | layout 377 | If set to ``'horizontal'`` then the field and label will be rendered side-by-side, as long as there 378 | is no ``field_class`` set as well. 379 | 380 | form_group_class 381 | CSS class of the ``div`` that wraps the field and label. 382 | 383 | :default: ``'mb-3'`` 384 | 385 | field_class 386 | CSS class of the ``div`` that wraps the field. 387 | 388 | label_class 389 | CSS class of the ``label`` element. 390 | 391 | :default: ``'form-label'`` 392 | 393 | form_check_class 394 | CSS class of the ``div`` element wrapping the label and input when rendering checkboxes and radio buttons. 395 | 396 | show_help 397 | Show the field's help text, if the field has help text. 398 | 399 | :default: ``True`` 400 | 401 | show_label 402 | Whether the show the label of the field. 403 | 404 | * ``True`` 405 | * ``False``/``'visually-hidden'`` 406 | * ``'skip'`` 407 | 408 | :default: ``True`` 409 | 410 | exclude 411 | A list of field names that should not be rendered 412 | 413 | size 414 | Controls the size of the rendered ``input.form-control`` through the use of CSS classes. 415 | 416 | One of the following values: 417 | 418 | * ``'small'`` 419 | * ``'medium'`` 420 | * ``'large'`` 421 | 422 | placeholder 423 | Sets the placeholder text of a textbox 424 | 425 | horizontal_label_class 426 | Class used on the label when the ``layout`` is set to ``horizontal``. 427 | 428 | :default: ``'col-md-3'``. Can be changed in :doc:`settings` 429 | 430 | horizontal_field_class 431 | Class used on the field when the ``layout`` is set to ``horizontal``. 432 | 433 | :default: ``'col-md-9'``. Can be changed in :doc:`settings` 434 | 435 | addon_before 436 | Text that should be prepended to the form field. Can also be an icon, e.g. 437 | ``''`` 438 | 439 | See the `Bootstrap docs ` 440 | for more examples. 441 | 442 | addon_after 443 | Text that should be appended to the form field. Can also be an icon, e.g. 444 | ``''`` 445 | 446 | See the `Bootstrap docs ` 447 | for more examples. 448 | 449 | addon_before_class 450 | Class used on the span when ``addon_before`` is used. 451 | 452 | One of the following values: 453 | 454 | * ``'input-group-text'`` 455 | * ``None`` 456 | 457 | Set to None to disable the span inside the addon. (for use with buttons) 458 | 459 | :default: ``input-group-text`` 460 | 461 | addon_after_class 462 | Class used on the span when ``addon_after`` is used. 463 | 464 | One of the following values: 465 | 466 | * ``'input-group-text'`` 467 | * ``None`` 468 | 469 | Set to None to disable the span inside the addon. (for use with buttons) 470 | 471 | :default: ``input-group-text`` 472 | 473 | error_css_class 474 | CSS class used when the field has an error 475 | 476 | :default: ``'has-error'``. Can be changed :doc:`settings` 477 | 478 | required_css_class 479 | CSS class used on the ``div.mb-3`` to indicate a field is required 480 | 481 | :default: ``''``. Can be changed :doc:`settings` 482 | 483 | bound_css_class 484 | CSS class used when the field is bound 485 | 486 | :default: ``'has-success'``. Can be changed :doc:`settings` 487 | 488 | **Usage**:: 489 | 490 | {% bootstrap_field field %} 491 | 492 | **Example**:: 493 | 494 | {% bootstrap_field field show_label=False %} 495 | """ 496 | return render_field(*args, **kwargs) 497 | 498 | 499 | @register.simple_tag() 500 | def bootstrap_label(*args, **kwargs): 501 | """ 502 | Render a label. 503 | 504 | **Tag name**:: 505 | 506 | bootstrap_label 507 | 508 | **Parameters**:: 509 | 510 | content 511 | The label's text 512 | 513 | label_for 514 | The value that will be in the ``for`` attribute of the rendered ``