mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 14:28:18 +00:00
refactor: don't inherit media if child set to None (#1224)
* refactor: don't inherit media if child set to None * refactor: fix typing errors * refactor: more type fixes
This commit is contained in:
parent
8677ee7941
commit
09cb8714cc
6 changed files with 482 additions and 77 deletions
38
CHANGELOG.md
38
CHANGELOG.md
|
@ -4,6 +4,12 @@
|
|||
|
||||
⚠️ Major release ⚠️ - Please test thoroughly before / after upgrading.
|
||||
|
||||
This is the biggest step towards v1. While this version introduces
|
||||
many small API changes, we don't expect to make further changes to
|
||||
the affected parts before v1.
|
||||
|
||||
For more details see [#433](https://github.com/django-components/django-components/issues/433).
|
||||
|
||||
Summary:
|
||||
|
||||
- Overhauled typing system
|
||||
|
@ -220,6 +226,38 @@ Summary:
|
|||
return dynamic_template.render(context)
|
||||
```
|
||||
|
||||
- Subclassing of components with `None` values has changed:
|
||||
|
||||
Previously, when a child component's template / JS / CSS attributes were set to `None`, the child component still inherited the parent's template / JS / CSS.
|
||||
|
||||
Now, the child component will not inherit the parent's template / JS / CSS if it sets the attribute to `None`.
|
||||
|
||||
Before:
|
||||
|
||||
```py
|
||||
class Parent(Component):
|
||||
template = "parent.html"
|
||||
|
||||
class Child(Parent):
|
||||
template = None
|
||||
|
||||
# Child still inherited parent's template
|
||||
assert Child.template == Parent.template
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```py
|
||||
class Parent(Component):
|
||||
template = "parent.html"
|
||||
|
||||
class Child(Parent):
|
||||
template = None
|
||||
|
||||
# Child does not inherit parent's template
|
||||
assert Child.template is None
|
||||
```
|
||||
|
||||
- The `Component.Url` class was merged with `Component.View`.
|
||||
|
||||
Instead of `Component.Url.public`, use `Component.View.public`.
|
||||
|
|
|
@ -4,7 +4,7 @@ In such cases, you can extract shared behavior into a standalone component class
|
|||
|
||||
When subclassing a component, there's a couple of things to keep in mind:
|
||||
|
||||
### Template, JS, and CSS inheritance
|
||||
## Template, JS, and CSS inheritance
|
||||
|
||||
When it comes to the pairs:
|
||||
|
||||
|
@ -52,7 +52,7 @@ class CustomCard(BaseCard):
|
|||
"""
|
||||
```
|
||||
|
||||
### Media inheritance
|
||||
## Media inheritance
|
||||
|
||||
The [`Component.Media`](../../reference/api.md#django_components.Component.Media) nested class follows Django's media inheritance rules:
|
||||
|
||||
|
@ -83,7 +83,35 @@ class SimpleModal(BaseModal):
|
|||
js = ["simple_modal.js"] # Only this JS will be included
|
||||
```
|
||||
|
||||
### Regular Python inheritance
|
||||
## Opt out of inheritance
|
||||
|
||||
For the following media attributes, when you don't want to inherit from the parent,
|
||||
but you also don't need to set the template / JS / CSS to any specific value,
|
||||
you can set these attributes to `None`.
|
||||
|
||||
- [`template`](../../reference/api.md#django_components.Component.template) / [`template_file`](../../reference/api.md#django_components.Component.template_file)
|
||||
- [`js`](../../reference/api.md#django_components.Component.js) / [`js_file`](../../reference/api.md#django_components.Component.js_file)
|
||||
- [`css`](../../reference/api.md#django_components.Component.css) / [`css_file`](../../reference/api.md#django_components.Component.css_file)
|
||||
- [`Media`](../../reference/api.md#django_components.Component.Media) class
|
||||
|
||||
For example:
|
||||
|
||||
```djc_py
|
||||
class BaseForm(Component):
|
||||
template = "..."
|
||||
css = "..."
|
||||
js = "..."
|
||||
|
||||
class Media:
|
||||
js = ["form.js"]
|
||||
|
||||
# Use parent's template and CSS, but no JS
|
||||
class ContactForm(BaseForm):
|
||||
js = None
|
||||
Media = None
|
||||
```
|
||||
|
||||
## Regular Python inheritance
|
||||
|
||||
All other attributes and methods (including the [`Component.View`](../../reference/api.md#django_components.ComponentView) class and its methods) follow standard Python inheritance rules.
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ from typing import (
|
|||
Sequence,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
@ -27,6 +28,7 @@ from django.contrib.staticfiles import finders
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.forms.widgets import Media as MediaCls
|
||||
from django.utils.safestring import SafeData
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
from django_components.template import load_component_template
|
||||
from django_components.util.loader import get_component_dirs, resolve_file
|
||||
|
@ -37,10 +39,28 @@ if TYPE_CHECKING:
|
|||
from django_components.component import Component
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
# These are all the attributes that are handled by ComponentMedia and lazily-resolved
|
||||
COMP_MEDIA_LAZY_ATTRS = ("media", "template", "template_file", "js", "js_file", "css", "css_file")
|
||||
|
||||
|
||||
# Sentinel value to indicate that a media attribute is not set.
|
||||
# We use this to differntiate between setting template to `None` and not setting it at all.
|
||||
# If not set, we will use the template from the parent component.
|
||||
# If set to `None`, then this component has no template.
|
||||
class Unset:
|
||||
def __bool__(self) -> bool:
|
||||
return False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<UNSET>"
|
||||
|
||||
|
||||
UNSET = Unset()
|
||||
|
||||
|
||||
ComponentMediaInputPath = Union[
|
||||
str,
|
||||
bytes,
|
||||
|
@ -240,20 +260,36 @@ class ComponentMedia:
|
|||
comp_cls: Type["Component"]
|
||||
resolved: bool = False
|
||||
resolved_relative_files: bool = False
|
||||
Media: Optional[Type[ComponentMediaInput]] = None
|
||||
template: Optional[str] = None
|
||||
template_file: Optional[str] = None
|
||||
js: Optional[str] = None
|
||||
js_file: Optional[str] = None
|
||||
css: Optional[str] = None
|
||||
css_file: Optional[str] = None
|
||||
Media: Union[Type[ComponentMediaInput], Unset, None] = UNSET
|
||||
template: Union[str, Unset, None] = UNSET
|
||||
template_file: Union[str, Unset, None] = UNSET
|
||||
js: Union[str, Unset, None] = UNSET
|
||||
js_file: Union[str, Unset, None] = UNSET
|
||||
css: Union[str, Unset, None] = UNSET
|
||||
css_file: Union[str, Unset, None] = UNSET
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
for inlined_attr in ("template", "js", "css"):
|
||||
file_attr = f"{inlined_attr}_file"
|
||||
if getattr(self, inlined_attr) is not None and getattr(self, file_attr) is not None:
|
||||
|
||||
inlined_val = getattr(self, inlined_attr)
|
||||
file_val = getattr(self, file_attr)
|
||||
# NOTE: We raise if Component class received both inlined and file values:
|
||||
# ```py
|
||||
# class MyComp(Component):
|
||||
# js = "..."
|
||||
# js_file = "..."
|
||||
# ```
|
||||
#
|
||||
# But both `None` are allowed:
|
||||
# ```py
|
||||
# class MyComp(Component):
|
||||
# js = None
|
||||
# js_file = None
|
||||
# ```
|
||||
if (inlined_val is not UNSET and file_val is not UNSET) and not (inlined_val is None and file_val is None):
|
||||
raise ImproperlyConfigured(
|
||||
f"Received non-null value from both '{inlined_attr}' and '{file_attr}' in"
|
||||
f"Received non-empty value from both '{inlined_attr}' and '{file_attr}' in"
|
||||
f" Component {self.comp_cls.__name__}. Only one of the two must be set."
|
||||
)
|
||||
# Make a copy of the original state, so we can reset it in tests
|
||||
|
@ -340,13 +376,13 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
|
|||
resolved=False,
|
||||
# NOTE: We take the values from `attrs` so we consider only the values that were set on THIS class,
|
||||
# and not the values that were inherited from the parent classes.
|
||||
Media=attrs.get("Media", None),
|
||||
template=attrs.get("template", None),
|
||||
template_file=attrs.get("template_file", None),
|
||||
js=attrs.get("js", None),
|
||||
js_file=attrs.get("js_file", None),
|
||||
css=attrs.get("css", None),
|
||||
css_file=attrs.get("css_file", None),
|
||||
Media=attrs.get("Media", UNSET),
|
||||
template=attrs.get("template", UNSET),
|
||||
template_file=attrs.get("template_file", UNSET),
|
||||
js=attrs.get("js", UNSET),
|
||||
js_file=attrs.get("js_file", UNSET),
|
||||
css=attrs.get("css", UNSET),
|
||||
css_file=attrs.get("css_file", UNSET),
|
||||
)
|
||||
|
||||
def get_comp_media_attr(attr: str) -> Any:
|
||||
|
@ -383,34 +419,34 @@ def _get_comp_cls_attr(comp_cls: Type["Component"], attr: str) -> Any:
|
|||
continue
|
||||
if not comp_media.resolved:
|
||||
_resolve_media(base, comp_media)
|
||||
value = getattr(comp_media, attr, None)
|
||||
|
||||
# NOTE: We differentiate between `None` and `UNSET`, so that users can set `None` to
|
||||
# override parent class's value and set it to `None`.
|
||||
value = getattr(comp_media, attr, UNSET)
|
||||
|
||||
# For each of the pairs of inlined_content + file (e.g. `js` + `js_file`), if at least one of the two
|
||||
# is defined, we interpret it such that this (sub)class has overridden what was set by the parent class(es),
|
||||
# and we won't search further up the MRO.
|
||||
def check_pair_empty(inline_attr: str, file_attr: str) -> bool:
|
||||
inline_attr_empty = getattr(comp_media, inline_attr, None) is None
|
||||
file_attr_empty = getattr(comp_media, file_attr, None) is None
|
||||
return inline_attr_empty and file_attr_empty
|
||||
def resolve_pair(inline_attr: str, file_attr: str) -> Any:
|
||||
inline_attr_empty = getattr(comp_media, inline_attr, UNSET) is UNSET
|
||||
file_attr_empty = getattr(comp_media, file_attr, UNSET) is UNSET
|
||||
|
||||
if attr in ("js", "js_file"):
|
||||
if check_pair_empty("js", "js_file"):
|
||||
continue
|
||||
else:
|
||||
return value
|
||||
if attr in ("css", "css_file"):
|
||||
if check_pair_empty("css", "css_file"):
|
||||
continue
|
||||
else:
|
||||
return value
|
||||
if attr in ("template", "template_file"):
|
||||
if check_pair_empty("template", "template_file"):
|
||||
continue
|
||||
is_pair_empty = inline_attr_empty and file_attr_empty
|
||||
if is_pair_empty:
|
||||
return UNSET
|
||||
else:
|
||||
return value
|
||||
|
||||
# For the other attributes, simply search for the closest non-null
|
||||
if value is not None:
|
||||
if attr in ("js", "js_file"):
|
||||
value = resolve_pair("js", "js_file")
|
||||
elif attr in ("css", "css_file"):
|
||||
value = resolve_pair("css", "css_file")
|
||||
elif attr in ("template", "template_file"):
|
||||
value = resolve_pair("template", "template_file")
|
||||
|
||||
if value is UNSET:
|
||||
continue
|
||||
else:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
@ -452,8 +488,20 @@ def _get_comp_cls_media(comp_cls: Type["Component"]) -> Any:
|
|||
continue
|
||||
|
||||
# Prepare base classes
|
||||
media_input = getattr(curr_cls, "Media", None)
|
||||
media_extend = getattr(media_input, "extend", True)
|
||||
# NOTE: If the `Component.Media` class is explicitly set to `None`, then we should not inherit
|
||||
# from any parent classes.
|
||||
# ```py
|
||||
# class MyComponent(Component):
|
||||
# Media = None
|
||||
# ```
|
||||
# But if the `Component.Media` class is NOT set, then we inherit from the parent classes.
|
||||
# ```py
|
||||
# class MyComponent(Component):
|
||||
# pass
|
||||
# ```
|
||||
media_input = getattr(curr_cls, "Media", UNSET)
|
||||
default_extend = True if media_input is not None else False
|
||||
media_extend = getattr(media_input, "extend", default_extend)
|
||||
|
||||
# This ensures the same behavior as Django's Media class, where:
|
||||
# - If `Media.extend == True`, then the media files are inherited from the parent classes.
|
||||
|
@ -746,13 +794,9 @@ def _resolve_component_relative_files(
|
|||
# First check if we even need to resolve anything. If the class doesn't define any
|
||||
# HTML/JS/CSS files, just skip.
|
||||
will_resolve_files = False
|
||||
if (
|
||||
getattr(comp_media, "template_file", None)
|
||||
or getattr(comp_media, "js_file", None)
|
||||
or getattr(comp_media, "css_file", None)
|
||||
):
|
||||
if is_set(comp_media.template_file) or is_set(comp_media.js_file) or is_set(comp_media.css_file):
|
||||
will_resolve_files = True
|
||||
elif not will_resolve_files and getattr(comp_media, "Media", None):
|
||||
elif not will_resolve_files and is_set(comp_media.Media):
|
||||
if getattr(comp_media.Media, "css", None) or getattr(comp_media.Media, "js", None):
|
||||
will_resolve_files = True
|
||||
|
||||
|
@ -831,14 +875,14 @@ def _resolve_component_relative_files(
|
|||
return resolved_filepaths
|
||||
|
||||
# Check if template name is a local file or not
|
||||
if getattr(comp_media, "template_file", None):
|
||||
if is_set(comp_media.template_file):
|
||||
comp_media.template_file = resolve_relative_media_file(comp_media.template_file, False)[0]
|
||||
if getattr(comp_media, "js_file", None):
|
||||
if is_set(comp_media.js_file):
|
||||
comp_media.js_file = resolve_relative_media_file(comp_media.js_file, False)[0]
|
||||
if getattr(comp_media, "css_file", None):
|
||||
if is_set(comp_media.css_file):
|
||||
comp_media.css_file = resolve_relative_media_file(comp_media.css_file, False)[0]
|
||||
|
||||
if hasattr(comp_media, "Media") and comp_media.Media:
|
||||
if is_set(comp_media.Media):
|
||||
_map_media_filepaths(
|
||||
comp_media.Media,
|
||||
# Media files can be defined as a glob patterns that match multiple files.
|
||||
|
@ -933,7 +977,7 @@ def _get_asset(
|
|||
file_attr: str,
|
||||
comp_dirs: List[Path],
|
||||
type: Literal["template", "static"],
|
||||
) -> Optional[str]:
|
||||
) -> Union[str, Unset, None]:
|
||||
"""
|
||||
In case of Component's JS or CSS, one can either define that as "inlined" or as a file.
|
||||
|
||||
|
@ -957,24 +1001,73 @@ def _get_asset(
|
|||
|
||||
These are mutually exclusive, so only one of the two can be set at class creation.
|
||||
"""
|
||||
asset_content = getattr(comp_media, inlined_attr, None)
|
||||
asset_file = getattr(comp_media, file_attr, None)
|
||||
asset_content = getattr(comp_media, inlined_attr, UNSET)
|
||||
asset_file = getattr(comp_media, file_attr, UNSET)
|
||||
|
||||
# No inlined content, nor file name
|
||||
if asset_content is None and asset_file is None:
|
||||
# ```py
|
||||
# class MyComp(Component):
|
||||
# pass
|
||||
# ```
|
||||
if asset_content is UNSET and asset_file is UNSET:
|
||||
return UNSET
|
||||
|
||||
# Either file or content attr was set to `None`
|
||||
# ```py
|
||||
# class MyComp(Component):
|
||||
# js_file = None
|
||||
# ```
|
||||
# or
|
||||
# ```py
|
||||
# class MyComp(Component):
|
||||
# js = None
|
||||
# ```
|
||||
# or
|
||||
# ```py
|
||||
# class MyComp(Component):
|
||||
# js = None
|
||||
# js_file = None
|
||||
# ```
|
||||
if (asset_content in (UNSET, None) and asset_file is None) or (
|
||||
asset_content is None and asset_file in (UNSET, None)
|
||||
):
|
||||
return None
|
||||
|
||||
if asset_content is not None and asset_file is not None:
|
||||
# Received both inlined content and file name
|
||||
# ```py
|
||||
# class MyComp(Component):
|
||||
# js = "..."
|
||||
# js_file = "..."
|
||||
# ```
|
||||
#
|
||||
# Or received file name / content AND explicit `None`
|
||||
# ```py
|
||||
# class MyComp(Component):
|
||||
# js = "..."
|
||||
# js_file = None
|
||||
# ```
|
||||
# or
|
||||
# ```py
|
||||
# class MyComp(Component):
|
||||
# js = None
|
||||
# js_file = "..."
|
||||
# ```
|
||||
if asset_content is not UNSET and asset_file is not UNSET:
|
||||
raise ValueError(
|
||||
f"Received both '{inlined_attr}' and '{file_attr}' in Component {comp_cls.__qualname__}."
|
||||
" Only one of the two must be set."
|
||||
)
|
||||
|
||||
# At this point we can tell that only EITHER `asset_content` OR `asset_file` is set.
|
||||
|
||||
# If the content was inlined into the component (e.g. `Component.template = "..."`)
|
||||
# then there's nothing to resolve. Return as is.
|
||||
if asset_content is not None:
|
||||
if asset_content is not UNSET:
|
||||
return asset_content
|
||||
|
||||
if asset_file is None:
|
||||
return None
|
||||
|
||||
# The rest of the code assumes that we were given only a file name
|
||||
asset_file = cast(str, asset_file)
|
||||
|
||||
|
@ -1004,3 +1097,7 @@ def _get_asset(
|
|||
# NOTE: `on_template_preprocess()` is already applied inside `load_component_template()`
|
||||
|
||||
return asset_content
|
||||
|
||||
|
||||
def is_set(value: Union[T, Unset, None]) -> TypeGuard[T]:
|
||||
return value is not None and value is not UNSET
|
||||
|
|
|
@ -381,7 +381,7 @@ def cache_component_template_file(component_cls: Type["Component"]) -> None:
|
|||
return
|
||||
|
||||
# NOTE: Avoids circular import
|
||||
from django_components.component_media import ComponentMedia, _resolve_component_relative_files
|
||||
from django_components.component_media import ComponentMedia, Unset, _resolve_component_relative_files, is_set
|
||||
|
||||
# If we access the `Component.template_file` attribute, then this triggers media resolution if it was not done yet.
|
||||
# The problem is that this also causes the loading of the Template, if Component has defined `template_file`.
|
||||
|
@ -395,7 +395,7 @@ def cache_component_template_file(component_cls: Type["Component"]) -> None:
|
|||
# directly, thus avoiding the triggering of the Template loading.
|
||||
comp_media: ComponentMedia = component_cls._component_media # type: ignore[attr-defined]
|
||||
if comp_media.resolved and comp_media.resolved_relative_files:
|
||||
template_file = component_cls.template_file
|
||||
template_file: Union[str, Unset, None] = component_cls.template_file
|
||||
else:
|
||||
# NOTE: This block of code is based on `_resolve_media()` in `component_media.py`
|
||||
if not comp_media.resolved_relative_files:
|
||||
|
@ -404,7 +404,7 @@ def cache_component_template_file(component_cls: Type["Component"]) -> None:
|
|||
|
||||
template_file = comp_media.template_file
|
||||
|
||||
if template_file is None:
|
||||
if not is_set(template_file):
|
||||
return
|
||||
|
||||
if template_file not in component_template_file_cache:
|
||||
|
|
|
@ -17,6 +17,7 @@ from django_components.component import ALL_COMPONENTS, Component, component_nod
|
|||
from django_components.component_media import ComponentMedia
|
||||
from django_components.component_registry import ALL_REGISTRIES, ComponentRegistry
|
||||
from django_components.extension import extensions
|
||||
from django_components.perfutil.provide import provide_cache
|
||||
from django_components.template import _reset_component_template_file_cache, loading_components
|
||||
|
||||
# NOTE: `ReferenceType` is NOT a generic pre-3.9
|
||||
|
@ -472,6 +473,9 @@ def _clear_djc_global_state(
|
|||
if component_media_cache:
|
||||
component_media_cache.clear()
|
||||
|
||||
if provide_cache:
|
||||
provide_cache.clear()
|
||||
|
||||
# Remove cached Node subclasses
|
||||
component_node_subclasses_by_name.clear()
|
||||
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.forms.widgets import Media
|
||||
from django.template import Context, Template
|
||||
from django.templatetags.static import static
|
||||
|
@ -11,6 +15,7 @@ from django.utils.safestring import mark_safe
|
|||
from pytest_django.asserts import assertHTMLEqual, assertInHTML
|
||||
|
||||
from django_components import Component, autodiscover, registry, render_dependencies, types
|
||||
from django_components.component_media import UNSET
|
||||
|
||||
from django_components.testing import djc_test
|
||||
from .testutils import setup_test_config
|
||||
|
@ -24,12 +29,14 @@ setup_test_config({"autodiscover": False})
|
|||
class TestMainMedia:
|
||||
def test_html_js_css_inlined(self):
|
||||
class TestComponent(Component):
|
||||
template = dedent("""
|
||||
template = dedent(
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% component_js_dependencies %}
|
||||
{% component_css_dependencies %}
|
||||
<div class='html-css-only'>Content</div>
|
||||
""")
|
||||
"""
|
||||
)
|
||||
css = ".html-css-only { color: blue; }"
|
||||
js = "console.log('HTML and JS only');"
|
||||
|
||||
|
@ -52,12 +59,14 @@ class TestMainMedia:
|
|||
)
|
||||
|
||||
# Check that the HTML / JS / CSS can be accessed on the component class
|
||||
assert TestComponent.template == dedent("""
|
||||
assert TestComponent.template == dedent(
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% component_js_dependencies %}
|
||||
{% component_css_dependencies %}
|
||||
<div class='html-css-only'>Content</div>
|
||||
""")
|
||||
"""
|
||||
)
|
||||
assert TestComponent.css == ".html-css-only { color: blue; }"
|
||||
assert TestComponent.js == "console.log('HTML and JS only');"
|
||||
|
||||
|
@ -170,8 +179,7 @@ class TestMainMedia:
|
|||
"}"
|
||||
)
|
||||
assert TestComponent.js == (
|
||||
"/* Used in `MainMediaTest` tests in `test_component_media.py` */\n"
|
||||
'console.log("HTML and JS only");\n'
|
||||
'/* Used in `MainMediaTest` tests in `test_component_media.py` */\nconsole.log("HTML and JS only");\n'
|
||||
)
|
||||
|
||||
@djc_test(
|
||||
|
@ -189,7 +197,7 @@ class TestMainMedia:
|
|||
|
||||
# NOTE: Since this is a subclass, actual CSS is defined on the parent class, and thus
|
||||
# the corresponding ComponentMedia instance is also on the parent class.
|
||||
assert AppLvlCompComponent._component_media.css is None # type: ignore[attr-defined]
|
||||
assert AppLvlCompComponent._component_media.css is UNSET # type: ignore[attr-defined]
|
||||
assert AppLvlCompComponent._component_media.css_file == "app_lvl_comp.css" # type: ignore[attr-defined]
|
||||
|
||||
# Access the property to load the CSS
|
||||
|
@ -379,6 +387,7 @@ class TestComponentMedia:
|
|||
)
|
||||
def test_glob_pattern_relative_to_component(self):
|
||||
from tests.components.glob.glob import GlobComponent
|
||||
|
||||
rendered = GlobComponent.render()
|
||||
|
||||
assertInHTML('<link href="glob/glob_1.css" media="all" rel="stylesheet">', rendered)
|
||||
|
@ -393,6 +402,7 @@ class TestComponentMedia:
|
|||
)
|
||||
def test_glob_pattern_relative_to_root_dir(self):
|
||||
from tests.components.glob.glob import GlobComponentRootDir
|
||||
|
||||
rendered = GlobComponentRootDir.render()
|
||||
|
||||
assertInHTML('<link href="glob/glob_1.css" media="all" rel="stylesheet">', rendered)
|
||||
|
@ -407,6 +417,7 @@ class TestComponentMedia:
|
|||
)
|
||||
def test_non_globs_not_modified(self):
|
||||
from tests.components.glob.glob import NonGlobComponentRootDir
|
||||
|
||||
rendered = NonGlobComponentRootDir.render()
|
||||
|
||||
assertInHTML('<link href="glob/glob_1.css" media="all" rel="stylesheet">', rendered)
|
||||
|
@ -419,6 +430,7 @@ class TestComponentMedia:
|
|||
)
|
||||
def test_non_globs_not_modified_nonexist(self):
|
||||
from tests.components.glob.glob import NonGlobNonexistComponentRootDir
|
||||
|
||||
rendered = NonGlobNonexistComponentRootDir.render()
|
||||
|
||||
assertInHTML('<link href="glob/glob_nonexist.css" media="all" rel="stylesheet">', rendered)
|
||||
|
@ -426,6 +438,7 @@ class TestComponentMedia:
|
|||
|
||||
def test_glob_pattern_does_not_break_urls(self):
|
||||
from tests.components.glob.glob import UrlComponent
|
||||
|
||||
rendered = UrlComponent.render()
|
||||
|
||||
assertInHTML('<link href="https://example.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
|
||||
|
@ -434,10 +447,16 @@ class TestComponentMedia:
|
|||
assertInHTML('<link href="%3A//example.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
|
||||
assertInHTML('<link href="/path/to/style.css" media="all" rel="stylesheet">', rendered)
|
||||
|
||||
assertInHTML('<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered)
|
||||
assertInHTML('<script src="http://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered)
|
||||
assertInHTML(
|
||||
'<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered
|
||||
)
|
||||
assertInHTML(
|
||||
'<script src="http://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered
|
||||
)
|
||||
# `://` is escaped because Django's `Media.absolute_path()` doesn't consider `://` a valid URL
|
||||
assertInHTML('<script src="%3A//cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered)
|
||||
assertInHTML(
|
||||
'<script src="%3A//cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered
|
||||
)
|
||||
assertInHTML('<script src="/path/to/script.js"></script>', rendered)
|
||||
|
||||
|
||||
|
@ -831,9 +850,7 @@ class TestMediaStaticfiles:
|
|||
|
||||
# NOTE: Since we're using ManifestStaticFilesStorage, we expect the rendered media to link
|
||||
# to the files as defined in staticfiles.json
|
||||
assertInHTML(
|
||||
'<link href="/static/calendar/style.0eeb72042b59.css" media="all" rel="stylesheet">', rendered
|
||||
)
|
||||
assertInHTML('<link href="/static/calendar/style.0eeb72042b59.css" media="all" rel="stylesheet">', rendered)
|
||||
|
||||
assertInHTML('<script defer src="/static/calendar/script.e1815e23e0ec.js"></script>', rendered)
|
||||
|
||||
|
@ -1014,6 +1031,129 @@ class TestMediaRelativePath:
|
|||
assertInHTML('<script type="module" src="relative_file_pathobj.js"></script>', rendered)
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestSubclassingAttributes:
|
||||
def test_both_js_and_js_file_none(self):
|
||||
class TestComp(Component):
|
||||
js = None
|
||||
js_file = None
|
||||
|
||||
assert TestComp.js is None
|
||||
assert TestComp.js_file is None
|
||||
|
||||
def test_mixing_none_and_non_none_raises(self):
|
||||
with pytest.raises(
|
||||
ImproperlyConfigured,
|
||||
match=re.escape("Received non-empty value from both 'js' and 'js_file' in Component TestComp"),
|
||||
):
|
||||
|
||||
class TestComp(Component):
|
||||
js = "console.log('hi')"
|
||||
js_file = None
|
||||
|
||||
def test_both_non_none_raises(self):
|
||||
with pytest.raises(
|
||||
ImproperlyConfigured,
|
||||
match=re.escape("Received non-empty value from both 'js' and 'js_file' in Component TestComp"),
|
||||
):
|
||||
|
||||
class TestComp(Component):
|
||||
js = "console.log('hi')"
|
||||
js_file = "file.js"
|
||||
|
||||
def test_parent_non_null_child_non_null(self):
|
||||
class ParentComp(Component):
|
||||
js = "console.log('parent')"
|
||||
|
||||
class TestComp(ParentComp):
|
||||
js = "console.log('child')"
|
||||
|
||||
assert TestComp.js == "console.log('child')"
|
||||
assert TestComp.js_file is None
|
||||
|
||||
def test_parent_null_child_non_null(self):
|
||||
class ParentComp(Component):
|
||||
js = None
|
||||
|
||||
class TestComp(ParentComp):
|
||||
js = "console.log('child')"
|
||||
|
||||
assert TestComp.js == "console.log('child')"
|
||||
assert TestComp.js_file is None
|
||||
|
||||
def test_parent_non_null_child_null(self):
|
||||
class ParentComp(Component):
|
||||
js: Optional[str] = "console.log('parent')"
|
||||
|
||||
class TestComp(ParentComp):
|
||||
js = None
|
||||
|
||||
assert TestComp.js is None
|
||||
assert TestComp.js_file is None
|
||||
|
||||
def test_parent_null_child_null(self):
|
||||
class ParentComp(Component):
|
||||
js = None
|
||||
|
||||
class TestComp(ParentComp):
|
||||
js = None
|
||||
|
||||
assert TestComp.js is None
|
||||
assert TestComp.js_file is None
|
||||
|
||||
def test_grandparent_non_null_parent_pass_child_pass(self):
|
||||
class GrandParentComp(Component):
|
||||
js = "console.log('grandparent')"
|
||||
|
||||
class ParentComp(GrandParentComp):
|
||||
pass
|
||||
|
||||
class TestComp(ParentComp):
|
||||
pass
|
||||
|
||||
assert TestComp.js == "console.log('grandparent')"
|
||||
assert TestComp.js_file is None
|
||||
|
||||
def test_grandparent_non_null_parent_null_child_pass(self):
|
||||
class GrandParentComp(Component):
|
||||
js: Optional[str] = "console.log('grandparent')"
|
||||
|
||||
class ParentComp(GrandParentComp):
|
||||
js = None
|
||||
|
||||
class TestComp(ParentComp):
|
||||
pass
|
||||
|
||||
assert TestComp.js is None
|
||||
assert TestComp.js_file is None
|
||||
|
||||
def test_grandparent_non_null_parent_pass_child_non_null(self):
|
||||
class GrandParentComp(Component):
|
||||
js = "console.log('grandparent')"
|
||||
|
||||
class ParentComp(GrandParentComp):
|
||||
pass
|
||||
|
||||
class TestComp(ParentComp):
|
||||
js = "console.log('child')"
|
||||
|
||||
assert TestComp.js == "console.log('child')"
|
||||
assert TestComp.js_file is None
|
||||
|
||||
def test_grandparent_null_parent_pass_child_non_null(self):
|
||||
class GrandParentComp(Component):
|
||||
js = None
|
||||
|
||||
class ParentComp(GrandParentComp):
|
||||
pass
|
||||
|
||||
class TestComp(ParentComp):
|
||||
js = "console.log('child')"
|
||||
|
||||
assert TestComp.js == "console.log('child')"
|
||||
assert TestComp.js_file is None
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestSubclassingMedia:
|
||||
def test_media_in_child_and_parent(self):
|
||||
|
@ -1060,8 +1200,9 @@ class TestSubclassingMedia:
|
|||
css = "grandparent.css"
|
||||
js = "grandparent.js"
|
||||
|
||||
# `pass` means that we inherit `Media` from `GrandParentComponent`
|
||||
class ParentComponent(GrandParentComponent):
|
||||
Media = None # type: ignore[assignment]
|
||||
pass
|
||||
|
||||
class ChildComponent(ParentComponent):
|
||||
class Media:
|
||||
|
@ -1083,6 +1224,40 @@ class TestSubclassingMedia:
|
|||
'<script src="grandparent.js"></script>'
|
||||
)
|
||||
|
||||
# Check that setting `Media = None` on a child class means that we will NOT inherit `Media` from the parent class
|
||||
def test_media_in_child_and_grandparent__inheritance_off(self):
|
||||
class GrandParentComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component_js_dependencies %}
|
||||
{% component_css_dependencies %}
|
||||
"""
|
||||
|
||||
class Media:
|
||||
css = "grandparent.css"
|
||||
js = "grandparent.js"
|
||||
|
||||
# `None` means that we will NOT inherit `Media` from `GrandParentComponent`
|
||||
class ParentComponent(GrandParentComponent):
|
||||
Media = None # type: ignore[assignment]
|
||||
|
||||
class ChildComponent(ParentComponent):
|
||||
class Media:
|
||||
css = "child.css"
|
||||
js = "child.js"
|
||||
|
||||
rendered = ChildComponent.render()
|
||||
|
||||
assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
|
||||
assertInHTML('<script src="child.js"></script>', rendered)
|
||||
|
||||
assert "grandparent.css" not in rendered
|
||||
assert "grandparent.js" not in rendered
|
||||
|
||||
assert str(ChildComponent.media) == (
|
||||
'<link href="child.css" media="all" rel="stylesheet">\n<script src="child.js"></script>'
|
||||
)
|
||||
|
||||
def test_media_in_parent_and_grandparent(self):
|
||||
class GrandParentComponent(Component):
|
||||
template: types.django_html = """
|
||||
|
@ -1143,8 +1318,9 @@ class TestSubclassingMedia:
|
|||
css = "parent1.css"
|
||||
js = "parent1.js"
|
||||
|
||||
# `pass` means that we inherit `Media` from `GrandParent3Component` and `GrandParent4Component`
|
||||
class Parent2Component(GrandParent3Component, GrandParent4Component):
|
||||
Media = None # type: ignore[assignment]
|
||||
pass
|
||||
|
||||
class ChildComponent(Parent1Component, Parent2Component):
|
||||
template: types.django_html = """
|
||||
|
@ -1180,6 +1356,69 @@ class TestSubclassingMedia:
|
|||
'<script src="grandparent1.js"></script>'
|
||||
)
|
||||
|
||||
# Check that setting `Media = None` on a child class means that we will NOT inherit `Media` from the parent class
|
||||
def test_media_in_multiple_bases__inheritance_off(self):
|
||||
class GrandParent1Component(Component):
|
||||
class Media:
|
||||
css = "grandparent1.css"
|
||||
js = "grandparent1.js"
|
||||
|
||||
class GrandParent2Component(Component):
|
||||
pass
|
||||
|
||||
# NOTE: The bases don't even have to be Component classes,
|
||||
# as long as they have the nested `Media` class.
|
||||
class GrandParent3Component:
|
||||
# NOTE: When we don't subclass `Component`, we have to correctly format the `Media` class
|
||||
class Media:
|
||||
css = {"all": ["grandparent3.css"]}
|
||||
js = ["grandparent3.js"]
|
||||
|
||||
class GrandParent4Component:
|
||||
pass
|
||||
|
||||
class Parent1Component(GrandParent1Component, GrandParent2Component):
|
||||
class Media:
|
||||
css = "parent1.css"
|
||||
js = "parent1.js"
|
||||
|
||||
# `None` means that we will NOT inherit `Media` from `GrandParent3Component` and `GrandParent4Component`
|
||||
class Parent2Component(GrandParent3Component, GrandParent4Component):
|
||||
Media = None # type: ignore[assignment]
|
||||
|
||||
class ChildComponent(Parent1Component, Parent2Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component_js_dependencies %}
|
||||
{% component_css_dependencies %}
|
||||
"""
|
||||
|
||||
class Media:
|
||||
css = "child.css"
|
||||
js = "child.js"
|
||||
|
||||
rendered = ChildComponent.render()
|
||||
|
||||
assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
|
||||
assertInHTML('<link href="parent1.css" media="all" rel="stylesheet">', rendered)
|
||||
assertInHTML('<link href="grandparent1.css" media="all" rel="stylesheet">', rendered)
|
||||
|
||||
assertInHTML('<script src="child.js"></script>', rendered)
|
||||
assertInHTML('<script src="parent1.js"></script>', rendered)
|
||||
assertInHTML('<script src="grandparent1.js"></script>', rendered)
|
||||
|
||||
assert "grandparent3.css" not in rendered
|
||||
assert "grandparent3.js" not in rendered
|
||||
|
||||
assert str(ChildComponent.media) == (
|
||||
'<link href="child.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="parent1.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="grandparent1.css" media="all" rel="stylesheet">\n'
|
||||
'<script src="child.js"></script>\n'
|
||||
'<script src="parent1.js"></script>\n'
|
||||
'<script src="grandparent1.js"></script>'
|
||||
)
|
||||
|
||||
def test_extend_false_in_child(self):
|
||||
class Parent1Component(Component):
|
||||
template: types.django_html = """
|
||||
|
@ -1214,8 +1453,7 @@ class TestSubclassingMedia:
|
|||
assertInHTML('<script src="child.js"></script>', rendered)
|
||||
|
||||
assert str(ChildComponent.media) == (
|
||||
'<link href="child.css" media="all" rel="stylesheet">\n'
|
||||
'<script src="child.js"></script>'
|
||||
'<link href="child.css" media="all" rel="stylesheet">\n<script src="child.js"></script>'
|
||||
)
|
||||
|
||||
def test_extend_false_in_parent(self):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue