From e1382d3ccd11249f6043a3e28215d8e076a104fe Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Wed, 11 Sep 2024 08:45:55 +0200 Subject: [PATCH] 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> --- README.md | 249 ++++++++++++++---- docs/migrating_from_safer_staticfiles.md | 98 +++++++ sampleproject/README.md | 2 +- sampleproject/components/calendar/calendar.py | 13 +- sampleproject/components/greeting.py | 8 +- .../components/nested/calendar/calendar.py | 11 +- sampleproject/sampleproject/settings.py | 27 +- src/django_components/app_settings.py | 56 +++- src/django_components/autodiscover.py | 24 +- src/django_components/component_media.py | 6 +- src/django_components/finders.py | 154 +++++++++++ .../safer_staticfiles/apps.py | 22 -- src/django_components/template_loader.py | 76 +++++- src/django_components/utils.py | 9 + .../staticfiles.css} | 0 .../staticfiles.html} | 0 .../staticfiles.js} | 0 .../staticfiles.py} | 10 +- tests/django_test_setup.py | 3 +- .../test_app}/__init__.py | 0 tests/test_app/apps.py | 6 + .../components/app_lvl_comp/app_lvl_comp.css | 3 + .../components/app_lvl_comp/app_lvl_comp.html | 5 + .../components/app_lvl_comp/app_lvl_comp.js | 1 + .../components/app_lvl_comp/app_lvl_comp.py | 16 ++ .../app_lvl_comp/app_lvl_comp.css | 3 + .../app_lvl_comp/app_lvl_comp.html | 5 + .../app_lvl_comp/app_lvl_comp.js | 1 + .../app_lvl_comp/app_lvl_comp.py | 16 ++ tests/test_component_media.py | 10 +- tests/test_finders.py | 210 +++++++++++++++ tests/test_registry.py | 8 +- tests/test_safer_staticfiles.py | 103 -------- tests/test_template_loader.py | 143 ++++++++-- 34 files changed, 1034 insertions(+), 264 deletions(-) create mode 100644 docs/migrating_from_safer_staticfiles.md create mode 100644 src/django_components/finders.py delete mode 100644 src/django_components/safer_staticfiles/apps.py rename tests/components/{safer_staticfiles/safer_staticfiles.css => staticfiles/staticfiles.css} (100%) rename tests/components/{safer_staticfiles/safer_staticfiles.html => staticfiles/staticfiles.html} (100%) rename tests/components/{safer_staticfiles/safer_staticfiles.js => staticfiles/staticfiles.js} (100%) rename tests/components/{safer_staticfiles/safer_staticfiles.py => staticfiles/staticfiles.py} (53%) rename {src/django_components/safer_staticfiles => tests/test_app}/__init__.py (100%) create mode 100644 tests/test_app/apps.py create mode 100644 tests/test_app/components/app_lvl_comp/app_lvl_comp.css create mode 100644 tests/test_app/components/app_lvl_comp/app_lvl_comp.html create mode 100644 tests/test_app/components/app_lvl_comp/app_lvl_comp.js create mode 100644 tests/test_app/components/app_lvl_comp/app_lvl_comp.py create mode 100644 tests/test_app/custom_comps_dir/app_lvl_comp/app_lvl_comp.css create mode 100644 tests/test_app/custom_comps_dir/app_lvl_comp/app_lvl_comp.html create mode 100644 tests/test_app/custom_comps_dir/app_lvl_comp/app_lvl_comp.js create mode 100644 tests/test_app/custom_comps_dir/app_lvl_comp/app_lvl_comp.py create mode 100644 tests/test_finders.py delete mode 100644 tests/test_safer_staticfiles.py diff --git a/README.md b/README.md index 318be911..b958f67d 100644 --- a/README.md +++ b/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). + + +#### 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 = """ -
- {% slot "content" default %} - input: {{ input }} - {% endslot %} -
- """ + template = """ +
+ {% slot "content" default %} + input: {{ input }} + {% endslot %} +
+ """ - 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 = """ -
- {% slot "content" default input=input %} - input: {{ input }} - {% endslot %} -
- """ + template = """ +
+ {% slot "content" default input=input %} + input: {{ input }} + {% endslot %} +
+ """ - 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 `