├── .dockerignore ├── .git-blame-ignore-revs ├── .github └── workflows │ └── main.yml ├── .gitignore ├── AUTHORS ├── CHANGES.rst ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docker-compose.yml ├── requirements-ci.txt ├── requirements-dev.txt ├── requirements-test.txt ├── setup.cfg ├── setup.py ├── src └── page_components │ ├── __init__.py │ ├── page_component.py │ └── views.py ├── tests ├── app │ ├── __init__.py │ ├── models.py │ ├── page_components │ │ ├── __init__.py │ │ └── article.py │ ├── templates │ │ ├── base.html │ │ ├── display-article-with-blank-namespace.html │ │ ├── display-article-with-custom-namespace.html │ │ ├── display-article.html │ │ └── page_components │ │ │ └── article.html │ └── views.py ├── conftest.py ├── test_page_component.py └── test_views.py └── tox.ini /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !src 3 | !tests 4 | !apps 5 | !requirements*.txt 6 | !setup.py 7 | !README.rst 8 | !CHANGES.rst 9 | !MANIFEST.in 10 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | 753783edf6f43edcda2a5930727d3d1fdb2b04f2 -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: 8 | - "*" 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | 28 | - name: build 29 | run: make 30 | 31 | - name: flake8 32 | run: make check-flake8 33 | 34 | - name: black 35 | run: make check-black 36 | 37 | - name: isort 38 | run: make check-isort 39 | 40 | - name: tests 41 | run: make test 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos,linux,python,windows,pycharm 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### PyCharm ### 47 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 48 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 49 | 50 | # User-specific stuff: 51 | .idea/**/workspace.xml 52 | .idea/**/tasks.xml 53 | .idea/dictionaries 54 | 55 | # Sensitive or high-churn files: 56 | .idea/**/dataSources/ 57 | .idea/**/dataSources.ids 58 | .idea/**/dataSources.xml 59 | .idea/**/dataSources.local.xml 60 | .idea/**/sqlDataSources.xml 61 | .idea/**/dynamic.xml 62 | .idea/**/uiDesigner.xml 63 | 64 | # Gradle: 65 | .idea/**/gradle.xml 66 | .idea/**/libraries 67 | 68 | # CMake 69 | cmake-build-debug/ 70 | 71 | # Mongo Explorer plugin: 72 | .idea/**/mongoSettings.xml 73 | 74 | ## File-based project format: 75 | *.iws 76 | 77 | ## Plugin-specific files: 78 | 79 | # IntelliJ 80 | /out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # Crashlytics plugin (for Android Studio and IntelliJ) 92 | com_crashlytics_export_strings.xml 93 | crashlytics.properties 94 | crashlytics-build.properties 95 | fabric.properties 96 | 97 | ### PyCharm Patch ### 98 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 99 | 100 | # *.iml 101 | # modules.xml 102 | # .idea/misc.xml 103 | # *.ipr 104 | 105 | # Sonarlint plugin 106 | .idea/sonarlint 107 | 108 | ### Python ### 109 | # Byte-compiled / optimized / DLL files 110 | __pycache__/ 111 | *.py[cod] 112 | *$py.class 113 | 114 | # C extensions 115 | *.so 116 | 117 | # Distribution / packaging 118 | .Python 119 | env/ 120 | build/ 121 | develop-eggs/ 122 | dist/ 123 | downloads/ 124 | eggs/ 125 | .eggs/ 126 | lib/ 127 | lib64/ 128 | parts/ 129 | sdist/ 130 | var/ 131 | wheels/ 132 | *.egg-info/ 133 | .installed.cfg 134 | *.egg 135 | 136 | # PyInstaller 137 | # Usually these files are written by a python script from a template 138 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 139 | *.manifest 140 | *.spec 141 | 142 | # Installer logs 143 | pip-log.txt 144 | pip-delete-this-directory.txt 145 | 146 | # Unit test / coverage reports 147 | htmlcov/ 148 | .tox/ 149 | .coverage 150 | .coverage.* 151 | .cache 152 | nosetests.xml 153 | coverage.xml 154 | *,cover 155 | .hypothesis/ 156 | 157 | # Translations 158 | *.mo 159 | *.pot 160 | 161 | # Django stuff: 162 | *.log 163 | local_settings.py 164 | 165 | # Flask stuff: 166 | instance/ 167 | .webassets-cache 168 | 169 | # Scrapy stuff: 170 | .scrapy 171 | 172 | # Sphinx documentation 173 | docs/_build/ 174 | 175 | # PyBuilder 176 | target/ 177 | 178 | # Jupyter Notebook 179 | .ipynb_checkpoints 180 | 181 | # pyenv 182 | .python-version 183 | 184 | # celery beat schedule file 185 | celerybeat-schedule 186 | 187 | # SageMath parsed files 188 | *.sage.py 189 | 190 | # dotenv 191 | .env 192 | 193 | # virtualenv 194 | .venv 195 | venv/ 196 | ENV/ 197 | 198 | # Spyder project settings 199 | .spyderproject 200 | .spyproject 201 | 202 | # Rope project settings 203 | .ropeproject 204 | 205 | # mkdocs documentation 206 | /site 207 | 208 | ### Windows ### 209 | # Windows thumbnail cache files 210 | Thumbs.db 211 | ehthumbs.db 212 | ehthumbs_vista.db 213 | 214 | # Folder config file 215 | Desktop.ini 216 | 217 | # Recycle Bin used on file shares 218 | $RECYCLE.BIN/ 219 | 220 | # Windows Installer files 221 | *.cab 222 | *.msi 223 | *.msm 224 | *.msp 225 | 226 | # Windows shortcuts 227 | *.lnk 228 | 229 | # End of https://www.gitignore.io/api/macos,linux,python,windows,pycharm 230 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Andrey Fedoseev [https://github.com/andreyfedoseev] 2 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Changes 3 | ======= 4 | 5 | 1.0.0 6 | ===== 7 | 8 | - Add Django 3.x and 4.x compatibility 9 | - Drop support for Python 2 10 | - Python 3.6 is the minimum requirement 11 | 12 | 0.1 13 | === 14 | 15 | - Initial release 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | MAINTAINER Andrey Fedoseev 3 | RUN apt-get update && \ 4 | apt-get install -y \ 5 | python3.6-dev \ 6 | python3.8-dev \ 7 | python3-pip 8 | RUN mkdir /app 9 | WORKDIR /app 10 | ADD requirements-*.txt /app/ 11 | RUN pip3 install -r requirements-dev.txt 12 | ADD . /app/ 13 | RUN pip3 install -e . 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | django-page-components 2 | ---------------------- 3 | Copyright (c) 2017 django-page-components authors (see AUTHORS file) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include README.rst 3 | include CHANGES.rst 4 | include LICENSE 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | docker-compose build 3 | 4 | shell: 5 | docker-compose run --rm app /bin/bash 6 | 7 | test: 8 | docker-compose run --rm app tox 9 | 10 | upload: 11 | python3 setup.py sdist upload 12 | 13 | check-flake8: 14 | docker-compose run --rm app flake8 ./src ./tests 15 | 16 | check-black: 17 | docker-compose run --rm app black --check ./src ./tests 18 | 19 | apply-black: 20 | docker-compose run --rm app black ./src ./tests 21 | 22 | check-isort: 23 | docker-compose run --rm app isort --check ./src ./tests 24 | 25 | apply-isort: 26 | docker-compose run --rm app isort ./src ./tests 27 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | django-page-components 3 | ====================== 4 | 5 | .. image:: https://github.com/andreyfedoseev/django-page-components/actions/workflows/main.yml/badge.svg 6 | :target: https://github.com/andreyfedoseev/django-page-components/actions/workflows/main.yml 7 | :alt: Tests Status 8 | 9 | 10 | "Page component" is a unit of a user interface (think ReactJS components). ``django-page-components`` provide 11 | a minimalistic framework for creating page components and using them in your Django views and templates. 12 | 13 | To define a page component, you need to create a sub-class of ``page_components.PageComponent`` 14 | and implement ``render`` method like so: 15 | 16 | .. code-block:: python 17 | 18 | import page_components 19 | import django.utils.html 20 | 21 | 22 | class AddToCartButton(page_components.PageComponent): 23 | 24 | def __init__(self, product): 25 | self.product = product 26 | 27 | class Media: 28 | js = ( 29 | "add-to-cart.js", # this is where addToCart is defined 30 | ) 31 | css = { 32 | "all": ( 33 | "add-to-cart.css" # this is where `.add-to-card` styles are defined 34 | ) 35 | } 36 | 37 | def render(self): 38 | return django.utils.html.format_html( 39 | """""", 40 | product_id=self.product.id 41 | ) 42 | 43 | 44 | You can also use a ``TemplatePageComponent`` base class to implement page components based on templates. 45 | In that case, you may want to implement ``get_context_data`` method: 46 | 47 | .. code-block:: python 48 | 49 | class AddToCartButton(page_components.TemplatePageComponent): 50 | 51 | template_name = "add-to-cart-button.html" 52 | 53 | ... 54 | 55 | def get_context_data(self, **kwargs): 56 | kwargs["product_id"] = self.product_id 57 | return super(AddToCartButton, self).get_context_data(**kwargs) 58 | 59 | Note that it's up to you to decide how to implement the ``render`` method and what additional methods should be added 60 | to your page components. One general recommendation is to keep the ``__init__`` method as lightweight as possible and do 61 | all the heavy lifting in the ``render`` method. 62 | 63 | A proposed convention is to store your page components classes in ``page_components`` package/module inside your app:: 64 | 65 | myapp.page_components.AddToCartButton 66 | 67 | Now, when we have some page components defined, it is time to use them in views: 68 | 69 | .. code-block:: python 70 | 71 | import django.views.generic 72 | import page_components 73 | 74 | import myapp.models 75 | import myapp.page_components 76 | 77 | class ProductPage( 78 | page_components.PageComponentsView, 79 | django.views.generic.DetailView, 80 | ): 81 | 82 | model = myapp.models.Product 83 | template_name = "product.html" 84 | 85 | def get_page_components(self): 86 | return { 87 | "add_to_cart_button": myapp.page_components.AddToCartButton(self.object) 88 | } 89 | 90 | 91 | and templates: 92 | 93 | .. code-block:: html 94 | 95 | 96 | 97 | /* this will include CSS files for all page components on that page */ 98 | {{ view.media.css.render }} 99 | 100 | 101 |

