mirror of
https://github.com/django-components/django-components.git
synced 2025-08-02 21:22:16 +00:00
Add pytest-cov
Apply black formatting Add component_css_dependencies_tag and component_js_dependencies_tag tags Add convenience methods render_css_dependencies and render_js_dependencies to component class Create additional test cases Add license to setup.py Add pytest.ini config file
This commit is contained in:
parent
e9929b1bff
commit
2c644d4c06
8 changed files with 199 additions and 66 deletions
|
@ -2,10 +2,11 @@ from django.forms.widgets import MediaDefiningClass
|
|||
from django.template import Context
|
||||
from django.template.base import NodeList, TextNode
|
||||
from django.template.loader import get_template
|
||||
from django.utils.safestring import mark_safe
|
||||
from six import with_metaclass
|
||||
|
||||
# Allow "component.AlreadyRegistered" instead of having to import these everywhere
|
||||
from django_components.component_registry import AlreadyRegistered, ComponentRegistry, NotRegistered # NOQA
|
||||
from django_components.component_registry import AlreadyRegistered, ComponentRegistry, NotRegistered # noqa
|
||||
|
||||
# Python 2 compatibility
|
||||
try:
|
||||
|
@ -24,6 +25,7 @@ except ImportError:
|
|||
VAR = TOKEN_VAR
|
||||
BLOCK = TOKEN_BLOCK
|
||||
|
||||
|
||||
class Component(with_metaclass(MediaDefiningClass)):
|
||||
def context(self):
|
||||
return {}
|
||||
|
@ -32,8 +34,20 @@ class Component(with_metaclass(MediaDefiningClass)):
|
|||
raise NotImplementedError("Missing template() method on component")
|
||||
|
||||
def render_dependencies(self):
|
||||
"""Helper function to access media.render()"""
|
||||
|
||||
return self.media.render()
|
||||
|
||||
def render_css_dependencies(self):
|
||||
"""Render only CSS dependencies available in the media class."""
|
||||
|
||||
return mark_safe("\n".join(self.media.render_css()))
|
||||
|
||||
def render_js_dependencies(self):
|
||||
"""Render only JS dependencies available in the media class."""
|
||||
|
||||
return mark_safe("\n".join(self.media.render_js()))
|
||||
|
||||
def slots_in_template(self, template):
|
||||
nodelist = NodeList()
|
||||
for node in template.template.nodelist:
|
||||
|
@ -48,7 +62,9 @@ class Component(with_metaclass(MediaDefiningClass)):
|
|||
def render(self, slots_filled=None, *args, **kwargs):
|
||||
slots_filled = slots_filled or []
|
||||
context_args_variables = getfullargspec(self.context).args[1:]
|
||||
context_args = {key: kwargs[key] for key in context_args_variables if key in kwargs}
|
||||
context_args = {
|
||||
key: kwargs[key] for key in context_args_variables if key in kwargs
|
||||
}
|
||||
context = self.context(**context_args)
|
||||
template = get_template(self.template(context))
|
||||
slots_in_template = self.slots_in_template(template)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
class AlreadyRegistered(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotRegistered(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ComponentRegistry(object):
|
||||
def __init__(self):
|
||||
self._registry = {} # component name -> component_class mapping
|
||||
|
|
|
@ -17,6 +17,7 @@ except ImportError:
|
|||
VAR = TOKEN_VAR
|
||||
BLOCK = TOKEN_BLOCK
|
||||
|
||||
|
||||
# Django < 2.0 compatibility
|
||||
if django.VERSION > (2, 0):
|
||||
PARSE_BITS_DEFAULTS = {
|
||||
|
@ -38,16 +39,43 @@ register = template.Library()
|
|||
COMPONENT_CONTEXT_KEY = "component_context"
|
||||
|
||||
|
||||
@register.simple_tag(name="component_dependencies")
|
||||
def component_dependencies_tag():
|
||||
def iter_components_from_registry(registry):
|
||||
"""Yields unique components from the registry."""
|
||||
|
||||
unique_component_classes = set(registry.all().values())
|
||||
|
||||
out = []
|
||||
for component_class in unique_component_classes:
|
||||
component = component_class()
|
||||
out.append(component.render_dependencies())
|
||||
yield component_class()
|
||||
|
||||
return mark_safe("\n".join(out))
|
||||
|
||||
@register.simple_tag(name="component_dependencies")
|
||||
def component_dependencies_tag():
|
||||
"""Render both the CSS and JS dependency tags."""
|
||||
|
||||
component_gen = iter_components_from_registry(registry)
|
||||
dependency_gen = map(lambda x: x.render_dependencies(), component_gen)
|
||||
|
||||
return mark_safe("\n".join(dependency_gen))
|
||||
|
||||
|
||||
@register.simple_tag(name="component_css_dependencies")
|
||||
def component_css_dependencies_tag():
|
||||
"""Render the CSS tags."""
|
||||
|
||||
component_gen = iter_components_from_registry(registry)
|
||||
dependency_gen = map(lambda x: x.render_css_dependencies(), component_gen)
|
||||
|
||||
return mark_safe("\n".join(dependency_gen))
|
||||
|
||||
|
||||
@register.simple_tag(name="component_js_dependencies")
|
||||
def component_js_dependencies_tag():
|
||||
"""Render the JS tags."""
|
||||
|
||||
component_gen = iter_components_from_registry(registry)
|
||||
dependency_gen = map(lambda x: x.render_js_dependencies(), component_gen)
|
||||
|
||||
return mark_safe("\n".join(dependency_gen))
|
||||
|
||||
|
||||
@register.simple_tag(name="component")
|
||||
|
@ -74,9 +102,11 @@ class SlotNode(Node):
|
|||
rendered_slot = self.nodelist.render(context)
|
||||
|
||||
if self.component:
|
||||
context.render_context[COMPONENT_CONTEXT_KEY][self.component][self.name] = rendered_slot
|
||||
context.render_context[COMPONENT_CONTEXT_KEY][self.component][
|
||||
self.name
|
||||
] = rendered_slot
|
||||
|
||||
return ''
|
||||
return ""
|
||||
|
||||
|
||||
@register.tag("slot")
|
||||
|
@ -135,11 +165,17 @@ def do_component(parser, token):
|
|||
tag_name = tag_args.pop(0)
|
||||
|
||||
if len(bits) < 2:
|
||||
raise TemplateSyntaxError("Call the '%s' tag with a component name as the first parameter" % tag_name)
|
||||
raise TemplateSyntaxError(
|
||||
"Call the '%s' tag with a component name as the first parameter" % tag_name
|
||||
)
|
||||
|
||||
component_name = bits[1]
|
||||
if not component_name.startswith(('"', "'")) or not component_name.endswith(('"', "'")):
|
||||
raise TemplateSyntaxError("Component name '%s' should be in quotes" % component_name)
|
||||
if not component_name.startswith(('"', "'")) or not component_name.endswith(
|
||||
('"', "'")
|
||||
):
|
||||
raise TemplateSyntaxError(
|
||||
"Component name '%s' should be in quotes" % component_name
|
||||
)
|
||||
|
||||
component_name = component_name.strip('"')
|
||||
component_class = registry.get(component_name)
|
||||
|
|
5
pytest.ini
Normal file
5
pytest.ini
Normal file
|
@ -0,0 +1,5 @@
|
|||
[pytest]
|
||||
addopts = --cov=django_components
|
||||
--cov-report=html
|
||||
--cov-report=term-missing:skip-covered
|
||||
--cov-fail-under=85
|
|
@ -2,5 +2,6 @@ django
|
|||
six
|
||||
tox
|
||||
pytest
|
||||
pytest-cov
|
||||
flake8
|
||||
isort
|
||||
|
|
|
@ -5,26 +5,28 @@
|
|||
# pip-compile requirements-dev.in
|
||||
#
|
||||
appdirs==1.4.4 # via virtualenv
|
||||
asgiref==3.2.7 # via django
|
||||
asgiref==3.2.10 # via django
|
||||
attrs==19.3.0 # via pytest
|
||||
distlib==0.3.0 # via virtualenv
|
||||
django==3.0.7 # via -r requirements-dev.in
|
||||
coverage==5.2 # via pytest-cov
|
||||
distlib==0.3.1 # via virtualenv
|
||||
django==3.0.8 # via -r requirements-dev.in
|
||||
filelock==3.0.12 # via tox, virtualenv
|
||||
flake8==3.8.2 # via -r requirements-dev.in
|
||||
isort==4.3.21 # via -r requirements-dev.in
|
||||
flake8==3.8.3 # via -r requirements-dev.in
|
||||
isort==5.0.5 # via -r requirements-dev.in
|
||||
mccabe==0.6.1 # via flake8
|
||||
more-itertools==8.3.0 # via pytest
|
||||
more-itertools==8.4.0 # via pytest
|
||||
packaging==20.4 # via pytest, tox
|
||||
pluggy==0.13.1 # via pytest, tox
|
||||
py==1.8.1 # via pytest, tox
|
||||
py==1.9.0 # via pytest, tox
|
||||
pycodestyle==2.6.0 # via flake8
|
||||
pyflakes==2.2.0 # via flake8
|
||||
pyparsing==2.4.7 # via packaging
|
||||
pytest==5.4.3 # via -r requirements-dev.in
|
||||
pytest-cov==2.10.0 # via -r requirements-dev.in
|
||||
pytest==5.4.3 # via -r requirements-dev.in, pytest-cov
|
||||
pytz==2020.1 # via django
|
||||
six==1.15.0 # via -r requirements-dev.in, packaging, tox, virtualenv
|
||||
sqlparse==0.3.1 # via django
|
||||
toml==0.10.1 # via tox
|
||||
tox==3.15.1 # via -r requirements-dev.in
|
||||
virtualenv==20.0.21 # via tox
|
||||
wcwidth==0.2.3 # via pytest
|
||||
tox==3.16.1 # via -r requirements-dev.in
|
||||
virtualenv==20.0.26 # via tox
|
||||
wcwidth==0.2.5 # via pytest
|
||||
|
|
22
setup.py
22
setup.py
|
@ -3,23 +3,21 @@ import os
|
|||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
VERSION = '0.3'
|
||||
VERSION = "0.3"
|
||||
|
||||
setup(
|
||||
name='django_reusable_components',
|
||||
name="django_reusable_components",
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
version=VERSION,
|
||||
description='A way to create simple reusable template components in Django.',
|
||||
long_description=open(os.path.join(os.path.dirname(__file__), 'README.md')).read(),
|
||||
description="A way to create simple reusable template components in Django.",
|
||||
long_description=open(os.path.join(os.path.dirname(__file__), "README.md")).read(),
|
||||
long_description_content_type="text/markdown",
|
||||
author=u'Emil Stenström',
|
||||
author_email='em@kth.se',
|
||||
url='https://github.com/EmilStenstrom/django-components/',
|
||||
install_requires=[
|
||||
"Django>=1.11",
|
||||
"six",
|
||||
],
|
||||
keywords=['django', 'components', 'css', 'js', 'html'],
|
||||
author=u"Emil Stenström",
|
||||
author_email="em@kth.se",
|
||||
url="https://github.com/EmilStenstrom/django-components/",
|
||||
install_requires=["Django>=1.11", "six"],
|
||||
license="MIT",
|
||||
keywords=["django", "components", "css", "js", "html"],
|
||||
classifiers=[
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
|
|
|
@ -32,10 +32,12 @@ class SlottedComponent(component.Component):
|
|||
def template(self, context):
|
||||
return "slotted_template.html"
|
||||
|
||||
|
||||
class SlottedComponentNoSlots(component.Component):
|
||||
def template(self, context):
|
||||
return "slotted_template_no_slots.html"
|
||||
|
||||
|
||||
class SlottedComponentWithContext(component.Component):
|
||||
def context(self, variable):
|
||||
return {"variable": variable}
|
||||
|
@ -43,6 +45,7 @@ class SlottedComponentWithContext(component.Component):
|
|||
def template(self, context):
|
||||
return "slotted_template.html"
|
||||
|
||||
|
||||
class ComponentTemplateTagTest(SimpleTestCase):
|
||||
def setUp(self):
|
||||
# NOTE: component.registry is global, so need to clear before each test
|
||||
|
@ -51,34 +54,63 @@ class ComponentTemplateTagTest(SimpleTestCase):
|
|||
def test_single_component_dependencies(self):
|
||||
component.registry.register(name="test", component=SimpleComponent)
|
||||
|
||||
template = Template('{% load component_tags %}{% component_dependencies %}')
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}")
|
||||
rendered = template.render(Context())
|
||||
self.assertHTMLEqual(rendered, dedent("""
|
||||
<link href="style.css" type="text/css" media="all" rel="stylesheet">
|
||||
<script type="text/javascript" src="script.js"></script>
|
||||
""").strip())
|
||||
expected_outcome = (
|
||||
"""<link href="style.css" type="text/css" media="all" rel="stylesheet">\n"""
|
||||
"""<script type="text/javascript" src="script.js"></script>"""
|
||||
)
|
||||
self.assertHTMLEqual(rendered, dedent(expected_outcome))
|
||||
|
||||
def test_single_component_css_dependencies(self):
|
||||
component.registry.register(name="test", component=SimpleComponent)
|
||||
|
||||
template = Template("{% load component_tags %}{% component_css_dependencies %}")
|
||||
rendered = template.render(Context())
|
||||
expected_outcome = (
|
||||
"""<link href="style.css" type="text/css" media="all" rel="stylesheet">"""
|
||||
)
|
||||
self.assertHTMLEqual(rendered, dedent(expected_outcome))
|
||||
|
||||
def test_single_component_js_dependencies(self):
|
||||
component.registry.register(name="test", component=SimpleComponent)
|
||||
|
||||
template = Template("{% load component_tags %}{% component_js_dependencies %}")
|
||||
rendered = template.render(Context())
|
||||
expected_outcome = (
|
||||
"""<script type="text/javascript" src="script.js"></script>"""
|
||||
)
|
||||
self.assertHTMLEqual(rendered, dedent(expected_outcome))
|
||||
|
||||
def test_single_component(self):
|
||||
component.registry.register(name="test", component=SimpleComponent)
|
||||
|
||||
template = Template('{% load component_tags %}{% component name="test" variable="variable" %}')
|
||||
template = Template(
|
||||
'{% load component_tags %}{% component name="test" variable="variable" %}'
|
||||
)
|
||||
rendered = template.render(Context({}))
|
||||
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
|
||||
|
||||
def test_call_component_with_two_variables(self):
|
||||
component.registry.register(name="test", component=IffedComponent)
|
||||
|
||||
template = Template('{% load component_tags %}{% component name="test" variable="variable" variable2="hej" %}')
|
||||
template = Template(
|
||||
"{% load component_tags %}"
|
||||
'{% component name="test" variable="variable" variable2="hej" %}'
|
||||
)
|
||||
rendered = template.render(Context({}))
|
||||
self.assertHTMLEqual(rendered, dedent("""
|
||||
Variable: <strong>variable</strong>
|
||||
Variable2: <strong>hej</strong>
|
||||
"""))
|
||||
expected_outcome = (
|
||||
"""Variable: <strong>variable</strong>\n"""
|
||||
"""Variable2: <strong>hej</strong>"""
|
||||
)
|
||||
self.assertHTMLEqual(rendered, dedent(expected_outcome))
|
||||
|
||||
def test_component_called_with_positional_name(self):
|
||||
component.registry.register(name="test", component=SimpleComponent)
|
||||
|
||||
template = Template('{% load component_tags %}{% component "test" variable="variable" %}')
|
||||
template = Template(
|
||||
'{% load component_tags %}{% component "test" variable="variable" %}'
|
||||
)
|
||||
rendered = template.render(Context({}))
|
||||
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
|
||||
|
||||
|
@ -86,12 +118,36 @@ class ComponentTemplateTagTest(SimpleTestCase):
|
|||
component.registry.register(name="test1", component=SimpleComponent)
|
||||
component.registry.register(name="test2", component=SimpleComponent)
|
||||
|
||||
template = Template('{% load component_tags %}{% component_dependencies %}')
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}")
|
||||
rendered = template.render(Context())
|
||||
self.assertHTMLEqual(rendered, dedent("""
|
||||
<link href="style.css" type="text/css" media="all" rel="stylesheet">
|
||||
<script type="text/javascript" src="script.js"></script>
|
||||
""").strip())
|
||||
expected_outcome = (
|
||||
"""<link href="style.css" type="text/css" media="all" rel="stylesheet">\n"""
|
||||
"""<script type="text/javascript" src="script.js"></script>"""
|
||||
)
|
||||
self.assertHTMLEqual(rendered, dedent(expected_outcome))
|
||||
|
||||
def test_multiple_component_css_dependencies(self):
|
||||
component.registry.register(name="test1", component=SimpleComponent)
|
||||
component.registry.register(name="test2", component=SimpleComponent)
|
||||
|
||||
template = Template("{% load component_tags %}{% component_css_dependencies %}")
|
||||
rendered = template.render(Context())
|
||||
expected_outcome = (
|
||||
"""<link href="style.css" type="text/css" media="all" rel="stylesheet">"""
|
||||
)
|
||||
self.assertHTMLEqual(rendered, dedent(expected_outcome))
|
||||
|
||||
def test_multiple_component_js_dependencies(self):
|
||||
component.registry.register(name="test1", component=SimpleComponent)
|
||||
component.registry.register(name="test2", component=SimpleComponent)
|
||||
|
||||
template = Template("{% load component_tags %}{% component_js_dependencies %}")
|
||||
rendered = template.render(Context())
|
||||
expected_outcome = (
|
||||
"""<script type="text/javascript" src="script.js"></script>"""
|
||||
)
|
||||
self.assertHTMLEqual(rendered, dedent(expected_outcome))
|
||||
|
||||
|
||||
class ComponentSlottedTemplateTagTest(SimpleTestCase):
|
||||
def setUp(self):
|
||||
|
@ -102,7 +158,8 @@ class ComponentSlottedTemplateTagTest(SimpleTestCase):
|
|||
component.registry.register(name="test1", component=SlottedComponent)
|
||||
component.registry.register(name="test2", component=SimpleComponent)
|
||||
|
||||
template = Template("""
|
||||
template = Template(
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% component_block "test1" %}
|
||||
{% slot "header" %}
|
||||
|
@ -112,21 +169,26 @@ class ComponentSlottedTemplateTagTest(SimpleTestCase):
|
|||
{% component "test2" variable="variable" %}
|
||||
{% endslot %}
|
||||
{% endcomponent_block %}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
rendered = template.render(Context({}))
|
||||
|
||||
self.assertHTMLEqual(rendered, """
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<custom-template>
|
||||
<header>Custom header</header>
|
||||
<main>Variable: <strong>variable</strong></main>
|
||||
<footer>Default footer</footer>
|
||||
</custom-template>
|
||||
""")
|
||||
""",
|
||||
)
|
||||
|
||||
def test_slotted_template_with_context_var(self):
|
||||
component.registry.register(name="test1", component=SlottedComponentWithContext)
|
||||
|
||||
template = Template("""
|
||||
template = Template(
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% with my_first_variable="test123" %}
|
||||
{% component_block "test1" variable="test456" %}
|
||||
|
@ -138,37 +200,48 @@ class ComponentSlottedTemplateTagTest(SimpleTestCase):
|
|||
{% endslot %}
|
||||
{% endcomponent_block %}
|
||||
{% endwith %}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
rendered = template.render(Context({"my_second_variable": "test321"}))
|
||||
|
||||
self.assertHTMLEqual(rendered, """
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<custom-template>
|
||||
<header>Default header</header>
|
||||
<main>test123 - test456</main>
|
||||
<footer>test321</footer>
|
||||
</custom-template>
|
||||
""")
|
||||
""",
|
||||
)
|
||||
|
||||
def test_slotted_template_no_slots_filled(self):
|
||||
component.registry.register(name="test", component=SlottedComponent)
|
||||
|
||||
template = Template('{% load component_tags %}{% component_block "test" %}{% endcomponent_block %}')
|
||||
template = Template(
|
||||
'{% load component_tags %}{% component_block "test" %}{% endcomponent_block %}'
|
||||
)
|
||||
rendered = template.render(Context({}))
|
||||
|
||||
self.assertHTMLEqual(rendered, """
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<custom-template>
|
||||
<header>Default header</header>
|
||||
<main>Default main</main>
|
||||
<footer>Default footer</footer>
|
||||
</custom-template>
|
||||
""")
|
||||
""",
|
||||
)
|
||||
|
||||
def test_slotted_template_without_slots(self):
|
||||
component.registry.register(name="test", component=SlottedComponentNoSlots)
|
||||
template = Template("""
|
||||
template = Template(
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% component_block "test" %}{% endcomponent_block %}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
rendered = template.render(Context({}))
|
||||
|
||||
self.assertHTMLEqual(rendered, "<custom-template></custom-template>")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue