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:
Juro Oravec 2024-07-08 07:25:03 +02:00 committed by GitHub
parent 3dadba6636
commit 23d91218bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 226 additions and 4 deletions

View file

@ -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:

View file

@ -1,2 +1,3 @@
.python-version
*.sqlite3
staticfiles

66
sampleproject/README.md Normal file
View 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

View file

@ -1,2 +1,4 @@
django
django_components
gunicorn
whitenoise

View file

@ -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",

View file

@ -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.

View file

@ -0,0 +1,3 @@
.html-css-only {
color: blue;
}

View file

@ -0,0 +1,5 @@
<form method="post">
{% csrf_token %}
<input type="text" name="variable" value="{{ variable }}">
<input type="submit">
</form>

View file

@ -0,0 +1 @@
console.log("JS file");

View 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}

View file

@ -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):

View file

@ -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 %}

View 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"], [])