mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34:59 +00:00
chore: util to manage URLs in the codebase (#1179)
* chore: util to manage URLs in the codebase * docs: mentiion validate_links and supported_versions in docs * refactor: fix linter errors
This commit is contained in:
parent
5f4fbe76e5
commit
ccf02fa316
67 changed files with 678 additions and 309 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -1,3 +1,6 @@
|
|||
# Project-specific files
|
||||
sampleproject/staticfiles/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
@ -43,6 +46,7 @@ htmlcov/
|
|||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
@ -76,7 +80,11 @@ poetry.lock
|
|||
site
|
||||
.direnv/
|
||||
.envrc
|
||||
.mypy_cache/
|
||||
|
||||
# JS, NPM Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Cursor
|
||||
.cursorrules
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -718,7 +718,7 @@ where each class name or style property can be managed separately.
|
|||
- If using `pytest`, the decorator allows you to parametrize Django or Components settings.
|
||||
- The decorator also serves as a stand-in for Django's `@override_settings`.
|
||||
|
||||
See the API reference for [`@djc_test`](https://django-components.github.io/django-components/0.131/reference/testing_api/#djc_test) for more details.
|
||||
See the API reference for [`@djc_test`](https://django-components.github.io/django-components/0.131/reference/testing_api/#django_components.testing.djc_test) for more details.
|
||||
|
||||
- `ComponentRegistry` now has a `has()` method to check if a component is registered
|
||||
without raising an error.
|
||||
|
@ -920,12 +920,12 @@ If you see any broken links or other issues, please report them in [#922](https:
|
|||
- 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).
|
||||
- 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.2/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`.
|
||||
|
||||
- Autodiscovery now ignores files and directories that start with an underscore (`_`), except `__init__.py`
|
||||
|
||||
- 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.2/topics/signals/) emitted by or during the use of django-components are now documented, together the `template_rendered` signal.
|
||||
|
||||
## v0.123
|
||||
|
||||
|
@ -937,7 +937,7 @@ If you see any broken links or other issues, please report them in [#922](https:
|
|||
|
||||
#### Feat
|
||||
|
||||
- Add support for HTML fragments. HTML fragments can be rendered by passing `type="fragment"` to `Component.render()` or `Component.render_to_response()`. Read more on how to [use HTML fragments with HTMX, AlpineJS, or vanillaJS](https://django-components.github.io/django-components/latest/concepts/advanced/html_tragments).
|
||||
- Add support for HTML fragments. HTML fragments can be rendered by passing `type="fragment"` to `Component.render()` or `Component.render_to_response()`. Read more on how to [use HTML fragments with HTMX, AlpineJS, or vanillaJS](https://django-components.github.io/django-components/latest/concepts/advanced/html_fragments).
|
||||
|
||||
## v0.121
|
||||
|
||||
|
@ -1555,7 +1555,7 @@ importing them.
|
|||
|
||||
- `SETTINGS_MODULE` - Define component dirs using `STATICFILES_DIRS`
|
||||
|
||||
- Previously, autodiscovery handled relative files in `STATICFILES_DIRS`. To align with Django, `STATICFILES_DIRS` now must be full paths ([Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS)).
|
||||
- Previously, autodiscovery handled relative files in `STATICFILES_DIRS`. To align with Django, `STATICFILES_DIRS` now must be full paths ([Django docs](https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS)).
|
||||
|
||||
## 🚨📢 v0.81
|
||||
|
||||
|
|
|
@ -121,7 +121,7 @@ class Calendar(Component):
|
|||
|
||||
# Additional JS and CSS
|
||||
class Media:
|
||||
js = ["https://cdn.jsdelivr.net/npm/htmx.org@2.1.1/dist/htmx.min.js"]
|
||||
js = ["https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"]
|
||||
css = ["bootstrap/dist/css/bootstrap.min.css"]
|
||||
|
||||
# Variables available in the template
|
||||
|
|
|
@ -26,7 +26,7 @@ django-components uses `asv` for these use cases:
|
|||
1. When a git tag is created and pushed, we also update the documentation website (see `docs.yml`).
|
||||
2. Before we publish the docs website, we generate the HTML report for the benchmark results.
|
||||
3. The generated report is placed in the `docs/benchmarks/` directory, and is thus
|
||||
published with the rest of the docs website and available under [`/benchmarks/`](https://django-components.github.io/django-components/benchmarks).
|
||||
published with the rest of the docs website and available under [`/benchmarks/`](https://django-components.github.io/django-components/latest/benchmarks).
|
||||
- NOTE: The location where the report is placed is defined in `asv.conf.json`.
|
||||
|
||||
- Compare performance between commits on pull requests:
|
||||
|
|
|
@ -53,8 +53,8 @@ This has two modes:
|
|||
you can access are a union of:
|
||||
|
||||
- All the variables that were OUTSIDE the fill tag, including any\
|
||||
[`{% with %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#with) tags.
|
||||
- Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#cycle))
|
||||
[`{% with %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#with) tags.
|
||||
- Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#cycle))
|
||||
that the `{% fill %}` tag is part of.
|
||||
- Data returned from [`Component.get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
|
||||
of the component that owns the fill tag.
|
||||
|
@ -67,7 +67,7 @@ This has two modes:
|
|||
|
||||
Inside the [`{% fill %}`](../../../reference/template_tags#fill) tag, you can ONLY access variables from 2 places:
|
||||
|
||||
- Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#cycle))
|
||||
- Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#cycle))
|
||||
that the `{% fill %}` tag is part of.
|
||||
- [`Component.get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
|
||||
of the component which defined the template (AKA the "root" component).
|
||||
|
@ -177,5 +177,5 @@ But since `"cheese"` is not defined there, it's empty.
|
|||
|
||||
!!! info
|
||||
|
||||
Notice that the variables defined with the [`{% with %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#with)
|
||||
Notice that the variables defined with the [`{% with %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#with)
|
||||
tag are ignored inside the [`{% fill %}`](../../../reference/template_tags#fill) tag with the `"isolated"` mode.
|
||||
|
|
|
@ -23,13 +23,13 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
|||
|-- mytags.py
|
||||
```
|
||||
|
||||
2. Create custom [`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#how-to-create-custom-template-tags-and-filters)
|
||||
2. Create custom [`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#how-to-create-custom-template-tags-and-filters)
|
||||
and [`ComponentRegistry`](django_components.component_registry.ComponentRegistry) instances in `mytags.py`
|
||||
|
||||
This will be the entrypoint for using the components inside Django templates.
|
||||
|
||||
Remember that Django requires the [`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#how-to-create-custom-template-tags-and-filters)
|
||||
instance to be accessible under the `register` variable ([See Django docs](https://docs.djangoproject.com/en/dev/howto/custom-template-tags)):
|
||||
Remember that Django requires the [`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#how-to-create-custom-template-tags-and-filters)
|
||||
instance to be accessible under the `register` variable ([See Django docs](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags)):
|
||||
|
||||
```py
|
||||
from django.template import Library
|
||||
|
@ -148,7 +148,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
|||
|
||||
Since you, as the library author, are not in control of the file system, it is recommended to load the components manually.
|
||||
|
||||
We recommend doing this in the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready)
|
||||
We recommend doing this in the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready)
|
||||
hook of your `apps.py`:
|
||||
|
||||
```py
|
||||
|
@ -170,7 +170,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
|||
```
|
||||
|
||||
Note that you can also include any other startup logic within
|
||||
[`AppConfig.ready()`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready).
|
||||
[`AppConfig.ready()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready).
|
||||
|
||||
And that's it! The next step is to publish it.
|
||||
|
||||
|
@ -185,7 +185,7 @@ django_components uses the [`build`](https://build.pypa.io/en/stable/) utility t
|
|||
python -m build --sdist --wheel --outdir dist/ .
|
||||
```
|
||||
|
||||
And to publish to PyPI, you can use [`twine`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready)
|
||||
And to publish to PyPI, you can use [`twine`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready)
|
||||
([See Python user guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/#uploading-the-distribution-archives))
|
||||
|
||||
```bash
|
||||
|
@ -219,7 +219,7 @@ After the package has been published, all that remains is to install it in other
|
|||
]
|
||||
```
|
||||
|
||||
3. Optionally add the template tags to the [`builtins`](https://docs.djangoproject.com/en/5.1/topics/templates/#django.template.backends.django.DjangoTemplates),
|
||||
3. Optionally add the template tags to the [`builtins`](https://docs.djangoproject.com/en/5.2/topics/templates/#django.template.backends.django.DjangoTemplates),
|
||||
so you don't have to call `{% load mytags %}` in every template:
|
||||
|
||||
```python
|
||||
|
|
|
@ -624,7 +624,7 @@ The help message prints out all the arguments and options available for the comm
|
|||
|
||||
### Testing Commands
|
||||
|
||||
Commands can be tested using Django's [`call_command()`](https://docs.djangoproject.com/en/5.1/ref/django-admin/#running-management-commands-from-your-code)
|
||||
Commands can be tested using Django's [`call_command()`](https://docs.djangoproject.com/en/5.2/ref/django-admin/#running-management-commands-from-your-code)
|
||||
function, which allows you to simulate running the command in tests.
|
||||
|
||||
```python
|
||||
|
@ -699,8 +699,8 @@ class MyExtension(ComponentExtension):
|
|||
|
||||
The [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) objects
|
||||
are different from objects created with Django's
|
||||
[`django.urls.path()`](https://docs.djangoproject.com/en/5.1/ref/urls/#path).
|
||||
Do NOT use `URLRoute` objects in Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.1/topics/http/urls/#example)
|
||||
[`django.urls.path()`](https://docs.djangoproject.com/en/5.2/ref/urls/#path).
|
||||
Do NOT use `URLRoute` objects in Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.2/topics/http/urls/#example)
|
||||
and vice versa!
|
||||
|
||||
django-components uses a custom [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) class to define framework-agnostic routing rules.
|
||||
|
@ -758,7 +758,7 @@ The [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) c
|
|||
so that extensions could be used with non-Django frameworks in the future.
|
||||
|
||||
However, that means that there may be some extra fields that Django's
|
||||
[`django.urls.path()`](https://docs.djangoproject.com/en/5.1/ref/urls/#path)
|
||||
[`django.urls.path()`](https://docs.djangoproject.com/en/5.2/ref/urls/#path)
|
||||
accepts, but which are not defined on the `URLRoute` object.
|
||||
|
||||
To address this, the [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) object has
|
||||
|
|
|
@ -82,7 +82,6 @@ class ChildComponent(Component):
|
|||
my_data = self.inject("my_data")
|
||||
print(my_data.hello) # hi
|
||||
print(my_data.another) # 123
|
||||
return {}
|
||||
```
|
||||
|
||||
First argument to [`Component.inject()`](../../../reference/api/#django_components.Component.inject) is the _key_ (or _name_) of the provided data. This
|
||||
|
@ -97,7 +96,6 @@ class ChildComponent(Component):
|
|||
def get_template_data(self, args, kwargs, slots, context):
|
||||
my_data = self.inject("invalid_key", DEFAULT_DATA)
|
||||
assert my_data == DEFAULT_DATA
|
||||
return {}
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
|
|
@ -109,8 +109,8 @@ fragment = MyComponent.render_to_response(deps_strategy="fragment")
|
|||
The `deps_strategy` parameter is set at the root of a component render tree, which is why it is not available for
|
||||
the [`{% component %}`](../../../reference/template_tags#component) tag.
|
||||
|
||||
When you use Django's [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render)
|
||||
or [`Template.render()`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template.render) to render templates,
|
||||
When you use Django's [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#render)
|
||||
or [`Template.render()`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template.render) to render templates,
|
||||
you can't directly set the `deps_strategy` parameter.
|
||||
|
||||
In this case, you can set the `deps_strategy` with the `DJC_DEPS_STRATEGY` context variable.
|
||||
|
@ -351,8 +351,8 @@ or templates can be rendered:
|
|||
|
||||
- [`Component.render()`](../../../reference/api/#django_components.Component.render)
|
||||
- [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
|
||||
- [`Template.render()`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template.render)
|
||||
- [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render)
|
||||
- [`Template.render()`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template.render)
|
||||
- [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#render)
|
||||
|
||||
This way you don't need to manually handle rendering of JS / CSS.
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ By default, the Python files found in the
|
|||
[`COMPONENTS.app_dirs`](../../../reference/settings#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||
are auto-imported in order to execute the code that registers the components.
|
||||
|
||||
Autodiscovery occurs when Django is loaded, during the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready)
|
||||
Autodiscovery occurs when Django is loaded, during the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready)
|
||||
hook of the `apps.py` file.
|
||||
|
||||
If you are using autodiscovery, keep a few points in mind:
|
||||
|
|
|
@ -11,7 +11,7 @@ django-components has a suite of features that help you write and manage views a
|
|||
|
||||
- For each component, you can define methods for handling HTTP requests (GET, POST, etc.) - `get()`, `post()`, etc.
|
||||
|
||||
- Use [`Component.as_view()`](../../../reference/api#django_components.Component.as_view) to be able to use your Components with Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.1/topics/http/urls/). This works the same way as [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view).
|
||||
- Use [`Component.as_view()`](../../../reference/api#django_components.Component.as_view) to be able to use your Components with Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.2/topics/http/urls/). This works the same way as [`View.as_view()`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View.as_view).
|
||||
|
||||
- To avoid having to manually define the endpoints for each component, you can set the component to be "public" with [`Component.View.public = True`](../../../reference/api#django_components.ComponentView.public). This will automatically create a URL for the component. To retrieve the component URL, use [`get_component_url()`](../../../reference/api#django_components.get_component_url).
|
||||
|
||||
|
@ -54,11 +54,11 @@ class Calendar(Component):
|
|||
|
||||
!!! info
|
||||
|
||||
The View class supports all the same HTTP methods as Django's [`View`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View) class. These are:
|
||||
The View class supports all the same HTTP methods as Django's [`View`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View) class. These are:
|
||||
|
||||
`get()`, `post()`, `put()`, `patch()`, `delete()`, `head()`, `options()`, `trace()`
|
||||
|
||||
Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest) object as the first argument.
|
||||
Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest) object as the first argument.
|
||||
|
||||
|
||||
<!-- TODO_V1 REMOVE -->
|
||||
|
@ -108,7 +108,7 @@ class Calendar(Component):
|
|||
## Register URLs manually
|
||||
|
||||
To register the component as a route / endpoint in Django, add an entry to your
|
||||
[`urlpatterns`](https://docs.djangoproject.com/en/5.1/topics/http/urls/).
|
||||
[`urlpatterns`](https://docs.djangoproject.com/en/5.2/topics/http/urls/).
|
||||
In place of the view function, create a view object with [`Component.as_view()`](../../../reference/api#django_components.Component.as_view):
|
||||
|
||||
```python title="[project root]/urls.py"
|
||||
|
@ -121,7 +121,7 @@ urlpatterns = [
|
|||
```
|
||||
|
||||
[`Component.as_view()`](../../../reference/api#django_components.Component.as_view)
|
||||
internally calls [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view), passing the component
|
||||
internally calls [`View.as_view()`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View.as_view), passing the component
|
||||
instance as one of the arguments.
|
||||
|
||||
## Register URLs automatically
|
||||
|
|
|
@ -56,7 +56,7 @@ class MyComponent(Component):
|
|||
|
||||
## Context Processors
|
||||
|
||||
Components support Django's [context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#using-requestcontext).
|
||||
Components support Django's [context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#using-requestcontext).
|
||||
|
||||
In regular Django templates, the context processors are applied only when the template is rendered with [`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext).
|
||||
|
||||
|
|
|
@ -80,8 +80,6 @@ class Table(Component):
|
|||
def get_template_data(self, args, kwargs, slots, context):
|
||||
# Access component's ID
|
||||
assert self.id == "c1A2b3c"
|
||||
|
||||
return {}
|
||||
```
|
||||
|
||||
## Component inputs
|
||||
|
@ -96,7 +94,7 @@ All the component inputs are captured and available as [`self.input`](../../../r
|
|||
- `context` - [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context) object that should be used to render the component
|
||||
- And other kwargs passed to [`Component.render()`](../../../reference/api/#django_components.Component.render) like `type` and `render_dependencies`
|
||||
|
||||
Thus, use can use [`self.input.args`](../../../reference/api/#django_components.ComponentInput.args)
|
||||
For example, you can use [`self.input.args`](../../../reference/api/#django_components.ComponentInput.args)
|
||||
and [`self.input.kwargs`](../../../reference/api/#django_components.ComponentInput.kwargs)
|
||||
to access the positional and keyword arguments passed to [`Component.render()`](../../../reference/api/#django_components.Component.render).
|
||||
|
||||
|
@ -109,8 +107,6 @@ class Table(Component):
|
|||
footer_slot = self.input.slots["footer"]
|
||||
some_var = self.input.context["some_var"]
|
||||
|
||||
return {}
|
||||
|
||||
rendered = TestComponent.render(
|
||||
kwargs={"variable": "test", "another": 1},
|
||||
args=(123, "str"),
|
||||
|
@ -120,9 +116,9 @@ rendered = TestComponent.render(
|
|||
|
||||
## Request object and context processors
|
||||
|
||||
If the component was either:
|
||||
Components have access to the request object and context processors data if the component was:
|
||||
|
||||
- Given a [`request`](../../../reference/api/#django_components.Component.render) kwarg
|
||||
- Given a [`request`](../../../reference/api/#django_components.Component.render) kwarg directly
|
||||
- Rendered with [`RenderContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
|
||||
- Nested in another component for which any of these conditions is true
|
||||
|
||||
|
@ -145,8 +141,6 @@ class Table(Component):
|
|||
assert self.request.GET == {"query": "something"}
|
||||
assert self.context_processors_data['user'].username == "admin"
|
||||
|
||||
return {}
|
||||
|
||||
rendered = Table.render(
|
||||
request=HttpRequest(),
|
||||
)
|
||||
|
|
|
@ -147,39 +147,39 @@ If you have embedded the component in a Django template using the
|
|||
|
||||
You can simply render the template with the Django's API:
|
||||
|
||||
- [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render)
|
||||
- [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#render)
|
||||
|
||||
```python
|
||||
from django.shortcuts import render
|
||||
```python
|
||||
from django.shortcuts import render
|
||||
|
||||
context = {"date": "2024-12-13"}
|
||||
rendered_template = render(request, "my_template.html", context)
|
||||
```
|
||||
context = {"date": "2024-12-13"}
|
||||
rendered_template = render(request, "my_template.html", context)
|
||||
```
|
||||
|
||||
- [`Template.render()`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template.render)
|
||||
- [`Template.render()`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template.render)
|
||||
|
||||
```python
|
||||
from django.template import Template
|
||||
from django.template.loader import get_template
|
||||
```python
|
||||
from django.template import Template
|
||||
from django.template.loader import get_template
|
||||
|
||||
# Either from a file
|
||||
template = get_template("my_template.html")
|
||||
# Either from a file
|
||||
template = get_template("my_template.html")
|
||||
|
||||
# or inlined
|
||||
template = Template("""
|
||||
{% load component_tags %}
|
||||
<div>
|
||||
{% component "calendar" date="2024-12-13" / %}
|
||||
</div>
|
||||
""")
|
||||
# or inlined
|
||||
template = Template("""
|
||||
{% load component_tags %}
|
||||
<div>
|
||||
{% component "calendar" date="2024-12-13" / %}
|
||||
</div>
|
||||
""")
|
||||
|
||||
rendered_template = template.render()
|
||||
```
|
||||
rendered_template = template.render()
|
||||
```
|
||||
|
||||
### Isolating components
|
||||
|
||||
By default, components behave similarly to Django's
|
||||
[`{% include %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#include),
|
||||
[`{% include %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#include),
|
||||
and the template inside the component has access to the variables defined in the outer template.
|
||||
|
||||
You can selectively isolate a component, using the `only` flag, so that the inner template
|
||||
|
@ -244,8 +244,8 @@ Button.render(
|
|||
- `kwargs` - Keyword arguments to pass to the component (as a dictionary)
|
||||
- `slots` - Slot content to pass to the component (as a dictionary)
|
||||
- `context` - Django context for rendering (can be a dictionary or a `Context` object)
|
||||
- `deps_strategy` - Dependencies rendering strategy (default: `"document"`)
|
||||
- `request` - HTTP request object, used for context processors (optional)
|
||||
- `deps_strategy` - [Dependencies rendering strategy](#dependencies-rendering) (default: `"document"`)
|
||||
- `request` - [HTTP request object](../http_request), used for context processors (optional)
|
||||
- `escape_slots_content` - Whether to HTML-escape slot content (default: `True`)
|
||||
|
||||
All arguments are optional. If not provided, they default to empty values or sensible defaults.
|
||||
|
@ -416,7 +416,7 @@ Instead, use `args`, `kwargs`, and `slots` to pass data to the component.
|
|||
However, you can pass
|
||||
[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
|
||||
to the `context` argument, so that the component will gain access to the request object and will use
|
||||
[context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#using-requestcontext).
|
||||
[context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#using-requestcontext).
|
||||
Read more on [Working with HTTP requests](../http_request).
|
||||
|
||||
```py
|
||||
|
|
|
@ -28,11 +28,11 @@ class Calendar(Component):
|
|||
|
||||
!!! note
|
||||
|
||||
django-component's management of files is inspired by [Django's `Media` class](https://docs.djangoproject.com/en/5.0/topics/forms/media/).
|
||||
django-component's management of files is inspired by [Django's `Media` class](https://docs.djangoproject.com/en/5.2/topics/forms/media/).
|
||||
|
||||
To be familiar with how Django handles static files, we recommend reading also:
|
||||
|
||||
- [How to manage static files (e.g. images, JavaScript, CSS)](https://docs.djangoproject.com/en/5.0/howto/static-files/)
|
||||
- [How to manage static files (e.g. images, JavaScript, CSS)](https://docs.djangoproject.com/en/5.2/howto/static-files/)
|
||||
|
||||
## `Media` class
|
||||
|
||||
|
@ -50,14 +50,14 @@ class Calendar(Component):
|
|||
Use the `Media` class to define secondary JS / CSS files for a component.
|
||||
|
||||
This `Media` class behaves similarly to
|
||||
[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.2/topics/forms/media/#assets-as-a-static-definition):
|
||||
|
||||
- **Static paths** - Paths are handled as static file paths, and are resolved to URLs with Django's
|
||||
[`{% static %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#static) template tag.
|
||||
- **URLs** - A path that starts with `http`, `https`, or `/` is considered a URL. URLs are NOT resolved with [`{% static %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#static).
|
||||
[`{% static %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#static) template tag.
|
||||
- **URLs** - A path that starts with `http`, `https`, or `/` is considered a URL. URLs are NOT resolved with [`{% static %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#static).
|
||||
- **HTML tags** - Both static paths and URLs are rendered to `<script>` and `<link>` HTML tags with
|
||||
`media_class.render_js()` and `media_class.render_css()`.
|
||||
- **Bypass formatting** - A [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
|
||||
- **Bypass formatting** - A [`SafeString`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.SafeString),
|
||||
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()`.
|
||||
- **Inheritance** - You can set [`extend`](../../../reference/api#django_components.ComponentMediaInput.extend) to configure
|
||||
|
@ -68,7 +68,7 @@ However, there's a few differences from Django's Media class:
|
|||
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 [`ComponentMediaInput`](../../../reference/api#django_components.ComponentMediaInput)).
|
||||
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.2/ref/utils/#django.utils.safestring.SafeString), or a function
|
||||
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
|
||||
3. Individual JS / CSS files can be glob patterns, e.g. `*.js` or `styles/**/*.css`.
|
||||
4. If you set [`Media.extend`](../../../reference/api/#django_components.ComponentMediaInput.extend) to a list,
|
||||
|
@ -104,7 +104,7 @@ class MyTable(Component):
|
|||
You can define which stylesheets will be associated with which
|
||||
[CSS media types](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries#targeting_media_types). You do so by defining CSS files as a dictionary.
|
||||
|
||||
See the corresponding [Django Documentation](https://docs.djangoproject.com/en/5.0/topics/forms/media/#css).
|
||||
See the corresponding [Django Documentation](https://docs.djangoproject.com/en/5.2/topics/forms/media/#css).
|
||||
|
||||
Again, you can set either a single file or a list of files per media type:
|
||||
|
||||
|
@ -196,7 +196,7 @@ 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),
|
||||
[Django's Media class](https://docs.djangoproject.com/en/5.2/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).
|
||||
|
@ -207,7 +207,7 @@ To access the files that you defined under [`Component.Media`](../../../referenc
|
|||
use [`Component.media`](../../reference/api.md#django_components.Component.media) (lowercase).
|
||||
|
||||
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.2/topics/forms/media/#assets-as-a-static-definition).
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
|
@ -396,11 +396,11 @@ class SimpleComponent(Component):
|
|||
### Paths as objects
|
||||
|
||||
In the example [above](#supported-types), you can see that when we used Django's
|
||||
[`mark_safe()`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.mark_safe)
|
||||
to mark a string as a [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
|
||||
[`mark_safe()`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.mark_safe)
|
||||
to mark a string as a [`SafeString`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.SafeString),
|
||||
we had to define the URL / path as an HTML `<script>`/`<link>` elements.
|
||||
|
||||
This is an extension of Django's [Paths as objects](https://docs.djangoproject.com/en/5.0/topics/forms/media/#paths-as-objects)
|
||||
This is an extension of Django's [Paths as objects](https://docs.djangoproject.com/en/5.2/topics/forms/media/#paths-as-objects)
|
||||
feature, where "safe" strings are taken as is, and are accessed only at render time.
|
||||
|
||||
Because of that, the paths defined as "safe" strings are NEVER resolved, neither relative to component's directory,
|
||||
|
@ -446,7 +446,7 @@ In the [Paths as objects](#paths-as-objects) section, we saw that we can use tha
|
|||
how the HTML tags are constructed.
|
||||
|
||||
However, if you need to change how ALL CSS and JS files are rendered for a given component,
|
||||
you can provide your own subclass of [Django's `Media` class](https://docs.djangoproject.com/en/5.0/topics/forms/media) to the [`Component.media_class`](../../reference/api.md#django_components.Component.media_class) attribute.
|
||||
you can provide your own subclass of [Django's `Media` class](https://docs.djangoproject.com/en/5.2/topics/forms/media) to the [`Component.media_class`](../../reference/api.md#django_components.Component.media_class) attribute.
|
||||
|
||||
To change how the tags are constructed, you can override the [`Media.render_js()` and `Media.render_css()` methods](https://github.com/django/django/blob/fa7848146738a9fe1d415ee4808664e54739eeb7/django/forms/widgets.py#L102):
|
||||
|
||||
|
|
|
@ -226,7 +226,7 @@ automatically embed the associated JS and CSS.
|
|||
Your components may depend on third-party packages or styling, or other shared logic.
|
||||
To load these additional dependencies, you can use a nested [`Media` class](../../reference/api#django_components.Component.Media).
|
||||
|
||||
This `Media` class behaves similarly to [Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition),
|
||||
This `Media` class behaves similarly to [Django's Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/#assets-as-a-static-definition),
|
||||
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).
|
||||
|
@ -275,7 +275,7 @@ class Calendar(Component):
|
|||
|
||||
!!! info
|
||||
|
||||
The `Media` nested class is shaped based on [Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/).
|
||||
The `Media` nested class is shaped based on [Django's Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/).
|
||||
|
||||
As such, django-components allows multiple formats to define the nested Media class:
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@ the component inputs, and massage them into a shape that's most appropriate for
|
|||
what the template needs. And it also allows us to pass in static data into the template.
|
||||
|
||||
Imagine our component receives data from the database that looks like below
|
||||
([taken from Django](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#regroup)).
|
||||
([taken from Django](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#regroup)).
|
||||
|
||||
```py
|
||||
cities = [
|
||||
|
|
|
@ -39,7 +39,7 @@ If you have embedded the component in a Django template using the
|
|||
|
||||
You can simply render the template with the Django tooling:
|
||||
|
||||
#### With [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render)
|
||||
#### With [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#render)
|
||||
|
||||
```python
|
||||
from django.shortcuts import render
|
||||
|
@ -48,9 +48,9 @@ context = {"date": "2024-12-13"}
|
|||
rendered_template = render(request, "my_template.html", context)
|
||||
```
|
||||
|
||||
#### With [`Template.render()`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template.render)
|
||||
#### With [`Template.render()`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template.render)
|
||||
|
||||
Either loading the template with [`get_template()`](https://docs.djangoproject.com/en/5.1/topics/templates/#django.template.loader.get_template):
|
||||
Either loading the template with [`get_template()`](https://docs.djangoproject.com/en/5.2/topics/templates/#django.template.loader.get_template):
|
||||
|
||||
```python
|
||||
from django.template.loader import get_template
|
||||
|
@ -60,7 +60,7 @@ context = {"date": "2024-12-13"}
|
|||
rendered_template = template.render(context)
|
||||
```
|
||||
|
||||
Or creating a new [`Template`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template) instance:
|
||||
Or creating a new [`Template`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template) instance:
|
||||
|
||||
```python
|
||||
from django.template import Template
|
||||
|
@ -113,7 +113,7 @@ rendered_component = calendar.render(
|
|||
rendered_component = calendar.render(request=request)
|
||||
```
|
||||
|
||||
The `request` object is required for some of the component's features, like using [Django's context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.RequestContext).
|
||||
The `request` object is required for some of the component's features, like using [Django's context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext).
|
||||
|
||||
### 3. Render the component to HttpResponse
|
||||
|
||||
|
@ -121,7 +121,7 @@ A common pattern in Django is to render the component and then return the result
|
|||
|
||||
For this, you can use the [`Component.render_to_response()`](../../reference/api#django_components.Component.render_to_response) convenience method.
|
||||
|
||||
`render_to_response()` accepts the same args, kwargs, slots, and more, as [`Component.render()`](../../reference/api#django_components.Component.render), but wraps the result in an [`HttpResponse`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpResponse).
|
||||
`render_to_response()` accepts the same args, kwargs, slots, and more, as [`Component.render()`](../../reference/api#django_components.Component.render), but wraps the result in an [`HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpResponse).
|
||||
|
||||
```python
|
||||
from components.calendar import Calendar
|
||||
|
@ -144,7 +144,7 @@ def my_view(request):
|
|||
|
||||
**Response class of `render_to_response`**
|
||||
|
||||
While `render` method returns a plain string, `render_to_response` wraps the rendered content in a "Response" class. By default, this is [`django.http.HttpResponse`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpResponse).
|
||||
While `render` method returns a plain string, `render_to_response` wraps the rendered content in a "Response" class. By default, this is [`django.http.HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpResponse).
|
||||
|
||||
If you want to use a different Response class in `render_to_response`, set the [`Component.response_class`](../../reference/api#django_components.Component.response_class) attribute:
|
||||
|
||||
|
@ -164,7 +164,7 @@ def my_view(request):
|
|||
|
||||
Slots content are automatically escaped by default to prevent XSS attacks.
|
||||
|
||||
In other words, it's as if you would be using Django's [`mark_safe()`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe) function on the slot content:
|
||||
In other words, it's as if you would be using Django's [`mark_safe()`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.mark_safe) function on the slot content:
|
||||
|
||||
```python
|
||||
from django.utils.safestring import mark_safe
|
||||
|
@ -196,8 +196,8 @@ methods.
|
|||
!!! info
|
||||
|
||||
If you're planning on passing an HTML string, check Django's use of
|
||||
[`format_html`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.html.format_html)
|
||||
and [`mark_safe`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe).
|
||||
[`format_html`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.html.format_html)
|
||||
and [`mark_safe`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.mark_safe).
|
||||
|
||||
### Component views and URLs
|
||||
|
||||
|
@ -247,11 +247,11 @@ class Calendar(Component):
|
|||
|
||||
!!! info
|
||||
|
||||
The View class supports all the same HTTP methods as Django's [`View`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View) class. These are:
|
||||
The View class supports all the same HTTP methods as Django's [`View`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View) class. These are:
|
||||
|
||||
`get()`, `post()`, `put()`, `patch()`, `delete()`, `head()`, `options()`, `trace()`
|
||||
|
||||
Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest) object as the first argument.
|
||||
Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest) object as the first argument.
|
||||
|
||||
Next, you need to set the URL for the component.
|
||||
|
||||
|
@ -284,7 +284,7 @@ You can either:
|
|||
|
||||
And with that, you're all set! When you visit the URL, the component will be rendered and the content will be returned.
|
||||
|
||||
The `get()`, `post()`, etc methods will receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest) object as the first argument. So you can parametrize how the component is rendered for example by passing extra query parameters to the URL:
|
||||
The `get()`, `post()`, etc methods will receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest) object as the first argument. So you can parametrize how the component is rendered for example by passing extra query parameters to the URL:
|
||||
|
||||
```
|
||||
http://localhost:8000/calendar/?date=2024-12-13
|
||||
|
|
|
@ -63,10 +63,10 @@ KeyError: "An error occured while rendering components my_page > layout > layout
|
|||
|
||||
## Debug and trace logging
|
||||
|
||||
Django components supports [logging with Django](https://docs.djangoproject.com/en/5.0/howto/logging/#logging-how-to).
|
||||
Django components supports [logging with Django](https://docs.djangoproject.com/en/5.2/howto/logging/#logging-how-to).
|
||||
|
||||
To configure logging for Django components, set the `django_components` logger in
|
||||
[`LOGGING`](https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-LOGGING)
|
||||
[`LOGGING`](https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-LOGGING)
|
||||
in `settings.py` (below).
|
||||
|
||||
Also see the [`settings.py` file in sampleproject](https://github.com/django-components/django-components/blob/master/sampleproject/sampleproject/settings.py) for a real-life example.
|
||||
|
|
|
@ -30,7 +30,7 @@ COMPONENTS = {
|
|||
}
|
||||
```
|
||||
|
||||
The value should be the name of one of your configured cache backends from Django's [`CACHES`](https://docs.djangoproject.com/en/stable/ref/settings/#std-setting-CACHES) setting.
|
||||
The value should be the name of one of your configured cache backends from Django's [`CACHES`](https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-CACHES) setting.
|
||||
|
||||
For example, to use Redis for caching component assets:
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Django-components supports all supported combinations versions of [Django](https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django) and [Python](https://devguide.python.org/versions/#versions).
|
||||
Django-components supports all supported combinations versions of [Django](https://docs.djangoproject.com/en/5.2/faq/install/#what-python-version-can-i-use-with-django) and [Python](https://devguide.python.org/versions/#versions).
|
||||
|
||||
| Python version | Django version |
|
||||
| -------------- | -------------- |
|
||||
|
|
|
@ -156,6 +156,37 @@ twine upload --repository pypi dist/* -u __token__ -p <PyPI_TOKEN>
|
|||
|
||||
[See the full workflow here.](https://github.com/django-components/django-components/discussions/557#discussioncomment-10179141)
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Updating supported versions
|
||||
|
||||
The `scripts/supported_versions.py` script can be used to update the supported versions.
|
||||
|
||||
```sh
|
||||
python scripts/supported_versions.py
|
||||
```
|
||||
|
||||
This will check the current versions of Django and Python, and will print to the terminal
|
||||
all the places that need updating and what to set them to.
|
||||
|
||||
### Updating link references
|
||||
|
||||
The `scripts/validate_links.py` script can be used to update the link references.
|
||||
|
||||
```sh
|
||||
python scripts/validate_links.py
|
||||
```
|
||||
|
||||
When new version of Django is released, you can use the script to update the URLs pointing to the Django documentation.
|
||||
|
||||
First, you need to update the `URL_REWRITE_MAP` in the script to point to the new version of Django.
|
||||
|
||||
Then, you can run the script to update the URLs in the codebase.
|
||||
|
||||
```sh
|
||||
python scripts/validate_links.py --rewrite
|
||||
```
|
||||
|
||||
## Development guides
|
||||
|
||||
Head over to [Dev guides](../guides/devguides/dependency_mgmt.md) for a deep dive into how django_components' features are implemented.
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
- _Remove `'APP_DIRS': True,`_
|
||||
- NOTE: Instead of `APP_DIRS: True`, we will use
|
||||
[`django.template.loaders.app_directories.Loader`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.loaders.app_directories.Loader),
|
||||
[`django.template.loaders.app_directories.Loader`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.loaders.app_directories.Loader),
|
||||
which has the same effect.
|
||||
- Add `loaders` to `OPTIONS` list and set it to following value:
|
||||
|
||||
|
@ -176,7 +176,7 @@ COMPONENTS = ComponentsSettings(
|
|||
|
||||
The input to [`COMPONENTS.dirs`](../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
|
||||
is the same as for `STATICFILES_DIRS`, and the paths must be full paths.
|
||||
[See Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#staticfiles-dirs).
|
||||
[See Django docs](https://docs.djangoproject.com/en/5.2/ref/settings/#staticfiles-dirs).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -50,15 +50,15 @@ If you are on an pre-v0.27 version of django-components, your alternatives are:
|
|||
- a) passing `--ignore <pattern>` options to the _collecstatic_ CLI command,
|
||||
- b) defining a subclass of StaticFilesConfig.
|
||||
|
||||
Both routes are described in the official [docs of the _staticfiles_ app](https://docs.djangoproject.com/en/4.2/ref/contrib/staticfiles/#customizing-the-ignored-pattern-list).
|
||||
Both routes are described in the official [docs of the _staticfiles_ app](https://docs.djangoproject.com/en/5.2/ref/contrib/staticfiles/#customizing-the-ignored-pattern-list).
|
||||
|
||||
Note that `safer_staticfiles` excludes the `.py` and `.html` files for [collectstatic command](https://docs.djangoproject.com/en/5.0/ref/contrib/staticfiles/#collectstatic):
|
||||
Note that `safer_staticfiles` excludes the `.py` and `.html` files for [collectstatic command](https://docs.djangoproject.com/en/5.2/ref/contrib/staticfiles/#collectstatic):
|
||||
|
||||
```sh
|
||||
python manage.py collectstatic
|
||||
```
|
||||
|
||||
but it is ignored on the [development server](https://docs.djangoproject.com/en/5.0/ref/django-admin/#runserver):
|
||||
but it is ignored on the [development server](https://docs.djangoproject.com/en/5.2/ref/django-admin/#runserver):
|
||||
|
||||
```sh
|
||||
python manage.py runserver
|
||||
|
|
|
@ -111,7 +111,7 @@ class Calendar(Component):
|
|||
|
||||
# Additional JS and CSS
|
||||
class Media:
|
||||
js = ["https://cdn.jsdelivr.net/npm/htmx.org@2.1.1/dist/htmx.min.js"]
|
||||
js = ["https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"]
|
||||
css = ["bootstrap/dist/css/bootstrap.min.css"]
|
||||
|
||||
# Variables available in the template
|
||||
|
|
|
@ -787,7 +787,7 @@ def _format_hook_type(type_str: str) -> str:
|
|||
elif "Component" in type_str:
|
||||
type_str = f"[{type_str}](../api#django_components.Component)"
|
||||
elif "Context" in type_str:
|
||||
type_str = f"[{type_str}](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)"
|
||||
type_str = f"[{type_str}](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)"
|
||||
|
||||
return type_str
|
||||
|
||||
|
@ -879,7 +879,7 @@ def _extract_property_docstrings(cls: Type) -> Dict[str, str]:
|
|||
# as Python code). Instead, we manually list all signals that are sent by django-components.
|
||||
def gen_reference_signals():
|
||||
"""
|
||||
Generate documentation for all [Django Signals](https://docs.djangoproject.com/en/5.1/ref/signals) that are
|
||||
Generate documentation for all [Django Signals](https://docs.djangoproject.com/en/5.2/ref/signals) that are
|
||||
send by or during the use of django-components.
|
||||
"""
|
||||
preface = "<!-- Autogenerated by reference.py -->\n\n"
|
||||
|
|
2
docs/templates/reference_commands.md
vendored
2
docs/templates/reference_commands.md
vendored
|
@ -1,4 +1,4 @@
|
|||
# Commands
|
||||
|
||||
These are all the [Django management commands](https://docs.djangoproject.com/en/5.1/ref/django-admin)
|
||||
These are all the [Django management commands](https://docs.djangoproject.com/en/5.2/ref/django-admin)
|
||||
that will be added by installing `django_components`:
|
||||
|
|
2
docs/templates/reference_signals.md
vendored
2
docs/templates/reference_signals.md
vendored
|
@ -4,7 +4,7 @@ Below are the signals that are sent by or during the use of django-components.
|
|||
|
||||
## template_rendered
|
||||
|
||||
Django's [`template_rendered`](https://docs.djangoproject.com/en/5.1/ref/signals/#template-rendered) signal.
|
||||
Django's [`template_rendered`](https://docs.djangoproject.com/en/5.2/ref/signals/#template-rendered) signal.
|
||||
This signal is sent when a template is rendered.
|
||||
|
||||
Django-components triggers this signal when a component is rendered. If there are nested components,
|
||||
|
|
|
@ -158,8 +158,8 @@ plugins:
|
|||
python:
|
||||
import:
|
||||
- https://docs.python.org/3.12/objects.inv
|
||||
- url: https://docs.djangoproject.com/en/5.0/_objects/
|
||||
base: https://docs.djangoproject.com/en/5.0/
|
||||
- url: https://docs.djangoproject.com/en/5.2/_objects/
|
||||
base: https://docs.djangoproject.com/en/5.2/
|
||||
domains: [std, py]
|
||||
paths: [src] # search packages in the src folder
|
||||
options:
|
||||
|
|
|
@ -10,4 +10,5 @@ asv
|
|||
virtualenv==20.30
|
||||
pytest-asyncio
|
||||
pytest-django
|
||||
typing-extensions>=4.12.2
|
||||
typing-extensions>=4.12.2
|
||||
pathspec
|
|
@ -46,6 +46,8 @@ packaging==24.2
|
|||
# pyproject-api
|
||||
# pytest
|
||||
# tox
|
||||
pathspec==0.12.1
|
||||
# via -r requirements-ci.in
|
||||
platformdirs==4.3.6
|
||||
# via
|
||||
# tox
|
||||
|
|
|
@ -21,4 +21,5 @@ asv
|
|||
# NOTE: pin virtualenv to <20.31 until asv fixes integration
|
||||
# See https://github.com/airspeed-velocity/asv/issues/1484
|
||||
virtualenv==20.30
|
||||
typing-extensions>=4.12.2
|
||||
typing-extensions>=4.12.2
|
||||
pathspec
|
|
@ -80,7 +80,7 @@ packaging==24.2
|
|||
# pytest
|
||||
# tox
|
||||
pathspec==0.12.1
|
||||
# via black
|
||||
# via -r requirements-ci.in
|
||||
platformdirs==4.3.6
|
||||
# via
|
||||
# black
|
||||
|
|
|
@ -4,7 +4,7 @@ ASGI config for sampleproject project.
|
|||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
|
@ -12,7 +12,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
|||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
|
@ -100,7 +100,7 @@ COMPONENTS = ComponentsSettings(
|
|||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
|
@ -111,7 +111,7 @@ DATABASES = {
|
|||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
|
@ -130,7 +130,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.0/topics/i18n/
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
|
@ -142,12 +142,12 @@ USE_TZ = True
|
|||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ WSGI config for sampleproject project.
|
|||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
406
scripts/validate_links.py
Normal file
406
scripts/validate_links.py
Normal file
|
@ -0,0 +1,406 @@
|
|||
"""
|
||||
validate_links.py - URL checker and rewriter for the codebase.
|
||||
|
||||
This script scans all files in the repository (respecting .gitignore and IGNORED_PATHS),
|
||||
finds all URLs, validates them (including checking for HTML fragments), and can optionally
|
||||
rewrite URLs in-place using a configurable mapping.
|
||||
|
||||
Features:
|
||||
- Finds all URLs in code, markdown, and docstrings.
|
||||
- Validates URLs by making GET requests (with caching and rate limiting).
|
||||
- Uses BeautifulSoup to check for HTML fragments (e.g., #section) in the target page.
|
||||
- Outputs a summary table of all issues (invalid, broken, missing fragment, etc).
|
||||
- Can output the summary table to a file with `-o`/`--output`.
|
||||
- Can rewrite URLs in-place using URL_REWRITE_MAP (supports both prefix and regex mapping).
|
||||
- Supports dry-run mode for rewrites with `--dry-run`.
|
||||
|
||||
Usage:
|
||||
|
||||
# Validate all links and print summary to stdout
|
||||
python scripts/validate_links.py
|
||||
|
||||
# Output summary table to a file
|
||||
python scripts/validate_links.py -o link_report.txt
|
||||
|
||||
# Rewrite URLs using URL_REWRITE_MAP (in-place)
|
||||
python scripts/validate_links.py --rewrite
|
||||
|
||||
# Show what would be rewritten, but do not write files
|
||||
python scripts/validate_links.py --rewrite --dry-run
|
||||
|
||||
Configuration:
|
||||
- IGNORED_PATHS: List of files/dirs to skip (in addition to .gitignore)
|
||||
- URL_REWRITE_MAP: Dict of {prefix or regex: replacement} for rewriting URLs
|
||||
|
||||
See the code for more details and examples.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
import sys
|
||||
import time
|
||||
from collections import defaultdict, deque
|
||||
from pathlib import Path
|
||||
from typing import DefaultDict, Deque, Dict, List, Tuple, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import pathspec
|
||||
|
||||
from django_components.util.misc import format_as_ascii_table
|
||||
|
||||
# This script relies on .gitignore to know which files to search for URLs,
|
||||
# and which files to ignore.
|
||||
#
|
||||
# If there are files / dirs that you need to ignore, but they are not (or cannot be)
|
||||
# included in .gitignore, you can add them here.
|
||||
IGNORED_PATHS = [
|
||||
"package-lock.json",
|
||||
"package.json",
|
||||
"yarn.lock",
|
||||
"mdn_complete_page.html",
|
||||
"supported_versions.py",
|
||||
# Ignore auto-generated files
|
||||
"node_modules",
|
||||
"node_modules/",
|
||||
".asv/",
|
||||
"__snapshots__/",
|
||||
"docs/benchmarks/",
|
||||
".git/",
|
||||
"*.min.js",
|
||||
"*.min.css",
|
||||
]
|
||||
|
||||
# Domains that are not real and should be ignored.
|
||||
IGNORE_DOMAINS = [
|
||||
"127.0.0.1",
|
||||
"localhost",
|
||||
"0.0.0.0",
|
||||
"example.com",
|
||||
]
|
||||
|
||||
# This allows us to rewrite URLs across the codebase.
|
||||
# - If key is a str, it's a prefix and the value is the new prefix.
|
||||
# - If key is a re.Pattern, it's a regex and the value is the replacement string.
|
||||
URL_REWRITE_MAP: Dict[Union[str, re.Pattern], str] = {
|
||||
# Example with regex and capture groups
|
||||
# re.compile(r"https://github.com/old-org/([^/]+)/"): r"https://github.com/new-org/\1/",
|
||||
# Update all Django docs URLs to 5.2
|
||||
re.compile(r"https://docs.djangoproject.com/en/([^/]+)/"): "https://docs.djangoproject.com/en/5.2/",
|
||||
}
|
||||
|
||||
|
||||
REQUEST_TIMEOUT = 8 # seconds
|
||||
REQUEST_DELAY = 0.5 # seconds between requests
|
||||
|
||||
|
||||
# Simple regex for URLs to scan for
|
||||
URL_REGEX = re.compile(r'https?://[^\s\'"\)\]]+')
|
||||
|
||||
# Detailed regex for URLs to validate
|
||||
# See https://stackoverflow.com/a/7160778/9788634
|
||||
URL_VALIDATOR_REGEX = re.compile(
|
||||
r"^(?:http|ftp)s?://" # http:// or https://
|
||||
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain...
|
||||
r"localhost|" # localhost...
|
||||
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
|
||||
r"(?::\d+)?" # optional port
|
||||
r"(?:/?|[/?]\S+)$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def is_binary_file(filepath: Path) -> bool:
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
chunk = f.read(1024)
|
||||
if b"\0" in chunk:
|
||||
return True
|
||||
except Exception:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def load_gitignore(root: Path) -> pathspec.PathSpec:
|
||||
gitignore = root / ".gitignore"
|
||||
patterns = []
|
||||
if gitignore.exists():
|
||||
with open(gitignore) as f:
|
||||
patterns = f.read().splitlines()
|
||||
# Add additional ignored paths
|
||||
patterns += IGNORED_PATHS
|
||||
return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
|
||||
|
||||
|
||||
# Recursively find all files not ignored by .gitignore
|
||||
def find_files(root: Path, spec: pathspec.PathSpec) -> List[Path]:
|
||||
files = []
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
# Remove ignored dirs in-place
|
||||
rel_dir = os.path.relpath(dirpath, root)
|
||||
if rel_dir == ".":
|
||||
rel_dir = ""
|
||||
ignored_dirs = [d for d in dirnames if spec.match_file(os.path.join(rel_dir, d))]
|
||||
for d in ignored_dirs:
|
||||
dirnames.remove(d)
|
||||
for filename in filenames:
|
||||
rel_file = os.path.join(rel_dir, filename)
|
||||
if not spec.match_file(rel_file):
|
||||
files.append(Path(dirpath) / filename)
|
||||
return files
|
||||
|
||||
|
||||
# Extract URLs from a file
|
||||
def extract_urls_from_file(filepath: Path) -> List[Tuple[str, int, str, str]]:
|
||||
urls = []
|
||||
try:
|
||||
with open(filepath, encoding="utf-8", errors="replace") as f:
|
||||
for i, line in enumerate(f, 1):
|
||||
for match in URL_REGEX.finditer(line):
|
||||
url = match.group(0)
|
||||
urls.append((str(filepath), i, line.rstrip(), url))
|
||||
except Exception as e:
|
||||
print(f"[WARN] Could not read {filepath}: {e}", file=sys.stderr)
|
||||
return urls
|
||||
|
||||
|
||||
def get_base_url(url: str) -> str:
|
||||
"""Return the URL without the fragment."""
|
||||
return url.split("#", 1)[0]
|
||||
|
||||
|
||||
def pick_next_url(domains, domain_to_urls, last_request_time):
|
||||
"""
|
||||
Pick the next (domain, url) to fetch, respecting REQUEST_DELAY per domain.
|
||||
Returns (domain, url) or None if all are on cooldown or empty.
|
||||
"""
|
||||
now = time.time()
|
||||
for domain in domains:
|
||||
if not domain_to_urls[domain]:
|
||||
continue
|
||||
since_last = now - last_request_time[domain]
|
||||
if since_last >= REQUEST_DELAY:
|
||||
url = domain_to_urls[domain].popleft()
|
||||
return domain, url
|
||||
return None
|
||||
|
||||
|
||||
def validate_urls(all_urls):
|
||||
"""
|
||||
For each unique base URL, make a GET request (with caching).
|
||||
Print progress for each request (including cache hits).
|
||||
If a URL is invalid, print a warning and skip fetching.
|
||||
Skip URLs whose netloc matches IGNORE_DOMAINS.
|
||||
Use round-robin scheduling per domain, with cooldown.
|
||||
"""
|
||||
url_cache: Dict[str, Union[requests.Response, Exception, str]] = {}
|
||||
unique_base_urls = sorted(set(get_base_url(url) for _, _, _, url in all_urls))
|
||||
|
||||
# NOTE: Originally we fetched the URLs one after another. But the issue with this was that
|
||||
# there is a few large domains like Github, MDN, Djagno docs, etc. And there's a lot of URLs
|
||||
# point to them. So we ended up with a lot of 429 errors.
|
||||
#
|
||||
# The current approach is to group the URLs by domain, and then fetch them in parallel,
|
||||
# preferentially fetching from domains with most URLs (if not on cooldown).
|
||||
# This way we can spread the load over the domains, and avoid hitting the rate limits.
|
||||
|
||||
# Group URLs by domain
|
||||
domain_to_urls: DefaultDict[str, Deque[str]] = defaultdict(deque)
|
||||
for url in unique_base_urls:
|
||||
parsed = urlparse(url)
|
||||
if parsed.hostname and any(parsed.hostname == d for d in IGNORE_DOMAINS):
|
||||
url_cache[url] = "SKIPPED"
|
||||
continue
|
||||
domain_to_urls[parsed.netloc].append(url)
|
||||
|
||||
# Sort domains by number of URLs (descending)
|
||||
domains = sorted(domain_to_urls, key=lambda d: -len(domain_to_urls[d]))
|
||||
last_request_time = {domain: 0.0 for domain in domains}
|
||||
total_urls = sum(len(q) for q in domain_to_urls.values())
|
||||
done_count = 0
|
||||
|
||||
print(f"\nValidating {total_urls} unique base URLs (round-robin by domain)...")
|
||||
while any(domain_to_urls.values()):
|
||||
pick = pick_next_url(domains, domain_to_urls, last_request_time)
|
||||
if pick is None:
|
||||
# All domains are on cooldown, sleep until the soonest one is ready
|
||||
soonest = min(
|
||||
(last_request_time[d] + REQUEST_DELAY for d in domains if domain_to_urls[d]),
|
||||
default=time.time() + REQUEST_DELAY,
|
||||
)
|
||||
sleep_time = max(soonest - time.time(), 0.05)
|
||||
time.sleep(sleep_time)
|
||||
continue
|
||||
domain, url = pick
|
||||
|
||||
# Classify and fetch
|
||||
if url in url_cache:
|
||||
print(f"[done {done_count + 1}/{total_urls}] {url} (cache hit)")
|
||||
done_count += 1
|
||||
continue
|
||||
if not URL_VALIDATOR_REGEX.match(url):
|
||||
url_cache[url] = "INVALID_URL"
|
||||
print(f"[done {done_count + 1}/{total_urls}] {url} WARNING: Invalid URL format, not fetched.")
|
||||
done_count += 1
|
||||
continue
|
||||
|
||||
print(f"[done {done_count + 1}/{total_urls}] {url} ...", end=" ")
|
||||
try:
|
||||
resp = requests.get(
|
||||
url, timeout=REQUEST_TIMEOUT, headers={"User-Agent": "django-components-link-checker/0.1"}
|
||||
)
|
||||
url_cache[url] = resp
|
||||
print(f"{resp.status_code}")
|
||||
except Exception as err:
|
||||
url_cache[url] = err
|
||||
print(f"ERROR: {err}")
|
||||
|
||||
last_request_time[domain] = time.time()
|
||||
done_count += 1
|
||||
return url_cache
|
||||
|
||||
|
||||
def check_fragment_in_html(html: str, fragment: str) -> bool:
|
||||
"""Return True if id=fragment exists in the HTML."""
|
||||
print(f"Checking fragment {fragment} in HTML...")
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
return bool(soup.find(id=fragment))
|
||||
|
||||
|
||||
def rewrite_url(url: str) -> Union[Tuple[None, None], Tuple[str, Union[str, re.Pattern]]]:
|
||||
"""Return (new_url, mapping_key) if a mapping applies, else (None, None)."""
|
||||
for key, repl in URL_REWRITE_MAP.items():
|
||||
if isinstance(key, str):
|
||||
if url.startswith(key):
|
||||
return url.replace(key, repl, 1), key
|
||||
elif isinstance(key, re.Pattern):
|
||||
if key.search(url):
|
||||
return key.sub(repl, url), key
|
||||
else:
|
||||
raise ValueError(f"Invalid key type: {type(key)}")
|
||||
return None, None
|
||||
|
||||
|
||||
def output_summary(errors: List[Tuple[str, int, str, str, str]], output: str):
|
||||
# Format the errors into a table
|
||||
headers = ["Type", "Details", "File", "URL"]
|
||||
data = [
|
||||
{"File": file + "#" + str(lineno), "Type": errtype, "URL": url, "Details": details}
|
||||
for file, lineno, errtype, url, details in errors
|
||||
]
|
||||
table = format_as_ascii_table(data, headers, include_headers=True)
|
||||
|
||||
# Output summary to file if specified
|
||||
if output:
|
||||
output_path = Path(output)
|
||||
output_path.write_text(table + "\n", encoding="utf-8")
|
||||
else:
|
||||
print(table + "\n")
|
||||
|
||||
|
||||
# TODO: Run this as a test in CI?
|
||||
# NOTE: At v0.140 there was ~800 URL instances total, ~300 unique URLs, and the script took 4 min.
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Validate links and fragments in the codebase.")
|
||||
parser.add_argument(
|
||||
"-o", "--output", type=str, help="Output summary table to file (suppress stdout except errors)"
|
||||
)
|
||||
parser.add_argument("--rewrite", action="store_true", help="Rewrite URLs using URL_REWRITE_MAP and update files")
|
||||
parser.add_argument(
|
||||
"--dry-run", action="store_true", help="Show what would be changed by --rewrite, but do not write files"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(os.getcwd())
|
||||
spec = load_gitignore(root)
|
||||
|
||||
files = find_files(root, spec)
|
||||
print(f"Scanning {len(files)} files...")
|
||||
|
||||
all_urls: List[Tuple[str, int, str, str]] = []
|
||||
for f in files:
|
||||
if is_binary_file(f):
|
||||
continue
|
||||
all_urls.extend(extract_urls_from_file(f))
|
||||
|
||||
# HTTP request and caching step
|
||||
url_cache = validate_urls(all_urls)
|
||||
|
||||
# --- URL rewriting logic ---
|
||||
if args.rewrite:
|
||||
# Group by file for efficient rewriting
|
||||
file_to_lines: Dict[str, List[str]] = {}
|
||||
for f in files:
|
||||
try:
|
||||
with open(f, encoding="utf-8", errors="replace") as fh:
|
||||
file_to_lines[str(f)] = fh.readlines()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
rewrites = []
|
||||
for file, lineno, line, url in all_urls:
|
||||
new_url, mapping_key = rewrite_url(url)
|
||||
if not new_url or new_url == url:
|
||||
continue
|
||||
|
||||
# Rewrite in memory, so we can have dry-run mode
|
||||
lines = file_to_lines[file]
|
||||
idx = lineno - 1
|
||||
old_line = lines[idx]
|
||||
new_line = old_line.replace(url, new_url)
|
||||
if old_line != new_line:
|
||||
lines[idx] = new_line
|
||||
rewrites.append((file, lineno, url, new_url, mapping_key))
|
||||
|
||||
# Write back or dry-run
|
||||
if args.dry_run:
|
||||
for file, lineno, old, new, _ in rewrites:
|
||||
print(f"[DRY-RUN] {file}#{lineno}: {old} -> {new}")
|
||||
else:
|
||||
for file, _, _, _, _ in rewrites:
|
||||
# Write only once per file
|
||||
lines = file_to_lines[file]
|
||||
Path(file).write_text("".join(lines), encoding="utf-8")
|
||||
for file, lineno, old, new, _ in rewrites:
|
||||
print(f"[REWRITE] {file}#{lineno}: {old} -> {new}")
|
||||
|
||||
return # After rewriting, skip error reporting
|
||||
|
||||
# --- Categorize the results / errors ---
|
||||
errors = []
|
||||
for file, lineno, line, url in all_urls:
|
||||
base_url = get_base_url(url)
|
||||
fragment = url.split("#", 1)[1] if "#" in url else None
|
||||
cache_val = url_cache.get(base_url)
|
||||
|
||||
if cache_val == "SKIPPED":
|
||||
continue
|
||||
elif cache_val == "INVALID_URL":
|
||||
errors.append((file, lineno, "INVALID", url, "Invalid URL format"))
|
||||
continue
|
||||
elif isinstance(cache_val, Exception):
|
||||
errors.append((file, lineno, "ERROR", url, str(cache_val)))
|
||||
continue
|
||||
elif hasattr(cache_val, "status_code") and getattr(cache_val, "status_code", 0) != 200:
|
||||
errors.append((file, lineno, "ERROR_HTTP", url, f"Status {getattr(cache_val, 'status_code', '?')}"))
|
||||
continue
|
||||
elif fragment and hasattr(cache_val, "text"):
|
||||
content_type = cache_val.headers.get("Content-Type", "")
|
||||
if "html" not in content_type:
|
||||
errors.append((file, lineno, "ERROR_FRAGMENT", url, "Not HTML content"))
|
||||
continue
|
||||
if not check_fragment_in_html(cache_val.text, fragment):
|
||||
errors.append((file, lineno, "ERROR_FRAGMENT", url, f"Fragment '#{fragment}' not found"))
|
||||
|
||||
if not errors:
|
||||
print("\nAll links and fragments are valid!")
|
||||
return
|
||||
|
||||
# Format the errors into a table
|
||||
output_summary(errors, args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -188,7 +188,7 @@ class ComponentsSettings(NamedTuple):
|
|||
Defaults to `[Path(settings.BASE_DIR) / "components"]`. That is, the root `components/` app.
|
||||
|
||||
Directories must be full paths, same as with
|
||||
[STATICFILES_DIRS](https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-STATICFILES_DIRS).
|
||||
[STATICFILES_DIRS](https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS).
|
||||
|
||||
These locations are searched during [autodiscovery](../../concepts/fundamentals/autodiscovery),
|
||||
or when you [define HTML, JS, or CSS as separate files](../../concepts/fundamentals/defining_js_css_html_files).
|
||||
|
@ -238,10 +238,10 @@ class ComponentsSettings(NamedTuple):
|
|||
|
||||
cache: Optional[str] = None
|
||||
"""
|
||||
Name of the [Django cache](https://docs.djangoproject.com/en/5.1/topics/cache/)
|
||||
Name of the [Django cache](https://docs.djangoproject.com/en/5.2/topics/cache/)
|
||||
to be used for storing component's JS and CSS files.
|
||||
|
||||
If `None`, a [`LocMemCache`](https://docs.djangoproject.com/en/5.1/topics/cache/#local-memory-caching)
|
||||
If `None`, a [`LocMemCache`](https://docs.djangoproject.com/en/5.2/topics/cache/#local-memory-caching)
|
||||
is used with default settings.
|
||||
|
||||
Defaults to `None`.
|
||||
|
@ -359,7 +359,7 @@ class ComponentsSettings(NamedTuple):
|
|||
```
|
||||
|
||||
This would be the equivalent of importing these modules from within Django's
|
||||
[`AppConfig.ready()`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready):
|
||||
[`AppConfig.ready()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready):
|
||||
|
||||
```python
|
||||
class MyAppConfig(AppConfig):
|
||||
|
@ -441,12 +441,12 @@ class ComponentsSettings(NamedTuple):
|
|||
[`COMPONENTS.dirs`](../settings/#django_components.app_settings.ComponentsSettings.dirs)
|
||||
or
|
||||
[`COMPONENTS.app_dirs`](../settings/#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||
are treated as [static files](https://docs.djangoproject.com/en/5.1/howto/static-files/).
|
||||
are treated as [static files](https://docs.djangoproject.com/en/5.2/howto/static-files/).
|
||||
|
||||
If a file is matched against any of the patterns, it's considered a static file. Such files are collected
|
||||
when running [`collectstatic`](https://docs.djangoproject.com/en/5.1/ref/contrib/staticfiles/#collectstatic),
|
||||
when running [`collectstatic`](https://docs.djangoproject.com/en/5.2/ref/contrib/staticfiles/#collectstatic),
|
||||
and can be accessed under the
|
||||
[static file endpoint](https://docs.djangoproject.com/en/5.1/ref/settings/#static-url).
|
||||
[static file endpoint](https://docs.djangoproject.com/en/5.2/ref/settings/#static-url).
|
||||
|
||||
You can also pass in compiled regexes ([`re.Pattern`](https://docs.python.org/3/library/re.html#re.Pattern))
|
||||
for more advanced patterns.
|
||||
|
@ -486,7 +486,7 @@ class ComponentsSettings(NamedTuple):
|
|||
[`COMPONENTS.dirs`](../settings/#django_components.app_settings.ComponentsSettings.dirs)
|
||||
or
|
||||
[`COMPONENTS.app_dirs`](../settings/#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||
will NEVER be treated as [static files](https://docs.djangoproject.com/en/5.1/howto/static-files/).
|
||||
will NEVER be treated as [static files](https://docs.djangoproject.com/en/5.2/howto/static-files/).
|
||||
|
||||
If a file is matched against any of the patterns, it will never be considered a static file,
|
||||
even if the file matches a pattern in
|
||||
|
@ -589,7 +589,7 @@ class ComponentsSettings(NamedTuple):
|
|||
|
||||
Defaults to `128`.
|
||||
|
||||
Each time a [Django template](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template)
|
||||
Each time a [Django template](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template)
|
||||
is rendered, it is cached to a global in-memory cache (using Python's
|
||||
[`lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache)
|
||||
decorator). This speeds up the next render of the component.
|
||||
|
|
|
@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Type
|
|||
|
||||
from django_components.component import all_components
|
||||
from django_components.util.command import CommandArg, ComponentCommand
|
||||
from django_components.util.misc import get_import_path, get_module_info
|
||||
from django_components.util.misc import format_as_ascii_table, get_import_path, get_module_info
|
||||
|
||||
|
||||
# This descriptor generates the list of command arguments (e.g. `--all`), such that
|
||||
|
@ -87,58 +87,6 @@ class ListCommand(ComponentCommand):
|
|||
print(table)
|
||||
|
||||
|
||||
def format_as_ascii_table(data: List[Dict[str, Any]], headers: List[str], include_headers: bool = True) -> str:
|
||||
"""
|
||||
Format a list of dictionaries as an ASCII table.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
data = [
|
||||
{"name": "ProjectPage", "full_name": "project.pages.project.ProjectPage", "path": "./project/pages/project"},
|
||||
{"name": "ProjectDashboard", "full_name": "project.components.dashboard.ProjectDashboard", "path": "./project/components/dashboard"},
|
||||
{"name": "ProjectDashboardAction", "full_name": "project.components.dashboard_action.ProjectDashboardAction", "path": "./project/components/dashboard_action"},
|
||||
]
|
||||
headers = ["name", "full_name", "path"]
|
||||
print(format_as_ascii_table(data, headers))
|
||||
```
|
||||
|
||||
Which prints:
|
||||
|
||||
```txt
|
||||
name full_name path
|
||||
==================================================================================================
|
||||
ProjectPage project.pages.project.ProjectPage ./project/pages/project
|
||||
ProjectDashboard project.components.dashboard.ProjectDashboard ./project/components/dashboard
|
||||
ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAction ./project/components/dashboard_action
|
||||
```
|
||||
""" # noqa: E501
|
||||
# Calculate the width of each column
|
||||
column_widths = {header: len(header) for header in headers}
|
||||
for row in data:
|
||||
for header in headers:
|
||||
row_value = str(row.get(header, ""))
|
||||
column_widths[header] = max(column_widths[header], len(row_value))
|
||||
|
||||
# Create the header row
|
||||
header_row = " ".join(f"{header:<{column_widths[header]}}" for header in headers)
|
||||
separator = "=" * len(header_row)
|
||||
|
||||
# Create the data rows
|
||||
data_rows = []
|
||||
for row in data:
|
||||
row_values = [str(row.get(header, "")) for header in headers]
|
||||
data_row = " ".join(f"{value:<{column_widths[header]}}" for value, header in zip(row_values, headers))
|
||||
data_rows.append(data_row)
|
||||
|
||||
# Combine all parts into the final table
|
||||
if include_headers:
|
||||
table = "\n".join([header_row, separator] + data_rows)
|
||||
else:
|
||||
table = "\n".join(data_rows)
|
||||
return table
|
||||
|
||||
|
||||
class ComponentListCommand(ListCommand):
|
||||
"""
|
||||
List all components.
|
||||
|
|
|
@ -522,7 +522,7 @@ class Component(metaclass=ComponentMeta):
|
|||
Alias for [`template_file`](../api#django_components.Component.template_file).
|
||||
|
||||
For historical reasons, django-components used `template_name` to align with Django's
|
||||
[TemplateView](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.TemplateView).
|
||||
[TemplateView](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.TemplateView).
|
||||
|
||||
`template_file` was introduced to align with `js/js_file` and `css/css_file`.
|
||||
|
||||
|
@ -640,7 +640,7 @@ class Component(metaclass=ComponentMeta):
|
|||
- `args`: Positional arguments passed to the component.
|
||||
- `kwargs`: Keyword arguments passed to the component.
|
||||
- `slots`: Slots passed to the component.
|
||||
- `context`: [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)
|
||||
- `context`: [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
|
||||
used for rendering the component template.
|
||||
|
||||
**Pass-through kwargs:**
|
||||
|
@ -911,7 +911,7 @@ class Component(metaclass=ComponentMeta):
|
|||
- `args`: Positional arguments passed to the component.
|
||||
- `kwargs`: Keyword arguments passed to the component.
|
||||
- `slots`: Slots passed to the component.
|
||||
- `context`: [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)
|
||||
- `context`: [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
|
||||
used for rendering the component template.
|
||||
|
||||
**Pass-through kwargs:**
|
||||
|
@ -1182,7 +1182,7 @@ class Component(metaclass=ComponentMeta):
|
|||
- `args`: Positional arguments passed to the component.
|
||||
- `kwargs`: Keyword arguments passed to the component.
|
||||
- `slots`: Slots passed to the component.
|
||||
- `context`: [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)
|
||||
- `context`: [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
|
||||
used for rendering the component template.
|
||||
|
||||
**Pass-through kwargs:**
|
||||
|
@ -1383,7 +1383,7 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
media_class: Type[MediaCls] = MediaCls
|
||||
"""
|
||||
Set the [Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition)
|
||||
Set the [Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/#assets-as-a-static-definition)
|
||||
that will be instantiated with the JS and CSS media files from
|
||||
[`Component.Media`](../api#django_components.Component.Media).
|
||||
|
||||
|
@ -1409,7 +1409,7 @@ class Component(metaclass=ComponentMeta):
|
|||
Defines JS and CSS media files associated with this component.
|
||||
|
||||
This `Media` class behaves similarly to
|
||||
[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.2/topics/forms/media/#assets-as-a-static-definition):
|
||||
|
||||
- Paths are generally handled as static file paths, and resolved URLs are rendered to HTML with
|
||||
`media_class.render_js()` or `media_class.render_css()`.
|
||||
|
@ -1667,7 +1667,6 @@ class Component(metaclass=ComponentMeta):
|
|||
class MyComponent(Component):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
print(f"Rendering '{self.id}'")
|
||||
return {}
|
||||
|
||||
MyComponent.render()
|
||||
# Rendering 'ab3c4d'
|
||||
|
@ -1713,8 +1712,6 @@ class Component(metaclass=ComponentMeta):
|
|||
footer_slot = self.input.slots["footer"]
|
||||
some_var = self.input.context["some_var"]
|
||||
|
||||
return {}
|
||||
|
||||
rendered = TestComponent.render(
|
||||
kwargs={"variable": "test", "another": 1},
|
||||
args=(123, "str"),
|
||||
|
@ -1756,11 +1753,11 @@ class Component(metaclass=ComponentMeta):
|
|||
@property
|
||||
def request(self) -> Optional[HttpRequest]:
|
||||
"""
|
||||
[HTTPRequest](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest)
|
||||
[HTTPRequest](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest)
|
||||
object passed to this component.
|
||||
|
||||
In regular Django templates, you have to use
|
||||
[`RequestContext`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.RequestContext)
|
||||
[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
|
||||
to pass the `HttpRequest` object to the template.
|
||||
|
||||
But in Components, you can either use `RequestContext`, or pass the `request` object
|
||||
|
@ -1794,30 +1791,30 @@ class Component(metaclass=ComponentMeta):
|
|||
def context_processors_data(self) -> Dict:
|
||||
"""
|
||||
Retrieve data injected by
|
||||
[`context_processors`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#configuring-an-engine).
|
||||
[`context_processors`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#configuring-an-engine).
|
||||
|
||||
This data is also available from within the component's template, without having to
|
||||
return this data from
|
||||
[`get_template_data()`](../api#django_components.Component.get_template_data).
|
||||
|
||||
In regular Django templates, you need to use
|
||||
[`RequestContext`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.RequestContext)
|
||||
[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
|
||||
to apply context processors.
|
||||
|
||||
In Components, the context processors are applied to components either when:
|
||||
|
||||
- The component is rendered with
|
||||
[`RequestContext`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.RequestContext)
|
||||
[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
|
||||
(Regular Django behavior)
|
||||
- The component is rendered with a regular
|
||||
[`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context) (or none),
|
||||
[`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context) (or none),
|
||||
but the `request` kwarg of [`Component.render()`](../api#django_components.Component.render) is set.
|
||||
- The component is nested in another component that matches any of these conditions.
|
||||
|
||||
See
|
||||
[`Component.request`](../api#django_components.Component.request)
|
||||
on how the `request`
|
||||
([HTTPRequest](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest))
|
||||
([HTTPRequest](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest))
|
||||
object is passed to and within the components.
|
||||
|
||||
Raises `RuntimeError` if accessed outside of rendering execution.
|
||||
|
@ -2160,7 +2157,7 @@ class Component(metaclass=ComponentMeta):
|
|||
You can pass
|
||||
[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
|
||||
to the `context` argument, so that the component will gain access to the request object and will use
|
||||
[context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#using-requestcontext).
|
||||
[context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#using-requestcontext).
|
||||
Read more on [Working with HTTP requests](../../concepts/fundamentals/http_request).
|
||||
|
||||
```py
|
||||
|
@ -2833,7 +2830,7 @@ class ComponentNode(BaseNode):
|
|||
### Isolating components
|
||||
|
||||
By default, components behave similarly to Django's
|
||||
[`{% include %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#include),
|
||||
[`{% include %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#include),
|
||||
and the template inside the component has access to the variables defined in the outer template.
|
||||
|
||||
You can selectively isolate a component, using the `only` flag, so that the inner template
|
||||
|
|
|
@ -172,14 +172,14 @@ class ComponentRegistry:
|
|||
|
||||
When you register a component to a registry, behind the scenes the registry
|
||||
automatically adds the component's template tag (e.g. `{% component %}` to
|
||||
the [`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#code-layout).
|
||||
the [`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#code-layout).
|
||||
And the opposite happens when you unregister a component - the tag is removed.
|
||||
|
||||
See [Registering components](../../concepts/advanced/component_registry).
|
||||
|
||||
Args:
|
||||
library (Library, optional): Django\
|
||||
[`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#code-layout)\
|
||||
[`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#code-layout)\
|
||||
associated with this registry. If omitted, the default Library instance from django_components is used.
|
||||
settings (Union[RegistrySettings, Callable[[ComponentRegistry], RegistrySettings]], optional): Configure\
|
||||
how the components registered with this registry will behave when rendered.\
|
||||
|
@ -281,7 +281,7 @@ class ComponentRegistry:
|
|||
@property
|
||||
def library(self) -> Library:
|
||||
"""
|
||||
The template tag [`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#code-layout)
|
||||
The template tag [`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#code-layout)
|
||||
that is associated with the registry.
|
||||
"""
|
||||
# Lazily use the default library if none was passed
|
||||
|
|
|
@ -11,9 +11,9 @@ class TagProtectedError(Exception):
|
|||
The way the [`TagFormatter`](../../concepts/advanced/tag_formatter) works is that,
|
||||
based on which start and end tags are used for rendering components,
|
||||
the [`ComponentRegistry`](../api#django_components.ComponentRegistry) behind the scenes
|
||||
[un-/registers the template tags](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#registering-the-tag)
|
||||
[un-/registers the template tags](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#registering-the-tag)
|
||||
with the associated instance of Django's
|
||||
[`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#code-layout).
|
||||
[`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#code-layout).
|
||||
|
||||
In other words, if I have registered a component `"table"`, and I use the shorthand
|
||||
syntax:
|
||||
|
|
|
@ -340,7 +340,7 @@ class BaseNode(Node, metaclass=NodeMeta):
|
|||
def parse(cls, parser: Parser, token: Token, **kwargs: Any) -> "BaseNode":
|
||||
"""
|
||||
This function is what is passed to Django's `Library.tag()` when
|
||||
[registering the tag](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#registering-the-tag).
|
||||
[registering the tag](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#registering-the-tag).
|
||||
|
||||
In other words, this method is called by Django's template parser when we encounter
|
||||
a tag that matches this node's tag, e.g. `{% component %}` or `{% slot %}`.
|
||||
|
@ -434,7 +434,7 @@ def template_tag(
|
|||
The function MUST accept at least two positional arguments: `node` and `context`
|
||||
|
||||
- `node` is the [`BaseNode`](../api#django_components.BaseNode) instance.
|
||||
- `context` is the [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)
|
||||
- `context` is the [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
|
||||
of the template.
|
||||
|
||||
Any extra parameters defined on this function will be part of the tag's input parameters.
|
||||
|
|
|
@ -21,11 +21,11 @@ def cached_template(
|
|||
|
||||
Args:
|
||||
template_string (str): Template as a string, same as the first argument to Django's\
|
||||
[`Template`](https://docs.djangoproject.com/en/5.1/topics/templates/#template). Required.
|
||||
[`Template`](https://docs.djangoproject.com/en/5.2/topics/templates/#template). Required.
|
||||
template_cls (Type[Template], optional): Specify the Template class that should be instantiated.\
|
||||
Defaults to Django's [`Template`](https://docs.djangoproject.com/en/5.1/topics/templates/#template) class.
|
||||
Defaults to Django's [`Template`](https://docs.djangoproject.com/en/5.2/topics/templates/#template) class.
|
||||
origin (Type[Origin], optional): Sets \
|
||||
[`Template.Origin`](https://docs.djangoproject.com/en/5.1/howto/custom-template-backend/#origin-api-and-3rd-party-integration).
|
||||
[`Template.Origin`](https://docs.djangoproject.com/en/5.2/howto/custom-template-backend/#origin-api-and-3rd-party-integration).
|
||||
name (Type[str], optional): Sets `Template.name`
|
||||
engine (Type[Any], optional): Sets `Template.engine`
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from django_components.provide import ProvideNode
|
|||
from django_components.slots import FillNode, SlotNode
|
||||
|
||||
# NOTE: Variable name `register` is required by Django to recognize this as a template tag library
|
||||
# See https://docs.djangoproject.com/en/dev/howto/custom-template-tags
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/custom-template-tags
|
||||
register = django.template.Library()
|
||||
|
||||
|
||||
|
|
|
@ -243,7 +243,7 @@ class ComponentCommand:
|
|||
Definition of a CLI command.
|
||||
|
||||
This class is based on Python's [`argparse`](https://docs.python.org/3/library/argparse.html)
|
||||
module and Django's [`BaseCommand`](https://docs.djangoproject.com/en/5.1/howto/custom-management-commands/)
|
||||
module and Django's [`BaseCommand`](https://docs.djangoproject.com/en/5.2/howto/custom-management-commands/)
|
||||
class. `ComponentCommand` allows you to define:
|
||||
|
||||
- Command name, description, and help text
|
||||
|
|
|
@ -87,7 +87,7 @@ def get_component_dirs(include_apps: bool = True) -> List[Path]:
|
|||
# Validate and add other values from the config
|
||||
for component_dir in component_dirs:
|
||||
# Consider tuples for STATICFILES_DIRS (See #489)
|
||||
# See https://docs.djangoproject.com/en/5.0/ref/settings/#prefixes-optional
|
||||
# See https://docs.djangoproject.com/en/5.2/ref/settings/#prefixes-optional
|
||||
if isinstance(component_dir, (tuple, list)):
|
||||
component_dir = component_dir[1]
|
||||
try:
|
||||
|
|
|
@ -168,3 +168,55 @@ def format_url(url: str, query: Optional[Dict] = None, fragment: Optional[str] =
|
|||
encoded_qs = parse.urlencode(merged, safe="")
|
||||
|
||||
return parse.urlunsplit(parts._replace(query=encoded_qs, fragment=fragment_enc))
|
||||
|
||||
|
||||
def format_as_ascii_table(data: List[Dict[str, Any]], headers: List[str], include_headers: bool = True) -> str:
|
||||
"""
|
||||
Format a list of dictionaries as an ASCII table.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
data = [
|
||||
{"name": "ProjectPage", "full_name": "project.pages.project.ProjectPage", "path": "./project/pages/project"},
|
||||
{"name": "ProjectDashboard", "full_name": "project.components.dashboard.ProjectDashboard", "path": "./project/components/dashboard"},
|
||||
{"name": "ProjectDashboardAction", "full_name": "project.components.dashboard_action.ProjectDashboardAction", "path": "./project/components/dashboard_action"},
|
||||
]
|
||||
headers = ["name", "full_name", "path"]
|
||||
print(format_as_ascii_table(data, headers))
|
||||
```
|
||||
|
||||
Which prints:
|
||||
|
||||
```txt
|
||||
name full_name path
|
||||
==================================================================================================
|
||||
ProjectPage project.pages.project.ProjectPage ./project/pages/project
|
||||
ProjectDashboard project.components.dashboard.ProjectDashboard ./project/components/dashboard
|
||||
ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAction ./project/components/dashboard_action
|
||||
```
|
||||
""" # noqa: E501
|
||||
# Calculate the width of each column
|
||||
column_widths = {header: len(header) for header in headers}
|
||||
for row in data:
|
||||
for header in headers:
|
||||
row_value = str(row.get(header, ""))
|
||||
column_widths[header] = max(column_widths[header], len(row_value))
|
||||
|
||||
# Create the header row
|
||||
header_row = " ".join(f"{header:<{column_widths[header]}}" for header in headers)
|
||||
separator = "=" * len(header_row)
|
||||
|
||||
# Create the data rows
|
||||
data_rows = []
|
||||
for row in data:
|
||||
row_values = [str(row.get(header, "")) for header in headers]
|
||||
data_row = " ".join(f"{value:<{column_widths[header]}}" for value, header in zip(row_values, headers))
|
||||
data_rows.append(data_row)
|
||||
|
||||
# Combine all parts into the final table
|
||||
if include_headers:
|
||||
table = "\n".join([header_row, separator] + data_rows)
|
||||
else:
|
||||
table = "\n".join(data_rows)
|
||||
return table
|
||||
|
|
|
@ -25,7 +25,7 @@ class URLRoute:
|
|||
Framework-agnostic route definition.
|
||||
|
||||
This is similar to Django's `URLPattern` object created with
|
||||
[`django.urls.path()`](https://docs.djangoproject.com/en/5.1/ref/urls/#path).
|
||||
[`django.urls.path()`](https://docs.djangoproject.com/en/5.2/ref/urls/#path).
|
||||
|
||||
The `URLRoute` must either define a `handler` function or have a list of child routes `children`.
|
||||
If both are defined, an error will be raised.
|
||||
|
|
|
@ -811,7 +811,7 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
# or here: ^
|
||||
if is_next_token(["'", '"', "_("]):
|
||||
# NOTE: Strings may be wrapped in `_()` to allow for translation.
|
||||
# See https://docs.djangoproject.com/en/5.1/topics/i18n/translation/#string-literals-passed-to-tags-and-filters # noqa: E501
|
||||
# See https://docs.djangoproject.com/en/5.2/topics/i18n/translation/#string-literals-passed-to-tags-and-filters # noqa: E501
|
||||
# NOTE 2: We could potentially raise if this token is supposed to be a filter
|
||||
# name (after `|`) and we got a translation or a quoted string instead. But we
|
||||
# leave that up for Django.
|
||||
|
|
|
@ -175,7 +175,7 @@ def djc_test(
|
|||
**Arguments:**
|
||||
|
||||
- `django_settings`: Django settings, a dictionary passed to Django's
|
||||
[`@override_settings`](https://docs.djangoproject.com/en/5.1/topics/testing/tools/#django.test.override_settings).
|
||||
[`@override_settings`](https://docs.djangoproject.com/en/5.2/topics/testing/tools/#django.test.override_settings).
|
||||
The test runs within the context of these overridden settings.
|
||||
|
||||
If `django_settings` contains django-components settings (`COMPONENTS` field), these are merged.
|
||||
|
|
|
@ -62,10 +62,10 @@ class UrlComponent(Component):
|
|||
|
||||
class Media:
|
||||
css = [
|
||||
"https://cdnjs.cloudflare.com/example/style.min.css",
|
||||
"http://cdnjs.cloudflare.com/example/style.min.css",
|
||||
"https://example.com/example/style.min.css",
|
||||
"http://example.com/example/style.min.css",
|
||||
# :// is not a valid URL - will be resolved as static path
|
||||
"://cdnjs.cloudflare.com/example/style.min.css",
|
||||
"://example.com/example/style.min.css",
|
||||
"/path/to/style.css",
|
||||
]
|
||||
js = [
|
||||
|
|
|
@ -4,7 +4,7 @@ ASGI config for sampleproject project.
|
|||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
|
@ -8,7 +8,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
|||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
|
||||
WSGI_APPLICATION = "testserver.wsgi.application"
|
||||
|
||||
|
@ -77,7 +77,7 @@ TEMPLATES = [
|
|||
]
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
|
||||
|
@ -92,7 +92,7 @@ STATICFILES_FINDERS = [
|
|||
]
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
|
@ -102,6 +102,6 @@ DATABASES = {
|
|||
}
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
|
|
@ -4,7 +4,7 @@ WSGI config for testserver project.
|
|||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"__COMMENT__": "This file is used by tests in `test_component_media.py` to test integration with Django's `staticfiles`",
|
||||
"__COMMENT2__": "Under normal conditions, this JSON would be generated by running Django's `collectstatic` with `ManifestStaticFilesStorage`",
|
||||
"__COMMENT3__": "See https://docs.djangoproject.com/en/5.0/ref/contrib/staticfiles/#manifeststaticfilesstorage",
|
||||
"__COMMENT3__": "See https://docs.djangoproject.com/en/5.2/ref/contrib/staticfiles/#manifeststaticfilesstorage",
|
||||
|
||||
"paths": {
|
||||
"calendar/script.js": "calendar/script.e1815e23e0ec.js",
|
||||
|
|
|
@ -85,12 +85,6 @@ class TestComponentMediaCache:
|
|||
<div>Template only component</div>
|
||||
"""
|
||||
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
@register("test_media_no_vars")
|
||||
class TestMediaNoVarsComponent(Component):
|
||||
template = """
|
||||
|
@ -100,12 +94,6 @@ class TestComponentMediaCache:
|
|||
js = "console.log('Hello from JS');"
|
||||
css = ".novars-component { color: blue; }"
|
||||
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
class TestMediaAndVarsComponent(Component):
|
||||
template = """
|
||||
<div>Full component</div>
|
||||
|
|
|
@ -35,7 +35,6 @@ class TestComponentCache:
|
|||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal did_call_get
|
||||
did_call_get = True
|
||||
return {}
|
||||
|
||||
# First render
|
||||
component = TestComponent()
|
||||
|
@ -70,7 +69,6 @@ class TestComponentCache:
|
|||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal did_call_get
|
||||
did_call_get = True
|
||||
return {}
|
||||
|
||||
# First render
|
||||
component = TestComponent()
|
||||
|
@ -199,9 +197,6 @@ class TestComponentCache:
|
|||
# Custom hash method for args and kwargs
|
||||
return "custom-args-and-kwargs"
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
component = TestComponent()
|
||||
component.render(args=(1, 2), kwargs={"key": "value"})
|
||||
|
||||
|
|
|
@ -440,10 +440,10 @@ class TestComponentMedia:
|
|||
from tests.components.glob.glob import UrlComponent
|
||||
rendered = UrlComponent.render()
|
||||
|
||||
assertInHTML('<link href="https://cdnjs.cloudflare.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
|
||||
assertInHTML('<link href="http://cdnjs.cloudflare.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
|
||||
assertInHTML('<link href="https://example.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
|
||||
assertInHTML('<link href="http://example.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
|
||||
# `://` is escaped because Django's `Media.absolute_path()` doesn't consider `://` a valid URL
|
||||
assertInHTML('<link href="%3A//cdnjs.cloudflare.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
|
||||
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)
|
||||
|
@ -460,7 +460,7 @@ class TestMediaPathAsObject:
|
|||
Test that media work with paths defined as instances of classes that define
|
||||
the `__html__` method.
|
||||
|
||||
See https://docs.djangoproject.com/en/5.0/topics/forms/media/#paths-as-objects
|
||||
See https://docs.djangoproject.com/en/5.2/topics/forms/media/#paths-as-objects
|
||||
"""
|
||||
|
||||
# NOTE: @html_safe adds __html__ method from __str__
|
||||
|
@ -745,7 +745,7 @@ class TestMediaStaticfiles:
|
|||
# Configure static files. The dummy files are set up in the `./static_root` dir.
|
||||
# The URL should have path prefix /static/.
|
||||
# NOTE: We don't need STATICFILES_DIRS, because we don't run collectstatic
|
||||
# See https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS
|
||||
# See https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS
|
||||
"STATIC_URL": "static/",
|
||||
"STATIC_ROOT": os.path.join(Path(__file__).resolve().parent, "static_root"),
|
||||
# `django.contrib.staticfiles` MUST be installed for staticfiles resolution to work.
|
||||
|
@ -793,11 +793,11 @@ class TestMediaStaticfiles:
|
|||
# Configure static files. The dummy files are set up in the `./static_root` dir.
|
||||
# The URL should have path prefix /static/.
|
||||
# NOTE: We don't need STATICFILES_DIRS, because we don't run collectstatic
|
||||
# See https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS
|
||||
# See https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS
|
||||
"STATIC_URL": "static/",
|
||||
"STATIC_ROOT": os.path.join(Path(__file__).resolve().parent, "static_root"),
|
||||
# NOTE: STATICFILES_STORAGE is deprecated since 5.1, use STORAGES instead
|
||||
# See https://docs.djangoproject.com/en/5.0/ref/settings/#staticfiles-storage
|
||||
# See https://docs.djangoproject.com/en/5.2/ref/settings/#storages
|
||||
"STORAGES": {
|
||||
# This was NOT changed
|
||||
"default": {
|
||||
|
|
|
@ -561,7 +561,6 @@ class TestContextProcessors:
|
|||
nonlocal inner_request
|
||||
context_processors_data = self.context_processors_data
|
||||
inner_request = self.request
|
||||
return {}
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
|
@ -597,7 +596,6 @@ class TestContextProcessors:
|
|||
nonlocal parent_request
|
||||
context_processors_data = self.context_processors_data
|
||||
parent_request = self.request
|
||||
return {}
|
||||
|
||||
@register("test_child")
|
||||
class TestChildComponent(Component):
|
||||
|
@ -608,7 +606,6 @@ class TestContextProcessors:
|
|||
nonlocal child_request
|
||||
context_processors_data_child = self.context_processors_data
|
||||
child_request = self.request
|
||||
return {}
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
|
@ -645,7 +642,6 @@ class TestContextProcessors:
|
|||
nonlocal parent_request
|
||||
context_processors_data = self.context_processors_data
|
||||
parent_request = self.request
|
||||
return {}
|
||||
|
||||
@register("test_child")
|
||||
class TestChildComponent(Component):
|
||||
|
@ -656,7 +652,6 @@ class TestContextProcessors:
|
|||
nonlocal child_request
|
||||
context_processors_data_child = self.context_processors_data
|
||||
child_request = self.request
|
||||
return {}
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
|
@ -690,7 +685,6 @@ class TestContextProcessors:
|
|||
nonlocal inner_request
|
||||
context_processors_data = self.context_processors_data
|
||||
inner_request = self.request
|
||||
return {}
|
||||
|
||||
request = HttpRequest()
|
||||
request_context = RequestContext(request)
|
||||
|
@ -719,7 +713,6 @@ class TestContextProcessors:
|
|||
nonlocal parent_request
|
||||
context_processors_data = self.context_processors_data
|
||||
parent_request = self.request
|
||||
return {}
|
||||
|
||||
@register("test_child")
|
||||
class TestChildComponent(Component):
|
||||
|
@ -730,7 +723,6 @@ class TestContextProcessors:
|
|||
nonlocal child_request
|
||||
context_processors_data_child = self.context_processors_data
|
||||
child_request = self.request
|
||||
return {}
|
||||
|
||||
request = HttpRequest()
|
||||
request_context = RequestContext(request)
|
||||
|
@ -756,7 +748,6 @@ class TestContextProcessors:
|
|||
nonlocal inner_request
|
||||
context_processors_data = self.context_processors_data
|
||||
inner_request = self.request
|
||||
return {}
|
||||
|
||||
request = HttpRequest()
|
||||
rendered = TestComponent.render(request=request)
|
||||
|
@ -784,7 +775,6 @@ class TestContextProcessors:
|
|||
nonlocal parent_request
|
||||
context_processors_data = self.context_processors_data
|
||||
parent_request = self.request
|
||||
return {}
|
||||
|
||||
@register("test_child")
|
||||
class TestChildComponent(Component):
|
||||
|
@ -795,7 +785,6 @@ class TestContextProcessors:
|
|||
nonlocal child_request
|
||||
context_processors_data_child = self.context_processors_data
|
||||
child_request = self.request
|
||||
return {}
|
||||
|
||||
request = HttpRequest()
|
||||
rendered = TestParentComponent.render(request=request)
|
||||
|
@ -821,7 +810,6 @@ class TestContextProcessors:
|
|||
nonlocal inner_request
|
||||
context_processors_data = self.context_processors_data
|
||||
inner_request = self.request
|
||||
return {}
|
||||
|
||||
rendered = TestComponent.render(context=Context())
|
||||
|
||||
|
@ -844,7 +832,6 @@ class TestContextProcessors:
|
|||
nonlocal inner_request
|
||||
context_processors_data = self.context_processors_data
|
||||
inner_request = self.request
|
||||
return {}
|
||||
|
||||
rendered = TestComponent.render()
|
||||
|
||||
|
@ -867,7 +854,6 @@ class TestContextProcessors:
|
|||
nonlocal inner_request
|
||||
context_processors_data = self.context_processors_data
|
||||
inner_request = self.request
|
||||
return {}
|
||||
|
||||
request = HttpRequest()
|
||||
rendered = TestComponent.render(Context(), request=request)
|
||||
|
@ -906,7 +892,6 @@ class TestContextProcessors:
|
|||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal context_processors_data
|
||||
context_processors_data = self.context_processors_data
|
||||
return {}
|
||||
|
||||
@register("test_child")
|
||||
class TestChildComponent(Component):
|
||||
|
@ -915,7 +900,6 @@ class TestContextProcessors:
|
|||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal context_processors_data_child
|
||||
context_processors_data_child = self.context_processors_data
|
||||
return {}
|
||||
|
||||
request = HttpRequest()
|
||||
TestParentComponent.render(request=request)
|
||||
|
|
|
@ -693,9 +693,6 @@ class TestDependenciesStrategySimple:
|
|||
console.log("Hello");
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
class Media:
|
||||
css = ["style.css", "style2.css"]
|
||||
js = "script2.js"
|
||||
|
@ -715,9 +712,6 @@ class TestDependenciesStrategySimple:
|
|||
console.log("xyz");
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
class Media:
|
||||
css = "xyz1.css"
|
||||
js = "xyz1.js"
|
||||
|
@ -853,9 +847,6 @@ class TestDependenciesStrategyPrepend:
|
|||
console.log("Hello");
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
class Media:
|
||||
css = ["style.css", "style2.css"]
|
||||
js = "script2.js"
|
||||
|
@ -875,9 +866,6 @@ class TestDependenciesStrategyPrepend:
|
|||
console.log("xyz");
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
class Media:
|
||||
css = "xyz1.css"
|
||||
js = "xyz1.js"
|
||||
|
@ -1010,9 +998,6 @@ class TestDependenciesStrategyAppend:
|
|||
console.log("Hello");
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
class Media:
|
||||
css = ["style.css", "style2.css"]
|
||||
js = "script2.js"
|
||||
|
@ -1032,9 +1017,6 @@ class TestDependenciesStrategyAppend:
|
|||
console.log("xyz");
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
class Media:
|
||||
css = "xyz1.css"
|
||||
js = "xyz1.js"
|
||||
|
|
|
@ -55,9 +55,6 @@ class SimpleComponentNested(Component):
|
|||
console.log("Hello");
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
class Media:
|
||||
css = ["style.css", "style2.css"]
|
||||
js = "script2.js"
|
||||
|
@ -78,9 +75,6 @@ class OtherComponent(Component):
|
|||
console.log("xyz");
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
class Media:
|
||||
css = "xyz1.css"
|
||||
js = "xyz1.js"
|
||||
|
@ -91,9 +85,6 @@ class SimpleComponentWithSharedDependency(Component):
|
|||
Variable: <strong>{{ variable }}</strong>
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
class Media:
|
||||
css = ["style.css", "style2.css"]
|
||||
js = ["script.js", "script2.js"]
|
||||
|
|
|
@ -751,7 +751,6 @@ class TestSpreadOperator:
|
|||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal captured
|
||||
captured = args, kwargs
|
||||
return {}
|
||||
|
||||
template_str: types.django_html = (
|
||||
"""
|
||||
|
@ -815,7 +814,6 @@ class TestAggregateKwargs:
|
|||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal captured
|
||||
captured = args, kwargs
|
||||
return {}
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
|
|
|
@ -2709,7 +2709,6 @@ class TestResolver:
|
|||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal captured
|
||||
captured = kwargs
|
||||
return {}
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
|
@ -2774,7 +2773,6 @@ class TestResolver:
|
|||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal captured
|
||||
captured = args, kwargs
|
||||
return {}
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
|
@ -2795,7 +2793,6 @@ class TestResolver:
|
|||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal captured
|
||||
captured = args, kwargs
|
||||
return {}
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
|
|
|
@ -213,9 +213,6 @@ class TestProvideTemplateTag:
|
|||
<div></div>
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% provide "my_provide" key="hi" another=6 %}
|
||||
|
|
|
@ -2411,7 +2411,6 @@ class TestSlotInput:
|
|||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal seen_slots
|
||||
seen_slots = slots
|
||||
return {}
|
||||
|
||||
assert seen_slots == {}
|
||||
|
||||
|
|
2
tox.ini
2
tox.ini
|
@ -1,5 +1,5 @@
|
|||
# This library strives to support all officially supported combinations of Python and Django:
|
||||
# https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django
|
||||
# https://docs.djangoproject.com/en/5.2/faq/install/#what-python-version-can-i-use-with-django
|
||||
# https://devguide.python.org/versions/#versions
|
||||
|
||||
[tox]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue