├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── benchmark.py ├── docs ├── Makefile ├── bootstrap.rst ├── changelog.rst ├── conf.py ├── customization.rst ├── differences.rst ├── examples.rst ├── geodjango.rst ├── images │ ├── datepicker-chromium.png │ ├── datepicker-jquery-ui.png │ ├── geomcollection.png │ ├── gmappoly.png │ ├── image_with_thumbnail.png │ ├── osmlinestring.png │ ├── pointfield.png │ ├── slider-chromium.png │ └── slider-jquery-ui.png ├── index.rst ├── layouts.rst ├── templatetags.rst ├── usage.rst ├── widgets-reference.rst └── widgets.rst ├── floppyforms ├── __future__ │ ├── __init__.py │ └── models.py ├── __init__.py ├── compat.py ├── fields.py ├── forms.py ├── gis │ ├── __init__.py │ ├── fields.py │ └── widgets.py ├── models.py ├── static │ └── floppyforms │ │ ├── gis │ │ └── img │ │ │ ├── move_vertex_off.png │ │ │ └── move_vertex_on.png │ │ ├── js │ │ └── MapWidget.js │ │ └── openlayers │ │ ├── OpenLayers.js │ │ └── theme │ │ └── default │ │ ├── google.css │ │ ├── ie6-style.css │ │ ├── img │ │ ├── add_point_off.png │ │ ├── add_point_on.png │ │ ├── blank.gif │ │ ├── close.gif │ │ ├── drag-rectangle-off.png │ │ ├── drag-rectangle-on.png │ │ ├── draw_line_off.png │ │ ├── draw_line_on.png │ │ ├── draw_point_off.png │ │ ├── draw_point_on.png │ │ ├── draw_polygon_off.png │ │ ├── draw_polygon_on.png │ │ ├── editing_tool_bar.png │ │ ├── move_feature_off.png │ │ ├── move_feature_on.png │ │ ├── navigation_history.png │ │ ├── overview_replacement.gif │ │ ├── pan-panel-NOALPHA.png │ │ ├── pan-panel.png │ │ ├── pan_off.png │ │ ├── pan_on.png │ │ ├── panning-hand-off.png │ │ ├── panning-hand-on.png │ │ ├── remove_point_off.png │ │ ├── remove_point_on.png │ │ ├── ruler.png │ │ ├── save_features_off.png │ │ ├── save_features_on.png │ │ ├── view_next_off.png │ │ ├── view_next_on.png │ │ ├── view_previous_off.png │ │ ├── view_previous_on.png │ │ ├── zoom-panel-NOALPHA.png │ │ └── zoom-panel.png │ │ ├── style.css │ │ └── style.mobile.css ├── templates │ └── floppyforms │ │ ├── _render_as.html │ │ ├── attrs.html │ │ ├── checkbox.html │ │ ├── checkbox_select.html │ │ ├── clearable_input.html │ │ ├── color.html │ │ ├── date.html │ │ ├── datetime.html │ │ ├── dummy.html │ │ ├── email.html │ │ ├── errors.html │ │ ├── file.html │ │ ├── gis │ │ ├── google.html │ │ ├── openlayers.html │ │ └── osm.html │ │ ├── hidden.html │ │ ├── input.html │ │ ├── ipaddress.html │ │ ├── layouts │ │ ├── default.html │ │ ├── p.html │ │ ├── table.html │ │ └── ul.html │ │ ├── number.html │ │ ├── password.html │ │ ├── phonenumber.html │ │ ├── radio.html │ │ ├── range.html │ │ ├── rows │ │ ├── default.html │ │ ├── li.html │ │ ├── p.html │ │ └── tr.html │ │ ├── search.html │ │ ├── select.html │ │ ├── select_date.html │ │ ├── slug.html │ │ ├── text.html │ │ ├── textarea.html │ │ ├── time.html │ │ └── url.html ├── templatetags │ ├── __init__.py │ ├── floppyforms.py │ └── floppyforms_internals.py └── widgets.py ├── pytest.ini ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── base.py ├── compat.py ├── demo │ ├── __init__.py │ ├── forms.py │ ├── manage.py │ ├── settings.py │ ├── templates │ │ └── demo │ │ │ └── index.html │ ├── urls.py │ └── views.py ├── models.py ├── requirements.txt ├── settings.py ├── templates │ ├── custom.html │ ├── extra_argument.html │ ├── extra_argument_with_config.html │ ├── formconfig_inside_only.html │ ├── media_widget.html │ ├── simple_form_tag.html │ ├── simple_formfield_tag.html │ ├── simple_formrow_tag.html │ └── simple_formrow_tag_with_config.html ├── test_deprecations.py ├── test_fields.py ├── test_forms.py ├── test_gis.py ├── test_layouts.py ├── test_modelforms.py ├── test_rendering.py ├── test_templatetags.py ├── test_widgets.py └── tests.py └── tox.ini /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | if: github.repository == 'jazzband/django-floppyforms' 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: 3.8 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install -U pip 26 | python -m pip install -U setuptools twine wheel 27 | 28 | - name: Build package 29 | run: | 30 | python setup.py --version 31 | python setup.py sdist --format=gztar bdist_wheel 32 | twine check dist/* 33 | 34 | - name: Upload packages to Jazzband 35 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 36 | uses: pypa/gh-action-pypi-publish@master 37 | with: 38 | user: jazzband 39 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 40 | repository_url: https://jazzband.co/projects/django-floppyforms/upload 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | max-parallel: 5 11 | matrix: 12 | python-version: ['3.9'] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | 22 | - name: Install GIS dependencies 23 | run: | 24 | sudo apt-get -q -y update 25 | sudo apt-get -q -y install binutils gdal-bin libproj-dev libgeos-c1v5 26 | 27 | - name: Get pip cache dir 28 | id: pip-cache 29 | run: | 30 | echo "::set-output name=dir::$(pip cache dir)" 31 | 32 | - name: Cache 33 | uses: actions/cache@v2 34 | with: 35 | path: ${{ steps.pip-cache.outputs.dir }} 36 | key: 37 | ${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}-${{ hashFiles('**/tox.ini') }} 38 | restore-keys: | 39 | ${{ matrix.python-version }}-v1- 40 | 41 | - name: Install Python dependencies 42 | run: | 43 | python -m pip install --upgrade pip 44 | python -m pip install --upgrade tox tox-gh-actions 45 | 46 | - name: Tox tests 47 | run: | 48 | tox -v 49 | 50 | - name: Upload coverage 51 | uses: codecov/codecov-action@v1 52 | with: 53 | name: Python ${{ matrix.python-version }} 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | django_floppyforms.egg-info 2 | dist 3 | build 4 | docs/_build 5 | .coverage 6 | .tox/ 7 | *.pyc 8 | __pycache__ 9 | .mypy_cache 10 | # PyCharm IDE files 11 | .idea/ 12 | coverage.xml 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | matrix: 3 | include: 4 | - python: 3.6 5 | env: TOXENV=py36-22 6 | - python: 3.7 7 | env: TOXENV=py37-22 8 | - python: 3.8 9 | env: TOXENV=py38-22 10 | - python: 3.7 11 | env: TOXENV=docs 12 | - python: 3.7 13 | env: TOXENV=checks 14 | before_install: 15 | - sudo apt-get -q -y update 16 | - sudo apt-get remove -q -y binutils gdal-bin libproj-dev libgeos-c1v5 17 | - sudo add-apt-repository -y ppa:ubuntugis/ubuntugis-unstable 18 | - sudo apt-get -q -y install build-essential 19 | - sudo apt-get -q -y install binutils gdal-bin libproj-dev libgeos-c1v5 20 | install: 21 | - pip install pip wheel 22 | - pip install tox 23 | script: 24 | - tox -e $TOXENV 25 | notifications: 26 | irc: 27 | channels: 28 | - irc.freenode.org#django-floppyforms 29 | on_success: change 30 | on_failure: always 31 | deploy: 32 | provider: pypi 33 | user: jazzband 34 | distributions: sdist bdist_wheel 35 | server: https://jazzband.co/projects/django-floppyforms/upload 36 | password: 37 | secure: FDtgzOv3wsBPnqF0LQKlGGyBVEfY9skxArRDHXz450InbsKZtmpGM5BWf98/GZzEOQQKNKP0E7dghrPFdYU95T+qAigUCFLSQLs8Hd/TrwatPRRkFTxzVsP5k2z9sq8yRg3N08zg2fsXP+RdrZ6puduqFso6fz7EGIv8v94ugyk= 38 | on: 39 | tags: true 40 | repo: jazzband/django-floppyforms 41 | condition: "$TOXENV = py37-22" 42 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | 4 | 1.9.0 5 | ~~~~~ 6 | 7 | This release changes the compatibility matrix for the project. 8 | 9 | We now support the following Django releases: 10 | - Django 2.2 11 | - Django 3.0 12 | 13 | We are also testing under the following Python versions: 14 | - Python 3.6 15 | - Python 3.7 16 | - Python 3.8 17 | 18 | 19 | No other significant changes in this release, but Django 1.11 and 2.1 20 | are no longer supported. 21 | 22 | 1.8.0 23 | ~~~~~ 24 | 25 | This is the first release to be done under the Jazzband organization. 26 | 27 | It collects several compatibility fixes to support Django 1.11 and 2.1. 28 | 29 | The currently tested versions of `django-floppyforms` is now: 30 | - Django 1.11 and Python 2.7 or 3.6 31 | - Django 2.1 and Python 3.6 32 | 33 | In principle, we want to support any reasonable combination of Django and Python that still receives security releases, so if you are using an untested combination and hit an issue, bug reports are welcome. 34 | 35 | *Breaking Change*: 36 | 37 | Because Django's widgets now render through a form-specific template renderer, but `floppyforms` widgets 38 | use the standard rendering template (that doesn't automatically include Django's form templates), it is 39 | recommended to manuallyput Django's form template directory directly into your own template backend 40 | configuration. 41 | 42 | If you don't add the following, you might experience issues mixing and matching vanilla widgets with 43 | floppyform widgets:: 44 | 45 | import django 46 | 47 | TEMPLATES = [ 48 | { 49 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 50 | 'DIRS': [ 51 | ..., # your other template directories 52 | # django's own form template directories 53 | os.path.join(os.path.dirname(django.__file__), "forms/templates/", 54 | ], 55 | ... 56 | }, 57 | ... 58 | ] 59 | 60 | 61 | * `#176`_: Fix HTML validation for hidden textarea used with GIS widgets. 62 | * `#191`_ + `#196`_ + `#209`_: Support for Django 1.11 and 2.1. Thanks to MrJmad and dryice for patches. 63 | * `#194`_: Remove official support for Python 2.6 and Python 3.2. 64 | * `#204`_: Use HTTPS for OpenStreetMap links. Thanks to dryice for the patch. 65 | 66 | .. _#176: https://github.com/jazzband/django-floppyforms/issues/176 67 | .. _#191: https://github.com/jazzband/django-floppyforms/pull/191 68 | .. _#194: https://github.com/jazzband/django-floppyforms/pull/194 69 | .. _#204: https://github.com/jazzband/django-floppyforms/pull/204 70 | .. _#196: https://github.com/jazzband/django-floppyforms/pull/196 71 | .. _#209: https://github.com/jazzband/django-floppyforms/pull/209 72 | 73 | 1.7.0 74 | ~~~~~ 75 | 76 | * `#171`_: Fix path to GIS widget images in ``openlayers.html`` template. The 77 | files coming with Django admin where used, but the naming changed in 1.9. We 78 | vendor these know to have better control over it. 79 | * `#174`_: Support for setting your own Google Maps key in the 80 | ``BaseGMapWidget``. `See the documentation 81 | `_ for 82 | details 83 | 84 | .. _#171: https://github.com/jazzband/django-floppyforms/issues/171 85 | .. _#174: https://github.com/jazzband/django-floppyforms/pull/174 86 | 87 | 1.6.2 88 | ~~~~~ 89 | 90 | * `#169`_: Use the attributes ``ClearableFileInput.initial_text``, 91 | ``ClearableFileInput.input_text``, 92 | ``ClearableFileInput.clear_checkbox_label`` to determine the used text in the 93 | template. This was inconsistent so far with Django's behaviour. 94 | 95 | .. _#169: https://github.com/jazzband/django-floppyforms/issues/169 96 | 97 | 1.6.1 98 | ~~~~~ 99 | 100 | * `#167`_: Fix django-floppyforms' ``CheckboxInput.value_from_datadict`` which 101 | was inconsistent with Django's behaviour. 102 | 103 | .. _#167: https://github.com/jazzband/django-floppyforms/issues/167 104 | 105 | 1.6.0 106 | ~~~~~ 107 | 108 | * `#160`_: Django 1.9 support! Thanks to Jonas Haag for the patch. 109 | 110 | .. _#160: https://github.com/jazzband/django-floppyforms/pull/160 111 | 112 | 1.5.2 113 | ~~~~~ 114 | 115 | * `#156`_: The ``min``, ``max``, ``step`` attributes for ``DecimalField`` and 116 | ``FloatField`` were localized which can result in invalid values (rendering 117 | ``0.01`` as ``0,01`` in respective locales). Those attributes won't get 118 | localized anymore. Thanks to Yannick Chabbert for the fix. 119 | 120 | .. _#156: https://github.com/jazzband/django-floppyforms/pull/156 121 | 122 | 1.5.1 123 | ~~~~~ 124 | 125 | * `FloatField`` now fills in ``min``, ``max``, and ``step`` attributes to match 126 | the behaviour of `DecimalField`. Leaving out the ``step`` attribute would 127 | result in widgets that only allow integers to be filled in (HTML 5 default 128 | for ``step`` is ``1``). 129 | 130 | 1.5.0 131 | ~~~~~ 132 | 133 | * `#148`_: Added support for custom ``label_suffix`` arguments in forms and fields. 134 | * The contents in ``floppyforms/input.html`` is now wrapped in a ``{% block 135 | content %}`` for easier extending. 136 | * `#70`_: `DecimalField`` now fills in ``min``, ``max``, and ``step`` attributes for 137 | better client side validation. Use the ``novalidate`` attribute on your 138 | ``
`` tag to disable HTML5 input validation in the browser. Thanks to 139 | caacree for the patch. 140 | 141 | .. _#148: https://github.com/jazzband/django-floppyforms/issues/148 142 | .. _#70: https://github.com/jazzband/django-floppyforms/issues/70 143 | 144 | 1.4.1 145 | ~~~~~ 146 | 147 | * Fixed source distribution to include all files in 148 | ``floppyforms/static/floppyforms/openlayers``. 149 | 150 | 1.4.0 151 | ~~~~~ 152 | 153 | * Every widget is now using its own template. Previously all widgets that are 154 | based on the HTML ```` tag used the generic ``floppyforms/input.html`` 155 | template. Now the widgets each have a custom element for easier 156 | customisation. For example ``CheckboxInput`` now uses 157 | ``floppyforms/checkbox.html`` instead of ``floppyforms/input.html``. See 158 | `Widgets reference 159 | `_ 160 | for a complete list of available widgets and which templates they use. 161 | 162 | * Adjusting the SRIDs used in the GeoDjango widgets to conform with 163 | Django 1.7. Thanks to Tyler Tipton for the patch. 164 | 165 | * Python 3.2 is now officially supported. 166 | 167 | * Django 1.8 is now officially supported. django-floppyforms no longers 168 | triggers Django deprecation warnings. 169 | 170 | * Adding `OpenLayers`_ distribution to django-floppyforms static files in order 171 | to better support HTTPS setups when GIS widgets are used (See #15 for more 172 | details). 173 | 174 | * Fix: ``python setup.py bdist_rpm`` failed because of wrong string encodings 175 | in setup.py. Thanks to Yuki Izumi for the fix. 176 | 177 | * Fix: The ``CheckboxInput`` widget did detect different values in Python 2 178 | when given ``'False'`` and ``u'False'`` as data. Thanks to @artscoop for the 179 | patch. 180 | 181 | * Fix: ``MultipleChoiceField`` can now correctly be rendered as hidden field by 182 | using the ``as_hidden`` helper in the template. That was not working 183 | previously as there was no value set for ``MultipleChoiceField.hidden_widget``. 184 | 185 | .. _OpenLayers: http://openlayers.org/ 186 | 187 | 1.3.0 188 | ~~~~~ 189 | 190 | * DateInput widget renders hardcoded "%Y-%m-%d" format. We don't allow custom 191 | formats there since the "%Y-%m-%d" format is what browsers are submitting 192 | with HTML5 date input fields. Thanks to Bojan Mihelac for the patch. 193 | 194 | * Adding ``supports_microseconds`` attribute to all relevant widget classes. 195 | Thanks to Stephen Burrows for the patch. 196 | 197 | * Using a property for ``Widget.is_hidden`` attribute on widgets to be in 198 | conformance with Django 1.7 default widget implementation. 199 | 200 | * The docs mentioned that the current ``ModelForm`` behaviour in 201 | ``floppyforms.__future__`` will become the default in 1.3. This is postpone 202 | for one release and will be part of 1.4. 203 | 204 | 1.2.0 205 | ~~~~~ 206 | 207 | * Subclasses of ``floppyforms.models.ModelForm`` did not convert widgets of 208 | form fields that were automatically created for the existing model fields 209 | into the floppyform variants. This is now changed, thanks to a patch by 210 | Stephen Burrows. 211 | 212 | Previously you had to set the widgets your self in a model form. For example 213 | you would write:: 214 | 215 | import floppyforms as forms 216 | 217 | class ProfileForm(forms.ModelForm): 218 | class Meta: 219 | model = Profile 220 | widgets = { 221 | 'name': forms.TextInput, 222 | 'url': forms.URLInput, 223 | ... 224 | } 225 | 226 | Now this is done automatically. But since this is a kind-of 227 | backwardsincompatible change, you need to use a special import:: 228 | 229 | import floppyforms.__future__ as forms 230 | 231 | class ProfileForm(forms.ModelForm): 232 | class Meta: 233 | model = Profile 234 | 235 | This feature will become the default behaviour in floppyforms 2.0. 236 | 237 | See the documentation for more information: 238 | http://django-floppyforms.readthedocs.org/en/latest/usage.html#modelforms 239 | 240 | * If you added an attribute with value 1 to the attrs kwargs (e.g. ``{'value': 241 | 1}``, you would get no attribute value in the rendered html (e.g. ``value`` 242 | instead of ``value="1"``). That's fixed now, thanks to Viktor Ershov for the 243 | report. 244 | 245 | * All floppyform widget classes now take a ``template_name`` argument in the 246 | ``__init__`` and ``render`` method. Thanks to Carl Meyer for the patch. 247 | 248 | 1.1.1 249 | ~~~~~ 250 | 251 | * Fix for Django 1.6 252 | 253 | * Fix for GIS widgets on Django 1.4 and some versions of GEOS. 254 | 255 | 1.1 256 | ~~~ 257 | 258 | * Added GenericIPAddressField. 259 | 260 | * Django 1.5 and Python 3.3 support added. 261 | 262 | * Django 1.3 support dropped. 263 | 264 | * GIS widgets switched to stable OpenLayers release instead of a dev build. 265 | 266 | * Fixed ``Textarea`` widget template to work with a non-empty 267 | ``TEMPLATE_STRING_IF_INVALID`` setting. Thanks to Leon Matthews for the 268 | report. 269 | 270 | * Fixed context handling in widget rendering. It didn't take care of popping 271 | the context as often as it was pushed onto. This could cause strange 272 | behaviour in the template by leaking variables into outer scopes. Thanks to 273 | David Danier for the report. 274 | 275 | * Added missing empty choice for selectboxes in ``SelectDateWidget``. Thanks 276 | fsx999 for the report. 277 | 278 | * ``IntegerField`` now automatically passes its ``min_value`` and 279 | ``max_value`` (if provided) to the ``NumberInput`` widget. 280 | 281 | * Added basic support for ```` elements for suggestions in 282 | ``Input`` widgets. 283 | 284 | * ``date``, ``datetime`` and ``time`` inputs are not localized anymore. The 285 | HTML5 spec requires the rendered values to be RFC3339-compliant and the 286 | browsers are in charge of localization. If you still want localized 287 | date/time inputs, use those provided by Django or override the 288 | ``_format_value()`` method of the relevant widgets. 289 | 290 | 1.0 291 | ~~~ 292 | 293 | * cleaned up the behaviour of ``attrs`` 294 | * compatible with Django 1.3 and 1.4 295 | * ```` support in select widgets 296 | * ``Select`` widgets: renamed ``choices`` context variable to ``optgroups``. 297 | This is **backwards-incompatible**: if you have custom templates for 298 | ``Select`` widgets, they need to be updated. 299 | * ``get_context()`` is more reliable 300 | * Added ``form``, ``formrow``, ``formfield``, ``formconfig`` and ``widget`` 301 | template tags. 302 | * Added template-based form layout system. 303 | * Added ability to render widgets with the broader page context, for 304 | instance for django-sekizai compatibility. 305 | 306 | 0.4 307 | ~~~ 308 | 309 | * All widgets from Django have their floppyforms equivalent 310 | * Added widgets for GeoDjango 311 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As contributors and maintainers of the Jazzband projects, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating documentation, 6 | submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in the Jazzband a harassment-free experience 9 | for everyone, regardless of the level of experience, gender, gender identity and 10 | expression, sexual orientation, disability, personal appearance, body size, race, 11 | ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | - The use of sexualized language or imagery 16 | - Personal attacks 17 | - Trolling or insulting/derogatory comments 18 | - Public or private harassment 19 | - Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | - Other unethical or unprofessional conduct 22 | 23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are not 25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing the jazzband 30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently 31 | removed from the Jazzband roadies. 32 | 33 | This code of conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and 38 | investigated and will result in a response that is deemed necessary and appropriate to 39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the 40 | reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/3/0/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) 2 | 3 | This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2012, Bruno Renié 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of django-floppyforms nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | For the base test cases (providing assertHTMLEquals and assertTemplatesUsed): 30 | 31 | Copyright (c) Django Software Foundation and individual contributors. 32 | All rights reserved. 33 | 34 | Redistribution and use in source and binary forms, with or without modification, 35 | are permitted provided that the following conditions are met: 36 | 37 | 1. Redistributions of source code must retain the above copyright notice, 38 | this list of conditions and the following disclaimer. 39 | 40 | 2. Redistributions in binary form must reproduce the above copyright 41 | notice, this list of conditions and the following disclaimer in the 42 | documentation and/or other materials provided with the distribution. 43 | 44 | 3. Neither the name of Django nor the names of its contributors may be used 45 | to endorse or promote products derived from this software without 46 | specific prior written permission. 47 | 48 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 49 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 50 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 51 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 52 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 53 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 54 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 55 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 56 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 57 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 58 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include CHANGES.rst 3 | recursive-include floppyforms/templates * 4 | recursive-include floppyforms/static * 5 | recursive-include requirements *.txt 6 | recursive-include docs *.rst *.png 7 | recursive-include tests *.py *.html 8 | recursive-include tests/demo/templates * 9 | recursive-include tests/templates * 10 | include docs/Makefile docs/conf.py 11 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-floppyforms 2 | ================== 3 | 4 | .. image:: https://jazzband.co/static/img/badge.svg 5 | :target: https://jazzband.co/ 6 | :alt: Jazzband 7 | 8 | .. image:: https://github.com/jazzband/django-floppyforms/workflows/Test/badge.svg 9 | :target: https://github.com/jazzband/django-floppyforms/actions 10 | :alt: GitHub Actions 11 | 12 | .. image:: https://codecov.io/gh/jazzband/django-floppyforms/branch/master/graph/badge.svg 13 | :target: https://codecov.io/gh/jazzband/django-floppyforms 14 | :alt: Coverage 15 | 16 | Full control of form rendering in the templates. 17 | 18 | * Authors: Gregor Müllegger and many many `contributors`_ 19 | * Original creator: Bruno Renié started this project and kept it going for many years. 20 | * Licence: BSD 21 | * Requirements: homework -- read `this`_. 22 | 23 | .. _contributors: https://github.com/jazzband/django-floppyforms/contributors 24 | .. _this: http://diveintohtml5.info/forms.html 25 | 26 | Installation 27 | ------------ 28 | 29 | * ``pip install -U django-floppyforms`` 30 | * Add ``floppyforms`` to your ``INSTALLED_APPS`` 31 | 32 | For those who want to mix and match with vanilla Django widgets, it is also recommended 33 | to put Django's form template directory into your template directories:: 34 | 35 | # in your template configuration 36 | TEMPLATES = [{ 37 | ..., 38 | # inside the directories parameter 39 | 'DIRS': [ 40 | # include django's form templates 41 | os.path.join( 42 | os.path.dirname(django.__file__), "forms/templates/" 43 | ), 44 | ... # the rest of your template directories 45 | }] 46 | 47 | For extensive documentation see the ``docs`` folder or `read it on 48 | readthedocs`_ 49 | 50 | .. _read it on readthedocs: http://django-floppyforms.readthedocs.org/ 51 | 52 | To install the `in-development version`_ of django-floppyforms, run ``pip 53 | install "https://github.com/jazzband/django-floppyforms/tarball/master#egg=django-floppyforms"``. 54 | 55 | .. _in-development version: https://github.com/jazzband/django-floppyforms 56 | 57 | Help 58 | ---- 59 | 60 | Create a ticket in the `issues section on github`_ or ask your questions on the 61 | #django-floppyforms IRC channel on freenode. 62 | 63 | You can get professional consulting regarding django-floppyforms or any other 64 | Django related work from django-floppyforms' maintainer `Gregor Müllegger`_. 65 | 66 | .. _issues section on github: https://github.com/jazzband/django-floppyforms/issues 67 | .. _Gregor Müllegger: http://gremu.net/ 68 | 69 | Bugs 70 | ---- 71 | 72 | Really? Oh well... Please Report. Or better, fix :) We are happy to help you 73 | through the process of fixing and testing a bug. Just get in touch. 74 | 75 | Development 76 | ----------- 77 | 78 | Thanks for asking! 79 | 80 | Get the code:: 81 | 82 | git clone git@github.com:jazzband/django-floppyforms.git 83 | cd django-floppyforms 84 | virtualenv env 85 | source env/bin/activate 86 | add2virtualenv . 87 | 88 | Install the development requirements:: 89 | 90 | pip install "tox>=1.8" 91 | 92 | 93 | Currently, you'll need to `install the GeoDjango requirements`_ when running tests. 94 | 95 | .. _install the GeoDjango requirements: https://docs.djangoproject.com/en/3.0/ref/contrib/gis/install/geolibs/ 96 | 97 | Run the tests:: 98 | 99 | tox 100 | tox -e py36-22 101 | 102 | You can see all the supported test configurations with ``tox -l``. 103 | -------------------------------------------------------------------------------- /benchmark.py: -------------------------------------------------------------------------------- 1 | """ 2 | Compares the rendering speed between Django forms and django-floppyforms 3 | 4 | Usage: DJANGO_SETTINGS_MODULE=benchmark python benchmark.py [--cache] 5 | """ 6 | import sys 7 | import timeit 8 | 9 | django = """from django import forms 10 | 11 | class DjangoForm(forms.Form): 12 | text = forms.CharField() 13 | slug = forms.SlugField() 14 | some_bool = forms.BooleanField() 15 | email = forms.EmailField() 16 | date = forms.DateTimeField() 17 | file_ = forms.FileField() 18 | 19 | rendered = DjangoForm().as_p()""" 20 | 21 | flop = """import floppyforms as forms 22 | 23 | class FloppyForm(forms.Form): 24 | text = forms.CharField() 25 | slug = forms.SlugField() 26 | some_bool = forms.BooleanField() 27 | email = forms.EmailField() 28 | date = forms.DateTimeField() 29 | file_ = forms.FileField() 30 | 31 | rendered = FloppyForm().as_p()""" 32 | 33 | def time(stmt): 34 | t = timeit.Timer(stmt=stmt) 35 | return t.timeit(number=1000) 36 | 37 | if __name__ == '__main__': 38 | print "Plain Django:", time(django) 39 | print "django-floppyforms:", time(flop) 40 | 41 | INSTALLED_APPS = ( 42 | 'floppyforms' 43 | ) 44 | 45 | if '--cache' in sys.argv: 46 | TEMPLATE_LOADERS = ( 47 | ('django.template.loaders.cached.Loader', ( 48 | 'django.template.loaders.filesystem.Loader', 49 | 'django.template.loaders.app_directories.Loader', 50 | )), 51 | ) 52 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-floppyforms.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-floppyforms.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-floppyforms" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-floppyforms" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/bootstrap.rst: -------------------------------------------------------------------------------- 1 | Layout example with Bootstrap 2 | ============================= 3 | 4 | If you use Floppyforms with Bootstrap you might be interested in using a 5 | bootstrap layout for your form. 6 | 7 | What you have to do is to create those two templates: 8 | 9 | **floppyforms/templates/floppyforms/layouts/bootstrap.html**: 10 | 11 | .. code-block:: django 12 | 13 | {% load floppyforms %}{% block formconfig %}{% formconfig row using "floppyforms/rows/bootstrap.html" %}{% endblock %} 14 | 15 | {% block forms %}{% for form in forms %} 16 | {% block errors %} 17 | {% for error in form.non_field_errors %} 18 |
19 | × 20 | {{ error }} 21 |
22 | {% endfor %} 23 | {% for error in form|hidden_field_errors %} 24 |
25 | × 26 | {{ error }} 27 |
28 | {% endfor %} 29 | {% endblock errors %} 30 | {% block rows %} 31 | {% for field in form.visible_fields %} 32 | {% if forloop.last %}{% formconfig row with hidden_fields=form.hidden_fields %}{% endif %} 33 | {% block row %}{% formrow field %}{% endblock %} 34 | {% endfor %} 35 | {% if not form.visible_fields %}{% for field in form.hidden_fields %}{% formfield field %}{% endfor %}{% endif %} 36 | {% endblock %} 37 | {% endfor %}{% endblock %} 38 | 39 | **floppyforms/templates/floppyforms/rows/bootstrap.html**: 40 | 41 | .. code-block:: django 42 | 43 | {% load floppyforms %}{% block row %}{% for field in fields %} 44 |
45 | {% with classes=field.css_classes label=label|default:field.label help_text=help_text|default:field.help_text %} 46 | {% block label %}{% if field|id %}{% endif %}{% endblock %} 47 | {% block field %} 48 |
49 | {% block widget %}{% formfield field %}{% endblock %} 50 | {% block errors %}{% include "floppyforms/errors.html" with errors=field.errors %}{% endblock %} 51 | {% block help_text %}{% if field.help_text %} 52 |

{{ field.help_text }}

53 | {% endif %}{% endblock %} 54 | {% block hidden_fields %}{% for field in hidden_fields %}{{ field.as_hidden }}{% endfor %}{% endblock %} 55 |
56 | {% endblock %} 57 | {% endwith %} 58 |
59 | {% endfor %}{% endblock %} 60 | 61 | You can also define this layout by default: 62 | 63 | **floppyforms/templates/floppyforms/layouts/default.html**: 64 | 65 | .. code-block:: django 66 | 67 | {% extends "floppyforms/layouts/bootstrap.html" %} 68 | 69 | You can also make a change to the error display: 70 | 71 | **floppyforms/templates/floppyforms/errors.html**: 72 | 73 | .. code-block:: django 74 | 75 | {% if errors %}{% for error in errors %}{{ error }}{% if not forloop.last %}
{% endif %}{% endfor %}
{% endif %} 76 | 77 | And that's it, you now have a perfect display for your form with bootstrap. 78 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-floppyforms documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Nov 26 16:01:17 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import datetime 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'django-floppyforms' 44 | copyright = u'2010-{0}, Bruno Renié and contributors'.format( 45 | datetime.datetime.today().year) 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | try: 52 | from floppyforms import __version__ 53 | # The short X.Y version. 54 | version = '.'.join(__version__.split('.')[:2]) 55 | # The full version, including alpha/beta/rc tags. 56 | release = __version__ 57 | except ImportError: 58 | version = release = 'dev' 59 | 60 | # The language for content autogenerated by Sphinx. Refer to documentation 61 | # for a list of supported languages. 62 | #language = None 63 | 64 | # There are two options for replacing |today|: either, you set today to some 65 | # non-false value, then it is used: 66 | #today = '' 67 | # Else, today_fmt is used as the format for a strftime call. 68 | #today_fmt = '%B %d, %Y' 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | exclude_patterns = ['_build'] 73 | 74 | # The reST default role (used for this markup: `text`) to use for all documents. 75 | #default_role = None 76 | 77 | # If true, '()' will be appended to :func: etc. cross-reference text. 78 | #add_function_parentheses = True 79 | 80 | # If true, the current module name will be prepended to all description 81 | # unit titles (such as .. function::). 82 | #add_module_names = True 83 | 84 | # If true, sectionauthor and moduleauthor directives will be shown in the 85 | # output. They are ignored by default. 86 | #show_authors = False 87 | 88 | # The name of the Pygments (syntax highlighting) style to use. 89 | pygments_style = 'sphinx' 90 | 91 | # A list of ignored prefixes for module index sorting. 92 | #modindex_common_prefix = [] 93 | 94 | 95 | # -- Options for HTML output --------------------------------------------------- 96 | 97 | # The theme to use for HTML and HTML Help pages. See the documentation for 98 | # a list of builtin themes. 99 | #html_theme = 'alabaster' 100 | 101 | # Theme options are theme-specific and customize the look and feel of a theme 102 | # further. For a list of options available for each theme, see the 103 | # documentation. 104 | #html_theme_options = {} 105 | 106 | # Add any paths that contain custom themes here, relative to this directory. 107 | #html_theme_path = [] 108 | 109 | # The name for this set of Sphinx documents. If None, it defaults to 110 | # " v documentation". 111 | #html_title = None 112 | 113 | # A shorter title for the navigation bar. Default is the same as html_title. 114 | #html_short_title = None 115 | 116 | # The name of an image file (relative to this directory) to place at the top 117 | # of the sidebar. 118 | #html_logo = None 119 | 120 | # The name of an image file (within the static path) to use as favicon of the 121 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 122 | # pixels large. 123 | #html_favicon = None 124 | 125 | # Add any paths that contain custom static files (such as style sheets) here, 126 | # relative to this directory. They are copied after the builtin static files, 127 | # so a file named "default.css" will overwrite the builtin "default.css". 128 | #html_static_path = ['_static'] 129 | 130 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 131 | # using the given strftime format. 132 | #html_last_updated_fmt = '%b %d, %Y' 133 | 134 | # If true, SmartyPants will be used to convert quotes and dashes to 135 | # typographically correct entities. 136 | #html_use_smartypants = True 137 | 138 | # Custom sidebar templates, maps document names to template names. 139 | #html_sidebars = {} 140 | 141 | # Additional templates that should be rendered to pages, maps page names to 142 | # template names. 143 | #html_additional_pages = {} 144 | 145 | # If false, no module index is generated. 146 | #html_domain_indices = True 147 | 148 | # If false, no index is generated. 149 | #html_use_index = True 150 | 151 | # If true, the index is split into individual pages for each letter. 152 | #html_split_index = False 153 | 154 | # If true, links to the reST sources are added to the pages. 155 | #html_show_sourcelink = True 156 | 157 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 158 | #html_show_sphinx = True 159 | 160 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 161 | #html_show_copyright = True 162 | 163 | # If true, an OpenSearch description file will be output, and all pages will 164 | # contain a tag referring to it. The value of this option must be the 165 | # base URL from which the finished HTML is served. 166 | #html_use_opensearch = '' 167 | 168 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 169 | #html_file_suffix = None 170 | 171 | # Output file base name for HTML help builder. 172 | htmlhelp_basename = 'django-floppyformsdoc' 173 | 174 | 175 | # -- Options for LaTeX output -------------------------------------------------- 176 | 177 | # The paper size ('letter' or 'a4'). 178 | #latex_paper_size = 'letter' 179 | 180 | # The font size ('10pt', '11pt' or '12pt'). 181 | #latex_font_size = '10pt' 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'django-floppyforms.tex', u'django-floppyforms Documentation', 187 | u'Bruno Renié', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Additional stuff for the LaTeX preamble. 205 | #latex_preamble = '' 206 | 207 | # Documents to append as an appendix to all manuals. 208 | #latex_appendices = [] 209 | 210 | # If false, no module index is generated. 211 | #latex_domain_indices = True 212 | 213 | 214 | # -- Options for manual page output -------------------------------------------- 215 | 216 | # One entry per manual page. List of tuples 217 | # (source start file, name, description, authors, manual section). 218 | man_pages = [ 219 | ('index', 'django-floppyforms', u'django-floppyforms Documentation', 220 | [u'Bruno Renié'], 1) 221 | ] 222 | 223 | # gorun 224 | DIRECTORIES = ( 225 | ('', 'make html'), 226 | ) 227 | -------------------------------------------------------------------------------- /docs/customization.rst: -------------------------------------------------------------------------------- 1 | Customization 2 | ============= 3 | 4 | Override default templates 5 | -------------------------- 6 | 7 | Widgets have a ``template_name`` attribute that points to the template that is 8 | used when rendering the form. Default templates are provided for all 9 | :doc:`built-in widgets `. In most cases the default 10 | implementation of these templates have no specific behaviour and simply inherit 11 | from ``floppyforms/input.html``. They are provided mainly to give an easy 12 | way for a site-wide customization of how a specifig widget is rendered. 13 | 14 | You can easily override these templates in your project-level 15 | ``TEMPLATE_DIRS``, assuming they take precedence over app-level templates. 16 | 17 | Custom widgets with custom templates 18 | ------------------------------------ 19 | 20 | If you want to override the rendering behaviour only for a few widgets, you 21 | can extend a ``Widget`` class from FloppyForms and override the 22 | ``template_name`` attribute:: 23 | 24 | import floppyforms as forms 25 | 26 | class OtherEmailInput(forms.EmailInput): 27 | template_name = 'path/to/other_email.html' 28 | 29 | Then, the output can be customized in ``other_email.html``: 30 | 31 | .. code-block:: jinja 32 | 33 | 38 | 39 | Here we have a hardcoded placeholder without needing to instantiate the widget 40 | with an ``attrs`` dictionary:: 41 | 42 | class EmailForm(forms.Form): 43 | email = forms.EmailField(widget=OtherEmailInput()) 44 | 45 | .. _template_name_customization: 46 | 47 | You can also customize the ``template_name`` without subclassing, by passing it 48 | as an argument when instantiating the widget:: 49 | 50 | class EmailForm(forms.Form): 51 | email = forms.EmailField( 52 | widget=forms.EmailInput(template_name='path/to/other_email.html')) 53 | 54 | For advanced use, you can even customize the template used per-render, by 55 | passing a ``template_name`` argument to the widget's ``render()`` method. 56 | 57 | Adding more template variables 58 | ------------------------------ 59 | 60 | There is also a way to add extra context. This is done by subclassing the 61 | widget class and extending the ``get_context()`` method:: 62 | 63 | class OtherEmailInput(forms.EmailInput): 64 | template_name = 'path/to/other.html' 65 | 66 | def get_context(self, name, value, attrs): 67 | ctx = super(OtherEmailInput, self).get_context(name, value, attrs) 68 | ctx['foo'] = 'bar' 69 | return ctx 70 | 71 | And then the ``other.html`` template can make use of the ``{{ foo }}`` context 72 | variable. 73 | 74 | ``get_context()`` takes ``name``, ``value`` and ``attrs`` as arguments, except 75 | for all ``Select`` widgets which take an additional ``choices`` argument. 76 | 77 | In case you don't need the arguments passed to ``get_context()``, you can 78 | extend ``get_context_data()`` which doesn't take any arguments:: 79 | 80 | class EmailInput(forms.EmailInput): 81 | def get_context_data(self): 82 | ctx = super(EmailInput, self).get_context_data() 83 | ctx.update({ 84 | 'placeholder': 'hello@example.com', 85 | }) 86 | return ctx 87 | 88 | Altering the widget's ``attrs`` 89 | ------------------------------- 90 | 91 | All widget attributes except for ``type``, ``name``, ``value`` and ``required`` 92 | are put in the ``attrs`` context variable, which you can extend in 93 | ``get_context()``: 94 | 95 | .. code-block:: python 96 | 97 | def get_context(self, name, value, attrs): 98 | ctx = super(MyWidget, self).get_context(name, value, attrs) 99 | ctx['attrs']['class'] = 'mywidget' 100 | return ctx 101 | 102 | This will render the widget with an additional ``class="mywidget"`` attribute. 103 | 104 | If you want only the attribute's key to be rendered, set it to ``True``: 105 | 106 | .. code-block:: python 107 | 108 | def get_context(self, name, value, attrs): 109 | ctx = super(MyWidget, self).get_context(name, value, attrs) 110 | ctx['attrs']['awesome'] = True 111 | return ctx 112 | 113 | This will simply add ``awesome`` as a key-only attribute. 114 | -------------------------------------------------------------------------------- /docs/differences.rst: -------------------------------------------------------------------------------- 1 | Differences with django.forms 2 | ============================= 3 | 4 | So, you have a project already using ``django.forms``, and you're considering 5 | a switch to floppyforms? Here's what you need to know, assuming the only 6 | change you've made to your code is a simple change, from: 7 | 8 | .. code-block:: python 9 | 10 | from django import forms 11 | 12 | 13 | to: 14 | 15 | .. code-block:: python 16 | 17 | import floppyforms as forms 18 | 19 | .. note:: ``django.forms.*`` modules 20 | 21 | Other modules contained by ``django.forms``, such as ``forms``, ``utils`` 22 | and ``formsets`` have not been aliased. 23 | 24 | HTML 5 forms! 25 | ------------- 26 | 27 | Floppyforms adds a couple of HTML 5 features on top of the standard Django 28 | widgets: HTML syntax, more native widget types, the ``required`` attribute and 29 | client-side validation. 30 | 31 | HTML syntax instead of XHTML 32 | ```````````````````````````` 33 | 34 | Floppyforms uses an HTML syntax instead of Django's XHTML syntax. You will see 35 | ```` and not ````. 36 | 37 | Native widget types 38 | ``````````````````` 39 | 40 | Floppyforms tries to use the native HTML5 widgets whenever it's possible. Thus 41 | some widgets which used to be simple ``TextInputs`` in ``django.forms`` are 42 | now specific input that will render as ```` with the HTML5 43 | types such as ``url``, ``email``. See :ref:`widgets` for a detailed list of 44 | specific widgets. 45 | 46 | For instance, if you have declared a form using django.forms: 47 | 48 | .. code-block:: python 49 | 50 | class ThisForm(forms.Form): 51 | date = forms.DateField() 52 | 53 | The ``date`` field will be rendered as an ````. However, by 54 | just changing the forms library to floppyforms, the input will be an ````. 56 | 57 | Required attribute 58 | `````````````````` 59 | 60 | In addition to the various input types, every required field has the 61 | ``required`` attribute set to ``True`` on its widget. That means that every 62 | ```` widget for a required field will be rendered as ````. This is used for client-side validation: for 64 | instance, Firefox 4 won't let the user submit the form unless he's filled the 65 | input. This saves HTTP requests but doesn't mean you can stop validating user 66 | input. 67 | 68 | Client-side validation 69 | `````````````````````` 70 | 71 | Like with the ``required`` attribute, the ``pattern`` attribute is especially 72 | interesting for slightly more complex client-side validation. The ``SlugField`` 73 | and the ``IPAddressField`` both have a pattern attached to the ````. 74 | 75 | However having these validations backed directly into the HTML and therefore 76 | allowing the browser to validate the user input might not always what you want 77 | to have. Sometimes you just want to have a form where it should be allowed to 78 | submit invalid data. In that case you can use the ``novalidate`` attribute on 79 | the ```` HTML tag or the ``formnovalidate`` attribute on the submit 80 | button: 81 | 82 | .. code-block:: html 83 | 84 | 85 | This input will not be validated: 86 | 87 | 88 | 89 |
90 | Another way to not validate the form in the browser is using the 91 | formnovalidate attribute on the submit button: 92 | 93 |
94 | 95 | Read the corresponding documentation for `novalidate`_ and `formnovalidate`_ on 96 | the Mozilla Developer Network if you want to know more. 97 | 98 | .. _novalidate: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-novalidate 99 | .. _formnovalidate: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-formnovalidate 100 | 101 | ModelForms 102 | ---------- 103 | 104 | Prior to version 1.2 of django-floppyforms, you had to take some manual 105 | efforts to make your modelforms work with floppyforms. This is now done 106 | seamlessly, but since this was introduced a backwards incompatible change, it 107 | was necessary to provde a deprecation path. 108 | 109 | So if you start out new with django-floppyforms just use ``import 110 | floppyforms.__future__ as forms`` as your import instead of ``import 111 | floppyforms as forms`` when you want to define modelforms. 112 | 113 | For more information see the :ref:`section about modelforms in the usage 114 | documentation `. 115 | 116 | ``help_text`` values are autoescaped by default 117 | ----------------------------------------------- 118 | 119 | If you use HTML in the ``help_text`` value for a Django form field and are not 120 | using django-floppyforms, then you will get the correct HTML rendered in the 121 | template. For example you have this form:: 122 | 123 | from django import forms 124 | 125 | class DjangoForm(forms.Form): 126 | myfield = forms.CharField(help_text='A help text.') 127 | 128 | When you now use this form with ``{{ form.as_p }}`` in the template, you will 129 | get the help text put in the template as it is, with no HTML escaping. That 130 | might imply a security risk if your help text contains content from untrusted 131 | sources. django-floppyforms applies autoescaping by default to the help text. 132 | So if you define:: 133 | 134 | import floppyforms as forms 135 | 136 | class FloppyForm(forms.Form): 137 | myfield = forms.CharField(help_text='A help text.') 138 | 139 | And then use ``{{ form.as_p }}``, you will get an output that contains ``A 140 | <strong&;gt;help</strong> text.``. You can disable the autoescaping 141 | of the help text by using Django's ``mark_safe`` helper:: 142 | 143 | from django.utils.html import mark_safe 144 | import floppyforms as forms 145 | 146 | class FloppyForm(forms.Form): 147 | myfield = forms.CharField(help_text=mark_safe('A help text.')) 148 | 149 | 150 | ``TEMPLATE_STRING_IF_INVALID`` caveats 151 | -------------------------------------- 152 | 153 | The use of a non-empty ``TEMPLATE_STRING_IF_INVALID`` setting can impact 154 | rendering. Missing template variables are rendered using the content of ``TEMPLATE_STRING_IF_INVALID`` but filters used on non-existing variables are not applied (see `django's documentation on how invalid template variables are 155 | handled`__ for more details). 156 | 157 | __ https://docs.djangoproject.com/en/dev/ref/templates/api/#invalid-template-variables 158 | 159 | django-floppyforms assumes in its predefined form layouts that 160 | all filters are applied. You can work around this by making your 161 | ``TEMPLATE_STRING_IF_INVALID`` evaluate to ``False`` but still keep its 162 | string representation. Here is an example how you could achieve this in your 163 | ``settings.py``: 164 | 165 | .. code-block:: python 166 | 167 | # on Python 2 168 | class InvalidVariable(unicode): 169 | def __nonzero__(self): 170 | return False 171 | 172 | # on Python 3 173 | class InvalidVariable(str): 174 | def __bool__(self): 175 | return False 176 | 177 | TEMPLATE_STRING_IF_INVALID = InvalidVariable(u'INVALID') 178 | 179 | Getting back Django's behaviour 180 | ------------------------------- 181 | 182 | If you need to get the same output as standard Django forms: 183 | 184 | * Override ``floppyforms/input.html``, ``floppyforms/radio.html``, 185 | ``floppyforms/clearable_input.html``, ``floppyforms/textarea.html`` and 186 | ``floppyforms/checkbox_select.html`` to use an XHTML syntax 187 | 188 | * Remove the ``required`` attribute from the same templates, as well as ``floppyforms/select.html`` 189 | 190 | * Make sure your fields which have HTML5 widgets by default get simple 191 | ``TextInputs`` instead: 192 | 193 | .. code-block:: python 194 | 195 | class Foo(forms.Form): 196 | url = forms.URLField(widget=forms.TextInput) 197 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Example widgets 2 | =============== 3 | 4 | A date picker 5 | ------------- 6 | 7 | This snippet implements a rich date picker using the browser's date picker if 8 | the ``date`` input type is supported and falls back to a jQuery UI date 9 | picker. 10 | 11 | .. code-block:: python 12 | 13 | # forms.py 14 | import floppyforms as forms 15 | 16 | 17 | class DatePicker(forms.DateInput): 18 | template_name = 'datepicker.html' 19 | 20 | class Media: 21 | js = ( 22 | 'js/jquery.min.js', 23 | 'js/jquery-ui.min.js', 24 | ) 25 | css = { 26 | 'all': ( 27 | 'css/jquery-ui.css', 28 | ) 29 | } 30 | 31 | 32 | class DateForm(forms.Form): 33 | date = forms.DateField(widget=DatePicker) 34 | 35 | .. code-block:: jinja 36 | 37 | {# datepicker.html #} 38 | {% include "floppyforms/input.html" %} 39 | 40 | 52 | 53 | Here is how chromium renders it with its native (but sparse) date picker: 54 | 55 | .. image:: images/datepicker-chromium.png 56 | 57 | And here is the jQuery UI date picker as shown by Firefox: 58 | 59 | .. image:: images/datepicker-jquery-ui.png 60 | 61 | An autofocus input 62 | ------------------ 63 | 64 | A text input with the autofocus attribute and a fallback for browsers that 65 | doesn't support it. 66 | 67 | .. code-block:: python 68 | 69 | # forms.py 70 | import floppyforms as forms 71 | 72 | 73 | class AutofocusInput(forms.TextInput): 74 | template_name = 'autofocus.html' 75 | 76 | def get_context_data(self): 77 | self.attrs['autofocus'] = True 78 | return super(AutofocusInput, self).get_context_data() 79 | 80 | 81 | class AutofocusForm(forms.Form): 82 | text = forms.CharField(widget=AutofocusInput) 83 | 84 | .. code-block:: jinja 85 | 86 | {# autofocus.html #} 87 | {% include "floppyforms/input.html" %} 88 | 89 | 96 | 97 | A slider 98 | -------- 99 | 100 | A ``range`` input that uses the browser implementation or falls back to 101 | jQuery UI. 102 | 103 | .. code-block:: python 104 | 105 | # forms.py 106 | import floppyforms as forms 107 | 108 | 109 | class Slider(forms.RangeInput): 110 | min = 5 111 | max = 20 112 | step = 5 113 | template_name = 'slider.html' 114 | 115 | class Media: 116 | js = ( 117 | 'js/jquery.min.js', 118 | 'js/jquery-ui.min.js', 119 | ) 120 | css = { 121 | 'all': ( 122 | 'css/jquery-ui.css', 123 | ) 124 | } 125 | 126 | 127 | class SlideForm(forms.Form): 128 | num = forms.IntegerField(widget=Slider) 129 | 130 | def clean_num(self): 131 | num = self.cleaned_data['num'] 132 | if not 5 <= num <= 20: 133 | raise forms.ValidationError("Enter a value between 5 and 20") 134 | 135 | if not num % 5 == 0: 136 | raise forms.ValidationError("Enter a multiple of 5") 137 | return num 138 | 139 | 140 | .. code-block:: jinja 141 | 142 | {# slider.html #} 143 | {% include "floppyforms/input.html" %} 144 |
145 | 146 | 163 | 164 | Here is how chromium renders it with its native slider: 165 | 166 | .. image:: images/slider-chromium.png 167 | 168 | And here is the jQuery UI slider as shown by Firefox: 169 | 170 | .. image:: images/slider-jquery-ui.png 171 | 172 | A placeholder with fallback 173 | --------------------------- 174 | 175 | An ```` with a ``placeholder`` attribute and a javascript fallback for 176 | broader browser support. 177 | 178 | .. code-block:: python 179 | 180 | 181 | # forms.py 182 | import floppyforms as forms 183 | 184 | 185 | class PlaceholderInput(forms.TextInput): 186 | template_name = 'placeholder_input.html' 187 | 188 | 189 | class MyForm(forms.Form): 190 | text = forms.CharField(widget=PlaceholderInput( 191 | attrs={'placeholder': _('Some text here')}, 192 | )) 193 | 194 | .. code-block:: jinja 195 | 196 | {# placeholder_input.html #} 197 | 198 | {% include "floppyforms/input.html" %} 199 | 200 | 218 | 219 | 220 | An image clearable input with thumbnail 221 | --------------------------------------- 222 | 223 | If we have an image set for the field, display the image and propose to clear or to update. 224 | 225 | .. code-block:: python 226 | 227 | # forms.py 228 | import floppyforms as forms 229 | 230 | class ImageThumbnailFileInput(forms.ClearableFileInput): 231 | template_name = 'floppyforms/image_thumbnail.html' 232 | 233 | 234 | class ImageForm(forms.ModelForm): 235 | class Meta: 236 | model = Item 237 | fields = ('image',) 238 | widgets = {'image': ImageThumbnailFileInput} 239 | 240 | 241 | .. code-block:: django 242 | 243 | {# image_thumbnail.html #} 244 | {% load i18n %} 245 | {% if value.url %}{% trans "Currently:" %} {{ value }} 246 | {% if not required %} 247 |

248 |

249 | {% else %}
250 | {% endif %} 251 | {% trans "Change:" %} 252 | {% endif %} 253 | 254 | 255 | You now have your image: 256 | 257 | .. image:: images/image_with_thumbnail.png 258 | -------------------------------------------------------------------------------- /docs/images/datepicker-chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-floppyforms/3c019feda452407522b61c71c1df373c7cc6152c/docs/images/datepicker-chromium.png -------------------------------------------------------------------------------- /docs/images/datepicker-jquery-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-floppyforms/3c019feda452407522b61c71c1df373c7cc6152c/docs/images/datepicker-jquery-ui.png -------------------------------------------------------------------------------- /docs/images/geomcollection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-floppyforms/3c019feda452407522b61c71c1df373c7cc6152c/docs/images/geomcollection.png -------------------------------------------------------------------------------- /docs/images/gmappoly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-floppyforms/3c019feda452407522b61c71c1df373c7cc6152c/docs/images/gmappoly.png -------------------------------------------------------------------------------- /docs/images/image_with_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-floppyforms/3c019feda452407522b61c71c1df373c7cc6152c/docs/images/image_with_thumbnail.png -------------------------------------------------------------------------------- /docs/images/osmlinestring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-floppyforms/3c019feda452407522b61c71c1df373c7cc6152c/docs/images/osmlinestring.png -------------------------------------------------------------------------------- /docs/images/pointfield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-floppyforms/3c019feda452407522b61c71c1df373c7cc6152c/docs/images/pointfield.png -------------------------------------------------------------------------------- /docs/images/slider-chromium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-floppyforms/3c019feda452407522b61c71c1df373c7cc6152c/docs/images/slider-chromium.png -------------------------------------------------------------------------------- /docs/images/slider-jquery-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-floppyforms/3c019feda452407522b61c71c1df373c7cc6152c/docs/images/slider-jquery-ui.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | django-floppyforms 2 | ================== 3 | 4 | **django-floppyforms** is an application that gives you full control of the 5 | output of forms rendering. The forms API and features are exactly the same 6 | as Django's, the key difference is that fields and widgets are rendered in 7 | templates instead of using string interpolation, giving you full control of 8 | the output using Django templates. 9 | 10 | The widgets API allows you to customize and extend the widgets behaviour, 11 | making it very easy to define custom widgets. The default widgets are very 12 | similar to the default Django widgets, except that they implement some nice 13 | features of HTML5 forms, such as the ``placeholder`` and ``required`` 14 | attribute, as well as the new ```` types. For more information, read 15 | `this`_ if you haven't yet. 16 | 17 | .. _this: http://diveintohtml5.info/forms.html 18 | 19 | The form rendering API is a set of template tags that lets you render forms 20 | using custom layouts. This is very similar to Django's ``as_p``, ``as_ul`` or 21 | ``as_table``, except that you can customize and add layouts to your 22 | convenience. 23 | 24 | The source code is hosted on `github`_. 25 | 26 | .. _github: https://github.com/jazzband/django-floppyforms 27 | 28 | Installation 29 | ------------ 30 | 31 | django-floppyforms is tested under the following configurations: 32 | 33 | - Django 1.11 and Pythons 2.7 or >=3.5 34 | - Django 2.1 and Python >= 3.5 35 | 36 | As a general principle, we aim to support Django + Python combinations that still receive security fixes, 37 | so bug reports on other combinations of library + interpreter are welcome. 38 | 39 | 40 | Two-step process to install django-floppyforms: 41 | 42 | * ``pip install django-floppyforms`` 43 | * Add ``'floppyforms'`` to your ``INSTALLED_APPS`` 44 | 45 | When you're done you can jump to the :doc:`usage ` section. For the 46 | impatient reader, there's also an :doc:`examples ` section. 47 | 48 | Using ``django-floppyforms`` 49 | ---------------------------- 50 | 51 | .. toctree:: 52 | :maxdepth: 2 53 | 54 | usage 55 | widgets 56 | customization 57 | widgets-reference 58 | geodjango 59 | layouts 60 | templatetags 61 | differences 62 | examples 63 | bootstrap 64 | 65 | .. toctree:: 66 | :maxdepth: 2 67 | 68 | changelog 69 | 70 | Why the name? 71 | ------------- 72 | 73 | * There aren't enough packages with silly names in the Django community. So, 74 | here's one more. 75 | * The name reflects the idea that a widget can take any kind of shape, if that 76 | makes any sense. 77 | 78 | Performance 79 | ----------- 80 | 81 | Each time a widget is rendered, there is a template inclusion. To what extent 82 | does it affect performance? You can try with this little script: 83 | 84 | .. code-block:: python 85 | 86 | import timeit 87 | 88 | django = """from django import forms 89 | 90 | class DjangoForm(forms.Form): 91 | text = forms.CharField() 92 | slug = forms.SlugField() 93 | some_bool = forms.BooleanField() 94 | email = forms.EmailField() 95 | date = forms.DateTimeField() 96 | file_ = forms.FileField() 97 | 98 | rendered = DjangoForm().as_p()""" 99 | 100 | flop = """import floppyforms as forms 101 | 102 | class FloppyForm(forms.Form): 103 | text = forms.CharField() 104 | slug = forms.SlugField() 105 | some_bool = forms.BooleanField() 106 | email = forms.EmailField() 107 | date = forms.DateTimeField() 108 | file_ = forms.FileField() 109 | 110 | rendered = FloppyForm().as_p()""" 111 | 112 | def time(stmt): 113 | t = timeit.Timer(stmt=stmt) 114 | return t.timeit(number=1000) 115 | 116 | print "Plain django:", time(django) 117 | print "django-floppyforms:", time(flop) 118 | 119 | The result varies if you're doing template caching or not. To put it simply, 120 | here is the average time for a single iteration on a MacBookPro @ 2.53GHz. 121 | 122 | ================== ============================= =========================== 123 | Method Time without template caching Time with template caching 124 | ================== ============================= =========================== 125 | Plain Django 1.63973999023 msec 1.6320669651 msec 126 | django-floppyforms 9.05481505394 msec 3.0161819458 msec 127 | ================== ============================= =========================== 128 | 129 | Even with template caching, the rendering time is doubled. However the impact 130 | is probably not noticeable since rendering the form above takes 3 131 | milliseconds instead of 1.6: **it still takes no time :)**. The use of 132 | template caching in production is, of course, encouraged. 133 | -------------------------------------------------------------------------------- /docs/templatetags.rst: -------------------------------------------------------------------------------- 1 | Template tags 2 | ============= 3 | 4 | .. highlight:: html+django 5 | 6 | To load the floppyforms template library you have to load it on 7 | top of your templates first:: 8 | 9 | {% load floppyforms %} 10 | 11 | .. _form templatetag: 12 | 13 | form 14 | ---- 15 | 16 | .. versionadded:: 1.0 17 | 18 | The ``form`` tag is used to render one or more form instances using a 19 | template. :: 20 | 21 | {% form myform using "floppyforms/layouts/p.html" %} 22 | {% form myform another_form form3 using "floppyforms/layouts/p.html" %} 23 | 24 | django-floppyforms provides three built-in layouts: 25 | 26 | * ``floppyforms/layouts/p.html``: wraps each field in a ``

`` tag. 27 | * ``floppyforms/layouts/ul.html``: wraps each field in a ``

  • `` tag. 28 | * ``floppyforms/layouts/table.html``: wraps each form row with a ````, 29 | the label with a ```` and the widget with a ```` tag. 30 | 31 | See the documentation on :doc:`layouts and how to customize them 32 | ` for more details. 33 | 34 | You can use a default layout by leaving the ``using ...`` out:: 35 | 36 | {% form myform %} 37 | 38 | In this case the ``floppyforms/layouts/default.html`` template will be used, 39 | which by default is the same as ``floppyforms/layouts/p.html``. 40 | 41 | Sometimes it is necessary to pass additional template variables into the 42 | context of a form layout. This can be done in the same way and with the same 43 | syntax as django's `include template tag`_:: 44 | 45 | {% form myform using "layout_with_title.html" with title="Please fill in the form" only %} 46 | 47 | The ``only`` keyword, as shown in the example above, acts also the same way as 48 | it does in the ``include`` tag. It prevents other, not explicitly 49 | specified, variables from being available in the layout's template context. 50 | 51 | .. _include template tag: https://docs.djangoproject.com/en/dev/ref/templates/builtins/#std:templatetag-include 52 | 53 | Inline layouts 54 | ~~~~~~~~~~~~~~ 55 | 56 | Inlining the form layout is also possible if you don't plan to reuse it 57 | somewhere else. This is done by not specifying a template name after the 58 | ``using`` keyword:: 59 | 60 | {% form myform using %} 61 | ... your form layout here ... 62 | {% endform %} 63 | 64 | .. _formconfig templatetag: 65 | 66 | formconfig 67 | ---------- 68 | 69 | .. versionadded:: 1.0 70 | 71 | The ``formconfig`` tag can be used to configure some of the form template 72 | tags arguments upfront so that they don't need to be specified over and over 73 | again. 74 | 75 | The first argument specifies which part of the form should be configured: 76 | 77 | ``row`` 78 | ~~~~~~~ 79 | 80 | The ``formrow`` tag takes arguments to specify which template is used to 81 | render the row and whether additional variables are passed into this template. 82 | These parameters can be configured for multiple form rows with a ``{% 83 | formconfig row ... %}`` tag. The syntax is the same as with ``formrow``:: 84 | 85 | {% formconfig row using "floppyforms/rows/p.html" %} 86 | {% formconfig row using "my_form_layout.html" with hide_errors=1 only %} 87 | 88 | Please note that form configurations will only be available in a form layout 89 | or wrapped by a ``form`` template tag. They also only apply to all the 90 | form tags that come after the ``formconfig``. It is possible to overwrite 91 | already set options. Here is a valid example:: 92 | 93 | {% form myform using %} 94 |
    {% csrf_token %} 95 | {% formconfig row using "floppyforms/rows/p.html" %} 96 | {% formrow form.username %} 97 | {% formrow form.password %} 98 | 99 | {% formconfig row using "floppyforms/rows/tr.html" %} 100 | 101 | {% formrow form.firstname form.lastname %} 102 | {% formrow form.age %} 103 | {% formrow form.city form.street %} 104 |
    105 | 106 |

    107 |
    108 | {% endform %} 109 | 110 | However a configuration set with ``formconfig`` will only be available inside 111 | the ``form`` tag that it was specified in. This makes it possible to scope the 112 | configuration with an extra use of the ``form`` tag. See this example:: 113 | 114 | {% form myform using %} 115 |
    {% csrf_token %} 116 | {# will use default row template #} 117 | {% formrow form.username %} 118 | 119 | {% form form using %} 120 |
      121 | {# this config will not be available outside of the wrapping form tag #} 122 | {% formconfig row using "floppyforms/rows/li.html" %} 123 | 124 | {# will use configured li row template #} 125 | {% formrow form.password form.password2 %} 126 |
    127 | {% endform %} 128 | 129 | {# will use default row template #} 130 | {% formrow form.firstname form.lastname %} 131 | 132 |

    133 |
    134 | {% endform %} 135 | 136 | ``field`` 137 | ~~~~~~~~~ 138 | 139 | A form field takes the same arguments as a form row does, so the same 140 | configuration options are available here, in addition to a ``for`` keyword to 141 | limit which fields the specified configuration will apply to. 142 | 143 | Using the ``for`` keyword allows you to limit the configuration to a specific 144 | field or a set of fields. After the ``for`` keyword, you can give: 145 | 146 | * a form field, like ``form.field_name`` 147 | * the name of a specific field, like ``"username"`` 148 | * a class name of a form field, like ``"CharField"`` 149 | * a class name of a widget, like ``"Textarea"`` 150 | 151 | The configuration applied by ``{% formconfig field ... %}`` is then only 152 | available on those fields that match the given criteria. 153 | 154 | Here is an example to clarify things. The ``formconfig`` in the snippet below 155 | will only affect the second ``formfield`` tag but the first one will be left 156 | untouched:: 157 | 158 | {% formconfig field using "input.html" with type="password" for userform.password %} 159 | {% formfield userform.username %} 160 | {% formfield userform.password %} 161 | 162 | And some more examples showing the filtering applied on field names, field 163 | types and widget types:: 164 | 165 | {% formconfig field with placeholder="Type to search ..." for "search" %} 166 | {% formfield myform.search %} 167 | 168 | {% formconfig field using "forms/widgets/textarea.html" for "CharField" %} 169 | {% formfield myform.comment %} 170 | 171 | {% formconfig field using class="text_input" for "TextInput" %} 172 | {% formfield myform.username %} 173 | 174 | .. note:: Please note that the filterings that act on the field class name and 175 | widget class name (like ``"CharField"``) also match on subclasses of those 176 | field. This means if your class inherits from 177 | ``django.forms.fields.CharField`` it will also get the configuration applied 178 | specified by ``{% formconfig field ... for "CharField" %}``. 179 | 180 | .. _formfield templatetag: 181 | 182 | formfield 183 | --------- 184 | 185 | .. versionadded:: 1.0 186 | 187 | Renders a form field using the associated widget. You can specify a widget 188 | template with the ``using`` keyword. Otherwise it will fall back to the 189 | :doc:`widget's default template `. 190 | 191 | It also accepts ``include``-like parameters:: 192 | 193 | {% formfield userform.password using "input.html" with type="password" %} 194 | 195 | The ``formfield`` tag should only be used inside a form layout, usually in a 196 | row template. 197 | 198 | .. _formrow templatetag: 199 | 200 | formrow 201 | ------- 202 | 203 | .. versionadded:: 1.0 204 | 205 | The ``formrow`` tag is a quite similar to the ``form`` tag but acts on a 206 | set of form fields instead of complete forms. It takes one or more fields as 207 | arguments and a template which should be used to render those fields:: 208 | 209 | {% formrow userform.firstname userform.lastname using "floppyforms/rows/p.html" %} 210 | 211 | It also accepts ``include``-like parameters:: 212 | 213 | {% formrow myform.field using "my_row_layout.html" with hide_errors=1 only %} 214 | 215 | The ``formrow`` tag is usually only used in form layouts. 216 | 217 | See the documentation on :doc:`row templates and how they are customized 218 | ` for more details. 219 | 220 | .. _widget templatetag: 221 | 222 | widget 223 | ------ 224 | 225 | .. versionadded:: 1.0 226 | 227 | The ``widget`` tag lets you render a widget with the outer template context 228 | available. By default widgets are rendered using a completely isolated 229 | context. In some cases you might want to access the outer context, for 230 | instance for using floppyforms widgets with `django-sekizai`_:: 231 | 232 | {% for field in form %} 233 | {% if not field.is_hidden %} 234 | {{ field.label_tag }} 235 | {% widget field %} 236 | {{ field.errors }} 237 | {% else %} 238 | {% widget field %} 239 | {% endif %} 240 | {% endfor %} 241 | 242 | .. _django-sekizai: http://django-sekizai.readthedocs.org/en/latest/ 243 | 244 | You can safely use the ``widget`` tag with non-floppyforms widgets, they will 245 | be properly rendered. However, since they're not template-based, they won't be 246 | able to access any template context. 247 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | Forms 5 | ````` 6 | 7 | Floppyforms are supposed to work just like Django forms: 8 | 9 | .. code-block:: python 10 | 11 | import floppyforms as forms 12 | 13 | class ProfileForm(forms.Form): 14 | name = forms.CharField() 15 | email = forms.EmailField() 16 | url = forms.URLField() 17 | 18 | With some template code: 19 | 20 | .. code-block:: jinja 21 | 22 |
    23 | {% csrf_token %} 24 | {{ form.as_p }} 25 |

    26 |
    27 | 28 | The form will be rendered using the ``floppyforms/layouts/p.html`` template. 29 | See the :doc:`documentation about layouts ` for details. 30 | 31 | Each field has a default widget and widgets are rendered using templates. 32 | 33 | Default templates are provided and their output is relatively similar to 34 | Django widgets, with a few :doc:`minor differences`: 35 | 36 | * HTML5 ```` types are supported: ``url``, ``email``, ``date``, 37 | ``datetime``, ``time``, ``number``, ``range``, ``search``, ``color``, 38 | ``tel``. 39 | 40 | * The ``required`` and ``placeholder`` attributes are also supported. 41 | 42 | Widgets are rendered with the following context variables: 43 | 44 | * ``hidden``: set to ``True`` if the field is hidden. 45 | * ``required``: set to ``True`` if the field is required. 46 | * ``type``: the input type. Can be ``text``, ``password``, etc. etc. 47 | * ``name``: the name of the input. 48 | * ``attrs``: the dictionary passed as a keyword argument to the widget. It 49 | contains the ``id`` attribute of the widget by default. 50 | 51 | Each widget has a ``template_name`` attribute which points to the template to 52 | use when rendering the widget. A basic template for an ```` widget may 53 | look like: 54 | 55 | .. code-block:: jinja 56 | 57 | 63 | 64 | The default floppyforms template for an ```` widget is slightly more 65 | complex. 66 | 67 | Some widgets may provide extra context variables and extra attributes: 68 | 69 | ====================== ====================================== ============== 70 | Widget Extra context Extra ``attrs`` 71 | ====================== ====================================== ============== 72 | Textarea ``rows``, ``cols`` 73 | NumberInput ``min``, ``max``, ``step`` 74 | RangeInput ``min``, ``max``, ``step`` 75 | Select ``optgroups``, ``multiple`` 76 | RadioSelect ``optgroups``, ``multiple`` 77 | NullBooleanSelect ``optgroups``, ``multiple`` 78 | SelectMultiple ``optgroups``, ``multiple`` (``True``) 79 | CheckboxSelectMultiple ``optgroups``, ``multiple`` (``True``) 80 | ====================== ====================================== ============== 81 | 82 | Furthermore, you can specify custom ``attrs`` during widget definition. For 83 | instance, with a field created this way: 84 | 85 | .. code-block:: python 86 | 87 | bar = forms.EmailField(widget=forms.EmailInput(attrs={'placeholder': 'john@example.com'})) 88 | 89 | Then the ``placeholder`` variable is available in the ``attrs`` template 90 | variable. 91 | 92 | .. _usage-modelforms: 93 | 94 | ModelForms 95 | `````````` 96 | 97 | You can use ``ModelForms`` with floppyforms as you would use a ordinary django 98 | ``ModelForm``. Here is an example showing it for a basic ``Profile`` model: 99 | 100 | .. code-block:: python 101 | 102 | class Profile(models.Model): 103 | name = models.CharField(max_length=255) 104 | url = models.URLField() 105 | 106 | Now create a ``ModelForm`` using floppyforms: 107 | 108 | .. code-block:: python 109 | 110 | import floppyforms.__future__ as forms 111 | 112 | class ProfileForm(forms.ModelForm): 113 | class Meta: 114 | model = Profile 115 | fields = ('name', 'url') 116 | 117 | The ``ProfileForm`` will now have form fields for all the model fields. So 118 | there will be a ``floppyforms.CharField`` used for the ``Profile.name`` model 119 | field and a ``floppyforms.URLField`` for ``Profile.url``. 120 | 121 | .. note:: 122 | 123 | Please note that you have to import from ``floppyforms.__future__`` to use 124 | this feature. Here is why: 125 | 126 | This behaviour changed in version 1.2 of **django-floppyforms**. Before, 127 | no alterations were made to the widgets of a ``ModelForm``. So you had to 128 | take care of assigning the floppyforms widgets to the django form fields 129 | yourself to use the template based rendering provided by floppyforms. Here 130 | is an example of how you would have done it with django-floppyforms 1.1 131 | and earlier: 132 | 133 | .. code-block:: python 134 | 135 | import floppyforms as forms 136 | 137 | class ProfileForm(forms.ModelForm): 138 | class Meta: 139 | model = Profile 140 | fields = ('name', 'url') 141 | widgets = { 142 | 'name': forms.TextInput, 143 | 'url': forms.URLInput, 144 | } 145 | 146 | Since the change is backwards incompatible, we decided to provide a 147 | deprecation path. If you create a ``ModelForm`` with django-floppyforms 148 | 1.2 and use ``import floppyforms as forms`` as the import you will get the 149 | old behaviour and you will see a ``DeprecationWarning``. 150 | 151 | To use the new behaviour, you can use ``import floppyforms.__future__ as 152 | forms`` as the import. 153 | 154 | Please make sure to test your code if your modelforms work still as 155 | expected with the new behaviour. The old version's behaviour will be 156 | removed completely with django-floppyforms 1.4. 157 | -------------------------------------------------------------------------------- /docs/widgets-reference.rst: -------------------------------------------------------------------------------- 1 | Widgets reference 2 | ================= 3 | 4 | For each widgets, the default class attributes. 5 | 6 | .. module:: floppyforms.widgets 7 | :synopsis: FloppyForm's form widgets 8 | 9 | .. class:: Input 10 | 11 | .. attribute:: Input.datalist 12 | 13 | A list of possible values, which will be rendered as a ```` 14 | element tied to the input. Note that the list of options passed as 15 | ``datalist`` elements are only **suggestions** and are not related to 16 | form validation. 17 | 18 | .. attribute:: Input.template_name 19 | 20 | A path to a template that should be used to render this widget. You can 21 | change the template name per instance by passing in a keyword argument 22 | called ``template_name``. This will override the default that is set by 23 | the widget class. You can also change the template used for rendering by 24 | an argument to the ``Input.render()`` method. See more about exchanging 25 | the templates in the :doc:`documentation about customization `. 26 | 27 | .. class:: TextInput 28 | 29 | .. attribute:: TextInput.template_name 30 | 31 | ``'floppyforms/text.html'`` 32 | 33 | .. attribute:: TextInput.input_type 34 | 35 | ``text`` 36 | 37 | .. class:: PasswordInput 38 | 39 | .. attribute:: PasswordInput.template_name 40 | 41 | ``'floppyforms/password.html'`` 42 | 43 | .. attribute:: PasswordInput.input_type 44 | 45 | ``password`` 46 | 47 | .. class:: HiddenInput 48 | 49 | .. attribute:: HiddenInput.template_name 50 | 51 | ``'floppyforms/hidden.html'`` 52 | 53 | .. attribute:: HiddenInput.input_type 54 | 55 | ``hidden`` 56 | 57 | .. class:: SlugInput 58 | 59 | .. attribute:: SlugInput.template_name 60 | 61 | ``'floppyforms/slug.html'`` 62 | 63 | .. attribute:: SlugInput.input_type 64 | 65 | ``text`` 66 | 67 | An text input that renders as ```` for 68 | client-side validation of the slug. 69 | 70 | .. class:: IPAddressInput 71 | 72 | .. attribute:: IPAddressInput.template_name 73 | 74 | ``'floppyforms/ipaddress.html'`` 75 | 76 | .. attribute:: IPAddressInput.input_type 77 | 78 | ``text`` 79 | 80 | An text input that renders as ```` for 81 | client-side validation. The pattern checks that the entered value is a 82 | valid IPv4 address. 83 | 84 | .. class:: FileInput 85 | 86 | .. attribute:: FileInput.template_name 87 | 88 | ``'floppyforms/file.html'`` 89 | 90 | .. attribute:: FileInput.input_type 91 | 92 | ``file`` 93 | 94 | .. class:: ClearableFileInput 95 | 96 | .. attribute:: ClearableFileInput.template_name 97 | 98 | ``'floppyforms/clearable_input.html'`` 99 | 100 | .. attribute:: ClearableFileInput.input_type 101 | 102 | ``file`` 103 | 104 | .. attribute:: ClearableFileInput.initial_text 105 | 106 | ``_('Currently')`` 107 | 108 | .. attribute:: ClearableFileInput.input_text 109 | 110 | ``_('Change')`` 111 | 112 | .. attribute:: ClearableFileInput.clear_checkbox_label 113 | 114 | ``_('Clear')`` 115 | 116 | The ``initial_text``, ``input_text`` and ``clear_checkbox_label`` 117 | attributes are provided in the template context. 118 | 119 | .. class:: EmailInput 120 | 121 | .. attribute:: EmailInput.template_name 122 | 123 | ``'floppyforms/email.html'`` 124 | 125 | .. attribute:: EmailInput.input_type 126 | 127 | ``email`` 128 | 129 | .. class:: URLInput 130 | 131 | .. attribute:: URLInput.template_name 132 | 133 | ``'floppyforms/url.html'`` 134 | 135 | .. attribute:: URLInput.input_type 136 | 137 | ``url`` 138 | 139 | .. class:: SearchInput 140 | 141 | .. attribute:: SearchInput.template_name 142 | 143 | ``'floppyforms/search.html'`` 144 | 145 | .. attribute:: SearchInput.input_type 146 | 147 | ``search`` 148 | 149 | .. class:: ColorInput 150 | 151 | .. attribute:: ColorInput.template_name 152 | 153 | ``'floppyforms/color.html'`` 154 | 155 | .. attribute:: ColorInput.input_type 156 | 157 | ``color`` 158 | 159 | .. class:: PhoneNumberInput 160 | 161 | .. attribute:: PhoneNumberInput.template_name 162 | 163 | ``'floppyforms/phonenumber.html'`` 164 | 165 | .. attribute:: PhoneNumberInput.input_type 166 | 167 | ``tel`` 168 | 169 | .. class:: DateInput 170 | 171 | .. attribute:: DateInput.template_name 172 | 173 | ``'floppyforms/date.html'`` 174 | 175 | .. attribute:: DateInput.input_type 176 | 177 | ``date`` 178 | 179 | A widget that renders as ````. Value 180 | is rendered in ISO-8601 format (i.e. ``YYYY-MM-DD``) regardless of 181 | localization settings. 182 | 183 | 184 | .. class:: DateTimeInput 185 | 186 | .. attribute:: DateTimeInput.template_name 187 | 188 | ``'floppyforms/datetime.html'`` 189 | 190 | .. attribute:: DateTimeInput.input_type 191 | 192 | ``datetime`` 193 | 194 | .. class:: TimeInput 195 | 196 | .. attribute:: TimeInput.template_name 197 | 198 | ``'floppyforms/time.html'`` 199 | 200 | .. attribute:: TimeInput.input_type 201 | 202 | ``time`` 203 | 204 | .. class:: NumberInput 205 | 206 | .. attribute:: NumberInput.template_name 207 | 208 | ``'floppyforms/number.html'`` 209 | 210 | .. attribute:: NumberInput.input_type 211 | 212 | ``number`` 213 | 214 | .. attribute:: NumberInput.min 215 | 216 | None 217 | 218 | .. attribute:: NumberInput.max 219 | 220 | None 221 | 222 | .. attribute:: NumberInput.step 223 | 224 | None 225 | 226 | ``min``, ``max`` and ``step`` are available in the ``attrs`` template 227 | variable if they are not None. 228 | 229 | .. class:: RangeInput 230 | 231 | .. attribute:: NumberInput.template_name 232 | 233 | ``'floppyforms/range.html'`` 234 | 235 | .. attribute:: RangeInput.input_type 236 | 237 | ``range`` 238 | 239 | .. attribute:: RangeInput.min 240 | 241 | None 242 | 243 | .. attribute:: RangeInput.max 244 | 245 | None 246 | 247 | .. attribute:: RangeInput.step 248 | 249 | None 250 | 251 | ``min``, ``max`` and ``step`` are available in the ``attrs`` template 252 | variable if they are not None. 253 | 254 | .. class:: Textarea 255 | 256 | .. attribute:: Textarea.template_name 257 | 258 | ``'floppyforms/textarea.html'`` 259 | 260 | .. attribute:: Textarea.rows 261 | 262 | 10 263 | 264 | .. attribute:: Textarea.cols 265 | 266 | 40 267 | 268 | ``rows`` and ``cols`` are available in the ``attrs`` variable. 269 | 270 | .. class:: CheckboxInput 271 | 272 | .. attribute:: CheckboxInput.template_name 273 | 274 | ``'floppyforms/checkbox.html'`` 275 | 276 | .. attribute:: CheckboxInput.input_type 277 | 278 | ``checkbox`` 279 | 280 | .. class:: Select 281 | 282 | .. attribute:: Select.template_name 283 | 284 | ``'floppyforms/select.html'`` 285 | 286 | .. class:: NullBooleanSelect 287 | 288 | .. attribute:: NullBooleanSelect.template_name 289 | 290 | ``'floppyforms/select.html'`` 291 | 292 | .. class:: RadioSelect 293 | 294 | .. attribute:: RadioSelect.template_name 295 | 296 | ``'floppyforms/radio.html'`` 297 | 298 | .. class:: SelectMultiple 299 | 300 | .. attribute:: SelectMultiple.template_name 301 | 302 | ``'floppyforms/select_multiple.html'`` 303 | 304 | .. class:: CheckboxSelectMultiple 305 | 306 | .. attribute:: CheckboxSelectMultiple.template_name 307 | 308 | ``'floppyforms/checkbox_select.html'`` 309 | 310 | .. class:: MultiWidget 311 | 312 | The same as ``django.forms.widgets.MultiWidget``. The rendering can be 313 | customized by overriding ``format_output``, which joins all the rendered 314 | widgets. 315 | 316 | .. class:: SplitDateTimeWidget 317 | 318 | Displays a ``DateInput`` and a ``TimeInput`` side by side. 319 | 320 | .. class:: MultipleHiddenInput 321 | 322 | A multiple for fields that have several values. 323 | 324 | .. class:: SelectDateWidget 325 | 326 | A widget that displays three ```` box. 332 | * ``month_field``: the name for the month's ```` box. 334 | 335 | .. attribute:: SelectDateWidget.template_name 336 | 337 | The template used to render the widget. Default: 338 | ``'floppyforms/select_date.html'``. 339 | 340 | .. attribute:: SelectDateWidget.none_value 341 | 342 | A tuple representing the value to display when there is no initial 343 | value. Default: ``(0, '---')``. 344 | 345 | .. attribute:: SelectDateWidget.day_field 346 | 347 | The way the day field's name is derived from the widget's name. 348 | Default: ``'%s_day'``. 349 | 350 | .. attribute:: SelectDateWidget.month_field 351 | 352 | The way the month field's name is derived. Default: ``'%s_month'``. 353 | 354 | .. attribute:: SelectDateWidget.year_field 355 | 356 | The way the year field's name is derived. Default: ``'%s_year'``. 357 | -------------------------------------------------------------------------------- /docs/widgets.rst: -------------------------------------------------------------------------------- 1 | Provided widgets 2 | ================ 3 | 4 | .. _widgets: 5 | 6 | Default widgets for form fields 7 | ------------------------------- 8 | 9 | The first column represents the name of a ``django.forms`` field. FloppyForms 10 | aims to implement all the Django fields with the same class name, in the 11 | ``floppyforms`` namespace. 12 | 13 | ======================== =================== ======================== 14 | Fields Widgets Specificities 15 | ======================== =================== ======================== 16 | BooleanField CheckboxInput 17 | CharField TextInput 18 | ComboField TextInput 19 | ChoiceField Select 20 | TypedChoiceField Select 21 | FilePathField Select 22 | ModelChoiceField Select 23 | DateField DateInput 24 | DateTimeField DateTimeInput 25 | DecimalField NumberInput 26 | EmailField EmailInput 27 | FileField ClearableFileInput 28 | FloatField NumberInput 29 | ImageField ClearableFileInput 30 | IntegerField NumberInput 31 | MultipleChoiceField SelectMultiple 32 | TypedMultipleChoiceField SelectMultiple 33 | ModelMultipleChoiceField SelectMultiple 34 | NullBooleanField NullBooleanSelect 35 | TimeField TimeInput 36 | URLField URLInput 37 | SlugField SlugInput 38 | RegexField TextInput 39 | IPAddressField IPAddressInput 40 | GenericIPAddressField TextInput 41 | MultiValueField None (*abstract*) 42 | SplitDateTimeField SplitDateTimeWidget 43 | ======================== =================== ======================== 44 | 45 | .. note:: Textarea 46 | 47 | The ``Textarea`` widget renders a `` 2 | -------------------------------------------------------------------------------- /floppyforms/templates/floppyforms/time.html: -------------------------------------------------------------------------------- 1 | {% extends "floppyforms/input.html" %} 2 | -------------------------------------------------------------------------------- /floppyforms/templates/floppyforms/url.html: -------------------------------------------------------------------------------- 1 | {% extends "floppyforms/input.html" %} 2 | -------------------------------------------------------------------------------- /floppyforms/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-floppyforms/3c019feda452407522b61c71c1df373c7cc6152c/floppyforms/templatetags/__init__.py -------------------------------------------------------------------------------- /floppyforms/templatetags/floppyforms_internals.py: -------------------------------------------------------------------------------- 1 | """ 2 | This template tag library contains tools that are used in 3 | ``floppyforms/templates/*``. We don't want to expose them publicly with 4 | ``{% load floppyforms %}``. 5 | """ 6 | from django import template 7 | 8 | 9 | register = template.Library() 10 | 11 | 12 | @register.filter 13 | def istrue(value): 14 | return value is True 15 | 16 | 17 | @register.filter 18 | def isfalse(value): 19 | return value is False 20 | 21 | 22 | @register.filter 23 | def isnone(value): 24 | return value is None 25 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE=tests.settings 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore=E501 3 | 4 | [wheel] 5 | universal = 1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import codecs 3 | import re 4 | from os import path 5 | from distutils.core import setup 6 | from setuptools import find_packages 7 | 8 | 9 | def read(*parts): 10 | return codecs.open(path.join(path.dirname(__file__), *parts), 11 | encoding='utf-8').read() 12 | 13 | 14 | def find_version(*file_paths): 15 | version_file = read(*file_paths) 16 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 17 | version_file, re.M) 18 | if version_match: 19 | return str(version_match.group(1)) 20 | raise RuntimeError("Unable to find version string.") 21 | 22 | 23 | setup( 24 | name='django-floppyforms', 25 | version=find_version('floppyforms', '__init__.py'), 26 | author='Gregor Müllegger', 27 | author_email='gregor@muellegger.de', 28 | packages=find_packages(exclude=["tests.*", "tests"]), 29 | include_package_data=True, 30 | url='https://github.com/jazzband/django-floppyforms', 31 | license='BSD licence, see LICENSE file', 32 | description='Full control of form rendering in the templates', 33 | long_description='\n\n'.join(( 34 | read('README.rst'), 35 | read('CHANGES.rst'))), 36 | classifiers=[ 37 | 'Development Status :: 5 - Production/Stable', 38 | 'Environment :: Web Environment', 39 | 'Framework :: Django', 40 | 'Framework :: Django :: 1.11', 41 | 'Framework :: Django :: 2.1', 42 | 'Intended Audience :: Developers', 43 | 'License :: OSI Approved :: BSD License', 44 | 'Natural Language :: English', 45 | 'Programming Language :: Python', 46 | 'Programming Language :: Python :: 2', 47 | 'Programming Language :: Python :: 2.7', 48 | 'Programming Language :: Python :: 3', 49 | 'Programming Language :: Python :: 3.6', 50 | ], 51 | zip_safe=False, 52 | ) 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-floppyforms/3c019feda452407522b61c71c1df373c7cc6152c/tests/__init__.py -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | class InvalidVariable(str): 4 | def __bool__(self): 5 | return False 6 | -------------------------------------------------------------------------------- /tests/compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import django 3 | 4 | import unittest 5 | 6 | from django.utils.encoding import force_str 7 | 8 | # TODO this file could probably be entirely removed at this point 9 | -------------------------------------------------------------------------------- /tests/demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-floppyforms/3c019feda452407522b61c71c1df373c7cc6152c/tests/demo/__init__.py -------------------------------------------------------------------------------- /tests/demo/forms.py: -------------------------------------------------------------------------------- 1 | import floppyforms as forms 2 | from floppyforms import gis 3 | 4 | 5 | try: 6 | import django.contrib.gis.forms as gis_forms 7 | except ImportError: 8 | gis_forms = None 9 | 10 | 11 | ALPHA_CHOICES = [ 12 | (c, c.upper()) 13 | for c in 'abcdefhijklmnopqrstuvwxyz' 14 | ] 15 | 16 | NUMERIC_CHOICES = [ 17 | (c, str(c)) 18 | for c in range(10) 19 | ] 20 | 21 | 22 | def mixin(*classes): 23 | return type( 24 | ''.join(cls.__name__ for cls in classes), 25 | tuple(classes), 26 | {}) 27 | 28 | 29 | class BaseGMapWidget(gis.BaseGMapWidget): 30 | # Paste your own Google Maps API key here to test the widgets out. 31 | google_maps_api_key = None 32 | 33 | 34 | class AllFieldsForm(forms.Form): 35 | boolean = forms.BooleanField() 36 | char = forms.CharField(max_length=50) 37 | choices = forms.ChoiceField(choices=ALPHA_CHOICES) 38 | date = forms.DateField() 39 | datetime = forms.DateTimeField() 40 | decimal = forms.DecimalField(decimal_places=2, max_digits=4) 41 | email = forms.EmailField() 42 | file_field = forms.FileField() 43 | file_path = forms.FilePathField(path='uploads/') 44 | float_field = forms.FloatField() 45 | generic_ip_address = forms.GenericIPAddressField() 46 | image = forms.ImageField() 47 | integer = forms.IntegerField() 48 | ip_address = forms.IPAddressField() 49 | multiple_choices = forms.MultipleChoiceField(choices=ALPHA_CHOICES) 50 | null_boolean = forms.NullBooleanField() 51 | regex_field = forms.RegexField(regex='^\w+$', js_regex='^[a-zA-Z]+$') 52 | slug = forms.SlugField() 53 | split_datetime = forms.SplitDateTimeField() 54 | time = forms.TimeField() 55 | typed_choices = forms.TypedChoiceField(choices=NUMERIC_CHOICES, coerce=int) 56 | typed_multiple_choices = forms.TypedMultipleChoiceField(choices=NUMERIC_CHOICES, coerce=int) 57 | url = forms.URLField() 58 | 59 | # GIS fields. 60 | if gis_forms: 61 | osm_point = gis.PointField(widget=mixin(gis.PointWidget, gis.BaseOsmWidget)) 62 | osm_multipoint = gis.MultiPointField(widget=mixin(gis.MultiPointWidget, gis.BaseOsmWidget)) 63 | osm_linestring = gis.LineStringField(widget=mixin(gis.LineStringWidget, gis.BaseOsmWidget)) 64 | osm_multilinestring = gis.MultiLineStringField(widget=mixin(gis.MultiLineStringWidget, gis.BaseOsmWidget)) 65 | osm_polygon = gis.PolygonField(widget=mixin(gis.PolygonWidget, gis.BaseOsmWidget)) 66 | osm_multipolygon = gis.MultiPolygonField(widget=mixin(gis.MultiPolygonWidget, gis.BaseOsmWidget)) 67 | 68 | gmap_point = gis.PointField(widget=mixin(gis.PointWidget, BaseGMapWidget)) 69 | gmap_multipoint = gis.MultiPointField(widget=mixin(gis.MultiPointWidget, BaseGMapWidget)) 70 | gmap_linestring = gis.LineStringField(widget=mixin(gis.LineStringWidget, BaseGMapWidget)) 71 | gmap_multilinestring = gis.MultiLineStringField(widget=mixin(gis.MultiLineStringWidget, BaseGMapWidget)) 72 | gmap_polygon = gis.PolygonField(widget=mixin(gis.PolygonWidget, BaseGMapWidget)) 73 | gmap_multipolygon = gis.MultiPolygonField(widget=mixin(gis.MultiPolygonWidget, BaseGMapWidget)) 74 | -------------------------------------------------------------------------------- /tests/demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | demo_path = os.path.dirname(os.path.abspath(__file__)) 7 | base_path = os.path.dirname(os.path.dirname(demo_path)) 8 | sys.path.insert(0, base_path) 9 | 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.demo.settings") 11 | 12 | from django.core.management import execute_from_command_line 13 | 14 | execute_from_command_line(sys.argv) 15 | -------------------------------------------------------------------------------- /tests/demo/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import warnings 4 | warnings.simplefilter('always') 5 | 6 | 7 | demo_path = os.path.dirname(os.path.abspath(__file__)) 8 | base_path = os.path.dirname(os.path.dirname(demo_path)) 9 | sys.path.insert(0, base_path) 10 | 11 | 12 | DEBUG = True 13 | 14 | USE_I18N = True 15 | USE_L10N = True 16 | 17 | INSTALLED_APPS = [ 18 | 'django.contrib.admin', 19 | 'django.contrib.auth', 20 | 'django.contrib.contenttypes', 21 | 'django.contrib.gis', 22 | 'django.contrib.staticfiles', 23 | 'floppyforms', 24 | ] 25 | 26 | MIDDLEWARE_CLASSES = () 27 | 28 | ROOT_URLCONF = 'tests.demo.urls' 29 | 30 | STATIC_URL = '/static/' 31 | 32 | TEMPLATE_DIRS = ( 33 | os.path.join(demo_path, 'templates'), 34 | ) 35 | 36 | SECRET_KEY = '0' 37 | -------------------------------------------------------------------------------- /tests/demo/templates/demo/index.html: -------------------------------------------------------------------------------- 1 | {% load floppyforms %} 2 | 3 | 4 | {{ form.media }} 5 | 6 | 7 |
    {% csrf_token %} 8 | 9 | {% form form %} 10 |
    11 |

    12 | 13 |

    14 |
    15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | 5 | urlpatterns = [ 6 | path('', views.index), 7 | ] 8 | -------------------------------------------------------------------------------- /tests/demo/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | from .forms import AllFieldsForm 4 | 5 | 6 | def index(request): 7 | if request.method == 'POST': 8 | form = AllFieldsForm(request.POST, request.FILES) 9 | else: 10 | form = AllFieldsForm() 11 | return render(request, 'demo/index.html', { 12 | 'form': form, 13 | }) 14 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.core.validators import validate_comma_separated_integer_list 3 | from django.db import models 4 | 5 | 6 | class Registration(models.Model): 7 | firstname = models.CharField(max_length=50) 8 | lastname = models.CharField(max_length=50) 9 | username = models.CharField(max_length=50) 10 | age = models.IntegerField() 11 | 12 | 13 | class AllFields(models.Model): 14 | boolean = models.BooleanField(default=False) 15 | char = models.CharField(max_length=50) 16 | comma_separated = models.CharField(max_length=50, validators=[validate_comma_separated_integer_list]) 17 | date = models.DateField() 18 | datetime = models.DateTimeField() 19 | decimal = models.DecimalField(decimal_places=2, max_digits=4) 20 | email = models.EmailField() 21 | file_path = models.FilePathField(path="tests") 22 | float_field = models.FloatField() 23 | integer = models.IntegerField() 24 | big_integer = models.BigIntegerField() 25 | if django.VERSION < (1, 9): 26 | ip_address = models.IPAddressField() 27 | generic_ip_address = models.GenericIPAddressField() 28 | null_boolean = models.NullBooleanField() 29 | positive_integer = models.PositiveIntegerField() 30 | positive_small_integer = models.PositiveSmallIntegerField() 31 | slug = models.SlugField() 32 | small_integer = models.SmallIntegerField() 33 | text = models.TextField() 34 | time = models.TimeField() 35 | url = models.URLField() 36 | file_field = models.FileField(upload_to="test/") 37 | image = models.ImageField(upload_to="test/") 38 | fk = models.ForeignKey(Registration, on_delete=models.CASCADE, related_name='all_fk') 39 | m2m = models.ManyToManyField(Registration, related_name='all_m2m') 40 | one = models.OneToOneField(Registration, on_delete=models.CASCADE, related_name='all_one') 41 | choices = models.CharField(max_length=50, choices=(('a', 'a'),)) 42 | 43 | 44 | class ImageFieldModel(models.Model): 45 | image_field = models.ImageField(upload_to='_test_uploads', null=True, 46 | blank=True) 47 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | argparse 2 | coverage 3 | flake8 4 | django-discover-runner 5 | Pillow 6 | pip 7 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | import django 4 | import warnings 5 | 6 | warnings.simplefilter('always') 7 | 8 | DATABASES = { 9 | 'default': { 10 | 'ENGINE': 'django.db.backends.sqlite3', 11 | 'NAME': 'floppyforms.sqlite', 12 | }, 13 | } 14 | 15 | USE_I18N = True 16 | USE_L10N = True 17 | 18 | INSTALLED_APPS = [ 19 | 'django.contrib.gis', 20 | 'floppyforms', 21 | 'tests', 22 | ] 23 | 24 | MIDDLEWARE_CLASSES = () 25 | 26 | STATIC_URL = '/static/' 27 | 28 | SECRET_KEY = '0' 29 | 30 | if django.VERSION < (1, 11): 31 | template_directories = [] 32 | else: 33 | template_directories = [ 34 | os.path.join( 35 | os.path.dirname(django.__file__), "forms/templates/" 36 | ) 37 | ] 38 | TEMPLATES = [ 39 | { 40 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 41 | 'DIRS': template_directories, 42 | 'APP_DIRS': True, 43 | 'OPTIONS': { 44 | 'context_processors': [ 45 | # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this 46 | # list if you haven't customized them: 47 | 'django.template.context_processors.debug', 48 | 'django.template.context_processors.i18n', 49 | 'django.template.context_processors.media', 50 | 'django.template.context_processors.static', 51 | 'django.template.context_processors.tz', 52 | ], 53 | }, 54 | }, 55 | ] 56 | 57 | 58 | import django 59 | if django.VERSION < (1, 6): 60 | TEST_RUNNER = 'discover_runner.DiscoverRunner' 61 | -------------------------------------------------------------------------------- /tests/templates/custom.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/templates/extra_argument.html: -------------------------------------------------------------------------------- 1 | {{ prefix }}{% if extra_argument %} argument: {{ extra_argument }}{% endif %} 2 | -------------------------------------------------------------------------------- /tests/templates/extra_argument_with_config.html: -------------------------------------------------------------------------------- 1 | {% load floppyforms %} 2 | {% formconfig field with extra_argument="defined inline" %} 3 | {{ prefix }}{% if extra_argument %} argument: {{ extra_argument }}{% endif %} 4 | -------------------------------------------------------------------------------- /tests/templates/formconfig_inside_only.html: -------------------------------------------------------------------------------- 1 | {% load floppyforms %} 2 | {% formconfig row with extra_argument="first argument" %} 3 | {% formrow form.firstname using "simple_formrow_tag.html" %} 4 | -------------------------------------------------------------------------------- /tests/templates/media_widget.html: -------------------------------------------------------------------------------- 1 | {% include "floppyforms/input.html" %} 2 | 3 | -------------------------------------------------------------------------------- /tests/templates/simple_form_tag.html: -------------------------------------------------------------------------------- 1 | Forms: {{ forms|length }} 2 | {% for form in forms %} 3 | {{ forloop.counter }}. Form Fields: {% for field in form %}{{ field.name }} {% endfor %} 4 | {% endfor %} 5 | {% if extra_argument %}Extra argument: {{ extra_argument }}{% endif %} 6 | -------------------------------------------------------------------------------- /tests/templates/simple_formfield_tag.html: -------------------------------------------------------------------------------- 1 | Type: {{ type }} 2 | {% if extra_argument %}Extra argument: {{ extra_argument }}{% endif %} 3 | -------------------------------------------------------------------------------- /tests/templates/simple_formrow_tag.html: -------------------------------------------------------------------------------- 1 | Fields: {{ fields|length }} 2 | {% for field in fields %} 3 | {{ forloop.counter }}. Field: {{ field.name }} 4 | {% endfor %} 5 | {% if extra_argument %}Extra argument: {{ extra_argument }}{% endif %} 6 | -------------------------------------------------------------------------------- /tests/templates/simple_formrow_tag_with_config.html: -------------------------------------------------------------------------------- 1 | {% load floppyforms %} 2 | {% formconfig row with extra_argument="defined inline" %} 3 | {% formconfig field with extra_argument="defined inline" %} 4 | Fields: {{ fields|length }} 5 | {% for field in fields %} 6 | {{ forloop.counter }}. Field: {% formfield field using "extra_argument.html" with prefix=field.name %} 7 | {% endfor %} 8 | {% if extra_argument %}Extra argument: {{ extra_argument }}{% endif %} 9 | -------------------------------------------------------------------------------- /tests/test_deprecations.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import django.forms 4 | from django.test import TestCase 5 | 6 | import floppyforms as forms 7 | from .models import Registration 8 | 9 | 10 | class ModelFormDeprecationTests(TestCase): 11 | def test_model_form_is_deprecated(self): 12 | class RegistrationModelForm(forms.ModelForm): 13 | class Meta: 14 | model = Registration 15 | fields = ( 16 | 'firstname', 17 | 'lastname', 18 | 'username', 19 | 'age', 20 | ) 21 | 22 | with warnings.catch_warnings(record=True) as w: 23 | modelform = RegistrationModelForm() 24 | self.assertEqual(len(w), 1) 25 | self.assertTrue(w[0].category is FutureWarning) 26 | 27 | self.assertFalse(isinstance(modelform.base_fields['firstname'], forms.CharField)) 28 | self.assertIsInstance(modelform.base_fields['firstname'], django.forms.CharField) 29 | -------------------------------------------------------------------------------- /tests/test_fields.py: -------------------------------------------------------------------------------- 1 | import django 2 | import decimal 3 | from datetime import datetime 4 | from django.test import TestCase 5 | 6 | import floppyforms.__future__ as forms 7 | 8 | from .compat import unittest 9 | from .models import ImageFieldModel 10 | 11 | 12 | skipIf = unittest.skipIf 13 | 14 | 15 | class ImageFieldModelForm(forms.ModelForm): 16 | class Meta: 17 | model = ImageFieldModel 18 | fields = ('image_field',) 19 | 20 | 21 | class DateTimeFieldTests(TestCase): 22 | def test_parse_datetime(self): 23 | field = forms.DateTimeField() 24 | result = field.clean('2000-01-01') 25 | self.assertEqual(result, datetime(2000, 1, 1)) 26 | 27 | def test_data_is_being_parsed(self): 28 | class SampleForm(forms.Form): 29 | datetime_field = forms.DateTimeField() 30 | 31 | form = SampleForm({'datetime_field': '2099-12-31'}) 32 | form.full_clean() 33 | self.assertTrue(form.is_valid()) 34 | self.assertEqual( 35 | form.cleaned_data['datetime_field'], 36 | datetime(2099, 12, 31)) 37 | 38 | 39 | class FloatFieldTests(TestCase): 40 | def test_parse(self): 41 | float_field = forms.FloatField() 42 | result = float_field.clean('1.5') 43 | self.assertIsInstance(result, float) 44 | self.assertEqual(result, 1.5) 45 | 46 | def test_pass_values(self): 47 | class FloatForm(forms.Form): 48 | no_options = forms.FloatField() 49 | min_value = forms.FloatField(min_value=1.234) 50 | step_attr = forms.FloatField(widget=forms.NumberInput(attrs={ 51 | 'step': '0.01' 52 | })) 53 | 54 | rendered = str(FloatForm()['no_options']) 55 | self.assertHTMLEqual(rendered, """ 56 | 58 | """) 59 | rendered = str(FloatForm()['min_value']) 60 | self.assertHTMLEqual(rendered, """ 61 | 63 | """) 64 | rendered = str(FloatForm()['step_attr']) 65 | self.assertHTMLEqual(rendered, """ 66 | 68 | """) 69 | 70 | 71 | class IntegerFieldTests(TestCase): 72 | def test_parse_int(self): 73 | int_field = forms.IntegerField() 74 | result = int_field.clean('15') 75 | self.assertEqual(15, result) 76 | self.assertIsInstance(result, int) 77 | 78 | def test_pass_values(self): 79 | class IntForm(forms.Form): 80 | num = forms.IntegerField(max_value=10) 81 | other = forms.IntegerField() 82 | third = forms.IntegerField(min_value=10, max_value=150) 83 | 84 | rendered = IntForm().as_p() 85 | self.assertHTMLEqual(rendered, """ 86 |

    87 | 88 | 89 |

    90 |

    91 | 92 | 93 |

    94 |

    95 | 96 | 97 |

    """) 98 | 99 | 100 | class DecimalFieldTests(TestCase): 101 | def test_parse_decimal(self): 102 | decimal_field = forms.DecimalField(decimal_places=2) 103 | result = decimal_field.clean('1.5') 104 | self.assertEqual(decimal.Decimal('1.5'), result) 105 | self.assertIsInstance(result, decimal.Decimal) 106 | 107 | def test_pass_values(self): 108 | class DecimalForm(forms.Form): 109 | num = forms.DecimalField(decimal_places=2, max_value=10.5) 110 | other = forms.DecimalField(decimal_places=1) 111 | third = forms.DecimalField(decimal_places=3, min_value=-10, max_value=15) 112 | 113 | rendered = DecimalForm().as_p() 114 | self.assertHTMLEqual(rendered, """ 115 |

    116 | 117 | 118 |

    119 |

    120 | 121 | 122 |

    123 |

    124 | 125 | 126 |

    """) 127 | 128 | 129 | class ImageFieldTests(TestCase): 130 | @skipIf(django.VERSION < (1, 6), 'Only applies to Django >= 1.6') 131 | def test_model_field_set_to_none(self): 132 | # ``models.ImageField``s return a file object with no associated file. 133 | # These objects raise errors if you try to access the url etc. So we 134 | # test here that this does not raise any errors. 135 | # See: https://github.com/jazzband/django-floppyforms/issues/128 136 | instance = ImageFieldModel.objects.create(image_field=None) 137 | form = ImageFieldModelForm(instance=instance) 138 | rendered = form.as_p() 139 | self.assertHTMLEqual(rendered, """ 140 |

    141 | 142 | 143 |

    """) 144 | 145 | context = form.fields['image_field'].widget.get_context( 146 | name='image_field', 147 | value=instance.image_field, 148 | attrs={}) 149 | self.assertEqual(context['value'], None) 150 | 151 | 152 | class MultipleChoiceFieldTests(TestCase): 153 | def test_as_hidden(self): 154 | some_choices = ( 155 | ('foo', 'bar'), 156 | ('baz', 'meh'), 157 | ('heh', 'what?!'), 158 | ) 159 | 160 | class MultiForm(forms.Form): 161 | multi = forms.MultipleChoiceField(choices=some_choices) 162 | 163 | rendered = MultiForm(data={'multi': ['heh', 'foo']})['multi'].as_hidden() 164 | self.assertHTMLEqual(rendered, """ 165 | 166 | 167 | """) 168 | -------------------------------------------------------------------------------- /tests/test_forms.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | import django 3 | from django.core.exceptions import ValidationError 4 | from django.test import TestCase 5 | 6 | from django.utils import translation 7 | from django.utils.translation import gettext_lazy as _ 8 | 9 | import floppyforms.__future__ as forms 10 | 11 | from .compat import unittest 12 | from .models import Registration 13 | 14 | 15 | expectedFailure = unittest.expectedFailure 16 | skipIf = unittest.skipIf 17 | 18 | 19 | class RegistrationForm(forms.Form): 20 | honeypot = forms.CharField(required=False, widget=forms.HiddenInput) 21 | firstname = forms.CharField(label=_('Your first name?')) 22 | lastname = forms.CharField(label=_('Your last name:')) 23 | username = forms.CharField(max_length=30) 24 | password = forms.CharField( 25 | widget=forms.PasswordInput, 26 | help_text=_('Make sure to use a secure password.'), 27 | ) 28 | password2 = forms.CharField(label=_('Retype password'), widget=forms.PasswordInput) 29 | age = forms.IntegerField(required=False) 30 | height = forms.DecimalField(localize=True, required=False) 31 | agree_to_terms = forms.BooleanField() 32 | 33 | def clean_honeypot(self): 34 | if self.cleaned_data.get('honeypot'): 35 | raise ValidationError('Haha, you trapped into the honeypot.') 36 | return self.cleaned_data['honeypot'] 37 | 38 | def clean(self): 39 | if self.errors: 40 | raise ValidationError('Please correct the errors below.') 41 | 42 | 43 | class RegistrationModelForm(forms.ModelForm): 44 | class Meta: 45 | model = Registration 46 | fields = ( 47 | 'firstname', 48 | 'lastname', 49 | 'username', 50 | 'age', 51 | ) 52 | 53 | 54 | class FormRenderAsMethodsTests(TestCase): 55 | def test_default_rendering(self): 56 | form = RegistrationForm() 57 | with self.assertTemplateUsed('floppyforms/layouts/default.html'): 58 | with self.assertTemplateUsed('floppyforms/layouts/table.html'): 59 | rendered = str(form) 60 | self.assertTrue(' name="firstname"' in rendered) 61 | 62 | form = RegistrationModelForm() 63 | with self.assertTemplateUsed('floppyforms/layouts/default.html'): 64 | with self.assertTemplateUsed('floppyforms/layouts/table.html'): 65 | rendered = str(form) 66 | self.assertTrue(' name="firstname"' in rendered) 67 | 68 | def test_as_p(self): 69 | form = RegistrationForm() 70 | with self.assertTemplateUsed('floppyforms/layouts/p.html'): 71 | rendered = form.as_p() 72 | self.assertTrue(' name="firstname"' in rendered) 73 | 74 | form = RegistrationModelForm() 75 | with self.assertTemplateUsed('floppyforms/layouts/p.html'): 76 | rendered = form.as_p() 77 | self.assertTrue(' name="firstname"' in rendered) 78 | 79 | def test_as_table(self): 80 | form = RegistrationForm() 81 | with self.assertTemplateUsed('floppyforms/layouts/table.html'): 82 | rendered = form.as_table() 83 | self.assertTrue(' name="firstname"' in rendered) 84 | 85 | form = RegistrationModelForm() 86 | with self.assertTemplateUsed('floppyforms/layouts/table.html'): 87 | rendered = form.as_table() 88 | self.assertTrue(' name="firstname"' in rendered) 89 | 90 | def test_as_ul(self): 91 | form = RegistrationForm() 92 | with self.assertTemplateUsed('floppyforms/layouts/ul.html'): 93 | rendered = form.as_ul() 94 | self.assertTrue(' name="firstname"' in rendered) 95 | 96 | form = RegistrationModelForm() 97 | with self.assertTemplateUsed('floppyforms/layouts/ul.html'): 98 | rendered = form.as_ul() 99 | self.assertTrue(' name="firstname"' in rendered) 100 | 101 | 102 | class FormHasChangedTests(TestCase): 103 | def test_basic_has_changed(self): 104 | form = RegistrationForm() 105 | self.assertFalse(form.has_changed()) 106 | 107 | form = RegistrationForm({'height': '1.89'}) 108 | self.assertTrue(form.has_changed()) 109 | 110 | form = RegistrationForm({'height': '1.89'}, 111 | initial={'height': Decimal('1.89')}) 112 | self.assertFalse(form.has_changed()) 113 | 114 | def test_custom_has_changed_logic_for_checkbox_input(self): 115 | form = RegistrationForm({'agree_to_terms': True}) 116 | self.assertTrue(form.has_changed()) 117 | 118 | form = RegistrationForm({'agree_to_terms': False}, 119 | initial={'agree_to_terms': False}) 120 | self.assertFalse(form.has_changed()) 121 | 122 | form = RegistrationForm({'agree_to_terms': False}, 123 | initial={'agree_to_terms': 'False'}) 124 | self.assertFalse(form.has_changed()) 125 | 126 | @skipIf(django.VERSION < (1, 6), 'Only applies to Django >= 1.6') 127 | def test_widgets_do_not_have_has_changed_method(self): 128 | self.assertFalse(hasattr(forms.CheckboxInput, '_has_changed')) 129 | self.assertFalse(hasattr(forms.NullBooleanSelect, '_has_changed')) 130 | self.assertFalse(hasattr(forms.SelectMultiple, '_has_changed')) 131 | self.assertFalse(hasattr(forms.FileInput, '_has_changed')) 132 | self.assertFalse(hasattr(forms.DateInput, '_has_changed')) 133 | self.assertFalse(hasattr(forms.DateTimeInput, '_has_changed')) 134 | self.assertFalse(hasattr(forms.TimeInput, '_has_changed')) 135 | 136 | def test_has_changed_logic_with_localized_values(self): 137 | ''' 138 | See: https://code.djangoproject.com/ticket/16612 139 | ''' 140 | with translation.override('de-de'): 141 | form = RegistrationForm({'height': '1,89'}, 142 | initial={'height': Decimal('1.89')}) 143 | self.assertFalse(form.has_changed()) 144 | 145 | if django.VERSION < (1, 6): 146 | test_has_changed_logic_with_localized_values = expectedFailure( 147 | test_has_changed_logic_with_localized_values) 148 | -------------------------------------------------------------------------------- /tests/test_modelforms.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.db import models 3 | from django.test import TestCase 4 | 5 | import floppyforms.__future__ as forms 6 | from floppyforms.__future__.models import modelform_factory, modelformset_factory, inlineformset_factory 7 | 8 | from .compat import unittest 9 | from .models import Registration, AllFields 10 | 11 | 12 | skipIf = unittest.skipIf 13 | 14 | 15 | class SomeModel2(models.Model): 16 | some_field = models.CharField(max_length=255) 17 | 18 | def __str__(self): 19 | return '%s' % self.some_field 20 | 21 | 22 | class BaseModelFormFieldRewritingTests(object): 23 | ''' 24 | A base class to mixin generic tests to check if the form fields on a 25 | modelform where set correctly to their floppyformic brother. 26 | 27 | A subclass must implement the ``get_test_object``, ``check_field`` and ``check_widget`` methods. 28 | ''' 29 | 30 | def test_auto_boolean(self): 31 | form_obj = self.get_test_object('boolean') 32 | self.check_field(form_obj, 'boolean', forms.BooleanField) 33 | 34 | def test_auto_char(self): 35 | form_obj = self.get_test_object('char') 36 | self.check_field(form_obj, 'char', forms.CharField) 37 | 38 | def test_auto_comma_separated(self): 39 | form_obj = self.get_test_object('comma_separated') 40 | self.check_field(form_obj, 'comma_separated', forms.CharField) 41 | 42 | def test_auto_date(self): 43 | form_obj = self.get_test_object('date') 44 | self.check_field(form_obj, 'date', forms.DateField) 45 | 46 | def test_auto_datetime(self): 47 | form_obj = self.get_test_object('datetime') 48 | self.check_field(form_obj, 'datetime', forms.DateTimeField) 49 | 50 | def test_auto_decimal(self): 51 | form_obj = self.get_test_object('decimal') 52 | self.check_field(form_obj, 'decimal', forms.DecimalField) 53 | 54 | def test_auto_email(self): 55 | form_obj = self.get_test_object('email') 56 | self.check_field(form_obj, 'email', forms.EmailField) 57 | 58 | def test_auto_file_path(self): 59 | form_obj = self.get_test_object('file_path') 60 | self.check_field(form_obj, 'file_path', forms.FilePathField) 61 | 62 | def test_auto_float_field(self): 63 | form_obj = self.get_test_object('float_field') 64 | self.check_field(form_obj, 'float_field', forms.FloatField) 65 | 66 | def test_auto_integer(self): 67 | form_obj = self.get_test_object('integer') 68 | self.check_field(form_obj, 'integer', forms.IntegerField) 69 | 70 | def test_auto_big_integer(self): 71 | form_obj = self.get_test_object('big_integer') 72 | self.check_field(form_obj, 'big_integer', forms.IntegerField) 73 | 74 | @skipIf(django.VERSION >= (1, 8), 'IPAddressField is deprecated with Django >= 1.8') 75 | def test_auto_ip_address(self): 76 | form_obj = self.get_test_object('ip_address') 77 | self.check_field(form_obj, 'ip_address', forms.IPAddressField) 78 | 79 | def test_auto_generic_ip_address(self): 80 | form_obj = self.get_test_object('generic_ip_address') 81 | self.check_field(form_obj, 'generic_ip_address', forms.GenericIPAddressField) 82 | 83 | def test_auto_null_boolean(self): 84 | form_obj = self.get_test_object('null_boolean') 85 | self.check_field(form_obj, 'null_boolean', forms.NullBooleanField) 86 | 87 | def test_auto_positive_integer(self): 88 | form_obj = self.get_test_object('positive_integer') 89 | self.check_field(form_obj, 'positive_integer', forms.IntegerField) 90 | 91 | def test_auto_positive_small_integer(self): 92 | form_obj = self.get_test_object('positive_small_integer') 93 | self.check_field(form_obj, 'positive_small_integer', forms.IntegerField) 94 | 95 | def test_auto_slug(self): 96 | form_obj = self.get_test_object('slug') 97 | self.check_field(form_obj, 'slug', forms.SlugField) 98 | 99 | def test_auto_small_integer(self): 100 | form_obj = self.get_test_object('small_integer') 101 | self.check_field(form_obj, 'small_integer', forms.IntegerField) 102 | 103 | def test_auto_text(self): 104 | form_obj = self.get_test_object('text') 105 | self.check_field(form_obj, 'text', forms.CharField) 106 | self.check_widget(form_obj, 'text', forms.Textarea) 107 | 108 | def test_auto_time(self): 109 | form_obj = self.get_test_object('time') 110 | self.check_field(form_obj, 'time', forms.TimeField) 111 | 112 | def test_auto_url(self): 113 | form_obj = self.get_test_object('url') 114 | self.check_field(form_obj, 'url', forms.URLField) 115 | 116 | def test_auto_file_field(self): 117 | form_obj = self.get_test_object('file_field') 118 | self.check_field(form_obj, 'file_field', forms.FileField) 119 | 120 | def test_auto_image(self): 121 | form_obj = self.get_test_object('image') 122 | self.check_field(form_obj, 'image', forms.ImageField) 123 | 124 | def test_auto_fk(self): 125 | form_obj = self.get_test_object('fk') 126 | self.check_field(form_obj, 'fk', forms.ModelChoiceField) 127 | 128 | def test_auto_m2m(self): 129 | form_obj = self.get_test_object('m2m') 130 | self.check_field(form_obj, 'm2m', forms.ModelMultipleChoiceField) 131 | 132 | def test_auto_one(self): 133 | form_obj = self.get_test_object('one') 134 | self.check_field(form_obj, 'one', forms.ModelChoiceField) 135 | 136 | def test_auto_choices(self): 137 | form_obj = self.get_test_object('choices') 138 | self.check_field(form_obj, 'choices', forms.TypedChoiceField) 139 | 140 | 141 | @skipIf(django.VERSION < (1, 6), 'Only applies to Django >= 1.6') 142 | class ModelFormTests(BaseModelFormFieldRewritingTests, TestCase): 143 | def get_test_object(self, field_name): 144 | class Form(forms.ModelForm): 145 | class Meta: 146 | model = AllFields 147 | fields = (field_name,) 148 | return Form 149 | 150 | def check_field(self, Form, field_name, field_class): 151 | self.assertIsInstance(Form.base_fields[field_name], field_class) 152 | 153 | def check_widget(self, Form, field_name, widget_class): 154 | self.assertIsInstance(Form.base_fields[field_name].widget, widget_class) 155 | 156 | 157 | @skipIf(django.VERSION < (1, 6), 'Only applies to Django >= 1.6') 158 | class ModelFormFactoryTests(BaseModelFormFieldRewritingTests, TestCase): 159 | def get_test_object(self, field_name): 160 | return modelform_factory(AllFields, form=forms.ModelForm, 161 | fields=(field_name,)) 162 | 163 | def check_field(self, Form, field_name, field_class): 164 | self.assertIsInstance(Form.base_fields[field_name], field_class) 165 | 166 | def check_widget(self, Form, field_name, widget_class): 167 | self.assertIsInstance(Form.base_fields[field_name].widget, widget_class) 168 | 169 | 170 | @skipIf(django.VERSION < (1, 6), 'Only applies to Django >= 1.6') 171 | class ModelFormSetFactoryTests(BaseModelFormFieldRewritingTests, TestCase): 172 | def get_test_object(self, field_name): 173 | return modelformset_factory( 174 | AllFields, 175 | form=forms.ModelForm, 176 | fields=(field_name,)) 177 | 178 | def check_field(self, Formset, field_name, field_class): 179 | self.assertIsInstance(Formset.form.base_fields[field_name], field_class) 180 | 181 | def check_widget(self, Formset, field_name, widget_class): 182 | self.assertIsInstance(Formset.form.base_fields[field_name].widget, widget_class) 183 | 184 | 185 | @skipIf(django.VERSION < (1, 6), 'Only applies to Django >= 1.6') 186 | class InlineFormSetFactoryTests(BaseModelFormFieldRewritingTests, TestCase): 187 | def get_test_object(self, field_name): 188 | return inlineformset_factory( 189 | Registration, 190 | AllFields, 191 | fk_name='fk', 192 | form=forms.ModelForm, 193 | fields=(field_name,)) 194 | 195 | def check_field(self, Formset, field_name, field_class): 196 | self.assertIsInstance(Formset.form.base_fields[field_name], field_class) 197 | 198 | def check_widget(self, Formset, field_name, widget_class): 199 | self.assertIsInstance(Formset.form.base_fields[field_name].widget, widget_class) 200 | 201 | 202 | class ModelMultipleChoiceFieldTests(TestCase): 203 | def test_model_choice_field(self): 204 | """ModelChoiceField and ModelMultipleChoiceField""" 205 | SomeModel2.objects.create(some_field='Meh') 206 | SomeModel2.objects.create(some_field='Bah') 207 | 208 | class MultiModelForm(forms.Form): 209 | mods = forms.ModelMultipleChoiceField(queryset=SomeModel2.objects.all()) 210 | 211 | rendered = MultiModelForm(data={'mods': [1, 2]})['mods'].as_hidden() 212 | self.assertHTMLEqual(rendered, """ 213 | 214 | 215 | """) 216 | -------------------------------------------------------------------------------- /tests/test_rendering.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | import floppyforms as forms 4 | 5 | from floppyforms import widgets 6 | from floppyforms.templatetags.floppyforms import ConfigFilter, FormConfig 7 | 8 | 9 | class AgeField(forms.IntegerField): 10 | pass 11 | 12 | 13 | class RegistrationForm(forms.Form): 14 | name = forms.CharField(label='First- and Lastname', max_length=50) 15 | email = forms.EmailField(max_length=50, 16 | help_text='Please enter a valid email.') 17 | age = AgeField() 18 | short_biography = forms.CharField(max_length=200) 19 | comment = forms.CharField(widget=widgets.Textarea) 20 | 21 | 22 | class FormConfigTests(TestCase): 23 | def test_default_retrieve(self): 24 | """ 25 | Test if FormConfig returns the correct default values if no 26 | configuration was made. 27 | """ 28 | form = RegistrationForm() 29 | config = FormConfig() 30 | 31 | # retrieve widget 32 | 33 | widget = config.retrieve('widget', bound_field=form['name']) 34 | self.assertTrue(isinstance(widget, widgets.TextInput)) 35 | self.assertEqual(widget, form.fields['name'].widget) 36 | 37 | widget = config.retrieve('widget', bound_field=form['comment']) 38 | self.assertTrue(isinstance(widget, widgets.Textarea)) 39 | self.assertEqual(widget, form.fields['comment'].widget) 40 | 41 | # retrieve widget template 42 | 43 | template_name = config.retrieve('widget_template', bound_field=form['name']) 44 | self.assertEqual(template_name, 'floppyforms/text.html') 45 | 46 | template_name = config.retrieve('widget_template', bound_field=form['comment']) 47 | self.assertEqual(template_name, 'floppyforms/textarea.html') 48 | 49 | # retrieve label 50 | 51 | label = config.retrieve('label', bound_field=form['email']) 52 | self.assertEqual(label, 'Email') 53 | 54 | label = config.retrieve('label', bound_field=form['name']) 55 | self.assertEqual(label, 'First- and Lastname') 56 | 57 | # retrieve help text 58 | 59 | help_text = config.retrieve('help_text', bound_field=form['name']) 60 | self.assertFalse(help_text) 61 | 62 | help_text = config.retrieve('help_text', bound_field=form['email']) 63 | self.assertEqual(help_text, 'Please enter a valid email.') 64 | 65 | # retrieve row template 66 | 67 | template = config.retrieve('row_template', fields=(form['name'], form['email'],)) 68 | self.assertEqual(template, 'floppyforms/rows/default.html') 69 | 70 | # retrieve form layout 71 | 72 | template = config.retrieve('layout', forms=(form,)) 73 | self.assertEqual(template, 'floppyforms/layouts/default.html') 74 | 75 | def test_configure_and_retrieve(self): 76 | form = RegistrationForm() 77 | 78 | config = FormConfig() 79 | widget = config.retrieve('widget', bound_field=form['comment']) 80 | self.assertEqual(widget.__class__, widgets.Textarea) 81 | 82 | config.configure('widget', widgets.TextInput(), filter=ConfigFilter('comment')) 83 | 84 | widget = config.retrieve('widget', bound_field=form['comment']) 85 | self.assertEqual(widget.__class__, widgets.TextInput) 86 | 87 | widget = config.retrieve('widget', bound_field=form['name']) 88 | self.assertEqual(widget.__class__, widgets.TextInput) 89 | 90 | def test_retrieve_for_multiple_valid_values(self): 91 | form = RegistrationForm() 92 | config = FormConfig() 93 | 94 | config.configure( 95 | 'widget', widgets.Textarea(), 96 | filter=ConfigFilter('CharField'), 97 | ) 98 | config.configure( 99 | 'widget', widgets.HiddenInput(), 100 | filter=ConfigFilter('short_biography'), 101 | ) 102 | 103 | widget = config.retrieve('widget', bound_field=form['name']) 104 | self.assertEqual(widget.__class__, widgets.Textarea) 105 | widget = config.retrieve('widget', bound_field=form['comment']) 106 | self.assertEqual(widget.__class__, widgets.Textarea) 107 | 108 | # we get HiddenInput since this was configured last, even the Textarea 109 | # config applies to ``short_biography`` 110 | widget = config.retrieve('widget', bound_field=form['short_biography']) 111 | self.assertEqual(widget.__class__, widgets.HiddenInput) 112 | 113 | def test_filter_for_field_class_name(self): 114 | form = RegistrationForm() 115 | 116 | config = FormConfig() 117 | config.configure('widget', widgets.TextInput(), filter=ConfigFilter('CharField')) 118 | 119 | widget = config.retrieve('widget', bound_field=form['comment']) 120 | self.assertEqual(widget.__class__, widgets.TextInput) 121 | 122 | widget = config.retrieve('widget', bound_field=form['name']) 123 | self.assertEqual(widget.__class__, widgets.TextInput) 124 | 125 | def test_filter_for_widget_class_name(self): 126 | form = RegistrationForm() 127 | 128 | config = FormConfig() 129 | config.configure('widget', widgets.TextInput(), filter=ConfigFilter('Textarea')) 130 | 131 | widget = config.retrieve('widget', bound_field=form['comment']) 132 | self.assertEqual(widget.__class__, widgets.TextInput) 133 | 134 | widget = config.retrieve('widget', bound_field=form['name']) 135 | self.assertEqual(widget.__class__, widgets.TextInput) 136 | 137 | # swap widgets TextInput <> Textarea 138 | 139 | config = FormConfig() 140 | config.configure('widget', widgets.Textarea(), filter=ConfigFilter('TextInput')) 141 | config.configure('widget', widgets.TextInput(), filter=ConfigFilter('Textarea')) 142 | 143 | widget = config.retrieve('widget', bound_field=form['comment']) 144 | self.assertEqual(widget.__class__, widgets.TextInput) 145 | 146 | widget = config.retrieve('widget', bound_field=form['name']) 147 | self.assertEqual(widget.__class__, widgets.Textarea) 148 | 149 | def test_filter_for_name_object(self): 150 | form = RegistrationForm() 151 | 152 | config = FormConfig() 153 | config.configure('widget', widgets.Textarea(), filter=ConfigFilter('object')) 154 | 155 | widget = config.retrieve('widget', bound_field=form['email']) 156 | self.assertEqual(widget.__class__, widgets.EmailInput) 157 | 158 | widget = config.retrieve('widget', bound_field=form['name']) 159 | self.assertEqual(widget.__class__, widgets.TextInput) 160 | 161 | widget = config.retrieve('widget', bound_field=form['comment']) 162 | self.assertEqual(widget.__class__, widgets.Textarea) 163 | 164 | def test_stacked_config(self): 165 | form = RegistrationForm() 166 | config = FormConfig() 167 | 168 | config.push() 169 | config.configure( 170 | 'widget', widgets.Textarea(), 171 | filter=ConfigFilter("CharField"), 172 | ) 173 | 174 | config.push() 175 | config.configure( 176 | 'widget', widgets.HiddenInput(), 177 | filter=ConfigFilter('short_biography'), 178 | ) 179 | 180 | widget = config.retrieve('widget', bound_field=form['short_biography']) 181 | self.assertEqual(widget.__class__, widgets.HiddenInput) 182 | 183 | config.pop() 184 | widget = config.retrieve('widget', bound_field=form['short_biography']) 185 | self.assertEqual(widget.__class__, widgets.Textarea) 186 | 187 | config.pop() 188 | widget = config.retrieve('widget', bound_field=form['short_biography']) 189 | self.assertEqual(widget.__class__, widgets.TextInput) 190 | 191 | def test_field_filter_works_on_subclasses(self): 192 | form = RegistrationForm() 193 | config = FormConfig() 194 | 195 | config.configure( 196 | 'widget', widgets.HiddenInput(), 197 | filter=ConfigFilter("IntegerField"), 198 | ) 199 | 200 | widget = config.retrieve('widget', bound_field=form['age']) 201 | self.assertEqual(widget.__class__, widgets.HiddenInput) 202 | 203 | def test_retrieve_all(self): 204 | config = FormConfig() 205 | 206 | config.configure('number', 1) 207 | config.configure('number', 2) 208 | self.assertEqual(list(config.retrieve_all('number')), [2, 1]) 209 | 210 | config.configure('number', 4, filter=lambda nr=None, **kwargs: nr == 'four') 211 | self.assertEqual(list(config.retrieve_all('number')), [2, 1]) 212 | self.assertEqual(list(config.retrieve_all('number', nr='four')), [4, 2, 1]) 213 | 214 | config.push() 215 | config.configure('number', 5, filter=lambda nr=None, **kwargs: nr == 'five') 216 | self.assertEqual(list(config.retrieve_all('number')), [2, 1]) 217 | self.assertEqual(list(config.retrieve_all('number', nr='five')), [5, 2, 1]) 218 | 219 | config.configure('number', -1) 220 | self.assertEqual(list(config.retrieve_all('number')), [-1, 2, 1]) 221 | self.assertEqual(list(config.retrieve_all('number', nr='four')), [-1, 4, 2, 1]) 222 | self.assertEqual(list(config.retrieve_all('number', nr='five')), [-1, 5, 2, 1]) 223 | 224 | config.pop() 225 | self.assertEqual(list(config.retrieve_all('number')), [2, 1]) 226 | self.assertEqual(list(config.retrieve_all('number', nr='four')), [4, 2, 1]) 227 | self.assertEqual(list(config.retrieve_all('number', nr='five')), [2, 1]) 228 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .test_deprecations import * 3 | from .test_forms import * 4 | from .test_gis import GisTests 5 | from .test_modelforms import * 6 | from .test_layouts import * 7 | from .test_rendering import * 8 | from .test_templatetags import * 9 | from .test_widgets import * 10 | from .test_fields import * 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 1.8 3 | envlist = 4 | docs 5 | checks 6 | py36-{22, 30} 7 | py37-{22, 30} 8 | py38-{22, 30} 9 | py39-{22, 30} 10 | 11 | [gh-actions] 12 | python = 13 | 3.6: py36 14 | 3.7: py37, docs, checks 15 | 3.8: py38 16 | 3.9: py39 17 | 18 | [testenv] 19 | deps = 20 | 22: Django >= 2.2, < 3.0 21 | 30: Django >= 3.0, < 4.0 22 | -r{toxinidir}/tests/requirements.txt 23 | commands = 24 | coverage run --source=floppyforms --branch {envbindir}/django-admin test --pythonpath=./ --settings=tests.settings 25 | coverage report -m 26 | coverage xml 27 | 28 | [testenv:docs] 29 | changedir = docs 30 | deps = 31 | Sphinx 32 | commands = 33 | sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 34 | 35 | [testenv:checks] 36 | deps = 37 | flake8 38 | readme_renderer 39 | commands = 40 | flake8 floppyforms 41 | python setup.py check -r -s 42 | --------------------------------------------------------------------------------