{{ object.title }}

102 | {{ page_components.add_to_cart_button }} 103 | 104 | /* this will include JavaScript files for all page components on that page */ 105 | {{ view.media.js.render }} 106 | 107 | 108 | 109 | Note that page components are placed to ``page_components`` namespace in template context by default. You can change 110 | that namespace on per-view basis by adding ``page_components_context_name`` attribute to a view class or globally with 111 | ``PAGE_COMPONENTS_CONTEXT_NAME`` setting. If you set ``page_components_context_name`` to ``None``, it will disable 112 | the namespace entirely. 113 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | app: 4 | build: . 5 | entrypoint: [] 6 | volumes: 7 | - .:/app 8 | -------------------------------------------------------------------------------- /requirements-ci.txt: -------------------------------------------------------------------------------- 1 | -r requirements-test.txt 2 | flake8 3 | tox 4 | black 5 | isort -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements-test.txt 2 | flake8 3 | black 4 | isort -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-django 3 | pytest-cov 4 | tox -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = docs, build, .git 3 | max-line-length = 120 4 | ignore = F402,F403,F405 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | 5 | from setuptools import find_packages, setup 6 | from setuptools.command.test import test as TestCommand 7 | 8 | 9 | class PyTest(TestCommand): 10 | user_options = [("pytest-args=", "a", "Arguments to pass to py.test")] 11 | 12 | def initialize_options(self): 13 | TestCommand.initialize_options(self) 14 | self.pytest_args = [] 15 | 16 | def finalize_options(self): 17 | TestCommand.finalize_options(self) 18 | self.test_args = [] 19 | self.test_suite = True 20 | 21 | def run_tests(self): 22 | # import here, cause outside the eggs aren't loaded 23 | import pytest 24 | 25 | errno = pytest.main(self.pytest_args) 26 | sys.exit(errno) 27 | 28 | 29 | def read(fname): 30 | path = os.path.join(os.path.dirname(__file__), fname) 31 | if sys.version < "3": 32 | return open(path).read() 33 | return open(path, encoding="utf-8").read() 34 | 35 | 36 | README = read("README.rst") 37 | CHANGES = read("CHANGES.rst") 38 | 39 | 40 | version = "" 41 | 42 | with open("src/page_components/__init__.py") as fd: 43 | version = re.search( 44 | r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE 45 | ).group(1) 46 | 47 | if not version: 48 | raise RuntimeError("Cannot find version information") 49 | 50 | 51 | setup( 52 | name="django-page-components", 53 | packages=find_packages("src"), 54 | package_dir={"": "src"}, 55 | version=version, 56 | author="Andrey Fedoseev", 57 | author_email="andrey.fedoseev@gmail.com", 58 | url="https://github.com/andreyfedoseev/django-page-components", 59 | description="Mini-framework for creating re-usable UI components for Django", 60 | long_description="\n\n".join([README, CHANGES]), 61 | classifiers=[ 62 | "Development Status :: 4 - Beta", 63 | "Framework :: Django", 64 | "Intended Audience :: Developers", 65 | "License :: OSI Approved :: BSD License", 66 | "Operating System :: OS Independent", 67 | "Programming Language :: Python", 68 | "Programming Language :: Python :: 3", 69 | "Topic :: Internet :: WWW/HTTP", 70 | ], 71 | keywords=[ 72 | "django", 73 | ], 74 | python_requires=">=3.6", 75 | install_requires=[ 76 | "Django>=2.0", 77 | "django-asset-definitions>=1.0.0", 78 | ], 79 | tests_require=[ 80 | "pytest", 81 | ], 82 | cmdclass={"test": PyTest}, 83 | ) 84 | -------------------------------------------------------------------------------- /src/page_components/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.0" 2 | 3 | 4 | from .page_component import * # noqa 5 | from .views import * # noqa 6 | -------------------------------------------------------------------------------- /src/page_components/page_component.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import Any, Dict 3 | 4 | import asset_definitions 5 | import django.core.exceptions 6 | import django.forms 7 | import django.template.loader 8 | 9 | TemplateContext = Dict[str, Any] 10 | 11 | 12 | __all__ = ( 13 | "PageComponent", 14 | "TemplatePageComponent", 15 | ) 16 | 17 | 18 | class PageComponent(abc.ABC, asset_definitions.MediaDefiningClass): 19 | @abc.abstractmethod 20 | def render(self) -> str: 21 | return "" 22 | 23 | def __str__(self): 24 | return self.render() 25 | 26 | 27 | class TemplatePageComponent(PageComponent): 28 | 29 | template_name = None 30 | 31 | def render(self) -> str: 32 | template_name = self.get_template_name() 33 | context_data = self.get_context_data() 34 | return django.template.loader.render_to_string( 35 | template_name, context=context_data 36 | ) 37 | 38 | # noinspection PyMethodMayBeStatic 39 | def get_template_name(self) -> str: 40 | if not self.template_name: 41 | raise django.core.exceptions.ImproperlyConfigured( 42 | "template_name is missing" 43 | ) 44 | return self.template_name 45 | 46 | # noinspection PyMethodMayBeStatic 47 | def get_context_data(self, **kwargs) -> TemplateContext: 48 | context_data = dict(kwargs) 49 | return context_data 50 | -------------------------------------------------------------------------------- /src/page_components/views.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional 2 | 3 | import asset_definitions 4 | import django.conf 5 | 6 | from . import page_component # noqa 7 | 8 | NOT_SPECIFIED = object() 9 | 10 | 11 | __all__ = ("PageComponentsView",) 12 | 13 | 14 | class PageComponentsView(asset_definitions.MediaDefiningView): 15 | 16 | page_components_context_name = NOT_SPECIFIED 17 | 18 | def get_context_data(self, **kwargs): 19 | page_components_context_name = self.get_page_components_context_name() 20 | if page_components_context_name: 21 | kwargs[page_components_context_name] = self.get_page_components() 22 | else: 23 | kwargs.update(self.get_page_components()) 24 | # noinspection PyUnresolvedReferences 25 | return super(PageComponentsView, self).get_context_data(**kwargs) 26 | 27 | def get_media(self): 28 | media = super(PageComponentsView, self).get_media() 29 | # noinspection PyShadowingNames 30 | for page_component in self.get_page_components().values(): 31 | media += page_component.media 32 | return media 33 | 34 | def get_page_components(self) -> Dict[str, page_component.PageComponent]: 35 | return {} 36 | 37 | def get_page_components_context_name(self) -> Optional[str]: 38 | if self.page_components_context_name is NOT_SPECIFIED: 39 | return getattr( 40 | django.conf.settings, "PAGE_COMPONENTS_CONTEXT_NAME", "page_components" 41 | ) 42 | return self.page_components_context_name 43 | -------------------------------------------------------------------------------- /tests/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreyfedoseev/django-page-components/6e531583a9a859c5b0f04a9dd31002794f2ffdc9/tests/app/__init__.py -------------------------------------------------------------------------------- /tests/app/models.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | Article = collections.namedtuple("Article", ("title", "body")) 4 | -------------------------------------------------------------------------------- /tests/app/page_components/__init__.py: -------------------------------------------------------------------------------- 1 | from .article import * # noqa 2 | -------------------------------------------------------------------------------- /tests/app/page_components/article.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import page_components 4 | 5 | __all__ = ("ArticlePageComponent",) 6 | 7 | 8 | class ArticlePageComponent(page_components.TemplatePageComponent): 9 | 10 | template_name = "page_components/article.html" 11 | 12 | class Media: 13 | js = ("article.js",) 14 | css = {"all": ("article.css",)} 15 | 16 | def __init__(self, article): 17 | self.article = article 18 | 19 | def get_context_data(self, **kwargs): 20 | kwargs.update( 21 | title=self.article.title, 22 | body=self.article.body, 23 | ) 24 | return super(ArticlePageComponent, self).get_context_data(**kwargs) 25 | -------------------------------------------------------------------------------- /tests/app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% spaceless %} 3 | 4 | 5 | {{ view.media.css.render }} 6 | 7 | 8 | {% block content %}{% endblock %} 9 | {{ view.media.js.render }} 10 | 11 | 12 | {% endspaceless %} 13 | -------------------------------------------------------------------------------- /tests/app/templates/display-article-with-blank-namespace.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | {{ article }} 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /tests/app/templates/display-article-with-custom-namespace.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | {{ custom_page_components_namespace.article }} 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /tests/app/templates/display-article.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | {{ page_components.article }} 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /tests/app/templates/page_components/article.html: -------------------------------------------------------------------------------- 1 |

