mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34:59 +00:00
refactor: usage notes + tests for safer_staticfiles (#538)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
3dadba6636
commit
23d91218bd
13 changed files with 226 additions and 4 deletions
17
README.md
17
README.md
|
@ -16,7 +16,7 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
|
|||
<div class="calendar-component">Today's date is <span>2015-06-19</span></div>
|
||||
```
|
||||
|
||||
Read on to learn about the details!
|
||||
[See the example project](./sampleproject) or read on to learn about the details!
|
||||
|
||||
## Table of Contents
|
||||
|
||||
|
@ -118,6 +118,21 @@ INSTALLED_APPS = [
|
|||
If you are on an older version of django-components, your alternatives are a) passing `--ignore <pattern>` options to the _collecstatic_ CLI command, or b) defining a subclass of StaticFilesConfig.
|
||||
Both routes are described in the official [docs of the _staticfiles_ app](https://docs.djangoproject.com/en/4.2/ref/contrib/staticfiles/#customizing-the-ignored-pattern-list).
|
||||
|
||||
Note that `safer_staticfiles` excludes the `.py` and `.html` files for [collectstatic command](https://docs.djangoproject.com/en/5.0/ref/contrib/staticfiles/#collectstatic):
|
||||
|
||||
```sh
|
||||
python manage.py collectstatic
|
||||
```
|
||||
|
||||
but it is ignored on the [development server](https://docs.djangoproject.com/en/5.0/ref/django-admin/#runserver):
|
||||
|
||||
```sh
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
For a step-by-step guide on deploying production server with static files,
|
||||
[see the demo project](./sampleproject/README.md).
|
||||
|
||||
## Installation
|
||||
|
||||
Install the app into your environment:
|
||||
|
|
1
sampleproject/.gitignore
vendored
1
sampleproject/.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
.python-version
|
||||
*.sqlite3
|
||||
staticfiles
|
66
sampleproject/README.md
Normal file
66
sampleproject/README.md
Normal file
|
@ -0,0 +1,66 @@
|
|||
# Sample Django project with django_components
|
||||
|
||||
## Installation
|
||||
|
||||
1. Prepare virtual environment:
|
||||
|
||||
```sh
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Development server
|
||||
|
||||
```sh
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
The app will be available at http://localhost:8000/.
|
||||
|
||||
### Serving static files
|
||||
|
||||
Assuming that you're running the dev server with `DEBUG=True` setting, ALL
|
||||
static files (JS/CSS/HTML/PY) will be accessible under the `/static/` URL path.
|
||||
|
||||
## Production server
|
||||
|
||||
1. Prepare static files
|
||||
|
||||
```sh
|
||||
python manage.py collectstatic
|
||||
```
|
||||
|
||||
2. Set `DEBUG = False` in [settings.py](./sampleproject/settings.py).
|
||||
|
||||
3. Start server with gunicorn
|
||||
|
||||
```sh
|
||||
gunicorn sampleproject.wsgi:application
|
||||
```
|
||||
|
||||
The app will be available at http://localhost:8000/.
|
||||
|
||||
### Serving static files
|
||||
|
||||
This project uses [WhiteNoise](https://whitenoise.readthedocs.io/en/stable/) to configure Django to serve static files
|
||||
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`
|
||||
|
||||
Then Django will server only JS and CSS files under the `/static/` URL path.
|
||||
|
||||
You can verify that this is true by starting the prod server and then navigating to:
|
||||
|
||||
- http://127.0.0.1:8000/static/calendar/calendar.js
|
||||
- http://127.0.0.1:8000/static/calendar/calendar.css
|
||||
- http://127.0.0.1:8000/static/calendar/calendar.html
|
||||
- http://127.0.0.1:8000/static/calendar/calendar.py
|
|
@ -1,2 +1,4 @@
|
|||
django
|
||||
django_components
|
||||
gunicorn
|
||||
whitenoise
|
|
@ -19,7 +19,7 @@ SECRET_KEY = os.environ.get("SECRET_KEY", secrets.token_hex(100))
|
|||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS: List[str] = []
|
||||
ALLOWED_HOSTS: List[str] = ["127.0.0.1", "localhost"]
|
||||
|
||||
|
||||
INSTALLED_APPS = [
|
||||
|
@ -39,6 +39,7 @@ INSTALLED_APPS = [
|
|||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
|
|
|
@ -10,6 +10,8 @@ class SaferStaticFilesConfig(StaticFilesConfig):
|
|||
`$ ./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.
|
||||
|
|
3
tests/components/safer_staticfiles/safer_staticfiles.css
Normal file
3
tests/components/safer_staticfiles/safer_staticfiles.css
Normal file
|
@ -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>
|
1
tests/components/safer_staticfiles/safer_staticfiles.js
Normal file
1
tests/components/safer_staticfiles/safer_staticfiles.js
Normal file
|
@ -0,0 +1 @@
|
|||
console.log("JS file");
|
16
tests/components/safer_staticfiles/safer_staticfiles.py
Normal file
16
tests/components/safer_staticfiles/safer_staticfiles.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
from django_components import component
|
||||
|
||||
|
||||
# Used for testing the safer_staticfiles app in `test_safer_staticfiles.py`
|
||||
@component.register("safer_staticfiles_component")
|
||||
class RelativeFileWithPathObjComponent(component.Component):
|
||||
template_name = "safer_staticfiles.html"
|
||||
|
||||
class Media:
|
||||
js = "safer_staticfiles.js"
|
||||
css = "safer_staticfiles.css"
|
||||
|
||||
def get_context_data(self, variable, *args, **kwargs) -> Dict[str, Any]:
|
||||
return {"variable": variable}
|
|
@ -38,7 +38,7 @@ class TestAutodiscover(BaseTestCase):
|
|||
|
||||
all_components_after = component_registry.registry.all().copy()
|
||||
imported_components_count = len(all_components_after) - len(all_components_before)
|
||||
self.assertEqual(imported_components_count, 2)
|
||||
self.assertEqual(imported_components_count, 3)
|
||||
|
||||
|
||||
class TestLoaderSettingsModule(BaseTestCase):
|
||||
|
|
|
@ -770,7 +770,14 @@ class MediaRelativePathTests(BaseTestCase):
|
|||
|
||||
# Fix the paths, since the "components" dir is nested
|
||||
with autodiscover_with_cleanup(map_import_paths=lambda p: f"tests.{p}"):
|
||||
component.registry.unregister("relative_file_pathobj_component")
|
||||
# Make sure that only relevant components are registered:
|
||||
comps_to_remove = [
|
||||
comp_name
|
||||
for comp_name in component.registry.all()
|
||||
if comp_name not in ["relative_file_component", "parent_component", "variable_display"]
|
||||
]
|
||||
for comp_name in comps_to_remove:
|
||||
component.registry.unregister(comp_name)
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}{% component_dependencies %}
|
||||
|
|
103
tests/test_safer_staticfiles.py
Normal file
103
tests/test_safer_staticfiles.py
Normal file
|
@ -0,0 +1,103 @@
|
|||
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"], [])
|
Loading…
Add table
Add a link
Reference in a new issue