mirror of
https://github.com/django-components/django-components.git
synced 2025-08-18 13:10:13 +00:00
feat: add Media.extend (#890)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
5e8770c720
commit
ab037f24b0
7 changed files with 727 additions and 61 deletions
|
@ -22,8 +22,7 @@
|
||||||
|
|
||||||
#### Refactor
|
#### Refactor
|
||||||
|
|
||||||
- The canonical way to define a template file was changed from `template_name` to `template_file`,
|
- The canonical way to define a template file was changed from `template_name` to `template_file`, to align with the rest of the API.
|
||||||
to align with the rest of the API.
|
|
||||||
|
|
||||||
`template_name` remains for backwards compatibility. When you get / set `template_name`,
|
`template_name` remains for backwards compatibility. When you get / set `template_name`,
|
||||||
internally this is proxied to `template_file`.
|
internally this is proxied to `template_file`.
|
||||||
|
@ -42,6 +41,12 @@
|
||||||
|
|
||||||
Read more on [Accessing component's HTML / JS / CSS](https://EmilStenstrom.github.io/django-components/0.124/concepts/fundamentals/defining_js_css_html_files/#customize-how-paths-are-rendered-into-html-tags).
|
Read more on [Accessing component's HTML / JS / CSS](https://EmilStenstrom.github.io/django-components/0.124/concepts/fundamentals/defining_js_css_html_files/#customize-how-paths-are-rendered-into-html-tags).
|
||||||
|
|
||||||
|
- Component inheritance:
|
||||||
|
|
||||||
|
- When you subclass a component, the JS and CSS defined on parent's `Media` class is now inherited by the child component.
|
||||||
|
- You can disable or customize Media inheritance by setting `extend` attribute on the `Component.Media` nested class. This work similarly to Django's [`Media.extend`](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend).
|
||||||
|
- When child component defines either `template` or `template_file`, both of parent's `template` and `template_file` are ignored. The same applies to `js_file` and `css_file`.
|
||||||
|
|
||||||
- The [Signals](https://docs.djangoproject.com/en/5.1/topics/signals/) emitted by or during the use of django-components are now documented, together the `template_rendered` signal.
|
- The [Signals](https://docs.djangoproject.com/en/5.1/topics/signals/) emitted by or during the use of django-components are now documented, together the `template_rendered` signal.
|
||||||
|
|
||||||
## v0.123
|
## v0.123
|
||||||
|
|
|
@ -137,6 +137,9 @@ This `Media` class behaves similarly to
|
||||||
- A [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
|
- A [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
|
||||||
or a function (with `__html__` method) is considered an already-formatted HTML tag, skipping both static file
|
or a function (with `__html__` method) is considered an already-formatted HTML tag, skipping both static file
|
||||||
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
|
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
|
||||||
|
- You can set [`extend`](../../../reference/api#django_components.ComponentMediaInput.extend) to configure
|
||||||
|
whether to inherit JS / CSS from parent components. See
|
||||||
|
[Controlling Media Inheritance](../../fundamentals/defining_js_css_html_files/#controlling-media-inheritance).
|
||||||
|
|
||||||
However, there's a few differences from Django's Media class:
|
However, there's a few differences from Django's Media class:
|
||||||
|
|
||||||
|
@ -145,8 +148,6 @@ However, there's a few differences from Django's Media class:
|
||||||
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
|
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
|
||||||
[`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
|
[`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
|
||||||
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
|
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
|
||||||
3. Our Media class does NOT support
|
|
||||||
[Django's `extend` keyword](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend)
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class MyTable(Component):
|
class MyTable(Component):
|
||||||
|
@ -402,8 +403,8 @@ print(Calendar.css)
|
||||||
|
|
||||||
## Accessing component's Media files
|
## Accessing component's Media files
|
||||||
|
|
||||||
To access the files defined under [`Component.Media`](../../../reference/api#django_components.Component.Media),
|
To access the files that you defined under [`Component.Media`](../../../reference/api#django_components.Component.Media),
|
||||||
you can access [`Component.media`](../../reference/api.md#django_components.Component.media) (lowercase).
|
use [`Component.media`](../../reference/api.md#django_components.Component.media) (lowercase).
|
||||||
This is consistent behavior with
|
This is consistent behavior with
|
||||||
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition).
|
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition).
|
||||||
|
|
||||||
|
@ -419,6 +420,95 @@ print(MyComponent.media)
|
||||||
# <link href="/static/path/to/style.css" media="all" rel="stylesheet">
|
# <link href="/static/path/to/style.css" media="all" rel="stylesheet">
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `Component.Media` vs `Component.media`
|
||||||
|
|
||||||
|
When working with component media files, there are a few important concepts to understand:
|
||||||
|
|
||||||
|
- `Component.Media`
|
||||||
|
|
||||||
|
- Is the "raw" media definition, or the input, which holds only the component's **own** media definition
|
||||||
|
- This class is NOT instantiated, it merely holds the JS / CSS files.
|
||||||
|
|
||||||
|
- `Component.media`
|
||||||
|
- Returns all resolved media files, **including** those inherited from parent components
|
||||||
|
- Is an instance of [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ParentComponent(Component):
|
||||||
|
class Media:
|
||||||
|
js = ["parent.js"]
|
||||||
|
|
||||||
|
class ChildComponent(ParentComponent):
|
||||||
|
class Media:
|
||||||
|
js = ["child.js"]
|
||||||
|
|
||||||
|
# Access only this component's media
|
||||||
|
print(ChildComponent.Media.js) # ["child.js"]
|
||||||
|
|
||||||
|
# Access all inherited media
|
||||||
|
print(ChildComponent.media._js) # ["parent.js", "child.js"]
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
You should **not** manually modify `Component.media` or `Component.Media` after the component has been resolved, as this may lead to unexpected behavior.
|
||||||
|
|
||||||
If you want to modify the class that is instantiated for [`Component.media`](../../reference/api.md#django_components.Component.media),
|
If you want to modify the class that is instantiated for [`Component.media`](../../reference/api.md#django_components.Component.media),
|
||||||
you can configure [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
|
you can configure [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
|
||||||
([See example](#customize-how-paths-are-rendered-into-html-tags)).
|
([See example](#customize-how-paths-are-rendered-into-html-tags)).
|
||||||
|
|
||||||
|
## Controlling Media Inheritance
|
||||||
|
|
||||||
|
By default, the media files are inherited from the parent component.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ParentComponent(Component):
|
||||||
|
class Media:
|
||||||
|
js = ["parent.js"]
|
||||||
|
|
||||||
|
class MyComponent(ParentComponent):
|
||||||
|
class Media:
|
||||||
|
js = ["script.js"]
|
||||||
|
|
||||||
|
print(MyComponent.media._js) # ["parent.js", "script.js"]
|
||||||
|
```
|
||||||
|
|
||||||
|
You can set the component NOT to inherit from the parent component by setting the [`extend`](../../reference/api.md#django_components.ComponentMediaInput.extend) attribute to `False`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ParentComponent(Component):
|
||||||
|
class Media:
|
||||||
|
js = ["parent.js"]
|
||||||
|
|
||||||
|
class MyComponent(ParentComponent):
|
||||||
|
class Media:
|
||||||
|
extend = False # Don't inherit parent media
|
||||||
|
js = ["script.js"]
|
||||||
|
|
||||||
|
print(MyComponent.media._js) # ["script.js"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can specify which components to inherit from. In such case, the media files are inherited ONLY from the specified components, and NOT from the original parent components:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ParentComponent(Component):
|
||||||
|
class Media:
|
||||||
|
js = ["parent.js"]
|
||||||
|
|
||||||
|
class MyComponent(ParentComponent):
|
||||||
|
class Media:
|
||||||
|
# Only inherit from these, ignoring the files from the parent
|
||||||
|
extend = [OtherComponent1, OtherComponent2]
|
||||||
|
|
||||||
|
js = ["script.js"]
|
||||||
|
|
||||||
|
print(MyComponent.media._js) # ["script.js", "other1.js", "other2.js"]
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
The `extend` behaves consistently with
|
||||||
|
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend),
|
||||||
|
with one exception:
|
||||||
|
|
||||||
|
- When you set `extend` to a list, the list is expected to contain Component classes (or other classes that have a nested `Media` class).
|
||||||
|
|
132
docs/concepts/fundamentals/subclassing_components.md
Normal file
132
docs/concepts/fundamentals/subclassing_components.md
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
---
|
||||||
|
title: Subclassing components
|
||||||
|
weight: 11
|
||||||
|
---
|
||||||
|
|
||||||
|
In larger projects, you might need to write multiple components with similar behavior.
|
||||||
|
In such cases, you can extract shared behavior into a standalone component class to keep things
|
||||||
|
[DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).
|
||||||
|
|
||||||
|
When subclassing a component, there's a couple of things to keep in mind:
|
||||||
|
|
||||||
|
### Template, JS, and CSS Inheritance
|
||||||
|
|
||||||
|
When it comes to the pairs:
|
||||||
|
|
||||||
|
- [`Component.template`](../../reference/api.md#django_components.Component.template)/[`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
|
||||||
|
- [`Component.js`](../../reference/api.md#django_components.Component.js)/[`Component.js_file`](../../reference/api.md#django_components.Component.js_file)
|
||||||
|
- [`Component.css`](../../reference/api.md#django_components.Component.css)/[`Component.css_file`](../../reference/api.md#django_components.Component.css_file)
|
||||||
|
|
||||||
|
inheritance follows these rules:
|
||||||
|
|
||||||
|
- If a child component class defines either member of a pair (e.g., either [`template`](../../reference/api.md#django_components.Component.template) or [`template_file`](../../reference/api.md#django_components.Component.template_file)), it takes precedence and the parent's definition is ignored completely.
|
||||||
|
- For example, if a child component defines [`template_file`](../../reference/api.md#django_components.Component.template_file), the parent's [`template`](../../reference/api.md#django_components.Component.template) or [`template_file`](../../reference/api.md#django_components.Component.template_file) will be ignored.
|
||||||
|
- This applies independently to each pair - you can inherit the JS while overriding the template, for instance.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BaseCard(Component):
|
||||||
|
template = """
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">{{ content }}</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
css = """
|
||||||
|
.card {
|
||||||
|
border: 1px solid gray;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
js = "console.log('Base card loaded');"
|
||||||
|
|
||||||
|
# This class overrides parent's template, but inherits CSS and JS
|
||||||
|
class SpecialCard(BaseCard):
|
||||||
|
template = """
|
||||||
|
<div class="card special">
|
||||||
|
<div class="card-content">✨ {{ content }} ✨</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This class overrides parent's template and CSS, but inherits JS
|
||||||
|
class CustomCard(BaseCard):
|
||||||
|
template_file = "custom_card.html"
|
||||||
|
css = """
|
||||||
|
.card {
|
||||||
|
border: 2px solid gold;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Media Class Inheritance
|
||||||
|
|
||||||
|
The [`Component.Media`](../../reference/api.md#django_components.Component.Media) nested class follows Django's media inheritance rules:
|
||||||
|
|
||||||
|
- If both parent and child define a `Media` class, the child's media will automatically include both its own and the parent's JS and CSS files.
|
||||||
|
- This behavior can be configured using the [`extend`](../../reference/api.md#django_components.Component.Media.extend) attribute in the Media class, similar to Django's forms.
|
||||||
|
Read more on this in [Controlling Media Inheritance](./defining_js_css_html_files.md#controlling-media-inheritance).
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BaseModal(Component):
|
||||||
|
template = "<div>Modal content</div>"
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = ["base_modal.css"]
|
||||||
|
js = ["base_modal.js"] # Contains core modal functionality
|
||||||
|
|
||||||
|
class FancyModal(BaseModal):
|
||||||
|
class Media:
|
||||||
|
# Will include both base_modal.css/js AND fancy_modal.css/js
|
||||||
|
css = ["fancy_modal.css"] # Additional styling
|
||||||
|
js = ["fancy_modal.js"] # Additional animations
|
||||||
|
|
||||||
|
class SimpleModal(BaseModal):
|
||||||
|
class Media:
|
||||||
|
extend = False # Don't inherit parent's media
|
||||||
|
css = ["simple_modal.css"] # Only this CSS will be included
|
||||||
|
js = ["simple_modal.js"] # Only this JS will be included
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BaseForm(Component):
|
||||||
|
template = """
|
||||||
|
<form>
|
||||||
|
{{ form_content }}
|
||||||
|
<button type="submit">
|
||||||
|
{{ submit_text }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return {
|
||||||
|
"form_content": self.get_form_content(),
|
||||||
|
"submit_text": "Submit"
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_form_content(self):
|
||||||
|
return "<input type='text' name='data'>"
|
||||||
|
|
||||||
|
class ContactForm(BaseForm):
|
||||||
|
# Extend parent's "context"
|
||||||
|
# but override "submit_text"
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["submit_text"] = "Send Message"
|
||||||
|
return context
|
||||||
|
|
||||||
|
# Completely override parent's get_form_content
|
||||||
|
def get_form_content(self):
|
||||||
|
return """
|
||||||
|
<input type='text' name='name' placeholder='Your Name'>
|
||||||
|
<input type='email' name='email' placeholder='Your Email'>
|
||||||
|
<textarea name='message' placeholder='Your Message'></textarea>
|
||||||
|
"""
|
||||||
|
```
|
|
@ -235,7 +235,7 @@ with a few differences:
|
||||||
|
|
||||||
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list, or (CSS-only) a dictonary (see below).
|
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list, or (CSS-only) a dictonary (see below).
|
||||||
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`, [`SafeString`](https://dev.to/doridoro/django-safestring-afj), or a function.
|
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`, [`SafeString`](https://dev.to/doridoro/django-safestring-afj), or a function.
|
||||||
3. Our Media class does NOT support [Django's `extend` keyword](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend).
|
3. If you set `Media.extend` to a list, it should be a list of `Component` classes.
|
||||||
|
|
||||||
[Learn more](../fundamentals/defining_js_css_html_files.md) about using Media.
|
[Learn more](../fundamentals/defining_js_css_html_files.md) about using Media.
|
||||||
|
|
||||||
|
|
|
@ -490,6 +490,9 @@ class Component(
|
||||||
This path is still rendered to HTML with `media_class.render_js()` or `media_class.render_css()`.
|
This path is still rendered to HTML with `media_class.render_js()` or `media_class.render_css()`.
|
||||||
- A `SafeString` (with `__html__` method) is considered an already-formatted HTML tag, skipping both static file
|
- A `SafeString` (with `__html__` method) is considered an already-formatted HTML tag, skipping both static file
|
||||||
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
|
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
|
||||||
|
- You can set [`extend`](../api#django_components.ComponentMediaInput.extend) to configure
|
||||||
|
whether to inherit JS / CSS from parent components. See
|
||||||
|
[Controlling Media Inheritance](../../concepts/fundamentals/defining_js_css_html_files/#controlling-media-inheritance).
|
||||||
|
|
||||||
However, there's a few differences from Django's Media class:
|
However, there's a few differences from Django's Media class:
|
||||||
|
|
||||||
|
@ -498,8 +501,6 @@ class Component(
|
||||||
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
|
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
|
||||||
[`SafeString`](https://dev.to/doridoro/django-safestring-afj), or a function
|
[`SafeString`](https://dev.to/doridoro/django-safestring-afj), or a function
|
||||||
(See [`ComponentMediaInputPath`](../api#django_components.ComponentMediaInputPath)).
|
(See [`ComponentMediaInputPath`](../api#django_components.ComponentMediaInputPath)).
|
||||||
3. Our Media class does NOT support
|
|
||||||
[Django's `extend` keyword](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend)
|
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
|
@ -518,7 +519,7 @@ class Component(
|
||||||
"print": ["path/to/style2.css"],
|
"print": ["path/to/style2.css"],
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
"""
|
""" # noqa: E501
|
||||||
|
|
||||||
response_class = HttpResponse
|
response_class = HttpResponse
|
||||||
"""This allows to configure what class is used to generate response from `render_to_response`"""
|
"""This allows to configure what class is used to generate response from `render_to_response`"""
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from collections import deque
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Protocol, Tuple, Type, Union, cast
|
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Protocol, Tuple, Type, Union, cast
|
||||||
|
@ -170,13 +171,57 @@ class ComponentMediaInput(Protocol):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
extend: Union[bool, List[Type["Component"]]] = True
|
||||||
|
"""
|
||||||
|
Configures whether the component should inherit the media files from the parent component.
|
||||||
|
|
||||||
|
- If `True`, the component inherits the media files from the parent component.
|
||||||
|
- If `False`, the component does not inherit the media files from the parent component.
|
||||||
|
- If a list of components classes, the component inherits the media files ONLY from these specified components.
|
||||||
|
|
||||||
|
Read more in [Controlling Media Inheritance](../concepts/fundamentals/defining_js_css_html_files.md#controlling-media-inheritance) section.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
Disable media inheritance:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ParentComponent(Component):
|
||||||
|
class Media:
|
||||||
|
js = ["parent.js"]
|
||||||
|
|
||||||
|
class MyComponent(ParentComponent):
|
||||||
|
class Media:
|
||||||
|
extend = False # Don't inherit parent media
|
||||||
|
js = ["script.js"]
|
||||||
|
|
||||||
|
print(MyComponent.media._js) # ["script.js"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Specify which components to inherit from. In this case, the media files are inherited ONLY
|
||||||
|
from the specified components, and NOT from the original parent components:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ParentComponent(Component):
|
||||||
|
class Media:
|
||||||
|
js = ["parent.js"]
|
||||||
|
|
||||||
|
class MyComponent(ParentComponent):
|
||||||
|
class Media:
|
||||||
|
# Only inherit from these, ignoring the files from the parent
|
||||||
|
extend = [OtherComponent1, OtherComponent2]
|
||||||
|
|
||||||
|
js = ["script.js"]
|
||||||
|
|
||||||
|
print(MyComponent.media._js) # ["script.js", "other1.js", "other2.js"]
|
||||||
|
```
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ComponentMedia:
|
class ComponentMedia:
|
||||||
resolved: bool = False
|
resolved: bool = False
|
||||||
Media: Optional[Type[ComponentMediaInput]] = None
|
Media: Optional[Type[ComponentMediaInput]] = None
|
||||||
media: Optional[MediaCls] = None
|
|
||||||
media_class: Type[MediaCls] = MediaCls
|
|
||||||
template: Optional[str] = None
|
template: Optional[str] = None
|
||||||
template_file: Optional[str] = None
|
template_file: Optional[str] = None
|
||||||
js: Optional[str] = None
|
js: Optional[str] = None
|
||||||
|
@ -260,8 +305,6 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
|
||||||
# NOTE: We take the values from `attrs` so we consider only the values that were set on THIS class,
|
# 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.
|
# and not the values that were inherited from the parent classes.
|
||||||
Media=attrs.get("Media", None),
|
Media=attrs.get("Media", None),
|
||||||
media=attrs.get("media", None),
|
|
||||||
media_class=attrs.get("media_class", None),
|
|
||||||
template=attrs.get("template", None),
|
template=attrs.get("template", None),
|
||||||
template_file=attrs.get("template_file", None),
|
template_file=attrs.get("template_file", None),
|
||||||
js=attrs.get("js", None),
|
js=attrs.get("js", None),
|
||||||
|
@ -270,11 +313,34 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
|
||||||
css_file=attrs.get("css_file", None),
|
css_file=attrs.get("css_file", None),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_comp_media_attr(attr: str) -> Any:
|
||||||
|
if attr == "media":
|
||||||
|
return _get_comp_cls_media(comp_cls)
|
||||||
|
else:
|
||||||
|
return _get_comp_cls_attr(comp_cls, attr)
|
||||||
|
|
||||||
|
# Because of the lazy resolution, we want to know when the user tries to access the media attributes.
|
||||||
|
# And because these fields are class attributes, we can't use `@property` decorator.
|
||||||
|
#
|
||||||
|
# Instead, we define a descriptor for each of the media attributes, and set it on the class.
|
||||||
|
# Read more on descriptors https://docs.python.org/3/howto/descriptor.html
|
||||||
|
class InterceptDescriptor:
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
self._attr_name = name
|
||||||
|
|
||||||
|
# `__get__` runs when a class/instance attribute is being accessed
|
||||||
|
def __get__(self, instance: Optional["Component"], cls: Type["Component"]) -> Any:
|
||||||
|
return get_comp_media_attr(self._attr_name)
|
||||||
|
|
||||||
|
for attr in COMP_MEDIA_LAZY_ATTRS:
|
||||||
|
setattr(comp_cls, attr, InterceptDescriptor(attr))
|
||||||
|
|
||||||
|
|
||||||
# Because the media values are not defined directly on the instance, but held in `_component_media`,
|
# Because the media values are not defined directly on the instance, but held in `_component_media`,
|
||||||
# then simply accessing `_component_media.js` will NOT get the values from parent classes.
|
# then simply accessing `_component_media.js` will NOT get the values from parent classes.
|
||||||
#
|
#
|
||||||
# So this function is like `getattr`, but for searching for values inside `_component_media`.
|
# So this function is like `getattr`, but for searching for values inside `_component_media`.
|
||||||
def get_comp_media_attr(attr: str) -> Any:
|
def _get_comp_cls_attr(comp_cls: Type["Component"], attr: str) -> Any:
|
||||||
for base in comp_cls.mro():
|
for base in comp_cls.mro():
|
||||||
comp_media: Optional[ComponentMedia] = getattr(base, "_component_media", None)
|
comp_media: Optional[ComponentMedia] = getattr(base, "_component_media", None)
|
||||||
if comp_media is None:
|
if comp_media is None:
|
||||||
|
@ -312,21 +378,90 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
|
||||||
return value
|
return value
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Because of the lazy resolution, we want to know when the user tries to access the media attributes.
|
|
||||||
# And because these fields are class attributes, we can't use `@property` decorator.
|
media_cache: Dict[Type["Component"], MediaCls] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_comp_cls_media(comp_cls: Type["Component"]) -> Any:
|
||||||
|
# Component's `media` attribute is a special case, because it should inherit all the JS/CSS files
|
||||||
|
# from the parent classes. So we need to walk up the MRO and merge all the Media classes.
|
||||||
#
|
#
|
||||||
# Instead, we define a descriptor for each of the media attributes, and set it on the class.
|
# But before we do that, we need to ensure that all parent classes have resolved their `media` attributes.
|
||||||
# Read more on descriptors https://docs.python.org/3/howto/descriptor.html
|
# Because to be able to construct `media` for a Component class, all its parent classes must have resolved
|
||||||
class InterceptDescriptor:
|
# their `media`.
|
||||||
def __init__(self, name: str) -> None:
|
#
|
||||||
self._attr_name = name
|
# So we will:
|
||||||
|
# 0. Cache the resolved media, so we don't have to resolve it again, and so we can store it even for classes
|
||||||
|
# that don't have `Media` attribute.
|
||||||
|
# 1. If the current class HAS `media` in the cache, we used that
|
||||||
|
# 2. Otherwise, we check if its parent bases have `media` in the cache,
|
||||||
|
# 3. If ALL parent bases have `media` in the cache, we can resolve the child class's `media`,
|
||||||
|
# and put it in the cache.
|
||||||
|
# 4. If ANY of the parent bases DOESN'T, then we add those parent bases to the stack (so they are processed
|
||||||
|
# right after this. And we add the child class right after that.
|
||||||
|
#
|
||||||
|
# E.g. `stack = [*cls.__bases__, cls, *stack]`
|
||||||
|
#
|
||||||
|
# That way, we go up one level of the bases, and then we eventually come back down to the
|
||||||
|
# class that we tried to resolve. But the second time, we will have `media` resolved for all its parent bases.
|
||||||
|
bases_stack = deque([comp_cls])
|
||||||
|
while bases_stack:
|
||||||
|
curr_cls = bases_stack.popleft()
|
||||||
|
|
||||||
# `__get__` runs when a class/instance attribute is being accessed
|
if curr_cls in media_cache:
|
||||||
def __get__(self, instance: Optional["Component"], cls: Type["Component"]) -> Any:
|
continue
|
||||||
return get_comp_media_attr(self._attr_name)
|
|
||||||
|
|
||||||
for attr in COMP_MEDIA_LAZY_ATTRS:
|
# Prepare base classes
|
||||||
setattr(comp_cls, attr, InterceptDescriptor(attr))
|
media_input = getattr(curr_cls, "Media", None)
|
||||||
|
media_extend = getattr(media_input, "extend", True)
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# - If `Media.extend == False`, then the media files are NOT inherited from the parent classes.
|
||||||
|
# - If `Media.extend == [Component1, Component2, ...]`, then the media files are inherited only
|
||||||
|
# from the specified classes.
|
||||||
|
if media_extend is True:
|
||||||
|
bases = curr_cls.__bases__
|
||||||
|
elif media_extend is False:
|
||||||
|
bases = tuple()
|
||||||
|
else:
|
||||||
|
bases = media_extend
|
||||||
|
|
||||||
|
unresolved_bases = [base for base in bases if base not in media_cache]
|
||||||
|
if unresolved_bases:
|
||||||
|
# Put the current class's bases at the FRONT of the queue, and put the current class back right after that.
|
||||||
|
# E.g. `[parentCls1, parentCls2, currCls, ...]`
|
||||||
|
# That way, we first resolve the parent classes, and then the current class.
|
||||||
|
bases_stack.extendleft(reversed([*unresolved_bases, curr_cls]))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Now, if we got here, then either all the bases of the current class have had their `media` resolved,
|
||||||
|
# or the current class has NO bases. So now we construct the `media` for the current class.
|
||||||
|
media_cls = getattr(curr_cls, "media_class", MediaCls)
|
||||||
|
# NOTE: If the class is a component and and it was not yet resolved, accessing `Media` should resolve it.
|
||||||
|
media_js = getattr(media_input, "js", [])
|
||||||
|
media_css = getattr(media_input, "css", {})
|
||||||
|
media: MediaCls = media_cls(js=media_js, css=media_css)
|
||||||
|
|
||||||
|
# We have the current class's `media`, now we add the JS and CSS from the parent classes.
|
||||||
|
# NOTE: Django's implementation of `Media` should ensure that duplicate files are not added.
|
||||||
|
for base in bases:
|
||||||
|
base_media = media_cache.get(base, None)
|
||||||
|
if base_media is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add JS / CSS from the base class's Media to the current class's Media.
|
||||||
|
# We make use of the fact that Django's Media class does this with `__add__` method.
|
||||||
|
#
|
||||||
|
# However, the `__add__` converts our `media_cls` to Django's Media class.
|
||||||
|
# So we also have to convert it back to `media_cls`.
|
||||||
|
merged_media = media + base_media
|
||||||
|
media = media_cls(js=merged_media._js, css=merged_media._css)
|
||||||
|
|
||||||
|
# Lastly, cache the merged-up Media, so we don't have to search further up the MRO the next time
|
||||||
|
media_cache[curr_cls] = media
|
||||||
|
|
||||||
|
return media_cache[comp_cls]
|
||||||
|
|
||||||
|
|
||||||
def _resolve_media(comp_cls: Type["Component"], comp_media: ComponentMedia) -> None:
|
def _resolve_media(comp_cls: Type["Component"], comp_media: ComponentMedia) -> None:
|
||||||
|
@ -399,11 +534,6 @@ def _resolve_media(comp_cls: Type["Component"], comp_media: ComponentMedia) -> N
|
||||||
comp_cls, comp_media, inlined_attr="css", file_attr="css_file", comp_dirs=comp_dirs, type="static"
|
comp_cls, comp_media, inlined_attr="css", file_attr="css_file", comp_dirs=comp_dirs, type="static"
|
||||||
)
|
)
|
||||||
|
|
||||||
media_cls = comp_media.media_class or MediaCls
|
|
||||||
media_js = getattr(comp_media.Media, "js", [])
|
|
||||||
media_css = getattr(comp_media.Media, "css", {})
|
|
||||||
comp_media.media = media_cls(js=media_js, css=media_css) if comp_media.Media is not None else None
|
|
||||||
|
|
||||||
comp_media.resolved = True
|
comp_media.resolved = True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -993,3 +993,311 @@ class MediaRelativePathTests(BaseTestCase):
|
||||||
self.assertInHTML('<link href="relative_file_pathobj.css" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="relative_file_pathobj.css" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
self.assertInHTML('<script type="module" src="relative_file_pathobj.js"></script>', rendered)
|
self.assertInHTML('<script type="module" src="relative_file_pathobj.js"></script>', rendered)
|
||||||
|
|
||||||
|
|
||||||
|
class SubclassingMediaTests(BaseTestCase):
|
||||||
|
def test_media_in_child_and_parent(self):
|
||||||
|
class ParentComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_js_dependencies %}
|
||||||
|
{% component_css_dependencies %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = "parent.css"
|
||||||
|
js = "parent.js"
|
||||||
|
|
||||||
|
class ChildComponent(ParentComponent):
|
||||||
|
class Media:
|
||||||
|
css = "child.css"
|
||||||
|
js = "child.js"
|
||||||
|
|
||||||
|
rendered = ChildComponent.render()
|
||||||
|
|
||||||
|
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<link href="parent.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
|
self.assertInHTML('<script src="child.js"></script>', rendered)
|
||||||
|
self.assertInHTML('<script src="parent.js"></script>', rendered)
|
||||||
|
|
||||||
|
def test_media_in_child_and_grandparent(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"
|
||||||
|
|
||||||
|
class ParentComponent(GrandParentComponent):
|
||||||
|
Media = None
|
||||||
|
|
||||||
|
class ChildComponent(ParentComponent):
|
||||||
|
class Media:
|
||||||
|
css = "child.css"
|
||||||
|
js = "child.js"
|
||||||
|
|
||||||
|
rendered = ChildComponent.render()
|
||||||
|
|
||||||
|
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<link href="grandparent.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
|
self.assertInHTML('<script src="child.js"></script>', rendered)
|
||||||
|
self.assertInHTML('<script src="grandparent.js"></script>', rendered)
|
||||||
|
|
||||||
|
def test_media_in_parent_and_grandparent(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"
|
||||||
|
|
||||||
|
class ParentComponent(GrandParentComponent):
|
||||||
|
class Media:
|
||||||
|
css = "parent.css"
|
||||||
|
js = "parent.js"
|
||||||
|
|
||||||
|
class ChildComponent(ParentComponent):
|
||||||
|
pass
|
||||||
|
|
||||||
|
rendered = ChildComponent.render()
|
||||||
|
|
||||||
|
self.assertInHTML('<link href="parent.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<link href="grandparent.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
|
self.assertInHTML('<script src="parent.js"></script>', rendered)
|
||||||
|
self.assertInHTML('<script src="grandparent.js"></script>', rendered)
|
||||||
|
|
||||||
|
def test_media_in_multiple_bases(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"
|
||||||
|
|
||||||
|
class Parent2Component(GrandParent3Component, GrandParent4Component):
|
||||||
|
Media = None
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<link href="parent1.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<link href="grandparent1.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<link href="grandparent3.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
|
self.assertInHTML('<script src="child.js"></script>', rendered)
|
||||||
|
self.assertInHTML('<script src="parent1.js"></script>', rendered)
|
||||||
|
self.assertInHTML('<script src="grandparent1.js"></script>', rendered)
|
||||||
|
self.assertInHTML('<script src="grandparent3.js"></script>', rendered)
|
||||||
|
|
||||||
|
def test_extend_false_in_child(self):
|
||||||
|
class Parent1Component(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_js_dependencies %}
|
||||||
|
{% component_css_dependencies %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = "parent1.css"
|
||||||
|
js = "parent1.js"
|
||||||
|
|
||||||
|
class Parent2Component(Component):
|
||||||
|
class Media:
|
||||||
|
css = "parent2.css"
|
||||||
|
js = "parent2.js"
|
||||||
|
|
||||||
|
class ChildComponent(Parent1Component, Parent2Component):
|
||||||
|
class Media:
|
||||||
|
extend = False
|
||||||
|
css = "child.css"
|
||||||
|
js = "child.js"
|
||||||
|
|
||||||
|
rendered = ChildComponent.render()
|
||||||
|
|
||||||
|
self.assertNotIn("parent1.css", rendered)
|
||||||
|
self.assertNotIn("parent2.css", rendered)
|
||||||
|
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
|
self.assertNotIn("parent1.js", rendered)
|
||||||
|
self.assertNotIn("parent2.js", rendered)
|
||||||
|
self.assertInHTML('<script src="child.js"></script>', rendered)
|
||||||
|
|
||||||
|
def test_extend_false_in_parent(self):
|
||||||
|
class GrandParentComponent(Component):
|
||||||
|
class Media:
|
||||||
|
css = "grandparent.css"
|
||||||
|
js = "grandparent.js"
|
||||||
|
|
||||||
|
class Parent1Component(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_js_dependencies %}
|
||||||
|
{% component_css_dependencies %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = "parent1.css"
|
||||||
|
js = "parent1.js"
|
||||||
|
|
||||||
|
class Parent2Component(GrandParentComponent):
|
||||||
|
class Media:
|
||||||
|
extend = False
|
||||||
|
css = "parent2.css"
|
||||||
|
js = "parent2.js"
|
||||||
|
|
||||||
|
class ChildComponent(Parent1Component, Parent2Component):
|
||||||
|
class Media:
|
||||||
|
css = "child.css"
|
||||||
|
js = "child.js"
|
||||||
|
|
||||||
|
rendered = ChildComponent.render()
|
||||||
|
|
||||||
|
self.assertNotIn("grandparent.css", rendered)
|
||||||
|
self.assertInHTML('<link href="parent1.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<link href="parent2.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
|
self.assertNotIn("grandparent.js", rendered)
|
||||||
|
self.assertInHTML('<script src="parent1.js"></script>', rendered)
|
||||||
|
self.assertInHTML('<script src="parent2.js"></script>', rendered)
|
||||||
|
self.assertInHTML('<script src="child.js"></script>', rendered)
|
||||||
|
|
||||||
|
def test_extend_list_in_child(self):
|
||||||
|
class Parent1Component(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_js_dependencies %}
|
||||||
|
{% component_css_dependencies %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = "parent1.css"
|
||||||
|
js = "parent1.js"
|
||||||
|
|
||||||
|
class Parent2Component(Component):
|
||||||
|
class Media:
|
||||||
|
css = "parent2.css"
|
||||||
|
js = "parent2.js"
|
||||||
|
|
||||||
|
class Other1Component(Component):
|
||||||
|
class Media:
|
||||||
|
css = "other1.css"
|
||||||
|
js = "other1.js"
|
||||||
|
|
||||||
|
class Other2Component:
|
||||||
|
class Media:
|
||||||
|
css = {"all": ["other2.css"]}
|
||||||
|
js = ["other2.js"]
|
||||||
|
|
||||||
|
class ChildComponent(Parent1Component, Parent2Component):
|
||||||
|
class Media:
|
||||||
|
extend = [Other1Component, Other2Component]
|
||||||
|
css = "child.css"
|
||||||
|
js = "child.js"
|
||||||
|
|
||||||
|
rendered = ChildComponent.render()
|
||||||
|
|
||||||
|
self.assertNotIn("parent1.css", rendered)
|
||||||
|
self.assertNotIn("parent2.css", rendered)
|
||||||
|
self.assertInHTML('<link href="other1.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<link href="other2.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
|
self.assertNotIn("parent1.js", rendered)
|
||||||
|
self.assertNotIn("parent2.js", rendered)
|
||||||
|
self.assertInHTML('<script src="other1.js"></script>', rendered)
|
||||||
|
self.assertInHTML('<script src="other2.js"></script>', rendered)
|
||||||
|
self.assertInHTML('<script src="child.js"></script>', rendered)
|
||||||
|
|
||||||
|
def test_extend_list_in_parent(self):
|
||||||
|
class Other1Component(Component):
|
||||||
|
class Media:
|
||||||
|
css = "other1.css"
|
||||||
|
js = "other1.js"
|
||||||
|
|
||||||
|
class Other2Component:
|
||||||
|
class Media:
|
||||||
|
css = {"all": ["other2.css"]}
|
||||||
|
js = ["other2.js"]
|
||||||
|
|
||||||
|
class GrandParentComponent(Component):
|
||||||
|
class Media:
|
||||||
|
css = "grandparent.css"
|
||||||
|
js = "grandparent.js"
|
||||||
|
|
||||||
|
class Parent1Component(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_js_dependencies %}
|
||||||
|
{% component_css_dependencies %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = "parent1.css"
|
||||||
|
js = "parent1.js"
|
||||||
|
|
||||||
|
class Parent2Component(GrandParentComponent):
|
||||||
|
class Media:
|
||||||
|
extend = [Other1Component, Other2Component]
|
||||||
|
css = "parent2.css"
|
||||||
|
js = "parent2.js"
|
||||||
|
|
||||||
|
class ChildComponent(Parent1Component, Parent2Component):
|
||||||
|
class Media:
|
||||||
|
css = "child.css"
|
||||||
|
js = "child.js"
|
||||||
|
|
||||||
|
rendered = ChildComponent.render()
|
||||||
|
|
||||||
|
self.assertNotIn("grandparent.css", rendered)
|
||||||
|
self.assertInHTML('<link href="other1.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<link href="other2.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<link href="parent1.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<link href="parent2.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
|
self.assertNotIn("grandparent.js", rendered)
|
||||||
|
self.assertInHTML('<script src="other1.js"></script>', rendered)
|
||||||
|
self.assertInHTML('<script src="other2.js"></script>', rendered)
|
||||||
|
self.assertInHTML('<script src="parent1.js"></script>', rendered)
|
||||||
|
self.assertInHTML('<script src="parent2.js"></script>', rendered)
|
||||||
|
self.assertInHTML('<script src="child.js"></script>', rendered)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue