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> <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 ## 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. 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). 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 ## Installation
Install the app into your environment: Install the app into your environment:

View file

@ -1,2 +1,3 @@
.python-version .python-version
*.sqlite3 *.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
django_components 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS: List[str] = [] ALLOWED_HOSTS: List[str] = ["127.0.0.1", "localhost"]
INSTALLED_APPS = [ INSTALLED_APPS = [
@ -39,6 +39,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",

View file

@ -10,6 +10,8 @@ class SaferStaticFilesConfig(StaticFilesConfig):
`$ ./manage.py collectstatic` will ignore .py and .html files, `$ ./manage.py collectstatic` will ignore .py and .html files,
preventing potentially sensitive backend logic from being leaked preventing potentially sensitive backend logic from being leaked
by the static file server. 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. 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() all_components_after = component_registry.registry.all().copy()
imported_components_count = len(all_components_after) - len(all_components_before) 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): class TestLoaderSettingsModule(BaseTestCase):

View file

@ -770,7 +770,14 @@ class MediaRelativePathTests(BaseTestCase):
# Fix the paths, since the "components" dir is nested # Fix the paths, since the "components" dir is nested
with autodiscover_with_cleanup(map_import_paths=lambda p: f"tests.{p}"): 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 = """ template_str: types.django_html = """
{% load component_tags %}{% component_dependencies %} {% 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"], [])