mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 06:18:17 +00:00
refactor: Remove safer_staticfiles, replace STATICFILES_DIRS with COMPONENTS.dirs, support [app]/components
(#652)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
728b4ffad7
commit
e1382d3ccd
34 changed files with 1034 additions and 264 deletions
249
README.md
249
README.md
|
@ -76,6 +76,16 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
|
||||||
|
🚨📢 **Version 0.100**
|
||||||
|
- BREAKING CHANGE:
|
||||||
|
- `django_components.safer_staticfiles` app was removed. It is no longer needed.
|
||||||
|
- Installation changes:
|
||||||
|
- Instead of defining component directories in `STATICFILES_DIRS`, set them to [`COMPONENTS.dirs`](#dirs).
|
||||||
|
- You now must define `STATICFILES_FINDERS`
|
||||||
|
- [See here how to migrate your settings.py](https://github.com/EmilStenstrom/django-components/blob/master/docs/migrating_from_safer_staticfiles.md)
|
||||||
|
- Beside the top-level `/components` directory, you can now define also app-level components dirs, e.g. `[app]/components`
|
||||||
|
(See [`COMPONENTS.app_dirs`](#app_dirs)).
|
||||||
|
|
||||||
**Version 0.97**
|
**Version 0.97**
|
||||||
- Fixed template caching. You can now also manually create cached templates with [`cached_template()`](#template_cache_size---tune-the-template-cache)
|
- Fixed template caching. You can now also manually create cached templates with [`cached_template()`](#template_cache_size---tune-the-template-cache)
|
||||||
- The previously undocumented `get_template` was made private.
|
- The previously undocumented `get_template` was made private.
|
||||||
|
@ -196,14 +206,25 @@ _You are advised to read this section before using django-components in producti
|
||||||
|
|
||||||
Components can be organized however you prefer.
|
Components can be organized however you prefer.
|
||||||
That said, our prefered way is to keep the files of a component close together by bundling them in the same directory.
|
That said, our prefered way is to keep the files of a component close together by bundling them in the same directory.
|
||||||
|
|
||||||
This means that files containing backend logic, such as Python modules and HTML templates, live in the same directory as static files, e.g. JS and CSS.
|
This means that files containing backend logic, such as Python modules and HTML templates, live in the same directory as static files, e.g. JS and CSS.
|
||||||
|
|
||||||
If your are using _django.contrib.staticfiles_ to collect static files, no distinction is made between the different kinds of files.
|
From v0.100 onwards, we keep component files (as defined by [`COMPONENTS.dirs`](#dirs) and [`COMPONENTS.app_dirs`](#app_dirs)) separate from the rest of the static
|
||||||
|
files (defined by `STATICFILES_DIRS`). That way, the Python and HTML files are NOT exposed by the server. Only the static JS, CSS, and [other common formats](#static_files_allowed).
|
||||||
|
|
||||||
|
> NOTE: If you need to expose different file formats, you can configure these with [`COMPONENTS.static_files_allowed`](#static_files_allowed)
|
||||||
|
and [`COMPONENTS.static_files_forbidden`](#static_files_forbidden).
|
||||||
|
|
||||||
|
<!-- # TODO_REMOVE_IN_V1 - Remove mentions of safer_staticfiles in V1 -->
|
||||||
|
#### Static files prior to v0.100
|
||||||
|
|
||||||
|
Prior to v0.100, if your were using _django.contrib.staticfiles_ to collect static files, no distinction was made between the different kinds of files.
|
||||||
|
|
||||||
As a result, your Python code and templates may inadvertently become available on your static file server.
|
As a result, your Python code and templates may inadvertently become available on your static file server.
|
||||||
You probably don't want this, as parts of your backend logic will be exposed, posing a **potential security vulnerability**.
|
You probably don't want this, as parts of your backend logic will be exposed, posing a **potential security vulnerability**.
|
||||||
|
|
||||||
As of _v0.27_, django-components ships with an additional installable app _django_components.**safer_staticfiles**_.
|
From _v0.27_ until _v0.100_, django-components shipped with an additional installable app _django_components.**safer_staticfiles**_.
|
||||||
It is a drop-in replacement for _django.contrib.staticfiles_.
|
It was a drop-in replacement for _django.contrib.staticfiles_.
|
||||||
Its behavior is 100% identical except it ignores .py and .html files, meaning these will not end up on your static files server.
|
Its behavior is 100% identical except it ignores .py and .html files, meaning these will not end up on your static files server.
|
||||||
To use it, add it to INSTALLED_APPS and remove _django.contrib.staticfiles_.
|
To use it, add it to INSTALLED_APPS and remove _django.contrib.staticfiles_.
|
||||||
|
|
||||||
|
@ -235,11 +256,11 @@ For a step-by-step guide on deploying production server with static files,
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Install the app into your environment:
|
1. Install `django_components` into your environment:
|
||||||
|
|
||||||
> `pip install django_components`
|
> `pip install django_components`
|
||||||
|
|
||||||
2. Then add the app into `INSTALLED_APPS` in settings.py
|
2. Load `django_components` into Django by adding it into `INSTALLED_APPS` in settings.py:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
@ -248,13 +269,33 @@ For a step-by-step guide on deploying production server with static files,
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Ensure that `BASE_DIR` setting is defined in settings.py:
|
3. `BASE_DIR` setting is required. Ensure that it is defined in settings.py:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Modify `TEMPLATES` section of settings.py as follows:
|
4. Add / modify [`COMPONENTS.dirs`](#dirs) and / or [`COMPONENTS.app_dirs`](#app_dirs) so django_components knows where to find component HTML, JS and CSS files:
|
||||||
|
|
||||||
|
```python
|
||||||
|
COMPONENTS = {
|
||||||
|
"dirs": [
|
||||||
|
...,
|
||||||
|
os.path.join(BASE_DIR, "components"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If `COMPONENTS.dirs` is omitted, django-components will by default look for a top-level `/components` directory,
|
||||||
|
`{BASE_DIR}/components`.
|
||||||
|
|
||||||
|
Irrespective of `COMPONENTS.dirs`, django_components will also load components from app-level directories, e.g. `my-app/components/`.
|
||||||
|
The directories within apps are configured with [`COMPONENTS.app_dirs`](#app_dirs), and the default is `[app]/components`.
|
||||||
|
|
||||||
|
NOTE: The input to `COMPONENTS.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).
|
||||||
|
|
||||||
|
|
||||||
|
5. Next, to make Django load component HTML files as Django templates, modify `TEMPLATES` section of settings.py as follows:
|
||||||
|
|
||||||
- _Remove `'APP_DIRS': True,`_
|
- _Remove `'APP_DIRS': True,`_
|
||||||
- NOTE: Instead of APP_DIRS, for the same effect, 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)
|
- NOTE: Instead of APP_DIRS, for the same effect, 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)
|
||||||
|
@ -283,19 +324,18 @@ For a step-by-step guide on deploying production server with static files,
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Modify `STATICFILES_DIRS` (or add it if you don't have it) so django can find your static JS and CSS files:
|
6. Lastly, be able to serve the component JS and CSS files as static files, modify `STATICFILES_FINDERS` section of settings.py as follows:
|
||||||
|
|
||||||
```python
|
```py
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_FINDERS = [
|
||||||
...,
|
# Default finders
|
||||||
os.path.join(BASE_DIR, "components"),
|
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||||
]
|
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||||
```
|
# Django components
|
||||||
|
"django_components.finders.ComponentsFileSystemFinder",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
If `STATICFILES_DIRS` is omitted or empty, django-components will by default look for
|
|
||||||
`{BASE_DIR}/components`
|
|
||||||
|
|
||||||
NOTE: The paths in `STATICFILES_DIRS` must be full paths. [See Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#staticfiles-dirs).
|
|
||||||
|
|
||||||
### Optional
|
### Optional
|
||||||
|
|
||||||
|
@ -397,7 +437,7 @@ class Calendar(Component):
|
||||||
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
|
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
|
||||||
# will be automatically found.
|
# will be automatically found.
|
||||||
#
|
#
|
||||||
# `template_name` can be relative to dir where `calendar.py` is, or relative to STATICFILES_DIRS
|
# `template_name` can be relative to dir where `calendar.py` is, or relative to COMPONENTS.dirs
|
||||||
template_name = "template.html"
|
template_name = "template.html"
|
||||||
# Or
|
# Or
|
||||||
def get_template_name(context):
|
def get_template_name(context):
|
||||||
|
@ -409,7 +449,7 @@ class Calendar(Component):
|
||||||
"date": date,
|
"date": date,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Both `css` and `js` can be relative to dir where `calendar.py` is, or relative to STATICFILES_DIRS
|
# Both `css` and `js` can be relative to dir where `calendar.py` is, or relative to COMPONENTS.dirs
|
||||||
class Media:
|
class Media:
|
||||||
css = "style.css"
|
css = "style.css"
|
||||||
js = "script.js"
|
js = "script.js"
|
||||||
|
@ -1230,7 +1270,7 @@ class MyAppConfig(AppConfig):
|
||||||
|
|
||||||
However, there's a simpler way!
|
However, there's a simpler way!
|
||||||
|
|
||||||
By default, the Python files in the `STATICFILES_DIRS` directories are auto-imported in order to auto-register the components.
|
By default, the Python files in the `COMPONENTS.dirs` directories (or app-level `[app]/components/`) are auto-imported in order to auto-register the components.
|
||||||
|
|
||||||
Autodiscovery occurs when Django is loaded, during the `ready` hook of the `apps.py` file.
|
Autodiscovery occurs when Django is loaded, during the `ready` hook of the `apps.py` file.
|
||||||
|
|
||||||
|
@ -1589,17 +1629,17 @@ Consider a component with slot(s). This component may do some processing on the
|
||||||
```py
|
```py
|
||||||
@register("my_comp")
|
@register("my_comp")
|
||||||
class MyComp(Component):
|
class MyComp(Component):
|
||||||
template = """
|
template = """
|
||||||
<div>
|
<div>
|
||||||
{% slot "content" default %}
|
{% slot "content" default %}
|
||||||
input: {{ input }}
|
input: {{ input }}
|
||||||
{% endslot %}
|
{% endslot %}
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_context_data(self, input):
|
def get_context_data(self, input):
|
||||||
processed_input = do_something(input)
|
processed_input = do_something(input)
|
||||||
return {"input": processed_input}
|
return {"input": processed_input}
|
||||||
```
|
```
|
||||||
|
|
||||||
You may want to design a component so that users of your component can still access the `input` variable, so they don't have to recompute it.
|
You may want to design a component so that users of your component can still access the `input` variable, so they don't have to recompute it.
|
||||||
|
@ -1618,17 +1658,17 @@ To pass the data to the `slot` tag, simply pass them as keyword attributes (`key
|
||||||
```py
|
```py
|
||||||
@register("my_comp")
|
@register("my_comp")
|
||||||
class MyComp(Component):
|
class MyComp(Component):
|
||||||
template = """
|
template = """
|
||||||
<div>
|
<div>
|
||||||
{% slot "content" default input=input %}
|
{% slot "content" default input=input %}
|
||||||
input: {{ input }}
|
input: {{ input }}
|
||||||
{% endslot %}
|
{% endslot %}
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_context_data(self, input):
|
def get_context_data(self, input):
|
||||||
processed_input = do_something(input)
|
processed_input = do_something(input)
|
||||||
return {
|
return {
|
||||||
"input": processed_input,
|
"input": processed_input,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -1962,13 +2002,13 @@ Assuming that:
|
||||||
class_from_var = "from-var"
|
class_from_var = "from-var"
|
||||||
|
|
||||||
attrs = {
|
attrs = {
|
||||||
"class": "from-attrs",
|
"class": "from-attrs",
|
||||||
"type": "submit",
|
"type": "submit",
|
||||||
}
|
}
|
||||||
|
|
||||||
defaults = {
|
defaults = {
|
||||||
"class": "from-defaults",
|
"class": "from-defaults",
|
||||||
"role": "button",
|
"role": "button",
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -2873,9 +2913,9 @@ class Calendar(Component):
|
||||||
|
|
||||||
In the example above, the files are defined relative to the directory where `component.py` is.
|
In the example above, the files are defined relative to the directory where `component.py` is.
|
||||||
|
|
||||||
Alternatively, you can specify the file paths relative to the directories set in `STATICFILES_DIRS`.
|
Alternatively, you can specify the file paths relative to the directories set in `COMPONENTS.dirs` or `COMPONENTS.app_dirs`.
|
||||||
|
|
||||||
Assuming that `STATICFILES_DIRS` contains path `[project root]/components`, we can rewrite the example as:
|
Assuming that `COMPONENTS.dirs` contains path `[project root]/components`, we can rewrite the example as:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# In a file [project root]/components/calendar/calendar.py
|
# In a file [project root]/components/calendar/calendar.py
|
||||||
|
@ -2971,7 +3011,7 @@ In the example [above](#supported-types-for-file-paths), you could see that when
|
||||||
|
|
||||||
This is an extension of Django's [Paths as objects](https://docs.djangoproject.com/en/5.0/topics/forms/media/#paths-as-objects) feature, where "safe" strings are taken as is, and accessed only at render time.
|
This is an extension of Django's [Paths as objects](https://docs.djangoproject.com/en/5.0/topics/forms/media/#paths-as-objects) feature, where "safe" strings are taken as is, and accessed only at render time.
|
||||||
|
|
||||||
Because of that, the paths defined as "safe" strings are NEVER resolved, neither relative to component's directory, nor relative to `STATICFILES_DIRS`.
|
Because of that, the paths defined as "safe" strings are NEVER resolved, neither relative to component's directory, nor relative to `COMPONENTS.dirs`.
|
||||||
|
|
||||||
"Safe" strings can be used to lazily resolve a path, or to customize the `<script>` or `<link>` tag for individual paths:
|
"Safe" strings can be used to lazily resolve a path, or to customize the `<script>` or `<link>` tag for individual paths:
|
||||||
|
|
||||||
|
@ -3091,10 +3131,27 @@ Here's overview of all available settings and their defaults:
|
||||||
COMPONENTS = {
|
COMPONENTS = {
|
||||||
"autodiscover": True,
|
"autodiscover": True,
|
||||||
"context_behavior": "django", # "django" | "isolated"
|
"context_behavior": "django", # "django" | "isolated"
|
||||||
|
"dirs": [BASE_DIR / "components"], # Root-level "components" dirs, e.g. `/path/to/proj/components/`
|
||||||
|
"app_dirs": ["components"], # App-level "components" dirs, e.g. `[app]/components/`
|
||||||
"dynamic_component_name": "dynamic",
|
"dynamic_component_name": "dynamic",
|
||||||
"libraries": [], # ["mysite.components.forms", ...]
|
"libraries": [], # ["mysite.components.forms", ...]
|
||||||
"multiline_tags": True,
|
"multiline_tags": True,
|
||||||
"reload_on_template_change": False,
|
"reload_on_template_change": False,
|
||||||
|
"static_files_allowed": [
|
||||||
|
".css",
|
||||||
|
".js",
|
||||||
|
# Images
|
||||||
|
".apng", ".png", ".avif", ".gif", ".jpg",
|
||||||
|
".jpeg", ".jfif", ".pjpeg", ".pjp", ".svg",
|
||||||
|
".webp", ".bmp", ".ico", ".cur", ".tif", ".tiff",
|
||||||
|
# Fonts
|
||||||
|
".eot", ".ttf", ".woff", ".otf", ".svg",
|
||||||
|
],
|
||||||
|
"static_files_forbidden": [
|
||||||
|
".html", ".django", ".dj", ".tpl",
|
||||||
|
# Python files
|
||||||
|
".py", ".pyc",
|
||||||
|
],
|
||||||
"tag_formatter": "django_components.component_formatter",
|
"tag_formatter": "django_components.component_formatter",
|
||||||
"template_cache_size": 128,
|
"template_cache_size": 128,
|
||||||
}
|
}
|
||||||
|
@ -3152,6 +3209,46 @@ COMPONENTS = {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `dirs`
|
||||||
|
|
||||||
|
Specify the directories that contain your components.
|
||||||
|
|
||||||
|
Directories must be full paths, same as with STATICFILES_DIRS.
|
||||||
|
|
||||||
|
These locations are searched during autodiscovery, or when you define HTML, JS, or CSS as
|
||||||
|
a separate file.
|
||||||
|
|
||||||
|
```py
|
||||||
|
COMPONENTS = {
|
||||||
|
"dirs": [BASE_DIR / "components"],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `app_dirs`
|
||||||
|
|
||||||
|
Specify the app-level directories that contain your components.
|
||||||
|
|
||||||
|
Directories must be relative to app, e.g.:
|
||||||
|
|
||||||
|
```py
|
||||||
|
COMPONENTS = {
|
||||||
|
"app_dirs": ["my_comps"], # To search for [app]/my_comps
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
These locations are searched during autodiscovery, or when you define HTML, JS, or CSS as
|
||||||
|
a separate file.
|
||||||
|
|
||||||
|
Each app will be searched for these directories.
|
||||||
|
|
||||||
|
Set to empty list to disable app-level components:
|
||||||
|
|
||||||
|
```py
|
||||||
|
COMPONENTS = {
|
||||||
|
"app_dirs": [],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### `dynamic_component_name`
|
### `dynamic_component_name`
|
||||||
|
|
||||||
By default, the dynamic component is registered under the name `"dynamic"`. In case of a conflict, use this setting to change the name used for the dynamic components.
|
By default, the dynamic component is registered under the name `"dynamic"`. In case of a conflict, use this setting to change the name used for the dynamic components.
|
||||||
|
@ -3172,6 +3269,60 @@ COMPONENTS = {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `static_files_allowed`
|
||||||
|
|
||||||
|
A list of regex patterns (as strings) that define which files within `COMPONENTS.dirs` and `COMPONENTS.app_dirs`
|
||||||
|
are treated as 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`, and can be accessed under the static file endpoint.
|
||||||
|
|
||||||
|
You can also pass in compiled regexes (`re.Pattern`) for more advanced patterns.
|
||||||
|
|
||||||
|
By default, JS, CSS, and common image and font file formats are considered static files:
|
||||||
|
|
||||||
|
```python
|
||||||
|
COMPONENTS = {
|
||||||
|
"static_files_allowed": [
|
||||||
|
"css",
|
||||||
|
"js",
|
||||||
|
# Images
|
||||||
|
".apng", ".png",
|
||||||
|
".avif",
|
||||||
|
".gif",
|
||||||
|
".jpg", ".jpeg", ".jfif", ".pjpeg", ".pjp", # JPEG
|
||||||
|
".svg",
|
||||||
|
".webp", ".bmp",
|
||||||
|
".ico", ".cur", # ICO
|
||||||
|
".tif", ".tiff",
|
||||||
|
# Fonts
|
||||||
|
".eot", ".ttf", ".woff", ".otf", ".svg",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `static_files_forbidden`
|
||||||
|
|
||||||
|
A list of suffixes that define which files within `COMPONENTS.dirs` and `COMPONENTS.app_dirs`
|
||||||
|
will NEVER be treated as 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 [`COMPONENTS.static_files_allowed`](#static_files_allowed).
|
||||||
|
|
||||||
|
Use this setting together with `COMPONENTS.static_files_allowed` for a fine control over what files will be exposed.
|
||||||
|
|
||||||
|
You can also pass in compiled regexes (`re.Pattern`) for more advanced patterns.
|
||||||
|
|
||||||
|
By default, any HTML and Python are considered NOT static files:
|
||||||
|
|
||||||
|
```python
|
||||||
|
COMPONENTS = {
|
||||||
|
"static_files_forbidden": [
|
||||||
|
".html", ".django", ".dj", ".tpl", ".py", ".pyc",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### `template_cache_size` - Tune the template cache
|
### `template_cache_size` - Tune the template cache
|
||||||
|
|
||||||
Each time a template is rendered it is cached to a global in-memory cache (using Python's `lru_cache` decorator). This speeds up the next render of the component. As the same component is often used many times on the same page, these savings add up.
|
Each time a template is rendered it is cached to a global in-memory cache (using Python's `lru_cache` decorator). This speeds up the next render of the component. As the same component is often used many times on the same page, these savings add up.
|
||||||
|
@ -3600,7 +3751,7 @@ You can publish and share your components for others to use. Here are the steps
|
||||||
|
|
||||||
4. Import the components in `apps.py`
|
4. Import the components in `apps.py`
|
||||||
|
|
||||||
Normally, users rely on [autodiscovery](#autodiscovery) and `STATICFILES_DIRS` to load the component files.
|
Normally, users rely on [autodiscovery](#autodiscovery) and `COMPONENTS.dirs` to load the component files.
|
||||||
|
|
||||||
Since you, as the library author, are not in control of the file system, it is recommended to load the components manually.
|
Since you, as the library author, are not in control of the file system, it is recommended to load the components manually.
|
||||||
|
|
||||||
|
|
98
docs/migrating_from_safer_staticfiles.md
Normal file
98
docs/migrating_from_safer_staticfiles.md
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
# Migrating from safer_staticfiles
|
||||||
|
|
||||||
|
This guide is for you if you're upgrating django_components to v0.100 or later
|
||||||
|
from older versions.
|
||||||
|
|
||||||
|
In version 0.100, we changed how components' static JS and CSS files are handled.
|
||||||
|
See more in the ["Static files" section](https://github.com/EmilStenstrom/django-components/tree/master/sampleproject).
|
||||||
|
|
||||||
|
Migration steps:
|
||||||
|
|
||||||
|
1. Remove `django_components.safer_staticfiles` from `INSTALLED_APPS` in your `settings.py`,
|
||||||
|
and replace it with `django.contrib.staticfiles`.
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```py
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
...
|
||||||
|
# "django.contrib.staticfiles", # <-- ADD
|
||||||
|
"django_components",
|
||||||
|
"django_components.safer_staticfiles", # <-- REMOVE
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```py
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
...
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"django_components",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add `STATICFILES_FINDERS` to `settings.py`, and add `django_components.finders.ComponentsFileSystemFinder`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
STATICFILES_FINDERS = [
|
||||||
|
# Default finders
|
||||||
|
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||||
|
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||||
|
# Django components
|
||||||
|
"django_components.finders.ComponentsFileSystemFinder", # <-- ADDED
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add `COMPONENTS.dirs` to `settings.py`.
|
||||||
|
|
||||||
|
If you previously defined `STATICFILES_DIRS`, move
|
||||||
|
only those directories from `STATICFILES_DIRS` that point to components directories, and keep the rest.
|
||||||
|
|
||||||
|
E.g. if you have `STATICFILES_DIRS` like this:
|
||||||
|
|
||||||
|
```py
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
BASE_DIR / "components", # <-- MOVE
|
||||||
|
BASE_DIR / "myapp" / "components", # <-- MOVE
|
||||||
|
BASE_DIR / "assets",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Then first two entries point to components dirs, whereas `/assets` points to non-component static files.
|
||||||
|
In this case move only the first two paths:
|
||||||
|
|
||||||
|
```py
|
||||||
|
COMPONENTS = {
|
||||||
|
"dirs": [
|
||||||
|
BASE_DIR / "components", # <-- MOVED
|
||||||
|
BASE_DIR / "myapp" / "components", # <-- MOVED
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
BASE_DIR / "assets",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Moreover, if you defined app-level component directories in `STATICFILES_DIRS` before,
|
||||||
|
you can now define as a RELATIVE path in `app_dirs`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
COMPONENTS = {
|
||||||
|
"dirs": [
|
||||||
|
# Search top-level "/components/" dir
|
||||||
|
BASE_DIR / "components",
|
||||||
|
],
|
||||||
|
"app_dirs": [
|
||||||
|
# Search "/[app]/components/" dirs
|
||||||
|
"components",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
BASE_DIR / "assets",
|
||||||
|
]
|
||||||
|
```
|
|
@ -54,7 +54,7 @@ even for production environment.
|
||||||
Assuming that you're running the prod server with:
|
Assuming that you're running the prod server with:
|
||||||
|
|
||||||
1. `DEBUG = False` setting
|
1. `DEBUG = False` setting
|
||||||
2. `"django_components.safer_staticfiles"` in the `INSTALLED_APPS`
|
2. `"django.contrib.staticfiles"` in the `INSTALLED_APPS`
|
||||||
|
|
||||||
Then Django will server only JS and CSS files under the `/static/` URL path.
|
Then Django will server only JS and CSS files under the `/static/` URL path.
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ class Calendar(Component):
|
||||||
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
|
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
|
||||||
# will be automatically found.
|
# will be automatically found.
|
||||||
#
|
#
|
||||||
# `template_name` can be relative to dir where `calendar.py` is, or relative to STATICFILES_DIRS
|
# `template_name` can be relative to dir where `calendar.py` is, or relative to COMPONENTS.dirs
|
||||||
template_name = "calendar/calendar.html"
|
template_name = "calendar/calendar.html"
|
||||||
# Or
|
# Or
|
||||||
# def get_template_name(context):
|
# def get_template_name(context):
|
||||||
|
@ -19,10 +19,11 @@ class Calendar(Component):
|
||||||
}
|
}
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
context = {
|
return self.render_to_response(
|
||||||
"date": request.GET.get("date", ""),
|
kwargs={
|
||||||
}
|
"date": request.GET.get("date", ""),
|
||||||
return self.render_to_response(context)
|
},
|
||||||
|
)
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = "calendar/calendar.css"
|
css = "calendar/calendar.css"
|
||||||
|
@ -34,7 +35,7 @@ class CalendarRelative(Component):
|
||||||
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
|
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
|
||||||
# will be automatically found.
|
# will be automatically found.
|
||||||
#
|
#
|
||||||
# `template_name` can be relative to dir where `calendar.py` is, or relative to STATICFILES_DIRS
|
# `template_name` can be relative to dir where `calendar.py` is, or relative to COMPONENTS.dirs
|
||||||
template_name = "calendar.html"
|
template_name = "calendar.html"
|
||||||
# Or
|
# Or
|
||||||
# def get_template_name(context):
|
# def get_template_name(context):
|
||||||
|
|
|
@ -7,8 +7,12 @@ from django_components import Component, register, types
|
||||||
class Greeting(Component):
|
class Greeting(Component):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
slots = {"message": "Hello, world!"}
|
slots = {"message": "Hello, world!"}
|
||||||
context = {"name": request.GET.get("name", "")}
|
return self.render_to_response(
|
||||||
return self.render_to_response(context=context, slots=slots)
|
slots=slots,
|
||||||
|
kwargs={
|
||||||
|
"name": request.GET.get("name", ""),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def get_context_data(self, name, *args, **kwargs) -> Dict[str, Any]:
|
def get_context_data(self, name, *args, **kwargs) -> Dict[str, Any]:
|
||||||
return {"name": name}
|
return {"name": name}
|
||||||
|
|
|
@ -6,7 +6,7 @@ class CalendarNested(Component):
|
||||||
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
|
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
|
||||||
# will be automatically found.
|
# will be automatically found.
|
||||||
#
|
#
|
||||||
# `template_name` can be relative to dir where `calendar.py` is, or relative to STATICFILES_DIRS
|
# `template_name` can be relative to dir where `calendar.py` is, or relative to COMPONENTS.dirs
|
||||||
template_name = "calendar.html"
|
template_name = "calendar.html"
|
||||||
# Or
|
# Or
|
||||||
# def get_template_name(context):
|
# def get_template_name(context):
|
||||||
|
@ -19,10 +19,11 @@ class CalendarNested(Component):
|
||||||
}
|
}
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
context = {
|
return self.render_to_response(
|
||||||
"date": request.GET.get("date", ""),
|
kwargs={
|
||||||
}
|
"date": request.GET.get("date", ""),
|
||||||
return self.render_to_response(context)
|
},
|
||||||
|
)
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = "calendar.css"
|
css = "calendar.css"
|
||||||
|
|
|
@ -28,10 +28,8 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.contenttypes",
|
"django.contrib.contenttypes",
|
||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
# Replaced by django_components.safer_staticfiles as of v0.27:
|
"django.contrib.staticfiles",
|
||||||
# "django.contrib.staticfiles",
|
|
||||||
"django_components",
|
"django_components",
|
||||||
"django_components.safer_staticfiles",
|
|
||||||
"calendarapp",
|
"calendarapp",
|
||||||
]
|
]
|
||||||
# Application definition
|
# Application definition
|
||||||
|
@ -79,14 +77,24 @@ TEMPLATES = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
STATICFILES_FINDERS = [
|
||||||
|
# Default finders
|
||||||
|
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||||
|
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||||
|
# Django components
|
||||||
|
"django_components.finders.ComponentsFileSystemFinder",
|
||||||
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = "sampleproject.wsgi.application"
|
WSGI_APPLICATION = "sampleproject.wsgi.application"
|
||||||
|
|
||||||
# COMPONENTS = {
|
COMPONENTS = {
|
||||||
# "autodiscover": True,
|
# "autodiscover": True,
|
||||||
# "libraries": [],
|
"dirs": [BASE_DIR / "components"],
|
||||||
# "template_cache_size": 128,
|
# "app_dirs": ["components"],
|
||||||
# "context_behavior": "isolated", # "django" | "isolated"
|
# "libraries": [],
|
||||||
# }
|
# "template_cache_size": 128,
|
||||||
|
# "context_behavior": "isolated", # "django" | "isolated"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
|
@ -135,7 +143,6 @@ USE_TZ = True
|
||||||
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = "static/"
|
STATIC_URL = "static/"
|
||||||
STATICFILES_DIRS = [BASE_DIR / "components"]
|
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import re
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Dict, List, Union
|
from typing import TYPE_CHECKING, Dict, List, Tuple, Union
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
@ -98,6 +99,14 @@ class AppSettings:
|
||||||
def AUTODISCOVER(self) -> bool:
|
def AUTODISCOVER(self) -> bool:
|
||||||
return self.settings.get("autodiscover", True)
|
return self.settings.get("autodiscover", True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def DIRS(self) -> List[Union[str, Tuple[str, str]]]:
|
||||||
|
return self.settings.get("dirs", [settings.BASE_DIR / "components"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def APP_DIRS(self) -> List[str]:
|
||||||
|
return self.settings.get("app_dirs", ["components"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def DYNAMIC_COMPONENT_NAME(self) -> str:
|
def DYNAMIC_COMPONENT_NAME(self) -> str:
|
||||||
return self.settings.get("dynamic_component_name", "dynamic")
|
return self.settings.get("dynamic_component_name", "dynamic")
|
||||||
|
@ -118,6 +127,51 @@ class AppSettings:
|
||||||
def TEMPLATE_CACHE_SIZE(self) -> int:
|
def TEMPLATE_CACHE_SIZE(self) -> int:
|
||||||
return self.settings.get("template_cache_size", 128)
|
return self.settings.get("template_cache_size", 128)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def STATIC_FILES_ALLOWED(self) -> List[Union[str, re.Pattern]]:
|
||||||
|
default_static_files = [
|
||||||
|
".css",
|
||||||
|
".js",
|
||||||
|
# Images - See https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types#common_image_file_types # noqa: E501
|
||||||
|
".apng",
|
||||||
|
".png",
|
||||||
|
".avif",
|
||||||
|
".gif",
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".jfif",
|
||||||
|
".pjpeg",
|
||||||
|
".pjp",
|
||||||
|
".svg",
|
||||||
|
".webp",
|
||||||
|
".bmp",
|
||||||
|
".ico",
|
||||||
|
".cur",
|
||||||
|
".tif",
|
||||||
|
".tiff",
|
||||||
|
# Fonts - See https://stackoverflow.com/q/30572159/9788634
|
||||||
|
".eot",
|
||||||
|
".ttf",
|
||||||
|
".woff",
|
||||||
|
".otf",
|
||||||
|
".svg",
|
||||||
|
]
|
||||||
|
return self.settings.get("static_files_allowed", default_static_files)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def STATIC_FILES_FORBIDDEN(self) -> List[Union[str, re.Pattern]]:
|
||||||
|
default_forbidden_static_files = [
|
||||||
|
".html",
|
||||||
|
# See https://marketplace.visualstudio.com/items?itemName=junstyle.vscode-django-support
|
||||||
|
".django",
|
||||||
|
".dj",
|
||||||
|
".tpl",
|
||||||
|
# Python files
|
||||||
|
".py",
|
||||||
|
".pyc",
|
||||||
|
]
|
||||||
|
return self.settings.get("forbidden_static_files", default_forbidden_static_files)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def CONTEXT_BEHAVIOR(self) -> ContextBehavior:
|
def CONTEXT_BEHAVIOR(self) -> ContextBehavior:
|
||||||
raw_value = self.settings.get("context_behavior", ContextBehavior.DJANGO.value)
|
raw_value = self.settings.get("context_behavior", ContextBehavior.DJANGO.value)
|
||||||
|
|
|
@ -5,10 +5,9 @@ from pathlib import Path
|
||||||
from typing import Callable, List, Optional, Union
|
from typing import Callable, List, Optional, Union
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.engine import Engine
|
|
||||||
|
|
||||||
from django_components.logger import logger
|
from django_components.logger import logger
|
||||||
from django_components.template_loader import Loader
|
from django_components.template_loader import get_dirs
|
||||||
|
|
||||||
|
|
||||||
def autodiscover(
|
def autodiscover(
|
||||||
|
@ -27,7 +26,13 @@ def autodiscover(
|
||||||
component_filepaths = search_dirs(dirs, "**/*.py")
|
component_filepaths = search_dirs(dirs, "**/*.py")
|
||||||
logger.debug(f"Autodiscover found {len(component_filepaths)} files in component directories.")
|
logger.debug(f"Autodiscover found {len(component_filepaths)} files in component directories.")
|
||||||
|
|
||||||
modules = [_filepath_to_python_module(filepath) for filepath in component_filepaths]
|
modules: List[str] = []
|
||||||
|
for filepath in component_filepaths:
|
||||||
|
module_path = _filepath_to_python_module(filepath)
|
||||||
|
# Ignore relative paths that are outside of the project root
|
||||||
|
if not module_path.startswith(".."):
|
||||||
|
modules.append(module_path)
|
||||||
|
|
||||||
return _import_modules(modules, map_module)
|
return _import_modules(modules, map_module)
|
||||||
|
|
||||||
|
|
||||||
|
@ -89,19 +94,6 @@ def _filepath_to_python_module(file_path: Union[Path, str]) -> str:
|
||||||
return module_name
|
return module_name
|
||||||
|
|
||||||
|
|
||||||
def get_dirs(engine: Optional[Engine] = None) -> List[Path]:
|
|
||||||
"""
|
|
||||||
Helper for using django_component's FilesystemLoader class to obtain a list
|
|
||||||
of directories where component python files may be defined.
|
|
||||||
"""
|
|
||||||
current_engine = engine
|
|
||||||
if current_engine is None:
|
|
||||||
current_engine = Engine.get_default()
|
|
||||||
|
|
||||||
loader = Loader(current_engine)
|
|
||||||
return loader.get_dirs()
|
|
||||||
|
|
||||||
|
|
||||||
def search_dirs(dirs: List[Path], search_glob: str) -> List[Path]:
|
def search_dirs(dirs: List[Path], search_glob: str) -> List[Path]:
|
||||||
"""
|
"""
|
||||||
Search the directories for the given glob pattern. Glob search results are returned
|
Search the directories for the given glob pattern. Glob search results are returned
|
||||||
|
|
|
@ -284,7 +284,7 @@ def _resolve_component_relative_files(attrs: MutableMapping) -> None:
|
||||||
f"No component directory found for component '{component_name}' in {file_path}"
|
f"No component directory found for component '{component_name}' in {file_path}"
|
||||||
" If this component defines HTML, JS or CSS templates relatively to the component file,"
|
" If this component defines HTML, JS or CSS templates relatively to the component file,"
|
||||||
" then check that the component's directory is accessible from one of the paths"
|
" then check that the component's directory is accessible from one of the paths"
|
||||||
" specified in the Django's 'STATICFILES_DIRS' settings."
|
" specified in the Django's 'COMPONENTS.dirs' settings."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -327,7 +327,7 @@ def _get_dir_path_from_component_path(
|
||||||
) -> Tuple[str, str]:
|
) -> Tuple[str, str]:
|
||||||
comp_dir_path_abs = os.path.dirname(abs_component_file_path)
|
comp_dir_path_abs = os.path.dirname(abs_component_file_path)
|
||||||
|
|
||||||
# From all dirs defined in settings.STATICFILES_DIRS, find one that's the parent
|
# From all dirs defined in settings.COMPONENTS.dirs, find one that's the parent
|
||||||
# to the component file.
|
# to the component file.
|
||||||
root_dir_abs = None
|
root_dir_abs = None
|
||||||
for candidate_dir in candidate_dirs:
|
for candidate_dir in candidate_dirs:
|
||||||
|
@ -341,7 +341,7 @@ def _get_dir_path_from_component_path(
|
||||||
f"Failed to resolve template directory for component file '{abs_component_file_path}'",
|
f"Failed to resolve template directory for component file '{abs_component_file_path}'",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Derive the path from matched STATICFILES_DIRS to the dir where the current component file is.
|
# Derive the path from matched COMPONENTS.dirs to the dir where the current component file is.
|
||||||
comp_dir_path_rel = os.path.relpath(comp_dir_path_abs, candidate_dir_abs)
|
comp_dir_path_rel = os.path.relpath(comp_dir_path_abs, candidate_dir_abs)
|
||||||
|
|
||||||
# Return both absolute and relative paths:
|
# Return both absolute and relative paths:
|
||||||
|
|
154
src/django_components/finders.py
Normal file
154
src/django_components/finders.py
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from django.contrib.staticfiles.finders import BaseFinder
|
||||||
|
from django.contrib.staticfiles.utils import get_files
|
||||||
|
from django.core.checks import CheckMessage, Error, Warning
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
from django.utils._os import safe_join
|
||||||
|
|
||||||
|
from django_components.app_settings import app_settings
|
||||||
|
from django_components.template_loader import get_dirs
|
||||||
|
from django_components.utils import any_regex_match, no_regex_match
|
||||||
|
|
||||||
|
# To keep track on which directories the finder has searched the static files.
|
||||||
|
searched_locations = []
|
||||||
|
|
||||||
|
|
||||||
|
# Custom Finder for staticfiles that searches for all files within the directories
|
||||||
|
# defined by `COMPONENTS.dirs`.
|
||||||
|
#
|
||||||
|
# This is what makes it possible to define JS and CSS files in the directories as
|
||||||
|
# defined by `COMPONENTS.dirs`, but still use the JS / CSS files with `static()` or
|
||||||
|
# `collectstatic` command.
|
||||||
|
class ComponentsFileSystemFinder(BaseFinder):
|
||||||
|
"""
|
||||||
|
A static files finder based on `FileSystemFinder`.
|
||||||
|
|
||||||
|
Differences:
|
||||||
|
- This finder uses `COMPONENTS.dirs` setting to locate files instead of `STATICFILES_DIRS`.
|
||||||
|
- Whether a file within `COMPONENTS.dirs` is considered a STATIC file is configured
|
||||||
|
by `COMPONENTS.static_files_allowed` and `COMPONENTS.forbidden_static_files`.
|
||||||
|
- If `COMPONENTS.dirs` is not set, defaults to `settings.BASE_DIR / "components"`
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app_names: Any = None, *args: Any, **kwargs: Any) -> None:
|
||||||
|
component_dirs = [str(p) for p in get_dirs()]
|
||||||
|
|
||||||
|
# NOTE: The rest of the __init__ is the same as `django.contrib.staticfiles.finders.FileSystemFinder`,
|
||||||
|
# but using our locations instead of STATICFILES_DIRS.
|
||||||
|
|
||||||
|
# List of locations with static files
|
||||||
|
self.locations: List[Tuple[str, str]] = []
|
||||||
|
|
||||||
|
# Maps dir paths to an appropriate storage instance
|
||||||
|
self.storages: Dict[str, FileSystemStorage] = {}
|
||||||
|
for root in component_dirs:
|
||||||
|
if isinstance(root, (list, tuple)):
|
||||||
|
prefix, root = root
|
||||||
|
else:
|
||||||
|
prefix = ""
|
||||||
|
if (prefix, root) not in self.locations:
|
||||||
|
self.locations.append((prefix, root))
|
||||||
|
for prefix, root in self.locations:
|
||||||
|
filesystem_storage = FileSystemStorage(location=root)
|
||||||
|
filesystem_storage.prefix = prefix
|
||||||
|
self.storages[root] = filesystem_storage
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# NOTE: Based on `FileSystemFinder.check`
|
||||||
|
def check(self, **kwargs: Any) -> List[CheckMessage]:
|
||||||
|
errors: List[CheckMessage] = []
|
||||||
|
if not isinstance(app_settings.DIRS, (list, tuple)):
|
||||||
|
errors.append(
|
||||||
|
Error(
|
||||||
|
"The COMPONENTS.dirs setting is not a tuple or list.",
|
||||||
|
hint="Perhaps you forgot a trailing comma?",
|
||||||
|
id="components.E001",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return errors
|
||||||
|
for root in app_settings.DIRS:
|
||||||
|
if isinstance(root, (list, tuple)):
|
||||||
|
prefix, root = root
|
||||||
|
if prefix.endswith("/"):
|
||||||
|
errors.append(
|
||||||
|
Error(
|
||||||
|
"The prefix %r in the COMPONENTS.dirs setting must not end with a slash." % prefix,
|
||||||
|
id="staticfiles.E003",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif not os.path.isdir(root):
|
||||||
|
errors.append(
|
||||||
|
Warning(
|
||||||
|
f"The directory '{root}' in the COMPONENTS.dirs setting does not exist.",
|
||||||
|
id="components.W004",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return errors
|
||||||
|
|
||||||
|
# NOTE: Same as `FileSystemFinder.find`
|
||||||
|
def find(self, path: str, all: bool = False) -> Union[List[str], str]:
|
||||||
|
"""
|
||||||
|
Look for files in the extra locations as defined in COMPONENTS.dirs.
|
||||||
|
"""
|
||||||
|
matches: List[str] = []
|
||||||
|
for prefix, root in self.locations:
|
||||||
|
if root not in searched_locations:
|
||||||
|
searched_locations.append(root)
|
||||||
|
matched_path = self.find_location(root, path, prefix)
|
||||||
|
if matched_path:
|
||||||
|
if not all:
|
||||||
|
return matched_path
|
||||||
|
matches.append(matched_path)
|
||||||
|
return matches
|
||||||
|
|
||||||
|
# NOTE: Same as `FileSystemFinder.find_local`, but we exclude Python/HTML files
|
||||||
|
def find_location(self, root: str, path: str, prefix: Optional[str] = None) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Find a requested static file in a location and return the found
|
||||||
|
absolute path (or ``None`` if no match).
|
||||||
|
"""
|
||||||
|
if prefix:
|
||||||
|
prefix = "%s%s" % (prefix, os.sep)
|
||||||
|
if not path.startswith(prefix):
|
||||||
|
return None
|
||||||
|
path = path.removeprefix(prefix)
|
||||||
|
path = safe_join(root, path)
|
||||||
|
|
||||||
|
if os.path.exists(path) and self._is_path_valid(path):
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
# `Finder.list` is called from `collectstatic` command,
|
||||||
|
# see https://github.com/django/django/blob/bc9b6251e0b54c3b5520e3c66578041cc17e4a28/django/contrib/staticfiles/management/commands/collectstatic.py#L126C23-L126C30 # noqa E501
|
||||||
|
#
|
||||||
|
# NOTE: This is same as `FileSystemFinder.list`, but we exclude Python/HTML files
|
||||||
|
# NOTE 2: Yield can be annotated as Iterable, see https://stackoverflow.com/questions/38419654
|
||||||
|
def list(self, ignore_patterns: List[str]) -> Iterable[Tuple[str, FileSystemStorage]]:
|
||||||
|
"""
|
||||||
|
List all files in all locations.
|
||||||
|
"""
|
||||||
|
for prefix, root in self.locations:
|
||||||
|
# Skip nonexistent directories.
|
||||||
|
if os.path.isdir(root):
|
||||||
|
storage = self.storages[root]
|
||||||
|
for path in get_files(storage, ignore_patterns):
|
||||||
|
if self._is_path_valid(path):
|
||||||
|
yield path, storage
|
||||||
|
|
||||||
|
def _is_path_valid(self, path: str) -> bool:
|
||||||
|
# Normalize patterns to regexes
|
||||||
|
allowed_patterns = [
|
||||||
|
# Convert suffixes like `.html` to regex `\.html$`
|
||||||
|
re.compile(rf"\{p}$") if isinstance(p, str) else p
|
||||||
|
for p in app_settings.STATIC_FILES_ALLOWED
|
||||||
|
]
|
||||||
|
forbidden_patterns = [
|
||||||
|
# Convert suffixes like `.html` to regex `\.html$`
|
||||||
|
re.compile(rf"\{p}$") if isinstance(p, str) else p
|
||||||
|
for p in app_settings.STATIC_FILES_FORBIDDEN
|
||||||
|
]
|
||||||
|
return any_regex_match(path, allowed_patterns) and no_regex_match(path, forbidden_patterns)
|
|
@ -1,22 +0,0 @@
|
||||||
from django.contrib.staticfiles.apps import StaticFilesConfig
|
|
||||||
|
|
||||||
|
|
||||||
class SaferStaticFilesConfig(StaticFilesConfig):
|
|
||||||
"""
|
|
||||||
Extend the `ignore_patterns` class attr of StaticFilesConfig to include Python
|
|
||||||
modules and HTML files.
|
|
||||||
|
|
||||||
When this class is registered as an installed app,
|
|
||||||
`$ ./manage.py collectstatic` will ignore .py and .html files,
|
|
||||||
preventing potentially sensitive backend logic from being leaked
|
|
||||||
by the static file server.
|
|
||||||
|
|
||||||
See https://docs.djangoproject.com/en/5.0/ref/contrib/staticfiles/#customizing-the-ignored-pattern-list
|
|
||||||
"""
|
|
||||||
|
|
||||||
default = True # Ensure that _this_ app is registered, as opposed to parent cls.
|
|
||||||
ignore_patterns = StaticFilesConfig.ignore_patterns + [
|
|
||||||
"*.py",
|
|
||||||
"*.html",
|
|
||||||
"*.pyc",
|
|
||||||
]
|
|
|
@ -3,55 +3,92 @@ Template loader that loads templates from each Django app's "components" directo
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Set
|
from typing import List, Optional, Set
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.template.engine import Engine
|
||||||
from django.template.loaders.filesystem import Loader as FilesystemLoader
|
from django.template.loaders.filesystem import Loader as FilesystemLoader
|
||||||
|
|
||||||
|
from django_components.app_settings import app_settings
|
||||||
from django_components.logger import logger
|
from django_components.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
# Similar to `Path.is_relative_to`, which is missing in 3.8
|
||||||
|
def is_relative_to(path: Path, other: Path) -> bool:
|
||||||
|
try:
|
||||||
|
path.relative_to(other)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# This is the heart of all features that deal with filesystem and file lookup.
|
||||||
|
# Autodiscovery, Django template resolution, static file resolution - They all
|
||||||
|
# depend on this loader.
|
||||||
class Loader(FilesystemLoader):
|
class Loader(FilesystemLoader):
|
||||||
def get_dirs(self) -> List[Path]:
|
def get_dirs(self) -> List[Path]:
|
||||||
"""
|
"""
|
||||||
Prepare directories that may contain component files:
|
Prepare directories that may contain component files:
|
||||||
|
|
||||||
Searches for dirs set in `STATICFILES_DIRS` settings. If none set, defaults to searching
|
Searches for dirs set in `COMPONENTS.dirs` settings. If none set, defaults to searching
|
||||||
for a "components" app. The dirs in `STATICFILES_DIRS` must be absolute paths.
|
for a "components" app. The dirs in `COMPONENTS.dirs` must be absolute paths.
|
||||||
|
|
||||||
|
In addition to that, also all apps are checked for `[app]/components` dirs.
|
||||||
|
|
||||||
Paths are accepted only if they resolve to a directory.
|
Paths are accepted only if they resolve to a directory.
|
||||||
E.g. `/path/to/django_project/my_app/components/`.
|
E.g. `/path/to/django_project/my_app/components/`.
|
||||||
|
|
||||||
If `STATICFILES_DIRS` is not set or empty, then `BASE_DIR` is required.
|
`BASE_DIR` setting is required.
|
||||||
"""
|
"""
|
||||||
# Allow to configure from settings which dirs should be checked for components
|
# Allow to configure from settings which dirs should be checked for components
|
||||||
if hasattr(settings, "STATICFILES_DIRS") and settings.STATICFILES_DIRS:
|
component_dirs = app_settings.DIRS
|
||||||
component_dirs = settings.STATICFILES_DIRS
|
|
||||||
else:
|
# TODO_REMOVE_IN_V1
|
||||||
component_dirs = [settings.BASE_DIR / "components"]
|
is_legacy_paths = (
|
||||||
|
# Use value of `STATICFILES_DIRS` ONLY if `COMPONENT.dirs` not set
|
||||||
|
not getattr(settings, "COMPONENTS", {}).get("dirs", None) is not None
|
||||||
|
and hasattr(settings, "STATICFILES_DIRS")
|
||||||
|
and settings.STATICFILES_DIRS
|
||||||
|
)
|
||||||
|
if is_legacy_paths:
|
||||||
|
# NOTE: For STATICFILES_DIRS, we use the defaults even for empty list.
|
||||||
|
# We don't do this for COMPONENTS.dirs, so user can explicitly specify "NO dirs".
|
||||||
|
component_dirs = settings.STATICFILES_DIRS or [settings.BASE_DIR / "components"]
|
||||||
|
source = "STATICFILES_DIRS" if is_legacy_paths else "COMPONENTS.dirs"
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Template loader will search for valid template dirs from following options:\n"
|
"Template loader will search for valid template dirs from following options:\n"
|
||||||
+ "\n".join([f" - {str(d)}" for d in component_dirs])
|
+ "\n".join([f" - {str(d)}" for d in component_dirs])
|
||||||
)
|
)
|
||||||
|
|
||||||
directories: Set[Path] = set()
|
# Add `[app]/[APP_DIR]` to the directories. This is, by default `[app]/components`
|
||||||
|
app_paths: List[Path] = []
|
||||||
|
for conf in apps.get_app_configs():
|
||||||
|
for app_dir in app_settings.APP_DIRS:
|
||||||
|
comps_path = Path(conf.path).joinpath(app_dir)
|
||||||
|
if comps_path.exists() and is_relative_to(comps_path, settings.BASE_DIR):
|
||||||
|
app_paths.append(comps_path)
|
||||||
|
|
||||||
|
directories: Set[Path] = set(app_paths)
|
||||||
|
|
||||||
|
# Validate and add other values from the config
|
||||||
for component_dir in component_dirs:
|
for component_dir in component_dirs:
|
||||||
# Consider tuples for STATICFILES_DIRS (See #489)
|
# 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.0/ref/settings/#prefixes-optional
|
||||||
if isinstance(component_dir, (tuple, list)) and len(component_dir) == 2:
|
if isinstance(component_dir, (tuple, list)):
|
||||||
component_dir = component_dir[1]
|
component_dir = component_dir[1]
|
||||||
try:
|
try:
|
||||||
Path(component_dir)
|
Path(component_dir)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"STATICFILES_DIRS expected str, bytes or os.PathLike object, or tuple/list of length 2. "
|
f"{source} expected str, bytes or os.PathLike object, or tuple/list of length 2. "
|
||||||
f"See Django documentation. Got {type(component_dir)} : {component_dir}"
|
f"See Django documentation for STATICFILES_DIRS. Got {type(component_dir)} : {component_dir}"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not Path(component_dir).is_absolute():
|
if not Path(component_dir).is_absolute():
|
||||||
raise ValueError(f"STATICFILES_DIRS must contain absolute paths, got '{component_dir}'")
|
raise ValueError(f"{source} must contain absolute paths, got '{component_dir}'")
|
||||||
else:
|
else:
|
||||||
directories.add(Path(component_dir).resolve())
|
directories.add(Path(component_dir).resolve())
|
||||||
|
|
||||||
|
@ -59,3 +96,16 @@ class Loader(FilesystemLoader):
|
||||||
"Template loader matched following template dirs:\n" + "\n".join([f" - {str(d)}" for d in directories])
|
"Template loader matched following template dirs:\n" + "\n".join([f" - {str(d)}" for d in directories])
|
||||||
)
|
)
|
||||||
return list(directories)
|
return list(directories)
|
||||||
|
|
||||||
|
|
||||||
|
def get_dirs(engine: Optional[Engine] = None) -> List[Path]:
|
||||||
|
"""
|
||||||
|
Helper for using django_component's FilesystemLoader class to obtain a list
|
||||||
|
of directories where component python files may be defined.
|
||||||
|
"""
|
||||||
|
current_engine = engine
|
||||||
|
if current_engine is None:
|
||||||
|
current_engine = Engine.get_default()
|
||||||
|
|
||||||
|
loader = Loader(current_engine)
|
||||||
|
return loader.get_dirs()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import functools
|
import functools
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import typing
|
import typing
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -211,3 +212,11 @@ def lazy_cache(
|
||||||
return cast(TFunc, wrapper)
|
return cast(TFunc, wrapper)
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def any_regex_match(string: str, patterns: List[re.Pattern]) -> bool:
|
||||||
|
return any(p.search(string) is not None for p in patterns)
|
||||||
|
|
||||||
|
|
||||||
|
def no_regex_match(string: str, patterns: List[re.Pattern]) -> bool:
|
||||||
|
return all(p.search(string) is None for p in patterns)
|
||||||
|
|
|
@ -3,14 +3,14 @@ from typing import Any, Dict
|
||||||
from django_components import Component, register
|
from django_components import Component, register
|
||||||
|
|
||||||
|
|
||||||
# Used for testing the safer_staticfiles app in `test_safer_staticfiles.py`
|
# Used for testing the staticfiles finder in `test_staticfiles.py`
|
||||||
@register("safer_staticfiles_component")
|
@register("staticfiles_component")
|
||||||
class RelativeFileWithPathObjComponent(Component):
|
class RelativeFileWithPathObjComponent(Component):
|
||||||
template_name = "safer_staticfiles.html"
|
template_name = "staticfiles.html"
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
js = "safer_staticfiles.js"
|
js = "staticfiles.js"
|
||||||
css = "safer_staticfiles.css"
|
css = "staticfiles.css"
|
||||||
|
|
||||||
def get_context_data(self, variable, *args, **kwargs) -> Dict[str, Any]:
|
def get_context_data(self, variable, *args, **kwargs) -> Dict[str, Any]:
|
||||||
return {"variable": variable}
|
return {"variable": variable}
|
|
@ -11,7 +11,7 @@ def setup_test_config(components: Optional[Dict] = None):
|
||||||
|
|
||||||
settings.configure(
|
settings.configure(
|
||||||
BASE_DIR=Path(__file__).resolve().parent,
|
BASE_DIR=Path(__file__).resolve().parent,
|
||||||
INSTALLED_APPS=("django_components",),
|
INSTALLED_APPS=("django_components", "tests.test_app"),
|
||||||
TEMPLATES=[
|
TEMPLATES=[
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
@ -32,6 +32,7 @@ def setup_test_config(components: Optional[Dict] = None):
|
||||||
"NAME": ":memory:",
|
"NAME": ":memory:",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
SECRET_KEY="secret",
|
||||||
)
|
)
|
||||||
|
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
6
tests/test_app/apps.py
Normal file
6
tests/test_app/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class TestAppConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "tests.test_app"
|
3
tests/test_app/components/app_lvl_comp/app_lvl_comp.css
Normal file
3
tests/test_app/components/app_lvl_comp/app_lvl_comp.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.html-css-only {
|
||||||
|
color: blue;
|
||||||
|
}
|
5
tests/test_app/components/app_lvl_comp/app_lvl_comp.html
Normal file
5
tests/test_app/components/app_lvl_comp/app_lvl_comp.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="text" name="variable" value="{{ variable }}">
|
||||||
|
<input type="submit">
|
||||||
|
</form>
|
1
tests/test_app/components/app_lvl_comp/app_lvl_comp.js
Normal file
1
tests/test_app/components/app_lvl_comp/app_lvl_comp.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
console.log("JS file");
|
16
tests/test_app/components/app_lvl_comp/app_lvl_comp.py
Normal file
16
tests/test_app/components/app_lvl_comp/app_lvl_comp.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from django_components import Component, register
|
||||||
|
|
||||||
|
|
||||||
|
# Used for testing the template_loader
|
||||||
|
@register("app_lvl_comp")
|
||||||
|
class AppLvlCompComponent(Component):
|
||||||
|
template_name = "app_lvl_comp.html"
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = "app_lvl_comp.js"
|
||||||
|
css = "app_lvl_comp.css"
|
||||||
|
|
||||||
|
def get_context_data(self, variable, *args, **kwargs) -> Dict[str, Any]:
|
||||||
|
return {"variable": variable}
|
|
@ -0,0 +1,3 @@
|
||||||
|
.html-css-only {
|
||||||
|
color: blue;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="text" name="variable" value="{{ variable }}">
|
||||||
|
<input type="submit">
|
||||||
|
</form>
|
|
@ -0,0 +1 @@
|
||||||
|
console.log("JS file");
|
16
tests/test_app/custom_comps_dir/app_lvl_comp/app_lvl_comp.py
Normal file
16
tests/test_app/custom_comps_dir/app_lvl_comp/app_lvl_comp.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from django_components import Component, register
|
||||||
|
|
||||||
|
|
||||||
|
# Used for testing the template_loader
|
||||||
|
@register("app_lvl_comp")
|
||||||
|
class AppLvlCompComponent(Component):
|
||||||
|
template_name = "app_lvl_comp.html"
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = "app_lvl_comp.js"
|
||||||
|
css = "app_lvl_comp.css"
|
||||||
|
|
||||||
|
def get_context_data(self, variable, *args, **kwargs) -> Dict[str, Any]:
|
||||||
|
return {"variable": variable}
|
|
@ -618,10 +618,9 @@ class MediaStaticfilesTests(BaseTestCase):
|
||||||
# See https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS
|
# See https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS
|
||||||
STATIC_URL="static/",
|
STATIC_URL="static/",
|
||||||
STATIC_ROOT=os.path.join(Path(__file__).resolve().parent, "static_root"),
|
STATIC_ROOT=os.path.join(Path(__file__).resolve().parent, "static_root"),
|
||||||
# Either `django.contrib.staticfiles` or `django_components.safer_staticfiles` MUST
|
# `django.contrib.staticfiles` MUST be installed for staticfiles resolution to work.
|
||||||
# be installed for staticfiles resolution to work.
|
|
||||||
INSTALLED_APPS=[
|
INSTALLED_APPS=[
|
||||||
"django_components.safer_staticfiles", # Or django.contrib.staticfiles
|
"django.contrib.staticfiles",
|
||||||
"django_components",
|
"django_components",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -675,10 +674,9 @@ class MediaStaticfilesTests(BaseTestCase):
|
||||||
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
|
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# Either `django.contrib.staticfiles` or `django_components.safer_staticfiles` MUST
|
# `django.contrib.staticfiles` MUST be installed for staticfiles resolution to work.
|
||||||
# be installed for staticfiles resolution to work.
|
|
||||||
INSTALLED_APPS=[
|
INSTALLED_APPS=[
|
||||||
"django_components.safer_staticfiles", # Or django.contrib.staticfiles
|
"django.contrib.staticfiles",
|
||||||
"django_components",
|
"django_components",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
210
tests/test_finders.py
Normal file
210
tests/test_finders.py
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.contrib.staticfiles.management.commands.collectstatic import Command
|
||||||
|
from django.test import SimpleTestCase, override_settings
|
||||||
|
|
||||||
|
from .django_test_setup import setup_test_config
|
||||||
|
|
||||||
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
|
# This subclass allows us to call the `collectstatic` command from within Python.
|
||||||
|
# We call the `collect` method, which returns info about what files were collected.
|
||||||
|
#
|
||||||
|
# The methods below are overriden to ensure we don't make any filesystem changes
|
||||||
|
# (copy/delete), as the original command copies files. Thus we can safely test that
|
||||||
|
# our app works as intended.
|
||||||
|
class MockCollectstaticCommand(Command):
|
||||||
|
# NOTE: We do not expect this to be called
|
||||||
|
def clear_dir(self, path):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
# NOTE: We do not expect this to be called
|
||||||
|
def link_file(self, path, prefixed_path, source_storage):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def copy_file(self, path, prefixed_path, source_storage):
|
||||||
|
# Skip this file if it was already copied earlier
|
||||||
|
if prefixed_path in self.copied_files:
|
||||||
|
return self.log("Skipping '%s' (already copied earlier)" % path)
|
||||||
|
# Delete the target file if needed or break
|
||||||
|
if not self.delete_file(path, prefixed_path, source_storage):
|
||||||
|
return
|
||||||
|
# The full path of the source file
|
||||||
|
source_path = source_storage.path(path)
|
||||||
|
# Finally start copying
|
||||||
|
if self.dry_run:
|
||||||
|
self.log("Pretending to copy '%s'" % source_path, level=1)
|
||||||
|
else:
|
||||||
|
self.log("Copying '%s'" % source_path, level=2)
|
||||||
|
# ############# OUR CHANGE ##############
|
||||||
|
# with source_storage.open(path) as source_file:
|
||||||
|
# self.storage.save(prefixed_path, source_file)
|
||||||
|
# ############# OUR CHANGE ##############
|
||||||
|
self.copied_files.append(prefixed_path)
|
||||||
|
|
||||||
|
|
||||||
|
def do_collect():
|
||||||
|
cmd = MockCollectstaticCommand()
|
||||||
|
cmd.set_options(
|
||||||
|
interactive=False,
|
||||||
|
verbosity=1,
|
||||||
|
link=False,
|
||||||
|
clear=False,
|
||||||
|
dry_run=False,
|
||||||
|
ignore_patterns=[],
|
||||||
|
use_default_ignore_patterns=True,
|
||||||
|
post_process=True,
|
||||||
|
)
|
||||||
|
collected = cmd.collect()
|
||||||
|
return collected
|
||||||
|
|
||||||
|
|
||||||
|
common_settings = {
|
||||||
|
"STATIC_URL": "static/",
|
||||||
|
"STATIC_ROOT": "staticfiles",
|
||||||
|
"ROOT_URLCONF": __name__,
|
||||||
|
"INSTALLED_APPS": ("django_components", "django.contrib.staticfiles"),
|
||||||
|
}
|
||||||
|
COMPONENTS = {
|
||||||
|
"dirs": [Path(__file__).resolve().parent / "components"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StaticFilesFinderTests(SimpleTestCase):
|
||||||
|
@override_settings(
|
||||||
|
**common_settings,
|
||||||
|
COMPONENTS=COMPONENTS,
|
||||||
|
STATICFILES_FINDERS=[
|
||||||
|
# Default finders
|
||||||
|
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||||
|
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_python_and_html_included(self):
|
||||||
|
collected = do_collect()
|
||||||
|
|
||||||
|
# Check that the component files are NOT loaded when our finder is NOT added
|
||||||
|
self.assertNotIn("staticfiles/staticfiles.css", collected["modified"])
|
||||||
|
self.assertNotIn("staticfiles/staticfiles.js", collected["modified"])
|
||||||
|
self.assertNotIn("staticfiles/staticfiles.html", collected["modified"])
|
||||||
|
self.assertNotIn("staticfiles/staticfiles.py", collected["modified"])
|
||||||
|
|
||||||
|
self.assertListEqual(collected["unmodified"], [])
|
||||||
|
self.assertListEqual(collected["post_processed"], [])
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
**common_settings,
|
||||||
|
COMPONENTS=COMPONENTS,
|
||||||
|
STATICFILES_FINDERS=[
|
||||||
|
# Default finders
|
||||||
|
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||||
|
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||||
|
# Django components
|
||||||
|
"django_components.finders.ComponentsFileSystemFinder",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_python_and_html_omitted(self):
|
||||||
|
collected = do_collect()
|
||||||
|
|
||||||
|
# Check that our staticfiles_finder finds the files and OMITS .py and .html files
|
||||||
|
self.assertIn("staticfiles/staticfiles.css", collected["modified"])
|
||||||
|
self.assertIn("staticfiles/staticfiles.js", collected["modified"])
|
||||||
|
self.assertNotIn("staticfiles/staticfiles.html", collected["modified"])
|
||||||
|
self.assertNotIn("staticfiles/staticfiles.py", collected["modified"])
|
||||||
|
|
||||||
|
self.assertListEqual(collected["unmodified"], [])
|
||||||
|
self.assertListEqual(collected["post_processed"], [])
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
**common_settings,
|
||||||
|
COMPONENTS={
|
||||||
|
**COMPONENTS,
|
||||||
|
"static_files_allowed": [
|
||||||
|
".js",
|
||||||
|
],
|
||||||
|
"forbidden_static_files": [],
|
||||||
|
},
|
||||||
|
STATICFILES_FINDERS=[
|
||||||
|
# Default finders
|
||||||
|
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||||
|
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||||
|
# Django components
|
||||||
|
"django_components.finders.ComponentsFileSystemFinder",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_set_static_files_allowed(self):
|
||||||
|
collected = do_collect()
|
||||||
|
|
||||||
|
# Check that our staticfiles_finder finds the files and OMITS .py and .html files
|
||||||
|
self.assertNotIn("staticfiles/staticfiles.css", collected["modified"])
|
||||||
|
self.assertIn("staticfiles/staticfiles.js", collected["modified"])
|
||||||
|
self.assertNotIn("staticfiles/staticfiles.html", collected["modified"])
|
||||||
|
self.assertNotIn("staticfiles/staticfiles.py", collected["modified"])
|
||||||
|
|
||||||
|
self.assertListEqual(collected["unmodified"], [])
|
||||||
|
self.assertListEqual(collected["post_processed"], [])
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
**common_settings,
|
||||||
|
COMPONENTS={
|
||||||
|
**COMPONENTS,
|
||||||
|
"static_files_allowed": [
|
||||||
|
re.compile(r".*"),
|
||||||
|
],
|
||||||
|
"forbidden_static_files": [
|
||||||
|
re.compile(r"\.(?:js)$"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
STATICFILES_FINDERS=[
|
||||||
|
# Default finders
|
||||||
|
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||||
|
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||||
|
# Django components
|
||||||
|
"django_components.finders.ComponentsFileSystemFinder",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_set_forbidden_files(self):
|
||||||
|
collected = do_collect()
|
||||||
|
|
||||||
|
# Check that our staticfiles_finder finds the files and OMITS .py and .html files
|
||||||
|
self.assertIn("staticfiles/staticfiles.css", collected["modified"])
|
||||||
|
self.assertNotIn("staticfiles/staticfiles.js", collected["modified"])
|
||||||
|
self.assertIn("staticfiles/staticfiles.html", collected["modified"])
|
||||||
|
self.assertIn("staticfiles/staticfiles.py", collected["modified"])
|
||||||
|
|
||||||
|
self.assertListEqual(collected["unmodified"], [])
|
||||||
|
self.assertListEqual(collected["post_processed"], [])
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
**common_settings,
|
||||||
|
COMPONENTS={
|
||||||
|
**COMPONENTS,
|
||||||
|
"static_files_allowed": [
|
||||||
|
".js",
|
||||||
|
".css",
|
||||||
|
],
|
||||||
|
"forbidden_static_files": [
|
||||||
|
".js",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
STATICFILES_FINDERS=[
|
||||||
|
# Default finders
|
||||||
|
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||||
|
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||||
|
# Django components
|
||||||
|
"django_components.finders.ComponentsFileSystemFinder",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_set_both_allowed_and_forbidden_files(self):
|
||||||
|
collected = do_collect()
|
||||||
|
|
||||||
|
# Check that our staticfiles_finder finds the files and OMITS .py and .html files
|
||||||
|
self.assertIn("staticfiles/staticfiles.css", collected["modified"])
|
||||||
|
self.assertNotIn("staticfiles/staticfiles.js", collected["modified"])
|
||||||
|
self.assertNotIn("staticfiles/staticfiles.html", collected["modified"])
|
||||||
|
self.assertNotIn("staticfiles/staticfiles.py", collected["modified"])
|
||||||
|
|
||||||
|
self.assertListEqual(collected["unmodified"], [])
|
||||||
|
self.assertListEqual(collected["post_processed"], [])
|
|
@ -56,15 +56,19 @@ class ComponentRegistryTest(unittest.TestCase):
|
||||||
my_lib = Library()
|
my_lib = Library()
|
||||||
my_reg = ComponentRegistry(library=my_lib)
|
my_reg = ComponentRegistry(library=my_lib)
|
||||||
|
|
||||||
|
default_registry_comps_before = len(registry.all())
|
||||||
|
|
||||||
self.assertDictEqual(my_reg.all(), {})
|
self.assertDictEqual(my_reg.all(), {})
|
||||||
self.assertDictEqual(registry.all(), {})
|
|
||||||
|
|
||||||
@register("decorated_component", registry=my_reg)
|
@register("decorated_component", registry=my_reg)
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.assertDictEqual(my_reg.all(), {"decorated_component": TestComponent})
|
self.assertDictEqual(my_reg.all(), {"decorated_component": TestComponent})
|
||||||
self.assertDictEqual(registry.all(), {})
|
|
||||||
|
# Check that the component was NOT added to the default registry
|
||||||
|
default_registry_comps_after = len(registry.all())
|
||||||
|
self.assertEqual(default_registry_comps_before, default_registry_comps_after)
|
||||||
|
|
||||||
def test_simple_register(self):
|
def test_simple_register(self):
|
||||||
self.registry.register(name="testcomponent", component=MockComponent)
|
self.registry.register(name="testcomponent", component=MockComponent)
|
||||||
|
|
|
@ -1,103 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from django.contrib.staticfiles.management.commands.collectstatic import Command
|
|
||||||
from django.test import SimpleTestCase, override_settings
|
|
||||||
|
|
||||||
from .django_test_setup import * # NOQA
|
|
||||||
|
|
||||||
|
|
||||||
# This subclass allows us to call the `collectstatic` command from within Python.
|
|
||||||
# We call the `collect` method, which returns info about what files were collected.
|
|
||||||
#
|
|
||||||
# The methods below are overriden to ensure we don't make any filesystem changes
|
|
||||||
# (copy/delete), as the original command copies files. Thus we can safely test that
|
|
||||||
# our `safer_staticfiles` app works as intended.
|
|
||||||
class MockCollectstaticCommand(Command):
|
|
||||||
# NOTE: We do not expect this to be called
|
|
||||||
def clear_dir(self, path):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
# NOTE: We do not expect this to be called
|
|
||||||
def link_file(self, path, prefixed_path, source_storage):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def copy_file(self, path, prefixed_path, source_storage):
|
|
||||||
# Skip this file if it was already copied earlier
|
|
||||||
if prefixed_path in self.copied_files:
|
|
||||||
return self.log("Skipping '%s' (already copied earlier)" % path)
|
|
||||||
# Delete the target file if needed or break
|
|
||||||
if not self.delete_file(path, prefixed_path, source_storage):
|
|
||||||
return
|
|
||||||
# The full path of the source file
|
|
||||||
source_path = source_storage.path(path)
|
|
||||||
# Finally start copying
|
|
||||||
if self.dry_run:
|
|
||||||
self.log("Pretending to copy '%s'" % source_path, level=1)
|
|
||||||
else:
|
|
||||||
self.log("Copying '%s'" % source_path, level=2)
|
|
||||||
# ############# OUR CHANGE ##############
|
|
||||||
# with source_storage.open(path) as source_file:
|
|
||||||
# self.storage.save(prefixed_path, source_file)
|
|
||||||
# ############# OUR CHANGE ##############
|
|
||||||
self.copied_files.append(prefixed_path)
|
|
||||||
|
|
||||||
|
|
||||||
def do_collect():
|
|
||||||
cmd = MockCollectstaticCommand()
|
|
||||||
cmd.set_options(
|
|
||||||
interactive=False,
|
|
||||||
verbosity=1,
|
|
||||||
link=False,
|
|
||||||
clear=False,
|
|
||||||
dry_run=False,
|
|
||||||
ignore_patterns=[],
|
|
||||||
use_default_ignore_patterns=True,
|
|
||||||
post_process=True,
|
|
||||||
)
|
|
||||||
collected = cmd.collect()
|
|
||||||
return collected
|
|
||||||
|
|
||||||
|
|
||||||
common_settings = {
|
|
||||||
"STATIC_URL": "static/",
|
|
||||||
"STATICFILES_DIRS": [Path(__file__).resolve().parent / "components"],
|
|
||||||
"STATIC_ROOT": "staticfiles",
|
|
||||||
"ROOT_URLCONF": __name__,
|
|
||||||
"SECRET_KEY": "secret",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Check that .py and .html files are INCLUDED with the original staticfiles app
|
|
||||||
@override_settings(
|
|
||||||
**common_settings,
|
|
||||||
INSTALLED_APPS=("django_components", "django.contrib.staticfiles"),
|
|
||||||
)
|
|
||||||
class OrigStaticFileTests(SimpleTestCase):
|
|
||||||
def test_python_and_html_included(self):
|
|
||||||
collected = do_collect()
|
|
||||||
|
|
||||||
self.assertIn("safer_staticfiles/safer_staticfiles.css", collected["modified"])
|
|
||||||
self.assertIn("safer_staticfiles/safer_staticfiles.js", collected["modified"])
|
|
||||||
self.assertIn("safer_staticfiles/safer_staticfiles.html", collected["modified"])
|
|
||||||
self.assertIn("safer_staticfiles/safer_staticfiles.py", collected["modified"])
|
|
||||||
|
|
||||||
self.assertListEqual(collected["unmodified"], [])
|
|
||||||
self.assertListEqual(collected["post_processed"], [])
|
|
||||||
|
|
||||||
|
|
||||||
# Check that .py and .html files are OMITTED from our version of staticfiles app
|
|
||||||
@override_settings(
|
|
||||||
**common_settings,
|
|
||||||
INSTALLED_APPS=("django_components", "django_components.safer_staticfiles"),
|
|
||||||
)
|
|
||||||
class SaferStaticFileTests(SimpleTestCase):
|
|
||||||
def test_python_and_html_omitted(self):
|
|
||||||
collected = do_collect()
|
|
||||||
|
|
||||||
self.assertIn("safer_staticfiles/safer_staticfiles.css", collected["modified"])
|
|
||||||
self.assertIn("safer_staticfiles/safer_staticfiles.js", collected["modified"])
|
|
||||||
self.assertNotIn("safer_staticfiles/safer_staticfiles.html", collected["modified"])
|
|
||||||
self.assertNotIn("safer_staticfiles/safer_staticfiles.py", collected["modified"])
|
|
||||||
|
|
||||||
self.assertListEqual(collected["unmodified"], [])
|
|
||||||
self.assertListEqual(collected["post_processed"], [])
|
|
|
@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch
|
||||||
from django.template.engine import Engine
|
from django.template.engine import Engine
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from django_components.template_loader import Loader
|
from django_components.template_loader import Loader, get_dirs
|
||||||
|
|
||||||
from .django_test_setup import setup_test_config
|
from .django_test_setup import setup_test_config
|
||||||
from .testutils import BaseTestCase
|
from .testutils import BaseTestCase
|
||||||
|
@ -12,10 +12,10 @@ from .testutils import BaseTestCase
|
||||||
setup_test_config({"autodiscover": False})
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
@override_settings(
|
|
||||||
BASE_DIR=Path(__file__).parent.resolve(),
|
|
||||||
)
|
|
||||||
class TemplateLoaderTest(BaseTestCase):
|
class TemplateLoaderTest(BaseTestCase):
|
||||||
|
@override_settings(
|
||||||
|
BASE_DIR=Path(__file__).parent.resolve(),
|
||||||
|
)
|
||||||
def test_get_dirs__base_dir(self):
|
def test_get_dirs__base_dir(self):
|
||||||
current_engine = Engine.get_default()
|
current_engine = Engine.get_default()
|
||||||
loader = Loader(current_engine)
|
loader = Loader(current_engine)
|
||||||
|
@ -24,7 +24,10 @@ class TemplateLoaderTest(BaseTestCase):
|
||||||
sorted(dirs),
|
sorted(dirs),
|
||||||
sorted(
|
sorted(
|
||||||
[
|
[
|
||||||
|
# Top-level /components dir
|
||||||
Path(__file__).parent.resolve() / "components",
|
Path(__file__).parent.resolve() / "components",
|
||||||
|
# App-level /components dir
|
||||||
|
Path(__file__).parent.resolve() / "test_app" / "components",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -42,35 +45,137 @@ class TemplateLoaderTest(BaseTestCase):
|
||||||
self.assertEqual(sorted(dirs), sorted(expected))
|
self.assertEqual(sorted(dirs), sorted(expected))
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
|
BASE_DIR=Path(__file__).parent.resolve(),
|
||||||
STATICFILES_DIRS=[
|
STATICFILES_DIRS=[
|
||||||
Path(__file__).parent.resolve() / "components",
|
Path(__file__).parent.resolve() / "components",
|
||||||
("with_alias", Path(__file__).parent.resolve() / "components"),
|
("with_alias", Path(__file__).parent.resolve() / "components"),
|
||||||
("too_many", "items", Path(__file__).parent.resolve() / "components"),
|
("too_many", Path(__file__).parent.resolve() / "components", Path(__file__).parent.resolve()),
|
||||||
("with_not_str_alias", 3),
|
("with_not_str_alias", 3),
|
||||||
] # noqa
|
], # noqa
|
||||||
)
|
)
|
||||||
@patch("django_components.template_loader.logger.warning")
|
@patch("django_components.template_loader.logger.warning")
|
||||||
def test_get_dirs__staticfiles_dirs(self, mock_warning: MagicMock):
|
def test_get_dirs__components_dirs(self, mock_warning: MagicMock):
|
||||||
mock_warning.reset_mock()
|
mock_warning.reset_mock()
|
||||||
current_engine = Engine.get_default()
|
dirs = get_dirs()
|
||||||
Loader(current_engine).get_dirs()
|
self.assertEqual(
|
||||||
|
sorted(dirs),
|
||||||
comps_path = Path(__file__).parent.resolve() / "components"
|
sorted(
|
||||||
|
[
|
||||||
|
# Top-level /components dir
|
||||||
|
Path(__file__).parent.resolve() / "components",
|
||||||
|
# App-level /components dir
|
||||||
|
Path(__file__).parent.resolve() / "test_app" / "components",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
warn_inputs = [warn.args[0] for warn in mock_warning.call_args_list]
|
warn_inputs = [warn.args[0] for warn in mock_warning.call_args_list]
|
||||||
assert f"Got <class 'tuple'> : ('too_many', 'items', {repr(comps_path)})" in warn_inputs[0]
|
assert "Got <class 'int'> : 3" in warn_inputs[0]
|
||||||
assert "Got <class 'int'> : 3" in warn_inputs[1]
|
|
||||||
|
|
||||||
@override_settings(STATICFILES_DIRS=["components"])
|
@override_settings(
|
||||||
def test_get_dirs__staticfiles_dirs__raises_on_relative_path_1(self):
|
BASE_DIR=Path(__file__).parent.resolve(),
|
||||||
|
COMPONENTS={
|
||||||
|
"dirs": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_get_dirs__components_dirs__empty(self):
|
||||||
|
dirs = get_dirs()
|
||||||
|
self.assertEqual(
|
||||||
|
sorted(dirs),
|
||||||
|
sorted(
|
||||||
|
[
|
||||||
|
# App-level /components dir
|
||||||
|
Path(__file__).parent.resolve()
|
||||||
|
/ "test_app"
|
||||||
|
/ "components",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
BASE_DIR=Path(__file__).parent.resolve(),
|
||||||
|
COMPONENTS={
|
||||||
|
"dirs": ["components"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_get_dirs__componenents_dirs__raises_on_relative_path_1(self):
|
||||||
current_engine = Engine.get_default()
|
current_engine = Engine.get_default()
|
||||||
loader = Loader(current_engine)
|
loader = Loader(current_engine)
|
||||||
with self.assertRaisesMessage(ValueError, "STATICFILES_DIRS must contain absolute paths"):
|
with self.assertRaisesMessage(ValueError, "COMPONENTS.dirs must contain absolute paths"):
|
||||||
loader.get_dirs()
|
loader.get_dirs()
|
||||||
|
|
||||||
@override_settings(STATICFILES_DIRS=[("with_alias", "components")])
|
@override_settings(
|
||||||
def test_get_dirs__staticfiles_dirs__raises_on_relative_path_2(self):
|
BASE_DIR=Path(__file__).parent.resolve(),
|
||||||
|
COMPONENTS={
|
||||||
|
"dirs": [("with_alias", "components")],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_get_dirs__component_dirs__raises_on_relative_path_2(self):
|
||||||
current_engine = Engine.get_default()
|
current_engine = Engine.get_default()
|
||||||
loader = Loader(current_engine)
|
loader = Loader(current_engine)
|
||||||
with self.assertRaisesMessage(ValueError, "STATICFILES_DIRS must contain absolute paths"):
|
with self.assertRaisesMessage(ValueError, "COMPONENTS.dirs must contain absolute paths"):
|
||||||
loader.get_dirs()
|
loader.get_dirs()
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
BASE_DIR=Path(__file__).parent.resolve(),
|
||||||
|
COMPONENTS={
|
||||||
|
"app_dirs": ["custom_comps_dir"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_get_dirs__app_dirs(self):
|
||||||
|
current_engine = Engine.get_default()
|
||||||
|
loader = Loader(current_engine)
|
||||||
|
dirs = loader.get_dirs()
|
||||||
|
self.assertEqual(
|
||||||
|
sorted(dirs),
|
||||||
|
sorted(
|
||||||
|
[
|
||||||
|
# Top-level /components dir
|
||||||
|
Path(__file__).parent.resolve() / "components",
|
||||||
|
# App-level /components dir
|
||||||
|
Path(__file__).parent.resolve() / "test_app" / "custom_comps_dir",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
BASE_DIR=Path(__file__).parent.resolve(),
|
||||||
|
COMPONENTS={
|
||||||
|
"app_dirs": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_get_dirs__app_dirs_empty(self):
|
||||||
|
current_engine = Engine.get_default()
|
||||||
|
loader = Loader(current_engine)
|
||||||
|
dirs = loader.get_dirs()
|
||||||
|
self.assertEqual(
|
||||||
|
sorted(dirs),
|
||||||
|
sorted(
|
||||||
|
[
|
||||||
|
# Top-level /components dir
|
||||||
|
Path(__file__).parent.resolve()
|
||||||
|
/ "components",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
BASE_DIR=Path(__file__).parent.resolve(),
|
||||||
|
COMPONENTS={
|
||||||
|
"app_dirs": ["this_dir_does_not_exist"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_get_dirs__app_dirs_not_found(self):
|
||||||
|
current_engine = Engine.get_default()
|
||||||
|
loader = Loader(current_engine)
|
||||||
|
dirs = loader.get_dirs()
|
||||||
|
self.assertEqual(
|
||||||
|
sorted(dirs),
|
||||||
|
sorted(
|
||||||
|
[
|
||||||
|
# Top-level /components dir
|
||||||
|
Path(__file__).parent.resolve()
|
||||||
|
/ "components",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue