├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── Makefile ├── changelog.rst ├── conf.py ├── extractor.rst ├── filters.rst ├── formulation.rst ├── index.rst ├── make.bat └── tags.rst ├── pyproject.toml ├── src └── sniplates │ ├── __init__.py │ ├── templates │ └── sniplates │ │ └── django.html │ └── templatetags │ ├── __init__.py │ └── sniplates.py └── tests ├── __init__.py ├── forms.py ├── models.py ├── report_settings.py ├── settings.py ├── templates ├── field_tag │ ├── choices │ ├── choices_multi │ ├── choices_multi2 │ ├── empty_field │ ├── field │ ├── override │ ├── override2 │ ├── widget2 │ ├── widgets │ └── widgets_django ├── filters │ └── flatattrs ├── inheritance │ ├── base │ ├── block_overlap │ ├── block_overlap_widget │ ├── parent_inherit │ ├── parent_inherit_base │ ├── parent_inherit_widgets │ ├── parent_overlap │ ├── parent_overlap_widgets │ ├── super │ ├── super_widget_base │ └── super_widget_inherit ├── invalid │ ├── bad_name │ ├── no_lib │ ├── no_widget │ ├── not_loaded │ └── simple.html ├── load_widgets │ ├── load_widgets │ ├── load_widgets_three │ ├── load_widgets_two │ ├── other.html │ └── simple.html ├── nested_tag │ ├── asvar │ ├── empty │ ├── invalid │ ├── invalid2 │ ├── keep_widgets │ ├── simple │ └── widgets ├── reuse │ ├── base │ ├── inwidget │ ├── reuse │ └── simple └── widget_tag │ ├── alias_self │ ├── asvar │ ├── fixed │ ├── inherit │ ├── var │ ├── widgets.1 │ ├── widgets.2 │ └── widgets.3 ├── test_core.py ├── test_filters.py ├── test_forms.py ├── test_inherited.py ├── test_nested.py ├── test_reuse.py ├── test_widgets_django.py └── utils.py /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | 6 | ci: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | max-parallel: 4 11 | matrix: 12 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 13 | django-version: ["3.0", "3.1", "3.2", "4.0", "4.1", "4.2"] 14 | exclude: 15 | - python-version: 3.11 16 | django-version: 3.0 17 | - python-version: 3.11 18 | django-version: 3.1 19 | - python-version: 3.11 20 | django-version: 3.2 21 | - python-version: 3.11 22 | django-version: 4.0 23 | - python-version: 3.7 24 | django-version: 4.0 25 | - python-version: 3.7 26 | django-version: 4.1 27 | - python-version: 3.7 28 | django-version: 4.2 29 | 30 | services: 31 | redis: 32 | image: redis 33 | ports: 34 | # Don't ask me why, but we need to map this to a different port 35 | # Thanks, Dan Sloan, for the tip. 36 | - 16379:6379 37 | 38 | steps: 39 | - uses: actions/checkout@v3 40 | - name: Set up Python ${{ matrix.python-version }} 41 | uses: actions/setup-python@v3 42 | with: 43 | python-version: ${{ matrix.python-version }} 44 | - name: Install 45 | run: | 46 | pip install -U pip 47 | pip install -e .[test] Django~=${{ matrix.django-version }} 48 | - name: Lint 49 | run: pylint src/ 50 | - name: isort 51 | run: isort --check src/ 52 | - name: Test 53 | run: pytest 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | .eggs/ 25 | 26 | # Installer logs 27 | pip-log.txt 28 | pip-delete-this-directory.txt 29 | 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .tox/ 33 | .coverage 34 | .cache 35 | nosetests.xml 36 | coverage.xml 37 | .coveralls.yml 38 | 39 | # Translations 40 | *.mo 41 | 42 | # Mr Developer 43 | .mr.developer.cfg 44 | .project 45 | .pydevproject 46 | 47 | # Rope 48 | .ropeproject 49 | 50 | # Django stuff: 51 | *.log 52 | *.pot 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Curtis Maloney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | django-sniplates 2 | ================ 3 | 4 | Template snippet libraries for Django 5 | 6 | Read the documentation at [Read The Docs](https://sniplates.readthedocs.io/en/latest/) 7 | -------------------------------------------------------------------------------- /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 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoSniplates.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoSniplates.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoSniplates" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoSniplates" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | 0.7.1 (2020-01-22) 6 | ------------------ 7 | 8 | .. note: Support for Python 2 has been completely dropped. 9 | 10 | Features: 11 | 12 | - Proper use of force_str now that old APIs have been dropped from Django. 13 | 14 | 0.6.0 (2018-08-08) 15 | ------------------ 16 | 17 | .. note: Support for Django < 2.0 has been officially dropped. 18 | 19 | Features: 20 | 21 | - Now also extracts `disabled` from form fields. 22 | 23 | Bugs fixed: 24 | 25 | - Password fields no longer render their value 26 | - Labels now have the correct ID value in their for attribute. 27 | - Date inputs format correctly. 28 | 29 | 0.5.0 (2016-08-16) 30 | ------------------ 31 | 32 | Features: 33 | 34 | - Add coverage to runtests.py 35 | - Added `initial` property to FieldExtractor 36 | - Added Extractors for date, datetime and time fields [Sergei Maertens] 37 | - Added Django 1.10 support 38 | 39 | Bugs Fixed: 40 | 41 | - Rewrote ChoiceWrapper [kezabelle with help from Sergei Maertens] 42 | - Raise TemplateSyntaxError when falsey value passed to {% form_field %} 43 | [kezzabelle] 44 | 45 | Backwards incompatible changes: 46 | 47 | - Dropped Django 1.7 support 48 | 49 | 0.4.1 (2016-01-25) 50 | ------------------ 51 | 52 | Features: 53 | 54 | - Fixed default django.html template to use raw_value [Sergei Maertens] 55 | - Added NullBooleanFieldExtractor 56 | 57 | Bugs Fixed; 58 | 59 | - Made choices lazy again - regression in 0.4 merge [Sergei Maertens] 60 | 61 | Testing: 62 | 63 | - Switched to having travis use tox [Sergei Maertens] 64 | 65 | 0.4.0 (2016-01-24) 66 | ------------------ 67 | 68 | .. note:: This app is no longer compatible with Django versions older than 1.6 69 | 70 | .. note:: The values in the `file` dict previously added by `form_field` are 71 | now exploded directly into the context. 72 | 73 | Features: 74 | 75 | - Added pluggable FieldExploder classes 76 | - Added ``raw_value`` to Form Field exploded attributes 77 | - ``choices`` and ``display`` are now lazy 78 | 79 | Bugs Fixed: 80 | 81 | Backwards incompatible: 82 | 83 | - Your form widgets may need updates related to ``value`` and ``raw_value``. 84 | ``value`` is no longer cleaned for a proper string-version. Consult 85 | ``sniplates/django.html`` for a guideline. 86 | 87 | 0.3.2 88 | ----- 89 | 90 | Features: 91 | 92 | - Explode details from FileField [Thanks mattmcc] 93 | 94 | Bugs Fixed: 95 | 96 | - Fixed use of 'flatattrs' in templates 97 | - Added ClearableFileInput to default template [Thanks mattmcc] 98 | - Corrected packaging to include templates [Thanks kezabelle] 99 | 100 | 0.3.1 101 | ----- 102 | 103 | Bugs Fixed: 104 | 105 | - Only set 'display' when value is a scalar. 106 | - BoundField.value is a callable 107 | - Always normalise value, not just when we have choices 108 | - Mark flatatt safe for Django 1.4 109 | 110 | 0.3.0 111 | ----- 112 | 113 | Features: 114 | 115 | - Reworked to no longer copy() the context - ever! 116 | - Now sets the right block context when rendering widgets so {{ block.super }} works. Thanks Schinckel! 117 | - Added default Django widget template. 118 | - Added Django 1.8 compatibility. 119 | - Added ``_soft`` option to {% load_widgets %} to avoid reloading an alias. 120 | - Added "as foo" support to {% widget %} and {% nested_widget %}. Thanks Schinckel! 121 | - A blank alias to {% widget %} and {% nested_widget %} will use the current template context. 122 | - {% reuse %} will now accept a list of block names to search for. 123 | 124 | Bugs Fixed: 125 | 126 | - Don't lose widget context when inside a widget. Thanks Schinckel! 127 | 128 | Testing: 129 | 130 | - Test on pypy3 131 | - Removed testing for Django 1.5 and 1.6. 132 | - Fixed test discovery on Django 1.4. Thanks Schinckel! 133 | 134 | 0.2.2 135 | ----- 136 | 137 | Bugs fixed: 138 | 139 | - Fix forcing multi-value fields to unicode in form tag 140 | 141 | 0.2.1 142 | ----- 143 | 144 | Features: 145 | 146 | - Added `reuse` tag. 147 | - Added 'widget_type' and 'field_type' to exploded data in form_field 148 | - Added 'display' to exploded data in form_field 149 | 150 | 0.2.0 151 | ----- 152 | 153 | .. note:: This release now encompases equivalent functionality to 154 | ``formulation``. 155 | 156 | Features: 157 | 158 | - Added `nested_widget` tag to allow widgets to contain template content. 159 | - Added `form_field` tag to ease rendering form fields 160 | - Added `flatarr` filter to help with rendering form fields. 161 | 162 | Bugs fixed: 163 | 164 | - Fix overlap problem when loading more than one widget lib in a single 165 | `load_widgets` tag. 166 | 167 | 0.1.1 168 | ----- 169 | 170 | Bugs fixed: 171 | 172 | - Fix overlap problem where a widget libs blocks would override those of the 173 | loading template. 174 | 175 | 0.1.0 176 | ----- 177 | 178 | Initial release 179 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django Sniplates documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Sep 6 10:23:25 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # The suffix of source filenames. 37 | source_suffix = '.rst' 38 | 39 | # The encoding of source files. 40 | #source_encoding = 'utf-8-sig' 41 | 42 | # The master toctree document. 43 | master_doc = 'index' 44 | 45 | # General information about the project. 46 | project = u'Django Sniplates' 47 | copyright = u'2014, Curtis Maloney' 48 | 49 | # The version info for the project you're documenting, acts as replacement for 50 | # |version| and |release|, also used in various other places throughout the 51 | # built documents. 52 | # 53 | # The short X.Y version. 54 | version = '0.3' 55 | # The full version, including alpha/beta/rc tags. 56 | release = '0.3.2' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | #language = None 61 | 62 | # There are two options for replacing |today|: either, you set today to some 63 | # non-false value, then it is used: 64 | #today = '' 65 | # Else, today_fmt is used as the format for a strftime call. 66 | #today_fmt = '%B %d, %Y' 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | exclude_patterns = ['_build'] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all 73 | # documents. 74 | #default_role = None 75 | 76 | # If true, '()' will be appended to :func: etc. cross-reference text. 77 | #add_function_parentheses = True 78 | 79 | # If true, the current module name will be prepended to all description 80 | # unit titles (such as .. function::). 81 | #add_module_names = True 82 | 83 | # If true, sectionauthor and moduleauthor directives will be shown in the 84 | # output. They are ignored by default. 85 | #show_authors = False 86 | 87 | # The name of the Pygments (syntax highlighting) style to use. 88 | pygments_style = 'sphinx' 89 | 90 | # A list of ignored prefixes for module index sorting. 91 | #modindex_common_prefix = [] 92 | 93 | # If true, keep warnings as "system message" paragraphs in the built documents. 94 | #keep_warnings = False 95 | 96 | 97 | # -- Options for HTML output ---------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. See the documentation for 100 | # a list of builtin themes. 101 | html_theme = 'alabaster' 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | #html_theme_options = {} 107 | 108 | # Add any paths that contain custom themes here, relative to this directory. 109 | #html_theme_path = [] 110 | 111 | # The name for this set of Sphinx documents. If None, it defaults to 112 | # " v documentation". 113 | #html_title = None 114 | 115 | # A shorter title for the navigation bar. Default is the same as html_title. 116 | #html_short_title = None 117 | 118 | # The name of an image file (relative to this directory) to place at the top 119 | # of the sidebar. 120 | #html_logo = None 121 | 122 | # The name of an image file (within the static path) to use as favicon of the 123 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 124 | # pixels large. 125 | #html_favicon = None 126 | 127 | # Add any paths that contain custom static files (such as style sheets) here, 128 | # relative to this directory. They are copied after the builtin static files, 129 | # so a file named "default.css" will overwrite the builtin "default.css". 130 | html_static_path = ['_static'] 131 | 132 | # Add any extra paths that contain custom files (such as robots.txt or 133 | # .htaccess) here, relative to this directory. These files are copied 134 | # directly to the root of the documentation. 135 | #html_extra_path = [] 136 | 137 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 138 | # using the given strftime format. 139 | #html_last_updated_fmt = '%b %d, %Y' 140 | 141 | # If true, SmartyPants will be used to convert quotes and dashes to 142 | # typographically correct entities. 143 | #html_use_smartypants = True 144 | 145 | # Custom sidebar templates, maps document names to template names. 146 | #html_sidebars = {} 147 | 148 | # Additional templates that should be rendered to pages, maps page names to 149 | # template names. 150 | #html_additional_pages = {} 151 | 152 | # If false, no module index is generated. 153 | #html_domain_indices = True 154 | 155 | # If false, no index is generated. 156 | #html_use_index = True 157 | 158 | # If true, the index is split into individual pages for each letter. 159 | #html_split_index = False 160 | 161 | # If true, links to the reST sources are added to the pages. 162 | #html_show_sourcelink = True 163 | 164 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 165 | #html_show_sphinx = True 166 | 167 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 168 | #html_show_copyright = True 169 | 170 | # If true, an OpenSearch description file will be output, and all pages will 171 | # contain a tag referring to it. The value of this option must be the 172 | # base URL from which the finished HTML is served. 173 | #html_use_opensearch = '' 174 | 175 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 176 | #html_file_suffix = None 177 | 178 | # Output file base name for HTML help builder. 179 | htmlhelp_basename = 'DjangoSniplatesdoc' 180 | 181 | 182 | # -- Options for LaTeX output --------------------------------------------- 183 | 184 | latex_elements = { 185 | # The paper size ('letterpaper' or 'a4paper'). 186 | #'papersize': 'letterpaper', 187 | 188 | # The font size ('10pt', '11pt' or '12pt'). 189 | #'pointsize': '10pt', 190 | 191 | # Additional stuff for the LaTeX preamble. 192 | #'preamble': '', 193 | } 194 | 195 | # Grouping the document tree into LaTeX files. List of tuples 196 | # (source start file, target name, title, 197 | # author, documentclass [howto, manual, or own class]). 198 | latex_documents = [ 199 | ('index', 'DjangoSniplates.tex', u'Django Sniplates Documentation', 200 | u'Curtis Maloney', 'manual'), 201 | ] 202 | 203 | # The name of an image file (relative to this directory) to place at the top of 204 | # the title page. 205 | #latex_logo = None 206 | 207 | # For "manual" documents, if this is true, then toplevel headings are parts, 208 | # not chapters. 209 | #latex_use_parts = False 210 | 211 | # If true, show page references after internal links. 212 | #latex_show_pagerefs = False 213 | 214 | # If true, show URL addresses after external links. 215 | #latex_show_urls = False 216 | 217 | # Documents to append as an appendix to all manuals. 218 | #latex_appendices = [] 219 | 220 | # If false, no module index is generated. 221 | #latex_domain_indices = True 222 | 223 | 224 | # -- Options for manual page output --------------------------------------- 225 | 226 | # One entry per manual page. List of tuples 227 | # (source start file, name, description, authors, manual section). 228 | man_pages = [ 229 | ('index', 'djangosniplates', u'Django Sniplates Documentation', 230 | [u'Curtis Maloney'], 1) 231 | ] 232 | 233 | # If true, show URL addresses after external links. 234 | #man_show_urls = False 235 | 236 | 237 | # -- Options for Texinfo output ------------------------------------------- 238 | 239 | # Grouping the document tree into Texinfo files. List of tuples 240 | # (source start file, target name, title, author, 241 | # dir menu entry, description, category) 242 | texinfo_documents = [ 243 | ('index', 'DjangoSniplates', u'Django Sniplates Documentation', 244 | u'Curtis Maloney', 'DjangoSniplates', 'One line description of project.', 245 | 'Miscellaneous'), 246 | ] 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #texinfo_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #texinfo_domain_indices = True 253 | 254 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 255 | #texinfo_show_urls = 'footnote' 256 | 257 | # If true, do not generate a @detailmenu in the "Top" node's menu. 258 | #texinfo_no_detailmenu = False 259 | -------------------------------------------------------------------------------- /docs/extractor.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | FieldExtractor 3 | ============== 4 | 5 | The ``FieldExtractor`` class defines which properties are extracted from Form 6 | Fields and made available in the rendering context. 7 | 8 | Extractors are registered in the sniplates.EXTRACTOR dict, mapping the Field 9 | class name to the exploder class to use. 10 | 11 | The default ``FieldExtractor`` copies the following attributes from the 12 | ``BoundField``: 13 | 14 | - css_classes 15 | - errors 16 | - field 17 | - form 18 | - help_text 19 | - html_name 20 | - id_for_label 21 | - label 22 | - name 23 | 24 | It also copies the following attributse from the ``Field``: 25 | 26 | - widget 27 | - required 28 | 29 | In addition, it will lazily evaluate the following: 30 | 31 | raw_value 32 | The raw value of the field. 33 | 34 | value 35 | The raw value forced to text. 36 | 37 | display 38 | The matching display value from the choices list for the current value, if 39 | this field has a value and a choices list. Otherwise empty string. 40 | 41 | choices 42 | A tuple of ``ChoiceWrapper`` instances. ``ChoiceWrapper`` is a named tuple 43 | with the attributes ``value`` and ``display``. It has one extra method: 44 | ``is_group`` which returns ``True`` for option-groups. 45 | ``ChoiceWrapper.value`` is forced to text. 46 | 47 | 48 | Sub-classes provided 49 | ==================== 50 | 51 | Three ``FieldExtractor`` sub-classes are provided out of the box: 52 | 53 | ``FileFieldExtractor`` adds ``file_size`` and ``url`` to the context. 54 | 55 | ``ImageFieldExtractor`` extends ``FileFieldExtractor``, and adds ``length`` and 56 | ``width`` also. 57 | 58 | ``NullBooleanFieldExtractor`` makes ``NullBooleanField`` compatible with the 59 | default ``Select`` widget. 60 | 61 | 62 | Providing your own extractors 63 | ============================= 64 | 65 | It's possible to write your own extractor and add it `to Sniplates`. You can 66 | use Django's ``django.apps.AppConfig`` for this. 67 | 68 | Example: 69 | 70 | .. code-block:: python 71 | 72 | # file myapp/extractors.py 73 | 74 | from sniplates.templatetags.sniplates import FieldExtractor 75 | 76 | 77 | class MultipleModelChoiceExtractor(FieldExtractor): 78 | 79 | @property 80 | def selected_instances(self): 81 | queryset = self.form_field.field.queryset 82 | if len(self.raw_value): 83 | return queryset.filter(pk__in=self.raw_value) 84 | return queryset.none() 85 | 86 | 87 | This would give you access to the queryset of selected instances in 88 | the field widget: 89 | 90 | .. code-block:: django 91 | 92 | {% for obj in selected_instances %} 93 | {{ obj }} 94 | {% endfor %} 95 | 96 | Registering it with Sniplates is done with the ``AppConfig``: 97 | 98 | .. code-block:: python 99 | 100 | # myapp/apps.py 101 | 102 | from django.apps import AppConfig 103 | 104 | 105 | class KitsConfig(AppConfig): 106 | name = 'myapp' 107 | 108 | def ready(self): 109 | # register the custom extractor 110 | from sniplates.templatetags.sniplates import EXTRACTOR 111 | from .extractors import MultipleModelChoiceExtractor 112 | EXTRACTOR['MultipleModelChoiceField'] = MultipleModelChoiceExtractor 113 | -------------------------------------------------------------------------------- /docs/filters.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Filters 3 | ======= 4 | 5 | The `flatattrs` filter 6 | ======================= 7 | 8 | .. code-block:: django 9 | 10 | {{ attrdict|flatarrs }} 11 | 12 | This is simply a wrapper around :func:`django.forms.utils.flatatt` 13 | 14 | It converts a dict of attributes into a string, in proper key="value" syntax. 15 | The values will be escaped, but keys will not. 16 | -------------------------------------------------------------------------------- /docs/formulation.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Migrating from ``formulation`` 3 | ============================== 4 | 5 | As of release 0.2.0 sniplates has equivalent functionality to formulation. 6 | 7 | The field widget templates from formulation are almost 100% compatible, except 8 | for the following cases: 9 | 10 | #. Replace ``use`` with ``widget`` 11 | 12 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Django Sniplates documentation master file, created by 2 | sphinx-quickstart on Sat Sep 6 10:23:25 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Django Sniplates 7 | ================ 8 | 9 | .. rubric:: Efficient template macros 10 | 11 | .. image:: https://travis-ci.org/funkybob/django-sniplates.png 12 | :target: https://travis-ci.org/funkybob/django-sniplates 13 | 14 | .. .. image:: https://pypip.in/d/django-sniplates/badge.png 15 | .. :target: https://crate.io/packages/django-sniplates 16 | 17 | .. image:: https://img.shields.io/pypi/v/django-sniplates.svg 18 | :target: https://pypi.python.org/pypi/django-sniplates 19 | 20 | 21 | Contents: 22 | 23 | .. toctree:: 24 | :maxdepth: 2 25 | 26 | tags 27 | filters 28 | extractor 29 | changelog 30 | formulation 31 | 32 | Overview 33 | -------- 34 | 35 | Have you ever found yourself repeating chunks of your templates, and wishing 36 | Django had a "macro" in templates? 37 | 38 | Tried using {% include %} only to find it slow and clumsy? 39 | 40 | Introducing Sniplates - the efficient way to provide your template authors with 41 | an arsenal of template macros. 42 | 43 | Requirements 44 | ------------ 45 | 46 | Requires Django 2.2 or newer, and is tested against Python 3.5, 3.6, 3.7, 3.8, 47 | PyP and PyPy3.5 48 | 49 | Quick-start 50 | ----------- 51 | 52 | First, install sniplates: 53 | 54 | .. code-block:: sh 55 | 56 | pip install django-sniplates 57 | 58 | And add it to ``INSTALLED_APPS``. 59 | 60 | Next, write a template with your widgets. Let's go with bootstrap tools, and 61 | call it `widgets/bootstrap.html` 62 | 63 | .. code-block:: django 64 | 65 | {% block label %}{{ text }}{% endblock %} 66 | {% block alert %}{% endblock %} 67 | 68 | Now, in your main template, you need to load the sniplates library. 69 | 70 | .. code-block:: django 71 | 72 | {% load sniplates %} 73 | 74 | Then load your widget library, and give it an alias: 75 | 76 | .. code-block:: django 77 | 78 | {% load_widgets bootstrap="widgets/bootstrap.html" %} 79 | 80 | Now, when you need to add a label or alert in your page: 81 | 82 | .. code-block:: django 83 | 84 | {% widget "bootstrap:label" text="Things go here" %} 85 | {% widget "bootstrap:alert" text="It's alive" alert_type="info" %} 86 | 87 | 88 | Indices and tables 89 | ================== 90 | 91 | * :ref:`genindex` 92 | * :ref:`modindex` 93 | * :ref:`search` 94 | 95 | 96 | Thanks 97 | ====== 98 | 99 | This project was originally inspired by a feature request by Stephan Sokolow. 100 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DjangoSniplates.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DjangoSniplates.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/tags.rst: -------------------------------------------------------------------------------- 1 | ==== 2 | Tags 3 | ==== 4 | 5 | The sniplates app consists solely of a custom template tag library. 6 | 7 | The `load_widgets` tag 8 | ====================== 9 | 10 | .. code-block:: django 11 | 12 | {% load_widgets alias=template_name ... %} 13 | 14 | This tag is used to load widget libraries from templates. You can load more 15 | than one library, either at a time, or in separate tags. Because the widgets 16 | are stored in the render context, not the context, they are available in child 17 | templates even when defined in an inherited template. 18 | 19 | If you pass `_soft=True` any alias that already exists will be skipped. This 20 | can be helpful to allow templates to ensure they have the widget sets they need 21 | without causing duplicate loads. 22 | 23 | .. caution:: 24 | 25 | Because of how Django's templates work, if you are using this in a template 26 | which inherits from another, it MUST be inside a {% block %} tag. 27 | 28 | Any content in a template starting with {% extends %} which it outside a 29 | {% block %} tag is ignored and never rendered. 30 | 31 | The `widget` tag 32 | ================ 33 | 34 | .. code-block:: django 35 | 36 | {% widget 'alias:block_name' ... [as asvar] %} 37 | 38 | Renders the specified widget with the current context. You can provide extra 39 | values to override, just like with `{% include %}`. Currently does not support 40 | the `only` argument. 41 | 42 | The name is composed of the alias specified in the `load_widgets` tag, and the 43 | name of the block in that template, joined with a ':'. If you use an "empty" 44 | alias, the block will be searched for in the current template (and any 45 | templates it extends). This form can be used without a `load_widgets` tag. 46 | 47 | You may use the `as` form of the tag to store the result of the block in the 48 | context variable you supply instead of rendering it in the template. 49 | 50 | 51 | The `form_field` tag 52 | ==================== 53 | 54 | .. code-block:: django 55 | 56 | {% form_field form.fieldname [widget=] [alias=form] .... %} 57 | 58 | Works like the ``widget`` tag, but extracts useful attributes of the field into 59 | the context. 60 | 61 | The values are extracted using a `FieldExtractor `_ class, selected according to the form field class. 62 | 63 | Any extra keyword arguments you pass to the field tag will overwrite values of 64 | the same name. 65 | 66 | If `widget` is not specified, it will be determined from the first found of any 67 | block matching the following patterns: 68 | 69 | - {field}_{widget}_{name} 70 | - {field}_{name} 71 | - {widget}_{name} 72 | - {field}_{widget} 73 | - {name} 74 | - {widget} 75 | - {field} 76 | 77 | .. note:: 78 | These will be looked up within the alias block set "**form**", unless the 79 | ``alias`` keyword is passed to override it. 80 | 81 | By way of example, given the following form:: 82 | 83 | from django.forms import Form, CharField, TextInput 84 | 85 | class MyForm(Form): 86 | example_field = CharField(widget=TextInput) 87 | 88 | using ``{% form_field myform.example_field %}`` without `widget` would look 89 | for a block with one of the following names, in the following order: 90 | 91 | - ``CharField_TextInput_example_field`` 92 | - ``CharField_example_field`` 93 | - ``TextInput_example_field`` 94 | - ``CharField_TextInput`` 95 | - ``example_field`` 96 | - ``TextInput`` 97 | - ``CharField`` 98 | 99 | 100 | Values from ``BoundField`` 101 | -------------------------- 102 | 103 | The following values are take from the ``BoundField``: 104 | 105 | - css_classes 106 | - errors 107 | - field 108 | - form 109 | - help_text 110 | - html_name 111 | - id_for_label 112 | - label 113 | - name 114 | - value 115 | 116 | If the field is a FileField, an extra value `file` will be added, which 117 | contains the size and url attributes of the current file. If it's an 118 | ImageField, the width and height may also be avaialble. 119 | 120 | Values from ``Field`` 121 | --------------------- 122 | 123 | And these from the ``Field`` itself: 124 | 125 | - choices 126 | - widget 127 | - required 128 | 129 | If the field is a ChoicesField, an extra value `display` will be added, which 130 | is the display value for the current value, if any. 131 | 132 | The `nested_widget` tag 133 | ======================= 134 | 135 | .. code-block:: django 136 | 137 | {% nested_widget widgetname .... [as asvar] %} 138 | ... 139 | {% endnested %} 140 | 141 | This tag is a container block that will render its contents, and pass the 142 | output to its widget as 'content'. 143 | 144 | An example use of this is for wrapping fields in a fieldset template: 145 | 146 | .. code-block:: django 147 | 148 | {% block fieldset %} 149 |
150 | {% if caption %}{{ caption }}{% endif %} 151 | {{ content }} 152 | {% endblock %} 153 | 154 | And would be used as follows: 155 | 156 | .. code-block:: django 157 | 158 | {% nested_widget 'form:fieldset' caption="About You" %} 159 | {% form_field form.first_name %}
160 | {% form_field form.last_name %} 161 | {% endnested %} 162 | 163 | This tag also supports storing the result in a context variable of your choice 164 | instead of rendering immediately. 165 | 166 | 167 | The `reuse` tag 168 | =============== 169 | 170 | .. code-block:: django 171 | 172 | {% reuse blockname ... %} 173 | 174 | Much like the `widget` tag, this re-renders an existing block tag in situ. 175 | However, instead of looking for the block in a loaded widget library, it 176 | searches the current template. This allows templates extending a base to 177 | define reusable "macro" blocks, without having to load a separate widget set. 178 | 179 | As with other tags, you can extend the context by passing keyword arguments. 180 | 181 | .. note:: This tag only works in templates that {% extends %} another template. 182 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "django-sniplates" 3 | version = "0.7.2" 4 | description = "Efficient template macro sets for Django" 5 | authors = [ 6 | { name = "Curtis Maloney", email="curtis@tinbrain.net" }, 7 | ] 8 | readme = "README.md" 9 | license = { file = "LICENSE" } 10 | keywords = ["django", "templates", "forms"] 11 | classifiers = [ 12 | "Environment :: Web Environment", 13 | "Framework :: Django", 14 | "Framework :: Django :: 2.2", 15 | "Framework :: Django :: 3", 16 | "Framework :: Django :: 4.0", 17 | "Framework :: Django :: 4.1", 18 | "Framework :: Django :: 4.2", 19 | "License :: OSI Approved :: BSD License", 20 | "Operating System :: OS Independent", 21 | "Programming Language :: Python", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | ] 27 | 28 | dependencies = [ 29 | "django (>=3.0.5)", 30 | ] 31 | 32 | [project.urls] 33 | Repository = "https://github.com/funkybob/django-sniplates" 34 | Documentation = "https://sniplates.readthedocs.io/en/latest/" 35 | 36 | [project.optional-dependencies] 37 | test = [ 38 | "isort", 39 | "pytest", 40 | "pytest-cov", 41 | "pytest-django", 42 | "pylint", 43 | "pylint-pytest", 44 | ] 45 | 46 | [tool.pytest.ini_options] 47 | DJANGO_SETTINGS_MODULE = "tests.settings" 48 | 49 | django_find_project = false 50 | pythonpath = ["."] 51 | 52 | addopts = "--cov=src/" 53 | 54 | [tool.coverage.run] 55 | branch = true 56 | source = ["src/"] 57 | 58 | [tool.pylint."message control"] 59 | disable = ["missing-module-docstring", "missing-class-docstring", "missing-function-docstring", "raise-missing-from", "redefined-outer-name"] 60 | 61 | [tool.isort] 62 | combine_as_imports = true 63 | default_section = "THIRDPARTY" 64 | include_trailing_comma = true 65 | line_length = 119 66 | -------------------------------------------------------------------------------- /src/sniplates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funkybob/django-sniplates/10ac7f2e436538979b6f5729a3108787b930b07f/src/sniplates/__init__.py -------------------------------------------------------------------------------- /src/sniplates/templates/sniplates/django.html: -------------------------------------------------------------------------------- 1 | {% load i18n sniplates %} 2 | Default, basic form rendering 3 | 4 | # Built in fields: 5 | 6 | 'CharField', 7 | 'IntegerField', 8 | 'DateField', 9 | 'TimeField', 10 | 'DateTimeField', 11 | 'TimeField', 12 | 'RegexField', 13 | 'EmailField', 14 | 'FileField', 15 | 'ImageField', 16 | 'URLField', 17 | 'BooleanField', 18 | 'NullBooleanField', 19 | 'ChoiceField', 20 | 'MultipleChoiceField', 21 | 'ComboField', 22 | 'MultiValueField', 23 | 'FloatField', 24 | 'DecimalField', 25 | 'SplitDateTimeField', 26 | 'IPAddressField', 27 | 'GenericIPAddressField', 28 | 'FilePathField', 29 | 'SlugField', 30 | 'TypedChoiceField', 31 | 'TypedMultipleChoiceField' 32 | 33 | # Built in widgets: 34 | 35 | 'TextInput', 36 | 'EmailInput', 37 | 'URLInput', 38 | 'NumberInput', 39 | 'PasswordInput', 40 | 'HiddenInput', 41 | 'MultipleHiddenInput', 42 | 'ClearableFileInput', 43 | 'FileInput', 44 | 'DateInput', 45 | 'DateTimeInput', 46 | 'TimeInput', 47 | 'Textarea', 48 | 'CheckboxInput', 49 | 'Select', 50 | 'NullBooleanSelect', 51 | 'SelectMultiple', 52 | 'RadioSelect', 53 | 'CheckboxSelectMultiple', 54 | 'SplitDateTimeWidget', 55 | 'SplitHiddenDateTimeWidget', 56 | 57 | How to render labels 58 | {% block _label %} 59 | {% if label %}{% endif %} 60 | {% endblock %} 61 | 62 | How to render help_text 63 | {% block _help %} 64 | {{ help_text }} 65 | {% endblock %} 66 | 67 | How to render errors 68 | {% block _errors %} 69 | {% if errors %} 70 |
    71 | {% for error in errors %} 72 |
  • {{ error }}
  • 73 | {% endfor %} 74 |
