mirror of
https://github.com/django-components/django-components.git
synced 2025-08-03 13:58:16 +00:00
Only render dependencies that are used of a specific page (#52)
Co-authored-by: rbeard0330 <@dul2k3BKW6m>
This commit is contained in:
parent
cc8db8056e
commit
57a5aa0f4b
11 changed files with 2576 additions and 99 deletions
|
@ -1,10 +1,12 @@
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
|
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
|
from django.test import override_settings
|
||||||
|
|
||||||
from django_components import component
|
from django_components.middleware import CSS_DEPENDENCY_PLACEHOLDER, JS_DEPENDENCY_PLACEHOLDER
|
||||||
from tests.django_test_setup import * # NOQA
|
from tests.django_test_setup import * # NOQA
|
||||||
from tests.testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
|
from django_components import component
|
||||||
|
from tests.testutils import Django30CompatibleSimpleTestCase as SimpleTestCase, create_and_process_template_response
|
||||||
|
|
||||||
|
|
||||||
class SlottedComponent(component.Component):
|
class SlottedComponent(component.Component):
|
||||||
|
@ -22,20 +24,112 @@ class SimpleComponent(component.Component):
|
||||||
def template(self, context):
|
def template(self, context):
|
||||||
return "simple_template.html"
|
return "simple_template.html"
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = {"all": ["style.css"]}
|
||||||
|
js = ["script.js"]
|
||||||
|
|
||||||
|
|
||||||
|
class BreadcrumbComponent(component.Component):
|
||||||
|
LINKS = [
|
||||||
|
('https://developer.mozilla.org/en-US/docs/Learn',
|
||||||
|
'Learn web development'),
|
||||||
|
('https://developer.mozilla.org/en-US/docs/Learn/HTML',
|
||||||
|
'Structuring the web with HTML'),
|
||||||
|
('https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML',
|
||||||
|
'Introduction to HTML'),
|
||||||
|
('https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML/Document_and_website_structure',
|
||||||
|
'Document and website structure')
|
||||||
|
]
|
||||||
|
|
||||||
|
def context(self, items):
|
||||||
|
if items > 4:
|
||||||
|
items = 4
|
||||||
|
elif items < 0:
|
||||||
|
items = 0
|
||||||
|
return {'links': self.LINKS[:items - 1]}
|
||||||
|
|
||||||
|
def template(self, context):
|
||||||
|
return "mdn_component_template.html"
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = {"all": ["test.css"]}
|
||||||
|
js = ["test.js"]
|
||||||
|
|
||||||
|
|
||||||
|
EXPECTED_CSS = """<link href="test.css" media="all" rel="stylesheet">"""
|
||||||
|
EXPECTED_JS = """<script src="test.js"></script>"""
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(COMPONENTS={'RENDER_DEPENDENCIES': True})
|
||||||
class RenderBenchmarks(SimpleTestCase):
|
class RenderBenchmarks(SimpleTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
component.registry.clear()
|
component.registry.clear()
|
||||||
component.registry.register('test_component', SlottedComponent)
|
component.registry.register('test_component', SlottedComponent)
|
||||||
component.registry.register('inner_component', SimpleComponent)
|
component.registry.register('inner_component', SimpleComponent)
|
||||||
|
component.registry.register('breadcrumb_component', BreadcrumbComponent)
|
||||||
|
|
||||||
def test_render_time(self):
|
@staticmethod
|
||||||
|
def timed_loop(func, iterations=1000):
|
||||||
|
"""Run func iterations times, and return the time in ms per iteration."""
|
||||||
|
start_time = perf_counter()
|
||||||
|
for _ in range(iterations):
|
||||||
|
func()
|
||||||
|
end_time = perf_counter()
|
||||||
|
total_elapsed = end_time - start_time # NOQA
|
||||||
|
return total_elapsed * 1000 / iterations
|
||||||
|
|
||||||
|
def test_render_time_for_small_component(self):
|
||||||
template = Template("{% load component_tags %}{% component_block 'test_component' %}"
|
template = Template("{% load component_tags %}{% component_block 'test_component' %}"
|
||||||
"{% slot \"header\" %}{% component 'inner_component' variable='foo' %}{% endslot %}"
|
"{% slot \"header\" %}{% component 'inner_component' variable='foo' %}{% endslot %}"
|
||||||
"{% endcomponent_block %}", name='root')
|
"{% endcomponent_block %}", name='root')
|
||||||
start_time = perf_counter()
|
|
||||||
for _ in range(1000):
|
print(f'{self.timed_loop(lambda: template.render(Context({})))} ms per iteration')
|
||||||
template.render(Context({}))
|
|
||||||
end_time = perf_counter()
|
def test_middleware_time_with_dependency_for_small_page(self):
|
||||||
total_elapsed = end_time - start_time # NOQA
|
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||||
print(f'{total_elapsed } ms per template')
|
"{% component_block 'test_component' %}{% slot \"header\" %}"
|
||||||
|
"{% component 'inner_component' variable='foo' %}{% endslot %}{% endcomponent_block %}",
|
||||||
|
name='root')
|
||||||
|
# Sanity tests
|
||||||
|
response = create_and_process_template_response(template)
|
||||||
|
response_content = response.content.decode('utf-8')
|
||||||
|
self.assertNotIn(CSS_DEPENDENCY_PLACEHOLDER, response_content)
|
||||||
|
self.assertNotIn(JS_DEPENDENCY_PLACEHOLDER, response_content)
|
||||||
|
self.assertIn('style.css', response_content)
|
||||||
|
self.assertIn('script.js', response_content)
|
||||||
|
|
||||||
|
without_middleware = self.timed_loop(lambda: create_and_process_template_response(template,
|
||||||
|
use_middleware=False))
|
||||||
|
with_middleware = self.timed_loop(lambda: create_and_process_template_response(template, use_middleware=True))
|
||||||
|
|
||||||
|
print('Small page middleware test')
|
||||||
|
self.report_results(with_middleware, without_middleware)
|
||||||
|
|
||||||
|
def test_render_time_with_dependency_for_large_page(self):
|
||||||
|
from django.template.loader import get_template
|
||||||
|
|
||||||
|
template = get_template('mdn_complete_page.html')
|
||||||
|
response = create_and_process_template_response(template, {})
|
||||||
|
response_content = response.content.decode('utf-8')
|
||||||
|
self.assertNotIn(CSS_DEPENDENCY_PLACEHOLDER, response_content)
|
||||||
|
self.assertNotIn(JS_DEPENDENCY_PLACEHOLDER, response_content)
|
||||||
|
self.assertIn('test.css', response_content)
|
||||||
|
self.assertIn('test.js', response_content)
|
||||||
|
|
||||||
|
without_middleware = self.timed_loop(
|
||||||
|
lambda: create_and_process_template_response(template, {}, use_middleware=False))
|
||||||
|
with_middleware = self.timed_loop(
|
||||||
|
lambda: create_and_process_template_response(template, {}, use_middleware=True))
|
||||||
|
|
||||||
|
print('Large page middleware test')
|
||||||
|
self.report_results(with_middleware, without_middleware)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def report_results(with_middleware, without_middleware):
|
||||||
|
print(f'Middleware active\t\t{with_middleware:.3f} ms per iteration')
|
||||||
|
print(f'Middleware inactive\t{without_middleware:.3f} ms per iteration')
|
||||||
|
time_difference = with_middleware - without_middleware
|
||||||
|
if without_middleware > with_middleware:
|
||||||
|
print(f'Decrease of {-100 * time_difference / with_middleware:.2f}%')
|
||||||
|
else:
|
||||||
|
print(f'Increase of {100 * time_difference / without_middleware:.2f}%')
|
||||||
|
|
|
@ -42,7 +42,7 @@ class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
||||||
class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
|
|
||||||
def __init__(self, component_name):
|
def __init__(self, component_name):
|
||||||
self.__component_name = component_name
|
self._component_name = component_name
|
||||||
self.instance_template = None
|
self.instance_template = None
|
||||||
self.slots = {}
|
self.slots = {}
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"Component {} was provided with unexpected slots: {}".format(
|
"Component {} was provided with unexpected slots: {}".format(
|
||||||
self.__component_name, unexpected_slots
|
self._component_name, unexpected_slots
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for unexpected_slot in unexpected_slots:
|
for unexpected_slot in unexpected_slots:
|
||||||
|
|
74
django_components/middleware.py
Normal file
74
django_components/middleware.py
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.forms import Media
|
||||||
|
from django.http import StreamingHttpResponse
|
||||||
|
|
||||||
|
RENDERED_COMPONENTS_CONTEXT_KEY = "_COMPONENT_DEPENDENCIES"
|
||||||
|
CSS_DEPENDENCY_PLACEHOLDER = '<link name="CSS_PLACEHOLDER">'
|
||||||
|
JS_DEPENDENCY_PLACEHOLDER = '<script name="JS_PLACEHOLDER">'
|
||||||
|
|
||||||
|
SCRIPT_TAG_REGEX = re.compile('<script')
|
||||||
|
COMPONENT_COMMENT_REGEX = re.compile(rb'<!-- _RENDERED (?P<name>\w+?) -->')
|
||||||
|
PLACEHOLDER_REGEX = re.compile(rb'<!-- _RENDERED (?P<name>\w+?) -->'
|
||||||
|
rb'|<link name="CSS_PLACEHOLDER">'
|
||||||
|
rb'|<script name="JS_PLACEHOLDER">')
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentDependencyMiddleware:
|
||||||
|
"""Middleware that inserts CSS/JS dependencies for all rendered components at points marked with template tags."""
|
||||||
|
|
||||||
|
dependency_regex = COMPONENT_COMMENT_REGEX
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
response = self.get_response(request)
|
||||||
|
if getattr(settings, "COMPONENTS", {}).get('RENDER_DEPENDENCIES', False)\
|
||||||
|
and not isinstance(response, StreamingHttpResponse)\
|
||||||
|
and response['Content-Type'].startswith('text/html'):
|
||||||
|
response.content = process_response_content(response.content)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def process_response_content(content):
|
||||||
|
from django_components.component import registry
|
||||||
|
|
||||||
|
component_names_seen = {match.group('name') for match in COMPONENT_COMMENT_REGEX.finditer(content)}
|
||||||
|
all_components = [registry.get(name.decode('utf-8'))('') for name in component_names_seen]
|
||||||
|
all_media = join_media(all_components)
|
||||||
|
js_dependencies = b''.join(media.encode('utf-8') for media in all_media.render_js())
|
||||||
|
css_dependencies = b''.join(media.encode('utf-8') for media in all_media.render_css())
|
||||||
|
return PLACEHOLDER_REGEX.sub(DependencyReplacer(css_dependencies, js_dependencies), content)
|
||||||
|
|
||||||
|
|
||||||
|
def add_module_attribute_to_scripts(scripts):
|
||||||
|
return re.sub(SCRIPT_TAG_REGEX, '<script type="module"', scripts)
|
||||||
|
|
||||||
|
|
||||||
|
class DependencyReplacer:
|
||||||
|
"""Replacer for use in re.sub that replaces the first placeholder CSS and JS
|
||||||
|
tags it encounters and removes any subsequent ones."""
|
||||||
|
|
||||||
|
CSS_PLACEHOLDER = bytes(CSS_DEPENDENCY_PLACEHOLDER, encoding='utf-8')
|
||||||
|
JS_PLACEHOLDER = bytes(JS_DEPENDENCY_PLACEHOLDER, encoding='utf-8')
|
||||||
|
|
||||||
|
def __init__(self, css_string, js_string):
|
||||||
|
self.js_string = js_string
|
||||||
|
self.css_string = css_string
|
||||||
|
|
||||||
|
def __call__(self, match):
|
||||||
|
if match[0] == self.CSS_PLACEHOLDER:
|
||||||
|
replacement, self.css_string = self.css_string, b""
|
||||||
|
elif match[0] == self.JS_PLACEHOLDER:
|
||||||
|
replacement, self.js_string = self.js_string, b""
|
||||||
|
else:
|
||||||
|
replacement = b''
|
||||||
|
return replacement
|
||||||
|
|
||||||
|
|
||||||
|
def join_media(components):
|
||||||
|
"""Return combined media object for iterable of components."""
|
||||||
|
|
||||||
|
return sum([component.media for component in components], Media())
|
|
@ -1,11 +1,13 @@
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
from django.conf import settings
|
||||||
from django.template.base import Node, NodeList, TemplateSyntaxError, TokenType
|
from django.template.base import Node, NodeList, TemplateSyntaxError, TokenType
|
||||||
from django.template.library import parse_bits
|
from django.template.library import parse_bits
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from django_components.component import registry
|
from django_components.component import registry
|
||||||
|
from django_components.middleware import CSS_DEPENDENCY_PLACEHOLDER, JS_DEPENDENCY_PLACEHOLDER
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
@ -26,35 +28,44 @@ def get_components_from_registry(registry):
|
||||||
|
|
||||||
@register.simple_tag(name="component_dependencies")
|
@register.simple_tag(name="component_dependencies")
|
||||||
def component_dependencies_tag():
|
def component_dependencies_tag():
|
||||||
"""Render both the CSS and JS dependency tags."""
|
"""Marks location where CSS link and JS script tags should be rendered."""
|
||||||
|
|
||||||
rendered_dependencies = []
|
if is_dependency_middleware_active():
|
||||||
for component in get_components_from_registry(registry):
|
return mark_safe(CSS_DEPENDENCY_PLACEHOLDER + JS_DEPENDENCY_PLACEHOLDER)
|
||||||
rendered_dependencies.append(component.render_dependencies())
|
else:
|
||||||
|
rendered_dependencies = []
|
||||||
|
for component in get_components_from_registry(registry):
|
||||||
|
rendered_dependencies.append(component.render_dependencies())
|
||||||
|
|
||||||
return mark_safe("\n".join(rendered_dependencies))
|
return mark_safe("\n".join(rendered_dependencies))
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(name="component_css_dependencies")
|
@register.simple_tag(name="component_css_dependencies")
|
||||||
def component_css_dependencies_tag():
|
def component_css_dependencies_tag():
|
||||||
"""Render the CSS tags."""
|
"""Marks location where CSS link tags should be rendered."""
|
||||||
|
|
||||||
rendered_dependencies = []
|
if is_dependency_middleware_active():
|
||||||
for component in get_components_from_registry(registry):
|
return mark_safe(CSS_DEPENDENCY_PLACEHOLDER)
|
||||||
rendered_dependencies.append(component.render_css_dependencies())
|
else:
|
||||||
|
rendered_dependencies = []
|
||||||
|
for component in get_components_from_registry(registry):
|
||||||
|
rendered_dependencies.append(component.render_css_dependencies())
|
||||||
|
|
||||||
return mark_safe("\n".join(rendered_dependencies))
|
return mark_safe("\n".join(rendered_dependencies))
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(name="component_js_dependencies")
|
@register.simple_tag(name="component_js_dependencies")
|
||||||
def component_js_dependencies_tag():
|
def component_js_dependencies_tag():
|
||||||
"""Render the JS tags."""
|
"""Marks location where JS script tags should be rendered."""
|
||||||
|
|
||||||
rendered_dependencies = []
|
if is_dependency_middleware_active():
|
||||||
for component in get_components_from_registry(registry):
|
return mark_safe(JS_DEPENDENCY_PLACEHOLDER)
|
||||||
rendered_dependencies.append(component.render_js_dependencies())
|
else:
|
||||||
|
rendered_dependencies = []
|
||||||
|
for component in get_components_from_registry(registry):
|
||||||
|
rendered_dependencies.append(component.render_js_dependencies())
|
||||||
|
|
||||||
return mark_safe("\n".join(rendered_dependencies))
|
return mark_safe("\n".join(rendered_dependencies))
|
||||||
|
|
||||||
|
|
||||||
@register.tag(name='component')
|
@register.tag(name='component')
|
||||||
|
@ -101,6 +112,7 @@ class ComponentNode(Node):
|
||||||
for slot in slots:
|
for slot in slots:
|
||||||
slot_dict[slot.name].extend(slot.nodelist)
|
slot_dict[slot.name].extend(slot.nodelist)
|
||||||
self.component.slots = slot_dict
|
self.component.slots = slot_dict
|
||||||
|
self.should_render_dependencies = is_dependency_middleware_active()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Component Node: %s. Contents: %r>" % (self.component, self.component.instance_template.nodelist)
|
return "<Component Node: %s. Contents: %r>" % (self.component, self.component.instance_template.nodelist)
|
||||||
|
@ -121,7 +133,11 @@ class ComponentNode(Node):
|
||||||
context = context.new()
|
context = context.new()
|
||||||
|
|
||||||
with context.update(component_context):
|
with context.update(component_context):
|
||||||
return self.component.render(context)
|
rendered_component = self.component.render(context)
|
||||||
|
if self.should_render_dependencies:
|
||||||
|
return f'<!-- _RENDERED {self.component._component_name} -->' + rendered_component
|
||||||
|
else:
|
||||||
|
return rendered_component
|
||||||
|
|
||||||
|
|
||||||
@register.tag("component_block")
|
@register.tag("component_block")
|
||||||
|
@ -227,3 +243,7 @@ def safe_resolve(context_item, context):
|
||||||
|
|
||||||
def is_wrapped_in_quotes(s):
|
def is_wrapped_in_quotes(s):
|
||||||
return s.startswith(('"', "'")) and s[0] == s[-1]
|
return s.startswith(('"', "'")) and s[0] == s[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def is_dependency_middleware_active():
|
||||||
|
return getattr(settings, "COMPONENTS", {}).get('RENDER_DEPENDENCIES', False)
|
||||||
|
|
|
@ -11,6 +11,8 @@ if not settings.configured:
|
||||||
COMPONENTS={
|
COMPONENTS={
|
||||||
'TEMPLATE_CACHE_SIZE': 128
|
'TEMPLATE_CACHE_SIZE': 128
|
||||||
},
|
},
|
||||||
|
MIDDLEWARE=['django_components.middleware.ComponentDependencyMiddleware'],
|
||||||
|
DATABASES={},
|
||||||
)
|
)
|
||||||
|
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
2101
tests/templates/mdn_complete_page.html
Normal file
2101
tests/templates/mdn_complete_page.html
Normal file
File diff suppressed because it is too large
Load diff
14
tests/templates/mdn_component_template.html
Normal file
14
tests/templates/mdn_component_template.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<div class="breadcrumb-container">
|
||||||
|
<nav class="breadcrumbs">
|
||||||
|
<ol typeof="BreadcrumbList" vocab="https://schema.org/" aria-label="breadcrumbs">
|
||||||
|
{% for label, url in links %}
|
||||||
|
<li property="itemListElement" typeof="ListItem">
|
||||||
|
<a class="breadcrumb-current-page" property="item" typeof="WebPage" href="{{ url }}">
|
||||||
|
<span property="name">{{ label }}</span>
|
||||||
|
</a>
|
||||||
|
<meta property="position" content="4">
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
|
@ -215,28 +215,27 @@ class ContextCalledOnceTests(SimpleTestCase):
|
||||||
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
|
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
|
||||||
|
|
||||||
def test_one_context_call_with_simple_component_and_arg(self):
|
def test_one_context_call_with_simple_component_and_arg(self):
|
||||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
template = Template("{% load component_tags %}{% component name='incrementer' value='2' %}")
|
||||||
"{% component name='incrementer' value='2' %}")
|
|
||||||
rendered = template.render(Context()).strip()
|
rendered = template.render(Context()).strip()
|
||||||
|
|
||||||
self.assertEqual(rendered, '<p class="incrementer">value=3;calls=1</p>', rendered)
|
self.assertEqual(rendered, '<p class="incrementer">value=3;calls=1</p>', rendered)
|
||||||
|
|
||||||
def test_one_context_call_with_component_block(self):
|
def test_one_context_call_with_component_block(self):
|
||||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
template = Template("{% load component_tags %}"
|
||||||
"{% component_block 'incrementer' %}{% endcomponent_block %}")
|
"{% component_block 'incrementer' %}{% endcomponent_block %}")
|
||||||
rendered = template.render(Context()).strip()
|
rendered = template.render(Context()).strip()
|
||||||
|
|
||||||
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
|
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
|
||||||
|
|
||||||
def test_one_context_call_with_component_block_and_arg(self):
|
def test_one_context_call_with_component_block_and_arg(self):
|
||||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
template = Template("{% load component_tags %}"
|
||||||
"{% component_block 'incrementer' value='3' %}{% endcomponent_block %}")
|
"{% component_block 'incrementer' value='3' %}{% endcomponent_block %}")
|
||||||
rendered = template.render(Context()).strip()
|
rendered = template.render(Context()).strip()
|
||||||
|
|
||||||
self.assertEqual(rendered, '<p class="incrementer">value=4;calls=1</p>', rendered)
|
self.assertEqual(rendered, '<p class="incrementer">value=4;calls=1</p>', rendered)
|
||||||
|
|
||||||
def test_one_context_call_with_slot(self):
|
def test_one_context_call_with_slot(self):
|
||||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
template = Template("{% load component_tags %}"
|
||||||
"{% component_block 'incrementer' %}{% slot 'content' %}"
|
"{% component_block 'incrementer' %}{% slot 'content' %}"
|
||||||
"<p>slot</p>{% endslot %}{% endcomponent_block %}")
|
"<p>slot</p>{% endslot %}{% endcomponent_block %}")
|
||||||
rendered = template.render(Context()).strip()
|
rendered = template.render(Context()).strip()
|
||||||
|
@ -244,7 +243,7 @@ class ContextCalledOnceTests(SimpleTestCase):
|
||||||
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>\n<p>slot</p>', rendered)
|
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>\n<p>slot</p>', rendered)
|
||||||
|
|
||||||
def test_one_context_call_with_slot_and_arg(self):
|
def test_one_context_call_with_slot_and_arg(self):
|
||||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
template = Template("{% load component_tags %}"
|
||||||
"{% component_block 'incrementer' value='3' %}{% slot 'content' %}"
|
"{% component_block 'incrementer' value='3' %}{% slot 'content' %}"
|
||||||
"<p>slot</p>{% endslot %}{% endcomponent_block %}")
|
"<p>slot</p>{% endslot %}{% endcomponent_block %}")
|
||||||
rendered = template.render(Context()).strip()
|
rendered = template.render(Context()).strip()
|
||||||
|
|
202
tests/test_dependency_rendering.py
Normal file
202
tests/test_dependency_rendering.py
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
from django.template import Template
|
||||||
|
from django.test import override_settings
|
||||||
|
|
||||||
|
from .django_test_setup import * # NOQA
|
||||||
|
from django_components import component
|
||||||
|
|
||||||
|
from .test_templatetags import SimpleComponent
|
||||||
|
from .testutils import create_and_process_template_response, Django30CompatibleSimpleTestCase as SimpleTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleComponentAlternate(component.Component):
|
||||||
|
def context(self, variable):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def template(self, context):
|
||||||
|
return "simple_template.html"
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = "style2.css"
|
||||||
|
js = "script2.js"
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleComponentWithSharedDependency(component.Component):
|
||||||
|
def context(self, variable, variable2="default"):
|
||||||
|
return {
|
||||||
|
"variable": variable,
|
||||||
|
"variable2": variable2,
|
||||||
|
}
|
||||||
|
|
||||||
|
def template(self, context):
|
||||||
|
return "simple_template.html"
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = ["style.css", "style2.css"]
|
||||||
|
js = ["script.js", "script2.js"]
|
||||||
|
|
||||||
|
|
||||||
|
class MultistyleComponent(component.Component):
|
||||||
|
def template(self, context):
|
||||||
|
return "simple_template.html"
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = ["style.css", "style2.css"]
|
||||||
|
js = ["script.js", "script2.js"]
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(COMPONENTS={'RENDER_DEPENDENCIES': True})
|
||||||
|
class ComponentMediaRenderingTests(SimpleTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# NOTE: component.registry is global, so need to clear before each test
|
||||||
|
component.registry.clear()
|
||||||
|
|
||||||
|
def test_no_dependencies_when_no_components_used(self):
|
||||||
|
component.registry.register(name="test", component=SimpleComponent)
|
||||||
|
|
||||||
|
template = Template("{% load component_tags %}{% component_dependencies %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertInHTML('<script src="script.js>"', rendered, count=0)
|
||||||
|
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
|
||||||
|
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
|
||||||
|
|
||||||
|
def test_no_js_dependencies_when_no_components_used(self):
|
||||||
|
component.registry.register(name="test", component=SimpleComponent)
|
||||||
|
|
||||||
|
template = Template("{% load component_tags %}{% component_js_dependencies %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertInHTML('<script src="script.js>"', rendered, count=0)
|
||||||
|
|
||||||
|
def test_no_css_dependencies_when_no_components_used(self):
|
||||||
|
component.registry.register(name="test", component=SimpleComponent)
|
||||||
|
|
||||||
|
template = Template("{% load component_tags %}{% component_css_dependencies %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
|
||||||
|
|
||||||
|
def test_single_component_dependencies_render_when_used(self):
|
||||||
|
component.registry.register(name="test", component=SimpleComponent)
|
||||||
|
|
||||||
|
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||||
|
"{% component 'test' variable='foo' %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
|
||||||
|
self.assertInHTML('<script src="script.js">', rendered, count=1)
|
||||||
|
|
||||||
|
def test_placeholder_removed_when_single_component_rendered(self):
|
||||||
|
component.registry.register(name="test", component=SimpleComponent)
|
||||||
|
|
||||||
|
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||||
|
"{% component 'test' variable='foo' %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertNotIn('_RENDERED', rendered)
|
||||||
|
|
||||||
|
def test_single_component_css_dependencies(self):
|
||||||
|
component.registry.register(name="test", component=SimpleComponent)
|
||||||
|
|
||||||
|
template = Template("{% load component_tags %}{% component_css_dependencies %}"
|
||||||
|
"{% component 'test' variable='foo' %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
|
||||||
|
|
||||||
|
def test_single_component_js_dependencies(self):
|
||||||
|
component.registry.register(name="test", component=SimpleComponent)
|
||||||
|
|
||||||
|
template = Template("{% load component_tags %}{% component_js_dependencies %}"
|
||||||
|
"{% component 'test' variable='foo' %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertInHTML('<script src="script.js">', rendered, count=1)
|
||||||
|
|
||||||
|
def test_all_dependencies_are_rendered_for_component_with_multiple_dependencies(self):
|
||||||
|
component.registry.register(name='test', component=MultistyleComponent)
|
||||||
|
template = Template("{% load component_tags %}{% component_dependencies %}{% component 'test' %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertInHTML('<script src="script.js">', rendered, count=1)
|
||||||
|
self.assertInHTML('<script src="script2.js">', rendered, count=1)
|
||||||
|
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
|
||||||
|
self.assertInHTML('<link href="style2.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
|
||||||
|
|
||||||
|
def test_all_js_dependencies_are_rendered_for_component_with_multiple_dependencies(self):
|
||||||
|
component.registry.register(name='test', component=MultistyleComponent)
|
||||||
|
template = Template("{% load component_tags %}{% component_js_dependencies %}{% component 'test' %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertInHTML('<script src="script.js">', rendered, count=1)
|
||||||
|
self.assertInHTML('<script src="script2.js">', rendered, count=1)
|
||||||
|
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
|
||||||
|
self.assertInHTML('<link href="style2.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
|
||||||
|
|
||||||
|
def test_all_css_dependencies_are_rendered_for_component_with_multiple_dependencies(self):
|
||||||
|
component.registry.register(name='test', component=MultistyleComponent)
|
||||||
|
template = Template("{% load component_tags %}{% component_css_dependencies %}{% component 'test' %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertInHTML('<script src="script.js">', rendered, count=0)
|
||||||
|
self.assertInHTML('<script src="script2.js">', rendered, count=0)
|
||||||
|
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
|
||||||
|
self.assertInHTML('<link href="style2.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
|
||||||
|
|
||||||
|
def test_no_dependencies_with_multiple_unused_components(self):
|
||||||
|
component.registry.register(name="test1", component=SimpleComponent)
|
||||||
|
component.registry.register(name="test2", component=SimpleComponentAlternate)
|
||||||
|
|
||||||
|
template = Template("{% load component_tags %}{% component_dependencies %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertInHTML('<script src="script.js">', rendered, count=0)
|
||||||
|
self.assertInHTML('<script src="script2.js">', rendered, count=0)
|
||||||
|
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
|
||||||
|
self.assertInHTML('<link href="style2.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
|
||||||
|
|
||||||
|
def test_correct_css_dependencies_with_multiple_components(self):
|
||||||
|
component.registry.register(name="test1", component=SimpleComponent)
|
||||||
|
component.registry.register(name="test2", component=SimpleComponentAlternate)
|
||||||
|
|
||||||
|
template = Template("{% load component_tags %}{% component_css_dependencies %}"
|
||||||
|
"{% component 'test1' 'variable' %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
|
||||||
|
self.assertInHTML('<link href="style2.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
|
||||||
|
|
||||||
|
def test_correct_js_dependencies_with_multiple_components(self):
|
||||||
|
component.registry.register(name="test1", component=SimpleComponent)
|
||||||
|
component.registry.register(name="test2", component=SimpleComponentAlternate)
|
||||||
|
|
||||||
|
template = Template("{% load component_tags %}{% component_js_dependencies %}"
|
||||||
|
"{% component 'test1' 'variable' %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertInHTML('<script src="script.js">', rendered, count=1)
|
||||||
|
self.assertInHTML('<script src="script2.js">', rendered, count=0)
|
||||||
|
|
||||||
|
def test_correct_dependencies_with_multiple_components(self):
|
||||||
|
component.registry.register(name="test1", component=SimpleComponent)
|
||||||
|
component.registry.register(name="test2", component=SimpleComponentAlternate)
|
||||||
|
|
||||||
|
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||||
|
"{% component 'test2' variable='variable' %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertInHTML('<script src="script.js">', rendered, count=0)
|
||||||
|
self.assertInHTML('<script src="script2.js">', rendered, count=1)
|
||||||
|
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=0)
|
||||||
|
self.assertInHTML('<link href="style2.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
|
||||||
|
|
||||||
|
def test_shared_dependencies_rendered_once(self):
|
||||||
|
component.registry.register(name="test1", component=SimpleComponent)
|
||||||
|
component.registry.register(name="test2", component=SimpleComponentAlternate)
|
||||||
|
component.registry.register(name="test3", component=SimpleComponentWithSharedDependency)
|
||||||
|
|
||||||
|
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||||
|
"{% component 'test1' variable='variable' %}{% component 'test2' variable='variable' %}"
|
||||||
|
"{% component 'test1' variable='variable' %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertInHTML('<script src="script.js">', rendered, count=1)
|
||||||
|
self.assertInHTML('<script src="script2.js">', rendered, count=1)
|
||||||
|
self.assertInHTML('<link href="style.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
|
||||||
|
self.assertInHTML('<link href="style2.css" type="text/css" media="all" rel="stylesheet"/>', rendered, count=1)
|
||||||
|
|
||||||
|
def test_placeholder_removed_when_multiple_component_rendered(self):
|
||||||
|
component.registry.register(name="test1", component=SimpleComponent)
|
||||||
|
component.registry.register(name="test2", component=SimpleComponentAlternate)
|
||||||
|
component.registry.register(name="test3", component=SimpleComponentWithSharedDependency)
|
||||||
|
|
||||||
|
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||||
|
"{% component 'test1' variable='variable' %}{% component 'test2' variable='variable' %}"
|
||||||
|
"{% component 'test1' variable='variable' %}")
|
||||||
|
rendered = create_and_process_template_response(template)
|
||||||
|
self.assertNotIn('_RENDERED', rendered)
|
|
@ -64,37 +64,6 @@ class ComponentTemplateTagTest(SimpleTestCase):
|
||||||
# NOTE: component.registry is global, so need to clear before each test
|
# NOTE: component.registry is global, so need to clear before each test
|
||||||
component.registry.clear()
|
component.registry.clear()
|
||||||
|
|
||||||
def test_single_component_dependencies(self):
|
|
||||||
component.registry.register(name="test", component=SimpleComponent)
|
|
||||||
|
|
||||||
template = Template("{% load component_tags %}{% component_dependencies %}")
|
|
||||||
rendered = template.render(Context())
|
|
||||||
expected_outcome = (
|
|
||||||
"""<link href="style.css" type="text/css" media="all" rel="stylesheet">\n"""
|
|
||||||
"""<script 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 src="script.js"></script>"""
|
|
||||||
)
|
|
||||||
self.assertHTMLEqual(rendered, dedent(expected_outcome))
|
|
||||||
|
|
||||||
def test_single_component(self):
|
def test_single_component(self):
|
||||||
component.registry.register(name="test", component=SimpleComponent)
|
component.registry.register(name="test", component=SimpleComponent)
|
||||||
|
|
||||||
|
@ -145,40 +114,6 @@ class ComponentTemplateTagTest(SimpleTestCase):
|
||||||
rendered = template.render(Context({}))
|
rendered = template.render(Context({}))
|
||||||
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
|
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
|
||||||
|
|
||||||
def test_multiple_component_dependencies(self):
|
|
||||||
component.registry.register(name="test1", component=SimpleComponent)
|
|
||||||
component.registry.register(name="test2", component=SimpleComponent)
|
|
||||||
|
|
||||||
template = Template("{% load component_tags %}{% component_dependencies %}")
|
|
||||||
rendered = template.render(Context())
|
|
||||||
expected_outcome = (
|
|
||||||
"""<link href="style.css" type="text/css" media="all" rel="stylesheet">\n"""
|
|
||||||
"""<script 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 src="script.js"></script>"""
|
|
||||||
)
|
|
||||||
self.assertHTMLEqual(rendered, dedent(expected_outcome))
|
|
||||||
|
|
||||||
|
|
||||||
class ComponentSlottedTemplateTagTest(SimpleTestCase):
|
class ComponentSlottedTemplateTagTest(SimpleTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
@ -1,7 +1,43 @@
|
||||||
from django.test import SimpleTestCase
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from django.template import Context
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import SimpleTestCase, TestCase
|
||||||
|
|
||||||
|
from django_components.middleware import ComponentDependencyMiddleware
|
||||||
|
|
||||||
|
# Create middleware instance
|
||||||
|
response_stash = None
|
||||||
|
middleware = ComponentDependencyMiddleware(get_response=lambda _: response_stash)
|
||||||
|
|
||||||
|
|
||||||
class Django30CompatibleSimpleTestCase(SimpleTestCase):
|
class Django30CompatibleSimpleTestCase(SimpleTestCase):
|
||||||
def assertHTMLEqual(self, left, right):
|
def assertHTMLEqual(self, left, right):
|
||||||
left = left.replace(' type="text/javascript"', '')
|
left = left.replace(' type="text/javascript"', '')
|
||||||
super(Django30CompatibleSimpleTestCase, self).assertHTMLEqual(left, right)
|
super(Django30CompatibleSimpleTestCase, self).assertHTMLEqual(left, right)
|
||||||
|
|
||||||
|
def assertInHTML(self, needle, haystack, count=None, msg_prefix=''):
|
||||||
|
haystack = haystack.replace(' type="text/javascript"', '')
|
||||||
|
super().assertInHTML(needle, haystack, count, msg_prefix)
|
||||||
|
|
||||||
|
|
||||||
|
class Django30CompatibleTestCase(Django30CompatibleSimpleTestCase, TestCase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
request = Mock()
|
||||||
|
mock_template = Mock()
|
||||||
|
|
||||||
|
|
||||||
|
def create_and_process_template_response(template, context=None, use_middleware=True):
|
||||||
|
context = context if context is not None else Context({})
|
||||||
|
mock_template.render = lambda context, _: template.render(context)
|
||||||
|
response = TemplateResponse(request, mock_template, context)
|
||||||
|
if use_middleware:
|
||||||
|
response.render()
|
||||||
|
global response_stash
|
||||||
|
response_stash = response
|
||||||
|
response = middleware(request)
|
||||||
|
else:
|
||||||
|
response.render()
|
||||||
|
return response.content.decode('utf-8')
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue