mirror of
https://github.com/django-components/django-components.git
synced 2025-09-22 13:42:27 +00:00
Resolve media and template files relative to component class dir (#395), thanks @JuroOravec
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Emil Stenström <emil@emilstenstrom.se>
This commit is contained in:
parent
1de859bd34
commit
37fd901908
21 changed files with 477 additions and 43 deletions
72
README.md
72
README.md
|
@ -196,7 +196,7 @@ from django_components import component
|
|||
class Calendar(component.Component):
|
||||
# Templates inside `[your apps]/components` dir and `[project root]/components` dir will be automatically found. To customize which template to use based on context
|
||||
# you can override def get_template_name() instead of specifying the below variable.
|
||||
template_name = "calendar/calendar.html"
|
||||
template_name = "calendar.html"
|
||||
|
||||
# This component takes one parameter, a date string to show in the template
|
||||
def get_context_data(self, date):
|
||||
|
@ -205,8 +205,8 @@ class Calendar(component.Component):
|
|||
}
|
||||
|
||||
class Media:
|
||||
css = "calendar/style.css"
|
||||
js = "calendar/script.js"
|
||||
css = "style.css"
|
||||
js = "script.js"
|
||||
```
|
||||
|
||||
And voilá!! We've created our first component.
|
||||
|
@ -681,6 +681,36 @@ COMPONENTS = {
|
|||
}
|
||||
```
|
||||
|
||||
## Logging and debugging
|
||||
|
||||
Django components supports [logging with Django](https://docs.djangoproject.com/en/5.0/howto/logging/#logging-how-to). This can help with troubleshooting.
|
||||
|
||||
To configure logging for Django components, set the `django_components` logger in `LOGGING` in `settings.py` (below).
|
||||
|
||||
Also see the [`settings.py` file in sampleproject](./sampleproject/sampleproject/settings.py) for a real-life example.
|
||||
|
||||
```py
|
||||
import logging
|
||||
import sys
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
'class': 'logging.StreamHandler',
|
||||
'stream': sys.stdout,
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"django_components": {
|
||||
"level": logging.DEBUG,
|
||||
"handlers": ["console"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Management Command
|
||||
|
||||
You can use the built-in management command `startcomponent` to create a django component. The command accepts the following arguments and options:
|
||||
|
@ -761,7 +791,10 @@ One of our goals with `django-components` is to make it easy to share components
|
|||
|
||||
- [django-htmx-components](https://github.com/iwanalabs/django-htmx-components): A set of components for use with [htmx](https://htmx.org/). Try out the [live demo](https://dhc.iwanalabs.com/).
|
||||
|
||||
## Install locally and run the tests
|
||||
|
||||
## Running django-components project locally
|
||||
|
||||
### Install locally and run the tests
|
||||
|
||||
Start by forking the project by clicking the **Fork button** up in the right corner in the GitHub . This makes a copy of the repository in your own name. Now you can clone this repository locally and start adding features:
|
||||
|
||||
|
@ -794,3 +827,34 @@ pyenv install -s 3.12
|
|||
pyenv local 3.6 3.7 3.8 3.9 3.10 3.11 3.12
|
||||
tox -p
|
||||
```
|
||||
|
||||
### Developing against live Django app
|
||||
|
||||
How do you check that your changes to django-components project will work in an actual Django project?
|
||||
|
||||
Use the [sampleproject](./sampleproject/) demo project to validate the changes:
|
||||
|
||||
1. Navigate to [sampleproject](./sampleproject/) directory:
|
||||
```sh
|
||||
cd sampleproject
|
||||
```
|
||||
|
||||
2. Install dependencies from the [requirements.txt](./sampleproject/requirements.txt) file:
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Link to your local version of django-components:
|
||||
```sh
|
||||
pip install -e ..
|
||||
```
|
||||
NOTE: The path (in this case `..`) must point to the directory that has the `setup.py` file.
|
||||
|
||||
4. Start Django server
|
||||
```sh
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
Once the server is up, it should be available at <http://127.0.0.1:8000>.
|
||||
|
||||
To display individual components, add them to the `urls.py`, like in the case of <http://127.0.0.1:8000/greeting>
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import glob
|
||||
import importlib
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import django
|
||||
from django.template.engine import Engine
|
||||
from django.utils.module_loading import autodiscover_modules
|
||||
|
||||
from django_components.template_loader import Loader
|
||||
from django_components.utils.autodiscover import search
|
||||
|
||||
if django.VERSION < (3, 2):
|
||||
default_app_config = "django_components.apps.ComponentsConfig"
|
||||
|
@ -22,11 +20,8 @@ def autodiscover():
|
|||
autodiscover_modules("components")
|
||||
|
||||
# Autodetect a <component>.py file in a components dir
|
||||
current_engine = Engine.get_default()
|
||||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
for directory in dirs:
|
||||
for path in glob.iglob(str(directory / "**/*.py"), recursive=True):
|
||||
component_filepaths = search(search_glob="**/*.py")
|
||||
for path in component_filepaths:
|
||||
import_file(path)
|
||||
|
||||
for path in app_settings.LIBRARIES:
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import difflib
|
||||
import inspect
|
||||
import os
|
||||
from collections import ChainMap
|
||||
from typing import Any, ClassVar, Dict, Iterable, List, Optional, Set, Tuple, Union
|
||||
|
||||
|
@ -25,6 +26,7 @@ from django_components.component_registry import ( # NOQA
|
|||
register,
|
||||
registry,
|
||||
)
|
||||
from django_components.logger import logger
|
||||
from django_components.templatetags.component_tags import (
|
||||
FILLED_SLOTS_CONTENT_CONTEXT_KEY,
|
||||
DefaultFillContent,
|
||||
|
@ -35,6 +37,7 @@ from django_components.templatetags.component_tags import (
|
|||
SlotName,
|
||||
SlotNode,
|
||||
)
|
||||
from django_components.utils.autodiscover import search
|
||||
|
||||
|
||||
class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
||||
|
@ -60,9 +63,104 @@ class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
|||
if hasattr(media, "js") and isinstance(media.js, str):
|
||||
media.js = [media.js]
|
||||
|
||||
_resolve_component_relative_files(attrs)
|
||||
|
||||
return super().__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
def _resolve_component_relative_files(attrs: dict):
|
||||
"""
|
||||
Check if component's HTML, JS and CSS files refer to files in the same directory
|
||||
as the component class. If so, modify the attributes so the class Django's rendering
|
||||
will pick up these files correctly.
|
||||
"""
|
||||
module_name = attrs["__module__"]
|
||||
|
||||
# Prepare all possible directories we need to check when searching for
|
||||
# component's template and media files
|
||||
components_dirs = search()
|
||||
|
||||
# Get the directory where the component class is defined
|
||||
try:
|
||||
comp_dir_abs, comp_dir_rel = _get_dir_path_from_component_module_path(module_name, components_dirs)
|
||||
except RuntimeError:
|
||||
# If no dir was found, we assume that the path is NOT relative to the component dir
|
||||
logger.debug(
|
||||
f"No component directory found for component '{module_name}'."
|
||||
" 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."
|
||||
)
|
||||
return
|
||||
|
||||
# Check if filepath refers to a file that's in the same directory as the component class.
|
||||
# If yes, modify the path to refer to the relative file.
|
||||
# If not, don't modify anything.
|
||||
def resolve_file(filepath: str):
|
||||
maybe_resolved_filepath = os.path.join(comp_dir_abs, filepath)
|
||||
component_import_filepath = os.path.join(comp_dir_rel, filepath)
|
||||
|
||||
if os.path.isfile(maybe_resolved_filepath):
|
||||
logger.debug(
|
||||
f"Interpreting template '{filepath}' of component '{module_name}' relatively to component file"
|
||||
)
|
||||
return component_import_filepath
|
||||
|
||||
logger.debug(
|
||||
f"Interpreting template '{filepath}' of component '{module_name}' relatively to components directory"
|
||||
)
|
||||
return filepath
|
||||
|
||||
# Check if template name is a local file or not
|
||||
if "template_name" in attrs and attrs["template_name"]:
|
||||
attrs["template_name"] = resolve_file(attrs["template_name"])
|
||||
|
||||
if "Media" in attrs:
|
||||
media = attrs["Media"]
|
||||
|
||||
# Now check the same for CSS files
|
||||
if hasattr(media, "css") and isinstance(media.css, dict):
|
||||
for media_type, path_list in media.css.items():
|
||||
media.css[media_type] = [resolve_file(filepath) for filepath in path_list]
|
||||
|
||||
# And JS
|
||||
if hasattr(media, "js") and isinstance(media.js, list):
|
||||
media.js = [resolve_file(filepath) for filepath in media.js]
|
||||
|
||||
|
||||
def _get_dir_path_from_component_module_path(component_module_path: str, candidate_dirs: List[str]):
|
||||
# Transform python module notation "pkg.module.name" to file path "pkg/module/name"
|
||||
# Thus, we should get file path relative to Django project root
|
||||
comp_path = os.sep.join(component_module_path.split("."))
|
||||
comp_dir_path = os.path.dirname(comp_path)
|
||||
|
||||
# NOTE: We assume that Django project root == current working directory!
|
||||
cwd = os.getcwd()
|
||||
comp_dir_path_abs = os.path.join(cwd, comp_dir_path)
|
||||
|
||||
# From all dirs defined in settings.STATICFILES_DIRS, find one that's the parent
|
||||
# to the component file.
|
||||
root_dir_abs = None
|
||||
for candidate_dir in candidate_dirs:
|
||||
candidate_dir_abs = os.path.abspath(candidate_dir)
|
||||
if comp_dir_path_abs.startswith(candidate_dir_abs):
|
||||
root_dir_abs = candidate_dir_abs
|
||||
break
|
||||
|
||||
if root_dir_abs is None:
|
||||
raise RuntimeError(
|
||||
f"Failed to resolve template directory for component '{component_module_path}'",
|
||||
)
|
||||
|
||||
# Derive the path from matched STATICFILES_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:
|
||||
# - Absolute path is used to check if the file exists
|
||||
# - Relative path is used for defining the import on the component class
|
||||
return comp_dir_path_abs, comp_dir_path_rel
|
||||
|
||||
|
||||
class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||
# Either template_name or template must be set on subclass OR subclass must implement get_template() with
|
||||
# non-null return.
|
||||
|
|
3
django_components/logger.py
Normal file
3
django_components/logger.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
import logging
|
||||
|
||||
logger = logging.getLogger("django_components")
|
|
@ -2,34 +2,84 @@
|
|||
Template loader that loads templates from each Django app's "components" directory.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Set
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.template.loaders.filesystem import Loader as FilesystemLoader
|
||||
from django.template.utils import get_app_template_dirs
|
||||
|
||||
from django_components.logger import logger
|
||||
|
||||
|
||||
# Same as `Path.is_relative_to`, defined as standalone function because `Path.is_relative_to`
|
||||
# is marked for deprecation.
|
||||
def path_is_relative_to(child_path: str, parent_path: str):
|
||||
# If the relative path doesn't start with `..`, then child is descendant of parent
|
||||
# See https://stackoverflow.com/a/7288073/9788634
|
||||
rel_path = os.path.relpath(child_path, parent_path)
|
||||
return not rel_path.startswith("..")
|
||||
|
||||
|
||||
class Loader(FilesystemLoader):
|
||||
def get_dirs(self):
|
||||
component_dir = "components"
|
||||
directories = set(get_app_template_dirs(component_dir))
|
||||
# Allow to configure from settings which dirs should be checked for components
|
||||
if hasattr(settings, "STATICFILES_DIRS") and len(settings.STATICFILES_DIRS):
|
||||
component_dirs = settings.STATICFILES_DIRS
|
||||
else:
|
||||
component_dirs = ["components"]
|
||||
|
||||
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()
|
||||
for component_dir in component_dirs:
|
||||
curr_directories: Set[Path] = set()
|
||||
|
||||
# For each dir in `settings.STATICFILES_DIRS`, we go over all Django apps
|
||||
# and, for each app, check if the STATICFILES_DIRS dir is within that app dir.
|
||||
# If so, we add the dir as a valid source.
|
||||
# The for loop is based on Django's `get_app_template_dirs`.
|
||||
for app_config in apps.get_app_configs():
|
||||
if not app_config.path:
|
||||
continue
|
||||
if not Path(component_dir).is_dir():
|
||||
continue
|
||||
|
||||
if path_is_relative_to(component_dir, app_config.path):
|
||||
curr_directories.add(Path(component_dir).resolve())
|
||||
|
||||
if hasattr(settings, "BASE_DIR"):
|
||||
path = (Path(settings.BASE_DIR) / component_dir).resolve()
|
||||
if path.is_dir():
|
||||
directories.add(path)
|
||||
curr_directories.add(path)
|
||||
|
||||
# Add the directory that holds the settings file
|
||||
if settings.SETTINGS_MODULE:
|
||||
module_parts = settings.SETTINGS_MODULE.split(".")
|
||||
module_path = Path(*module_parts)
|
||||
|
||||
# - If len() == 2, then path to settings file is <app_name>/<settings_file>.py
|
||||
# - If len() > 2, then we assume that settings file is in a settings directory,
|
||||
# e.g. <app_name>/settings/<settings_file>.py
|
||||
if len(module_parts) > 2:
|
||||
module_path = Path(*module_parts[:-1])
|
||||
|
||||
# Use list() for < Python 3.9
|
||||
# Get the paths for the nested settings dir like `<app_name>/settings`,
|
||||
# or for non-nested dir like `<app_name>`
|
||||
#
|
||||
# NOTE: Use list() for < Python 3.9
|
||||
for parent in list(module_path.parents)[:2]:
|
||||
path = (parent / component_dir).resolve()
|
||||
if path.is_dir():
|
||||
directories.add(path)
|
||||
curr_directories.add(path)
|
||||
|
||||
directories.update(curr_directories)
|
||||
|
||||
logger.debug(
|
||||
"Template loader matched following template dirs:\n" + "\n".join([f" - {str(d)}" for d in directories])
|
||||
)
|
||||
return list(directories)
|
||||
|
|
32
django_components/utils/autodiscover.py
Normal file
32
django_components/utils/autodiscover.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
import glob
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from django.template.engine import Engine
|
||||
|
||||
from django_components.template_loader import Loader
|
||||
|
||||
|
||||
def search(search_glob: Optional[str] = None, engine: Optional[Engine] = None):
|
||||
"""
|
||||
Search for directories that may contain components.
|
||||
|
||||
If `search_glob` is given, the directories are searched for said glob pattern,
|
||||
and glob search results are returned as a flattened list.
|
||||
"""
|
||||
current_engine = engine
|
||||
if current_engine is None:
|
||||
current_engine = Engine.get_default()
|
||||
|
||||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
|
||||
if search_glob is None:
|
||||
return dirs
|
||||
|
||||
component_filenames: List[str] = []
|
||||
for directory in dirs:
|
||||
for path in glob.iglob(str(Path(directory) / search_glob), recursive=True):
|
||||
component_filenames.append(path)
|
||||
|
||||
return component_filenames
|
|
@ -23,3 +23,27 @@ class Calendar(component.Component):
|
|||
class Media:
|
||||
css = "calendar/calendar.css"
|
||||
js = "calendar/calendar.js"
|
||||
|
||||
|
||||
@component.register("calendar_relative")
|
||||
class CalendarRelative(component.Component):
|
||||
# Note that Django will look for templates inside `[your apps]/components` dir and
|
||||
# `[project root]/components` dir. To customize which template to use based on context
|
||||
# you can override def get_template_name() instead of specifying the below variable.
|
||||
template_name = "calendar.html"
|
||||
|
||||
# This component takes one parameter, a date string to show in the template
|
||||
def get_context_data(self, date):
|
||||
return {
|
||||
"date": date,
|
||||
}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = {
|
||||
"date": request.GET.get("date", ""),
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
class Media:
|
||||
css = "calendar.css"
|
||||
js = "calendar.js"
|
||||
|
|
2
sampleproject/components/nested/calendar/calendar.css
Normal file
2
sampleproject/components/nested/calendar/calendar.css
Normal file
|
@ -0,0 +1,2 @@
|
|||
.calendar-component { width: 200px; background: pink; }
|
||||
.calendar-component span { font-weight: bold; }
|
19
sampleproject/components/nested/calendar/calendar.html
Normal file
19
sampleproject/components/nested/calendar/calendar.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<div class="calendar-component">
|
||||
<div>Today's date is <span>{{ date }}</span></div>
|
||||
<div>Your to-dos:</div>
|
||||
<ul>
|
||||
<li>
|
||||
{% component "todo" %}
|
||||
{% fill "todo_text" %}
|
||||
Stop forgetting the milk!
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
</li>
|
||||
<li>
|
||||
{% component "todo" %}
|
||||
{# As of v0.28, 'fill' tag optional for 1-slot filling if component template specifies a 'default' slot #}
|
||||
Wear all-white clothes to laser tag tournament.
|
||||
{% endcomponent %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
5
sampleproject/components/nested/calendar/calendar.js
Normal file
5
sampleproject/components/nested/calendar/calendar.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
(function(){
|
||||
if (document.querySelector(".calendar-component")) {
|
||||
document.querySelector(".calendar-component").onclick = function(){ alert("Clicked calendar!"); };
|
||||
}
|
||||
})()
|
25
sampleproject/components/nested/calendar/calendar.py
Normal file
25
sampleproject/components/nested/calendar/calendar.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from django_components import component
|
||||
|
||||
|
||||
@component.register("calendar_nested")
|
||||
class CalendarNested(component.Component):
|
||||
# Note that Django will look for templates inside `[your apps]/components` dir and
|
||||
# `[project root]/components` dir. To customize which template to use based on context
|
||||
# you can override def get_template_name() instead of specifying the below variable.
|
||||
template_name = "calendar.html"
|
||||
|
||||
# This component takes one parameter, a date string to show in the template
|
||||
def get_context_data(self, date):
|
||||
return {
|
||||
"date": date,
|
||||
}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = {
|
||||
"date": request.GET.get("date", ""),
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
class Media:
|
||||
css = "calendar.css"
|
||||
js = "calendar.js"
|
|
@ -1,8 +1,11 @@
|
|||
from components.calendar.calendar import Calendar
|
||||
from components.calendar.calendar import Calendar, CalendarRelative
|
||||
from components.greeting import Greeting
|
||||
from components.nested.calendar.calendar import CalendarNested
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path("greeting/", Greeting.as_view(), name="greeting"),
|
||||
path("calendar/", Calendar.as_view(), name="calendar"),
|
||||
path("calendar-relative/", CalendarRelative.as_view(), name="calendar-relative"),
|
||||
path("calendar-nested/", CalendarNested.as_view(), name="calendar-nested"),
|
||||
]
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
|
@ -133,3 +135,20 @@ STATICFILES_DIRS = [BASE_DIR / "components"]
|
|||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
STATIC_ROOT = "staticfiles"
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": sys.stdout,
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"django_components": {
|
||||
"level": logging.DEBUG,
|
||||
"handlers": ["console"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
3
tests/components/relative_file/relative_file.css
Normal file
3
tests/components/relative_file/relative_file.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.html-css-only {
|
||||
color: blue;
|
||||
}
|
5
tests/components/relative_file/relative_file.html
Normal file
5
tests/components/relative_file/relative_file.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/components/relative_file/relative_file.js
Normal file
1
tests/components/relative_file/relative_file.js
Normal file
|
@ -0,0 +1 @@
|
|||
console.log("JS file");
|
24
tests/components/relative_file/relative_file.py
Normal file
24
tests/components/relative_file/relative_file.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
from django.http import HttpResponse
|
||||
|
||||
from django_components import component
|
||||
|
||||
|
||||
@component.register("relative_file_component")
|
||||
class RelativeFileComponent(component.Component):
|
||||
template_name = "relative_file.html"
|
||||
|
||||
class Media:
|
||||
js = "relative_file.js"
|
||||
css = "relative_file.css"
|
||||
|
||||
def post(self, request, *args, **kwargs) -> HttpResponse:
|
||||
variable = request.POST.get("variable")
|
||||
return self.render_to_response({"variable": variable})
|
||||
|
||||
def get(self, request, *args, **kwargs) -> HttpResponse:
|
||||
return self.render_to_response({"variable": "GET"})
|
||||
|
||||
def get_context_data(self, variable, *args, **kwargs) -> Dict[str, Any]:
|
||||
return {"variable": variable}
|
|
@ -7,7 +7,10 @@ if not settings.configured:
|
|||
TEMPLATES=[
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": ["tests/templates/"],
|
||||
"DIRS": [
|
||||
"tests/templates/",
|
||||
"tests/components/", # Required for template relative imports in tests
|
||||
],
|
||||
}
|
||||
],
|
||||
COMPONENTS={"template_cache_size": 128},
|
||||
|
|
|
@ -40,7 +40,14 @@ class TestLoaderSettingsModule(SimpleTestCase):
|
|||
current_engine = Engine.get_default()
|
||||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
self.assertEqual(dirs, [Path(__file__).parent.resolve() / "components"])
|
||||
self.assertEqual(
|
||||
sorted(dirs),
|
||||
sorted(
|
||||
[
|
||||
Path(__file__).parent.resolve() / "components",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
def test_complex_settings_module(self):
|
||||
settings.SETTINGS_MODULE = "tests.test_structures.test_structure_1.config.settings" # noqa
|
||||
|
@ -49,8 +56,12 @@ class TestLoaderSettingsModule(SimpleTestCase):
|
|||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
self.assertEqual(
|
||||
dirs,
|
||||
[Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components"],
|
||||
sorted(dirs),
|
||||
sorted(
|
||||
[
|
||||
Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
def test_complex_settings_module_2(self):
|
||||
|
@ -60,8 +71,16 @@ class TestLoaderSettingsModule(SimpleTestCase):
|
|||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
self.assertEqual(
|
||||
dirs,
|
||||
[Path(__file__).parent.resolve() / "test_structures" / "test_structure_2" / "project" / "components"],
|
||||
sorted(dirs),
|
||||
sorted(
|
||||
[
|
||||
Path(__file__).parent.resolve()
|
||||
/ "test_structures"
|
||||
/ "test_structure_2"
|
||||
/ "project"
|
||||
/ "components",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
def test_complex_settings_module_3(self):
|
||||
|
@ -71,8 +90,8 @@ class TestLoaderSettingsModule(SimpleTestCase):
|
|||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
expected = [
|
||||
(Path(__file__).parent.resolve() / "test_structures" / "test_structure_3" / "components"),
|
||||
(Path(__file__).parent.resolve() / "test_structures" / "test_structure_3" / "project" / "components"),
|
||||
Path(__file__).parent.resolve() / "test_structures" / "test_structure_3" / "components",
|
||||
Path(__file__).parent.resolve() / "test_structures" / "test_structure_3" / "project" / "components",
|
||||
]
|
||||
self.assertEqual(
|
||||
sorted(dirs),
|
||||
|
@ -93,5 +112,7 @@ class TestBaseDir(SimpleTestCase):
|
|||
current_engine = Engine.get_default()
|
||||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
expected = [Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components"]
|
||||
self.assertEqual(dirs, expected)
|
||||
expected = [
|
||||
Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components",
|
||||
]
|
||||
self.assertEqual(sorted(dirs), sorted(expected))
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.template import Context, Template
|
||||
from django.test import override_settings
|
||||
|
||||
# isort: off
|
||||
from .django_test_setup import * # NOQA
|
||||
|
@ -383,6 +385,38 @@ class ComponentMediaTests(SimpleTestCase):
|
|||
),
|
||||
)
|
||||
|
||||
@override_settings(
|
||||
BASE_DIR=Path(__file__).resolve().parent,
|
||||
STATICFILES_DIRS=[
|
||||
Path(__file__).resolve().parent / "components",
|
||||
],
|
||||
)
|
||||
def test_component_media_with_dict_with_relative_paths(self):
|
||||
from .components.relative_file.relative_file import RelativeFileComponent
|
||||
|
||||
comp = RelativeFileComponent("")
|
||||
|
||||
self.assertHTMLEqual(
|
||||
comp.render_dependencies(),
|
||||
dedent(
|
||||
"""\
|
||||
<link href="relative_file/relative_file.css" media="all" rel="stylesheet">
|
||||
<script src="relative_file/relative_file.js"></script>
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
rendered = comp.render(Context(comp.get_context_data(variable="test")))
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<form method="post">
|
||||
<input type="text" name="variable" value="test">
|
||||
<input type="submit">
|
||||
</form>
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
class ComponentIsolationTests(SimpleTestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -246,9 +246,13 @@ class ContextCalledOnceTests(SimpleTestCase):
|
|||
"{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component name='incrementer' %}{% endcomponent %}"
|
||||
)
|
||||
rendered = template.render(Context()).strip()
|
||||
|
||||
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
|
||||
rendered = template.render(Context()).strip().replace("\n", "")
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
'<link href="relative_file/relative_file.css" media="all" rel="stylesheet">'
|
||||
'<script src="relative_file/relative_file.js"></script>'
|
||||
'<p class="incrementer">value=1;calls=1</p>',
|
||||
)
|
||||
|
||||
def test_one_context_call_with_simple_component_and_arg(self):
|
||||
template = Template("{% load component_tags %}{% component name='incrementer' value='2' %}{% endcomponent %}")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue