mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34:59 +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
|
||||
|
||||
🚨📢 **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**
|
||||
- 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.
|
||||
|
@ -196,14 +206,25 @@ _You are advised to read this section before using django-components in producti
|
|||
|
||||
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.
|
||||
|
||||
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.
|
||||
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**_.
|
||||
It is a drop-in replacement for _django.contrib.staticfiles_.
|
||||
From _v0.27_ until _v0.100_, django-components shipped with an additional installable app _django_components.**safer_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.
|
||||
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
|
||||
|
||||
1. Install the app into your environment:
|
||||
1. Install `django_components` into your environment:
|
||||
|
||||
> `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
|
||||
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
|
||||
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,`_
|
||||
- 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
|
||||
STATICFILES_DIRS = [
|
||||
...,
|
||||
os.path.join(BASE_DIR, "components"),
|
||||
]
|
||||
```
|
||||
```py
|
||||
STATICFILES_FINDERS = [
|
||||
# Default finders
|
||||
"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
|
||||
|
||||
|
@ -397,7 +437,7 @@ class Calendar(Component):
|
|||
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
|
||||
# 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"
|
||||
# Or
|
||||
def get_template_name(context):
|
||||
|
@ -409,7 +449,7 @@ class Calendar(Component):
|
|||
"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:
|
||||
css = "style.css"
|
||||
js = "script.js"
|
||||
|
@ -1230,7 +1270,7 @@ class MyAppConfig(AppConfig):
|
|||
|
||||
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.
|
||||
|
||||
|
@ -1589,17 +1629,17 @@ Consider a component with slot(s). This component may do some processing on the
|
|||
```py
|
||||
@register("my_comp")
|
||||
class MyComp(Component):
|
||||
template = """
|
||||
<div>
|
||||
{% slot "content" default %}
|
||||
input: {{ input }}
|
||||
{% endslot %}
|
||||
</div>
|
||||
"""
|
||||
template = """
|
||||
<div>
|
||||
{% slot "content" default %}
|
||||
input: {{ input }}
|
||||
{% endslot %}
|
||||
</div>
|
||||
"""
|
||||
|
||||
def get_context_data(self, input):
|
||||
processed_input = do_something(input)
|
||||
return {"input": processed_input}
|
||||
def get_context_data(self, input):
|
||||
processed_input = do_something(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.
|
||||
|
@ -1618,17 +1658,17 @@ To pass the data to the `slot` tag, simply pass them as keyword attributes (`key
|
|||
```py
|
||||
@register("my_comp")
|
||||
class MyComp(Component):
|
||||
template = """
|
||||
<div>
|
||||
{% slot "content" default input=input %}
|
||||
input: {{ input }}
|
||||
{% endslot %}
|
||||
</div>
|
||||
"""
|
||||
template = """
|
||||
<div>
|
||||
{% slot "content" default input=input %}
|
||||
input: {{ input }}
|
||||
{% endslot %}
|
||||
</div>
|
||||
"""
|
||||
|
||||
def get_context_data(self, input):
|
||||
processed_input = do_something(input)
|
||||
return {
|
||||
def get_context_data(self, input):
|
||||
processed_input = do_something(input)
|
||||
return {
|
||||
"input": processed_input,
|
||||
}
|
||||
```
|
||||
|
@ -1962,13 +2002,13 @@ Assuming that:
|
|||
class_from_var = "from-var"
|
||||
|
||||
attrs = {
|
||||
"class": "from-attrs",
|
||||
"type": "submit",
|
||||
"class": "from-attrs",
|
||||
"type": "submit",
|
||||
}
|
||||
|
||||
defaults = {
|
||||
"class": "from-defaults",
|
||||
"role": "button",
|
||||
"class": "from-defaults",
|
||||
"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.
|
||||
|
||||
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
|
||||
# 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.
|
||||
|
||||
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:
|
||||
|
||||
|
@ -3091,10 +3131,27 @@ Here's overview of all available settings and their defaults:
|
|||
COMPONENTS = {
|
||||
"autodiscover": True,
|
||||
"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",
|
||||
"libraries": [], # ["mysite.components.forms", ...]
|
||||
"multiline_tags": True,
|
||||
"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",
|
||||
"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`
|
||||
|
||||
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
|
||||
|
||||
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`
|
||||
|
||||
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.
|
||||
|
||||
|
|
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:
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ class Calendar(Component):
|
|||
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
|
||||
# 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"
|
||||
# Or
|
||||
# def get_template_name(context):
|
||||
|
@ -19,10 +19,11 @@ class Calendar(Component):
|
|||
}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = {
|
||||
"date": request.GET.get("date", ""),
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
return self.render_to_response(
|
||||
kwargs={
|
||||
"date": request.GET.get("date", ""),
|
||||
},
|
||||
)
|
||||
|
||||
class Media:
|
||||
css = "calendar/calendar.css"
|
||||
|
@ -34,7 +35,7 @@ class CalendarRelative(Component):
|
|||
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
|
||||
# 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"
|
||||
# Or
|
||||
# def get_template_name(context):
|
||||
|
|
|
@ -7,8 +7,12 @@ from django_components import Component, register, types
|
|||
class Greeting(Component):
|
||||
def get(self, request, *args, **kwargs):
|
||||
slots = {"message": "Hello, world!"}
|
||||
context = {"name": request.GET.get("name", "")}
|
||||
return self.render_to_response(context=context, slots=slots)
|
||||
return self.render_to_response(
|
||||
slots=slots,
|
||||
kwargs={
|
||||
"name": request.GET.get("name", ""),
|
||||
},
|
||||
)
|
||||
|
||||
def get_context_data(self, name, *args, **kwargs) -> Dict[str, Any]:
|
||||
return {"name": name}
|
||||
|
|
|
@ -6,7 +6,7 @@ class CalendarNested(Component):
|
|||
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
|
||||
# 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"
|
||||
# Or
|
||||
# def get_template_name(context):
|
||||
|
@ -19,10 +19,11 @@ class CalendarNested(Component):
|
|||
}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = {
|
||||
"date": request.GET.get("date", ""),
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
return self.render_to_response(
|
||||
kwargs={
|
||||
"date": request.GET.get("date", ""),
|
||||
},
|
||||
)
|
||||
|
||||
class Media:
|
||||
css = "calendar.css"
|
||||
|
|
|
@ -28,10 +28,8 @@ INSTALLED_APPS = [
|
|||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
# Replaced by django_components.safer_staticfiles as of v0.27:
|
||||
# "django.contrib.staticfiles",
|
||||
"django.contrib.staticfiles",
|
||||
"django_components",
|
||||
"django_components.safer_staticfiles",
|
||||
"calendarapp",
|
||||
]
|
||||
# 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"
|
||||
|
||||
# COMPONENTS = {
|
||||
# "autodiscover": True,
|
||||
# "libraries": [],
|
||||
# "template_cache_size": 128,
|
||||
# "context_behavior": "isolated", # "django" | "isolated"
|
||||
# }
|
||||
COMPONENTS = {
|
||||
# "autodiscover": True,
|
||||
"dirs": [BASE_DIR / "components"],
|
||||
# "app_dirs": ["components"],
|
||||
# "libraries": [],
|
||||
# "template_cache_size": 128,
|
||||
# "context_behavior": "isolated", # "django" | "isolated"
|
||||
}
|
||||
|
||||
|
||||
# Database
|
||||
|
@ -135,7 +143,6 @@ USE_TZ = True
|
|||
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATICFILES_DIRS = [BASE_DIR / "components"]
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import re
|
||||
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
|
||||
|
||||
|
@ -98,6 +99,14 @@ class AppSettings:
|
|||
def AUTODISCOVER(self) -> bool:
|
||||
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
|
||||
def DYNAMIC_COMPONENT_NAME(self) -> str:
|
||||
return self.settings.get("dynamic_component_name", "dynamic")
|
||||
|
@ -118,6 +127,51 @@ class AppSettings:
|
|||
def TEMPLATE_CACHE_SIZE(self) -> int:
|
||||
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
|
||||
def CONTEXT_BEHAVIOR(self) -> ContextBehavior:
|
||||
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 django.conf import settings
|
||||
from django.template.engine import Engine
|
||||
|
||||
from django_components.logger import logger
|
||||
from django_components.template_loader import Loader
|
||||
from django_components.template_loader import get_dirs
|
||||
|
||||
|
||||
def autodiscover(
|
||||
|
@ -27,7 +26,13 @@ def autodiscover(
|
|||
component_filepaths = search_dirs(dirs, "**/*.py")
|
||||
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)
|
||||
|
||||
|
||||
|
@ -89,19 +94,6 @@ def _filepath_to_python_module(file_path: Union[Path, str]) -> str:
|
|||
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]:
|
||||
"""
|
||||
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}"
|
||||
" 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"
|
||||
" specified in the Django's 'STATICFILES_DIRS' settings."
|
||||
" specified in the Django's 'COMPONENTS.dirs' settings."
|
||||
)
|
||||
return
|
||||
|
||||
|
@ -327,7 +327,7 @@ def _get_dir_path_from_component_path(
|
|||
) -> Tuple[str, str]:
|
||||
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.
|
||||
root_dir_abs = None
|
||||
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}'",
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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 typing import List, Set
|
||||
from typing import List, Optional, Set
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.template.engine import Engine
|
||||
from django.template.loaders.filesystem import Loader as FilesystemLoader
|
||||
|
||||
from django_components.app_settings import app_settings
|
||||
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):
|
||||
def get_dirs(self) -> List[Path]:
|
||||
"""
|
||||
Prepare directories that may contain component files:
|
||||
|
||||
Searches for dirs set in `STATICFILES_DIRS` settings. If none set, defaults to searching
|
||||
for a "components" app. The dirs in `STATICFILES_DIRS` must be absolute paths.
|
||||
Searches for dirs set in `COMPONENTS.dirs` settings. If none set, defaults to searching
|
||||
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.
|
||||
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
|
||||
if hasattr(settings, "STATICFILES_DIRS") and settings.STATICFILES_DIRS:
|
||||
component_dirs = settings.STATICFILES_DIRS
|
||||
else:
|
||||
component_dirs = [settings.BASE_DIR / "components"]
|
||||
component_dirs = app_settings.DIRS
|
||||
|
||||
# TODO_REMOVE_IN_V1
|
||||
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(
|
||||
"Template loader will search for valid template dirs from following options:\n"
|
||||
+ "\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:
|
||||
# Consider tuples for STATICFILES_DIRS (See #489)
|
||||
# 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]
|
||||
try:
|
||||
Path(component_dir)
|
||||
except TypeError:
|
||||
logger.warning(
|
||||
f"STATICFILES_DIRS expected str, bytes or os.PathLike object, or tuple/list of length 2. "
|
||||
f"See Django documentation. Got {type(component_dir)} : {component_dir}"
|
||||
f"{source} expected str, bytes or os.PathLike object, or tuple/list of length 2. "
|
||||
f"See Django documentation for STATICFILES_DIRS. Got {type(component_dir)} : {component_dir}"
|
||||
)
|
||||
continue
|
||||
|
||||
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:
|
||||
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])
|
||||
)
|
||||
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 re
|
||||
import sys
|
||||
import typing
|
||||
from pathlib import Path
|
||||
|
@ -211,3 +212,11 @@ def lazy_cache(
|
|||
return cast(TFunc, wrapper)
|
||||
|
||||
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
|
||||
|
||||
|
||||
# Used for testing the safer_staticfiles app in `test_safer_staticfiles.py`
|
||||
@register("safer_staticfiles_component")
|
||||
# Used for testing the staticfiles finder in `test_staticfiles.py`
|
||||
@register("staticfiles_component")
|
||||
class RelativeFileWithPathObjComponent(Component):
|
||||
template_name = "safer_staticfiles.html"
|
||||
template_name = "staticfiles.html"
|
||||
|
||||
class Media:
|
||||
js = "safer_staticfiles.js"
|
||||
css = "safer_staticfiles.css"
|
||||
js = "staticfiles.js"
|
||||
css = "staticfiles.css"
|
||||
|
||||
def get_context_data(self, variable, *args, **kwargs) -> Dict[str, Any]:
|
||||
return {"variable": variable}
|
|
@ -11,7 +11,7 @@ def setup_test_config(components: Optional[Dict] = None):
|
|||
|
||||
settings.configure(
|
||||
BASE_DIR=Path(__file__).resolve().parent,
|
||||
INSTALLED_APPS=("django_components",),
|
||||
INSTALLED_APPS=("django_components", "tests.test_app"),
|
||||
TEMPLATES=[
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
|
@ -32,6 +32,7 @@ def setup_test_config(components: Optional[Dict] = None):
|
|||
"NAME": ":memory:",
|
||||
}
|
||||
},
|
||||
SECRET_KEY="secret",
|
||||
)
|
||||
|
||||
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
|
||||
STATIC_URL="static/",
|
||||
STATIC_ROOT=os.path.join(Path(__file__).resolve().parent, "static_root"),
|
||||
# Either `django.contrib.staticfiles` or `django_components.safer_staticfiles` MUST
|
||||
# be installed for staticfiles resolution to work.
|
||||
# `django.contrib.staticfiles` MUST be installed for staticfiles resolution to work.
|
||||
INSTALLED_APPS=[
|
||||
"django_components.safer_staticfiles", # Or django.contrib.staticfiles
|
||||
"django.contrib.staticfiles",
|
||||
"django_components",
|
||||
],
|
||||
)
|
||||
|
@ -675,10 +674,9 @@ class MediaStaticfilesTests(BaseTestCase):
|
|||
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
|
||||
},
|
||||
},
|
||||
# Either `django.contrib.staticfiles` or `django_components.safer_staticfiles` MUST
|
||||
# be installed for staticfiles resolution to work.
|
||||
# `django.contrib.staticfiles` MUST be installed for staticfiles resolution to work.
|
||||
INSTALLED_APPS=[
|
||||
"django_components.safer_staticfiles", # Or django.contrib.staticfiles
|
||||
"django.contrib.staticfiles",
|
||||
"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_reg = ComponentRegistry(library=my_lib)
|
||||
|
||||
default_registry_comps_before = len(registry.all())
|
||||
|
||||
self.assertDictEqual(my_reg.all(), {})
|
||||
self.assertDictEqual(registry.all(), {})
|
||||
|
||||
@register("decorated_component", registry=my_reg)
|
||||
class TestComponent(Component):
|
||||
pass
|
||||
|
||||
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):
|
||||
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.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 .testutils import BaseTestCase
|
||||
|
@ -12,10 +12,10 @@ from .testutils import BaseTestCase
|
|||
setup_test_config({"autodiscover": False})
|
||||
|
||||
|
||||
@override_settings(
|
||||
BASE_DIR=Path(__file__).parent.resolve(),
|
||||
)
|
||||
class TemplateLoaderTest(BaseTestCase):
|
||||
@override_settings(
|
||||
BASE_DIR=Path(__file__).parent.resolve(),
|
||||
)
|
||||
def test_get_dirs__base_dir(self):
|
||||
current_engine = Engine.get_default()
|
||||
loader = Loader(current_engine)
|
||||
|
@ -24,7 +24,10 @@ class TemplateLoaderTest(BaseTestCase):
|
|||
sorted(dirs),
|
||||
sorted(
|
||||
[
|
||||
# Top-level /components dir
|
||||
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))
|
||||
|
||||
@override_settings(
|
||||
BASE_DIR=Path(__file__).parent.resolve(),
|
||||
STATICFILES_DIRS=[
|
||||
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),
|
||||
] # noqa
|
||||
], # noqa
|
||||
)
|
||||
@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()
|
||||
current_engine = Engine.get_default()
|
||||
Loader(current_engine).get_dirs()
|
||||
|
||||
comps_path = Path(__file__).parent.resolve() / "components"
|
||||
dirs = 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" / "components",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
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[1]
|
||||
assert "Got <class 'int'> : 3" in warn_inputs[0]
|
||||
|
||||
@override_settings(STATICFILES_DIRS=["components"])
|
||||
def test_get_dirs__staticfiles_dirs__raises_on_relative_path_1(self):
|
||||
@override_settings(
|
||||
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()
|
||||
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()
|
||||
|
||||
@override_settings(STATICFILES_DIRS=[("with_alias", "components")])
|
||||
def test_get_dirs__staticfiles_dirs__raises_on_relative_path_2(self):
|
||||
@override_settings(
|
||||
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()
|
||||
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()
|
||||
|
||||
@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