{{ title }}

2 |

{{ body }}

3 | -------------------------------------------------------------------------------- /tests/app/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import django.views.generic 4 | 5 | import app.page_components 6 | import page_components 7 | 8 | 9 | class DisplayArticleView( 10 | page_components.PageComponentsView, django.views.generic.TemplateView 11 | ): 12 | 13 | article = None 14 | 15 | def get_page_components(self): 16 | return { 17 | "article": app.page_components.ArticlePageComponent(self.article), 18 | } 19 | 20 | def get_media(self): 21 | return super(DisplayArticleView, self).get_media() 22 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import django 5 | from django.conf import settings 6 | 7 | 8 | def pytest_configure(): 9 | sys.path.append( 10 | os.path.dirname(__file__), 11 | ) 12 | settings.configure( 13 | DEBUG=True, 14 | STATIC_URL="/static/", 15 | INSTALLED_APPS=("app",), 16 | TEMPLATES=[ 17 | { 18 | "BACKEND": "django.template.backends.django.DjangoTemplates", 19 | "APP_DIRS": True, 20 | "OPTIONS": { 21 | "debug": True, 22 | }, 23 | }, 24 | ], 25 | ) 26 | django.setup() 27 | -------------------------------------------------------------------------------- /tests/test_page_component.py: -------------------------------------------------------------------------------- 1 | import app.models 2 | import app.page_components.article 3 | 4 | 5 | def test_template_page_component(): 6 | 7 | article = app.models.Article( 8 | title="Foo", 9 | body="Bar", 10 | ) 11 | 12 | article_page_component = app.page_components.article.ArticlePageComponent(article) 13 | 14 | assert article_page_component.render() == ("

Foo

\n" "

Bar

\n") 15 | -------------------------------------------------------------------------------- /tests/test_views.py: -------------------------------------------------------------------------------- 1 | import django.utils.encoding 2 | import pytest 3 | 4 | import app.models 5 | import app.views 6 | 7 | NOT_SPECIFIED = object() 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "page_components_context_name,template_name", 12 | ( 13 | (NOT_SPECIFIED, "display-article.html"), 14 | (None, "display-article-with-blank-namespace.html"), 15 | ( 16 | "custom_page_components_namespace", 17 | "display-article-with-custom-namespace.html", 18 | ), 19 | ), 20 | ) 21 | def test_page_component_view(page_components_context_name, template_name, rf): 22 | 23 | view_kwargs = { 24 | "template_name": template_name, 25 | "article": app.models.Article(title="Foo", body="Bar"), 26 | } 27 | if page_components_context_name is not NOT_SPECIFIED: 28 | view_kwargs["page_components_context_name"] = page_components_context_name 29 | 30 | view_function = app.views.DisplayArticleView.as_view(**view_kwargs) 31 | 32 | request = rf.get("/display-article") 33 | response = view_function(request) 34 | response.render() 35 | 36 | assert django.utils.encoding.force_str(response.content) == ( 37 | """\n""" 38 | """""" 39 | """""" 40 | """""" 41 | """""" 42 | """""" 43 | """

Foo

""" 44 | """

Bar

""" 45 | """""" 46 | """""" 47 | """\n""" 48 | ) 49 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py36-django2, 4 | py36-django3, 5 | py38-django4, 6 | 7 | [testenv] 8 | deps = 9 | -rrequirements-dev.txt 10 | django2: Django>=2.0,<3 11 | django3: Django>=3.0,<4 12 | django4: Django>=4.0,<5 13 | commands = py.test --cov {envsitepackagesdir}/page_components --cov-report xml --cov-append 14 | --------------------------------------------------------------------------------