75 | {% endif %} 76 | {% endblock %} 77 | 78 | {% block input %} 79 | {% with input_type=input_type|default:"text" %} 80 | 89 | {% endwith %} 90 | {% endblock %} 91 | 92 | {% block TextInput %}{% reuse "input" %}{% endblock %} 93 | {% block EmailInput %}{% reuse "input" input_type="email" %}{% endblock %} 94 | {% block NumberInput %}{% reuse "input" input_type="number" %}{% endblock %} 95 | {% block URLInput %}{% reuse "input" input_type="url" %}{% endblock %} 96 | {% block PasswordInput %}{% reuse "input" input_type="password" raw_value="" %}{% endblock %} 97 | {% block HiddenInput %}{% reuse "input" input_type="hidden" label="" %}{% endblock %} 98 | {% block FileInput %}{% reuse "input" input_type="file" value="" %}{% endblock %} 99 | {% block DateInput %}{% reuse "input" input_type="text" raw_value=value %}{% endblock %} 100 | {% block DateTimeInput %}{% reuse "input" input_type="text" raw_value=value %}{% endblock %} 101 | {% block TimeInput %}{% reuse "input" input_type="text" raw_value=value %}{% endblock %} 102 | 103 | {% block ClearableFileInput %} 104 | {% if raw_value %} 105 | {% trans "Currently" %}: {{ url }} 106 | 107 |
108 | {% trans "Change" %}: 109 | {% endif %} 110 | {% reuse "input" input_type="file" raw_value="" %} 111 | {% endblock %} 112 | 113 | TODO: 114 | 115 | {% block SplitDateTimeWidget %}{% endblock %} 116 | {% block SplitHiddenDateTimeWidget %}{% endblock %} 117 | 118 | Fields not derived from InputField: 119 | 120 | {% block MultipleHiddenInput %} 121 | {% for value in raw_value %} 122 | 123 | {% endfor %} 124 | {% endblock %} 125 | 126 | {% block Textarea %} 127 | 133 | {% endblock %} 134 | 135 | 136 | Underscore prefixed to avoid potentially clashing with: 137 | - {field}_{name} 138 | - {widget}_{name} 139 | - {field}_{widget} 140 | {% block _Select_Option %} 141 | 142 | {% endblock %} 143 | 144 | {% block Select %} 145 | 158 | {% endblock %} 159 | 160 | {% block SelectMultiple %} 161 | 166 | {% endblock %} 167 | {% block NullBooleanSelect %}{% reuse "Select" %}{% endblock %} 168 | 169 | Checkbox is a special case and needs its own template. 170 | {% block CheckboxInput %} 171 | 175 | {% endblock %} 176 | 177 | {% block RadioSelect %} 178 |
    179 | {% for val, display in choices %} 180 |
  • {{ display }}
  • 181 | {% endfor %} 182 |
