mirror of
https://github.com/django-components/django-components.git
synced 2025-07-14 20:24:59 +00:00
902 lines
28 KiB
Markdown
902 lines
28 KiB
Markdown
_New in version 0.131_
|
|
|
|
Django-components functionality can be extended with "extensions". Extensions allow for powerful customization and integrations. They can:
|
|
|
|
- Tap into lifecycle events, such as when a component is created, deleted, registered, or unregistered.
|
|
- Add new attributes and methods to the components under an extension-specific nested class.
|
|
- Define custom commands that can be executed via the Django management command interface.
|
|
|
|
## Live examples
|
|
|
|
- [djc-ext-pydantic](https://github.com/django-components/djc-ext-pydantic)
|
|
|
|
## Install extensions
|
|
|
|
Extensions are configured in the Django settings under [`COMPONENTS.extensions`](../../../reference/settings#django_components.app_settings.ComponentsSettings.extensions).
|
|
|
|
Extensions can be set by either as an import string or by passing in a class:
|
|
|
|
```python
|
|
# settings.py
|
|
|
|
class MyExtension(ComponentExtension):
|
|
name = "my_extension"
|
|
|
|
class ComponentConfig(ExtensionComponentConfig):
|
|
...
|
|
|
|
COMPONENTS = ComponentsSettings(
|
|
extensions=[
|
|
MyExtension,
|
|
"another_app.extensions.AnotherExtension",
|
|
"my_app.extensions.ThirdExtension",
|
|
],
|
|
)
|
|
```
|
|
|
|
## Lifecycle hooks
|
|
|
|
Extensions can define methods to hook into lifecycle events, such as:
|
|
|
|
- Component creation or deletion
|
|
- Un/registering a component
|
|
- Creating or deleting a registry
|
|
- Pre-processing data passed to a component on render
|
|
- Post-processing data returned from [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
|
|
and others.
|
|
|
|
See the full list in [Extension Hooks Reference](../../../reference/extension_hooks).
|
|
|
|
## Per-component configuration
|
|
|
|
Each extension has a corresponding nested class within the [`Component`](../../../reference/api#django_components.Component) class. These allow
|
|
to configure the extensions on a per-component basis.
|
|
|
|
E.g.:
|
|
|
|
- `"view"` extension -> [`Component.View`](../../../reference/api#django_components.Component.View)
|
|
- `"cache"` extension -> [`Component.Cache`](../../../reference/api#django_components.Component.Cache)
|
|
- `"defaults"` extension -> [`Component.Defaults`](../../../reference/api#django_components.Component.Defaults)
|
|
|
|
!!! note
|
|
|
|
**Accessing the component instance from inside the nested classes:**
|
|
|
|
Each method of the nested classes has access to the `component` attribute,
|
|
which points to the component instance.
|
|
|
|
```python
|
|
class MyTable(Component):
|
|
class MyExtension:
|
|
def get(self, request):
|
|
# `self.component` points to the instance of `MyTable` Component.
|
|
return self.component.render_to_response(request=request)
|
|
```
|
|
|
|
### Example: Component as View
|
|
|
|
The [Components as Views](../../fundamentals/component_views_urls) feature is actually implemented as an extension
|
|
that is configured by a `View` nested class.
|
|
|
|
You can override the `get()`, `post()`, etc methods to customize the behavior of the component as a view:
|
|
|
|
```python
|
|
class MyTable(Component):
|
|
class View:
|
|
def get(self, request):
|
|
return self.component_class.render_to_response(request=request)
|
|
|
|
def post(self, request):
|
|
return self.component_class.render_to_response(request=request)
|
|
|
|
...
|
|
```
|
|
|
|
<!-- TODO - LINK TO IT ONCE RELEASED -->
|
|
### Example: Storybook integration
|
|
|
|
The Storybook integration (work in progress) is an extension that is configured by a `Storybook` nested class.
|
|
|
|
You can override methods such as `title`, `parameters`, etc, to customize how to generate a Storybook
|
|
JSON file from the component.
|
|
|
|
```python
|
|
class MyTable(Component):
|
|
class Storybook:
|
|
def title(self):
|
|
return self.component_cls.__name__
|
|
|
|
def parameters(self) -> Parameters:
|
|
return {
|
|
"server": {
|
|
"id": self.component_cls.__name__,
|
|
}
|
|
}
|
|
|
|
def stories(self) -> List[StoryAnnotations]:
|
|
return []
|
|
|
|
...
|
|
```
|
|
|
|
### Extension defaults
|
|
|
|
Extensions are incredibly flexible, but configuring the same extension for every component can be a pain.
|
|
|
|
For this reason, django-components allows for extension defaults. This is like setting the extension config for every component.
|
|
|
|
To set extension defaults, use the [`COMPONENTS.extensions_defaults`](../../../reference/settings#django_components.app_settings.ComponentsSettings.extensions_defaults) setting.
|
|
|
|
The `extensions_defaults` setting is a dictionary where the key is the extension name and the value is a dictionary of config attributes:
|
|
|
|
```python
|
|
COMPONENTS = ComponentsSettings(
|
|
extensions=[
|
|
"my_extension.MyExtension",
|
|
"storybook.StorybookExtension",
|
|
],
|
|
extensions_defaults={
|
|
"my_extension": {
|
|
"key": "value",
|
|
},
|
|
"view": {
|
|
"public": True,
|
|
},
|
|
"cache": {
|
|
"ttl": 60,
|
|
},
|
|
"storybook": {
|
|
"title": lambda self: self.component_cls.__name__,
|
|
},
|
|
},
|
|
)
|
|
```
|
|
|
|
Which is equivalent to setting the following for every component:
|
|
|
|
```python
|
|
class MyTable(Component):
|
|
class MyExtension:
|
|
key = "value"
|
|
|
|
class View:
|
|
public = True
|
|
|
|
class Cache:
|
|
ttl = 60
|
|
|
|
class Storybook:
|
|
def title(self):
|
|
return self.component_cls.__name__
|
|
```
|
|
|
|
!!! info
|
|
|
|
If you define an attribute as a function, it is like defining a method on the extension class.
|
|
|
|
E.g. in the example above, `title` is a method on the `Storybook` extension class.
|
|
|
|
As the name suggests, these are defaults, and so you can still selectively override them on a per-component basis:
|
|
|
|
```python
|
|
class MyTable(Component):
|
|
class View:
|
|
public = False
|
|
```
|
|
|
|
### Extensions in component instances
|
|
|
|
Above, we've configured extensions `View` and `Storybook` for the `MyTable` component.
|
|
|
|
You can access the instances of these extension classes in the component instance.
|
|
|
|
Extensions are available under their names (e.g. `self.view`, `self.storybook`).
|
|
|
|
For example, the View extension is available as `self.view`:
|
|
|
|
```python
|
|
class MyTable(Component):
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
# `self.view` points to the instance of `View` extension.
|
|
return {
|
|
"view": self.view,
|
|
}
|
|
```
|
|
|
|
And the Storybook extension is available as `self.storybook`:
|
|
|
|
```python
|
|
class MyTable(Component):
|
|
def get_template_data(self, args, kwargs, slots, context):
|
|
# `self.storybook` points to the instance of `Storybook` extension.
|
|
return {
|
|
"title": self.storybook.title(),
|
|
}
|
|
```
|
|
|
|
## Writing extensions
|
|
|
|
Creating extensions in django-components involves defining a class that inherits from
|
|
[`ComponentExtension`](../../../reference/api/#django_components.ComponentExtension).
|
|
This class can implement various lifecycle hooks and define new attributes or methods to be added to components.
|
|
|
|
### Extension class
|
|
|
|
To create an extension, define a class that inherits from [`ComponentExtension`](../../../reference/api/#django_components.ComponentExtension)
|
|
and implement the desired hooks.
|
|
|
|
- Each extension MUST have a `name` attribute. The name MUST be a valid Python identifier.
|
|
- The extension may implement any of the [hook methods](../../../reference/extension_hooks).
|
|
|
|
Each hook method receives a context object with relevant data.
|
|
|
|
- Extension may own [URLs](#extension-urls) or [CLI commands](#extension-commands).
|
|
|
|
```python
|
|
from django_components.extension import ComponentExtension, OnComponentClassCreatedContext
|
|
|
|
class MyExtension(ComponentExtension):
|
|
name = "my_extension"
|
|
|
|
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
|
# Custom logic for when a component class is created
|
|
ctx.component_cls.my_attr = "my_value"
|
|
```
|
|
|
|
!!! warning
|
|
|
|
The `name` attribute MUST be unique across all extensions.
|
|
|
|
Moreover, the `name` attribute MUST NOT conflict with existing Component class API.
|
|
|
|
So if you name an extension `render`, it will conflict with the [`render()`](../../../reference/api/#django_components.Component.render) method of the `Component` class.
|
|
|
|
### Component config
|
|
|
|
In previous sections we've seen the `View` and `Storybook` extensions classes that were nested
|
|
within the [`Component`](../../../reference/api/#django_components.Component) class:
|
|
|
|
```python
|
|
class MyComponent(Component):
|
|
class View:
|
|
...
|
|
|
|
class Storybook:
|
|
...
|
|
```
|
|
|
|
These can be understood as component-specific overrides or configuration.
|
|
|
|
Whether it's `Component.View` or `Component.Storybook`, their respective extensions
|
|
defined how these nested classes will behave.
|
|
|
|
For example, the View extension defines the API that users may override in `ViewExtension.ComponentConfig`:
|
|
|
|
```python
|
|
from django_components.extension import ComponentExtension, ExtensionComponentConfig
|
|
|
|
class ViewExtension(ComponentExtension):
|
|
name = "view"
|
|
|
|
# The default behavior of the `View` extension class.
|
|
class ComponentConfig(ExtensionComponentConfig):
|
|
def get(self, request):
|
|
raise NotImplementedError("You must implement the `get` method.")
|
|
|
|
def post(self, request):
|
|
raise NotImplementedError("You must implement the `post` method.")
|
|
|
|
...
|
|
```
|
|
|
|
In any component that then defines a nested `Component.View` extension class, the resulting `View` class
|
|
will actually subclass from the `ViewExtension.ComponentConfig` class.
|
|
|
|
In other words, when you define a component like this:
|
|
|
|
```python
|
|
class MyTable(Component):
|
|
class View:
|
|
def get(self, request):
|
|
# Do something
|
|
...
|
|
```
|
|
|
|
Behind the scenes it is as if you defined the following:
|
|
|
|
```python
|
|
class MyTable(Component):
|
|
class View(ViewExtension.ComponentConfig):
|
|
def get(self, request):
|
|
# Do something
|
|
...
|
|
```
|
|
|
|
!!! warning
|
|
|
|
When writing an extension, the `ComponentConfig` MUST subclass the base class [`ExtensionComponentConfig`](../../../reference/api/#django_components.ExtensionComponentConfig).
|
|
|
|
This base class ensures that the extension class will have access to the component instance.
|
|
|
|
### Install your extension
|
|
|
|
Once the extension is defined, it needs to be installed in the Django settings to be used by the application.
|
|
|
|
Extensions can be given either as an extension class, or its import string:
|
|
|
|
```python
|
|
# settings.py
|
|
COMPONENTS = {
|
|
"extensions": [
|
|
"my_app.extensions.MyExtension",
|
|
],
|
|
}
|
|
```
|
|
|
|
Or by reference:
|
|
|
|
```python
|
|
# settings.py
|
|
from my_app.extensions import MyExtension
|
|
|
|
COMPONENTS = {
|
|
"extensions": [
|
|
MyExtension,
|
|
],
|
|
}
|
|
```
|
|
|
|
### Full example: Custom logging extension
|
|
|
|
To tie it all together, here's an example of a custom logging extension that logs when components are created, deleted, or rendered:
|
|
|
|
- Each component can specify which color to use for the logging by setting `Component.ColorLogger.color`.
|
|
- The extension will log the component name and color when the component is created, deleted, or rendered.
|
|
|
|
```python
|
|
from django_components.extension import (
|
|
ComponentExtension,
|
|
ExtensionComponentConfig,
|
|
OnComponentClassCreatedContext,
|
|
OnComponentClassDeletedContext,
|
|
OnComponentInputContext,
|
|
)
|
|
|
|
|
|
class ColorLoggerExtension(ComponentExtension):
|
|
name = "color_logger"
|
|
|
|
# All `Component.ColorLogger` classes will inherit from this class.
|
|
class ComponentConfig(ExtensionComponentConfig):
|
|
color: str
|
|
|
|
# These hooks don't have access to the Component instance,
|
|
# only to the Component class, so we access the color
|
|
# as `Component.ColorLogger.color`.
|
|
def on_component_class_created(self, ctx: OnComponentClassCreatedContext):
|
|
log.info(
|
|
f"Component {ctx.component_cls} created.",
|
|
color=ctx.component_cls.ColorLogger.color,
|
|
)
|
|
|
|
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext):
|
|
log.info(
|
|
f"Component {ctx.component_cls} deleted.",
|
|
color=ctx.component_cls.ColorLogger.color,
|
|
)
|
|
|
|
# This hook has access to the Component instance, so we access the color
|
|
# as `self.component.color_logger.color`.
|
|
def on_component_input(self, ctx: OnComponentInputContext):
|
|
log.info(
|
|
f"Rendering component {ctx.component_cls}.",
|
|
color=ctx.component.color_logger.color,
|
|
)
|
|
```
|
|
|
|
To use the `ColorLoggerExtension`, add it to your settings:
|
|
|
|
```python
|
|
# settings.py
|
|
COMPONENTS = {
|
|
"extensions": [
|
|
ColorLoggerExtension,
|
|
],
|
|
}
|
|
```
|
|
|
|
Once installed, in any component, you can define a `ColorLogger` attribute:
|
|
|
|
```python
|
|
class MyComponent(Component):
|
|
class ColorLogger:
|
|
color = "red"
|
|
```
|
|
|
|
This will log the component name and color when the component is created, deleted, or rendered.
|
|
|
|
### Utility functions
|
|
|
|
django-components provides a few utility functions to help with writing extensions:
|
|
|
|
- [`all_components()`](../../../reference/api#django_components.all_components) - returns a list of all created component classes.
|
|
- [`all_registries()`](../../../reference/api#django_components.all_registries) - returns a list of all created registry instances.
|
|
|
|
### Access component class
|
|
|
|
You can access the owner [`Component`](../../../reference/api/#django_components.Component) class (`MyTable`) from within methods
|
|
of the extension class (`MyExtension`) by using
|
|
the [`component_cls`](../../../reference/api/#django_components.ExtensionComponentConfig.component_cls) attribute:
|
|
|
|
```py
|
|
class MyTable(Component):
|
|
class MyExtension:
|
|
def some_method(self):
|
|
print(self.component_cls)
|
|
```
|
|
|
|
Here is how the `component_cls` attribute may be used with our `ColorLogger`
|
|
extension shown above:
|
|
|
|
```python
|
|
class ColorLoggerComponentConfig(ExtensionComponentConfig):
|
|
color: str
|
|
|
|
def log(self, msg: str) -> None:
|
|
print(f"{self.component_cls.__name__}: {msg}")
|
|
|
|
|
|
class ColorLoggerExtension(ComponentExtension):
|
|
name = "color_logger"
|
|
|
|
# All `Component.ColorLogger` classes will inherit from this class.
|
|
ComponentConfig = ColorLoggerComponentConfig
|
|
```
|
|
|
|
### Pass slot metadata
|
|
|
|
When a slot is passed to a component, it is copied, so that the original slot is not modified
|
|
with rendering metadata.
|
|
|
|
Therefore, don't use slot's identity to associate metadata with the slot:
|
|
|
|
```py
|
|
# ❌ Don't do this:
|
|
slots_cache = {}
|
|
|
|
class LoggerExtension(ComponentExtension):
|
|
name = "logger"
|
|
|
|
def on_component_input(self, ctx: OnComponentInputContext):
|
|
for slot in ctx.component.slots.values():
|
|
slots_cache[id(slot)] = {"some": "metadata"}
|
|
```
|
|
|
|
Instead, use the [`Slot.extra`](../../../reference/api#django_components.Slot.extra) attribute,
|
|
which is copied from the original slot:
|
|
|
|
```python
|
|
# ✅ Do this:
|
|
class LoggerExtension(ComponentExtension):
|
|
name = "logger"
|
|
|
|
# Save component-level logger settings for each slot when a component is rendered.
|
|
def on_component_input(self, ctx: OnComponentInputContext):
|
|
for slot in ctx.component.slots.values():
|
|
slot.extra["logger"] = ctx.component.logger
|
|
|
|
# Then, when a fill is rendered with `{% slot %}`, we can access the logger settings
|
|
# from the slot's metadata.
|
|
def on_slot_rendered(self, ctx: OnSlotRenderedContext):
|
|
logger = ctx.slot.extra["logger"]
|
|
logger.log(...)
|
|
```
|
|
|
|
## Extension commands
|
|
|
|
Extensions in django-components can define custom commands that can be executed via the Django management command interface. This allows for powerful automation and customization capabilities.
|
|
|
|
For example, if you have an extension that defines a command that prints "Hello world", you can run the command with:
|
|
|
|
```bash
|
|
python manage.py components ext run my_ext hello
|
|
```
|
|
|
|
Where:
|
|
|
|
- `python manage.py components` - is the Django entrypoint
|
|
- `ext run` - is the subcommand to run extension commands
|
|
- `my_ext` - is the extension name
|
|
- `hello` - is the command name
|
|
|
|
### Define commands
|
|
|
|
To define a command, subclass from [`ComponentCommand`](../../../reference/extension_commands#django_components.ComponentCommand).
|
|
This subclass should define:
|
|
|
|
- `name` - the command's name
|
|
- `help` - the command's help text
|
|
- `handle` - the logic to execute when the command is run
|
|
|
|
```python
|
|
from django_components import ComponentCommand, ComponentExtension
|
|
|
|
class HelloCommand(ComponentCommand):
|
|
name = "hello"
|
|
help = "Say hello"
|
|
|
|
def handle(self, *args, **kwargs):
|
|
print("Hello, world!")
|
|
|
|
class MyExt(ComponentExtension):
|
|
name = "my_ext"
|
|
commands = [HelloCommand]
|
|
```
|
|
|
|
### Define arguments and options
|
|
|
|
Commands can accept positional arguments and options (e.g. `--foo`), which are defined using the
|
|
[`arguments`](../../../reference/extension_commands#django_components.ComponentCommand.arguments)
|
|
attribute of the [`ComponentCommand`](../../../reference/extension_commands#django_components.ComponentCommand) class.
|
|
|
|
The arguments are parsed with [`argparse`](https://docs.python.org/3/library/argparse.html)
|
|
into a dictionary of arguments and options. These are then available
|
|
as keyword arguments to the [`handle`](../../../reference/extension_commands#django_components.ComponentCommand.handle)
|
|
method of the command.
|
|
|
|
```python
|
|
from django_components import CommandArg, ComponentCommand, ComponentExtension
|
|
|
|
class HelloCommand(ComponentCommand):
|
|
name = "hello"
|
|
help = "Say hello"
|
|
|
|
arguments = [
|
|
# Positional argument
|
|
CommandArg(
|
|
name_or_flags="name",
|
|
help="The name to say hello to",
|
|
),
|
|
# Optional argument
|
|
CommandArg(
|
|
name_or_flags=["--shout", "-s"],
|
|
action="store_true",
|
|
help="Shout the hello",
|
|
),
|
|
]
|
|
|
|
def handle(self, name: str, *args, **kwargs):
|
|
shout = kwargs.get("shout", False)
|
|
msg = f"Hello, {name}!"
|
|
if shout:
|
|
msg = msg.upper()
|
|
print(msg)
|
|
```
|
|
|
|
You can run the command with arguments and options:
|
|
|
|
```bash
|
|
python manage.py components ext run my_ext hello John --shout
|
|
>>> HELLO, JOHN!
|
|
```
|
|
|
|
!!! note
|
|
|
|
Command definitions are parsed with `argparse`, so you can use all the features of `argparse` to define your arguments and options.
|
|
|
|
See the [argparse documentation](https://docs.python.org/3/library/argparse.html) for more information.
|
|
|
|
django-components defines types as
|
|
[`CommandArg`](../../../reference/extension_commands#django_components.CommandArg),
|
|
[`CommandArgGroup`](../../../reference/extension_commands#django_components.CommandArgGroup),
|
|
[`CommandSubcommand`](../../../reference/extension_commands#django_components.CommandSubcommand),
|
|
and [`CommandParserInput`](../../../reference/extension_commands#django_components.CommandParserInput)
|
|
to help with type checking.
|
|
|
|
!!! note
|
|
|
|
If a command doesn't have the [`handle`](../../../reference/extension_commands#django_components.ComponentCommand.handle)
|
|
method defined, the command will print a help message and exit.
|
|
|
|
### Argument groups
|
|
|
|
Arguments can be grouped using [`CommandArgGroup`](../../../reference/extension_commands#django_components.CommandArgGroup)
|
|
to provide better organization and help messages.
|
|
|
|
Read more on [argparse argument groups](https://docs.python.org/3/library/argparse.html#argument-groups).
|
|
|
|
```python
|
|
from django_components import CommandArg, CommandArgGroup, ComponentCommand, ComponentExtension
|
|
|
|
class HelloCommand(ComponentCommand):
|
|
name = "hello"
|
|
help = "Say hello"
|
|
|
|
# Argument parsing is managed by `argparse`.
|
|
arguments = [
|
|
# Positional argument
|
|
CommandArg(
|
|
name_or_flags="name",
|
|
help="The name to say hello to",
|
|
),
|
|
# Optional argument
|
|
CommandArg(
|
|
name_or_flags=["--shout", "-s"],
|
|
action="store_true",
|
|
help="Shout the hello",
|
|
),
|
|
# When printing the command help message, `--bar` and `--baz`
|
|
# will be grouped under "group bar".
|
|
CommandArgGroup(
|
|
title="group bar",
|
|
description="Group description.",
|
|
arguments=[
|
|
CommandArg(
|
|
name_or_flags="--bar",
|
|
help="Bar description.",
|
|
),
|
|
CommandArg(
|
|
name_or_flags="--baz",
|
|
help="Baz description.",
|
|
),
|
|
],
|
|
),
|
|
]
|
|
|
|
def handle(self, name: str, *args, **kwargs):
|
|
shout = kwargs.get("shout", False)
|
|
msg = f"Hello, {name}!"
|
|
if shout:
|
|
msg = msg.upper()
|
|
print(msg)
|
|
```
|
|
|
|
### Subcommands
|
|
|
|
Extensions can define subcommands, allowing for more complex command structures.
|
|
|
|
Subcommands are defined similarly to root commands, as subclasses of
|
|
[`ComponentCommand`](../../../reference/extension_commands#django_components.ComponentCommand) class.
|
|
|
|
However, instead of defining the subcommands in the
|
|
[`commands`](../../../reference/extension_commands#django_components.ComponentExtension.commands)
|
|
attribute of the extension, you define them in the
|
|
[`subcommands`](../../../reference/extension_commands#django_components.ComponentCommand.subcommands)
|
|
attribute of the parent command:
|
|
|
|
```python
|
|
from django_components import CommandArg, CommandArgGroup, ComponentCommand, ComponentExtension
|
|
|
|
class ChildCommand(ComponentCommand):
|
|
name = "child"
|
|
help = "Child command"
|
|
|
|
def handle(self, *args, **kwargs):
|
|
print("Child command")
|
|
|
|
class ParentCommand(ComponentCommand):
|
|
name = "parent"
|
|
help = "Parent command"
|
|
subcommands = [
|
|
ChildCommand,
|
|
]
|
|
|
|
def handle(self, *args, **kwargs):
|
|
print("Parent command")
|
|
```
|
|
|
|
In this example, we can run two commands.
|
|
|
|
Either the parent command:
|
|
|
|
```bash
|
|
python manage.py components ext run parent
|
|
>>> Parent command
|
|
```
|
|
|
|
Or the child command:
|
|
|
|
```bash
|
|
python manage.py components ext run parent child
|
|
>>> Child command
|
|
```
|
|
|
|
!!! warning
|
|
|
|
Subcommands are independent of the parent command. When a subcommand runs, the parent command is NOT executed.
|
|
|
|
As such, if you want to pass arguments to both the parent and child commands, e.g.:
|
|
|
|
```bash
|
|
python manage.py components ext run parent --foo child --bar
|
|
```
|
|
|
|
You should instead pass all the arguments to the subcommand:
|
|
|
|
```bash
|
|
python manage.py components ext run parent child --foo --bar
|
|
```
|
|
|
|
### Help message
|
|
|
|
By default, all commands will print their help message when run with the `--help` / `-h` flag.
|
|
|
|
```bash
|
|
python manage.py components ext run my_ext --help
|
|
```
|
|
|
|
The help message prints out all the arguments and options available for the command, as well as any subcommands.
|
|
|
|
### Testing commands
|
|
|
|
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
|
|
from django.core.management import call_command
|
|
|
|
call_command('components', 'ext', 'run', 'my_ext', 'hello', '--name', 'John')
|
|
```
|
|
|
|
To capture the output of the command, you can use the [`StringIO`](https://docs.python.org/3/library/io.html#io.StringIO)
|
|
module to redirect the output to a string:
|
|
|
|
```python
|
|
from io import StringIO
|
|
|
|
out = StringIO()
|
|
with patch("sys.stdout", new=out):
|
|
call_command('components', 'ext', 'run', 'my_ext', 'hello', '--name', 'John')
|
|
output = out.getvalue()
|
|
```
|
|
|
|
And to temporarily set the extensions, you can use the [`@djc_test`](../../../reference/testing_api#djc_test) decorator.
|
|
|
|
Thus, a full test example can then look like this:
|
|
|
|
```python
|
|
from io import StringIO
|
|
from unittest.mock import patch
|
|
|
|
from django.core.management import call_command
|
|
from django_components.testing import djc_test
|
|
|
|
@djc_test(
|
|
components_settings={
|
|
"extensions": [
|
|
"my_app.extensions.MyExtension",
|
|
],
|
|
},
|
|
)
|
|
def test_hello_command(self):
|
|
out = StringIO()
|
|
with patch("sys.stdout", new=out):
|
|
call_command('components', 'ext', 'run', 'my_ext', 'hello', '--name', 'John')
|
|
output = out.getvalue()
|
|
assert output == "Hello, John!\n"
|
|
```
|
|
|
|
## Extension URLs
|
|
|
|
Extensions can define custom views and endpoints that can be accessed through the Django application.
|
|
|
|
To define URLs for an extension, set them in the [`urls`](../../../reference/api#django_components.ComponentExtension.urls) attribute of your [`ComponentExtension`](../../../reference/api#django_components.ComponentExtension) class. Each URL is defined using the [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) class, which specifies the path, handler, and optional name for the route.
|
|
|
|
Here's an example of how to define URLs within an extension:
|
|
|
|
```python
|
|
from django_components.extension import ComponentExtension, URLRoute
|
|
from django.http import HttpResponse
|
|
|
|
def my_view(request):
|
|
return HttpResponse("Hello from my extension!")
|
|
|
|
class MyExtension(ComponentExtension):
|
|
name = "my_extension"
|
|
|
|
urls = [
|
|
URLRoute(path="my-view/", handler=my_view, name="my_view"),
|
|
URLRoute(path="another-view/<int:id>/", handler=my_view, name="another_view"),
|
|
]
|
|
```
|
|
|
|
!!! warning
|
|
|
|
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.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.
|
|
|
|
As of v0.131, `URLRoute` objects are directly converted to Django's `URLPattern` and `URLResolver` objects.
|
|
|
|
### URL paths
|
|
|
|
The URLs defined in an extension are available under the path
|
|
|
|
```
|
|
/components/ext/<extension_name>/
|
|
```
|
|
|
|
For example, if you have defined a URL with the path `my-view/<str:name>/` in an extension named `my_extension`, it can be accessed at:
|
|
|
|
```
|
|
/components/ext/my_extension/my-view/john/
|
|
```
|
|
|
|
### Nested URLs
|
|
|
|
Extensions can also define nested URLs to allow for more complex routing structures.
|
|
|
|
To define nested URLs, set the [`children`](../../../reference/extension_urls#django_components.URLRoute.children)
|
|
attribute of the [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) object to
|
|
a list of child [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) objects:
|
|
|
|
```python
|
|
class MyExtension(ComponentExtension):
|
|
name = "my_extension"
|
|
|
|
urls = [
|
|
URLRoute(
|
|
path="parent/",
|
|
name="parent_view",
|
|
children=[
|
|
URLRoute(path="child/<str:name>/", handler=my_view, name="child_view"),
|
|
],
|
|
),
|
|
]
|
|
```
|
|
|
|
In this example, the URL
|
|
|
|
```
|
|
/components/ext/my_extension/parent/child/john/
|
|
```
|
|
|
|
would call the `my_view` handler with the parameter `name` set to `"John"`.
|
|
|
|
### Extra URL data
|
|
|
|
The [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) class is framework-agnostic,
|
|
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.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
|
|
an [`extra`](../../../reference/extension_urls#django_components.URLRoute.extra) attribute,
|
|
which is a dictionary that can be used to pass any extra kwargs to `django.urls.path()`:
|
|
|
|
```python
|
|
URLRoute(
|
|
path="my-view/<str:name>/",
|
|
handler=my_view,
|
|
name="my_view",
|
|
extra={"kwargs": {"foo": "bar"} },
|
|
)
|
|
```
|
|
|
|
Is the same as:
|
|
|
|
```python
|
|
django.urls.path(
|
|
"my-view/<str:name>/",
|
|
view=my_view,
|
|
name="my_view",
|
|
kwargs={"foo": "bar"},
|
|
)
|
|
```
|
|
|
|
because `URLRoute` is converted to Django's route like so:
|
|
|
|
```python
|
|
django.urls.path(
|
|
route.path,
|
|
view=route.handler,
|
|
name=route.name,
|
|
**route.extra,
|
|
)
|
|
```
|