├── .coveragerc ├── .editorconfig ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── docs.yml │ ├── lint.yml │ ├── publish-to-live-pypi.yml │ ├── publish-to-test-pypi.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── conf.py ├── index.rst └── requirements.txt ├── requirements.in ├── sekizai ├── __init__.py ├── context.py ├── context_processors.py ├── context_processors.pyi ├── data.py ├── helpers.py ├── helpers.pyi ├── models.py └── templatetags │ ├── __init__.py │ ├── sekizai_tags.py │ └── sekizai_tags.pyi ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── requirements │ └── base.txt ├── settings.py ├── templates │ ├── basic.html │ ├── css.html │ ├── css2.html │ ├── easy_base.html │ ├── easy_inherit.html │ ├── eat.html │ ├── errors │ │ ├── failadd.html │ │ ├── failbase.html │ │ ├── failbase2.html │ │ ├── failinc.html │ │ └── failrender.html │ ├── inherit │ │ ├── base.html │ │ ├── baseinc.html │ │ ├── chain.html │ │ ├── extend.html │ │ ├── extinc.html │ │ ├── nullbase.html │ │ ├── nullext.html │ │ ├── spacechain.html │ │ ├── subvarchain.html │ │ ├── super_blocks.html │ │ └── varchain.html │ ├── named_end.html │ ├── namespaces.html │ ├── processors │ │ ├── addtoblock_namespace.html │ │ ├── addtoblock_null.html │ │ ├── namespace.html │ │ └── null.html │ ├── unique.html │ ├── variables.html │ ├── with_data.html │ └── with_data_basic.html └── test_core.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = sekizai 4 | omit = 5 | migrations/* 6 | tests/* 7 | 8 | [report] 9 | exclude_lines = 10 | pragma: no cover 11 | def __repr__ 12 | if self.debug: 13 | if settings.DEBUG 14 | raise AssertionError 15 | raise NotImplementedError 16 | if 0: 17 | if __name__ == .__main__.: 18 | ignore_errors = True 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | max_line_length = 80 13 | 14 | [*.py] 15 | max_line_length = 120 16 | quote_type = single 17 | 18 | [*.{scss,js,html}] 19 | max_line_length = 120 20 | indent_style = space 21 | quote_type = double 22 | 23 | [*.js] 24 | max_line_length = 120 25 | quote_type = single 26 | 27 | [*.rst] 28 | max_line_length = 80 29 | 30 | [*.yml] 31 | indent_size = 2 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ master ] 9 | schedule: 10 | - cron: '41 7 * * 5' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | language: [ 'python' ] 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v3 25 | 26 | # Initializes the CodeQL tools for scanning. 27 | - name: Initialize CodeQL 28 | uses: github/codeql-action/init@v1 29 | with: 30 | languages: ${{ matrix.language }} 31 | 32 | - name: Autobuild 33 | uses: github/codeql-action/autobuild@v1 34 | 35 | # ℹ️ Command-line programs to run using the OS shell. 36 | # 📚 https://git.io/JvXDl 37 | 38 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 39 | # and modify them (or add more) to build your code if your project 40 | # uses a compiled language 41 | 42 | #- run: | 43 | # make bootstrap 44 | # make release 45 | 46 | - name: Perform CodeQL Analysis 47 | uses: github/codeql-action/analyze@v1 48 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | docs: 7 | runs-on: ubuntu-latest 8 | name: docs 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: 3.9 16 | - run: python -m pip install -r docs/requirements.txt 17 | - name: Build docs 18 | run: | 19 | cd docs 20 | sphinx-build -b dirhtml -n -d build/doctrees . build/dirhtml 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | flake8: 7 | name: flake8 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: 3.9 16 | - name: Install flake8 17 | run: pip install --upgrade flake8 18 | - name: Run flake8 19 | uses: liskin/gh-problem-matcher-wrap@v1 20 | with: 21 | linters: flake8 22 | run: flake8 23 | 24 | isort: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v3 29 | - name: Set up Python 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: 3.9 33 | - run: python -m pip install isort 34 | - name: isort 35 | uses: liskin/gh-problem-matcher-wrap@v1 36 | with: 37 | linters: isort 38 | run: isort --check --diff sekizai 39 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-live-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish 🐍 📦 to pypi 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | build-n-publish: 10 | name: Build and publish 🐍 📦 to pypi 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Set up Python 3.9 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: 3.9 18 | 19 | - name: Install pypa/build 20 | run: >- 21 | python -m 22 | pip install 23 | build 24 | --user 25 | - name: Build a binary wheel and a source tarball 26 | run: >- 27 | python -m 28 | build 29 | --sdist 30 | --wheel 31 | --outdir dist/ 32 | . 33 | 34 | - name: Publish 📦 to PyPI 35 | if: startsWith(github.ref, 'refs/tags') 36 | uses: pypa/gh-action-pypi-publish@release/v1 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-test-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish 🐍 📦 to TestPyPI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-n-publish: 10 | name: Build and publish 🐍 📦 to TestPyPI 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Set up Python 3.9 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: 3.9 18 | 19 | - name: Install pypa/build 20 | run: >- 21 | python -m 22 | pip install 23 | build 24 | --user 25 | - name: Build a binary wheel and a source tarball 26 | run: >- 27 | python -m 28 | build 29 | --sdist 30 | --wheel 31 | --outdir dist/ 32 | . 33 | 34 | - name: Publish 📦 to Test PyPI 35 | uses: pypa/gh-action-pypi-publish@release/v1 36 | with: 37 | user: __token__ 38 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 39 | repository_url: https://test.pypi.org/legacy/ 40 | skip_existing: true 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CodeCov tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | unit-tests: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 12 | django-version: ['3.2', '4.2', '5.0', '5.1', '5.2'] 13 | os: [ 14 | ubuntu-latest 15 | ] 16 | exclude: 17 | - django-version: '5.0' 18 | python-version: '3.9' 19 | - django-version: '5.1' 20 | python-version: '3.9' 21 | - django-version: '5.2' 22 | python-version: '3.9' 23 | - django-version: '3.2' 24 | python-version: '3.13' 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install django~=${{ matrix.django-version }} coverage 35 | pip install -r tests/requirements/base.txt 36 | python setup.py install 37 | 38 | - name: Run coverage 39 | run: coverage run tests/settings.py 40 | 41 | - name: Upload Coverage to Codecov 42 | uses: codecov/codecov-action@v1 43 | 44 | unit-tests-future-versions: 45 | # Runs for all Django/Python versions which are not yet supported 46 | runs-on: ${{ matrix.os }} 47 | strategy: 48 | fail-fast: false 49 | matrix: 50 | python-version: ['3.12', '3.13'] 51 | django-version: [ 52 | 'https://github.com/django/django/archive/main.tar.gz' 53 | ] 54 | os: [ 55 | ubuntu-latest, 56 | ] 57 | 58 | steps: 59 | - uses: actions/checkout@v3 60 | - name: Set up Python ${{ matrix.python-version }} 61 | 62 | uses: actions/setup-python@v4 63 | with: 64 | python-version: ${{ matrix.python-version }} 65 | - name: Install dependencies 66 | run: | 67 | python -m pip install --upgrade pip 68 | pip install ${{ matrix.django-version }} coverage 69 | pip install -r tests/requirements/base.txt 70 | python setup.py install 71 | 72 | - name: Run coverage 73 | run: coverage run tests/settings.py 74 | continue-on-error: true 75 | 76 | - name: Upload Coverage to Codecov 77 | uses: codecov/codecov-action@v3 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *$py.class 3 | *.egg-info 4 | *.log 5 | *.pot 6 | .DS_Store 7 | .coverage 8 | .eggs/ 9 | .idea/ 10 | .project/ 11 | .pydevproject/ 12 | .vscode/ 13 | .settings/ 14 | .tox/ 15 | __pycache__/ 16 | build/ 17 | dist/ 18 | env/ 19 | .venv/ 20 | 21 | local.sqlite 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autofix_commit_msg: | 3 | ci: auto fixes from pre-commit hooks 4 | 5 | for more information, see https://pre-commit.ci 6 | autofix_prs: false 7 | autoupdate_commit_msg: 'ci: pre-commit autoupdate' 8 | autoupdate_schedule: monthly 9 | 10 | repos: 11 | - repo: https://github.com/asottile/pyupgrade 12 | rev: v3.20.0 13 | hooks: 14 | - id: pyupgrade 15 | args: ["--py38-plus"] 16 | 17 | - repo: https://github.com/adamchainz/django-upgrade 18 | rev: '1.25.0' 19 | hooks: 20 | - id: django-upgrade 21 | args: [--target-version, "3.2"] 22 | 23 | - repo: https://github.com/PyCQA/flake8 24 | rev: 7.2.0 25 | hooks: 26 | - id: flake8 27 | 28 | - repo: https://github.com/asottile/yesqa 29 | rev: v1.5.0 30 | hooks: 31 | - id: yesqa 32 | 33 | - repo: https://github.com/pre-commit/pre-commit-hooks 34 | rev: v5.0.0 35 | hooks: 36 | - id: check-merge-conflict 37 | - id: mixed-line-ending 38 | 39 | - repo: https://github.com/pycqa/isort 40 | rev: 6.0.1 41 | hooks: 42 | - id: isort 43 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | version: 2 4 | 5 | build: 6 | os: ubuntu-22.04 7 | tools: 8 | python: "3.11" 9 | 10 | sphinx: 11 | configuration: docs/conf.py 12 | fail_on_warning: false 13 | 14 | formats: 15 | - epub 16 | - pdf 17 | 18 | python: 19 | install: 20 | - requirements: docs/requirements.txt 21 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | unreleased 6 | ========== 7 | 8 | 4.1.0 2023-05-02 9 | ================ 10 | 11 | * Added django 4.2 and main to the test suite 12 | * Fix bug which let checks fail on templates using 13 | the with_data template tag. 14 | 15 | 4.0.0 2022-07-26 16 | ================ 17 | 18 | * Django 4 support added 19 | * Dropped python 3.7, django 2.2 and 3.1 20 | 21 | 3.0.1 2022-02-01 22 | ================ 23 | 24 | * Fix for tests in sdist tarball [#115] 25 | 26 | 3.0.0 2022-01-22 27 | ================ 28 | 29 | * Added support for Django 3.2 30 | * Drop support for python 3.5 and 3.6 31 | 32 | 2.0.0 (2020-08-26) 33 | ================== 34 | 35 | * Added support for Django 3.1 36 | * Dropped support for Python 2.7 and Python 3.4 37 | * Dropped support for Django < 2.2 38 | * Replaced pep8 with flake8 39 | * Adapted documentation 40 | 41 | 42 | 1.1.0 (2020-01-22) 43 | ================== 44 | 45 | * Added support for Django 3.0 46 | * Added support for Python 3.8 47 | * Extended test matrix 48 | * Added isort and adapted imports 49 | * Adapted code base to align with other supported addons 50 | * Adapted ``README.rst`` instructions 51 | 52 | 53 | 1.0.0 (2019-04-11) 54 | ================== 55 | 56 | * Added support for Django 1.11, 2.0, 2.1, and 2.2 57 | * Removed support for Django < 1.11 58 | 59 | 60 | 0.10.0 (2016-08-28) 61 | =================== 62 | 63 | * Added support for Django 1.10 64 | * Removed support for Python 2.6 65 | 66 | 67 | 0.9.0 (2015-12-06) 68 | ================== 69 | 70 | * Added Changelog 71 | * Added support for Django 1.9 72 | * Added support for Python 3.5 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Jonas Obrist 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Jonas Obrist nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-exclude * *.py[co] 4 | recursive-include tests *.html 5 | recursive-include tests *.py 6 | recursive-include tests *.txt 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Django Sekizai 3 | ============== 4 | 5 | |pypi| |coverage| 6 | 7 | Sekizai means "blocks" in Japanese, and that's what this app provides. A fresh 8 | look at blocks. With django-sekizai you can define placeholders where your 9 | blocks get rendered and at different places in your templates append to those 10 | blocks. This is especially useful for css and javascript. Your sub-templates can 11 | now define css and Javascript files to be included, and the css will be nicely 12 | put at the top and the Javascript to the bottom, just like you should. Also 13 | sekizai will ignore any duplicate content in a single block. 14 | 15 | There are some issue/restrictions with this implementation due to how the 16 | django template language works, but if used properly it can be very useful and 17 | it is the media handling framework for the django CMS (since version 2.2). 18 | 19 | .. note:: 20 | 21 | This project is endorsed by the `django CMS Association `_. 22 | That means that it is officially accepted by the dCA as being in line with our roadmap vision and development/plugin policy. 23 | Join us on `Slack `_. 24 | 25 | 26 | ******************************************* 27 | Contribute to this project and win rewards 28 | ******************************************* 29 | 30 | Because this is a an open-source project, we welcome everyone to 31 | `get involved in the project `_ and 32 | `receive a reward `_ for their contribution. 33 | Become part of a fantastic community and help us make django CMS the best CMS in the world. 34 | 35 | We'll be delighted to receive your 36 | feedback in the form of issues and pull requests. Before submitting your 37 | pull request, please review our `contribution guidelines 38 | `_. 39 | 40 | We're grateful to all contributors who have helped create and maintain this package. 41 | Contributors are listed at the `contributors `_ 42 | section. 43 | 44 | 45 | Documentation 46 | ============= 47 | 48 | See ``REQUIREMENTS`` in the `setup.py `_ 49 | file for additional dependencies: 50 | 51 | |python| |django| 52 | 53 | Please refer to the documentation in the docs/ directory for more information or visit our 54 | `online documentation `_. 55 | 56 | 57 | Running Tests 58 | ------------- 59 | 60 | You can run tests by executing:: 61 | 62 | virtualenv env 63 | source env/bin/activate 64 | pip install -r tests/requirements.txt 65 | python setup.py test 66 | 67 | 68 | .. |pypi| image:: https://badge.fury.io/py/django-sekizai.svg 69 | :target: http://badge.fury.io/py/django-sekizai 70 | .. |coverage| image:: https://codecov.io/gh/django-cms/django-sekizai/branch/master/graph/badge.svg 71 | :target: https://codecov.io/gh/divio/django-sekizai 72 | 73 | .. |python| image:: https://img.shields.io/badge/python-3.9+-blue.svg 74 | :target: https://pypi.org/project/django-sekizai/ 75 | .. |django| image:: https://img.shields.io/badge/django-4.2--5.2-blue.svg 76 | :target: https://www.djangoproject.com/ 77 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " pickle to make pickle files" 22 | @echo " json to make JSON files" 23 | @echo " htmlhelp to make HTML files and a HTML help project" 24 | @echo " qthelp to make HTML files and a qthelp project" 25 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 26 | @echo " changes to make an overview of all changed/added/deprecated items" 27 | @echo " linkcheck to check all external links for integrity" 28 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 29 | 30 | clean: 31 | -rm -rf $(BUILDDIR)/* 32 | 33 | html: 34 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 35 | @echo 36 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 37 | 38 | dirhtml: 39 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 40 | @echo 41 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 42 | 43 | pickle: 44 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 45 | @echo 46 | @echo "Build finished; now you can process the pickle files." 47 | 48 | json: 49 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 50 | @echo 51 | @echo "Build finished; now you can process the JSON files." 52 | 53 | htmlhelp: 54 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 55 | @echo 56 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 57 | ".hhp project file in $(BUILDDIR)/htmlhelp." 58 | 59 | qthelp: 60 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 61 | @echo 62 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 63 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 64 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-sekizai.qhcp" 65 | @echo "To view the help file:" 66 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-sekizai.qhc" 67 | 68 | latex: 69 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 70 | @echo 71 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 72 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 73 | "run these through (pdf)latex." 74 | 75 | changes: 76 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 77 | @echo 78 | @echo "The overview file is in $(BUILDDIR)/changes." 79 | 80 | linkcheck: 81 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 82 | @echo 83 | @echo "Link check complete; look for any errors in the above output " \ 84 | "or in $(BUILDDIR)/linkcheck/output.txt." 85 | 86 | doctest: 87 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 88 | @echo "Testing of doctests in the sources finished, look at the " \ 89 | "results in $(BUILDDIR)/doctest/output.txt." 90 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # django-sekizai documentation build configuration file, created by 3 | # sphinx-quickstart on Tue Jun 29 23:12:20 2010. 4 | # 5 | # This file is execfile()d with the current directory set to its containing dir. 6 | # 7 | # Note that not all possible configuration values are present in this 8 | # autogenerated file. 9 | # 10 | # All configuration values have a default; values that are commented out 11 | # serve to show the default. 12 | 13 | 14 | # If extensions (or modules to document with autodoc) are in another directory, 15 | # add these directories to sys.path here. If the directory is relative to the 16 | # documentation root, use os.path.abspath to make it absolute, like shown here. 17 | #sys.path.append(os.path.abspath('.')) 18 | 19 | # -- General configuration ----------------------------------------------------- 20 | 21 | # Add any Sphinx extension module names here, as strings. They can be extensions 22 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 23 | extensions = [] 24 | 25 | # Add any paths that contain templates here, relative to this directory. 26 | templates_path = ['_templates'] 27 | 28 | # The suffix of source filenames. 29 | source_suffix = '.rst' 30 | 31 | # The encoding of source files. 32 | #source_encoding = 'utf-8' 33 | 34 | # The master toctree document. 35 | master_doc = 'index' 36 | 37 | # General information about the project. 38 | project = 'django-sekizai' 39 | copyright = '2010, Jonas Obrist' 40 | 41 | # The version info for the project you're documenting, acts as replacement for 42 | # |version| and |release|, also used in various other places throughout the 43 | # built documents. 44 | # 45 | # The short X.Y version. 46 | version = '0.6' 47 | # The full version, including alpha/beta/rc tags. 48 | release = '0.6.1' 49 | 50 | # The language for content autogenerated by Sphinx. Refer to documentation 51 | # for a list of supported languages. 52 | #language = None 53 | 54 | # There are two options for replacing |today|: either, you set today to some 55 | # non-false value, then it is used: 56 | #today = '' 57 | # Else, today_fmt is used as the format for a strftime call. 58 | #today_fmt = '%B %d, %Y' 59 | 60 | # List of documents that shouldn't be included in the build. 61 | #unused_docs = [] 62 | 63 | # List of directories, relative to source directory, that shouldn't be searched 64 | # for source files. 65 | exclude_trees = ['_build'] 66 | 67 | # The reST default role (used for this markup: `text`) to use for all documents. 68 | #default_role = None 69 | 70 | # If true, '()' will be appended to :func: etc. cross-reference text. 71 | #add_function_parentheses = True 72 | 73 | # If true, the current module name will be prepended to all description 74 | # unit titles (such as .. function::). 75 | #add_module_names = True 76 | 77 | # If true, sectionauthor and moduleauthor directives will be shown in the 78 | # output. They are ignored by default. 79 | #show_authors = False 80 | 81 | # The name of the Pygments (syntax highlighting) style to use. 82 | pygments_style = 'sphinx' 83 | 84 | # A list of ignored prefixes for module index sorting. 85 | #modindex_common_prefix = [] 86 | 87 | 88 | # -- Options for HTML output --------------------------------------------------- 89 | 90 | # The theme to use for HTML and HTML Help pages. Major themes that come with 91 | # Sphinx are currently 'default' and 'sphinxdoc'. 92 | html_theme = 'default' 93 | 94 | # Theme options are theme-specific and customize the look and feel of a theme 95 | # further. For a list of options available for each theme, see the 96 | # documentation. 97 | #html_theme_options = {} 98 | 99 | # Add any paths that contain custom themes here, relative to this directory. 100 | #html_theme_path = [] 101 | 102 | # The name for this set of Sphinx documents. If None, it defaults to 103 | # " v documentation". 104 | #html_title = None 105 | 106 | # A shorter title for the navigation bar. Default is the same as html_title. 107 | #html_short_title = None 108 | 109 | # The name of an image file (relative to this directory) to place at the top 110 | # of the sidebar. 111 | #html_logo = None 112 | 113 | # The name of an image file (within the static path) to use as favicon of the 114 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 115 | # pixels large. 116 | #html_favicon = None 117 | 118 | # Add any paths that contain custom static files (such as style sheets) here, 119 | # relative to this directory. They are copied after the builtin static files, 120 | # so a file named "default.css" will overwrite the builtin "default.css". 121 | html_static_path = ['_static'] 122 | 123 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 124 | # using the given strftime format. 125 | #html_last_updated_fmt = '%b %d, %Y' 126 | 127 | # If true, SmartyPants will be used to convert quotes and dashes to 128 | # typographically correct entities. 129 | #html_use_smartypants = True 130 | 131 | # Custom sidebar templates, maps document names to template names. 132 | #html_sidebars = {} 133 | 134 | # Additional templates that should be rendered to pages, maps page names to 135 | # template names. 136 | #html_additional_pages = {} 137 | 138 | # If false, no module index is generated. 139 | #html_use_modindex = True 140 | 141 | # If false, no index is generated. 142 | #html_use_index = True 143 | 144 | # If true, the index is split into individual pages for each letter. 145 | #html_split_index = False 146 | 147 | # If true, links to the reST sources are added to the pages. 148 | #html_show_sourcelink = True 149 | 150 | # If true, an OpenSearch description file will be output, and all pages will 151 | # contain a tag referring to it. The value of this option must be the 152 | # base URL from which the finished HTML is served. 153 | #html_use_opensearch = '' 154 | 155 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 156 | #html_file_suffix = '' 157 | 158 | # Output file base name for HTML help builder. 159 | htmlhelp_basename = 'django-sekizaidoc' 160 | 161 | 162 | # -- Options for LaTeX output -------------------------------------------------- 163 | 164 | # The paper size ('letter' or 'a4'). 165 | #latex_paper_size = 'letter' 166 | 167 | # The font size ('10pt', '11pt' or '12pt'). 168 | #latex_font_size = '10pt' 169 | 170 | # Grouping the document tree into LaTeX files. List of tuples 171 | # (source start file, target name, title, author, documentclass [howto/manual]). 172 | latex_documents = [ 173 | ('index', 'django-sekizai.tex', 'django-sekizai Documentation', 174 | 'Jonas Obrist', 'manual'), 175 | ] 176 | 177 | # The name of an image file (relative to this directory) to place at the top of 178 | # the title page. 179 | #latex_logo = None 180 | 181 | # For "manual" documents, if this is true, then toplevel headings are parts, 182 | # not chapters. 183 | #latex_use_parts = False 184 | 185 | # Additional stuff for the LaTeX preamble. 186 | #latex_preamble = '' 187 | 188 | # Documents to append as an appendix to all manuals. 189 | #latex_appendices = [] 190 | 191 | # If false, no module index is generated. 192 | #latex_use_modindex = True 193 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-sekizai documentation master file, created by 2 | sphinx-quickstart on Tue Jun 29 23:12:20 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ########################################## 7 | Welcome to django-sekizai's documentation! 8 | ########################################## 9 | 10 | 11 | .. note:: 12 | 13 | If you get an error when using django-sekizai that starts with 14 | **Invalid block tag:**, please read :ref:`restrictions`. 15 | 16 | 17 | ***** 18 | About 19 | ***** 20 | 21 | Sekizai means "blocks" in Japanese, and that's what this app provides. A fresh 22 | look at blocks. With django-sekizai you can define placeholders where your 23 | blocks get rendered and at different places in your templates append to those 24 | blocks. This is especially useful for css and javascript. Your sub-templates can 25 | now define css and Javascript files to be included, and the css will be nicely 26 | put at the top and the Javascript to the bottom, just like you should. Also 27 | sekizai will ignore any duplicate content in a single block. 28 | 29 | 30 | ***** 31 | Usage 32 | ***** 33 | 34 | Configuration 35 | ============= 36 | 37 | In order to get started with django-sekizai, you'll need to do the following 38 | steps: 39 | 40 | * Put 'sekizai' into your ``INSTALLED_APPS`` setting. 41 | * Add ``sekizai.context_processors.sekizai`` to your 42 | ``TEMPLATES['OPTIONS']['context_processors']`` setting and use 43 | ``django.template.RequestContext`` when rendering your templates. 44 | 45 | 46 | Template Tag Reference 47 | ====================== 48 | 49 | 50 | .. highlight:: html+django 51 | 52 | .. note:: All sekizai template tags require the ``sekizai_tags`` template tag 53 | library to be loaded. 54 | 55 | 56 | Handling code snippets 57 | ---------------------- 58 | 59 | .. versionadded:: 0.7 60 | The ``strip`` flag was added. 61 | 62 | Sekizai uses ``render_block`` and ``addtoblock`` to handle unique code snippets. 63 | Define your blocks using ``{% render_block %}`` and add data to that 64 | block using ``{% addtoblock [strip] %}...{% endaddtoblock %}``. If the 65 | strip flag is set, leading and trailing whitespace will be removed. 66 | 67 | Example Template:: 68 | 69 | {% load sekizai_tags %} 70 | 71 | 72 | 73 | {% block css %}{% endblock %} 74 | {% render_block "css" %} 75 | 76 | 77 | Your content comes here. 78 | Maybe you want to throw in some css: 79 | {% addtoblock "css" %} 80 | 81 | {% endaddtoblock %} 82 | Some more content here. 83 | {% addtoblock "js" %} 84 | 87 | {% endaddtoblock %} 88 | And even more content. 89 | {% block js %}{% endblock %} 90 | {% render_block "js" %} 91 | 92 | 93 | 94 | Above example would roughly render like this:: 95 | 96 | 97 | 98 | 99 | 100 | 101 | Your content comes here. 102 | Maybe you want to throw in some css: 103 | Some more content here. 104 | And even more content. 105 | 108 | 109 | 110 | 111 | .. note:: 112 | 113 | It's recommended to have all ``render_block`` tags in your base template, the one that gets extended by all your 114 | other templates. 115 | 116 | .. _restrictions: 117 | 118 | Restrictions 119 | ------------ 120 | 121 | .. warning:: 122 | 123 | ``{% render_block %}`` tags **must not** be placed inside a template tag block (a template tag which has an 124 | end tag, such as ``{% block %}...{% endblock %}`` or ``{% if %}...{% endif %}``). 125 | 126 | .. warning:: 127 | 128 | ``{% render_block %}`` tags **must not** be in an included template! 129 | 130 | .. warning:: 131 | 132 | If the ``{% addtoblock %}`` tag is used in an **extending** template, the tags **must** be 133 | placed within ``{% block %}...{% endblock %}`` tags. If this block extension, where ``{% addtoblock %}`` 134 | lies, is actually overridden in a child template (i.e by a same-name block which doesn't call ``block.super``), 135 | then this ``{% addtoblock %}`` will be ignored. 136 | 137 | .. warning:: 138 | 139 | ``{% addtoblock %}`` tags **must not** be used in a template included with ``only`` option! 140 | 141 | Handling data 142 | ------------- 143 | 144 | Sometimes you might not want to use code snippets but rather just add a value to 145 | a list. For this purpose there are the 146 | ``{% with_data as %}...{% end_with_data %}`` and 147 | ``{% add_data %}`` template tags. 148 | 149 | Example:: 150 | 151 | {% load sekizai_tags %} 152 | 153 | 154 | 155 | {% with_data "css-data" as stylesheets %} 156 | {% for stylesheet in stylesheets %} 157 | 158 | {% endfor %} 159 | {% end_with_data %} 160 | 161 | 162 | Your content comes here. 163 | Maybe you want to throw in some css: 164 | {% add_data "css-data" "css/stylesheet.css" %} 165 | Some more content here. 166 | 167 | 168 | 169 | Above example would roughly render like this:: 170 | 171 | 172 | 173 | 174 | 175 | 176 | Your content comes here. 177 | Maybe you want to throw in some css: 178 | Some more content here. 179 | And even more content. 180 | 181 | 182 | 183 | .. warning:: 184 | 185 | The restrictions for ``{% render_block %}`` also apply to ``{% with_data %}``, see above. 186 | 187 | The restrictions for ``{% addtoblock %}`` also apply to ``{% add_data %}``, see above. 188 | 189 | 190 | Sekizai data is unique 191 | ---------------------- 192 | 193 | 194 | All data in sekizai is enforced to be unique within its block namespace. This 195 | is because the main purpose of sekizai is to handle javascript and css 196 | dependencies in templates. 197 | 198 | A simple example using ``addtoblock`` and ``render_block`` would be:: 199 | 200 | {% load sekizai_tags %} 201 | 202 | {% addtoblock "js" %} 203 | 204 | {% endaddtoblock %} 205 | 206 | {% addtoblock "js" %} 207 | 210 | {% endaddtoblock %} 211 | 212 | {% addtoblock "js" %} 213 | 214 | {% endaddtoblock %} 215 | 216 | {% addtoblock "js" %} 217 | 220 | {% endaddtoblock %} 221 | 222 | {% render_block "js" %} 223 | 224 | Above template would roughly render to:: 225 | 226 | 227 | 230 | 233 | 234 | 235 | .. versionadded:: 0.5 236 | 237 | Processing sekizai data 238 | ----------------------- 239 | 240 | Because of the restrictions of the ``{% render_block %}`` tag, it is not possible 241 | to use sekizai with libraries such as django-compressor directly. For that 242 | reason, sekizai added postprocessing capabilities to ``render_block`` in 243 | version 0.5. 244 | 245 | Postprocessors are callable Python objects (usually functions) that get the 246 | render context, the data in a sekizai namespace and the name of the namespace 247 | passed as arguments and should return a string. 248 | 249 | An example for a processor that uses the Django builtin spaceless functionality 250 | would be: 251 | 252 | .. code-block:: python 253 | 254 | def spaceless_post_processor(context, data, namespace): 255 | from django.utils.html import strip_spaces_between_tags 256 | return strip_spaces_between_tags(data) 257 | 258 | 259 | To use this post processor you have to tell ``render_block`` where it's 260 | located. If above code sample lives in the Python module 261 | ``myapp.sekizai_processors`` you could use it like this:: 262 | 263 | ... 264 | {% render_block "js" postprocessor "myapp.sekizai_processors.spaceless_post_processor" %} 265 | ... 266 | 267 | 268 | It's also possible to pre-process data in ``{% addtoblock %}`` like this:: 269 | 270 | {% addtoblock "css" preprocessor "myapp.sekizai_processors.processor" %} 271 | 272 | 273 | 274 | ******* 275 | Helpers 276 | ******* 277 | 278 | 279 | :mod:`sekizai.helpers` 280 | ====================== 281 | 282 | 283 | .. function:: get_namespaces(template) 284 | 285 | Returns a list of all sekizai namespaces found in ``template``, which should 286 | be the name of a template. This method also checks extended templates. 287 | 288 | 289 | .. function:: validate_template(template, namespaces) 290 | 291 | Returns ``True`` if all namespaces given are found in the template given. 292 | Useful to check that the namespaces required by your application are 293 | available, so you can failfast if they're not. 294 | 295 | 296 | ******* 297 | Example 298 | ******* 299 | 300 | .. highlight:: html+django 301 | 302 | A full example on how to use django-sekizai and when. 303 | 304 | Let's assume you have a website, where all templates extend base.html, which 305 | just contains your basic HTML structure. Now you also have a small template 306 | which gets included on some pages. This template needs to load a javascript 307 | library and execute some specific javascript code. 308 | 309 | Your ``base.html`` might look like this:: 310 | 311 | {% load sekizai_tags %} 312 | 313 | 314 | 315 | 316 | Your website 317 | 318 | 319 | 320 | {% render_block "css" %} 321 | 322 | 323 | {% block "content" %} 324 | {% endblock %} 325 | 326 | {% render_block "js" %} 327 | 328 | 329 | 330 | As you can see, we load ``sekizai_tags`` at the very beginning. We have two 331 | sekizai namespaces: "css" and "js". The "css" namespace is rendered in the head 332 | right after the base css files, the "js" namespace is rendered at the very 333 | bottom of the body, right after we load jQuery. 334 | 335 | 336 | Now to our included template. We assume there's a context variable called 337 | ``userid`` which will be used with the javascript code. 338 | 339 | Your template (``inc.html``) might look like this:: 340 | 341 | {% load sekizai_tags %} 342 |
343 |
    344 |
    345 | 346 | {% addtoblock "js" %} 347 | 348 | {% endaddtoblock %} 349 | 350 | {% addtoblock "js" %} 351 | 356 | {% endaddtoblock %} 357 | 358 | The important thing to notice here is that we split the javascript into two 359 | ``addtoblock`` blocks. Like this, the library 'mylib.js' is only included once, 360 | and the userid specific code will be included once per userid. 361 | 362 | 363 | Now to put it all together let's assume we render a third template with 364 | ``[1, 2, 3]`` as ``my_userids`` variable. 365 | 366 | The third template looks like this:: 367 | 368 | {% extends "base.html" %} 369 | 370 | {% block "content" %} 371 | {% for userid in my_userids %} 372 | {% include "inc.html" %} 373 | {% endfor %} 374 | {% endblock %} 375 | 376 | And here's the rendered template:: 377 | 378 | 379 | 380 | 381 | 382 | 383 | Your website 384 | 385 | 386 | 387 | 388 | 389 |
    390 |
      391 |
      392 |
      393 |
        394 |
        395 |
        396 |
          397 |
          398 | 399 | 400 | 405 | 410 | 415 | 416 | 417 | 418 | 419 | ********* 420 | Changelog 421 | ********* 422 | 423 | See `CHANGELOG.rst `_ for a full list. 424 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | django<4.3 2 | MarkupSafe 3 | Pygments 4 | restructuredtext_lint 5 | sphinx 6 | sphinxcontrib-spelling 7 | sphinx-autobuild 8 | sphinxcontrib-inlinesyntaxhighlight 9 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | bump2version 2 | Django>3.2 3 | django-classy-tags>=3.0 4 | pip-tools 5 | pre-commit 6 | wheel 7 | -------------------------------------------------------------------------------- /sekizai/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '4.1.0' 2 | -------------------------------------------------------------------------------- /sekizai/context.py: -------------------------------------------------------------------------------- 1 | from django.template import Context 2 | 3 | from sekizai.context_processors import sekizai 4 | 5 | 6 | class SekizaiContext(Context): 7 | """ 8 | An alternative context to be used instead of RequestContext in places where 9 | no request is available. 10 | """ 11 | def __init__(self, *args, **kwargs): 12 | super().__init__(*args, **kwargs) 13 | self.update(sekizai()) 14 | -------------------------------------------------------------------------------- /sekizai/context_processors.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from sekizai.data import UniqueSequence 4 | from sekizai.helpers import get_varname 5 | 6 | 7 | def sekizai(request=None): 8 | """ 9 | Simple context processor which makes sure that the SekizaiDictionary is 10 | available in all templates. 11 | """ 12 | return {get_varname(): defaultdict(UniqueSequence)} 13 | -------------------------------------------------------------------------------- /sekizai/context_processors.pyi: -------------------------------------------------------------------------------- 1 | from typing import Dict, Union 2 | 3 | from django.http import HttpRequest 4 | 5 | from sekizai.data import UniqueSequence 6 | 7 | 8 | def sekizai(request: Union[None, HttpRequest]) -> Dict[str, UniqueSequence]: ... 9 | -------------------------------------------------------------------------------- /sekizai/data.py: -------------------------------------------------------------------------------- 1 | from collections.abc import MutableSequence 2 | 3 | 4 | class UniqueSequence(MutableSequence): 5 | def __init__(self): 6 | self.data = [] 7 | 8 | def __contains__(self, item): 9 | return item in self.data 10 | 11 | def __iter__(self): 12 | return iter(self.data) 13 | 14 | def __getitem__(self, item): 15 | return self.data[item] 16 | 17 | def __setitem__(self, key, value): 18 | self.data[key] = value 19 | 20 | def __delitem__(self, key): 21 | del self.data[key] 22 | 23 | def __len__(self): 24 | return len(self.data) 25 | 26 | def insert(self, index, value): 27 | if value not in self: 28 | self.data.insert(index, value) 29 | -------------------------------------------------------------------------------- /sekizai/helpers.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.template.base import Template, Variable, VariableNode 3 | from django.template.context import Context 4 | from django.template.loader import get_template 5 | from django.template.loader_tags import BlockNode, ExtendsNode 6 | 7 | 8 | def _get_nodelist(tpl): 9 | if isinstance(tpl, Template): 10 | return tpl.nodelist 11 | else: 12 | return tpl.template.nodelist 13 | 14 | 15 | def is_variable_extend_node(node): 16 | if hasattr(node, 'parent_name_expr') and node.parent_name_expr: 17 | return True 18 | if hasattr(node, 'parent_name') and hasattr(node.parent_name, 'filters'): 19 | if (node.parent_name.filters or 20 | isinstance(node.parent_name.var, Variable)): 21 | return True 22 | return False 23 | 24 | 25 | def get_context(): 26 | context = Context() 27 | context.template = Template('') 28 | return context 29 | 30 | 31 | def _extend_blocks(extend_node, blocks): 32 | """ 33 | Extends the dictionary `blocks` with *new* blocks in the parent node 34 | (recursive) 35 | """ 36 | # we don't support variable extensions 37 | if is_variable_extend_node(extend_node): 38 | return 39 | parent = extend_node.get_parent(get_context()) 40 | # Search for new blocks 41 | for node in _get_nodelist(parent).get_nodes_by_type(BlockNode): 42 | if node.name not in blocks: 43 | blocks[node.name] = node 44 | else: 45 | # set this node as the super node (for {{ block.super }}) 46 | block = blocks[node.name] 47 | seen_supers = [] 48 | while (hasattr(block.super, 'nodelist') and 49 | block.super not in seen_supers): 50 | seen_supers.append(block.super) 51 | block = block.super 52 | block.super = node 53 | # search for further ExtendsNodes 54 | for node in _get_nodelist(parent).get_nodes_by_type(ExtendsNode): 55 | _extend_blocks(node, blocks) 56 | break 57 | 58 | 59 | def _extend_nodelist(extend_node): 60 | """ 61 | Returns a list of namespaces found in the parent template(s) of this 62 | ExtendsNode 63 | """ 64 | # we don't support variable extensions (1.3 way) 65 | if is_variable_extend_node(extend_node): 66 | return [] 67 | blocks = extend_node.blocks 68 | _extend_blocks(extend_node, blocks) 69 | found = [] 70 | 71 | for block in blocks.values(): 72 | found += _scan_namespaces(block.nodelist, block) 73 | 74 | parent_template = extend_node.get_parent(get_context()) 75 | # if this is the topmost template, check for namespaces outside of blocks 76 | if not _get_nodelist(parent_template).get_nodes_by_type(ExtendsNode): 77 | found += _scan_namespaces( 78 | _get_nodelist(parent_template), 79 | None 80 | ) 81 | else: 82 | found += _scan_namespaces( 83 | _get_nodelist(parent_template), 84 | extend_node 85 | ) 86 | return found 87 | 88 | 89 | def _scan_namespaces(nodelist, current_block=None): 90 | from sekizai.templatetags.sekizai_tags import RenderBlock, WithData 91 | found = [] 92 | 93 | for node in nodelist: 94 | # check if this is RenderBlock node 95 | if isinstance(node, (RenderBlock, WithData)): 96 | # resolve it's name against a dummy context 97 | found.append(node.kwargs['name'].resolve({})) 98 | found += _scan_namespaces(node.blocks['nodelist'], node) 99 | # handle {% extends ... %} tags if check_inheritance is True 100 | elif isinstance(node, ExtendsNode): 101 | found += _extend_nodelist(node) 102 | # in block nodes we have to scan for super blocks 103 | elif isinstance(node, VariableNode) and current_block: 104 | if node.filter_expression.token == 'block.super': 105 | if hasattr(current_block.super, 'nodelist'): 106 | found += _scan_namespaces( 107 | current_block.super.nodelist, 108 | current_block.super 109 | ) 110 | return found 111 | 112 | 113 | def get_namespaces(template): 114 | compiled_template = get_template(template) 115 | return _scan_namespaces(_get_nodelist(compiled_template)) 116 | 117 | 118 | def validate_template(template, namespaces): 119 | """ 120 | Validates that a template (or it's parents if check_inheritance is True) 121 | contain all given namespaces 122 | """ 123 | if getattr(settings, 'SEKIZAI_IGNORE_VALIDATION', False): 124 | return True 125 | found = get_namespaces(template) 126 | for namespace in namespaces: 127 | if namespace not in found: 128 | return False 129 | return True 130 | 131 | 132 | def get_varname(): 133 | return getattr(settings, 'SEKIZAI_VARNAME', 'SEKIZAI_CONTENT_HOLDER') 134 | 135 | 136 | class Watcher: 137 | """ 138 | Watches a context for changes to the sekizai data, so it can be replayed 139 | later. This is useful for caching. 140 | 141 | NOTE: This class assumes you ONLY ADD, NEVER REMOVE data from the context! 142 | """ 143 | def __init__(self, context): 144 | self.context = context 145 | self.frozen = { 146 | key: list(value) for key, value in self.data.items() 147 | } 148 | 149 | @property 150 | def data(self): 151 | return self.context.get(get_varname(), {}) 152 | 153 | def get_changes(self): 154 | sfrozen = set(self.frozen) 155 | sdata = set(self.data) 156 | new_keys = sfrozen ^ sdata 157 | changes = {} 158 | for key in new_keys: 159 | changes[key] = list(self.data[key]) 160 | shared_keys = sfrozen & sdata 161 | for key in shared_keys: 162 | old_set = set(self.frozen[key]) 163 | new_values = [ 164 | item for item in self.data[key] if item not in old_set 165 | ] 166 | changes[key] = new_values 167 | return changes 168 | -------------------------------------------------------------------------------- /sekizai/helpers.pyi: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Union 2 | 3 | from django.template.base import Node, NodeList, Template 4 | from django.template.context import Context 5 | 6 | 7 | def _get_nodelist(tpl: Template) -> NodeList: ... 8 | 9 | def is_variable_extend_node(node: Node) -> bool: ... 10 | 11 | def get_context() -> Context: ... 12 | 13 | def _extend_blocks(extend_node: Node, blocks: Dict[str, Node]): ... 14 | 15 | def _extend_nodelist(extend_node: Node) -> List[str]: ... 16 | 17 | def _scan_namespaces(nodelist: NodeList, current_block: Union[None, Node]) -> List[str]: ... 18 | 19 | def get_namespaces(template: str) -> List[str]: ... 20 | 21 | def validate_template(template: str, namespaces: List[str]) -> bool: ... 22 | 23 | def get_varname() -> str: ... 24 | 25 | class Watcher: 26 | def __init__(self, context: Context) -> None: ... 27 | 28 | @property 29 | def data(self) -> dict: ... 30 | 31 | def get_changes(self) -> dict: ... 32 | -------------------------------------------------------------------------------- /sekizai/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/django-sekizai/28812ea44bbc1a5848ac200abc23140ae9a719cd/sekizai/models.py -------------------------------------------------------------------------------- /sekizai/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/django-sekizai/28812ea44bbc1a5848ac200abc23140ae9a719cd/sekizai/templatetags/__init__.py -------------------------------------------------------------------------------- /sekizai/templatetags/sekizai_tags.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | from django import template 4 | 5 | from classytags.arguments import Argument, Flag 6 | from classytags.core import Options, Tag 7 | from classytags.parser import Parser 8 | 9 | from sekizai.helpers import get_varname 10 | 11 | 12 | register = template.Library() 13 | 14 | 15 | def validate_context(context): 16 | """ 17 | Validates a given context. 18 | 19 | Returns True if the context is valid. 20 | 21 | Returns False if the context is invalid but the error should be silently 22 | ignored. 23 | 24 | Raises a TemplateSyntaxError if the context is invalid and we're in debug 25 | mode. 26 | """ 27 | try: 28 | template_debug = context.template.engine.debug 29 | except AttributeError: 30 | # Get the default engine debug value 31 | template_debug = template.Engine.get_default().debug 32 | 33 | if get_varname() in context: 34 | return True 35 | if not template_debug: 36 | return False 37 | raise template.TemplateSyntaxError( 38 | "You must enable the 'sekizai.context_processors.sekizai' template " 39 | "context processor or use 'sekizai.context.SekizaiContext' to " 40 | "render your templates." 41 | ) 42 | 43 | 44 | def import_processor(import_path): 45 | if '.' not in import_path: 46 | raise TypeError("Import paths must contain at least one '.'") 47 | module_name, object_name = import_path.rsplit('.', 1) 48 | module = import_module(module_name) 49 | return getattr(module, object_name) 50 | 51 | 52 | class SekizaiParser(Parser): 53 | def parse_blocks(self): 54 | super().parse_blocks() 55 | self.blocks['nodelist'] = self.parser.parse() 56 | 57 | 58 | class AddtoblockParser(Parser): 59 | def parse_blocks(self): 60 | name = self.kwargs['name'].var.token 61 | self.blocks['nodelist'] = self.parser.parse( 62 | ('endaddtoblock', 'endaddtoblock %s' % name) 63 | ) 64 | self.parser.delete_first_token() 65 | 66 | 67 | class SekizaiTag(Tag): 68 | def render(self, context): 69 | if validate_context(context): 70 | return super().render(context) 71 | return '' 72 | 73 | 74 | class RenderBlock(Tag): 75 | name = 'render_block' 76 | 77 | options = Options( 78 | Argument('name'), 79 | 'postprocessor', 80 | Argument('postprocessor', required=False, default=None, resolve=False), 81 | parser_class=SekizaiParser, 82 | ) 83 | 84 | def render_tag(self, context, name, postprocessor, nodelist): 85 | if not validate_context(context): 86 | return nodelist.render(context) 87 | rendered_contents = nodelist.render(context) 88 | varname = get_varname() 89 | data = '\n'.join(context[varname][name]) 90 | if postprocessor: 91 | func = import_processor(postprocessor) 92 | data = func(context, data, name) 93 | return f'{data}\n{rendered_contents}' 94 | 95 | 96 | register.tag('render_block', RenderBlock) 97 | 98 | 99 | class AddData(SekizaiTag): 100 | name = 'add_data' 101 | 102 | options = Options( 103 | Argument('key'), 104 | Argument('value'), 105 | ) 106 | 107 | def render_tag(self, context, key, value): 108 | varname = get_varname() 109 | context[varname][key].append(value) 110 | return '' 111 | 112 | 113 | register.tag('add_data', AddData) 114 | 115 | 116 | class WithData(SekizaiTag): 117 | name = 'with_data' 118 | 119 | options = Options( 120 | Argument('name'), 121 | 'as', 122 | Argument('variable', resolve=False), 123 | blocks=[ 124 | ('end_with_data', 'inner_nodelist'), 125 | ], 126 | parser_class=SekizaiParser, 127 | ) 128 | 129 | def render_tag(self, context, name, variable, inner_nodelist, nodelist): 130 | rendered_contents = nodelist.render(context) 131 | varname = get_varname() 132 | data = context[varname][name] 133 | context.push() 134 | context[variable] = data 135 | inner_contents = inner_nodelist.render(context) 136 | context.pop() 137 | return f'{inner_contents}\n{rendered_contents}' 138 | 139 | 140 | register.tag('with_data', WithData) 141 | 142 | 143 | class Addtoblock(SekizaiTag): 144 | name = 'addtoblock' 145 | 146 | options = Options( 147 | Argument('name'), 148 | Flag('strip', default=False, true_values=['strip']), 149 | 'preprocessor', 150 | Argument('preprocessor', required=False, default=None, resolve=False), 151 | parser_class=AddtoblockParser, 152 | ) 153 | 154 | def render_tag(self, context, name, strip, preprocessor, nodelist): 155 | rendered_contents = nodelist.render(context) 156 | if strip: 157 | rendered_contents = rendered_contents.strip() 158 | if preprocessor: 159 | func = import_processor(preprocessor) 160 | rendered_contents = func(context, rendered_contents, name) 161 | varname = get_varname() 162 | context[varname][name].append(rendered_contents) 163 | return "" 164 | 165 | 166 | register.tag('addtoblock', Addtoblock) 167 | -------------------------------------------------------------------------------- /sekizai/templatetags/sekizai_tags.pyi: -------------------------------------------------------------------------------- 1 | from types import ModuleType 2 | 3 | from django.template import Context 4 | 5 | 6 | def validate_context(context: Context) -> bool: ... 7 | 8 | def import_processor(import_path: str) -> ModuleType: ... 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 4.1.0 3 | commit = True 4 | tag = False 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:sekizai/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bumpversion:file:CHANGELOG.rst] 15 | search = 16 | unreleased 17 | ========== 18 | replace = 19 | unreleased 20 | ========== 21 | 22 | {new_version} {utcnow:%%Y-%%m-%%d} 23 | ================ 24 | 25 | [flake8] 26 | max-line-length = 119 27 | exclude = 28 | *.egg-info, 29 | .eggs, 30 | .env, 31 | .git, 32 | .settings, 33 | .tox, 34 | .venv, 35 | build, 36 | data, 37 | dist, 38 | docs/conf.py, 39 | *migrations*, 40 | requirements, 41 | tmp 42 | 43 | [isort] 44 | line_length = 119 45 | skip = manage.py, *migrations*, .tox, .eggs, data, .env, .venv 46 | include_trailing_comma = true 47 | multi_line_output = 5 48 | lines_after_imports = 2 49 | default_section = THIRDPARTY 50 | sections = FUTURE, STDLIB, DJANGO, THIRDPARTY, FIRSTPARTY, LOCALFOLDER 51 | known_first_party = sekizai 52 | known_django = django 53 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from pathlib import Path 3 | 4 | from setuptools import find_packages, setup 5 | 6 | 7 | REQUIREMENTS = [ 8 | 'django>=3.2', 9 | 'django-classy-tags>=3.0', 10 | ] 11 | 12 | 13 | CLASSIFIERS = [ 14 | 'Development Status :: 5 - Production/Stable', 15 | 'Environment :: Web Environment', 16 | 'Intended Audience :: Developers', 17 | 'License :: OSI Approved :: BSD License', 18 | 'Operating System :: OS Independent', 19 | 'Programming Language :: Python', 20 | 'Programming Language :: Python :: 3 :: Only', 21 | 'Programming Language :: Python :: 3.8', 22 | 'Programming Language :: Python :: 3.9', 23 | 'Programming Language :: Python :: 3.10', 24 | 'Programming Language :: Python :: 3.11', 25 | 'Framework :: Django', 26 | 'Framework :: Django :: 3.2', 27 | 'Framework :: Django :: 4.0', 28 | 'Framework :: Django :: 4.1', 29 | 'Framework :: Django :: 4.2', 30 | 'Topic :: Internet :: WWW/HTTP', 31 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 32 | 'Topic :: Software Development', 33 | 'Topic :: Software Development :: Libraries', 34 | ] 35 | 36 | this_directory = Path(__file__).parent 37 | long_description = (this_directory / "README.rst").read_text() 38 | 39 | setup( 40 | name='django-sekizai', 41 | version='4.1.0', 42 | author='Jonas Obrist', 43 | author_email='ojiidotch@gmail.com', 44 | maintainer='Django CMS Association and contributors', 45 | maintainer_email='info@django-cms.org', 46 | url='https://github.com/django-cms/django-sekizai', 47 | license='BSD-3-Clause', 48 | description='Django Sekizai', 49 | long_description=long_description, 50 | long_description_content_type='text/x-rst', 51 | packages=find_packages(exclude=['tests']), 52 | python_requires='>=3.8', 53 | include_package_data=True, 54 | zip_safe=False, 55 | install_requires=REQUIREMENTS, 56 | classifiers=CLASSIFIERS, 57 | test_suite='tests.settings.run', 58 | ) 59 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/django-sekizai/28812ea44bbc1a5848ac200abc23140ae9a719cd/tests/__init__.py -------------------------------------------------------------------------------- /tests/requirements/base.txt: -------------------------------------------------------------------------------- 1 | # other requirements 2 | tox 3 | coverage 4 | flake8 5 | setuptools 6 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | urlpatterns = [] 6 | 7 | DATABASES = { 8 | 'default': { 9 | 'ENGINE': 'django.db.backends.sqlite3', 10 | 'NAME': ':memory:' 11 | } 12 | } 13 | 14 | INSTALLED_APPS = [ 15 | 'sekizai', 16 | 'tests', 17 | ] 18 | 19 | ROOT_URLCONF = 'tests.settings' 20 | 21 | TEMPLATES = [ 22 | { 23 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 24 | 'DIRS': [ 25 | os.path.join(os.path.dirname(__file__), 'templates') 26 | ], 27 | 'OPTIONS': { 28 | 'context_processors': ['sekizai.context_processors.sekizai'], 29 | 'debug': True, 30 | }, 31 | }, 32 | ] 33 | 34 | 35 | def runtests(): 36 | from django import setup 37 | from django.conf import settings 38 | from django.test.utils import get_runner 39 | settings.configure( 40 | INSTALLED_APPS=INSTALLED_APPS, 41 | ROOT_URLCONF=ROOT_URLCONF, 42 | DATABASES=DATABASES, 43 | TEST_RUNNER='django.test.runner.DiscoverRunner', 44 | TEMPLATES=TEMPLATES, 45 | ) 46 | setup() 47 | 48 | # Run the test suite, including the extra validation tests. 49 | TestRunner = get_runner(settings) 50 | 51 | test_runner = TestRunner(verbosity=1, interactive=False, failfast=False) 52 | failures = test_runner.run_tests(INSTALLED_APPS) 53 | return failures 54 | 55 | 56 | def run(): 57 | failures = runtests() 58 | sys.exit(failures) 59 | 60 | 61 | if __name__ == '__main__': 62 | # Add current directory to python path - works if this script is called python tests/settings,.py 63 | sys.path.insert(0, '.') 64 | run() 65 | -------------------------------------------------------------------------------- /tests/templates/basic.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | {% render_block "css" %} 4 | 5 | some content 6 | 7 | {% addtoblock "css" %} 8 | my css file 9 | {% endaddtoblock %} 10 | 11 | more content 12 | 13 | {% addtoblock "js" %} 14 | my js file 15 | {% endaddtoblock %} 16 | 17 | final content 18 | 19 | {% render_block "js" %} 20 | -------------------------------------------------------------------------------- /tests/templates/css.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | {% render_block "css-to-file" %} 4 | 5 | {% addtoblock "css-to-file" %} 6 | 7 | {% endaddtoblock %} -------------------------------------------------------------------------------- /tests/templates/css2.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | {% render_block "css-onefile" %} 4 | 5 | {% addtoblock "css-onefile" %}{% endaddtoblock %} 6 | {% addtoblock "css-onefile" %}{% endaddtoblock %} -------------------------------------------------------------------------------- /tests/templates/easy_base.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | {% render_block "css" %} 4 | 5 | {% block main %}content{% endblock %} -------------------------------------------------------------------------------- /tests/templates/easy_inherit.html: -------------------------------------------------------------------------------- 1 | {% extends "easy_base.html" %} 2 | 3 | {% block main %}{{ block.super }}{% endblock %} -------------------------------------------------------------------------------- /tests/templates/eat.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | {% addtoblock "css" %} 4 | my css file 5 | {% endaddtoblock %} 6 | 7 | mycontent -------------------------------------------------------------------------------- /tests/templates/errors/failadd.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | {% addtoblock %} 4 | file one 5 | {% endaddtoblock %} -------------------------------------------------------------------------------- /tests/templates/errors/failbase.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | {% render_block "js" %} 4 | 5 | {% include "errors/failinc.html" %} 6 | 7 | {% addtoblock "js" %} 8 | file one 9 | {% endaddtoblock %} -------------------------------------------------------------------------------- /tests/templates/errors/failbase2.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | {% include "errors/failinc.html" %} 4 | 5 | {% addtoblock "js" %} 6 | file one 7 | {% endaddtoblock %} 8 | 9 | {% render_block "js" %} -------------------------------------------------------------------------------- /tests/templates/errors/failinc.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | {% addtoblock "js %} 4 | file two 5 | {% endaddtoblock %} -------------------------------------------------------------------------------- /tests/templates/errors/failrender.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | {% render_block %} -------------------------------------------------------------------------------- /tests/templates/inherit/base.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | head start 4 | 5 | {% render_block "css" %} 6 | 7 | head end 8 | 9 | include start 10 | 11 | {% include "inherit/baseinc.html" %} 12 | 13 | include end 14 | 15 | block main start 16 | {% block "main" %} 17 | block main base contents 18 | {% endblock %} 19 | block main end 20 | 21 | body pre-end 22 | 23 | {% render_block "js" %} 24 | 25 | body end 26 | 27 | {% addtoblock "css" %} 28 | some css file 29 | {% endaddtoblock %} -------------------------------------------------------------------------------- /tests/templates/inherit/baseinc.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | inc add js 4 | 5 | {% addtoblock "js" %} 6 | inc js file 7 | {% endaddtoblock %} -------------------------------------------------------------------------------- /tests/templates/inherit/chain.html: -------------------------------------------------------------------------------- 1 | {% extends "inherit/extend.html" %} 2 | 3 | {% block "main" %} 4 | {{ block.super }} 5 | 6 | {{ other_var }} 7 | {% endblock %} -------------------------------------------------------------------------------- /tests/templates/inherit/extend.html: -------------------------------------------------------------------------------- 1 | {% extends "inherit/base.html" %} 2 | 3 | {% load sekizai_tags %} 4 | 5 | {% addtoblock "css" %} 6 | invisible css file 7 | {% endaddtoblock %} 8 | 9 | {% block "main" %} 10 | {% include "inherit/extinc.html" %} 11 | {% endblock %} -------------------------------------------------------------------------------- /tests/templates/inherit/extinc.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | extinc 4 | -------------------------------------------------------------------------------- /tests/templates/inherit/nullbase.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/django-sekizai/28812ea44bbc1a5848ac200abc23140ae9a719cd/tests/templates/inherit/nullbase.html -------------------------------------------------------------------------------- /tests/templates/inherit/nullext.html: -------------------------------------------------------------------------------- 1 | {% extends "inherit/nullbase.html" %} 2 | 3 | {% load sekizai_tags %} 4 | 5 | {% render_block "js" %} -------------------------------------------------------------------------------- /tests/templates/inherit/spacechain.html: -------------------------------------------------------------------------------- 1 | {% extends "inherit/extend.html" %} -------------------------------------------------------------------------------- /tests/templates/inherit/subvarchain.html: -------------------------------------------------------------------------------- 1 | {% extends "inherit/varchain.html" %} -------------------------------------------------------------------------------- /tests/templates/inherit/super_blocks.html: -------------------------------------------------------------------------------- 1 | {% extends "inherit/base.html" %} 2 | 3 | {% load sekizai_tags %} 4 | 5 | {% addtoblock "css" %} 6 | invisible css file 7 | {% endaddtoblock %} 8 | 9 | {% block "main" %} 10 | {{ block.super }} 11 | {% addtoblock "css" %} 12 | visible css file 13 | {% endaddtoblock %} 14 | more contents 15 | {% endblock %} -------------------------------------------------------------------------------- /tests/templates/inherit/varchain.html: -------------------------------------------------------------------------------- 1 | {% extends var %} 2 | 3 | -------------------------------------------------------------------------------- /tests/templates/named_end.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | {% addtoblock "myblock" %} 4 | mycontent 5 | {% endaddtoblock "myblock" %} 6 | 7 | {% render_block "myblock" %} 8 | -------------------------------------------------------------------------------- /tests/templates/namespaces.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | {% render_block "one" %} 4 | {% render_block "two" %} 5 | 6 | {% addtoblock "one" %} 7 | the same file 8 | {% endaddtoblock %} 9 | 10 | {% addtoblock "two" %} 11 | the same file 12 | {% endaddtoblock %} 13 | 14 | {% addtoblock "one" %} 15 | the same file 16 | {% endaddtoblock %} -------------------------------------------------------------------------------- /tests/templates/processors/addtoblock_namespace.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | header 4 | 5 | {% addtoblock "js" preprocessor "tests.test_core.namespace_processor" %} 6 | javascript 7 | {% endaddtoblock %} 8 | 9 | footer 10 | 11 | {% render_block "js" %} 12 | -------------------------------------------------------------------------------- /tests/templates/processors/addtoblock_null.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | header 4 | 5 | {% addtoblock "js" preprocessor "tests.test_core.null_processor" %} 6 | javascript 7 | {% endaddtoblock %} 8 | 9 | footer 10 | 11 | {% render_block "js" %} 12 | -------------------------------------------------------------------------------- /tests/templates/processors/namespace.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | header 4 | 5 | {% addtoblock "js" %} 6 | javascript 7 | {% endaddtoblock %} 8 | 9 | footer 10 | 11 | {% render_block "js" postprocessor "tests.test_core.namespace_processor" %} 12 | -------------------------------------------------------------------------------- /tests/templates/processors/null.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | header 4 | 5 | {% addtoblock "js" %} 6 | javascript 7 | {% endaddtoblock %} 8 | 9 | footer 10 | 11 | {% render_block "js" postprocessor "tests.test_core.null_processor" %} 12 | -------------------------------------------------------------------------------- /tests/templates/unique.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | {% render_block "unique" %} 4 | 5 | {% addtoblock "unique" %}unique data{% endaddtoblock %} 6 | {% addtoblock "unique" %}unique data{% endaddtoblock %} 7 | -------------------------------------------------------------------------------- /tests/templates/variables.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | {% addtoblock blockname %} 4 | file two 5 | {% endaddtoblock %} 6 | 7 | {% addtoblock "one" %} 8 | file two 9 | {% endaddtoblock %} 10 | 11 | {% addtoblock blockname|upper %} 12 | file one 13 | {% endaddtoblock %} 14 | 15 | {% render_block "ONE" %} 16 | {% render_block blockname %} -------------------------------------------------------------------------------- /tests/templates/with_data.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | {% with_data "key" as mylist %} 3 | {% for obj in mylist %} 4 | {{ obj }} 5 | {% endfor %} 6 | {% end_with_data %} 7 | 8 | {% add_data "key" 1 %} 9 | {% add_data "key" 2 %} -------------------------------------------------------------------------------- /tests/templates/with_data_basic.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | 3 | 4 | {% block title %}{% block pagetitle %}{% endblock pagetitle %}{% endblock title %} 5 | {% with_data "google-fonts" as fonts %}{% if fonts %} 6 | 7 | {% endif %}{% end_with_data %} 8 | 9 | {% render_block "css" %} 10 | 11 | {% block live_css %}{% endblock live_css %} 12 | {% block head %}{% endblock head %} 13 | 14 | 15 | 16 |

          Test page.

          17 | {% block breadcrumb %}{% endblock %} 18 | {% block content %} 19 |

          This page intentionally left blank.

          20 | {% endblock content %} 21 | {% render_block "js" %} 22 | {% block live_js %}{% endblock live_js %} 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from difflib import SequenceMatcher 3 | from io import StringIO 4 | from unittest import TestCase 5 | 6 | from django import template 7 | from django.conf import settings 8 | from django.template.engine import Engine 9 | from django.template.loader import render_to_string 10 | 11 | from sekizai import context_processors 12 | from sekizai.context import SekizaiContext 13 | from sekizai.helpers import Watcher, get_namespaces, get_varname, validate_template 14 | from sekizai.templatetags.sekizai_tags import import_processor, validate_context 15 | 16 | 17 | def null_processor(context, data, namespace): 18 | return '' 19 | 20 | 21 | def namespace_processor(context, data, namespace): 22 | return namespace 23 | 24 | 25 | class SettingsOverride: 26 | """ 27 | Overrides Django settings within a context and resets them to their initial 28 | values on exit. 29 | 30 | Example: 31 | 32 | with SettingsOverride(DEBUG=True): 33 | # do something 34 | """ 35 | class NULL: 36 | pass 37 | 38 | def __init__(self, **overrides): 39 | self.overrides = overrides 40 | 41 | def __enter__(self): 42 | self.old = {} 43 | for key, value in self.overrides.items(): 44 | self.old[key] = getattr(settings, key, self.NULL) 45 | setattr(settings, key, value) 46 | 47 | def __exit__(self, type, value, traceback): 48 | for key, value in self.old.items(): 49 | if value is self.NULL: 50 | delattr(settings, key) 51 | else: 52 | setattr(settings, key, value) 53 | 54 | 55 | class CaptureStdout: 56 | """ 57 | Overrides sys.stdout with a StringIO stream. 58 | """ 59 | def __init__(self): 60 | self.old = None 61 | 62 | def __enter__(self): 63 | self.old = sys.stdout 64 | new = sys.stdout = StringIO() 65 | return new 66 | 67 | def __exit__(self, exc_type, exc_val, exc_tb): 68 | sys.stdout = self.old 69 | 70 | 71 | class Match(tuple): # pragma: no cover 72 | @property 73 | def a(self): 74 | return self[0] 75 | 76 | @property 77 | def b(self): 78 | return self[1] 79 | 80 | @property 81 | def size(self): 82 | return self[2] 83 | 84 | 85 | def _backwards_compat_match(thing): # pragma: no cover 86 | if isinstance(thing, tuple): 87 | return Match(thing) 88 | return thing 89 | 90 | 91 | class BitDiffResult: 92 | 93 | def __init__(self, status, message): 94 | self.status = status 95 | self.message = message 96 | 97 | 98 | class BitDiff: 99 | 100 | """ 101 | Visual aid for failing tests 102 | """ 103 | 104 | def __init__(self, expected): 105 | self.expected = [repr(str(bit)) for bit in expected] 106 | 107 | def test(self, result): 108 | result = [repr(str(bit)) for bit in result] 109 | if self.expected == result: 110 | return BitDiffResult(True, "success") 111 | else: # pragma: no cover 112 | longest = max( 113 | [len(x) for x in self.expected] + 114 | [len(x) for x in result] + 115 | [len('Expected')] 116 | ) 117 | sm = SequenceMatcher() 118 | sm.set_seqs(self.expected, result) 119 | matches = sm.get_matching_blocks() 120 | lasta = 0 121 | lastb = 0 122 | data = [] 123 | for match in [_backwards_compat_match(match) for match in matches]: 124 | unmatcheda = self.expected[lasta:match.a] 125 | unmatchedb = result[lastb:match.b] 126 | unmatchedlen = max([len(unmatcheda), len(unmatchedb)]) 127 | unmatcheda += ['' for x in range(unmatchedlen)] 128 | unmatchedb += ['' for x in range(unmatchedlen)] 129 | for i in range(unmatchedlen): 130 | data.append((False, unmatcheda[i], unmatchedb[i])) 131 | for i in range(match.size): 132 | data.append(( 133 | True, self.expected[match.a + i], result[match.b + i] 134 | )) 135 | lasta = match.a + match.size 136 | lastb = match.b + match.size 137 | padlen = (longest - len('Expected')) 138 | padding = ' ' * padlen 139 | line1 = '-' * padlen 140 | line2 = '-' * (longest - len('Result')) 141 | msg = '\nExpected%s | | Result' % padding 142 | msg += f'\n--------{line1}-|---|-------{line2}' 143 | for success, a, b in data: 144 | pad = ' ' * (longest - len(a)) 145 | if success: 146 | msg += f'\n{a}{pad} | | {b}' 147 | else: 148 | msg += f'\n{a}{pad} | ! | {b}' 149 | return BitDiffResult(False, msg) 150 | 151 | 152 | def update_template_debug(debug=True): 153 | """ 154 | Helper method for updating the template debug option based on 155 | the django version. Use the results of this function as the context. 156 | 157 | :return: SettingsOverride object 158 | """ 159 | # Create our overridden template settings with debug turned off. 160 | templates_override = settings.TEMPLATES 161 | templates_override[0]['OPTIONS'].update({'debug': debug}) 162 | # Engine gets created based on template settings initial value so 163 | # changing the settings after the fact won't update, so do it 164 | # manually. Necessary when testing validate_context 165 | # with render method and want debug off. 166 | Engine.get_default().debug = debug 167 | return SettingsOverride(TEMPLATES=templates_override) 168 | 169 | 170 | class SekizaiTestCase(TestCase): 171 | 172 | def _render(self, tpl, ctx=None, sekizai_context=True): 173 | ctx = dict(ctx) if ctx else {} 174 | if sekizai_context: 175 | ctx.update(context_processors.sekizai()) 176 | return render_to_string(tpl, ctx) 177 | 178 | def _get_bits(self, tpl, ctx=None, sekizai_context=True): 179 | ctx = ctx or {} 180 | rendered = self._render(tpl, ctx, sekizai_context) 181 | bits = [ 182 | bit for bit in [bit.strip('\n') 183 | for bit in rendered.split('\n')] if bit 184 | ] 185 | return bits, rendered 186 | 187 | def _test(self, tpl, res, ctx=None, sekizai_context=True): 188 | """ 189 | Helper method to render template and compare it's bits 190 | """ 191 | ctx = ctx or {} 192 | bits, rendered = self._get_bits(tpl, ctx, sekizai_context) 193 | differ = BitDiff(res) 194 | result = differ.test(bits) 195 | self.assertTrue(result.status, result.message) 196 | return rendered 197 | 198 | def test_basic_dual_block(self): 199 | """ 200 | Basic dual block testing 201 | """ 202 | bits = [ 203 | 'my css file', 'some content', 'more content', 'final content', 204 | 'my js file' 205 | ] 206 | self._test('basic.html', bits) 207 | 208 | def test_named_endaddtoblock(self): 209 | """ 210 | Testing with named endaddblock 211 | """ 212 | bits = ["mycontent"] 213 | self._test('named_end.html', bits) 214 | 215 | def test_eat_content_before_render_block(self): 216 | """ 217 | Testing that content gets eaten if no render_blocks is available 218 | """ 219 | bits = ["mycontent"] 220 | self._test("eat.html", bits) 221 | 222 | def test_sekizai_context_required(self): 223 | """ 224 | Test that the template tags properly fail if not used with either 225 | SekizaiContext or the context processor. 226 | """ 227 | with self.assertRaises(template.TemplateSyntaxError): 228 | self._render('basic.html', {}, sekizai_context=False) 229 | 230 | def test_complex_template_inheritance(self): 231 | """ 232 | Test that (complex) template inheritances work properly 233 | """ 234 | bits = [ 235 | "head start", 236 | "some css file", 237 | "head end", 238 | "include start", 239 | "inc add js", 240 | "include end", 241 | "block main start", 242 | "extinc", 243 | "block main end", 244 | "body pre-end", 245 | "inc js file", 246 | "body end" 247 | ] 248 | self._test("inherit/extend.html", bits) 249 | """ 250 | Test that blocks (and block.super) work properly with sekizai 251 | """ 252 | bits = [ 253 | "head start", 254 | "visible css file", 255 | "some css file", 256 | "head end", 257 | "include start", 258 | "inc add js", 259 | "include end", 260 | "block main start", 261 | "block main base contents", 262 | "more contents", 263 | "block main end", 264 | "body pre-end", 265 | "inc js file", 266 | "body end" 267 | ] 268 | self._test("inherit/super_blocks.html", bits) 269 | 270 | def test_namespace_isolation(self): 271 | """ 272 | Tests that namespace isolation works 273 | """ 274 | bits = ["the same file", "the same file"] 275 | self._test('namespaces.html', bits) 276 | 277 | def test_variable_namespaces(self): 278 | """ 279 | Tests variables and filtered variables as block names. 280 | """ 281 | bits = ["file one", "file two"] 282 | self._test('variables.html', bits, {'blockname': 'one'}) 283 | 284 | def test_invalid_addtoblock(self): 285 | """ 286 | Tests that template syntax errors are raised properly in templates 287 | rendered by sekizai tags 288 | """ 289 | self.assertRaises( 290 | template.TemplateSyntaxError, 291 | self._render, 'errors/failadd.html' 292 | ) 293 | 294 | def test_invalid_renderblock(self): 295 | self.assertRaises( 296 | template.TemplateSyntaxError, 297 | self._render, 'errors/failrender.html' 298 | ) 299 | 300 | def test_invalid_include(self): 301 | self.assertRaises( 302 | template.TemplateSyntaxError, 303 | self._render, 'errors/failinc.html' 304 | ) 305 | 306 | def test_invalid_basetemplate(self): 307 | self.assertRaises( 308 | template.TemplateSyntaxError, 309 | self._render, 'errors/failbase.html' 310 | ) 311 | 312 | def test_invalid_basetemplate_two(self): 313 | self.assertRaises( 314 | template.TemplateSyntaxError, 315 | self._render, 'errors/failbase2.html' 316 | ) 317 | 318 | def test_with_data(self): 319 | """ 320 | Tests the with_data/add_data tags. 321 | """ 322 | bits = ["1", "2"] 323 | self._test('with_data.html', bits) 324 | 325 | def test_easy_inheritance(self): 326 | self.assertEqual('content', self._render("easy_inherit.html").strip()) 327 | 328 | def test_validate_context(self): 329 | sekizai_ctx = SekizaiContext() 330 | django_ctx = template.Context() 331 | self.assertRaises( 332 | template.TemplateSyntaxError, 333 | validate_context, django_ctx 334 | ) 335 | self.assertEqual(validate_context(sekizai_ctx), True) 336 | 337 | with update_template_debug(debug=False): 338 | self.assertEqual(validate_context(django_ctx), False) 339 | self.assertEqual(validate_context(sekizai_ctx), True) 340 | bits = ['some content', 'more content', 'final content'] 341 | self._test('basic.html', bits, sekizai_context=False) 342 | 343 | def test_post_processor_null(self): 344 | bits = ['header', 'footer'] 345 | self._test('processors/null.html', bits) 346 | 347 | def test_post_processor_namespace(self): 348 | bits = ['header', 'footer', 'js'] 349 | self._test('processors/namespace.html', bits) 350 | 351 | def test_import_processor_failfast(self): 352 | self.assertRaises(TypeError, import_processor, 'invalidpath') 353 | 354 | def test_unique(self): 355 | bits = ['unique data'] 356 | self._test('unique.html', bits) 357 | 358 | def test_strip(self): 359 | tpl = template.Template(""" 360 | {% load sekizai_tags %} 361 | {% addtoblock 'a' strip %} test{% endaddtoblock %} 362 | {% addtoblock 'a' strip %}test {% endaddtoblock %} 363 | {% render_block 'a' %}""") 364 | context = SekizaiContext() 365 | output = tpl.render(context) 366 | self.assertEqual(output.count('test'), 1, output) 367 | 368 | def test_addtoblock_processor_null(self): 369 | bits = ['header', 'footer'] 370 | self._test('processors/addtoblock_null.html', bits) 371 | 372 | def test_addtoblock_processor_namespace(self): 373 | bits = ['header', 'footer', 'js'] 374 | self._test('processors/addtoblock_namespace.html', bits) 375 | 376 | 377 | class HelperTests(TestCase): 378 | 379 | def test_validate_template_js_css(self): 380 | self.assertTrue(validate_template('basic.html', ['js', 'css'])) 381 | 382 | def test_validate_template_with_data(self): 383 | self.assertTrue(validate_template('with_data_basic.html', ['js', 'css'])) 384 | 385 | def test_validate_template_js(self): 386 | self.assertTrue(validate_template('basic.html', ['js'])) 387 | 388 | def test_validate_template_css(self): 389 | self.assertTrue(validate_template('basic.html', ['css'])) 390 | 391 | def test_validate_template_empty(self): 392 | self.assertTrue(validate_template('basic.html', [])) 393 | 394 | def test_validate_template_notfound(self): 395 | self.assertFalse(validate_template('basic.html', ['notfound'])) 396 | 397 | def test_get_namespaces_easy_inherit(self): 398 | self.assertEqual(get_namespaces('easy_inherit.html'), ['css']) 399 | 400 | def test_get_namespaces_chain_inherit(self): 401 | self.assertEqual(get_namespaces('inherit/chain.html'), ['css', 'js']) 402 | 403 | def test_get_namespaces_space_chain_inherit(self): 404 | self.assertEqual( 405 | get_namespaces('inherit/spacechain.html'), 406 | ['css', 'js'] 407 | ) 408 | 409 | def test_get_namespaces_var_inherit(self): 410 | self.assertEqual(get_namespaces('inherit/varchain.html'), []) 411 | 412 | def test_get_namespaces_sub_var_inherit(self): 413 | self.assertEqual(get_namespaces('inherit/subvarchain.html'), []) 414 | 415 | def test_get_namespaces_null_ext(self): 416 | self.assertEqual(get_namespaces('inherit/nullext.html'), []) 417 | 418 | def test_deactivate_validate_template(self): 419 | with SettingsOverride(SEKIZAI_IGNORE_VALIDATION=True): 420 | self.assertTrue(validate_template('basic.html', ['js', 'css'])) 421 | self.assertTrue(validate_template('basic.html', ['js'])) 422 | self.assertTrue(validate_template('basic.html', ['css'])) 423 | self.assertTrue(validate_template('basic.html', [])) 424 | self.assertTrue(validate_template('basic.html', ['notfound'])) 425 | 426 | def test_watcher_add_namespace(self): 427 | context = SekizaiContext() 428 | watcher = Watcher(context) 429 | varname = get_varname() 430 | context[varname]['key'].append('value') 431 | changes = watcher.get_changes() 432 | self.assertEqual(changes, {'key': ['value']}) 433 | 434 | def test_watcher_add_data(self): 435 | context = SekizaiContext() 436 | varname = get_varname() 437 | context[varname]['key'].append('value') 438 | watcher = Watcher(context) 439 | context[varname]['key'].append('value2') 440 | changes = watcher.get_changes() 441 | self.assertEqual(changes, {'key': ['value2']}) 442 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | flake8 4 | isort 5 | py{38,39,310}-dj{32,40,41} 6 | py{310,311}-dj{42,main} 7 | 8 | skip_missing_interpreters=True 9 | 10 | [testenv] 11 | deps = 12 | -r{toxinidir}/tests/requirements/base.txt 13 | dj32: Django>=3.2,<3.3 14 | dj42: Django>=4.2,<4.3 15 | dj50: Django>=5.0,<5.1 16 | dj51: Django>=5.1,<5.2 17 | djmain: https://github.com/django/django/archive/main.tar.gz 18 | commands = 19 | {envpython} --version 20 | {env:COMMAND:coverage} erase 21 | {env:COMMAND:coverage} run tests/settings.py 22 | {env:COMMAND:coverage} report 23 | ignore_outcome = 24 | djmain: True 25 | ignore_errors = 26 | djmain: True 27 | 28 | [testenv:flake8] 29 | deps = flake8 30 | commands = flake8 --config=setup.cfg 31 | 32 | [testenv:isort] 33 | deps = isort 34 | commands = isort -c -df sekizai 35 | skip_install = true 36 | --------------------------------------------------------------------------------