183 | {% endblock %} 184 | 185 | {% block CheckboxSelectMultiple %} 186 |
    187 | {% for val, display in choices %} 188 |
  • {{ display }}
  • 189 | {% endfor %} 190 |
191 | {% endblock %} 192 | -------------------------------------------------------------------------------- /src/sniplates/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funkybob/django-sniplates/10ac7f2e436538979b6f5729a3108787b930b07f/src/sniplates/templatetags/__init__.py -------------------------------------------------------------------------------- /src/sniplates/templatetags/sniplates.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from contextlib import contextmanager 3 | 4 | from django.forms.utils import flatatt 5 | from django.forms.widgets import DateTimeBaseInput 6 | from django.template import Library, Node, TemplateSyntaxError 7 | from django.template.base import token_kwargs 8 | from django.template.loader import get_template 9 | from django.template.loader_tags import BLOCK_CONTEXT_KEY, BlockContext, BlockNode, ExtendsNode 10 | from django.utils.encoding import force_str 11 | from django.utils.functional import cached_property 12 | 13 | register = Library() 14 | 15 | ''' 16 | Sniplates 17 | 18 | Re-usable template widgets. 19 | 20 | {% load_widgets alias="template.name" .... %} 21 | 22 | 23 | {% widget 'alias:blockname' .... %} 24 | 25 | ''' 26 | 27 | WIDGET_CONTEXT_KEY = '_widgets_' 28 | 29 | 30 | def resolve_blocks(template, context): 31 | ''' 32 | Return a BlockContext instance of all the {% block %} tags in the template. 33 | 34 | If template is a string, it will be resolved through get_template 35 | ''' 36 | try: 37 | blocks = context.render_context[BLOCK_CONTEXT_KEY] 38 | except KeyError: 39 | blocks = context.render_context[BLOCK_CONTEXT_KEY] = BlockContext() 40 | 41 | # If it's just the name, resolve into template 42 | if isinstance(template, str): 43 | template = get_template(template) 44 | 45 | # For Django 1.8 compatibility 46 | template = getattr(template, 'template', template) 47 | 48 | # Add this templates blocks as the first 49 | local_blocks = { 50 | block.name: block 51 | for block in template.nodelist.get_nodes_by_type(BlockNode) 52 | } 53 | blocks.add_blocks(local_blocks) 54 | 55 | # Do we extend a parent template? 56 | extends = template.nodelist.get_nodes_by_type(ExtendsNode) 57 | if extends: 58 | # Can only have one extends in a template 59 | extends_node = extends[0] 60 | 61 | # Get the parent, and recurse 62 | parent_template = extends_node.get_parent(context) 63 | resolve_blocks(parent_template, context) 64 | 65 | return blocks 66 | 67 | 68 | def parse_widget_name(widget): 69 | ''' 70 | Parse a alias:block_name string into separate parts. 71 | ''' 72 | try: 73 | alias, block_name = widget.split(':', 1) 74 | except ValueError: 75 | raise TemplateSyntaxError(f'widget name must be "alias:block_name" - {widget}') 76 | 77 | return alias, block_name 78 | 79 | 80 | @contextmanager 81 | def using(context, alias): 82 | ''' 83 | Temporarily update the context to use the BlockContext for the given alias. 84 | ''' 85 | 86 | # An empty alias means look in the current widget set. 87 | if alias == '': 88 | yield context 89 | else: 90 | try: 91 | widgets = context.render_context[WIDGET_CONTEXT_KEY] 92 | except KeyError: 93 | raise TemplateSyntaxError('No widget libraries loaded!') 94 | 95 | try: 96 | block_set = widgets[alias] 97 | except KeyError: 98 | raise TemplateSyntaxError(f'No widget library loaded for alias: {alias !r}') 99 | 100 | context.render_context.push() 101 | context.render_context[BLOCK_CONTEXT_KEY] = block_set 102 | context.render_context[WIDGET_CONTEXT_KEY] = widgets 103 | 104 | yield context 105 | 106 | context.render_context.pop() 107 | 108 | 109 | def find_block(context, *names): 110 | ''' 111 | Find the first matching block in the current block_context 112 | ''' 113 | block_set = context.render_context[BLOCK_CONTEXT_KEY] 114 | for name in names: 115 | block = block_set.get_block(name) 116 | if block is not None: 117 | return block 118 | 119 | raise TemplateSyntaxError(f'No widget found for: {names !r}') 120 | 121 | 122 | @register.simple_tag(takes_context=True) 123 | def load_widgets(context, **kwargs): 124 | ''' 125 | Load a series of widget libraries. 126 | ''' 127 | _soft = kwargs.pop('_soft', False) 128 | 129 | try: 130 | widgets = context.render_context[WIDGET_CONTEXT_KEY] 131 | except KeyError: 132 | widgets = context.render_context[WIDGET_CONTEXT_KEY] = {} 133 | 134 | for alias, template_name in kwargs.items(): 135 | if _soft and alias in widgets: 136 | continue 137 | 138 | with context.render_context.push({BLOCK_CONTEXT_KEY: BlockContext()}): 139 | blocks = resolve_blocks(template_name, context) 140 | widgets[alias] = blocks 141 | 142 | return '' 143 | 144 | 145 | def pop_asvar(bits): 146 | if len(bits) >= 2 and bits[-2] == 'as': 147 | asvar = bits[-1] 148 | del bits[-2:] 149 | return asvar 150 | return None 151 | 152 | 153 | @register.simple_tag(takes_context=True) 154 | def widget(context, name, **kwargs): 155 | alias, block_name = parse_widget_name(name) 156 | 157 | with using(context, alias): 158 | block = find_block(context, block_name) 159 | with context.push(kwargs): 160 | return block.render(context) 161 | 162 | 163 | class NestedWidget(Node): 164 | def __init__(self, widget, nodelist, kwargs, asvar): 165 | self.widget = widget 166 | self.nodelist = nodelist 167 | self.kwargs = kwargs 168 | self.asvar = asvar 169 | 170 | def render(self, context): 171 | widget = self.widget.resolve(context) 172 | 173 | alias, block_name = parse_widget_name(widget) 174 | 175 | with using(context, alias): 176 | block = find_block(context, block_name) 177 | 178 | kwargs = { 179 | key: val.resolve(context) 180 | for key, val in self.kwargs.items() 181 | } 182 | 183 | with context.push(kwargs): 184 | content = self.nodelist.render(context) 185 | with context.push({'content': content}): 186 | result = block.render(context) 187 | 188 | if self.asvar: 189 | context[self.asvar] = result 190 | return '' 191 | 192 | return result 193 | 194 | 195 | @register.tag 196 | def nested_widget(parser, token): 197 | bits = token.split_contents() 198 | tag_name = bits.pop(0) 199 | 200 | try: 201 | widget = parser.compile_filter(bits.pop(0)) 202 | except IndexError: 203 | raise TemplateSyntaxError(f'{tag_name} requires one positional argument') 204 | 205 | asvar = pop_asvar(bits) 206 | 207 | kwargs = token_kwargs(bits, parser) 208 | 209 | if bits: 210 | raise TemplateSyntaxError('{tag_name} accepts only one positional argument') 211 | 212 | nodelist = parser.parse(('endnested',)) 213 | parser.delete_first_token() 214 | 215 | return NestedWidget(widget, nodelist, kwargs, asvar) 216 | 217 | 218 | class ChoiceWrapper(tuple): 219 | 220 | def __new__(cls, value=None, display=None): 221 | tuple_args = [value, display] 222 | return super(ChoiceWrapper, cls).__new__(cls, tuple(tuple_args)) 223 | 224 | def __init__(self, value, display): 225 | self.value = force_str(value) 226 | self._display = display 227 | 228 | def __repr__(self): 229 | return f'ChoiceWrapper(value={self.value}, display={self.display})' 230 | 231 | def __iter__(self): 232 | # overriden from tuple to retrun the formatted display 233 | yield self.value 234 | yield self.display 235 | 236 | def is_group(self): 237 | return isinstance(self._display, (list, tuple)) 238 | 239 | @property 240 | def display(self): 241 | """ 242 | When dealing with optgroups, ensure that the value is properly force_str'd. 243 | """ 244 | if not self.is_group(): 245 | return self._display 246 | return ((force_str(k), v) for k, v in self._display) 247 | 248 | 249 | class FieldExtractor(dict): 250 | ''' 251 | Base class for extracting Field details. 252 | Acts as a dict so we can push it on the context stack. 253 | ''' 254 | def __init__(self, field): 255 | self.form_field = field 256 | self.update({ 257 | 'id': field.auto_id, 258 | 'widget_type': field.field.widget.__class__.__name__, 259 | 'field_type': field.field.__class__.__name__, 260 | }) 261 | 262 | for attr in ( 263 | 'css_classes', 264 | 'errors', 265 | 'field', 266 | 'form', 267 | 'help_text', 268 | 'html_name', 269 | 'id_for_label', 270 | 'label', 271 | 'name', 272 | ): 273 | self[attr] = getattr(field, attr) 274 | 275 | for attr in ( 276 | 'widget', 277 | 'required', 278 | 'disabled', 279 | ): 280 | self[attr] = getattr(field.field, attr, None) 281 | 282 | def __contains__(self, key): 283 | ''' 284 | Context uses 'if key in ...' 285 | ''' 286 | return key in self.keys() or hasattr(self, key) 287 | 288 | def __missing__(self, key): 289 | return getattr(self, key) 290 | 291 | @cached_property 292 | def raw_value(self): 293 | return self.form_field.value() 294 | 295 | @cached_property 296 | def value(self): 297 | if isinstance(self.raw_value, (tuple, list)): 298 | return [force_str(bit) for bit in self.raw_value] 299 | return force_str(self.raw_value) 300 | 301 | @cached_property 302 | def initial(self): 303 | data = self.form_field.form.initial.get(self.form_field.name, self.form_field.field.initial) 304 | if callable(data): 305 | data = data() 306 | # If this is an auto-generated default date, nix the 307 | # microseconds for standardized handling. See #22502. 308 | if ( 309 | isinstance(data, (datetime.datetime, datetime.time)) and 310 | not self.form_field.widget.supports_microseconds 311 | ): 312 | data = data.replace(microsecond=0) 313 | return self.form_field.field.prepare_value(data) 314 | 315 | @cached_property 316 | def display(self): 317 | '''Display value for selected choice.''' 318 | return dict(self.choices).get(self.value, '') 319 | 320 | @cached_property 321 | def choices(self): 322 | choices = self.form_field.field.choices 323 | if not choices: 324 | return choices 325 | return tuple( 326 | ChoiceWrapper(value=k, display=v) 327 | for k, v in choices 328 | ) 329 | 330 | 331 | class FileFieldExtractor(FieldExtractor): 332 | 333 | @cached_property 334 | def file_size(self): 335 | return self.raw_value.size if self.raw_value else None 336 | 337 | @cached_property 338 | def url(self): 339 | return self.raw_value.url if self.raw_value else None 340 | 341 | 342 | class ImageFieldExtractor(FileFieldExtractor): 343 | 344 | @cached_property 345 | def width(self): 346 | return self.value.widget if self.value else None 347 | 348 | @cached_property 349 | def height(self): 350 | return self.value.height if self.value else None 351 | 352 | 353 | class NullBooleanFieldExtractor(FieldExtractor): 354 | 355 | @cached_property 356 | def raw_value(self): 357 | """ 358 | When the value is None, it's actually rendered as 'unknown', see 359 | ``django.forms.widgets.NullBooleanSelect.__init__`` 360 | """ 361 | raw_value = super().raw_value 362 | if raw_value is None: 363 | return 'unknown' 364 | return raw_value 365 | 366 | @cached_property 367 | def value(self): 368 | """ 369 | Maps True/False and 2/3 to the correct stringified version. 370 | 371 | See ``django.forms.widgets.NullBooleanSelect.value_from_datadict``. 372 | """ 373 | try: 374 | return { 375 | True: 'true', 376 | False: 'false', 377 | 'true': 'true', 378 | 'false': 'false', 379 | 'True': 'true', 380 | 'False': 'false', 381 | '2': 'true', 382 | '3': 'false' 383 | }[self.raw_value] 384 | except KeyError: 385 | return 'unknown' 386 | 387 | @cached_property 388 | def choices(self): 389 | choices = self['widget'].choices 390 | if not choices: 391 | return choices 392 | 393 | return tuple( 394 | ChoiceWrapper(value=force_str(k), display=v) 395 | for k, v in choices 396 | ) 397 | 398 | 399 | class DateTimeBaseExtractor(FieldExtractor): 400 | """ 401 | Applies the date/time/datetime formatting to the value. 402 | """ 403 | 404 | @cached_property 405 | def value(self): 406 | if isinstance(self['widget'], DateTimeBaseInput): 407 | return self['widget'].format_value(self.raw_value) 408 | # if it's a different widget, fall back to the default 409 | return super().value 410 | 411 | 412 | # Map of field types to functions for extracting their data 413 | EXTRACTOR = { 414 | 'FileField': FileFieldExtractor, 415 | 'ImageField': ImageFieldExtractor, 416 | 'NullBooleanField': NullBooleanFieldExtractor, 417 | 'DateField': DateTimeBaseExtractor, 418 | 'DateTimeField': DateTimeBaseExtractor, 419 | 'TimeField': DateTimeBaseExtractor, 420 | } 421 | 422 | 423 | @register.simple_tag(takes_context=True) 424 | def form_field(context, field, widget=None, **kwargs): 425 | if not field: 426 | raise TemplateSyntaxError('form_field requires a value field as first argument') 427 | 428 | if widget is None: 429 | alias = kwargs.pop('alias', 'form') 430 | 431 | block_names = auto_widget(field) 432 | else: 433 | alias, block_name = parse_widget_name(widget) 434 | 435 | block_names = [block_name] 436 | 437 | field_type = field.field.__class__.__name__ 438 | field_data = EXTRACTOR.get(field_type, FieldExtractor)(field) 439 | 440 | # Allow supplied values to override field data 441 | field_data.update(kwargs) 442 | 443 | with using(context, alias): 444 | block = find_block(context, *block_names) 445 | 446 | try: 447 | context.dicts.append(field_data) 448 | return block.render(context) 449 | finally: 450 | context.dicts.pop() 451 | 452 | 453 | def auto_widget(field): 454 | '''Return a list of widget names for the provided field.''' 455 | 456 | name = field.name 457 | widget = field.field.widget.__class__.__name__ 458 | field = field.field.__class__.__name__ 459 | 460 | return [ 461 | f'{field}_{widget}_{name}', 462 | f'{field}_{name}', 463 | f'{widget}_{name}', 464 | f'{field}_{widget}', 465 | f'{name}', 466 | f'{widget}', 467 | f'{field}', 468 | ] 469 | 470 | 471 | @register.filter 472 | def flatattrs(attrs): 473 | return flatatt(attrs) 474 | 475 | 476 | @register.simple_tag(takes_context=True) 477 | def reuse(context, block_list, **kwargs): 478 | ''' 479 | Allow reuse of a block within a template. 480 | 481 | {% reuse '_myblock' foo=bar %} 482 | 483 | If passed a list of block names, will use the first that matches: 484 | 485 | {% reuse list_of_block_names .... %} 486 | ''' 487 | try: 488 | block_context = context.render_context[BLOCK_CONTEXT_KEY] 489 | except KeyError: 490 | block_context = BlockContext() 491 | 492 | if not isinstance(block_list, (list, tuple)): 493 | block_list = [block_list] 494 | 495 | for block_name in block_list: 496 | block = block_context.get_block(block_name) 497 | if block: 498 | break 499 | else: 500 | return '' 501 | 502 | with context.push(kwargs): 503 | return block.render(context) 504 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funkybob/django-sniplates/10ac7f2e436538979b6f5729a3108787b930b07f/tests/__init__.py -------------------------------------------------------------------------------- /tests/forms.py: -------------------------------------------------------------------------------- 1 | 2 | from django import forms 3 | 4 | 5 | class TestForm(forms.Form): 6 | char = forms.CharField() 7 | oneof = forms.ChoiceField(choices=tuple(enumerate('abcd'))) 8 | many = forms.MultipleChoiceField(choices=tuple(enumerate('abcd'))) 9 | many2 = forms.MultipleChoiceField(choices=((1, 'a'), (11, 'b'), (22, 'c'))) 10 | 11 | 12 | class DjangoWidgetsForm(forms.Form): 13 | char = forms.CharField() 14 | email = forms.EmailField() 15 | url = forms.URLField() 16 | number = forms.IntegerField() 17 | password = forms.CharField(widget=forms.PasswordInput) 18 | hidden = forms.CharField(widget=forms.HiddenInput) 19 | multiple_hidden = forms.ChoiceField(choices=((1, 'a'), (11, 'b'), (22, 'c')), 20 | widget=forms.MultipleHiddenInput) 21 | date = forms.DateField() 22 | datetime = forms.DateTimeField() 23 | time = forms.TimeField() 24 | text = forms.CharField(widget=forms.Textarea) 25 | checkbox = forms.BooleanField() 26 | select = forms.ChoiceField(choices=((1, 'a'), (11, 'b'), (22, 'c'))) 27 | optgroup_select = forms.ChoiceField(choices=( 28 | ('label1', [(1, 'a'), (11, 'b')]), 29 | ('label2', [(22, 'c')]) 30 | )) 31 | null_boolean_select = forms.NullBooleanField() 32 | select_multiple = forms.MultipleChoiceField(choices=((1, 'a'), (11, 'b'), (22, 'c'))) 33 | radio_select = forms.ChoiceField(choices=((1, 'a'), (11, 'b'), (22, 'c')), 34 | widget=forms.RadioSelect) 35 | checkbox_select_multiple = forms.MultipleChoiceField( 36 | choices=((1, 'a'), (11, 'b'), (22, 'c')), 37 | widget=forms.CheckboxSelectMultiple 38 | ) 39 | 40 | file = forms.FileField(widget=forms.FileInput) 41 | clearable_file = forms.FileField() 42 | # TODO: clearableFileInput, FileInput, Split(Hidden)DateTimeWidget 43 | 44 | 45 | class FilesForm(forms.Form): 46 | clearable_file = forms.FileField() 47 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funkybob/django-sniplates/10ac7f2e436538979b6f5729a3108787b930b07f/tests/models.py -------------------------------------------------------------------------------- /tests/report_settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings file to be able to generate a report from the CLI. 3 | """ 4 | 5 | SECRET_KEY = 'not-so-secret' 6 | TEMPLATE_DEBUG = True 7 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | DATABASES={ 2 | 'default': { 3 | 'ENGINE': 'django.db.backends.sqlite3', 4 | } 5 | } 6 | 7 | INSTALLED_APPS=( 8 | 'sniplates', 9 | 'tests', 10 | ) 11 | 12 | MIDDLEWARE_CLASSES=[] 13 | 14 | TEMPLATES=[ 15 | { 16 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 17 | 'DIRS': [], 18 | 'APP_DIRS': True, 19 | 'OPTIONS': { 20 | 'debug': True 21 | } 22 | }, 23 | ] 24 | 25 | USE_TZ = True 26 | -------------------------------------------------------------------------------- /tests/templates/field_tag/choices: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets form="widgets" %}{% form_field form.oneof %} 2 | -------------------------------------------------------------------------------- /tests/templates/field_tag/choices_multi: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets form="widgets" %}{% form_field form.many %} 2 | 3 | -------------------------------------------------------------------------------- /tests/templates/field_tag/choices_multi2: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets form="widgets" %}{% form_field form.many2 %} 2 | 3 | -------------------------------------------------------------------------------- /tests/templates/field_tag/empty_field: -------------------------------------------------------------------------------- 1 | {% load sniplates %} 2 | {% form_field '' %} 3 | -------------------------------------------------------------------------------- /tests/templates/field_tag/field: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets form="widgets" %}{% form_field form.char %} 2 | -------------------------------------------------------------------------------- /tests/templates/field_tag/override: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets form="widgets" other="widget2" %}{% form_field form.char widget="other:password" %} 2 | -------------------------------------------------------------------------------- /tests/templates/field_tag/override2: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets form="widgets" other="widget2" %}{% form_field form.char alias="other" %} 2 | 3 | -------------------------------------------------------------------------------- /tests/templates/field_tag/widget2: -------------------------------------------------------------------------------- 1 | {% block CharField %} 8 | {% for val, disp in choices %} 9 | 10 | {% endfor %} 11 | 12 | {% endblock %} 13 | 14 | {% block MultipleChoiceField %} 15 | {{ value }} 16 | {{ choices|safe }} 17 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /tests/templates/field_tag/widgets_django: -------------------------------------------------------------------------------- 1 | {% load sniplates %} 2 | {% load_widgets form="sniplates/django.html" %} 3 | {% for field in form %} 4 | {% form_field field %} 5 | {% endfor %} 6 | -------------------------------------------------------------------------------- /tests/templates/filters/flatattrs: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{{ a_dict|flatattrs }} 2 | -------------------------------------------------------------------------------- /tests/templates/inheritance/base: -------------------------------------------------------------------------------- 1 | DOCUMENT {% block content %}{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/templates/inheritance/block_overlap: -------------------------------------------------------------------------------- 1 | {% extends 'base' %}{% load sniplates %}{% load_widgets foo='block_overlap_widgets' %}{% block content %}content {% widget 'foo:bar' %}{% endblock %}{% block bar %}bar{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/templates/inheritance/block_overlap_widget: -------------------------------------------------------------------------------- 1 | {% block foo %}foo{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/templates/inheritance/parent_inherit: -------------------------------------------------------------------------------- 1 | {% extends 'parent_inherit_base' %}{% load sniplates %}{% block content %}{% widget 'foo:test' %}{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/templates/inheritance/parent_inherit_base: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets foo='parent_inherit_widgets' %}{% block content %}{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/templates/inheritance/parent_inherit_widgets: -------------------------------------------------------------------------------- 1 | {% block test %}foo{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/templates/inheritance/parent_overlap: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets foo='parent_overlap_widgets' %}{% block main %}first{% endblock%} 2 | -------------------------------------------------------------------------------- /tests/templates/inheritance/parent_overlap_widgets: -------------------------------------------------------------------------------- 1 | {% block main %}second{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/templates/inheritance/super: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets w=widget_template %}{% widget "w:foo" %} -------------------------------------------------------------------------------- /tests/templates/inheritance/super_widget_base: -------------------------------------------------------------------------------- 1 | {% block foo %}BASE{% endblock %} -------------------------------------------------------------------------------- /tests/templates/inheritance/super_widget_inherit: -------------------------------------------------------------------------------- 1 | {% extends 'super_widget_base' %}{% block foo %}EXTENDING {{ block.super }}{% endblock %} -------------------------------------------------------------------------------- /tests/templates/invalid/bad_name: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% widget 'boo-bar' %} 2 | -------------------------------------------------------------------------------- /tests/templates/invalid/no_lib: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets foo='simple.html' %}{% widget 'bar:no_lib' %} 2 | -------------------------------------------------------------------------------- /tests/templates/invalid/no_widget: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets foo='simple.html' %}{% widget "foo:no_widget" %} 2 | -------------------------------------------------------------------------------- /tests/templates/invalid/not_loaded: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% widget 'foo:bar' %} 2 | -------------------------------------------------------------------------------- /tests/templates/invalid/simple.html: -------------------------------------------------------------------------------- 1 | {% block simple %}success{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/templates/load_widgets/load_widgets: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets foo='simple.html' %}{% widget "foo:simple" %} 2 | -------------------------------------------------------------------------------- /tests/templates/load_widgets/load_widgets_three: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets foo='simple.html' %}{% load_widgets bar='other.html' %}{% widget 'foo:simple' %}<=>{% widget 'bar:other' %} 2 | -------------------------------------------------------------------------------- /tests/templates/load_widgets/load_widgets_two: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets foo='simple.html' bar='other.html' %}{% widget 'foo:simple' %}<=>{% widget 'bar:other' %} 2 | -------------------------------------------------------------------------------- /tests/templates/load_widgets/other.html: -------------------------------------------------------------------------------- 1 | {% block other %}winning{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/templates/load_widgets/simple.html: -------------------------------------------------------------------------------- 1 | {% block simple %}success{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/templates/nested_tag/asvar: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets form="widgets" %}{% nested_widget "form:fieldset" caption="Caption" as asvar %}content goes here{% endnested %} 2 | BEFORE {{ asvar }} 3 | -------------------------------------------------------------------------------- /tests/templates/nested_tag/empty: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets form="widgets" %}{% nested_widget "form:fieldset" %}{% endnested %} 2 | -------------------------------------------------------------------------------- /tests/templates/nested_tag/invalid: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% nested_widget %} 2 | -------------------------------------------------------------------------------- /tests/templates/nested_tag/invalid2: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% nested_widget 'foo:bar' baz %} 2 | -------------------------------------------------------------------------------- /tests/templates/nested_tag/keep_widgets: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets w='widgets' %} 2 | {% nested_widget 'w:fieldset' %}{% widget 'w:CharField' %}{% endnested %} 3 | -------------------------------------------------------------------------------- /tests/templates/nested_tag/simple: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets form="widgets" %}{% nested_widget "form:fieldset" caption="Caption" %}content goes here{% endnested %} 2 | -------------------------------------------------------------------------------- /tests/templates/nested_tag/widgets: -------------------------------------------------------------------------------- 1 | {% block fieldset %}
{{ caption }}{{ content|safe }}
{% endblock %} 2 | {% block CharField %}{% endblock %} 3 | {% block ChoiceField %}{% endblock %} 6 | -------------------------------------------------------------------------------- /tests/templates/reuse/base: -------------------------------------------------------------------------------- 1 | {% block main %}{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/templates/reuse/inwidget: -------------------------------------------------------------------------------- 1 | {% load sniplates %} 2 | {% load_widgets simple="simple" %} 3 | 4 | {% widget "simple:second" %} 5 | -------------------------------------------------------------------------------- /tests/templates/reuse/reuse: -------------------------------------------------------------------------------- 1 | {% extends 'base' %}{% load sniplates %}{% block true %}true{% endblock %}{% block main %}{% reuse 'true' %}{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/templates/reuse/simple: -------------------------------------------------------------------------------- 1 | {% load sniplates %} 2 | {% block first %}true{% endblock %} 3 | {% block second %}{% reuse "first" %}{% endblock %} 4 | -------------------------------------------------------------------------------- /tests/templates/widget_tag/alias_self: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets widgets='widgets.3' %}{% widget 'widgets:referencing' %} -------------------------------------------------------------------------------- /tests/templates/widget_tag/asvar: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets foo='widgets.1' %}{% widget 'foo:fixed' as asvar %}AFTER {{ asvar }} -------------------------------------------------------------------------------- /tests/templates/widget_tag/fixed: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets foo='widgets.1' %}{% widget 'foo:fixed' %} 2 | -------------------------------------------------------------------------------- /tests/templates/widget_tag/inherit: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets foo='widgets.2' %}{% widget 'foo:var' var='value' %} 2 | -------------------------------------------------------------------------------- /tests/templates/widget_tag/var: -------------------------------------------------------------------------------- 1 | {% load sniplates %}{% load_widgets foo='widgets.1' %}{% widget 'foo:var' var='value' %} 2 | -------------------------------------------------------------------------------- /tests/templates/widget_tag/widgets.1: -------------------------------------------------------------------------------- 1 | {% block fixed %}fixed{% endblock %} 2 | {% block var %}{{ var }}{% endblock %} 3 | {% block default %}{{ var|default:'default' }}{% endblock %} 4 | -------------------------------------------------------------------------------- /tests/templates/widget_tag/widgets.2: -------------------------------------------------------------------------------- 1 | {% extends 'widgets.1' %} 2 | {% block var %}more {{ var }}{% endblock %} 3 | -------------------------------------------------------------------------------- /tests/templates/widget_tag/widgets.3: -------------------------------------------------------------------------------- 1 | {% load sniplates %} 2 | 3 | {% block referenced %}referenced{% endblock %} 4 | 5 | {% block referencing %}{% widget ':referenced' %} referencing{% endblock %} -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | 2 | from django.template import TemplateSyntaxError 3 | from django.template.loader import get_template 4 | from django.test import SimpleTestCase 5 | 6 | from .utils import TemplateTestMixin, template_dirs 7 | 8 | 9 | @template_dirs('load_widgets') 10 | class TestLoadWidgets(TemplateTestMixin, SimpleTestCase): 11 | 12 | def test_load_widgets(self): 13 | tmpl = get_template('load_widgets') 14 | output = tmpl.render(self.ctx) 15 | 16 | self.assertEqual(output, 'success\n') 17 | 18 | def test_load_widgets_two(self): 19 | tmpl = get_template('load_widgets_two') 20 | output = tmpl.render(self.ctx) 21 | 22 | self.assertEqual(output, 'success<=>winning\n') 23 | 24 | def test_load_widgets_three(self): 25 | tmpl = get_template('load_widgets_three') 26 | output = tmpl.render(self.ctx) 27 | 28 | self.assertEqual(output, 'success<=>winning\n') 29 | 30 | 31 | @template_dirs('invalid') 32 | class TestInvalid(TemplateTestMixin, SimpleTestCase): 33 | 34 | def test_bad_name(self): 35 | tmpl = get_template('bad_name') 36 | with self.assertRaises(TemplateSyntaxError): 37 | tmpl.render(self.ctx) 38 | 39 | def test_not_loaded(self): 40 | tmpl = get_template('not_loaded') 41 | with self.assertRaises(TemplateSyntaxError): 42 | tmpl.render(self.ctx) 43 | 44 | def test_no_lib(self): 45 | tmpl = get_template('no_lib') 46 | with self.assertRaises(TemplateSyntaxError): 47 | tmpl.render(self.ctx) 48 | 49 | def test_no_widget(self): 50 | tmpl = get_template('no_widget') 51 | with self.assertRaises(TemplateSyntaxError): 52 | tmpl.render(self.ctx) 53 | 54 | 55 | @template_dirs('widget_tag') 56 | class TestWidgetTag(TemplateTestMixin, SimpleTestCase): 57 | 58 | def test_fixed(self): 59 | tmpl = get_template('fixed') 60 | output = tmpl.render(self.ctx) 61 | 62 | self.assertEqual(output, 'fixed\n') 63 | 64 | def test_var(self): 65 | tmpl = get_template('var') 66 | output = tmpl.render(self.ctx) 67 | 68 | self.assertEqual(output, 'value\n') 69 | 70 | def test_inherit(self): 71 | tmpl = get_template('inherit') 72 | output = tmpl.render(self.ctx) 73 | 74 | self.assertEqual(output, 'more value\n') 75 | 76 | def test_asvar(self): 77 | tmpl = get_template('asvar') 78 | output = tmpl.render(self.ctx) 79 | 80 | self.assertEqual(output, 'AFTER fixed') 81 | 82 | def test_empty_alias_reference(self): 83 | tmpl = get_template('alias_self') 84 | output = tmpl.render(self.ctx) 85 | 86 | self.assertEqual(output, 'referenced referencing') 87 | -------------------------------------------------------------------------------- /tests/test_filters.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from django.template.loader import get_template 4 | from django.test import SimpleTestCase 5 | 6 | from .utils import TemplateTestMixin, template_dirs 7 | 8 | 9 | @template_dirs('filters') 10 | class TestFilters(TemplateTestMixin, SimpleTestCase): 11 | 12 | def test_flatattrs(self): 13 | tmpl = get_template('flatattrs') 14 | self.ctx['a_dict'] = OrderedDict([ 15 | ('a', 'aye'), 16 | ('b', 'bee'), 17 | ('c', 'cee'), 18 | ]) 19 | output = tmpl.render(self.ctx) 20 | 21 | self.assertEqual(output, ' a="aye" b="bee" c="cee" \n') 22 | -------------------------------------------------------------------------------- /tests/test_forms.py: -------------------------------------------------------------------------------- 1 | 2 | from django.template import TemplateSyntaxError 3 | from django.template.loader import get_template 4 | from django.test import SimpleTestCase 5 | 6 | from .forms import TestForm 7 | from .utils import TemplateTestMixin, template_dirs 8 | 9 | 10 | @template_dirs('field_tag') 11 | class TestFieldTag(TemplateTestMixin, SimpleTestCase): 12 | 13 | def setUp(self): 14 | super(TestFieldTag, self).setUp() 15 | self.ctx['form'] = TestForm() 16 | 17 | def test_field_tag(self): 18 | ''' 19 | Make sure the field tag is usable. 20 | ''' 21 | tmpl = get_template('field') 22 | tmpl.render(self.ctx) 23 | 24 | def test_choices_field(self): 25 | tmpl = get_template('choices') 26 | output = tmpl.render(self.ctx) 27 | 28 | self.assertTrue('' in output) 29 | 30 | def test_choices_display(self): 31 | tmpl = get_template('choices') 32 | self.ctx['form'] = TestForm(initial={'oneof': 2}) 33 | output = tmpl.render(self.ctx) 34 | 35 | self.assertTrue('Selected: c' in output) 36 | 37 | def test_choices_multi(self): 38 | tmpl = get_template('choices_multi') 39 | selected = [1, 3] 40 | self.ctx['form'] = TestForm(initial={'many': selected}) 41 | output = tmpl.render(self.ctx) 42 | 43 | for idx, opt in enumerate('abcd'): 44 | self.assertTrue('