├── config ├── __init__.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── src ├── __init__.py └── component_tags │ ├── templates │ └── component_tags │ │ ├── slot.html │ │ └── base.html │ ├── apps.py │ ├── template │ ├── choices.py │ ├── __init__.py │ ├── builtins.py │ ├── wrappers.py │ ├── components.py │ ├── helpers.py │ ├── parser.py │ ├── media.py │ ├── library.py │ ├── nodes.py │ ├── attributes.py │ └── context.py │ ├── __init__.py │ └── tests.py ├── templatetags └── __init__.py ├── docs ├── _static │ └── .gitignore ├── readme.rst ├── authors.rst ├── changelog.rst ├── license.rst ├── requirements.txt ├── Makefile ├── index.rst └── conf.py ├── AUTHORS.rst ├── tests └── conftest.py ├── pyproject.toml ├── Pipfile ├── .readthedocs.yml ├── CHANGELOG.rst ├── .coveragerc ├── .github └── workflows │ └── tests.yml ├── setup.py ├── manage.py ├── .gitignore ├── LICENSE.txt ├── requirements.txt ├── tox.ini ├── setup.cfg ├── README.rst └── Pipfile.lock /config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/.gitignore: -------------------------------------------------------------------------------- 1 | # Empty directory 2 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. _readme: 2 | .. include:: ../README.rst 3 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. _authors: 2 | .. include:: ../AUTHORS.rst 3 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changes: 2 | .. include:: ../CHANGELOG.rst 3 | -------------------------------------------------------------------------------- /src/component_tags/templates/component_tags/slot.html: -------------------------------------------------------------------------------- 1 | {% extends 'component_tags/base.html' %} -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. _license: 2 | 3 | ======= 4 | License 5 | ======= 6 | 7 | .. include:: ../LICENSE.txt 8 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributors 3 | ============ 4 | 5 | * David Sosa Valdes 6 | -------------------------------------------------------------------------------- /src/component_tags/templates/component_tags/base.html: -------------------------------------------------------------------------------- 1 | {% block content %} 2 | {{ nodelist }} 3 | {% endblock content %} -------------------------------------------------------------------------------- /src/component_tags/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class ComponentTagsConfig(AppConfig): 6 | name = 'component_tags' 7 | verbose_name = _('Django Component Tags') 8 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements file for ReadTheDocs, check .readthedocs.yml. 2 | # To build the module reference correctly, make sure every external package 3 | # under `install_requires` in `setup.cfg` is also listed here! 4 | sphinx>=3.2.1 5 | # sphinx_rtd_theme 6 | -------------------------------------------------------------------------------- /src/component_tags/template/choices.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | __all__ = ['AttributeChoices'] 4 | 5 | 6 | class AttributeChoices(str, Enum): 7 | """ 8 | Generic attribute choices. 9 | 10 | Derive from this class to define new choices. 11 | """ 12 | pass 13 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dummy conftest.py for component_tags. 3 | 4 | If you don't know what this is for, just leave it empty. 5 | Read more about conftest.py under: 6 | - https://docs.pytest.org/en/stable/fixture.html 7 | - https://docs.pytest.org/en/stable/writing_plugins.html 8 | """ 9 | 10 | # import pytest 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | # AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD! 3 | requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=5", "wheel"] 4 | build-backend = "setuptools.build_meta" 5 | 6 | [tool.setuptools_scm] 7 | # See configuration details in https://github.com/pypa/setuptools_scm 8 | version_scheme = "no-guess-dev" 9 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | django = "*" 8 | 9 | [dev-packages] 10 | django-extensions = "*" 11 | django-debug-toolbar = "*" 12 | setuptools = "*" 13 | setuptools_scm = "*" 14 | wheel = "*" 15 | conf = "*" 16 | pytest = "*" 17 | pytest-cov = "*" 18 | pytest-django = "*" 19 | config = "*" 20 | python-memcached = "*" 21 | 22 | [requires] 23 | python_version = "3.9" 24 | -------------------------------------------------------------------------------- /src/component_tags/template/__init__.py: -------------------------------------------------------------------------------- 1 | from .attributes import Attribute 2 | from .choices import AttributeChoices 3 | from .context import TagContext, ComponentContext 4 | from .library import Library 5 | from .nodes import ComponentNode 6 | 7 | 8 | __all__ = [ 9 | 'Library', 10 | 'Attribute', 11 | 'AttributeChoices', 12 | 'Component', 13 | 'ComponentContext', 14 | 'TagContext', 15 | ] 16 | 17 | 18 | Component = ComponentNode 19 | -------------------------------------------------------------------------------- /config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for foo project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'foo.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for foo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Build documentation in the docs/ directory with Sphinx 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | # Build documentation with MkDocs 12 | #mkdocs: 13 | # configuration: mkdocs.yml 14 | 15 | # Optionally build your docs in additional formats such as PDF 16 | formats: 17 | - pdf 18 | 19 | python: 20 | version: 3.8 21 | install: 22 | - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /src/component_tags/template/builtins.py: -------------------------------------------------------------------------------- 1 | from . import Library 2 | from .components import Slot 3 | from .media import media_tag 4 | 5 | """ 6 | Builtin tags 7 | 8 | * Slot: can be used inside any other component, therefore it can be pre-loaded inside django. 9 | * components_css: import css scripts from rendered components 10 | * components_js: import js scripts from rendered components 11 | """ 12 | register = Library() 13 | 14 | register.tag('slot', Slot) 15 | register.tag("components_css", media_tag("css")) 16 | register.tag("components_js", media_tag("js")) 17 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | Version 0.0.5 6 | ============= 7 | 8 | - Media class implementation 9 | 10 | Version 0.0.4 11 | ============= 12 | 13 | - Minimal environment version (stable) 14 | - Fix attribute tests 15 | 16 | Version 0.0.3 17 | ============= 18 | 19 | - Add an extendable components metaclass (Django way). 20 | - Change the regular name attribute to context_name, which it makes more sense. 21 | - Remove some unused app scripts. 22 | 23 | Version 0.0.2 24 | ============= 25 | 26 | - Reformat the library with new standard specifications. 27 | - Fix the test environment 28 | 29 | Version 0.0.1 30 | =========== 31 | 32 | - Initial release 33 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | source = component_tags 5 | # omit = bad_file.py 6 | 7 | [paths] 8 | source = src/component_tags/ 9 | [report] 10 | # Regexes for lines to exclude from consideration 11 | exclude_lines = 12 | # Have to re-enable the standard pragma 13 | pragma: no cover 14 | 15 | # Don't complain about missing debug-only code: 16 | def __repr__ 17 | if self\.debug 18 | 19 | # Don't complain if tests don't hit defensive assertion code: 20 | raise AssertionError 21 | raise NotImplementedError 22 | 23 | # Don't complain if non-runnable code isn't run: 24 | if 0: 25 | if __name__ == .__main__.: 26 | -------------------------------------------------------------------------------- /src/component_tags/template/wrappers.py: -------------------------------------------------------------------------------- 1 | from .parser import parse_component 2 | from functools import wraps 3 | 4 | __all__ = ['component_wrapper'] 5 | 6 | 7 | def component_wrapper(name, component_node): 8 | """ 9 | Component wrapper to parse all component args 10 | """ 11 | @wraps(component_node) 12 | def func(parser, token): 13 | nodelist = parser.parse(('end%s' % name,)) 14 | parser.delete_first_token() 15 | tag_name, args, kwargs, options, slots, isolated_context = parse_component(nodelist, token, parser) 16 | return component_node(tag_name, nodelist, options, slots, *args, isolated_context=isolated_context, **kwargs) 17 | return name, func 18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | run-tests: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: 11 | - 3.6 12 | - 3.7 13 | - 3.8 14 | - 3.9 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install tox tox-gh-actions 25 | - name: Run tests 26 | run: | 27 | tox 28 | -------------------------------------------------------------------------------- /src/component_tags/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info[:2] >= (3, 8): 4 | # TODO: Import directly (no need for conditional) when `python_requires = >= 3.8` 5 | from importlib.metadata import PackageNotFoundError, version # pragma: no cover 6 | else: 7 | # noinspection PyUnresolvedReferences 8 | from importlib_metadata import PackageNotFoundError, version # pragma: no cover 9 | 10 | try: 11 | # Change here if project is renamed and does not equal the package name 12 | dist_name = __name__ 13 | __version__ = version(dist_name) 14 | except PackageNotFoundError: # pragma: no cover 15 | __version__ = "unknown" 16 | finally: 17 | del version, PackageNotFoundError 18 | 19 | 20 | default_app_config = 'component_tags.apps.ComponentTagsConfig' 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Setup file for component_tags. 3 | Use setup.cfg to configure your project. 4 | 5 | This file was generated with PyScaffold 4.0rc2. 6 | PyScaffold helps you to put up the scaffold of your new Python project. 7 | Learn more under: https://pyscaffold.org/ 8 | """ 9 | from setuptools import setup 10 | 11 | if __name__ == "__main__": 12 | try: 13 | setup(use_scm_version={"version_scheme": "no-guess-dev"}) 14 | except: # noqa 15 | print( 16 | "\n\nAn error occurred while building the project, " 17 | "please ensure you have the most updated version of setuptools, " 18 | "setuptools_scm and wheel with:\n" 19 | " pip install -U setuptools setuptools_scm wheel\n\n" 20 | ) 21 | raise 22 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src")) 7 | 8 | 9 | def main(): 10 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | execute_from_command_line(sys.argv) 20 | 21 | 22 | if __name__ == '__main__': 23 | main() 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary and binary files 2 | *~ 3 | *.py[cod] 4 | *.so 5 | *.cfg 6 | !.isort.cfg 7 | !setup.cfg 8 | *.orig 9 | *.log 10 | *.pot 11 | __pycache__/* 12 | .cache/* 13 | .*.swp 14 | */.ipynb_checkpoints/* 15 | .DS_Store 16 | 17 | # Project files 18 | .ropeproject 19 | .project 20 | .pydevproject 21 | .settings 22 | .idea 23 | .vscode 24 | /tags 25 | 26 | # Package files 27 | *.egg 28 | *.eggs/ 29 | .installed.cfg 30 | *.egg-info 31 | 32 | # Unittest and coverage 33 | htmlcov/* 34 | .coverage 35 | .coverage.* 36 | .tox 37 | junit*.xml 38 | coverage.xml 39 | .pytest_cache/ 40 | 41 | # Build and docs folder/files 42 | build/* 43 | dist/* 44 | sdist/* 45 | docs/api/* 46 | docs/_rst/* 47 | docs/_build/* 48 | cover/* 49 | MANIFEST 50 | 51 | # Per-project virtualenvs 52 | .venv*/ 53 | .conda*/ 54 | {} 55 | 56 | # Django 57 | /*.sqlite3 58 | /static/ 59 | /templatetags/test_tags.py 60 | /templates/* 61 | /htmlcov/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 David Sosa Valdes 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 | -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | """django-lambda-theme URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.shortcuts import render 18 | from django.urls import path, include 19 | from django.views.decorators.cache import cache_page 20 | 21 | import debug_toolbar 22 | 23 | 24 | @cache_page(60 * 0.5) # convert to seconds 25 | def index(request): 26 | return render(request, 'index.html') 27 | 28 | 29 | urlpatterns = [ 30 | path('__debug__/', include(debug_toolbar.urls)), 31 | path('admin/', admin.site.urls), 32 | path('', index, name='index'), 33 | ] 34 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | AUTODOCDIR = api 11 | 12 | # User-friendly check for sphinx-build 13 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $?), 1) 14 | $(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/") 15 | endif 16 | 17 | .PHONY: help clean Makefile 18 | 19 | # Put it first so that "make" without argument is like "make help". 20 | help: 21 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | 23 | clean: 24 | rm -rf $(BUILDDIR)/* $(AUTODOCDIR) 25 | 26 | # Catch-all target: route all unknown targets to Sphinx using the new 27 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 28 | %: Makefile 29 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 30 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # These requirements were autogenerated by pipenv 3 | # To regenerate from the project's Pipfile, run: 4 | # 5 | # pipenv lock --requirements --dev 6 | # 7 | 8 | # Note: in pipenv 2020.x, "--dev" changed to emit both default and development 9 | # requirements. To emit only development requirements, pass "--dev-only". 10 | 11 | -i https://pypi.org/simple/ 12 | asgiref==3.5.0; python_version >= '3.7' 13 | attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' 14 | conf==0.4.1 15 | config==0.5.0.post0 16 | coverage==6.3.2; python_version >= '3.7' 17 | django-debug-toolbar==3.2.1 18 | django-extensions==3.1.1 19 | django==3.1.14 20 | iniconfig==1.1.1 21 | packaging==21.3; python_version >= '3.6' 22 | pluggy==0.13.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 23 | py==1.11.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' 24 | pyparsing==3.0.7; python_version >= '3.6' 25 | pytest-cov==2.11.1 26 | pytest-django==4.1.0 27 | pytest==6.2.2 28 | python-memcached==1.59 29 | pytz==2021.3 30 | setuptools-scm==6.0.1 31 | setuptools==60.9.3 32 | six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 33 | sqlparse==0.4.2; python_version >= '3.5' 34 | toml==0.10.2; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' 35 | wheel==0.36.2 36 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | Django - Component Tags 3 | ======================= 4 | 5 | Create advanced HTML components using Django Tags. 6 | 7 | 8 | 9 | Contents 10 | ======== 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | Overview 16 | License 17 | Authors 18 | Changelog 19 | Module Reference 20 | 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | 29 | .. _toctree: http://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html 30 | .. _reStructuredText: http://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html 31 | .. _references: http://www.sphinx-doc.org/en/stable/markup/inline.html 32 | .. _Python domain syntax: http://sphinx-doc.org/domains.html#the-python-domain 33 | .. _Sphinx: http://www.sphinx-doc.org/ 34 | .. _Python: http://docs.python.org/ 35 | .. _Numpy: http://docs.scipy.org/doc/numpy 36 | .. _SciPy: http://docs.scipy.org/doc/scipy/reference/ 37 | .. _matplotlib: https://matplotlib.org/contents.html# 38 | .. _Pandas: http://pandas.pydata.org/pandas-docs/stable 39 | .. _Scikit-Learn: http://scikit-learn.org/stable 40 | .. _autodoc: http://www.sphinx-doc.org/en/stable/ext/autodoc.html 41 | .. _Google style: https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings 42 | .. _NumPy style: https://numpydoc.readthedocs.io/en/latest/format.html 43 | .. _classical style: http://www.sphinx-doc.org/en/stable/domains.html#info-field-lists 44 | -------------------------------------------------------------------------------- /src/component_tags/template/components.py: -------------------------------------------------------------------------------- 1 | from django.template.base import Variable, NodeList 2 | 3 | from .nodes import ComponentNode 4 | 5 | __all__ = ['Slot'] 6 | 7 | 8 | class Slot(ComponentNode): 9 | """ 10 | Can be used inside any other component to create dynamic blocks inside components. 11 | 12 | Attributes 13 | ---------- 14 | name: str 15 | name used to identify the slot inside the component tag 16 | 17 | Examples 18 | -------- 19 | 20 | .. code-block:: 21 | 22 | # tags/button.html 23 | {{ slot_foo }} 24 | 27 | 28 | # base.html 29 | {% button type="button" %} 30 | {% slot 'foo' %}Slot 1{% endslot %} 31 | Button 1 32 | {% endbutton %} 33 | 34 | # Output: 35 | Slot 1 36 | 37 | """ 38 | 39 | def __init__(self, *args, **kwargs): 40 | tag_name, nodelist, options, slots, name = args 41 | kwargs['isolated_context'] = True # Make sure slot has the parent context 42 | super().__init__(tag_name, nodelist, options, slots, **kwargs) 43 | 44 | # TODO: 45 | # there should be a better way to get the variable value 46 | # but name should be declared inside this constructor 47 | self.slot_name = getattr(name, 'var', None) 48 | 49 | class Meta: 50 | template_name = 'component_tags/slot.html' 51 | -------------------------------------------------------------------------------- /src/component_tags/template/helpers.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | from django.template import Context, RequestContext 4 | from django.utils.safestring import SafeText, SafeString 5 | 6 | __all__ = [ 7 | 'format_value', 8 | 'format_classes', 9 | 'format_attributes', 10 | ] 11 | 12 | 13 | def format_value(value, context=None): 14 | """ 15 | Resole the Variable/FilterExpression value else nothing happens 16 | """ 17 | try: 18 | return value.resolve(context) 19 | except AttributeError: 20 | return value 21 | 22 | 23 | def format_classes(classes: Optional[list], context=Union[Context, RequestContext]) -> Optional[SafeText]: 24 | """ 25 | Join all classes as a string 26 | """ 27 | # Is format_classes already called before? 28 | if isinstance(classes, SafeString): 29 | return classes 30 | 31 | # Do not print anything if there are no html classes 32 | if not classes: 33 | return None 34 | 35 | elements = [] 36 | for value in classes: 37 | value = format_value(value, context) 38 | if value is not None: 39 | elements.append(value) 40 | 41 | return SafeText(" ".join(elements)) 42 | 43 | 44 | # TODO: remove this function 45 | # def format_options(options, context=None) -> SafeText: 46 | # elements = [] 47 | # for (key, value) in options.items(): 48 | # value = format_value(value, context) 49 | # if value is not None: 50 | # elements.append("{}: {}".format(key, value)) 51 | # return SafeText("; ".join(elements).lower()) 52 | 53 | 54 | def format_attributes(properties, context=None) -> SafeText: 55 | """ 56 | Join all attributes as a string 57 | """ 58 | # Is format_attributes already called before? 59 | if isinstance(properties, SafeString): 60 | return properties 61 | 62 | elements = [] 63 | for (key, value) in properties.items(): 64 | # Is this a class attribute 65 | if key == 'class': 66 | value = format_classes(value, context) 67 | else: 68 | value = format_value(value, context) 69 | if value is not None: 70 | elements.append(f'{key}="{value}"') 71 | 72 | return SafeText(" ".join(elements)) 73 | -------------------------------------------------------------------------------- /src/component_tags/template/parser.py: -------------------------------------------------------------------------------- 1 | from django.template.exceptions import TemplateSyntaxError 2 | from django.template.base import kwarg_re, FilterExpression, token_kwargs 3 | 4 | from .components import Slot 5 | 6 | 7 | def parse_component(nodelist, token, parser): 8 | """ 9 | Load a component template and render it with the current context. You can pass 10 | additional context using keyword arguments. 11 | 12 | Example 13 | 14 | .. code-block:: 15 | 16 | {% component %}{% endcomponent %} 17 | {% component with bar="BAZZ!" baz="BING!" %}{% endcomponent %} 18 | 19 | """ 20 | bits = token.split_contents() 21 | 22 | # if len(bits) < 2: 23 | # raise TemplateSyntaxError( 24 | # "%r tag takes at least one argument: the name of the template to " 25 | # "be included." % bits[0] 26 | # ) 27 | 28 | tag_name = bits.pop(0) 29 | args = [] 30 | kwargs, options, slots = {}, {}, {} 31 | isolated_context = True 32 | 33 | while bits: 34 | bit = bits.pop(0) 35 | 36 | # if bit in options: 37 | # raise TemplateSyntaxError('The %r option was specified more than once.' % bit) 38 | 39 | match = kwarg_re.match(bit) 40 | kwarg_format = match and match.group(1) 41 | 42 | if kwarg_format: 43 | key, value = match.groups() 44 | filter_expr = FilterExpression(value, parser) 45 | kwargs[key] = filter_expr 46 | else: 47 | filter_expr = FilterExpression(bit, parser) 48 | args.append(filter_expr) 49 | 50 | if bit == 'with': 51 | options = token_kwargs(bits, parser, support_legacy=False) 52 | if not options: 53 | raise TemplateSyntaxError('"with" in %r tag needs at least ' 54 | 'one keyword argument.' % tag_name) 55 | 56 | slot_nodes = list(filter(lambda x: isinstance(x[1], Slot), enumerate(nodelist))) 57 | 58 | while slot_nodes: 59 | pos, node = slot_nodes.pop() 60 | name = getattr(node, 'slot_name', pos) 61 | key = f'slot_{pos if name is None else name}' 62 | slots[key] = node 63 | del nodelist[pos] 64 | 65 | return tag_name, args, kwargs, options, slots, isolated_context 66 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox configuration file 2 | # Read more under https://tox.readthedocs.org/ 3 | # THIS SCRIPT IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS! 4 | 5 | [tox] 6 | minversion = 3.15 7 | envlist = default 8 | 9 | 10 | [testenv] 11 | description = invoke pytest to run automated tests 12 | isolated_build = True 13 | setenv = 14 | TOXINIDIR = {toxinidir} 15 | passenv = 16 | HOME 17 | extras = 18 | all 19 | testing 20 | deps = 21 | -r {toxinidir}/requirements.txt 22 | commands = 23 | python3 manage.py test component_tags 24 | pytest --cov src/component_tags --cov-report xml 25 | 26 | 27 | [testenv:{clean,build}] 28 | description = 29 | Build (or clean) the package in isolation according to instructions in: 30 | https://setuptools.readthedocs.io/en/latest/build_meta.html#how-to-use-it 31 | https://github.com/pypa/pep517/issues/91 32 | https://github.com/pypa/build 33 | # NOTE: build is still experimental, please refer to the links for updates/issues 34 | skip_install = True 35 | changedir = {toxinidir} 36 | deps = 37 | build: build 38 | commands = 39 | clean: python -c 'from shutil import rmtree; rmtree("build", True); rmtree("dist", True)' 40 | build: python -m build . 41 | # By default `build` produces wheels, you can also explicitly use the flags `--sdist` and `--wheel` 42 | 43 | 44 | [testenv:{docs,doc_tests,doc_coverage}] 45 | description = invoke sphinx-build to build the docs/run doctests 46 | setenv = 47 | DOCSDIR = {toxinidir}/docs 48 | BUILDDIR = {toxinidir}/docs/_build 49 | docs: BUILD = html 50 | doc_tests: BUILD = doctest 51 | doc_coverage: BUILD = coverage 52 | deps = 53 | # ^ requirements.txt shared with Read The Docs 54 | -r {toxinidir}/docs/requirements.txt 55 | -r {toxinidir}/requirements.txt 56 | commands = 57 | sphinx-build -b {env:BUILD} -d "{env:BUILDDIR}/doctrees" "{env:DOCSDIR}" "{env:BUILDDIR}/{env:BUILD}" {posargs} 58 | 59 | 60 | [testenv:publish] 61 | description = 62 | Publish the package you have been developing to a package index server. 63 | By default, it uses testpypi. If you really want to publish your package 64 | to be publicly accessible in PyPI, use the `-- --repository pypi` option. 65 | skip_install = True 66 | changedir = {toxinidir} 67 | passenv = 68 | TWINE_USERNAME 69 | TWINE_PASSWORD 70 | TWINE_REPOSITORY 71 | deps = twine 72 | commands = 73 | python -m twine check dist/* 74 | python -m twine upload {posargs:--repository testpypi} dist/* 75 | -------------------------------------------------------------------------------- /src/component_tags/template/media.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.forms.widgets import Media 3 | from django.template import Context 4 | 5 | """ 6 | Copyright (c) 2015 Jérôme Bon 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 9 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 10 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 11 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or substantial 14 | portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | DEALINGS IN THE SOFTWARE. 21 | 22 | For more information about this: 23 | 24 | https://gitlab.com/Mojeer/django_components 25 | 26 | """ 27 | 28 | 29 | MEDIA_CONTEXT_KEY = "__django_component__media" 30 | 31 | 32 | def media_tag(media_type): 33 | def media(parser, token): 34 | nodelist = parser.parse() 35 | return MediaNode(media_type, nodelist) 36 | 37 | return media 38 | 39 | 40 | def ensure_media_context(root_context: dict): 41 | if MEDIA_CONTEXT_KEY not in root_context: 42 | root_context[MEDIA_CONTEXT_KEY] = Media() 43 | 44 | 45 | def add_media(context: Context, media): 46 | root_context = context.dicts[0] 47 | ensure_media_context(root_context) 48 | root_context[MEDIA_CONTEXT_KEY] += media 49 | 50 | 51 | class MediaNode(template.Node): 52 | def __init__(self, media_type, nodelist): 53 | self.media_type = media_type 54 | self.nodelist = nodelist 55 | 56 | def render(self, context): 57 | rendered = self.nodelist.render(context) 58 | return self.render_media(context) + rendered 59 | 60 | def render_media(self, context): 61 | tags = [] 62 | if MEDIA_CONTEXT_KEY in context: 63 | media = context[MEDIA_CONTEXT_KEY] 64 | if self.media_type == "css": 65 | tags = media.render_css() 66 | elif self.media_type == "js": 67 | tags = media.render_js() 68 | return "".join(tags) 69 | -------------------------------------------------------------------------------- /src/component_tags/template/library.py: -------------------------------------------------------------------------------- 1 | from django.template.library import Library as BaseLibrary 2 | 3 | from .wrappers import component_wrapper 4 | from .nodes import BaseComponent 5 | 6 | __all__ = ['Library'] 7 | 8 | 9 | class Library(BaseLibrary): 10 | """ 11 | A custom class for registering template components, tags and filters. 12 | The filter, simple_tag, and inclusion_tag methods provide a convenient 13 | way to register callables/components as tags. 14 | 15 | Examples 16 | -------- 17 | There is a lot of ways to register a component tag: 18 | 19 | .. code-block:: python 20 | 21 | register.tag('name', ComponentClass) 22 | 23 | @register.tag 24 | class ComponentClass: 25 | pass 26 | 27 | @register.tag() 28 | class ComponentClass: 29 | pass 30 | 31 | @register.tag('name') 32 | class ComponentClass: 33 | pass 34 | 35 | register.tag_function(ComponentClass) 36 | 37 | register.component('name', ComponentClass) 38 | 39 | @register.component 40 | class ComponentClass: 41 | pass 42 | 43 | Notes 44 | ---- 45 | - Pycharm doesnt recognize @register.component, therefore the recommended way to register all components 46 | using Pycharm is with `register.tag('name', ComponentClass)` 47 | - Does not interfere with the current Django Library behavior, but it decorates the callable function 48 | with the component wrapper if this is a component tag. 49 | """ 50 | 51 | def tag_function(self, func): 52 | if isinstance(func, BaseComponent): 53 | name = getattr(func, "_decorated_function", func).__name__.lower() 54 | name, func = component_wrapper(name, func) 55 | self.tags[name] = func 56 | return func 57 | return super().tag_function(func) 58 | 59 | def tag(self, name=None, compile_function=None): 60 | # @register.tag() 61 | if name is None and compile_function is None: 62 | return self.tag_function 63 | if name is not None and compile_function is None: 64 | # @register.tag 65 | if callable(name): 66 | if isinstance(name, BaseComponent): 67 | component_name = name.__name__.lower() 68 | name, func = component_wrapper(component_name, name) 69 | self.tags[name] = func 70 | return func 71 | return super().tag_function(name) 72 | # @register.tag('foobar') or @register.tag(name='foobar') 73 | else: 74 | tag_func = super().tag 75 | 76 | def dec(f): 77 | if isinstance(f, BaseComponent): 78 | return tag_func(*component_wrapper(name, f)) 79 | return tag_func(name, f) 80 | 81 | return dec 82 | # register.tag('foobar', foobar) 83 | elif name is not None and compile_function is not None: 84 | if isinstance(compile_function, BaseComponent): 85 | name, func = component_wrapper(name, compile_function) 86 | self.tags[name] = func 87 | return func 88 | self.tags[name] = compile_function 89 | return compile_function 90 | else: 91 | raise ValueError("Unsupported arguments to Library.tag: (%r, %r)" % (name, compile_function)) 92 | 93 | def component(self, name=None, compile_function=None): 94 | return self.tag(name=name, compile_function=compile_function) 95 | -------------------------------------------------------------------------------- /config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for foo project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '-#_i0+q7z-6_!dofe+@)f#!d3pfe7^gs#s@6(%!ne$*^!ndu2l' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | INTERNAL_IPS = ['127.0.0.1'] 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'django_extensions', 42 | 'debug_toolbar', 43 | 'component_tags', 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | 'django.middleware.security.SecurityMiddleware', 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.messages.middleware.MessageMiddleware', 53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 54 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 55 | ] 56 | 57 | ROOT_URLCONF = 'config.urls' 58 | 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'DIRS': [ 63 | os.path.join(BASE_DIR, 'templates'), 64 | ], 65 | 'APP_DIRS': True, 66 | 'OPTIONS': { 67 | 'builtins': ['component_tags.template.builtins'], 68 | 'libraries': { 69 | 'test_tags': 'templatetags.test_tags', 70 | }, 71 | 'context_processors': [ 72 | 'django.template.context_processors.debug', 73 | 'django.template.context_processors.request', 74 | 'django.contrib.auth.context_processors.auth', 75 | 'django.contrib.messages.context_processors.messages', 76 | ], 77 | }, 78 | }, 79 | ] 80 | 81 | 82 | CACHES = { 83 | 'default': { 84 | 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 85 | 'LOCATION': '127.0.0.1:11211', 86 | } 87 | } 88 | 89 | WSGI_APPLICATION = 'config.wsgi.application' 90 | 91 | 92 | # Database 93 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 94 | 95 | DATABASES = { 96 | 'default': { 97 | 'ENGINE': 'django.db.backends.sqlite3', 98 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 99 | } 100 | } 101 | 102 | 103 | # Password validation 104 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 105 | 106 | AUTH_PASSWORD_VALIDATORS = [ 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 109 | }, 110 | { 111 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 112 | }, 113 | { 114 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 115 | }, 116 | { 117 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 118 | }, 119 | ] 120 | 121 | 122 | # Internationalization 123 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 124 | 125 | LANGUAGE_CODE = 'en-us' 126 | 127 | TIME_ZONE = 'UTC' 128 | 129 | USE_I18N = True 130 | 131 | USE_L10N = True 132 | 133 | USE_TZ = True 134 | 135 | 136 | # Static files (CSS, JavaScript, Images) 137 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 138 | 139 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 140 | 141 | STATIC_URL = '/static/' 142 | -------------------------------------------------------------------------------- /src/component_tags/tests.py: -------------------------------------------------------------------------------- 1 | from django.template import Context 2 | from django.template.base import Variable 3 | from django.test import TestCase 4 | 5 | from .template.choices import AttributeChoices 6 | from .template.attributes import Attribute 7 | from .template.builtins import register 8 | from .template.components import Slot 9 | 10 | 11 | class ChoiceTestCase(TestCase): 12 | 13 | def test_format(self): 14 | class FooChoices(AttributeChoices): 15 | bar = 'bar' 16 | 17 | self.assertEqual(format(FooChoices.bar), 'bar') 18 | 19 | def test_format_repr(self): 20 | class FooChoices(AttributeChoices): 21 | bar = 1 22 | 23 | self.assertIsInstance(format(FooChoices.bar), str) 24 | 25 | 26 | class AttributeTestCase(TestCase): 27 | 28 | def setUp(self): 29 | class CustomChoices(AttributeChoices): 30 | foo = 'bar' 31 | 32 | self.choices = CustomChoices 33 | 34 | def test_is_instance(self): 35 | self.assertIsInstance(Attribute(), Attribute) 36 | 37 | def test_choices(self): 38 | attr = Attribute(choices=self.choices) 39 | x = Variable('attr') 40 | c = Context({'attr': 'foo'}) 41 | self.assertEqual(attr.resolve(x, c), 'bar') 42 | 43 | def test_choice_does_not_exist(self): 44 | attr = Attribute(choices=self.choices, required=True) 45 | x = Variable('attr') 46 | c = Context({'attr': 'ERROR'}) 47 | self.assertRaises(Attribute.ChoiceDoesNotExist, lambda: attr.resolve(x, c)) 48 | 49 | attr = Attribute(choices=self.choices, required=True) 50 | x = Variable('attr') 51 | c = Context({'attr': 'ERROR'}) 52 | self.assertRaises(Attribute.ChoiceDoesNotExist, lambda: attr.get_choice('x', raise_exception=True)) 53 | 54 | def test_choices_default(self): 55 | attr = Attribute(choices=self.choices, default=self.choices.foo) 56 | 57 | try: 58 | # Not placing the attr inside context on purpose 59 | x = Variable('attr') 60 | c = Context() 61 | value = attr.resolve(x, c) 62 | except Attribute.VariableDoesNotExist: 63 | value = attr.default 64 | 65 | self.assertEqual(value, 'bar') 66 | 67 | def test_variable_does_not_exist(self): 68 | attr = Attribute(choices=self.choices) 69 | 70 | def test_func(): 71 | x = Variable('attr') 72 | c = Context() 73 | return attr.resolve(x, c) 74 | 75 | self.assertRaises(Attribute.VariableDoesNotExist, test_func) 76 | 77 | def test_choices_are_required_and_value_is_none(self): 78 | attr = Attribute(choices=self.choices, required=True) 79 | 80 | def test_func(): 81 | try: 82 | x = Variable('attr') 83 | c = Context({'attr': None}) 84 | return attr.resolve(x, c) 85 | except Attribute.VariableDoesNotExist: 86 | return attr.default 87 | 88 | self.assertRaises(Attribute.RequiredValue, test_func) 89 | 90 | def test_choices_are_not_enum_type(self): 91 | class Choices: 92 | bar = 1 93 | 94 | def test_with_class(): 95 | return Attribute(choices=Choices) 96 | 97 | def test_with_list(): 98 | return Attribute(choices=[{'foo': 'bar'}]) 99 | 100 | self.assertRaises(Attribute.RequiredValue, test_with_class) 101 | self.assertRaises(Attribute.RequiredValue, test_with_list) 102 | 103 | def test_set_context_name(self): 104 | attr = Attribute(choices=self.choices) 105 | attr.set_context_name('foo') 106 | self.assertEqual(attr.context_name, 'foo') 107 | 108 | 109 | class BuiltinsTestCase(TestCase): 110 | 111 | def setUp(self) -> None: 112 | self.BUILTIN_TAGS = [ 113 | ('slot', Slot) 114 | ] 115 | 116 | def test_default_tags(self): 117 | for name, i in self.BUILTIN_TAGS: 118 | self.assertIn(name, register.tags) 119 | self.assertTrue(issubclass(register.tags[name].__wrapped__, Slot)) 120 | 121 | 122 | class ComponentTestCase(TestCase): 123 | pass 124 | 125 | 126 | class ContextTestCase(TestCase): 127 | pass 128 | 129 | 130 | class HelperTestCase(TestCase): 131 | pass 132 | 133 | 134 | class LibraryTestCase(TestCase): 135 | pass 136 | 137 | 138 | class NodeTestCase(TestCase): 139 | pass 140 | 141 | 142 | class ParserTestCase(TestCase): 143 | pass 144 | 145 | 146 | class WrapperTestCase(TestCase): 147 | pass 148 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # This file is used to configure your project. 2 | # Read more about the various options under: 3 | # http://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files 4 | 5 | [metadata] 6 | name = django_component_tags 7 | description = Create advanced HTML components using Django Tags. 8 | author = David Sosa Valdes 9 | author_email = david.sosa.valdes@gmail.com 10 | license = MIT 11 | long_description = file: README.rst 12 | long_description_content_type = text/x-rst; charset=UTF-8 13 | url = https://github.com/syse-i/django-component-tags 14 | keywords = 15 | "django" 16 | "component" 17 | "tags" 18 | "templatetags" 19 | "template" 20 | # Add here related links, for example: 21 | project_urls = 22 | Documentation = https://github.com/syse-i/django-component-tags#description 23 | Source = https://github.com/syse-i/django-component-tags 24 | Changelog = https://github.com/syse-i/django-component-tags/blob/main/CHANGELOG.rst 25 | Tracker = https://github.com/syse-i/django-component-tags/issues 26 | # Conda-Forge = https://anaconda.org/conda-forge/pyscaffold 27 | # Download = https://pypi.org/project/PyScaffold/#files 28 | # Twitter = https://twitter.com/PyScaffold 29 | 30 | # Change if running only on Windows, Mac or Linux (comma-separated) 31 | platforms = any 32 | 33 | # Add here all kinds of additional classifiers as defined under 34 | # https://pypi.python.org/pypi?%3Aaction=list_classifiers 35 | classifiers = 36 | Development Status :: 4 - Beta 37 | Programming Language :: Python 38 | Programming Language :: Python :: 3 39 | Programming Language :: Python :: 3.6 40 | Programming Language :: Python :: 3.7 41 | Programming Language :: Python :: 3.8 42 | Programming Language :: Python :: 3.9 43 | Framework :: Django 44 | Framework :: Django :: 2.2 45 | Framework :: Django :: 3.0 46 | Framework :: Django :: 3.1 47 | Framework :: Django 48 | 49 | 50 | [options] 51 | zip_safe = False 52 | packages = find_namespace: 53 | include_package_data = True 54 | package_dir = 55 | =src 56 | 57 | # Require a min/specific Python version (comma-separated conditions) 58 | # python_requires = >=3.8 59 | 60 | # Add here dependencies of your project (line-separated) 61 | # TODO: Remove conditional dependencies according to `python_requires` above 62 | install_requires = 63 | importlib-metadata; python_version<"3.8" 64 | django 65 | 66 | 67 | [options.packages.find] 68 | where = src 69 | exclude = 70 | tests 71 | 72 | [options.extras_require] 73 | # Add here additional requirements for extra features, to install with: 74 | # `pip install django_component_tags[PDF]` like: 75 | # PDF = ReportLab; RXP 76 | 77 | # Add here test requirements (semicolon/line-separated) 78 | testing = 79 | setuptools 80 | pytest 81 | pytest-cov 82 | pytest-django 83 | 84 | [options.entry_points] 85 | # Add here console scripts like: 86 | # console_scripts = 87 | # script_name = django_component_tags.module:function 88 | # For example: 89 | # console_scripts = 90 | # fibonacci = django_component_tags.skeleton:run 91 | # And any other entry points, for example: 92 | # pyscaffold.cli = 93 | # awesome = pyscaffoldext.awesome.extension:AwesomeExtension 94 | 95 | [tool:pytest] 96 | # Specify command line options as you would do when invoking pytest directly. 97 | # e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml 98 | # in order to write a coverage file that can be read by Jenkins. 99 | # CAUTION: --cov flags may prohibit setting breakpoints while debugging. 100 | # Comment those flags to avoid this py.test issue. 101 | DJANGO_SETTINGS_MODULE = config.settings 102 | ;addopts = 103 | ; --cov component_tags --cov-report html 104 | ; --verbose 105 | norecursedirs = 106 | dist 107 | build 108 | .tox 109 | testpaths = src/component_tags 110 | # -- recommended but optional: 111 | python_files = tests.py test_*.py *_tests.py 112 | # Use pytest markers to select/deselect specific tests 113 | markers = 114 | slow: mark tests as slow (deselect with '-m "not slow"') 115 | system: mark end-to-end system tests 116 | 117 | [bdist_wheel] 118 | # Use this option if your package is pure-python 119 | universal = 1 120 | 121 | [devpi:upload] 122 | # Options for the devpi: PyPI server and packaging tool 123 | # VCS export must be deactivated since we are using setuptools-scm 124 | no-vcs = 1 125 | formats = bdist_wheel 126 | 127 | [flake8] 128 | # Some sane defaults for the code style checker flake8 129 | max-line-length = 88 130 | extend-ignore = E203, W503 131 | # ^ Black-compatible 132 | # E203 and W503 have edge cases handled by black 133 | exclude = 134 | .tox 135 | build 136 | dist 137 | .eggs 138 | docs/conf.py 139 | 140 | [pyscaffold] 141 | # PyScaffold's parameters when the project was created. 142 | # This will be used when updating. Do not change! 143 | version = 4.0rc2 144 | package = django_component_tags 145 | extensions = 146 | -------------------------------------------------------------------------------- /src/component_tags/template/nodes.py: -------------------------------------------------------------------------------- 1 | from inspect import getmembers 2 | from copy import copy 3 | 4 | from django.forms.widgets import Media 5 | from django.template.base import Node, NodeList 6 | 7 | from .media import add_media 8 | from .attributes import Attribute 9 | from .context import ComponentContext 10 | 11 | 12 | __all__ = ['ComponentNode', 'BaseComponent', 'Meta', 'Media'] 13 | 14 | 15 | class TemplateIsNull(Exception): 16 | pass 17 | 18 | 19 | class Meta(Media): 20 | """ 21 | Meta definition of component tags, currently only used to declare template information 22 | """ 23 | def __init__(self, meta=None, css=None, js=None): 24 | super().__init__(meta, css, js) 25 | self.template_name = getattr(meta, 'template_name', None) 26 | 27 | 28 | def meta_property(cls): 29 | """ 30 | Get the media property of the superclass, if it exists 31 | """ 32 | def _meta(self): 33 | 34 | sup_cls = super(cls, self) 35 | try: 36 | # noinspection PyUnresolvedReferences 37 | base = sup_cls.meta 38 | except AttributeError: 39 | base = Meta() 40 | 41 | # Get the media definition for this class 42 | definition = getattr(cls, 'Meta', None) 43 | 44 | if definition: 45 | extend = getattr(definition, 'extend', True) 46 | if extend: 47 | if extend is True: 48 | m = base 49 | else: 50 | m = Meta() 51 | for medium in extend: 52 | m = m + base[medium] 53 | extended = m + Meta(definition) 54 | else: 55 | extended = Meta(definition) 56 | 57 | template_name = getattr(definition, 'template_name', None) 58 | if template_name: 59 | setattr(extended, 'template_name', template_name) 60 | return extended 61 | return base 62 | return property(_meta) 63 | 64 | 65 | class BaseComponent(type): 66 | """Metaclass for all component nodes.""" 67 | 68 | def __new__(mcs, name, bases, attrs): 69 | new_class = super().__new__(mcs, name, bases, attrs) 70 | 71 | if 'meta' not in attrs: 72 | new_class.meta = meta_property(new_class) 73 | 74 | return new_class 75 | 76 | 77 | class ComponentNode(Node, metaclass=BaseComponent): 78 | """ 79 | Components are used to mark up the start of an HTML element 80 | and they are usually enclosed in angle brackets. 81 | """ 82 | 83 | TemplateIsNull = TemplateIsNull 84 | 85 | def __init__(self, tag_name: str, nodelist: NodeList, options: dict, slots: dict, *args, 86 | isolated_context: bool = True, **kwargs): 87 | self.tag_name = tag_name 88 | self.nodelist = nodelist 89 | self.attrs = kwargs 90 | self.slots = slots 91 | self.options = options 92 | self.isolated_context = isolated_context 93 | 94 | def get_template_name(self): 95 | return getattr(self.meta, 'template_name', None) 96 | 97 | def get_template(self, context): 98 | template_name = self.get_template_name() 99 | 100 | if not template_name: 101 | raise self.TemplateIsNull(f'[{self.tag_name.title()}] component does not have a template assigned.') 102 | 103 | return context.template.engine.get_template(template_name) 104 | 105 | def get_context_data(self, context): 106 | return ComponentContext(self.nodelist, initial=context, isolated=self.isolated_context) 107 | 108 | def render(self, context): 109 | add_media(context, self.meta) 110 | template = self.get_template(context) 111 | 112 | # Does this quack like a Template? 113 | if not callable(getattr(template, 'render', None)): 114 | # If not, try the cache and select_template(). 115 | template_name = template or () 116 | if isinstance(template_name, str): 117 | template_name = (template_name,) 118 | else: 119 | template_name = tuple(template_name) 120 | cache = context.render_context.dicts[0].setdefault(self, {}) 121 | template = cache.get(template_name) 122 | 123 | if template is None: 124 | template = context.template.engine.select_template(template_name) 125 | cache[template_name] = template 126 | 127 | # Use the base.Template of a backends.django.Template. 128 | elif hasattr(template, 'template'): 129 | template = template.template 130 | 131 | attrs = self.attrs.copy() 132 | 133 | # Do not use original context since we are updating values inside this function 134 | _context = self.get_context_data(copy(context)) 135 | 136 | # Class attributes 137 | class_attrs = getmembers(self, lambda a: isinstance(a, Attribute)) 138 | 139 | while class_attrs: 140 | key, attr = class_attrs.pop() 141 | 142 | if not attr.context_name: 143 | attr.set_context_name(key) 144 | 145 | try: 146 | value = attr.resolve(attrs.pop(key), context) 147 | except KeyError: 148 | value = attr.default 149 | 150 | key = attr.context_name 151 | 152 | if attr.as_context: 153 | _context[key] = value 154 | elif attr.as_class: 155 | _context.add_class(value) 156 | else: 157 | _context.add_attribute(key, value) 158 | 159 | # Attribute variables 160 | for name, value in attrs.items(): 161 | _context.add_attribute(name, value) 162 | 163 | # Option variables 164 | for name, value in self.options.items(): 165 | _context[name] = value.resolve(context) 166 | 167 | context = _context.make() # Slots should only have access to parent context 168 | 169 | # Slot nodes 170 | for name, value in self.slots.items(): 171 | _context[name] = value.render(context) 172 | 173 | return template.render(context) 174 | -------------------------------------------------------------------------------- /src/component_tags/template/attributes.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Optional, Union 3 | 4 | from django.template.base import Variable, FilterExpression, VariableDoesNotExist 5 | 6 | __all__ = ['Attribute'] 7 | 8 | 9 | class ChoiceDoesNotExist(Exception): 10 | """ 11 | Raised when the specified choice is not a choice's member 12 | """ 13 | pass 14 | 15 | 16 | class RequiredValue(Exception): 17 | """ 18 | Raised when a variable value is required 19 | """ 20 | pass 21 | 22 | 23 | class Attribute: 24 | """ 25 | Template Components have attributes; these are additional values that configure the elements or adjust their 26 | behavior in various ways to meet the criteria the users want. 27 | 28 | Attributes 29 | ---------- 30 | name : str 31 | name used to identify it inside the context 32 | choices: AttributeChoices 33 | choices are used to specify the attribute correct value from a list of available options 34 | default: Any 35 | default attribute value 36 | required: bool 37 | set if the attribute value is required 38 | as_class: bool 39 | set the attribute as "class type" inside the template 40 | as_context: bool 41 | set the attribute as "context type" inside the template 42 | 43 | Examples 44 | -------- 45 | **Attribute as class** 46 | 47 | Considering the following definition: 48 | 49 | .. code-block:: python 50 | 51 | # templatetags/custom_tags.py 52 | 53 | @register.component 54 | class Button(template.Component): 55 | class ColorChoices(template.AttributeChoices): 56 | primary = 'btn-primary' 57 | secondary = 'btn-secondary' 58 | 59 | color = template.Attribute(choices=ColorChoices, default=ColorChoices.primary, as_class=True) 60 | 61 | class Meta: 62 | template_name = 'components/button.html' 63 | 64 | And the following template 65 | 66 | .. code-block:: 67 | 68 | # templates/components/button.html 69 | 70 | 71 | 72 | Output 73 | 74 | .. code-block:: 75 | 76 | # Setting a value 77 | {% button color="secondary" %}{% endbutton %} 78 | 79 | # output: 80 | 81 | 82 | # Without setting a value 83 | {% button %}{% endbutton %} 84 | 85 | # output: 86 | 87 | 88 | **Attribute as context** 89 | 90 | Considering the following definition: 91 | 92 | .. code-block:: python 93 | 94 | class Button(template.Component): 95 | href = template.Attribute(default="#", as_context=True) 96 | 97 | You can use this attribute inside the component context as follows: 98 | 99 | .. code-block:: 100 | 101 | # Setting a value 102 | {% button href="/some-url" %} 103 | {{ href }} # output: "/some-url" 104 | {% endbutton %} 105 | 106 | # Without setting a value 107 | {% button %} 108 | {{ href }} # output: "#" 109 | {% endbutton %} 110 | """ 111 | 112 | ChoiceDoesNotExist = ChoiceDoesNotExist 113 | VariableDoesNotExist = VariableDoesNotExist 114 | RequiredValue = RequiredValue 115 | 116 | def __init__(self, choices=None, default=None, required: bool = False, context_name: str = None, 117 | as_class: bool = False, as_context: bool = False): 118 | self.context_name = context_name 119 | 120 | try: 121 | if choices and not issubclass(choices, Enum): 122 | raise RequiredValue("choices must be Enum's subclass") 123 | except TypeError: 124 | raise RequiredValue("choices must be Enum's subclass") 125 | 126 | self.choices = choices 127 | self.required = required 128 | self.default = default 129 | self.as_class = as_class 130 | self.as_context = as_context 131 | 132 | @property 133 | def default(self): 134 | return self._default 135 | 136 | @default.setter 137 | def default(self, value: Optional[Enum]): 138 | """ 139 | Sets the default value and checks if can be applied. 140 | """ 141 | self._default = self.check_value(value) if value is not None else value 142 | 143 | def get_member_choices(self): 144 | """ 145 | Get the member choices as list 146 | """ 147 | return [key for key, value in self.choices.__members__.items()] 148 | 149 | def get_choice(self, key: Union[Enum, str], raise_exception: bool = False): 150 | """ 151 | Get the member choice value using the Enum format 152 | """ 153 | if not raise_exception: 154 | raise_exception = self.required 155 | 156 | try: 157 | # noinspection PyCallingNonCallable,PyArgumentList 158 | return format(key if isinstance(key, self.choices) else self.choices[key]) 159 | except (KeyError, ValueError): 160 | if raise_exception: 161 | raise ChoiceDoesNotExist(f'{key} is not an available choice, choices are: {self.get_member_choices()}') 162 | 163 | def check_value(self, value, raise_exception: bool = False): 164 | """ 165 | Verify if the current value is valid based on different criteria 166 | """ 167 | if self.required and value is None: 168 | raise RequiredValue(f'{self.context_name} should be different than null') 169 | return self.get_choice(value, raise_exception) if self.choices else value 170 | 171 | def set_context_name(self, value: str): 172 | """ 173 | The name of the attribute used as context name. 174 | 175 | Sometimes declaring the names it is not enough to get the result needed, specially when you want to 176 | work with javascript. 177 | 178 | Examples 179 | -------- 180 | 181 | **As a class instance:** 182 | 183 | .. code-block:: python 184 | 185 | class Button(template.Component): 186 | href = template.Attribute(default="#", as_context=True) 187 | 188 | Output: 189 | 190 | .. code-block:: 191 | 192 | # base.html 193 | 194 | # Setting a value 195 | {% button href="foo-bar/" %}{% button %} 196 | 197 | # Output: 198 | 199 | 200 | # Without setting a value 201 | {% button %}{% button %} 202 | 203 | # Output: 204 | 205 | 206 | **As parameter:** 207 | 208 | .. code-block:: python 209 | 210 | class Button(template.Component): 211 | href = template.Attribute(default="#", name="data-href") 212 | 213 | Output: 214 | 215 | .. code-block:: 216 | 217 | # base.html 218 | 219 | # Setting a value 220 | {% button href="foo-bar/" %}{% button %} 221 | 222 | # Output: 223 | 224 | 225 | # Without setting a value 226 | {% button %}{% button %} 227 | 228 | # Output: 229 | 230 | 231 | """ 232 | self.context_name = value 233 | 234 | def resolve(self, value: Union[Variable, FilterExpression, str], context, raise_exception: bool = True): 235 | """ 236 | Resolves the template variable/expression specified as an argument, then checks if the value can be used. 237 | """ 238 | try: 239 | return self.check_value(value.resolve(context), raise_exception=raise_exception) 240 | except VariableDoesNotExist as ex: 241 | raise self.VariableDoesNotExist(ex) 242 | -------------------------------------------------------------------------------- /src/component_tags/template/context.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | from django.template import Context, RequestContext 4 | from django.template.base import NodeList 5 | 6 | from .helpers import format_attributes, format_value 7 | 8 | __all__ = [ 9 | 'BaseContext', 10 | 'Context', 11 | 'TagContext', 12 | 'ComponentContext' 13 | ] 14 | 15 | 16 | class BaseContext: 17 | """ 18 | Context wrapper which includes a couple of methods to create the same outcome. 19 | 20 | Attributes 21 | ---------- 22 | initial: Union[Context, RequestContext, None] 23 | current template context 24 | dict_: dict 25 | extra values added to the context 26 | isolated: bool 27 | ensures that the context is isolated from the global context 28 | """ 29 | 30 | default_class = Context 31 | 32 | def __init__(self, initial: Union[Context, RequestContext, None], dict_: dict, isolated: bool = True): 33 | if initial is None: 34 | self._wrap = self.default_class(dict_) 35 | elif isinstance(initial, (Context, RequestContext)): 36 | self._wrap = initial 37 | else: 38 | raise Exception('Cannot define initial') 39 | 40 | if isolated: 41 | self._wrap = self.new(dict_) 42 | else: 43 | self.update(dict_) 44 | 45 | def __copy__(self): 46 | return self._wrap.__copy__() 47 | 48 | def __repr__(self): 49 | return self._wrap.__repr__() 50 | 51 | def __iter__(self): 52 | return self._wrap.__iter__() 53 | 54 | def __setitem__(self, key, value): 55 | self._wrap.__setitem__(key, self.resolve(value)) 56 | 57 | def __getitem__(self, key): 58 | """ 59 | Get a variable's value, starting at the current context and going upward 60 | """ 61 | return self._wrap.__getitem__(key) 62 | 63 | def __delitem__(self, key): 64 | """ 65 | Delete a variable from the current context 66 | """ 67 | self._wrap.__delitem__(key) 68 | 69 | def __contains__(self, key): 70 | return self._wrap.__contains__(key) 71 | 72 | def __eq__(self, other): 73 | """ 74 | Compare two contexts by comparing theirs 'dicts' attributes. 75 | """ 76 | return self._wrap.__eq__(other) 77 | 78 | def clean(self): 79 | """ 80 | Return the wrapped context 81 | """ 82 | return self._wrap 83 | 84 | def flatten(self) -> dict: 85 | """ 86 | Return self.dicts as one dictionary. 87 | """ 88 | return self.make().flatten() 89 | 90 | def push(self, *args, **kwargs): 91 | return self._wrap.push(*args, **kwargs) 92 | 93 | def pop(self): 94 | return self._wrap.pop() 95 | 96 | def set_upward(self, key, value): 97 | """ 98 | Set a variable in one of the higher contexts if it exists there, 99 | otherwise in the current context. 100 | """ 101 | self._wrap.set_upward(key, self.resolve(value)) 102 | 103 | def get(self, key, otherwise=None): 104 | return self._wrap.get(key, otherwise) 105 | 106 | def setdefault(self, key, default=None): 107 | return self._wrap.setdefault(key, default) 108 | 109 | def new(self, values=None): 110 | """ 111 | Return a new context with the same properties, but with only the 112 | values given in 'values' stored. 113 | """ 114 | return self._wrap.new(values) 115 | 116 | def update(self, *args, **kwargs): 117 | return self._wrap.update(*args, **kwargs) 118 | 119 | def make(self): 120 | """ 121 | Used to exec any function before compile the component tag. 122 | """ 123 | return self.clean() 124 | 125 | def resolve(self, value): 126 | """ 127 | Resolve the Variable/FilterExpression using the wrapped context 128 | """ 129 | return format_value(value, self._wrap) 130 | 131 | 132 | class TagContext(BaseContext): 133 | """ 134 | Context used inside a tag function, if there is no need to create a component tag this is the way to 135 | recreate the same context. 136 | 137 | Attributes 138 | ---------- 139 | attributes: Optional[dict] 140 | html attributes stored inside the context, and can be used as: {{ attributes }} 141 | initial: Optional[RequestContext] 142 | current template context 143 | isolated: bool 144 | ensures that the context is isolated from the global context 145 | **kwargs: dict 146 | extra values added to the context 147 | """ 148 | 149 | default_class = RequestContext 150 | 151 | def __init__(self, attributes: Optional[dict] = None, initial: Optional[RequestContext] = None, 152 | isolated: bool = False, **kwargs): 153 | super().__init__(initial, kwargs, isolated=isolated) 154 | self._attributes = {} if attributes is None else attributes 155 | 156 | @property 157 | def _attributes(self): 158 | return self['attributes'] 159 | 160 | @_attributes.setter 161 | def _attributes(self, values: dict): 162 | if not isinstance(values, dict): 163 | raise Exception('Set attrs as a dict') 164 | self['attributes'] = values 165 | 166 | def add_class(self, *value): 167 | """ 168 | Add a html "class" attribute inside the context. 169 | There can be used multiple times to store multiple classes in different scenarios. 170 | 171 | TODO: class as list to perform: 172 | - context.class.append 173 | - context.class = | 174 | - context.class.remove() 175 | - context.class.reset() === list() 176 | """ 177 | 178 | values = [self.resolve(v) for v in value] 179 | 180 | try: 181 | self._attributes['class'].extend(values) 182 | except AttributeError: # cannot extend 183 | classes = self._attributes['class'] 184 | self._attributes['class'] = [classes] + values 185 | except KeyError: # class not defined 186 | self._attributes['class'] = values 187 | 188 | def add_attribute(self, name: str, value): 189 | """ 190 | Add a html attribute inside the context. 191 | There can be used multiple times to store multiple key/values in different scenarios. 192 | 193 | TODO: attribute as dict to perform: 194 | - context.attributes[] = 195 | - context.attributes = 196 | - del context.attributes[] 197 | - context.attributes.reset() === dict() 198 | """ 199 | value = self.resolve(value) 200 | 201 | # is this a class attribute? 202 | if name in 'class': 203 | return self.add_class(value) 204 | 205 | self._attributes[name] = value 206 | 207 | def make(self): 208 | """ 209 | Collection of actions executed before render the component: 210 | - Format attributes as strings 211 | """ 212 | context = super().make() 213 | context['attributes'] = format_attributes(self._attributes, context) 214 | return context 215 | 216 | 217 | class ComponentContext(TagContext): 218 | """ 219 | Context used inside component tags. 220 | 221 | Attributes 222 | ---------- 223 | nodelist: NodeList 224 | nodelist passed to the component context 225 | initial: Optional[RequestContext] 226 | current template context 227 | attributes: Optional[dict] 228 | html attributes stored inside the context, and can be used as: {{ attributes }} 229 | isolated: bool 230 | ensures that the context is isolated from the global context 231 | **kwargs: dict 232 | extra values added to the context 233 | """ 234 | def __init__(self, nodelist: NodeList, initial: RequestContext, attributes: Optional[dict] = None, 235 | isolated: bool = True, **kwargs): 236 | super().__init__(attributes, initial=initial, isolated=isolated, **kwargs) 237 | self._nodelist = nodelist 238 | 239 | # TODO: get rid of this implementation 240 | # Make sure that request is part of the context 241 | if 'request' not in self and getattr(self._wrap, 'request', False): 242 | self['request'] = self._wrap.request 243 | 244 | def make(self): 245 | """ 246 | Collection of actions executed before render the component: 247 | - Format attributes as strings 248 | - Render the current nodelist and store it inside the current context 249 | """ 250 | context = super().make() 251 | context['nodelist'] = self._nodelist.render(context) 252 | return context 253 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # This file is execfile()d with the current directory set to its containing dir. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | # 7 | # All configuration values have a default; values that are commented out 8 | # serve to show the default. 9 | 10 | import os 11 | import sys 12 | import inspect 13 | import shutil 14 | 15 | # -- Path setup -------------------------------------------------------------- 16 | 17 | __location__ = os.path.join( 18 | os.getcwd(), os.path.dirname(inspect.getfile(inspect.currentframe())) 19 | ) 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | sys.path.insert(0, os.path.join(__location__, "../src")) 25 | 26 | # -- Run sphinx-apidoc ------------------------------------------------------- 27 | # This hack is necessary since RTD does not issue `sphinx-apidoc` before running 28 | # `sphinx-build -b html . _build/html`. See Issue: 29 | # https://github.com/rtfd/readthedocs.org/issues/1139 30 | # DON'T FORGET: Check the box "Install your project inside a virtualenv using 31 | # setup.py install" in the RTD Advanced Settings. 32 | # Additionally it helps us to avoid running apidoc manually 33 | 34 | try: # for Sphinx >= 1.7 35 | from sphinx.ext import apidoc 36 | except ImportError: 37 | # noinspection PyUnresolvedReferences 38 | from sphinx import apidoc 39 | 40 | output_dir = os.path.join(__location__, "api") 41 | module_dir = os.path.join(__location__, "../src/component_tags") 42 | try: 43 | shutil.rmtree(output_dir) 44 | except FileNotFoundError: 45 | pass 46 | 47 | try: 48 | import sphinx 49 | 50 | cmd_line_template = ( 51 | "sphinx-apidoc --implicit-namespaces -f -o {outputdir} {moduledir}" 52 | ) 53 | cmd_line = cmd_line_template.format(outputdir=output_dir, moduledir=module_dir) 54 | 55 | args = cmd_line.split(" ") 56 | if tuple(sphinx.__version__.split(".")) >= ("1", "7"): 57 | # This is a rudimentary parse_version to avoid external dependencies 58 | args = args[1:] 59 | 60 | apidoc.main(args) 61 | except Exception as e: 62 | print("Running `sphinx-apidoc` failed!\n{}".format(e)) 63 | 64 | # -- General configuration --------------------------------------------------- 65 | 66 | # If your documentation needs a minimal Sphinx version, state it here. 67 | # needs_sphinx = '1.0' 68 | 69 | # Add any Sphinx extension module names here, as strings. They can be extensions 70 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 71 | extensions = [ 72 | "sphinx.ext.autodoc", 73 | "sphinx.ext.intersphinx", 74 | "sphinx.ext.todo", 75 | "sphinx.ext.autosummary", 76 | "sphinx.ext.viewcode", 77 | "sphinx.ext.coverage", 78 | "sphinx.ext.doctest", 79 | "sphinx.ext.ifconfig", 80 | "sphinx.ext.mathjax", 81 | "sphinx.ext.napoleon", 82 | ] 83 | 84 | # Add any paths that contain templates here, relative to this directory. 85 | templates_path = ["_templates"] 86 | 87 | # The suffix of source filenames. 88 | source_suffix = ".rst" 89 | 90 | # The encoding of source files. 91 | # source_encoding = 'utf-8-sig' 92 | 93 | # The master toctree document. 94 | master_doc = "index" 95 | 96 | # General information about the project. 97 | project = "component_tags" 98 | copyright = "2021, David Sosa Valdes" 99 | 100 | # The version info for the project you're documenting, acts as replacement for 101 | # |version| and |release|, also used in various other places throughout the 102 | # built documents. 103 | # 104 | # The short X.Y version. 105 | version = "" # Is set by calling `setup.py docs` 106 | # The full version, including alpha/beta/rc tags. 107 | release = "" # Is set by calling `setup.py docs` 108 | 109 | # The language for content autogenerated by Sphinx. Refer to documentation 110 | # for a list of supported languages. 111 | # language = None 112 | 113 | # There are two options for replacing |today|: either, you set today to some 114 | # non-false value, then it is used: 115 | # today = '' 116 | # Else, today_fmt is used as the format for a strftime call. 117 | # today_fmt = '%B %d, %Y' 118 | 119 | # List of patterns, relative to source directory, that match files and 120 | # directories to ignore when looking for source files. 121 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".venv", "test*.py"] 122 | 123 | # The reST default role (used for this markup: `text`) to use for all documents. 124 | # default_role = None 125 | 126 | # If true, '()' will be appended to :func: etc. cross-reference text. 127 | # add_function_parentheses = True 128 | 129 | # If true, the current module name will be prepended to all description 130 | # unit titles (such as .. function::). 131 | # add_module_names = True 132 | 133 | # If true, sectionauthor and moduleauthor directives will be shown in the 134 | # output. They are ignored by default. 135 | # show_authors = False 136 | 137 | # The name of the Pygments (syntax highlighting) style to use. 138 | pygments_style = "sphinx" 139 | 140 | # A list of ignored prefixes for module index sorting. 141 | # modindex_common_prefix = [] 142 | 143 | # If true, keep warnings as "system message" paragraphs in the built documents. 144 | # keep_warnings = False 145 | 146 | 147 | # -- Options for HTML output ------------------------------------------------- 148 | 149 | # The theme to use for HTML and HTML Help pages. See the documentation for 150 | # a list of builtin themes. 151 | html_theme = "alabaster" 152 | 153 | # Theme options are theme-specific and customize the look and feel of a theme 154 | # further. For a list of options available for each theme, see the 155 | # documentation. 156 | html_theme_options = { 157 | "sidebar_width": "300px", 158 | "page_width": "1200px" 159 | } 160 | 161 | # Add any paths that contain custom themes here, relative to this directory. 162 | # html_theme_path = [] 163 | 164 | # The name for this set of Sphinx documents. If None, it defaults to 165 | # " v documentation". 166 | try: 167 | from component_tags import __version__ as version 168 | except ImportError: 169 | pass 170 | else: 171 | release = version 172 | 173 | # A shorter title for the navigation bar. Default is the same as html_title. 174 | # html_short_title = None 175 | 176 | # The name of an image file (relative to this directory) to place at the top 177 | # of the sidebar. 178 | # html_logo = "" 179 | 180 | # The name of an image file (within the static path) to use as favicon of the 181 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 182 | # pixels large. 183 | # html_favicon = None 184 | 185 | # Add any paths that contain custom static files (such as style sheets) here, 186 | # relative to this directory. They are copied after the builtin static files, 187 | # so a file named "default.css" will overwrite the builtin "default.css". 188 | html_static_path = ["_static"] 189 | 190 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 191 | # using the given strftime format. 192 | # html_last_updated_fmt = '%b %d, %Y' 193 | 194 | # If true, SmartyPants will be used to convert quotes and dashes to 195 | # typographically correct entities. 196 | # html_use_smartypants = True 197 | 198 | # Custom sidebar templates, maps document names to template names. 199 | # html_sidebars = {} 200 | 201 | # Additional templates that should be rendered to pages, maps page names to 202 | # template names. 203 | # html_additional_pages = {} 204 | 205 | # If false, no module index is generated. 206 | # html_domain_indices = True 207 | 208 | # If false, no index is generated. 209 | # html_use_index = True 210 | 211 | # If true, the index is split into individual pages for each letter. 212 | # html_split_index = False 213 | 214 | # If true, links to the reST sources are added to the pages. 215 | # html_show_sourcelink = True 216 | 217 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 218 | # html_show_sphinx = True 219 | 220 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 221 | # html_show_copyright = True 222 | 223 | # If true, an OpenSearch description file will be output, and all pages will 224 | # contain a tag referring to it. The value of this option must be the 225 | # base URL from which the finished HTML is served. 226 | # html_use_opensearch = '' 227 | 228 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 229 | # html_file_suffix = None 230 | 231 | # Output file base name for HTML help builder. 232 | htmlhelp_basename = "component_tags-doc" 233 | 234 | 235 | # -- Options for LaTeX output ------------------------------------------------ 236 | 237 | latex_elements = { 238 | # The paper size ("letterpaper" or "a4paper"). 239 | # "papersize": "letterpaper", 240 | # The font size ("10pt", "11pt" or "12pt"). 241 | # "pointsize": "10pt", 242 | # Additional stuff for the LaTeX preamble. 243 | # "preamble": "", 244 | } 245 | 246 | # Grouping the document tree into LaTeX files. List of tuples 247 | # (source start file, target name, title, author, documentclass [howto/manual]). 248 | latex_documents = [ 249 | ("index", "user_guide.tex", "component_tags Documentation", "David Sosa Valdes", "manual") 250 | ] 251 | 252 | # The name of an image file (relative to this directory) to place at the top of 253 | # the title page. 254 | # latex_logo = "" 255 | 256 | # For "manual" documents, if this is true, then toplevel headings are parts, 257 | # not chapters. 258 | # latex_use_parts = False 259 | 260 | # If true, show page references after internal links. 261 | # latex_show_pagerefs = False 262 | 263 | # If true, show URL addresses after external links. 264 | # latex_show_urls = False 265 | 266 | # Documents to append as an appendix to all manuals. 267 | # latex_appendices = [] 268 | 269 | # If false, no module index is generated. 270 | # latex_domain_indices = True 271 | 272 | # -- External mapping -------------------------------------------------------- 273 | python_version = ".".join(map(str, sys.version_info[0:2])) 274 | intersphinx_mapping = { 275 | "sphinx": ("http://www.sphinx-doc.org/en/stable", None), 276 | "python": ("https://docs.python.org/" + python_version, None), 277 | "matplotlib": ("https://matplotlib.org", None), 278 | "numpy": ("https://docs.scipy.org/doc/numpy", None), 279 | "sklearn": ("https://scikit-learn.org/stable", None), 280 | "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), 281 | "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), 282 | "pyscaffold": ("https://pyscaffold.org/en/stable", None), 283 | } 284 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | Django - Component Tags 3 | ======================= 4 | 5 | :Test Status: 6 | .. image:: https://img.shields.io/github/workflow/status/syse-i/django-component-tags/Run%20tests 7 | :alt: GitHub Workflow Status 8 | 9 | :Version Info: 10 | .. image:: https://img.shields.io/pypi/v/django-component-tags?label=PyPi 11 | :alt: PyPI 12 | 13 | .. image:: https://img.shields.io/pypi/dm/django-component-tags?label=Downloads&style=flat-square 14 | :alt: PyPI - Downloads 15 | 16 | :Compatibility: 17 | .. image:: https://img.shields.io/pypi/pyversions/django-component-tags?style=flat-square&label=Python%20Versions 18 | :target: https://pypi.org/project/coveralls/ 19 | 20 | .. image:: https://img.shields.io/pypi/djversions/django-component-tags?label=Django%20Versions&style=flat-square 21 | :alt: PyPI - Django Version 22 | 23 | Create advanced HTML components using Django Tags. 24 | 25 | 26 | Description 27 | =========== 28 | 29 | Use `Django Template Tags `_ and write 30 | **reusable html components**. 31 | 32 | 33 | Features 34 | ======== 35 | 36 | * Class based template tags. 37 | * Template tag argument parser. 38 | * Declarative component attributes. 39 | * Extendable components. 40 | * Embedded slot components. 41 | * Media class (css/js) implementation (`Django Form-Media class `_) 42 | 43 | .. note:: 44 | 45 | **django-component-tags** implements a simple content distribution API inspired by the 46 | `Web Components spec draft `_, 47 | using the ``{% slot %}`` component inside another component to serve as distribution outlets for content. 48 | 49 | 50 | Extra 51 | ===== 52 | 53 | Libraries created with ``django-component-tags``: 54 | 55 | * `django-component-tags-tailwindcss `_ 56 | 57 | 58 | Requirements 59 | ============ 60 | 61 | Requires Django 2.2 or newer, tested against Python 3.7 and PyPy. 62 | 63 | 64 | Quick Start 65 | =========== 66 | 67 | Install the library: 68 | 69 | .. code-block:: 70 | 71 | pip3 install django-component-tags 72 | 73 | Update your ``settings.py``: 74 | 75 | .. code-block:: 76 | 77 | INSTALLED_APPS = [ 78 | ... 79 | 'component_tags', 80 | ... 81 | ] 82 | 83 | ... 84 | 85 | TEMPLATES = [ 86 | { 87 | 'OPTIONS': { 88 | 'builtins': ['component_tags.template.builtins'], # slot component 89 | }, 90 | }, 91 | ] 92 | 93 | ... 94 | 95 | 96 | Assuming that you already have an `application `_ 97 | called **foo**, lets create a new component tag: 98 | 99 | .. code-block:: python 100 | 101 | # foo/templatetags/foo_tags.py 102 | from component_tags import template 103 | 104 | register = template.Library() 105 | 106 | @register.tag 107 | class Link(template.Component): 108 | href = template.Attribute(default='#') 109 | 110 | class Meta: 111 | template_name = 'foo/tags/link.html' 112 | 113 | 114 | .. note:: 115 | 116 | **django-component-tags** extends the default django template library, because it wraps component classes with a parser 117 | function and extracts template tag arguments, everything else is left in the same way. 118 | 119 | Please check out `the template library `_ 120 | if you want to know more about this process. 121 | 122 | Next, creating the component template: 123 | 124 | .. code-block:: 125 | 126 | # foo/templates/foo/tags/link.html 127 | 128 | 129 | {{ nodelist }} 130 | 131 | 132 | Here we have a couple of variables inside a component template: 133 | 134 | * **attributes**: component template/class attributes (formatted). 135 | * **nodelist**: the content created between ``{% link %}`` and ``{% endlink %}`` will be rendered here. 136 | 137 | Finally, you can use it as follows: 138 | 139 | .. code-block:: 140 | 141 | # foo/templates/foo/index.html 142 | {% load foo_tags %} 143 | 144 | {% link %} 145 | Link 1 146 | {% endlink %} 147 | 148 | Output: 149 | 150 | .. code-block:: 151 | 152 | # foo/templates/foo/index.html 153 | 154 | 155 | Link 1 156 | 157 | 158 | This is the simplest way to start, there is a lot of different settings that you can combine to create complex 159 | html components. 160 | 161 | 162 | Considerations 163 | ============== 164 | 165 | Making multiple changes on html components and using cache interferes with the ``Media Class Library``, 166 | which i believe its good on **production**. Django recommends to set up 167 | `DummyCache `_ 168 | on **development** environments: 169 | 170 | .. code-block:: python 171 | 172 | CACHES = { 173 | 'default': { 174 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 175 | } 176 | } 177 | 178 | 179 | Examples 180 | ======== 181 | 182 | Adding css/js scripts 183 | --------------------- 184 | 185 | Assuming that you already downloaded a css framework in your project like `BootstrapCSS `_. 186 | 187 | Lets create a component: 188 | 189 | .. code-block:: python 190 | 191 | # foo/templatetags/foo_tags.py 192 | from component_tags import template 193 | 194 | register = template.Library() 195 | 196 | @register.tag 197 | class Link(template.Component): 198 | href = template.Attribute(default='#') 199 | 200 | class Meta: 201 | template_name = 'tags/link.html' 202 | css = { 203 | 'all': ('css/bootstrap.min.css',) 204 | } 205 | js = [ 206 | 'js/bootstrap.bundle.min.js', 207 | ] 208 | 209 | 210 | Rendering the component in the main template: 211 | 212 | .. code-block:: 213 | 214 | # foo/templates/foo/index.html 215 | {% load foo_tags %} 216 | 217 | 218 | 219 | 220 | --- 221 | 222 | 223 | {% components_css %} 224 | 225 | 226 | 227 | {% link %} 228 | Link 1 229 | {% endlink %} 230 | {% components_js %} 231 | 232 | 233 | 234 | Output: 235 | 236 | .. code-block:: 237 | 238 | # foo/templates/foo/index.html 239 | {% load foo_tags %} 240 | 241 | 242 | 243 | 244 | --- 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | Link 1 253 | 254 | 255 | 256 | 257 | 258 | 259 | Adding css classes 260 | ------------------ 261 | 262 | Lets create a html component using the `bootstrap framework `_ 263 | 264 | .. code-block:: python 265 | 266 | # foo/templatetags/foo_tags.py 267 | from component_tags import template 268 | 269 | register = template.Library() 270 | 271 | @register.tag 272 | class Link(template.Component): 273 | class ColorChoices(template.AttributeChoices): 274 | primary = 'btn btn-primary' 275 | secondary = 'btn btn-secondary' 276 | success = 'btn btn-success' 277 | danger = 'btn btn-danger' 278 | warning = 'btn btn-warning' 279 | info = 'btn btn-info' 280 | 281 | color = template.Attribute(choices=TypeChoices, default=TypeChoices.submit, as_class=True) 282 | href = template.Attribute(default='#') 283 | 284 | class Meta: 285 | template_name = 'tags/link.html' 286 | css = { 287 | 'all': ('css/bootstrap.min.css',) 288 | } 289 | js = [ 290 | 'js/bootstrap.bundle.min.js', 291 | ] 292 | 293 | Rendering the component: 294 | 295 | .. code-block:: 296 | 297 | # foo/templates/foo/index.html 298 | {% load foo_tags %} 299 | 300 | 301 | 302 | 303 | --- 304 | 305 | 306 | {% components_css %} 307 | 308 | 309 | 310 | {% link color="primary" class="foo-bar" %} 311 | Link 1 312 | {% endlink %} 313 | 314 | {% components_js %} 315 | 316 | 317 | 318 | Also we added the ``class`` argument to the component tag, so even if the components strictly have class attributes, 319 | you will always have a flexible way to customize your components any time in different scenarios. 320 | 321 | Output: 322 | 323 | .. code-block:: 324 | 325 | # foo/templates/foo/index.html 326 | {% load foo_tags %} 327 | 328 | 329 | 330 | 331 | --- 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | Link 1 340 | 341 | 342 | 343 | 344 | 345 | Note that it was merged with all attribute classes previously declared. 346 | 347 | 348 | Using slot components 349 | --------------------- 350 | 351 | Lets make another html component using the `bootstrap framework `_, 352 | this one is going to be a ``Card`` component. 353 | 354 | .. code-block:: python 355 | 356 | # foo/templatetags/foo_tags.py 357 | from component_tags import template 358 | 359 | register = template.Library() 360 | 361 | @register.tag 362 | class Card(template.Component): 363 | title = template.Attribute(required=True, as_context=True) 364 | 365 | class Meta: 366 | template_name = 'tags/card.html' 367 | 368 | Create the component template: 369 | 370 | .. code-block:: 371 | 372 | # foo/templates/foo/tags/card.html 373 | 374 |
375 | ... 376 |
377 |
{{ title }}
378 |
379 | {{ nodelist }} 380 |
381 | {% if slot_footer %} 382 | 385 | {% endif %} 386 |
387 |
388 | 389 | Rendering the component: 390 | 391 | .. code-block:: 392 | 393 | # foo/templates/foo/index.html 394 | {% load foo_tags %} 395 | 396 | {% card title='foo' %} 397 | Some quick example text to build on the card title and make up the bulk of the card's content. 398 | {% slot 'footer' %} 399 | Go somewhere 400 | {% endslot %} 401 | {% endcard %} 402 | 403 | Output: 404 | 405 | .. code-block:: 406 | 407 | # foo/templates/foo/index.html 408 | 409 |
410 | ... 411 |
412 |
foo
413 |
414 | Some quick example text to build on the card title and make up the bulk of the card's content. 415 |
416 | 419 |
420 |
421 | 422 | 423 | Adding extra context 424 | -------------------- 425 | 426 | By default, all components used isolated context to work with. If you want to pass global context to the component tag 427 | it is required to use the ``with`` argument. 428 | 429 | .. code-block:: python 430 | 431 | # foo/views.py 432 | def foo(request, object_id=None): 433 | return render(request, 'foo/index.html', { 434 | 'object_id': object_id 435 | }) 436 | 437 | .. code-block:: 438 | 439 | # foo/templates/foo/index.html 440 | {% load foo_tags %} 441 | 442 | {% link color="primary" with id=object_id %} 443 | Link {{ id }} 444 | {% endlink %} 445 | 446 | Assuming that the request of the page will be something like ``http://localhost:8000/foo/1/``, the output will be: 447 | 448 | .. code-block:: 449 | 450 | # foo/templates/foo/index.html 451 | 452 | 453 | Link 1 454 | 455 | 456 | .. note:: 457 | 458 | ``Slot`` components doesn't need to specify global context, they always use the parent context as default. 459 | 460 | .. _pyscaffold-notes: 461 | 462 | Note 463 | ==== 464 | 465 | This project has been set up using PyScaffold 4.0rc2. For details and usage 466 | information on PyScaffold see https://pyscaffold.org/. 467 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "4d623292409b737ae2b4ec8075511d6ef21c51c5604c8240f72c6f3dc5412d5a" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0", 22 | "sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9" 23 | ], 24 | "markers": "python_version >= '3.7'", 25 | "version": "==3.5.0" 26 | }, 27 | "django": { 28 | "hashes": [ 29 | "sha256:0fabc786489af16ad87a8c170ba9d42bfd23f7b699bd5ef05675864e8d012859", 30 | "sha256:72a4a5a136a214c39cf016ccdd6b69e2aa08c7479c66d93f3a9b5e4bb9d8a347" 31 | ], 32 | "index": "pypi", 33 | "version": "==3.1.14" 34 | }, 35 | "pytz": { 36 | "hashes": [ 37 | "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", 38 | "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" 39 | ], 40 | "version": "==2021.3" 41 | }, 42 | "sqlparse": { 43 | "hashes": [ 44 | "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae", 45 | "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d" 46 | ], 47 | "markers": "python_version >= '3.5'", 48 | "version": "==0.4.2" 49 | } 50 | }, 51 | "develop": { 52 | "asgiref": { 53 | "hashes": [ 54 | "sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0", 55 | "sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9" 56 | ], 57 | "markers": "python_version >= '3.7'", 58 | "version": "==3.5.0" 59 | }, 60 | "attrs": { 61 | "hashes": [ 62 | "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", 63 | "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" 64 | ], 65 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 66 | "version": "==21.4.0" 67 | }, 68 | "conf": { 69 | "hashes": [ 70 | "sha256:d17b309d74ec1cf71ba885c97619bd4d9f938036c7830a715b25ed75defe95a4" 71 | ], 72 | "index": "pypi", 73 | "version": "==0.4.1" 74 | }, 75 | "config": { 76 | "hashes": [ 77 | "sha256:73ea4a8064facc61fcba736be71996aaa7f38a564166a284628b8e8c53444f2e", 78 | "sha256:c7e48f9820758a4ddc23ae3bf10b118959f6fd06018be9eb0e1d9d5a3b227de1" 79 | ], 80 | "index": "pypi", 81 | "version": "==0.5.0.post0" 82 | }, 83 | "coverage": { 84 | "hashes": [ 85 | "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9", 86 | "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d", 87 | "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf", 88 | "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7", 89 | "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6", 90 | "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4", 91 | "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059", 92 | "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39", 93 | "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536", 94 | "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac", 95 | "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c", 96 | "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903", 97 | "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d", 98 | "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05", 99 | "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684", 100 | "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1", 101 | "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f", 102 | "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7", 103 | "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca", 104 | "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad", 105 | "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca", 106 | "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d", 107 | "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92", 108 | "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4", 109 | "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf", 110 | "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6", 111 | "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1", 112 | "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4", 113 | "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359", 114 | "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3", 115 | "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620", 116 | "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512", 117 | "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69", 118 | "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2", 119 | "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518", 120 | "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0", 121 | "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa", 122 | "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4", 123 | "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e", 124 | "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1", 125 | "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2" 126 | ], 127 | "markers": "python_version >= '3.7'", 128 | "version": "==6.3.2" 129 | }, 130 | "django": { 131 | "hashes": [ 132 | "sha256:0fabc786489af16ad87a8c170ba9d42bfd23f7b699bd5ef05675864e8d012859", 133 | "sha256:72a4a5a136a214c39cf016ccdd6b69e2aa08c7479c66d93f3a9b5e4bb9d8a347" 134 | ], 135 | "index": "pypi", 136 | "version": "==3.1.14" 137 | }, 138 | "django-debug-toolbar": { 139 | "hashes": [ 140 | "sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33", 141 | "sha256:e759e63e3fe2d3110e0e519639c166816368701eab4a47fed75d7de7018467b9" 142 | ], 143 | "index": "pypi", 144 | "version": "==3.2.1" 145 | }, 146 | "django-extensions": { 147 | "hashes": [ 148 | "sha256:674ad4c3b1587a884881824f40212d51829e662e52f85b012cd83d83fe1271d9", 149 | "sha256:9507f8761ee760748938fd8af766d0608fb2738cf368adfa1b2451f61c15ae35" 150 | ], 151 | "index": "pypi", 152 | "version": "==3.1.1" 153 | }, 154 | "iniconfig": { 155 | "hashes": [ 156 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", 157 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" 158 | ], 159 | "version": "==1.1.1" 160 | }, 161 | "packaging": { 162 | "hashes": [ 163 | "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", 164 | "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" 165 | ], 166 | "markers": "python_version >= '3.6'", 167 | "version": "==21.3" 168 | }, 169 | "pluggy": { 170 | "hashes": [ 171 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 172 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 173 | ], 174 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 175 | "version": "==0.13.1" 176 | }, 177 | "py": { 178 | "hashes": [ 179 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", 180 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" 181 | ], 182 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 183 | "version": "==1.11.0" 184 | }, 185 | "pyparsing": { 186 | "hashes": [ 187 | "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", 188 | "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" 189 | ], 190 | "markers": "python_version >= '3.6'", 191 | "version": "==3.0.7" 192 | }, 193 | "pytest": { 194 | "hashes": [ 195 | "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9", 196 | "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839" 197 | ], 198 | "index": "pypi", 199 | "version": "==6.2.2" 200 | }, 201 | "pytest-cov": { 202 | "hashes": [ 203 | "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7", 204 | "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da" 205 | ], 206 | "index": "pypi", 207 | "version": "==2.11.1" 208 | }, 209 | "pytest-django": { 210 | "hashes": [ 211 | "sha256:10e384e6b8912ded92db64c58be8139d9ae23fb8361e5fc139d8e4f8fc601bc2", 212 | "sha256:26f02c16d36fd4c8672390deebe3413678d89f30720c16efb8b2a6bf63b9041f" 213 | ], 214 | "index": "pypi", 215 | "version": "==4.1.0" 216 | }, 217 | "python-memcached": { 218 | "hashes": [ 219 | "sha256:4dac64916871bd3550263323fc2ce18e1e439080a2d5670c594cf3118d99b594", 220 | "sha256:a2e28637be13ee0bf1a8b6843e7490f9456fd3f2a4cb60471733c7b5d5557e4f" 221 | ], 222 | "index": "pypi", 223 | "version": "==1.59" 224 | }, 225 | "setuptools": { 226 | "hashes": [ 227 | "sha256:2347b2b432c891a863acadca2da9ac101eae6169b1d3dfee2ec605ecd50dbfe5", 228 | "sha256:e4f30b9f84e5ab3decf945113119649fec09c1fc3507c6ebffec75646c56e62b" 229 | ], 230 | "index": "pypi", 231 | "version": "==60.9.3" 232 | }, 233 | "setuptools-scm": { 234 | "hashes": [ 235 | "sha256:c3bd5f701c8def44a5c0bfe8d407bef3f80342217ef3492b951f3777bd2d915c", 236 | "sha256:d1925a69cb07e9b29416a275b9fadb009a23c148ace905b2fb220649a6c18e92" 237 | ], 238 | "index": "pypi", 239 | "version": "==6.0.1" 240 | }, 241 | "six": { 242 | "hashes": [ 243 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 244 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 245 | ], 246 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 247 | "version": "==1.16.0" 248 | }, 249 | "sqlparse": { 250 | "hashes": [ 251 | "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae", 252 | "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d" 253 | ], 254 | "markers": "python_version >= '3.5'", 255 | "version": "==0.4.2" 256 | }, 257 | "toml": { 258 | "hashes": [ 259 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 260 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 261 | ], 262 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 263 | "version": "==0.10.2" 264 | }, 265 | "wheel": { 266 | "hashes": [ 267 | "sha256:78b5b185f0e5763c26ca1e324373aadd49182ca90e825f7853f4b2509215dc0e", 268 | "sha256:e11eefd162658ea59a60a0f6c7d493a7190ea4b9a85e335b33489d9f17e0245e" 269 | ], 270 | "index": "pypi", 271 | "version": "==0.36.2" 272 | } 273 | } 274 | } 275 | --------------------------------------------------------------------------------