mirror of
https://github.com/django-components/django-components.git
synced 2025-10-09 13:40:18 +00:00
Merge branch 'master' into add-type-hints
This commit is contained in:
commit
25fe39c6d6
34 changed files with 649 additions and 1068 deletions
|
@ -11,4 +11,4 @@ repos:
|
|||
rev: 7.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
|
||||
additional_dependencies: [flake8-pyproject]
|
||||
|
|
60
README.md
60
README.md
|
@ -7,7 +7,7 @@ A way to create simple reusable template components in Django.
|
|||
It lets you create "template components", that contains both the template, the Javascript and the CSS needed to generate the front end code you need for a modern app. Components look like this:
|
||||
|
||||
```htmldjango
|
||||
{% component "calendar" date="2015-06-19" %}
|
||||
{% component "calendar" date="2015-06-19" %}{% endcomponent %}
|
||||
```
|
||||
|
||||
And this is what gets rendered (plus the CSS and Javascript you've specified):
|
||||
|
@ -20,17 +20,21 @@ Read on to learn about the details!
|
|||
|
||||
## Release notes
|
||||
|
||||
*Version 0.34* adds components as views, which allows you to handle requests and render responses from within a component. See the [documentation](#components-as-views) for more details.
|
||||
🚨📢 **Version 0.5** CHANGES THE SYNTAX for components. `component_block` is now `component`, and `component` blocks need an ending `endcomponent` tag. The new `python manage.py upgradecomponent` command can be used to upgrade a directory (use --path argument to point to each dir) of components to the new syntax automatically.
|
||||
|
||||
*Version 0.28* introduces 'implicit' slot filling and the `default` option for `slot` tags.
|
||||
This change is done to simplify the API in anticipation of a 1.0 release of django_components. After 1.0 we intend to be stricter with big changes like this in point releases.
|
||||
|
||||
*Version 0.27* adds a second installable app: *django_components.safer_staticfiles*. It provides the same behavior as *django.contrib.staticfiles* but with extra security guarantees (more info below in Security Notes).
|
||||
**Version 0.34** adds components as views, which allows you to handle requests and render responses from within a component. See the [documentation](#components-as-views) for more details.
|
||||
|
||||
*Version 0.26* changes the syntax for `{% slot %}` tags. From now on, we separate defining a slot (`{% slot %}`) from filling a slot with content (`{% fill %}`). This means you will likely need to change a lot of slot tags to fill. We understand this is annoying, but it's the only way we can get support for nested slots that fill in other slots, which is a very nice feature to have access to. Hoping that this will feel worth it!
|
||||
**Version 0.28** introduces 'implicit' slot filling and the `default` option for `slot` tags.
|
||||
|
||||
*Version 0.22* starts autoimporting all files inside components subdirectores, to simplify setup. An existing project might start to get AlreadyRegistered-errors because of this. To solve this, either remove your custom loading of components, or set "autodiscover": False in settings.COMPONENTS.
|
||||
**Version 0.27** adds a second installable app: *django_components.safer_staticfiles*. It provides the same behavior as *django.contrib.staticfiles* but with extra security guarantees (more info below in Security Notes).
|
||||
|
||||
*Version 0.17* renames `Component.context` and `Component.template` to `get_context_data` and `get_template_name`. The old methods still work, but emit a deprecation warning. This change was done to sync naming with Django's class based views, and make using django-components more familiar to Django users. `Component.context` and `Component.template` will be removed when version 1.0 is released.
|
||||
**Version 0.26** changes the syntax for `{% slot %}` tags. From now on, we separate defining a slot (`{% slot %}`) from filling a slot with content (`{% fill %}`). This means you will likely need to change a lot of slot tags to fill. We understand this is annoying, but it's the only way we can get support for nested slots that fill in other slots, which is a very nice featuPpre to have access to. Hoping that this will feel worth it!
|
||||
|
||||
**Version 0.22** starts autoimporting all files inside components subdirectores, to simplify setup. An existing project might start to get AlreadyRegistered-errors because of this. To solve this, either remove your custom loading of components, or set "autodiscover": False in settings.COMPONENTS.
|
||||
|
||||
**Version 0.17** renames `Component.context` and `Component.template` to `get_context_data` and `get_template_name`. The old methods still work, but emit a deprecation warning. This change was done to sync naming with Django's class based views, and make using django-components more familiar to Django users. `Component.context` and `Component.template` will be removed when version 1.0 is released.
|
||||
|
||||
## Security notes 🚨
|
||||
|
||||
|
@ -220,7 +224,7 @@ First load the `component_tags` tag library, then use the `component_[js/css]_de
|
|||
{% component_css_dependencies %}
|
||||
</head>
|
||||
<body>
|
||||
{% component "calendar" date="2015-06-19" %}
|
||||
{% component "calendar" date="2015-06-19" %}{% endcomponent %}
|
||||
{% component_js_dependencies %}
|
||||
</body>
|
||||
<html>
|
||||
|
@ -300,7 +304,7 @@ This mechanism makes components more reusable and composable.
|
|||
In the example below we introduce two block tags that work hand in hand to make this work. These are...
|
||||
|
||||
- `{% slot <name> %}`/`{% endslot %}`: Declares a new slot in the component template.
|
||||
- `{% fill <name> %}`/`{% endfill %}`: (Used inside a `component_block` tag pair.) Fills a declared slot with the specified content.
|
||||
- `{% fill <name> %}`/`{% endfill %}`: (Used inside a `component` tag pair.) Fills a declared slot with the specified content.
|
||||
|
||||
Let's update our calendar component to support more customization. We'll add `slot` tag pairs to its template, _calendar.html_.
|
||||
|
||||
|
@ -318,9 +322,9 @@ Let's update our calendar component to support more customization. We'll add `sl
|
|||
When using the component, you specify which slots you want to fill and where you want to use the defaults from the template. It looks like this:
|
||||
|
||||
```htmldjango
|
||||
{% component_block "calendar" date="2020-06-06" %}
|
||||
{% component "calendar" date="2020-06-06" %}
|
||||
{% fill "body" %}Can you believe it's already <span>{{ date }}</span>??{% endfill %}
|
||||
{% endcomponent_block %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
Since the header block is unspecified, it's taken from the base template. If you put this in a template, and pass in `date=2020-06-06`, this is what gets rendered:
|
||||
|
@ -338,7 +342,7 @@ Since the header block is unspecified, it's taken from the base template. If you
|
|||
|
||||
As you can see, component slots lets you write reusable containers that you fill in when you use a component. This makes for highly reusable components that can be used in different circumstances.
|
||||
|
||||
It can become tedious to use `fill` tags everywhere, especially when you're using a component that declares only one slot. To make things easier, `slot` tags can be marked with an optional keyword: `default`. When added to the end of the tag (as shown below), this option lets you pass filling content directly in the body of a `component_block` tag pair – without using a `fill` tag. Choose carefully, though: a component template may contain at most one slot that is marked as `default`. The `default` option can be combined with other slot options, e.g. `required`.
|
||||
It can become tedious to use `fill` tags everywhere, especially when you're using a component that declares only one slot. To make things easier, `slot` tags can be marked with an optional keyword: `default`. When added to the end of the tag (as shown below), this option lets you pass filling content directly in the body of a `component` tag pair – without using a `fill` tag. Choose carefully, though: a component template may contain at most one slot that is marked as `default`. The `default` option can be combined with other slot options, e.g. `required`.
|
||||
|
||||
Here's the same example as before, except with default slots and implicit filling.
|
||||
|
||||
|
@ -358,9 +362,9 @@ The template:
|
|||
Including the component (notice how the `fill` tag is omitted):
|
||||
|
||||
```htmldjango
|
||||
{% component_block "calendar" date="2020-06-06" %}
|
||||
{% component "calendar" date="2020-06-06" %}
|
||||
Can you believe it's already <span>{{ date }}</span>??
|
||||
{% endcomponent_block %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
The rendered result (exactly the same as before):
|
||||
|
@ -380,32 +384,32 @@ You may be tempted to combine implicit fills with explicit `fill` tags. This wil
|
|||
|
||||
```htmldjango
|
||||
{# DON'T DO THIS #}
|
||||
{% component_block "calendar" date="2020-06-06" %}
|
||||
{% component "calendar" date="2020-06-06" %}
|
||||
{% fill "header" %}Totally new header!{% endfill %}
|
||||
Can you believe it's already <span>{{ date }}</span>??
|
||||
{% endcomponent_block %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
By contrast, it is permitted to use `fill` tags in nested components, e.g.:
|
||||
|
||||
```htmldjango
|
||||
{% component_block "calendar" date="2020-06-06" %}
|
||||
{% component_block "beautiful-box" %}
|
||||
{% component "calendar" date="2020-06-06" %}
|
||||
{% component "beautiful-box" %}
|
||||
{% fill "content" %} Can you believe it's already <span>{{ date }}</span>?? {% endfill %}
|
||||
{% endcomponent_block %}
|
||||
{% endcomponent_block %}
|
||||
{% endcomponent %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
This is fine too:
|
||||
|
||||
```htmldjango
|
||||
{% component_block "calendar" date="2020-06-06" %}
|
||||
{% component "calendar" date="2020-06-06" %}
|
||||
{% fill "header" %}
|
||||
{% component_block "calendar-header" %}
|
||||
{% component "calendar-header" %}
|
||||
Super Special Calendar Header
|
||||
{% endcomponent_block %}
|
||||
{% endcomponent %}
|
||||
{% endfill %}
|
||||
{% endcomponent_block %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
### Components as views
|
||||
|
@ -482,9 +486,9 @@ If you're planning on passing an HTML string, check Django's use of [`format_htm
|
|||
Certain properties of a slot can be accessed from within a 'fill' context. They are provided as attributes on a user-defined alias of the targeted slot. For instance, let's say you're filling a slot called 'body'. To access properties of this slot, alias it using the 'as' keyword to a new name -- or keep the original name. With the new slot alias, you can call `<alias>.default` to insert the default content.
|
||||
|
||||
```htmldjango
|
||||
{% component_block "calendar" date="2020-06-06" %}
|
||||
{% component "calendar" date="2020-06-06" %}
|
||||
{% fill "body" as "body" %}{{ body.default }}. Have a great day!{% endfill %}
|
||||
{% endcomponent_block %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
Produces:
|
||||
|
@ -617,10 +621,10 @@ COMPONENTS = {
|
|||
|
||||
## Component context and scope
|
||||
|
||||
By default, components can access context variables from the parent template, just like templates that are included with the `{% include %}` tag. Just like with `{% include %}`, if you don't want the component template to have access to the parent context, add `only` to the end of the `{% component %}` (or `{% component_block %}` tag):
|
||||
By default, components can access context variables from the parent template, just like templates that are included with the `{% include %}` tag. Just like with `{% include %}`, if you don't want the component template to have access to the parent context, add `only` to the end of the `{% component %}` tag):
|
||||
|
||||
```htmldjango
|
||||
{% component "calendar" date="2015-06-19" only %}
|
||||
{% component "calendar" date="2015-06-19" only %}{% endcomponent %}
|
||||
```
|
||||
|
||||
NOTE: `{% csrf_token %}` tags need access to the top-level context, and they will not function properly if they are rendered in a component that is called with the `only` modifier.
|
||||
|
|
|
@ -4,10 +4,7 @@ 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 django_components.middleware import CSS_DEPENDENCY_PLACEHOLDER, JS_DEPENDENCY_PLACEHOLDER
|
||||
from tests.django_test_setup import * # NOQA
|
||||
from tests.testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
|
||||
from tests.testutils import create_and_process_template_response
|
||||
|
@ -75,9 +72,7 @@ class RenderBenchmarks(SimpleTestCase):
|
|||
component.registry.clear()
|
||||
component.registry.register("test_component", SlottedComponent)
|
||||
component.registry.register("inner_component", SimpleComponent)
|
||||
component.registry.register(
|
||||
"breadcrumb_component", BreadcrumbComponent
|
||||
)
|
||||
component.registry.register("breadcrumb_component", BreadcrumbComponent)
|
||||
|
||||
@staticmethod
|
||||
def timed_loop(func, iterations=1000):
|
||||
|
@ -91,22 +86,28 @@ class RenderBenchmarks(SimpleTestCase):
|
|||
|
||||
def test_render_time_for_small_component(self):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_block 'test_component' %}"
|
||||
"{% slot \"header\" %}{% component 'inner_component' variable='foo' %}{% endslot %}"
|
||||
"{% endcomponent_block %}",
|
||||
name="root",
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% component 'test_component' %}
|
||||
{% slot "header" %}
|
||||
{% component 'inner_component' variable='foo' %}{% endcomponent %}
|
||||
{% endslot %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
)
|
||||
|
||||
print(
|
||||
f"{self.timed_loop(lambda: template.render(Context({})))} ms per iteration"
|
||||
)
|
||||
print(f"{self.timed_loop(lambda: template.render(Context({})))} ms per iteration")
|
||||
|
||||
def test_middleware_time_with_dependency_for_small_page(self):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'test_component' %}{% slot \"header\" %}"
|
||||
"{% component 'inner_component' variable='foo' %}{% endslot %}{% endcomponent_block %}",
|
||||
name="root",
|
||||
"""
|
||||
{% load component_tags %}{% component_dependencies %}
|
||||
{% component 'test_component' %}
|
||||
{% slot "header" %}
|
||||
{% component 'inner_component' variable='foo' %}{% endcomponent %}
|
||||
{% endslot %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
)
|
||||
# Sanity tests
|
||||
response_content = create_and_process_template_response(template)
|
||||
|
@ -116,15 +117,9 @@ class RenderBenchmarks(SimpleTestCase):
|
|||
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
|
||||
)
|
||||
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)
|
||||
|
@ -140,14 +135,10 @@ class RenderBenchmarks(SimpleTestCase):
|
|||
self.assertIn("test.js", response_content)
|
||||
|
||||
without_middleware = self.timed_loop(
|
||||
lambda: create_and_process_template_response(
|
||||
template, {}, use_middleware=False
|
||||
)
|
||||
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
|
||||
)
|
||||
lambda: create_and_process_template_response(template, {}, use_middleware=True)
|
||||
)
|
||||
|
||||
print("Large page middleware test")
|
||||
|
@ -156,15 +147,9 @@ class RenderBenchmarks(SimpleTestCase):
|
|||
@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"
|
||||
)
|
||||
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}%"
|
||||
)
|
||||
print(f"Decrease of {-100 * time_difference / with_middleware:.2f}%")
|
||||
else:
|
||||
print(
|
||||
f"Increase of {100 * time_difference / without_middleware:.2f}%"
|
||||
)
|
||||
print(f"Increase of {100 * time_difference / without_middleware:.2f}%")
|
||||
|
|
|
@ -26,9 +26,7 @@ class AppSettings:
|
|||
|
||||
@property
|
||||
def CONTEXT_BEHAVIOR(self):
|
||||
raw_value = self.settings.setdefault(
|
||||
"context_behavior", ContextBehavior.GLOBAL.value
|
||||
)
|
||||
raw_value = self.settings.setdefault("context_behavior", ContextBehavior.GLOBAL.value)
|
||||
return self._validate_context_behavior(raw_value)
|
||||
|
||||
def _validate_context_behavior(self, raw_value):
|
||||
|
@ -36,9 +34,7 @@ class AppSettings:
|
|||
return ContextBehavior(raw_value)
|
||||
except ValueError:
|
||||
valid_values = [behavior.value for behavior in ContextBehavior]
|
||||
raise ValueError(
|
||||
f"Invalid context behavior: {raw_value}. Valid options are {valid_values}"
|
||||
)
|
||||
raise ValueError(f"Invalid context behavior: {raw_value}. Valid options are {valid_values}")
|
||||
|
||||
|
||||
app_settings = AppSettings()
|
||||
|
|
|
@ -80,9 +80,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
self,
|
||||
registered_name: Optional[str] = None,
|
||||
outer_context: Optional[Context] = None,
|
||||
fill_content: Union[
|
||||
DefaultFillContent, Iterable[NamedFillContent]
|
||||
] = (),
|
||||
fill_content: Union[DefaultFillContent, Iterable[NamedFillContent]] = (),
|
||||
):
|
||||
self.registered_name: Optional[str] = registered_name
|
||||
self.outer_context: Context = outer_context or Context()
|
||||
|
@ -152,14 +150,10 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
if slots_data:
|
||||
self._fill_slots(slots_data, escape_slots_content)
|
||||
|
||||
updated_filled_slots_context: FilledSlotsContext = (
|
||||
self._process_template_and_update_filled_slot_context(
|
||||
context, template
|
||||
)
|
||||
updated_filled_slots_context: FilledSlotsContext = self._process_template_and_update_filled_slot_context(
|
||||
context, template
|
||||
)
|
||||
with context.update(
|
||||
{FILLED_SLOTS_CONTENT_CONTEXT_KEY: updated_filled_slots_context}
|
||||
):
|
||||
with context.update({FILLED_SLOTS_CONTENT_CONTEXT_KEY: updated_filled_slots_context}):
|
||||
return template.render(context)
|
||||
|
||||
def render_to_response(
|
||||
|
@ -201,19 +195,14 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
named_fills_content = {}
|
||||
else:
|
||||
default_fill_content = None
|
||||
named_fills_content = {
|
||||
name: (nodelist, alias)
|
||||
for name, nodelist, alias in self.fill_content
|
||||
}
|
||||
named_fills_content = {name: (nodelist, alias) for name, nodelist, alias in self.fill_content}
|
||||
|
||||
# If value is `None`, then slot is unfilled.
|
||||
slot_name2fill_content: Dict[SlotName, Optional[FillContent]] = {}
|
||||
default_slot_encountered: bool = False
|
||||
required_slot_names: Set[str] = set()
|
||||
|
||||
for node in template.nodelist.get_nodes_by_type(
|
||||
(SlotNode, IfSlotFilledConditionBranchNode) # type: ignore
|
||||
):
|
||||
for node in template.nodelist.get_nodes_by_type((SlotNode, IfSlotFilledConditionBranchNode)): # type: ignore
|
||||
if isinstance(node, SlotNode):
|
||||
# Give slot node knowledge of its parent template.
|
||||
node.template = template
|
||||
|
@ -225,9 +214,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
f"To fix, check template '{template.name}' "
|
||||
f"of component '{self.registered_name}'."
|
||||
)
|
||||
content_data: Optional[FillContent] = (
|
||||
None # `None` -> unfilled
|
||||
)
|
||||
content_data: Optional[FillContent] = None # `None` -> unfilled
|
||||
if node.is_required:
|
||||
required_slot_names.add(node.name)
|
||||
if node.is_default:
|
||||
|
@ -245,25 +232,19 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
elif isinstance(node, IfSlotFilledConditionBranchNode):
|
||||
node.template = template
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Node of {type(node).__name__} does not require linking."
|
||||
)
|
||||
raise RuntimeError(f"Node of {type(node).__name__} does not require linking.")
|
||||
|
||||
# Check: Only component templates that include a 'default' slot
|
||||
# can be invoked with implicit filling.
|
||||
if default_fill_content and not default_slot_encountered:
|
||||
raise TemplateSyntaxError(
|
||||
f"Component '{self.registered_name}' passed default fill content "
|
||||
f"Component '{self.registered_name}' passed default fill content '{default_fill_content}'"
|
||||
f"(i.e. without explicit 'fill' tag), "
|
||||
f"even though none of its slots is marked as 'default'."
|
||||
)
|
||||
|
||||
unfilled_slots: Set[str] = set(
|
||||
k for k, v in slot_name2fill_content.items() if v is None
|
||||
)
|
||||
unmatched_fills: Set[str] = (
|
||||
named_fills_content.keys() - slot_name2fill_content.keys()
|
||||
)
|
||||
unfilled_slots: Set[str] = set(k for k, v in slot_name2fill_content.items() if v is None)
|
||||
unmatched_fills: Set[str] = named_fills_content.keys() - slot_name2fill_content.keys()
|
||||
|
||||
# Check that 'required' slots are filled.
|
||||
for slot_name in unfilled_slots:
|
||||
|
@ -286,9 +267,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
# Higher values make matching stricter. This is probably preferable, as it
|
||||
# reduces false positives.
|
||||
for fill_name in unmatched_fills:
|
||||
fuzzy_slot_name_matches = difflib.get_close_matches(
|
||||
fill_name, unfilled_slots, n=1, cutoff=0.7
|
||||
)
|
||||
fuzzy_slot_name_matches = difflib.get_close_matches(fill_name, unfilled_slots, n=1, cutoff=0.7)
|
||||
msg = (
|
||||
f"Component '{self.registered_name}' passed fill "
|
||||
f"that refers to undefined slot: '{fill_name}'."
|
||||
|
@ -305,9 +284,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
if content_data # Slots whose content is None (i.e. unfilled) are dropped.
|
||||
}
|
||||
try:
|
||||
prev_context: FilledSlotsContext = context[
|
||||
FILLED_SLOTS_CONTENT_CONTEXT_KEY
|
||||
]
|
||||
prev_context: FilledSlotsContext = context[FILLED_SLOTS_CONTENT_CONTEXT_KEY]
|
||||
return prev_context.new_child(filled_slots_map)
|
||||
except KeyError:
|
||||
return ChainMap(filled_slots_map)
|
||||
|
|
|
@ -12,13 +12,8 @@ class ComponentRegistry(object):
|
|||
|
||||
def register(self, name=None, component=None):
|
||||
existing_component = self._registry.get(name)
|
||||
if (
|
||||
existing_component
|
||||
and existing_component.class_hash != component.class_hash
|
||||
):
|
||||
raise AlreadyRegistered(
|
||||
'The component "%s" has already been registered' % name
|
||||
)
|
||||
if existing_component and existing_component.class_hash != component.class_hash:
|
||||
raise AlreadyRegistered('The component "%s" has already been registered' % name)
|
||||
self._registry[name] = component
|
||||
|
||||
def unregister(self, name):
|
||||
|
|
|
@ -9,9 +9,7 @@ class Command(BaseCommand):
|
|||
help = "Creates a new component"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"name", type=str, help="The name of the component to create"
|
||||
)
|
||||
parser.add_argument("name", type=str, help="The name of the component to create")
|
||||
parser.add_argument(
|
||||
"--path",
|
||||
type=str,
|
||||
|
@ -71,9 +69,7 @@ class Command(BaseCommand):
|
|||
elif base_dir:
|
||||
component_path = os.path.join(base_dir, "components", name)
|
||||
else:
|
||||
raise CommandError(
|
||||
"You must specify a path or set BASE_DIR in your django settings"
|
||||
)
|
||||
raise CommandError("You must specify a path or set BASE_DIR in your django settings")
|
||||
|
||||
if os.path.exists(component_path):
|
||||
if force:
|
||||
|
@ -84,11 +80,7 @@ class Command(BaseCommand):
|
|||
)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f'The component "{name}" already exists. Overwriting...'
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.WARNING(f'The component "{name}" already exists. Overwriting...'))
|
||||
else:
|
||||
raise CommandError(
|
||||
f'The component "{name}" already exists at {component_path}. Use --force to overwrite.'
|
||||
|
@ -107,9 +99,7 @@ class Command(BaseCommand):
|
|||
)
|
||||
f.write(script_content.strip())
|
||||
|
||||
with open(
|
||||
os.path.join(component_path, css_filename), "w"
|
||||
) as f:
|
||||
with open(os.path.join(component_path, css_filename), "w") as f:
|
||||
style_content = dedent(
|
||||
f"""
|
||||
.component-{name} {{
|
||||
|
@ -119,9 +109,7 @@ class Command(BaseCommand):
|
|||
)
|
||||
f.write(style_content.strip())
|
||||
|
||||
with open(
|
||||
os.path.join(component_path, template_filename), "w"
|
||||
) as f:
|
||||
with open(os.path.join(component_path, template_filename), "w") as f:
|
||||
template_content = dedent(
|
||||
f"""
|
||||
<div class="component-{name}">
|
||||
|
@ -133,9 +121,7 @@ class Command(BaseCommand):
|
|||
)
|
||||
f.write(template_content.strip())
|
||||
|
||||
with open(
|
||||
os.path.join(component_path, f"{name}.py"), "w"
|
||||
) as f:
|
||||
with open(os.path.join(component_path, f"{name}.py"), "w") as f:
|
||||
py_content = dedent(
|
||||
f"""
|
||||
from django_components import component
|
||||
|
@ -157,16 +143,8 @@ class Command(BaseCommand):
|
|||
f.write(py_content.strip())
|
||||
|
||||
if verbose:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Successfully created {name} component at {component_path}"
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"Successfully created {name} component at {component_path}"))
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Successfully created {name} component"
|
||||
)
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"Successfully created {name} component"))
|
||||
else:
|
||||
raise CommandError("You must specify a component name")
|
||||
|
|
65
django_components/management/commands/upgradecomponent.py
Normal file
65
django_components/management/commands/upgradecomponent.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.template.engine import Engine
|
||||
|
||||
from django_components.template_loader import Loader
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Updates component and component_block tags to the new syntax"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--path", type=str, help="Path to search for components")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
current_engine = Engine.get_default()
|
||||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
|
||||
if settings.BASE_DIR:
|
||||
dirs.append(Path(settings.BASE_DIR) / "templates")
|
||||
|
||||
if options["path"]:
|
||||
dirs = [options["path"]]
|
||||
|
||||
for dir_path in dirs:
|
||||
self.stdout.write(f"Searching for components in {dir_path}...")
|
||||
for root, _, files in os.walk(dir_path):
|
||||
for file in files:
|
||||
if file.endswith((".html", ".py")):
|
||||
file_path = os.path.join(root, file)
|
||||
with open(file_path, "r+", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
content_with_closed_components, step0_count = re.subn(
|
||||
r'({%\s*component\s*"(\w+?)"(.*?)%})(?!.*?{%\s*endcomponent\s*%})',
|
||||
r"\1{% endcomponent %}",
|
||||
content,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
updated_content, step1_count_opening = re.subn(
|
||||
r'{%\s*component_block\s*"(\w+?)"\s*(.*?)%}',
|
||||
r'{% component "\1" \2%}',
|
||||
content_with_closed_components,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
updated_content, step2_count_closing = re.subn(
|
||||
r'{%\s*endcomponent_block\s*"(\w+?)"\s*%}',
|
||||
r"{% endcomponent %}",
|
||||
updated_content,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
updated_content, step2_count_closing_no_name = re.subn(
|
||||
r"{%\s*endcomponent_block\s*%}", r"{% endcomponent %}", updated_content, flags=re.DOTALL
|
||||
)
|
||||
total_updates = (
|
||||
step0_count + step1_count_opening + step2_count_closing + step2_count_closing_no_name
|
||||
)
|
||||
if total_updates > 0:
|
||||
f.seek(0)
|
||||
f.write(updated_content)
|
||||
f.truncate()
|
||||
self.stdout.write(f"Updated {file_path}: {total_updates} changes made")
|
|
@ -11,9 +11,7 @@ CSS_DEPENDENCY_PLACEHOLDER = '<link name="CSS_PLACEHOLDER">'
|
|||
JS_DEPENDENCY_PLACEHOLDER = '<script name="JS_PLACEHOLDER"></script>'
|
||||
|
||||
SCRIPT_TAG_REGEX = re.compile("<script")
|
||||
COMPONENT_COMMENT_REGEX = re.compile(
|
||||
rb"<!-- _RENDERED (?P<name>[\w\-/]+?) -->"
|
||||
)
|
||||
COMPONENT_COMMENT_REGEX = re.compile(rb"<!-- _RENDERED (?P<name>[\w\-/]+?) -->")
|
||||
PLACEHOLDER_REGEX = re.compile(
|
||||
rb"<!-- _RENDERED (?P<name>[\w\-/]+?) -->"
|
||||
rb'|<link name="CSS_PLACEHOLDER">'
|
||||
|
@ -32,9 +30,7 @@ class ComponentDependencyMiddleware:
|
|||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
if (
|
||||
getattr(settings, "COMPONENTS", {}).get(
|
||||
"RENDER_DEPENDENCIES", False
|
||||
)
|
||||
getattr(settings, "COMPONENTS", {}).get("RENDER_DEPENDENCIES", False)
|
||||
and not isinstance(response, StreamingHttpResponse)
|
||||
and response.get("Content-Type", "").startswith("text/html")
|
||||
):
|
||||
|
@ -43,23 +39,12 @@ class ComponentDependencyMiddleware:
|
|||
|
||||
|
||||
def process_response_content(content):
|
||||
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
|
||||
]
|
||||
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
|
||||
)
|
||||
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):
|
||||
|
|
|
@ -12,9 +12,7 @@ class SaferStaticFilesConfig(StaticFilesConfig):
|
|||
by the static file server.
|
||||
"""
|
||||
|
||||
default = (
|
||||
True # Ensure that _this_ app is registered, as opposed to parent cls.
|
||||
)
|
||||
default = True # Ensure that _this_ app is registered, as opposed to parent cls.
|
||||
ignore_patterns = StaticFilesConfig.ignore_patterns + [
|
||||
"*.py",
|
||||
"*.html",
|
||||
|
|
|
@ -9,13 +9,7 @@ else:
|
|||
import django.template
|
||||
from django.conf import settings
|
||||
from django.template import Context, Template
|
||||
from django.template.base import (
|
||||
FilterExpression,
|
||||
Node,
|
||||
NodeList,
|
||||
TextNode,
|
||||
TokenType,
|
||||
)
|
||||
from django.template.base import FilterExpression, Node, NodeList, TextNode, TokenType
|
||||
from django.template.defaulttags import CommentNode
|
||||
from django.template.exceptions import TemplateSyntaxError
|
||||
from django.template.library import parse_bits
|
||||
|
@ -24,10 +18,7 @@ from django.utils.safestring import mark_safe
|
|||
from django_components.app_settings import app_settings
|
||||
from django_components.component_registry import ComponentRegistry
|
||||
from django_components.component_registry import registry as component_registry
|
||||
from django_components.middleware import (
|
||||
CSS_DEPENDENCY_PLACEHOLDER,
|
||||
JS_DEPENDENCY_PLACEHOLDER,
|
||||
)
|
||||
from django_components.middleware import CSS_DEPENDENCY_PLACEHOLDER, JS_DEPENDENCY_PLACEHOLDER
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component import Component
|
||||
|
@ -88,16 +79,8 @@ def component_dependencies_tag(preload=""):
|
|||
if is_dependency_middleware_active():
|
||||
preloaded_dependencies = []
|
||||
for component in get_components_from_preload_str(preload):
|
||||
preloaded_dependencies.append(
|
||||
RENDERED_COMMENT_TEMPLATE.format(
|
||||
name=component.registered_name
|
||||
)
|
||||
)
|
||||
return mark_safe(
|
||||
"\n".join(preloaded_dependencies)
|
||||
+ CSS_DEPENDENCY_PLACEHOLDER
|
||||
+ JS_DEPENDENCY_PLACEHOLDER
|
||||
)
|
||||
preloaded_dependencies.append(RENDERED_COMMENT_TEMPLATE.format(name=component.registered_name))
|
||||
return mark_safe("\n".join(preloaded_dependencies) + CSS_DEPENDENCY_PLACEHOLDER + JS_DEPENDENCY_PLACEHOLDER)
|
||||
else:
|
||||
rendered_dependencies = []
|
||||
for component in get_components_from_registry(component_registry):
|
||||
|
@ -113,14 +96,8 @@ def component_css_dependencies_tag(preload=""):
|
|||
if is_dependency_middleware_active():
|
||||
preloaded_dependencies = []
|
||||
for component in get_components_from_preload_str(preload):
|
||||
preloaded_dependencies.append(
|
||||
RENDERED_COMMENT_TEMPLATE.format(
|
||||
name=component.registered_name
|
||||
)
|
||||
)
|
||||
return mark_safe(
|
||||
"\n".join(preloaded_dependencies) + CSS_DEPENDENCY_PLACEHOLDER
|
||||
)
|
||||
preloaded_dependencies.append(RENDERED_COMMENT_TEMPLATE.format(name=component.registered_name))
|
||||
return mark_safe("\n".join(preloaded_dependencies) + CSS_DEPENDENCY_PLACEHOLDER)
|
||||
else:
|
||||
rendered_dependencies = []
|
||||
for component in get_components_from_registry(component_registry):
|
||||
|
@ -136,14 +113,8 @@ def component_js_dependencies_tag(preload=""):
|
|||
if is_dependency_middleware_active():
|
||||
preloaded_dependencies = []
|
||||
for component in get_components_from_preload_str(preload):
|
||||
preloaded_dependencies.append(
|
||||
RENDERED_COMMENT_TEMPLATE.format(
|
||||
name=component.registered_name
|
||||
)
|
||||
)
|
||||
return mark_safe(
|
||||
"\n".join(preloaded_dependencies) + JS_DEPENDENCY_PLACEHOLDER
|
||||
)
|
||||
preloaded_dependencies.append(RENDERED_COMMENT_TEMPLATE.format(name=component.registered_name))
|
||||
return mark_safe("\n".join(preloaded_dependencies) + JS_DEPENDENCY_PLACEHOLDER)
|
||||
else:
|
||||
rendered_dependencies = []
|
||||
for component in get_components_from_registry(component_registry):
|
||||
|
@ -152,22 +123,6 @@ def component_js_dependencies_tag(preload=""):
|
|||
return mark_safe("\n".join(rendered_dependencies))
|
||||
|
||||
|
||||
@register.tag(name="component")
|
||||
def do_component(parser, token):
|
||||
bits = token.split_contents()
|
||||
bits, isolated_context = check_for_isolated_context_keyword(bits)
|
||||
|
||||
component_name, context_args, context_kwargs = parse_component_with_args(
|
||||
parser, bits, "component"
|
||||
)
|
||||
return ComponentNode(
|
||||
FilterExpression(component_name, parser),
|
||||
context_args,
|
||||
context_kwargs,
|
||||
isolated_context=isolated_context,
|
||||
)
|
||||
|
||||
|
||||
class UserSlotVar:
|
||||
"""
|
||||
Extensible mechanism for offering 'fill' blocks in template access to properties
|
||||
|
@ -233,24 +188,17 @@ class SlotNode(Node, TemplateAwareNodeMixin):
|
|||
|
||||
def render(self, context):
|
||||
try:
|
||||
filled_slots_map: FilledSlotsContext = context[
|
||||
FILLED_SLOTS_CONTENT_CONTEXT_KEY
|
||||
]
|
||||
filled_slots_map: FilledSlotsContext = context[FILLED_SLOTS_CONTENT_CONTEXT_KEY]
|
||||
except KeyError:
|
||||
raise TemplateSyntaxError(
|
||||
f"Attempted to render SlotNode '{self.name}' outside a parent component."
|
||||
)
|
||||
raise TemplateSyntaxError(f"Attempted to render SlotNode '{self.name}' outside a parent component.")
|
||||
|
||||
extra_context = {}
|
||||
try:
|
||||
slot_fill_content: Optional[FillContent] = filled_slots_map[
|
||||
(self.name, self.template)
|
||||
]
|
||||
slot_fill_content: Optional[FillContent] = filled_slots_map[(self.name, self.template)]
|
||||
except KeyError:
|
||||
if self.is_required:
|
||||
raise TemplateSyntaxError(
|
||||
f"Slot '{self.name}' is marked as 'required' (i.e. non-optional), "
|
||||
f"yet no fill is provided. "
|
||||
f"Slot '{self.name}' is marked as 'required' (i.e. non-optional), " f"yet no fill is provided. "
|
||||
)
|
||||
nodelist = self.nodelist
|
||||
else:
|
||||
|
@ -274,9 +222,7 @@ def do_slot(parser, token):
|
|||
if 1 <= len(args) <= 3:
|
||||
slot_name, *options = args
|
||||
if not is_wrapped_in_quotes(slot_name):
|
||||
raise TemplateSyntaxError(
|
||||
f"'{bits[0]}' name must be a string 'literal'."
|
||||
)
|
||||
raise TemplateSyntaxError(f"'{bits[0]}' name must be a string 'literal'.")
|
||||
slot_name = strip_quotes(slot_name)
|
||||
modifiers_count = len(options)
|
||||
if SLOT_REQUIRED_OPTION_KEYWORD in options:
|
||||
|
@ -290,9 +236,7 @@ def do_slot(parser, token):
|
|||
SLOT_REQUIRED_OPTION_KEYWORD,
|
||||
SLOT_DEFAULT_OPTION_KEYWORD,
|
||||
]
|
||||
raise TemplateSyntaxError(
|
||||
f"Invalid options passed to 'slot' tag. Valid choices: {keywords}."
|
||||
)
|
||||
raise TemplateSyntaxError(f"Invalid options passed to 'slot' tag. Valid choices: {keywords}.")
|
||||
else:
|
||||
raise TemplateSyntaxError(
|
||||
"'slot' tag does not match pattern "
|
||||
|
@ -321,7 +265,7 @@ class BaseFillNode(Node):
|
|||
raise TemplateSyntaxError(
|
||||
"{% fill ... %} block cannot be rendered directly. "
|
||||
"You are probably seeing this because you have used one outside "
|
||||
"a {% component_block %} context."
|
||||
"a {% component %} context."
|
||||
)
|
||||
|
||||
|
||||
|
@ -342,7 +286,7 @@ class NamedFillNode(BaseFillNode):
|
|||
|
||||
class ImplicitFillNode(BaseFillNode):
|
||||
"""
|
||||
Instantiated when a `component_block` tag pair is passed template content that
|
||||
Instantiated when a `component` tag pair is passed template content that
|
||||
excludes `fill` tags. Nodes of this type contribute their nodelists to slots marked
|
||||
as 'default'.
|
||||
"""
|
||||
|
@ -354,10 +298,10 @@ class ImplicitFillNode(BaseFillNode):
|
|||
@register.tag("fill")
|
||||
def do_fill(parser, token):
|
||||
"""Block tag whose contents 'fill' (are inserted into) an identically named
|
||||
'slot'-block in the component template referred to by a parent component_block.
|
||||
'slot'-block in the component template referred to by a parent component.
|
||||
It exists to make component nesting easier.
|
||||
|
||||
This tag is available only within a {% component_block %}..{% endcomponent_block %} block.
|
||||
This tag is available only within a {% component %}..{% endcomponent %} block.
|
||||
Runtime checks should prohibit other usages.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
|
@ -371,14 +315,10 @@ def do_fill(parser, token):
|
|||
elif len(args) == 3:
|
||||
tgt_slot_name, as_keyword, alias = args
|
||||
if as_keyword.lower() != "as":
|
||||
raise TemplateSyntaxError(
|
||||
f"{tag} tag args do not conform to pattern '<target slot> as <alias>'"
|
||||
)
|
||||
raise TemplateSyntaxError(f"{tag} tag args do not conform to pattern '<target slot> as <alias>'")
|
||||
alias_fexp = FilterExpression(alias, parser)
|
||||
else:
|
||||
raise TemplateSyntaxError(
|
||||
f"'{tag}' tag takes either 1 or 3 arguments: Received {len(args)}."
|
||||
)
|
||||
raise TemplateSyntaxError(f"'{tag}' tag takes either 1 or 3 arguments: Received {len(args)}.")
|
||||
nodelist = parser.parse(parse_until=["endfill"])
|
||||
parser.delete_first_token()
|
||||
|
||||
|
@ -414,27 +354,18 @@ class ComponentNode(Node):
|
|||
def __repr__(self):
|
||||
return "<ComponentNode: %s. Contents: %r>" % (
|
||||
self.name_fexp,
|
||||
getattr(
|
||||
self, "nodelist", None
|
||||
), # 'nodelist' attribute only assigned later.
|
||||
getattr(self, "nodelist", None), # 'nodelist' attribute only assigned later.
|
||||
)
|
||||
|
||||
def render(self, context: Context):
|
||||
resolved_component_name = self.name_fexp.resolve(context)
|
||||
component_cls: Type[Component] = component_registry.get(
|
||||
resolved_component_name
|
||||
)
|
||||
component_cls: Type[Component] = component_registry.get(resolved_component_name)
|
||||
|
||||
# Resolve FilterExpressions and Variables that were passed as args to the
|
||||
# component, then call component's context method
|
||||
# to get values to insert into the context
|
||||
resolved_context_args = [
|
||||
safe_resolve(arg, context) for arg in self.context_args
|
||||
]
|
||||
resolved_context_kwargs = {
|
||||
key: safe_resolve(kwarg, context)
|
||||
for key, kwarg in self.context_kwargs.items()
|
||||
}
|
||||
resolved_context_args = [safe_resolve(arg, context) for arg in self.context_args]
|
||||
resolved_context_kwargs = {key: safe_resolve(kwarg, context) for key, kwarg in self.context_kwargs.items()}
|
||||
|
||||
if isinstance(self.fill_nodes, ImplicitFillNode):
|
||||
fill_content = self.fill_nodes.nodelist
|
||||
|
@ -454,9 +385,7 @@ class ComponentNode(Node):
|
|||
)
|
||||
else:
|
||||
resolved_alias: None = None
|
||||
fill_content.append(
|
||||
(resolved_name, fill_node.nodelist, resolved_alias)
|
||||
)
|
||||
fill_content.append((resolved_name, fill_node.nodelist, resolved_alias))
|
||||
|
||||
component: Component = component_cls(
|
||||
registered_name=resolved_component_name,
|
||||
|
@ -464,9 +393,7 @@ class ComponentNode(Node):
|
|||
fill_content=fill_content,
|
||||
)
|
||||
|
||||
component_context: dict = component.get_context_data(
|
||||
*resolved_context_args, **resolved_context_kwargs
|
||||
)
|
||||
component_context: dict = component.get_context_data(*resolved_context_args, **resolved_context_kwargs)
|
||||
|
||||
if self.isolated_context:
|
||||
context = context.new()
|
||||
|
@ -474,22 +401,19 @@ class ComponentNode(Node):
|
|||
rendered_component = component.render(context)
|
||||
|
||||
if is_dependency_middleware_active():
|
||||
return (
|
||||
RENDERED_COMMENT_TEMPLATE.format(name=resolved_component_name)
|
||||
+ rendered_component
|
||||
)
|
||||
return RENDERED_COMMENT_TEMPLATE.format(name=resolved_component_name) + rendered_component
|
||||
else:
|
||||
return rendered_component
|
||||
|
||||
|
||||
@register.tag(name="component_block")
|
||||
def do_component_block(parser, token):
|
||||
@register.tag(name="component")
|
||||
def do_component(parser, token):
|
||||
"""
|
||||
To give the component access to the template context:
|
||||
{% component_block "name" positional_arg keyword_arg=value ... %}
|
||||
{% component "name" positional_arg keyword_arg=value ... %}
|
||||
|
||||
To render the component in an isolated context:
|
||||
{% component_block "name" positional_arg keyword_arg=value ... only %}
|
||||
{% component "name" positional_arg keyword_arg=value ... only %}
|
||||
|
||||
Positional and keyword arguments can be literals or template variables.
|
||||
The component name must be a single- or double-quotes string and must
|
||||
|
@ -499,10 +423,8 @@ def do_component_block(parser, token):
|
|||
|
||||
bits = token.split_contents()
|
||||
bits, isolated_context = check_for_isolated_context_keyword(bits)
|
||||
component_name, context_args, context_kwargs = parse_component_with_args(
|
||||
parser, bits, "component_block"
|
||||
)
|
||||
body: NodeList = parser.parse(parse_until=["endcomponent_block"])
|
||||
component_name, context_args, context_kwargs = parse_component_with_args(parser, bits, "component")
|
||||
body: NodeList = parser.parse(parse_until=["endcomponent"])
|
||||
parser.delete_first_token()
|
||||
fill_nodes = ()
|
||||
if block_has_content(body):
|
||||
|
@ -515,7 +437,7 @@ def do_component_block(parser, token):
|
|||
break
|
||||
else:
|
||||
raise TemplateSyntaxError(
|
||||
"Illegal content passed to 'component_block' tag pair. "
|
||||
"Illegal content passed to 'component' tag pair. "
|
||||
"Possible causes: 1) Explicit 'fill' tags cannot occur alongside other "
|
||||
"tags except comment tags; 2) Default (default slot-targeting) content "
|
||||
"is mixed with explict 'fill' tags."
|
||||
|
@ -592,10 +514,7 @@ def is_whitespace_token(token):
|
|||
|
||||
|
||||
def is_block_tag_token(token, name):
|
||||
return (
|
||||
token.token_type == TokenType.BLOCK
|
||||
and token.split_contents()[0] == name
|
||||
)
|
||||
return token.token_type == TokenType.BLOCK and token.split_contents()[0] == name
|
||||
|
||||
|
||||
@register.tag(name="if_filled")
|
||||
|
@ -627,9 +546,7 @@ def do_if_filled_block(parser, token):
|
|||
slot_name, is_positive = parse_if_filled_bits(bits)
|
||||
nodelist = parser.parse(("elif_filled", "else_filled", "endif_filled"))
|
||||
branches: List[_IfSlotFilledBranchNode] = [
|
||||
IfSlotFilledConditionBranchNode(
|
||||
slot_name=slot_name, nodelist=nodelist, is_positive=is_positive
|
||||
)
|
||||
IfSlotFilledConditionBranchNode(slot_name=slot_name, nodelist=nodelist, is_positive=is_positive)
|
||||
]
|
||||
|
||||
token = parser.next_token()
|
||||
|
@ -638,13 +555,9 @@ def do_if_filled_block(parser, token):
|
|||
while token.contents.startswith("elif_filled"):
|
||||
bits = token.split_contents()
|
||||
slot_name, is_positive = parse_if_filled_bits(bits)
|
||||
nodelist: NodeList = parser.parse(
|
||||
("elif_filled", "else_filled", "endif_filled")
|
||||
)
|
||||
nodelist: NodeList = parser.parse(("elif_filled", "else_filled", "endif_filled"))
|
||||
branches.append(
|
||||
IfSlotFilledConditionBranchNode(
|
||||
slot_name=slot_name, nodelist=nodelist, is_positive=is_positive
|
||||
)
|
||||
IfSlotFilledConditionBranchNode(slot_name=slot_name, nodelist=nodelist, is_positive=is_positive)
|
||||
)
|
||||
|
||||
token = parser.next_token()
|
||||
|
@ -673,9 +586,7 @@ def parse_if_filled_bits(
|
|||
tag, args = bits[0], bits[1:]
|
||||
if tag in ("else_filled", "endif_filled"):
|
||||
if len(args) != 0:
|
||||
raise TemplateSyntaxError(
|
||||
f"Tag '{tag}' takes no arguments. Received '{' '.join(args)}'"
|
||||
)
|
||||
raise TemplateSyntaxError(f"Tag '{tag}' takes no arguments. Received '{' '.join(args)}'")
|
||||
else:
|
||||
return None, None
|
||||
if len(args) == 1:
|
||||
|
@ -686,13 +597,10 @@ def parse_if_filled_bits(
|
|||
is_positive = bool_from_string(args[1])
|
||||
else:
|
||||
raise TemplateSyntaxError(
|
||||
f"{bits[0]} tag arguments '{' '.join(args)}' do not match pattern "
|
||||
f"'<slotname> (<is_positive>)'"
|
||||
f"{bits[0]} tag arguments '{' '.join(args)}' do not match pattern " f"'<slotname> (<is_positive>)'"
|
||||
)
|
||||
if not is_wrapped_in_quotes(slot_name):
|
||||
raise TemplateSyntaxError(
|
||||
f"First argument of '{bits[0]}' must be a quoted string 'literal'."
|
||||
)
|
||||
raise TemplateSyntaxError(f"First argument of '{bits[0]}' must be a quoted string 'literal'.")
|
||||
slot_name = strip_quotes(slot_name)
|
||||
return slot_name, is_positive
|
||||
|
||||
|
@ -708,9 +616,7 @@ class _IfSlotFilledBranchNode(Node):
|
|||
raise NotImplementedError
|
||||
|
||||
|
||||
class IfSlotFilledConditionBranchNode(
|
||||
_IfSlotFilledBranchNode, TemplateAwareNodeMixin
|
||||
):
|
||||
class IfSlotFilledConditionBranchNode(_IfSlotFilledBranchNode, TemplateAwareNodeMixin):
|
||||
def __init__(
|
||||
self,
|
||||
slot_name: str,
|
||||
|
@ -723,9 +629,7 @@ class IfSlotFilledConditionBranchNode(
|
|||
|
||||
def evaluate(self, context) -> bool:
|
||||
try:
|
||||
filled_slots: FilledSlotsContext = context[
|
||||
FILLED_SLOTS_CONTENT_CONTEXT_KEY
|
||||
]
|
||||
filled_slots: FilledSlotsContext = context[FILLED_SLOTS_CONTENT_CONTEXT_KEY]
|
||||
except KeyError:
|
||||
raise TemplateSyntaxError(
|
||||
f"Attempted to render {type(self).__name__} outside a Component rendering context."
|
||||
|
@ -794,9 +698,7 @@ def parse_component_with_args(parser, bits, tag_name):
|
|||
)
|
||||
|
||||
if tag_name != tag_args[0].token:
|
||||
raise RuntimeError(
|
||||
f"Internal error: Expected tag_name to be {tag_name}, but it was {tag_args[0].token}"
|
||||
)
|
||||
raise RuntimeError(f"Internal error: Expected tag_name to be {tag_name}, but it was {tag_args[0].token}")
|
||||
if len(tag_args) > 1:
|
||||
# At least one position arg, so take the first as the component name
|
||||
component_name = tag_args[1].token
|
||||
|
@ -808,9 +710,7 @@ def parse_component_with_args(parser, bits, tag_name):
|
|||
context_args = []
|
||||
context_kwargs = tag_kwargs
|
||||
except IndexError:
|
||||
raise TemplateSyntaxError(
|
||||
f"Call the '{tag_name}' tag with a component name as the first parameter"
|
||||
)
|
||||
raise TemplateSyntaxError(f"Call the '{tag_name}' tag with a component name as the first parameter")
|
||||
|
||||
return component_name, context_args, context_kwargs
|
||||
|
||||
|
@ -818,11 +718,7 @@ def parse_component_with_args(parser, bits, tag_name):
|
|||
def safe_resolve(context_item, context):
|
||||
"""Resolve FilterExpressions and Variables in context if possible. Return other items unchanged."""
|
||||
|
||||
return (
|
||||
context_item.resolve(context)
|
||||
if hasattr(context_item, "resolve")
|
||||
else context_item
|
||||
)
|
||||
return context_item.resolve(context) if hasattr(context_item, "resolve") else context_item
|
||||
|
||||
|
||||
def is_wrapped_in_quotes(s):
|
||||
|
@ -830,9 +726,7 @@ def is_wrapped_in_quotes(s):
|
|||
|
||||
|
||||
def is_dependency_middleware_active():
|
||||
return getattr(settings, "COMPONENTS", {}).get(
|
||||
"RENDER_DEPENDENCIES", False
|
||||
)
|
||||
return getattr(settings, "COMPONENTS", {}).get("RENDER_DEPENDENCIES", False)
|
||||
|
||||
|
||||
def norm_and_validate_name(name: str, tag: str, context: Optional[str] = None):
|
||||
|
@ -843,10 +737,7 @@ def norm_and_validate_name(name: str, tag: str, context: Optional[str] = None):
|
|||
name = strip_quotes(name)
|
||||
if not name.isidentifier():
|
||||
context = f" in '{context}'" if context else ""
|
||||
raise TemplateSyntaxError(
|
||||
f"{tag} name '{name}'{context} "
|
||||
"is not a valid Python identifier."
|
||||
)
|
||||
raise TemplateSyntaxError(f"{tag} name '{name}'{context} " "is not a valid Python identifier.")
|
||||
return name
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[tool.black]
|
||||
line-length = 79
|
||||
line-length = 119
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
/(
|
||||
|
@ -18,7 +18,21 @@ exclude = '''
|
|||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 79
|
||||
line_length = 119
|
||||
multi_line_output = 3
|
||||
include_trailing_comma = "True"
|
||||
known_first_party = "django_components"
|
||||
|
||||
[tool.flake8]
|
||||
ignore = ['E302', 'W503']
|
||||
max-line-length = 119
|
||||
exclude = [
|
||||
'migrations',
|
||||
'__pycache__',
|
||||
'manage.py',
|
||||
'settings.py',
|
||||
'env',
|
||||
'.env',
|
||||
'.venv',
|
||||
'.tox',
|
||||
]
|
|
@ -2,5 +2,7 @@ django
|
|||
tox
|
||||
pytest
|
||||
flake8
|
||||
flake8-pyproject
|
||||
isort
|
||||
pre-commit
|
||||
pre-commit
|
||||
black
|
|
@ -6,23 +6,31 @@
|
|||
#
|
||||
asgiref==3.7.2
|
||||
# via django
|
||||
black==24.2.0
|
||||
# via -r requirements-dev.in
|
||||
cachetools==5.3.2
|
||||
# via tox
|
||||
cfgv==3.4.0
|
||||
# via pre-commit
|
||||
chardet==5.2.0
|
||||
# via tox
|
||||
click==8.1.7
|
||||
# via black
|
||||
colorama==0.4.6
|
||||
# via tox
|
||||
distlib==0.3.8
|
||||
# via virtualenv
|
||||
django==5.0.2
|
||||
django==5.0.3
|
||||
# via -r requirements-dev.in
|
||||
filelock==3.13.1
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
flake8==7.0.0
|
||||
# via
|
||||
# -r requirements-dev.in
|
||||
# flake8-pyproject
|
||||
flake8-pyproject==1.2.3
|
||||
# via -r requirements-dev.in
|
||||
identify==2.5.33
|
||||
# via pre-commit
|
||||
|
@ -32,15 +40,21 @@ isort==5.13.2
|
|||
# via -r requirements-dev.in
|
||||
mccabe==0.7.0
|
||||
# via flake8
|
||||
mypy-extensions==1.0.0
|
||||
# via black
|
||||
nodeenv==1.8.0
|
||||
# via pre-commit
|
||||
packaging==23.2
|
||||
# via
|
||||
# black
|
||||
# pyproject-api
|
||||
# pytest
|
||||
# tox
|
||||
pathspec==0.12.1
|
||||
# via black
|
||||
platformdirs==4.1.0
|
||||
# via
|
||||
# black
|
||||
# tox
|
||||
# virtualenv
|
||||
pluggy==1.3.0
|
||||
|
@ -55,7 +69,7 @@ pyflakes==3.2.0
|
|||
# via flake8
|
||||
pyproject-api==1.6.1
|
||||
# via tox
|
||||
pytest==8.0.1
|
||||
pytest==8.0.2
|
||||
# via -r requirements-dev.in
|
||||
pyyaml==6.0.1
|
||||
# via pre-commit
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
{% component_css_dependencies %}
|
||||
</head>
|
||||
<body>
|
||||
{% component "calendar" date=date %}
|
||||
{% component_block "greeting" name='Joe' %}
|
||||
{% component "calendar" date=date %}{% endcomponent %}
|
||||
{% component "greeting" name='Joe' %}
|
||||
{% fill "message" %}
|
||||
Howdy?
|
||||
{% endfill %}
|
||||
{% endcomponent_block %}
|
||||
{% endcomponent %}
|
||||
{% component_js_dependencies %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -3,17 +3,17 @@
|
|||
<div>Your to-dos:</div>
|
||||
<ul>
|
||||
<li>
|
||||
{% component_block "todo" %}
|
||||
{% component "todo" %}
|
||||
{% fill "todo_text" %}
|
||||
Stop forgetting the milk!
|
||||
{% endfill %}
|
||||
{% endcomponent_block %}
|
||||
{% endcomponent %}
|
||||
</li>
|
||||
<li>
|
||||
{% component_block "todo" %}
|
||||
{% component "todo" %}
|
||||
{# As of v0.28, 'fill' tag optional for 1-slot filling if component template specifies a 'default' slot #}
|
||||
Wear all-white clothes to laser tag tournament.
|
||||
{% endcomponent_block %}
|
||||
{% endcomponent %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
|
@ -30,9 +30,7 @@ def get_supported_versions(url):
|
|||
django_to_python = {
|
||||
version_to_tuple(python_version): [
|
||||
version_to_tuple(version_string)
|
||||
for version_string in re.findall(
|
||||
r"(?<!\.)\d+\.\d+(?!\.)", django_versions
|
||||
)
|
||||
for version_string in re.findall(r"(?<!\.)\d+\.\d+(?!\.)", django_versions)
|
||||
]
|
||||
for python_version, django_versions in version_dict.items()
|
||||
}
|
||||
|
@ -46,9 +44,7 @@ def get_latest_version(url):
|
|||
response_content = response.read()
|
||||
|
||||
content = response_content.decode("utf-8")
|
||||
version_string = re.findall(
|
||||
r"The latest official version is (\d+\.\d)", content
|
||||
)[0]
|
||||
version_string = re.findall(r"The latest official version is (\d+\.\d)", content)[0]
|
||||
return version_to_tuple(version_string)
|
||||
|
||||
|
||||
|
@ -108,9 +104,7 @@ def build_deps_envlist(python_to_django):
|
|||
(
|
||||
env_format(django_version),
|
||||
env_format(django_version, divider="."),
|
||||
env_format(
|
||||
(django_version[0], django_version[1] + 1), divider="."
|
||||
),
|
||||
env_format((django_version[0], django_version[1] + 1), divider="."),
|
||||
)
|
||||
for django_version in sorted(all_django_versions)
|
||||
]
|
||||
|
@ -123,9 +117,7 @@ def build_pypi_classifiers(python_to_django):
|
|||
|
||||
all_python_versions = python_to_django.keys()
|
||||
for python_version in all_python_versions:
|
||||
classifiers.append(
|
||||
f'"Programming Language :: Python :: {env_format(python_version, divider=".")}",'
|
||||
)
|
||||
classifiers.append(f'"Programming Language :: Python :: {env_format(python_version, divider=".")}",')
|
||||
|
||||
all_django_versions = set()
|
||||
for django_versions in python_to_django.values():
|
||||
|
@ -133,13 +125,9 @@ def build_pypi_classifiers(python_to_django):
|
|||
all_django_versions.add(django_version)
|
||||
|
||||
for django_version in sorted(all_django_versions):
|
||||
classifiers.append(
|
||||
f'"Framework :: Django :: {env_format(django_version, divider=".")}",'
|
||||
)
|
||||
classifiers.append(f'"Framework :: Django :: {env_format(django_version, divider=".")}",')
|
||||
|
||||
return textwrap.indent(
|
||||
"classifiers=[\n", prefix=" " * 4
|
||||
) + textwrap.indent("\n".join(classifiers), prefix=" " * 8)
|
||||
return textwrap.indent("classifiers=[\n", prefix=" " * 4) + textwrap.indent("\n".join(classifiers), prefix=" " * 8)
|
||||
|
||||
|
||||
def build_readme(python_to_django):
|
||||
|
@ -154,9 +142,7 @@ def build_readme(python_to_django):
|
|||
lines = [
|
||||
(
|
||||
env_format(python_version, divider="."),
|
||||
", ".join(
|
||||
env_format(version, divider=".") for version in django_versions
|
||||
),
|
||||
", ".join(env_format(version, divider=".") for version in django_versions),
|
||||
)
|
||||
for python_version, django_versions in python_to_django.items()
|
||||
]
|
||||
|
@ -169,13 +155,9 @@ def build_pyenv(python_to_django):
|
|||
lines = []
|
||||
all_python_versions = python_to_django.keys()
|
||||
for python_version in all_python_versions:
|
||||
lines.append(
|
||||
f'pyenv install -s {env_format(python_version, divider=".")}'
|
||||
)
|
||||
lines.append(f'pyenv install -s {env_format(python_version, divider=".")}')
|
||||
|
||||
lines.append(
|
||||
f'pyenv local {" ".join(env_format(version, divider=".") for version in all_python_versions)}'
|
||||
)
|
||||
lines.append(f'pyenv local {" ".join(env_format(version, divider=".") for version in all_python_versions)}')
|
||||
|
||||
lines.append("tox -p")
|
||||
|
||||
|
@ -185,20 +167,15 @@ def build_pyenv(python_to_django):
|
|||
def build_ci_python_versions(python_to_django):
|
||||
# Outputs python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11']
|
||||
lines = [
|
||||
f"'{env_format(python_version, divider='.')}'"
|
||||
for python_version, django_versions in python_to_django.items()
|
||||
f"'{env_format(python_version, divider='.')}'" for python_version, django_versions in python_to_django.items()
|
||||
]
|
||||
lines = " " * 8 + f"python-version: [{', '.join(lines)}]"
|
||||
return lines
|
||||
|
||||
|
||||
def main():
|
||||
django_to_python = get_supported_versions(
|
||||
"https://docs.djangoproject.com/en/dev/faq/install/"
|
||||
)
|
||||
latest_version = get_latest_version(
|
||||
"https://www.djangoproject.com/download/"
|
||||
)
|
||||
django_to_python = get_supported_versions("https://docs.djangoproject.com/en/dev/faq/install/")
|
||||
latest_version = get_latest_version("https://www.djangoproject.com/download/")
|
||||
|
||||
python_to_django = build_python_to_django(django_to_python, latest_version)
|
||||
|
||||
|
|
12
setup.cfg
12
setup.cfg
|
@ -1,12 +0,0 @@
|
|||
[flake8]
|
||||
ignore = E302,W503
|
||||
max-line-length = 119
|
||||
exclude =
|
||||
migrations
|
||||
__pycache__
|
||||
manage.py
|
||||
settings.py
|
||||
env
|
||||
.env
|
||||
.venv
|
||||
.tox
|
6
setup.py
6
setup.py
|
@ -3,16 +3,14 @@ import os
|
|||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
VERSION = "0.37"
|
||||
VERSION = "0.50"
|
||||
|
||||
setup(
|
||||
name="django_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"), encoding="utf8"
|
||||
).read(),
|
||||
long_description=open(os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf8").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
author="Emil Stenström",
|
||||
author_email="emil@emilstenstrom.se",
|
||||
|
|
|
@ -11,9 +11,7 @@ if not settings.configured:
|
|||
}
|
||||
],
|
||||
COMPONENTS={"template_cache_size": 128},
|
||||
MIDDLEWARE=[
|
||||
"django_components.middleware.ComponentDependencyMiddleware"
|
||||
],
|
||||
MIDDLEWARE=["django_components.middleware.ComponentDependencyMiddleware"],
|
||||
DATABASES={
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
|
|
|
@ -200,7 +200,7 @@ Page lightly modified and partially extracted as a component #}
|
|||
</div>
|
||||
</header>
|
||||
<div class="titlebar-container"><h1 class="title">Document and website structure</h1></div>
|
||||
{% component 'breadcrumb_component' items=5 %}
|
||||
{% component 'breadcrumb_component' items=5 %}{% endcomponent %}
|
||||
<div class="locale-container">
|
||||
<form class="language-menu"><label for="select_language" class="visually-hidden">Select your preferred
|
||||
language</label> <select id="select_language" name="language">
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
<div>
|
||||
<h1>Parent content</h1>
|
||||
{% component name="variable_display" shadowing_variable='override' new_variable='unique_val' %}
|
||||
{% component name="variable_display" shadowing_variable='override' new_variable='unique_val' %}{% endcomponent %}
|
||||
</div>
|
||||
<div>
|
||||
{% slot 'content' %}
|
||||
<h2>Slot content</h2>
|
||||
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %}
|
||||
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %}{% endcomponent %}
|
||||
{% endslot %}
|
||||
</div>
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
<div>
|
||||
<h1>Parent content</h1>
|
||||
{% component name="variable_display" shadowing_variable=inner_parent_value new_variable='unique_val' %}
|
||||
{% component name="variable_display" shadowing_variable=inner_parent_value new_variable='unique_val' %}{% endcomponent %}
|
||||
</div>
|
||||
<div>
|
||||
{% slot 'content' %}
|
||||
<h2>Slot content</h2>
|
||||
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable=inner_parent_value %}
|
||||
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable=inner_parent_value %}{% endcomponent %}
|
||||
{% endslot %}
|
||||
</div>
|
|
@ -1,11 +1,11 @@
|
|||
{% load component_tags %}
|
||||
<div class="dashboard-component">
|
||||
{% component_block "calendar" date="2020-06-06" %}
|
||||
{% component "calendar" date="2020-06-06" %}
|
||||
{% fill "header" %} {# fills and slots with same name relate to diff. things. #}
|
||||
{% slot "header" %}Welcome to your dashboard!{% endslot %}
|
||||
{% endfill %}
|
||||
{% fill "body" %}Here are your to-do items for today:{% endfill %}
|
||||
{% endcomponent_block %}
|
||||
{% endcomponent %}
|
||||
<ol>
|
||||
{% for item in items %}
|
||||
<li>{{ item }}</li>
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.template.engine import Engine
|
|||
from django.urls import include, path
|
||||
|
||||
# isort: off
|
||||
from .django_test_setup import * # noqa
|
||||
from .django_test_setup import settings
|
||||
from .testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
|
||||
|
||||
# isort: on
|
||||
|
@ -28,9 +28,7 @@ class TestAutodiscover(SimpleTestCase):
|
|||
try:
|
||||
autodiscover()
|
||||
except component.AlreadyRegistered:
|
||||
self.fail(
|
||||
"Autodiscover should not raise AlreadyRegistered exception"
|
||||
)
|
||||
self.fail("Autodiscover should not raise AlreadyRegistered exception")
|
||||
|
||||
|
||||
class TestLoaderSettingsModule(SimpleTestCase):
|
||||
|
@ -42,26 +40,17 @@ class TestLoaderSettingsModule(SimpleTestCase):
|
|||
current_engine = Engine.get_default()
|
||||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
self.assertEqual(
|
||||
dirs, [Path(__file__).parent.resolve() / "components"]
|
||||
)
|
||||
self.assertEqual(dirs, [Path(__file__).parent.resolve() / "components"])
|
||||
|
||||
def test_complex_settings_module(self):
|
||||
settings.SETTINGS_MODULE = ( # noqa
|
||||
"tests.test_structures.test_structure_1.config.settings"
|
||||
)
|
||||
settings.SETTINGS_MODULE = "tests.test_structures.test_structure_1.config.settings" # noqa
|
||||
|
||||
current_engine = Engine.get_default()
|
||||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
self.assertEqual(
|
||||
dirs,
|
||||
[
|
||||
Path(__file__).parent.resolve()
|
||||
/ "test_structures"
|
||||
/ "test_structure_1"
|
||||
/ "components"
|
||||
],
|
||||
[Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components"],
|
||||
)
|
||||
|
||||
def test_complex_settings_module_2(self):
|
||||
|
@ -72,13 +61,7 @@ class TestLoaderSettingsModule(SimpleTestCase):
|
|||
dirs = loader.get_dirs()
|
||||
self.assertEqual(
|
||||
dirs,
|
||||
[
|
||||
Path(__file__).parent.resolve()
|
||||
/ "test_structures"
|
||||
/ "test_structure_2"
|
||||
/ "project"
|
||||
/ "components"
|
||||
],
|
||||
[Path(__file__).parent.resolve() / "test_structures" / "test_structure_2" / "project" / "components"],
|
||||
)
|
||||
|
||||
def test_complex_settings_module_3(self):
|
||||
|
@ -88,19 +71,8 @@ class TestLoaderSettingsModule(SimpleTestCase):
|
|||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
expected = [
|
||||
(
|
||||
Path(__file__).parent.resolve()
|
||||
/ "test_structures"
|
||||
/ "test_structure_3"
|
||||
/ "components"
|
||||
),
|
||||
(
|
||||
Path(__file__).parent.resolve()
|
||||
/ "test_structures"
|
||||
/ "test_structure_3"
|
||||
/ "project"
|
||||
/ "components"
|
||||
),
|
||||
(Path(__file__).parent.resolve() / "test_structures" / "test_structure_3" / "components"),
|
||||
(Path(__file__).parent.resolve() / "test_structures" / "test_structure_3" / "project" / "components"),
|
||||
]
|
||||
self.assertEqual(
|
||||
sorted(dirs),
|
||||
|
@ -110,11 +82,7 @@ class TestLoaderSettingsModule(SimpleTestCase):
|
|||
|
||||
class TestBaseDir(SimpleTestCase):
|
||||
def setUp(self):
|
||||
settings.BASE_DIR = ( # noqa
|
||||
Path(__file__).parent.resolve()
|
||||
/ "test_structures"
|
||||
/ "test_structure_1"
|
||||
)
|
||||
settings.BASE_DIR = Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" # noqa
|
||||
settings.SETTINGS_MODULE = "tests_fake.test_autodiscover_fake" # noqa
|
||||
|
||||
def tearDown(self) -> None:
|
||||
|
@ -125,10 +93,5 @@ class TestBaseDir(SimpleTestCase):
|
|||
current_engine = Engine.get_default()
|
||||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
expected = [
|
||||
Path(__file__).parent.resolve()
|
||||
/ "test_structures"
|
||||
/ "test_structure_1"
|
||||
/ "components"
|
||||
]
|
||||
expected = [Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components"]
|
||||
self.assertEqual(dirs, expected)
|
||||
|
|
|
@ -234,9 +234,7 @@ class InlineComponentTest(SimpleTestCase):
|
|||
css = "path/to/style.css"
|
||||
js = "path/to/script.js"
|
||||
|
||||
comp = HTMLStringFileCSSJSComponent(
|
||||
"html_string_file_css_js_component"
|
||||
)
|
||||
comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
|
||||
self.assertHTMLEqual(
|
||||
comp.render(Context({})),
|
||||
"<div class='html-string-file'>Content</div>",
|
||||
|
@ -259,9 +257,7 @@ class InlineComponentTest(SimpleTestCase):
|
|||
class Media:
|
||||
css = "path/to/style.css"
|
||||
|
||||
comp = HTMLStringFileCSSJSComponent(
|
||||
"html_string_file_css_js_component"
|
||||
)
|
||||
comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
|
||||
self.assertHTMLEqual(
|
||||
comp.render(Context({})),
|
||||
"<div class='html-string-file'>Content</div>",
|
||||
|
@ -284,9 +280,7 @@ class InlineComponentTest(SimpleTestCase):
|
|||
class Media:
|
||||
js = "path/to/script.js"
|
||||
|
||||
comp = HTMLStringFileCSSJSComponent(
|
||||
"html_string_file_css_js_component"
|
||||
)
|
||||
comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
|
||||
self.assertHTMLEqual(
|
||||
comp.render(Context({})),
|
||||
"<div class='html-string-file'>Content</div>",
|
||||
|
@ -303,9 +297,7 @@ class InlineComponentTest(SimpleTestCase):
|
|||
def test_component_with_variable_in_html(self):
|
||||
class VariableHTMLComponent(component.Component):
|
||||
def get_template(self, context):
|
||||
return Template(
|
||||
"<div class='variable-html'>{{ variable }}</div>"
|
||||
)
|
||||
return Template("<div class='variable-html'>{{ variable }}</div>")
|
||||
|
||||
comp = VariableHTMLComponent("variable_html_component")
|
||||
context = Context({"variable": "Dynamic Content"})
|
||||
|
@ -403,15 +395,15 @@ class ComponentIsolationTests(SimpleTestCase):
|
|||
template = Template(
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% component_block "test" %}
|
||||
{% component "test" %}
|
||||
{% fill "header" %}Override header{% endfill %}
|
||||
{% endcomponent_block %}
|
||||
{% component_block "test" %}
|
||||
{% endcomponent %}
|
||||
{% component "test" %}
|
||||
{% fill "main" %}Override main{% endfill %}
|
||||
{% endcomponent_block %}
|
||||
{% component_block "test" %}
|
||||
{% endcomponent %}
|
||||
{% component "test" %}
|
||||
{% fill "footer" %}Override footer{% endfill %}
|
||||
{% endcomponent_block %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
)
|
||||
|
||||
|
|
|
@ -49,9 +49,7 @@ class MockComponentSlot(component.Component):
|
|||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs) -> HttpResponse:
|
||||
return self.render_to_response(
|
||||
{"name": "Bob"}, {"second_slot": "Nice to meet you, Bob"}
|
||||
)
|
||||
return self.render_to_response({"name": "Bob"}, {"second_slot": "Nice to meet you, Bob"})
|
||||
|
||||
|
||||
@component.register("testcomponent_context_insecure")
|
||||
|
@ -64,9 +62,7 @@ class MockInsecureComponentContext(component.Component):
|
|||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs) -> HttpResponse:
|
||||
return self.render_to_response(
|
||||
{"variable": "<script>alert(1);</script>"}
|
||||
)
|
||||
return self.render_to_response({"variable": "<script>alert(1);</script>"})
|
||||
|
||||
|
||||
@component.register("testcomponent_slot_insecure")
|
||||
|
@ -80,16 +76,14 @@ class MockInsecureComponentSlot(component.Component):
|
|||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs) -> HttpResponse:
|
||||
return self.render_to_response(
|
||||
{}, {"test_slot": "<script>alert(1);</script>"}
|
||||
)
|
||||
return self.render_to_response({}, {"test_slot": "<script>alert(1);</script>"})
|
||||
|
||||
|
||||
def render_template_view(request):
|
||||
template = Template(
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% component "testcomponent" variable="TEMPLATE" %}
|
||||
{% component "testcomponent" variable="TEMPLATE" %}{% endcomponent %}
|
||||
"""
|
||||
)
|
||||
return HttpResponse(template.render(Context({})))
|
||||
|
|
|
@ -65,15 +65,11 @@ class OuterContextComponent(component.Component):
|
|||
|
||||
|
||||
component.registry.register(name="parent_component", component=ParentComponent)
|
||||
component.registry.register(
|
||||
name="parent_with_args", component=ParentComponentWithArgs
|
||||
)
|
||||
component.registry.register(name="parent_with_args", component=ParentComponentWithArgs)
|
||||
component.registry.register(name="variable_display", component=VariableDisplay)
|
||||
component.registry.register(name="incrementer", component=IncrementerComponent)
|
||||
component.registry.register(name="simple_component", component=SimpleComponent)
|
||||
component.registry.register(
|
||||
name="outer_context_component", component=OuterContextComponent
|
||||
)
|
||||
component.registry.register(name="outer_context_component", component=OuterContextComponent)
|
||||
|
||||
|
||||
class ContextTests(SimpleTestCase):
|
||||
|
@ -82,73 +78,28 @@ class ContextTests(SimpleTestCase):
|
|||
):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component 'parent_component' %}"
|
||||
"{% component 'parent_component' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = override</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = slot_default_override</h1>",
|
||||
rendered,
|
||||
rendered,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
|
||||
)
|
||||
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
|
||||
|
||||
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag(
|
||||
self,
|
||||
):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component name='parent_component' %}"
|
||||
"{% component name='parent_component' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn(
|
||||
"<h1>Uniquely named variable = unique_val</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn(
|
||||
"<h1>Uniquely named variable = slot_default_unique</h1>",
|
||||
rendered,
|
||||
rendered,
|
||||
)
|
||||
|
||||
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_block_tag(
|
||||
self,
|
||||
):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_component' %}{% endcomponent_block %}"
|
||||
)
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = override</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = slot_default_override</h1>",
|
||||
rendered,
|
||||
rendered,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
|
||||
)
|
||||
|
||||
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_block_tag(
|
||||
self,
|
||||
):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_component' %}{% endcomponent_block %}"
|
||||
)
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn(
|
||||
"<h1>Uniquely named variable = unique_val</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn("<h1>Uniquely named variable = unique_val</h1>", rendered, rendered)
|
||||
self.assertIn(
|
||||
"<h1>Uniquely named variable = slot_default_unique</h1>",
|
||||
rendered,
|
||||
|
@ -157,41 +108,43 @@ class ContextTests(SimpleTestCase):
|
|||
|
||||
def test_nested_component_context_shadows_parent_with_filled_slots(self):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_component' %}"
|
||||
"{% fill 'content' %}{% component name='variable_display' "
|
||||
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endfill %}"
|
||||
"{% endcomponent_block %}"
|
||||
"""
|
||||
{% load component_tags %}{% component_dependencies %}
|
||||
{% component 'parent_component' %}
|
||||
{% fill 'content' %}
|
||||
{% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
|
||||
{% endcomponent %}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
""" # NOQA
|
||||
)
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = override</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = shadow_from_slot</h1>",
|
||||
rendered,
|
||||
rendered,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
|
||||
)
|
||||
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
|
||||
|
||||
def test_nested_component_instances_have_unique_context_with_filled_slots(
|
||||
self,
|
||||
):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_component' %}"
|
||||
"{% fill 'content' %}{% component name='variable_display' "
|
||||
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endfill %}"
|
||||
"{% endcomponent_block %}"
|
||||
"""
|
||||
{% load component_tags %}{% component_dependencies %}
|
||||
{% component 'parent_component' %}
|
||||
{% fill 'content' %}
|
||||
{% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
|
||||
{% endcomponent %}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
""" # NOQA
|
||||
)
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn(
|
||||
"<h1>Uniquely named variable = unique_val</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn("<h1>Uniquely named variable = unique_val</h1>", rendered, rendered)
|
||||
self.assertIn(
|
||||
"<h1>Uniquely named variable = unique_from_slot</h1>",
|
||||
rendered,
|
||||
|
@ -203,180 +156,119 @@ class ContextTests(SimpleTestCase):
|
|||
):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component name='parent_component' %}"
|
||||
)
|
||||
rendered = template.render(
|
||||
Context({"shadowing_variable": "NOT SHADOWED"})
|
||||
"{% component name='parent_component' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
|
||||
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = override</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = slot_default_override</h1>",
|
||||
rendered,
|
||||
rendered,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
|
||||
)
|
||||
|
||||
def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_block_tag(
|
||||
self,
|
||||
):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_component' %}{% endcomponent_block %}"
|
||||
)
|
||||
rendered = template.render(
|
||||
Context({"shadowing_variable": "NOT SHADOWED"})
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = override</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = slot_default_override</h1>",
|
||||
rendered,
|
||||
rendered,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
|
||||
)
|
||||
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
|
||||
|
||||
def test_nested_component_context_shadows_outer_context_with_filled_slots(
|
||||
self,
|
||||
):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_component' %}"
|
||||
"{% fill 'content' %}{% component name='variable_display' "
|
||||
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endfill %}"
|
||||
"{% endcomponent_block %}"
|
||||
)
|
||||
rendered = template.render(
|
||||
Context({"shadowing_variable": "NOT SHADOWED"})
|
||||
"""
|
||||
{% load component_tags %}{% component_dependencies %}
|
||||
{% component 'parent_component' %}
|
||||
{% fill 'content' %}
|
||||
{% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
|
||||
{% endcomponent %}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
""" # NOQA
|
||||
)
|
||||
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
|
||||
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = override</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = shadow_from_slot</h1>",
|
||||
rendered,
|
||||
rendered,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
|
||||
)
|
||||
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
|
||||
|
||||
|
||||
class ParentArgsTests(SimpleTestCase):
|
||||
def test_parent_args_can_be_drawn_from_context(self):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_with_args' parent_value=parent_value %}"
|
||||
"{% endcomponent_block %}"
|
||||
"{% component 'parent_with_args' parent_value=parent_value %}"
|
||||
"{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(Context({"parent_value": "passed_in"}))
|
||||
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = passed_in</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn(
|
||||
"<h1>Uniquely named variable = passed_in</h1>", rendered, rendered
|
||||
)
|
||||
self.assertNotIn(
|
||||
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn("<h1>Shadowing variable = passed_in</h1>", rendered, rendered)
|
||||
self.assertIn("<h1>Uniquely named variable = passed_in</h1>", rendered, rendered)
|
||||
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
|
||||
|
||||
def test_parent_args_available_outside_slots(self):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_with_args' parent_value='passed_in' %}{%endcomponent_block %}"
|
||||
"{% component 'parent_with_args' parent_value='passed_in' %}{%endcomponent %}"
|
||||
)
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = passed_in</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn(
|
||||
"<h1>Uniquely named variable = passed_in</h1>", rendered, rendered
|
||||
)
|
||||
self.assertNotIn(
|
||||
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn("<h1>Shadowing variable = passed_in</h1>", rendered, rendered)
|
||||
self.assertIn("<h1>Uniquely named variable = passed_in</h1>", rendered, rendered)
|
||||
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
|
||||
|
||||
def test_parent_args_available_in_slots(self):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_with_args' parent_value='passed_in' %}"
|
||||
"{% fill 'content' %}{% component name='variable_display' "
|
||||
"shadowing_variable='value_from_slot' new_variable=inner_parent_value %}{% endfill %}"
|
||||
"{%endcomponent_block %}"
|
||||
"""
|
||||
{% load component_tags %}{% component_dependencies %}
|
||||
{% component 'parent_with_args' parent_value='passed_in' %}
|
||||
{% fill 'content' %}
|
||||
{% component name='variable_display' shadowing_variable='value_from_slot' new_variable=inner_parent_value %}
|
||||
{% endcomponent %}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
""" # NOQA
|
||||
)
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn(
|
||||
"<h1>Shadowing variable = value_from_slot</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn(
|
||||
"<h1>Uniquely named variable = passed_in</h1>", rendered, rendered
|
||||
)
|
||||
self.assertNotIn(
|
||||
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
|
||||
)
|
||||
self.assertIn("<h1>Shadowing variable = value_from_slot</h1>", rendered, rendered)
|
||||
self.assertIn("<h1>Uniquely named variable = passed_in</h1>", rendered, rendered)
|
||||
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
|
||||
|
||||
|
||||
class ContextCalledOnceTests(SimpleTestCase):
|
||||
def test_one_context_call_with_simple_component(self):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component name='incrementer' %}"
|
||||
"{% component name='incrementer' %}{% endcomponent %}"
|
||||
)
|
||||
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_simple_component_and_arg(self):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component name='incrementer' value='2' %}"
|
||||
)
|
||||
template = Template("{% load component_tags %}{% component name='incrementer' value='2' %}{% endcomponent %}")
|
||||
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):
|
||||
template = Template(
|
||||
"{% load component_tags %}"
|
||||
"{% component_block 'incrementer' %}{% endcomponent_block %}"
|
||||
)
|
||||
def test_one_context_call_with_component(self):
|
||||
template = Template("{% load component_tags %}" "{% component 'incrementer' %}{% endcomponent %}")
|
||||
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):
|
||||
template = Template(
|
||||
"{% load component_tags %}"
|
||||
"{% component_block 'incrementer' value='3' %}{% endcomponent_block %}"
|
||||
)
|
||||
def test_one_context_call_with_component_and_arg(self):
|
||||
template = Template("{% load component_tags %}" "{% component 'incrementer' value='3' %}{% endcomponent %}")
|
||||
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):
|
||||
template = Template(
|
||||
"{% load component_tags %}"
|
||||
"{% component_block 'incrementer' %}{% fill 'content' %}"
|
||||
"<p>slot</p>{% endfill %}{% endcomponent_block %}"
|
||||
"{% component 'incrementer' %}{% fill 'content' %}"
|
||||
"<p>slot</p>{% endfill %}{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(Context()).strip()
|
||||
|
||||
|
@ -389,8 +281,8 @@ class ContextCalledOnceTests(SimpleTestCase):
|
|||
def test_one_context_call_with_slot_and_arg(self):
|
||||
template = Template(
|
||||
"{% load component_tags %}"
|
||||
"{% component_block 'incrementer' value='3' %}{% fill 'content' %}"
|
||||
"<p>slot</p>{% endfill %}{% endcomponent_block %}"
|
||||
"{% component 'incrementer' value='3' %}{% fill 'content' %}"
|
||||
"<p>slot</p>{% endfill %}{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(Context()).strip()
|
||||
|
||||
|
@ -405,11 +297,9 @@ class ComponentsCanAccessOuterContext(SimpleTestCase):
|
|||
def test_simple_component_can_use_outer_context(self):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component 'simple_component' %}"
|
||||
"{% component 'simple_component' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(
|
||||
Context({"variable": "outer_value"})
|
||||
).strip()
|
||||
rendered = template.render(Context({"variable": "outer_value"})).strip()
|
||||
self.assertIn("outer_value", rendered, rendered)
|
||||
|
||||
|
||||
|
@ -417,21 +307,17 @@ class IsolatedContextTests(SimpleTestCase):
|
|||
def test_simple_component_can_pass_outer_context_in_args(self):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component 'simple_component' variable only %}"
|
||||
"{% component 'simple_component' variable only %}{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(
|
||||
Context({"variable": "outer_value"})
|
||||
).strip()
|
||||
rendered = template.render(Context({"variable": "outer_value"})).strip()
|
||||
self.assertIn("outer_value", rendered, rendered)
|
||||
|
||||
def test_simple_component_cannot_use_outer_context(self):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component 'simple_component' only %}"
|
||||
"{% component 'simple_component' only %}{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(
|
||||
Context({"variable": "outer_value"})
|
||||
).strip()
|
||||
rendered = template.render(Context({"variable": "outer_value"})).strip()
|
||||
self.assertNotIn("outer_value", rendered, rendered)
|
||||
|
||||
|
||||
|
@ -452,7 +338,7 @@ class IsolatedContextSettingTests(SimpleTestCase):
|
|||
):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component 'simple_component' variable %}"
|
||||
"{% component 'simple_component' variable %}{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(Context({"variable": "outer_value"}))
|
||||
self.assertIn("outer_value", rendered, rendered)
|
||||
|
@ -462,29 +348,29 @@ class IsolatedContextSettingTests(SimpleTestCase):
|
|||
):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component 'simple_component' %}"
|
||||
"{% component 'simple_component' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(Context({"variable": "outer_value"}))
|
||||
self.assertNotIn("outer_value", rendered, rendered)
|
||||
|
||||
def test_component_block_includes_variable_with_isolated_context_from_settings(
|
||||
def test_component_includes_variable_with_isolated_context_from_settings(
|
||||
self,
|
||||
):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'simple_component' variable %}"
|
||||
"{% endcomponent_block %}"
|
||||
"{% component 'simple_component' variable %}"
|
||||
"{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(Context({"variable": "outer_value"}))
|
||||
self.assertIn("outer_value", rendered, rendered)
|
||||
|
||||
def test_component_block_excludes_variable_with_isolated_context_from_settings(
|
||||
def test_component_excludes_variable_with_isolated_context_from_settings(
|
||||
self,
|
||||
):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'simple_component' %}"
|
||||
"{% endcomponent_block %}"
|
||||
"{% component 'simple_component' %}"
|
||||
"{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(Context({"variable": "outer_value"}))
|
||||
self.assertNotIn("outer_value", rendered, rendered)
|
||||
|
@ -494,19 +380,7 @@ class OuterContextPropertyTests(SimpleTestCase):
|
|||
def test_outer_context_property_with_component(self):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component 'outer_context_component' only %}"
|
||||
"{% component 'outer_context_component' only %}{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(
|
||||
Context({"variable": "outer_value"})
|
||||
).strip()
|
||||
self.assertIn("outer_value", rendered, rendered)
|
||||
|
||||
def test_outer_context_property_with_component_block(self):
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'outer_context_component' only %}{% endcomponent_block %}"
|
||||
)
|
||||
rendered = template.render(
|
||||
Context({"variable": "outer_value"})
|
||||
).strip()
|
||||
rendered = template.render(Context({"variable": "outer_value"})).strip()
|
||||
self.assertIn("outer_value", rendered, rendered)
|
||||
|
|
|
@ -52,9 +52,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
def test_no_dependencies_when_no_components_used(self):
|
||||
component.registry.register(name="test", component=SimpleComponent)
|
||||
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
)
|
||||
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(
|
||||
|
@ -66,18 +64,14 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
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 %}"
|
||||
)
|
||||
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 %}"
|
||||
)
|
||||
template = Template("{% load component_tags %}{% component_css_dependencies %}")
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML(
|
||||
'<link href="style.css" media="all" rel="stylesheet"/>',
|
||||
|
@ -88,9 +82,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
def test_preload_dependencies_render_when_no_components_used(self):
|
||||
component.registry.register(name="test", component=SimpleComponent)
|
||||
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies preload='test' %}"
|
||||
)
|
||||
template = Template("{% load component_tags %}{% component_dependencies preload='test' %}")
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML('<script src="script.js">', rendered, count=1)
|
||||
self.assertInHTML(
|
||||
|
@ -102,9 +94,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
def test_preload_css_dependencies_render_when_no_components_used(self):
|
||||
component.registry.register(name="test", component=SimpleComponent)
|
||||
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_css_dependencies preload='test' %}"
|
||||
)
|
||||
template = Template("{% load component_tags %}{% component_css_dependencies preload='test' %}")
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML(
|
||||
'<link href="style.css" media="all" rel="stylesheet"/>',
|
||||
|
@ -117,7 +107,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component 'test' variable='foo' %}"
|
||||
"{% component 'test' variable='foo' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML(
|
||||
|
@ -132,7 +122,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component 'test' variable='foo' %}"
|
||||
"{% component 'test' variable='foo' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML(
|
||||
|
@ -147,7 +137,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies preload='test' %}"
|
||||
"{% component 'test' variable='foo' %}"
|
||||
"{% component 'test' variable='foo' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML(
|
||||
|
@ -162,7 +152,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component 'test' variable='foo' %}"
|
||||
"{% component 'test' variable='foo' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertNotIn("_RENDERED", rendered)
|
||||
|
@ -170,9 +160,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
def test_placeholder_removed_when_preload_rendered(self):
|
||||
component.registry.register(name="test", component=SimpleComponent)
|
||||
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies preload='test' %}"
|
||||
)
|
||||
template = Template("{% load component_tags %}{% component_dependencies preload='test' %}")
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertNotIn("_RENDERED", rendered)
|
||||
|
||||
|
@ -181,7 +169,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_css_dependencies %}"
|
||||
"{% component 'test' variable='foo' %}"
|
||||
"{% component 'test' variable='foo' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML(
|
||||
|
@ -195,7 +183,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_js_dependencies %}"
|
||||
"{% component 'test' variable='foo' %}"
|
||||
"{% component 'test' variable='foo' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML('<script src="script.js">', rendered, count=1)
|
||||
|
@ -205,7 +193,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
):
|
||||
component.registry.register(name="test", component=MultistyleComponent)
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}{% component 'test' %}"
|
||||
"{% load component_tags %}{% component_dependencies %}{% component 'test' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML('<script src="script.js">', rendered, count=1)
|
||||
|
@ -226,7 +214,10 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
):
|
||||
component.registry.register(name="test", component=MultistyleComponent)
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_js_dependencies %}{% component 'test' %}"
|
||||
"""
|
||||
{% load component_tags %}{% component_js_dependencies %}
|
||||
{% component 'test' %}{% endcomponent %}
|
||||
"""
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML('<script src="script.js">', rendered, count=1)
|
||||
|
@ -247,7 +238,10 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
):
|
||||
component.registry.register(name="test", component=MultistyleComponent)
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_css_dependencies %}{% component 'test' %}"
|
||||
"""
|
||||
{% load component_tags %}{% component_css_dependencies %}
|
||||
{% component 'test' %}{% endcomponent %}
|
||||
"""
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML('<script src="script.js">', rendered, count=0)
|
||||
|
@ -265,13 +259,9 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
|
||||
def test_no_dependencies_with_multiple_unused_components(self):
|
||||
component.registry.register(name="test1", component=SimpleComponent)
|
||||
component.registry.register(
|
||||
name="test2", component=SimpleComponentAlternate
|
||||
)
|
||||
component.registry.register(name="test2", component=SimpleComponentAlternate)
|
||||
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
)
|
||||
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)
|
||||
|
@ -288,13 +278,11 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
|
||||
def test_correct_css_dependencies_with_multiple_components(self):
|
||||
component.registry.register(name="test1", component=SimpleComponent)
|
||||
component.registry.register(
|
||||
name="test2", component=SimpleComponentAlternate
|
||||
)
|
||||
component.registry.register(name="test2", component=SimpleComponentAlternate)
|
||||
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_css_dependencies %}"
|
||||
"{% component 'test1' 'variable' %}"
|
||||
"{% component 'test1' 'variable' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML(
|
||||
|
@ -310,13 +298,11 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
|
||||
def test_correct_js_dependencies_with_multiple_components(self):
|
||||
component.registry.register(name="test1", component=SimpleComponent)
|
||||
component.registry.register(
|
||||
name="test2", component=SimpleComponentAlternate
|
||||
)
|
||||
component.registry.register(name="test2", component=SimpleComponentAlternate)
|
||||
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_js_dependencies %}"
|
||||
"{% component 'test1' 'variable' %}"
|
||||
"{% component 'test1' 'variable' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML('<script src="script.js">', rendered, count=1)
|
||||
|
@ -324,13 +310,11 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
|
||||
def test_correct_dependencies_with_multiple_components(self):
|
||||
component.registry.register(name="test1", component=SimpleComponent)
|
||||
component.registry.register(
|
||||
name="test2", component=SimpleComponentAlternate
|
||||
)
|
||||
component.registry.register(name="test2", component=SimpleComponentAlternate)
|
||||
|
||||
template = Template(
|
||||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component 'test2' variable='variable' %}"
|
||||
"{% component 'test2' variable='variable' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML('<script src="script.js">', rendered, count=0)
|
||||
|
@ -348,17 +332,16 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
|
||||
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
|
||||
)
|
||||
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' %}"
|
||||
"""
|
||||
{% load component_tags %}{% component_dependencies %}
|
||||
{% component 'test1' variable='variable' %}{% endcomponent %}
|
||||
{% component 'test2' variable='variable' %}{% endcomponent %}
|
||||
{% component 'test1' variable='variable' %}{% endcomponent %}
|
||||
"""
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertInHTML('<script src="script.js">', rendered, count=1)
|
||||
|
@ -376,26 +359,23 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
|
||||
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
|
||||
)
|
||||
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' %}"
|
||||
"""
|
||||
{% load component_tags %}{% component_dependencies %}
|
||||
{% component 'test1' variable='variable' %}{% endcomponent %}
|
||||
{% component 'test2' variable='variable' %}{% endcomponent %}
|
||||
{% component 'test1' variable='variable' %}{% endcomponent %}
|
||||
"""
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertNotIn("_RENDERED", rendered)
|
||||
|
||||
def test_middleware_response_without_content_type(self):
|
||||
response = HttpResponseNotModified()
|
||||
middleware = ComponentDependencyMiddleware(
|
||||
get_response=lambda _: response
|
||||
)
|
||||
middleware = ComponentDependencyMiddleware(get_response=lambda _: response)
|
||||
request = Mock()
|
||||
self.assertEqual(response, middleware(request=request))
|
||||
|
||||
|
@ -408,14 +388,12 @@ class ComponentMediaRenderingTests(SimpleTestCase):
|
|||
"test_component",
|
||||
]
|
||||
for component_name in component_names:
|
||||
component.registry.register(
|
||||
name=component_name, component=SimpleComponent
|
||||
)
|
||||
component.registry.register(name=component_name, component=SimpleComponent)
|
||||
template = Template(
|
||||
"{% load component_tags %}"
|
||||
"{% component_js_dependencies %}"
|
||||
"{% component_css_dependencies %}"
|
||||
f"{{% component '{component_name}' variable='value' %}}"
|
||||
f"{{% component '{component_name}' variable='value' %}}{{% endcomponent %}}"
|
||||
)
|
||||
rendered = create_and_process_template_response(template)
|
||||
self.assertHTMLEqual(
|
||||
|
|
|
@ -27,9 +27,7 @@ class ComponentRegistryTest(unittest.TestCase):
|
|||
class TestComponent(component.Component):
|
||||
pass
|
||||
|
||||
self.assertEqual(
|
||||
component.registry.get("decorated_component"), TestComponent
|
||||
)
|
||||
self.assertEqual(component.registry.get("decorated_component"), TestComponent)
|
||||
|
||||
def test_simple_register(self):
|
||||
self.registry.register(name="testcomponent", component=MockComponent)
|
||||
|
@ -49,18 +47,12 @@ class ComponentRegistryTest(unittest.TestCase):
|
|||
def test_prevent_registering_different_components_with_the_same_name(self):
|
||||
self.registry.register(name="testcomponent", component=MockComponent)
|
||||
with self.assertRaises(component.AlreadyRegistered):
|
||||
self.registry.register(
|
||||
name="testcomponent", component=MockComponent2
|
||||
)
|
||||
self.registry.register(name="testcomponent", component=MockComponent2)
|
||||
|
||||
def test_allow_duplicated_registration_of_the_same_component(self):
|
||||
try:
|
||||
self.registry.register(
|
||||
name="testcomponent", component=MockComponentView
|
||||
)
|
||||
self.registry.register(
|
||||
name="testcomponent", component=MockComponentView
|
||||
)
|
||||
self.registry.register(name="testcomponent", component=MockComponentView)
|
||||
self.registry.register(name="testcomponent", component=MockComponentView)
|
||||
except component.AlreadyRegistered:
|
||||
self.fail("Should not raise AlreadyRegistered")
|
||||
|
||||
|
|
|
@ -52,15 +52,11 @@ class CreateComponentCommandTest(TestCase):
|
|||
os.path.join(self.temp_dir, component_name, "test.js"),
|
||||
os.path.join(self.temp_dir, component_name, "test.css"),
|
||||
os.path.join(self.temp_dir, component_name, "test.html"),
|
||||
os.path.join(
|
||||
self.temp_dir, component_name, f"{component_name}.py"
|
||||
),
|
||||
os.path.join(self.temp_dir, component_name, f"{component_name}.py"),
|
||||
]
|
||||
|
||||
for file_path in expected_files:
|
||||
self.assertTrue(
|
||||
os.path.exists(file_path), f"File {file_path} was not created"
|
||||
)
|
||||
self.assertTrue(os.path.exists(file_path), f"File {file_path} was not created")
|
||||
|
||||
def test_dry_run(self):
|
||||
component_name = "dryruncomponent"
|
||||
|
@ -80,9 +76,7 @@ class CreateComponentCommandTest(TestCase):
|
|||
component_path = os.path.join(self.temp_dir, component_name)
|
||||
os.makedirs(component_path)
|
||||
|
||||
with open(
|
||||
os.path.join(component_path, f"{component_name}.py"), "w"
|
||||
) as f:
|
||||
with open(os.path.join(component_path, f"{component_name}.py"), "w") as f:
|
||||
f.write("hello world")
|
||||
|
||||
call_command(
|
||||
|
@ -93,9 +87,7 @@ class CreateComponentCommandTest(TestCase):
|
|||
"--force",
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(component_path, f"{component_name}.py"), "r"
|
||||
) as f:
|
||||
with open(os.path.join(component_path, f"{component_name}.py"), "r") as f:
|
||||
self.assertNotIn("hello world", f.read())
|
||||
|
||||
def test_error_existing_component_no_force(self):
|
||||
|
@ -104,9 +96,7 @@ class CreateComponentCommandTest(TestCase):
|
|||
os.makedirs(component_path)
|
||||
|
||||
with self.assertRaises(CommandError):
|
||||
call_command(
|
||||
"startcomponent", component_name, "--path", self.temp_dir
|
||||
)
|
||||
call_command("startcomponent", component_name, "--path", self.temp_dir)
|
||||
|
||||
def test_verbose_output(self):
|
||||
component_name = "verbosecomponent"
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,9 +8,7 @@ from django_components.middleware import ComponentDependencyMiddleware
|
|||
|
||||
# Create middleware instance
|
||||
response_stash = None
|
||||
middleware = ComponentDependencyMiddleware(
|
||||
get_response=lambda _: response_stash
|
||||
)
|
||||
middleware = ComponentDependencyMiddleware(get_response=lambda _: response_stash)
|
||||
|
||||
|
||||
class Django30CompatibleSimpleTestCase(SimpleTestCase):
|
||||
|
@ -19,9 +17,7 @@ class Django30CompatibleSimpleTestCase(SimpleTestCase):
|
|||
left = left.replace(' type="text/css"', "")
|
||||
right = right.replace(' type="text/javascript"', "")
|
||||
right = right.replace(' type="text/css"', "")
|
||||
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"', "")
|
||||
|
@ -37,9 +33,7 @@ request = Mock()
|
|||
mock_template = Mock()
|
||||
|
||||
|
||||
def create_and_process_template_response(
|
||||
template, context=None, use_middleware=True
|
||||
):
|
||||
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)
|
||||
|
|
4
tox.ini
4
tox.ini
|
@ -45,7 +45,9 @@ commands = py.test {posargs}
|
|||
[testenv:flake8]
|
||||
# Note: Settings for flake8 exists in the setup.cfg file
|
||||
changedir = {toxinidir}
|
||||
deps = flake8
|
||||
deps =
|
||||
flake8
|
||||
flake8-pyproject
|
||||
commands =
|
||||
flake8 .
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue