Published 2021-03-14.
Time to read: 5 minutes.
django collection.
To style a Django web application, assets (also known as static assets) are required, such as images, JavaScript and CSS / SCSS. This article discusses:
Settingsfor establishing where Django looks for assets.- Assets provided by Python dependencies.
- A Django software tool for discovering where assets are served from.
A previous article walks though the process of preparing AWS to provide data storage services to a Django webapp, and another article shows how to switch between a local collection of assets and an asset collection stored on AWS.
Definitions
- Media assets
- User uploaded files. Not discussed in this article, but the docs referenced do encompass that topic.
- Static assets
- CSS, JavaScript, images, etc. required for the web application to function properly.
Django Asset Locations
Django assets for a webapp can be stored in many locations:
- Dependent Python modules. These static files have the lowest precedence.
-
In the
static/directory of each app, which is not created by default. Apps are searched for assets in the order that they are listed inINSTALLED_APPSinsettings. - In a top-level webapp directory of static assets (there is no default location). Files placed in this directory have the highest precedence.
- Content delivery networks like
Alibaba CDN,
AWS CloudFront,
CDN77,
cdnjs, CloudFlare, Fastly, Google Cloud, IBM CDN, KeyCDN, StackPath, Microsoft Azure CDN, and many more.
Django settings control where assets will be searched for.
The SCRIPT_NAME Environment Variable
This variable is important because it forms part of the directory path to assets.
The WSGI standard
can be interpreted to mean that the SCRIPT_NAME environment variable points to a deployed Django webapp.
From the WSGI standard:
Is this true? Why are there two names for one thing?
Setting FORCE_SCRIPT_NAME is how to set the value of SCRIPT_NAME.
This ensures that the directory that defines the web application for a Django web application is one level down from the root of the webapp.
FORCE_SCRIPT_NAME = "blah/"
Is this true?
Setting FORCE_SCRIPT_NAME to a slash causes a Django web application"s guts to be located in the top-level Django directory.
FORCE_SCRIPT_NAME = "/"
Django Asset Settings
The django.contrib.staticfiles app
provides support for various directories containing static assets.
It does this by supporting settings that define where static assets are looked for,
where generated assets are placed,
and provides software tools for manipulating assets.
A separate static directory is possible for production, located outside of the Django webapp.
Static files are collected and copied or linked to this directory (this is an output directory.)
This directory is normally set up so nginx or Apache httpd
can serve static files such as CSS, JavaScript and images,
or so those files can be synchronized with a directory in a CDN.
The STATIC_ROOT setting defines the location of this directory.
STATIC_ROOT = '/var/sites/myWebApp/collected_static_assets/'
Within a Django webapp,
every app can have its own static/ directory.
-
For development mode, the
django.contrib.staticfilesapp modifies themanage.py runserversubcommand so static files inside each app’sstatic/directory (myProject/myAppName/static/) are served automatically. -
For production mode, the
manage.py collectstaticsubcommand collects the contents of thestatic/directories and places them in the directory pointed to bySTATIC_ROOT.
It is also possible to collect assets from other directories.
The STATICFILES_DIRS
setting can be defined to hold an array of directory names to search for assets.
The following adds a top-level static/ directory in the Django webapp to the asset search path.
This directory could be used to hold static files that are common to several apps.
from pathlib import Path
STATICFILES_DIRS = [
BASE_DIR / 'static/',
]
Serving Assets
The STATIC_URL setting
defines the base URL for serving static assets associated with STATIC_ROOT.
The STATIC_URL setting can be a URL:
STATIC_URL = 'https://my_bucket.s3.amazonaws.com/'
It can also be a top-level directory path within the Django app:
STATIC_URL = '/static/'
The manage.py collectstatic Subcommand
(aw) $ ./manage.py collectstatic -h usage: manage.py collectstatic [-h] [--noinput] [--no-post-process] [-i PATTERN] [-n] [-c] [-l] [--no-default-ignore] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks] Collect static files in a single location. optional arguments: -h, --help show this help message and exit --noinput, --no-input Do NOT prompt the user for input of any kind. --no-post-process Do NOT post process collected files. -i PATTERN, --ignore PATTERN Ignore files or directories matching this glob-style pattern. Use multiple times to ignore more. -n, --dry-run Do everything except modify the filesystem. -c, --clear Clear the existing files using the storage before trying to copy or link the original file. -l, --link Create a symbolic link to each file instead of copying. --no-default-ignore Don’t ignore the common private glob-style patterns (defaults to "CVS", ".*" and "*~"). --version show program’s version number and exit -v {0,1,2,3}, --verbosity {0,1,2,3} Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output --settings SETTINGS The Python path to a settings module, e.g. "myproject.settings.main". If this isn’t provided, the DJANGO_SETTINGS_MODULE environment variable will be used. --pythonpath PYTHONPATH A directory to add to the Python path, e.g. "/home/djangoprojects/myproject". --traceback Raise on CommandError exceptions --no-color Don’t colorize the command output. --force-color Force colorization of the command output. --skip-checks Skip system checks.
Defining Assets
By default, collectstatic copies all files from directories containing static assets.
Storage Provider
Django supports storing and retrieving assets to and from various online storage providers.
The STATICFILES_STORAGE setting defines the
storage provider.
Check out the django-storages GitHub project.
Install the boto3 and django-storages dependencies:
(aw) $ pip install boto3 django-storages
The most interesting storage provider for me is the AWS S3 storage provider. The previous article walked though the process of preparing AWS to provide data storage services to a Django webapp. If you have not read that article yet, please do so now, then come back here and continue.
AWS S3 Storage Provider Settings
Immediately below are the AWS S3 storage provider settings I found to be of most interest.
Note that I do not include AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY because
boto3 will use credentials from
~/.aws/credentials if present,
just like aws cli:
AWS_STORAGE_BUCKET_NAME = 'assets.mywebsite.com'
AWS_LOCATION = 'static'
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
AWS_QUERYSTRING_AUTH = False
STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{AWS_LOCATION}/'
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage'
The documentation has a pointed note about directory syntax.
STATIC_URL must end in a slash, and AWS_S3_CUSTOM_DOMAIN must not.
Django AWS Classsloader Bug
😁I believe I found a bug in the Django AWS asset collection implementation. The content-related classloaders are not invoked, so the content is only loaded by the primary classloader. The result is that none of your web content customizations get loaded. I experienced this as a critical path / stop-other-work-and-fix-it-now bug. Here is my workaround.
Referencing Assets
In templates, the static tag builds URLs for a given relative path from a storage provider.
{ % load static %}
<img src="{% static 'my_app/example.jpg' %}" alt="My image">
Django provides many built-in tags and filters for templates.
Collecting Assets
Uploading to AWS
Now that AWS is configured to collect assets,
they are automatically uploaded using aws s3 sync when collectstatic is run.
In this mode, no assets are copied locally to STATIC_ROOT.
(aw) $ ./manage.py collectstatic Using AWS datastore for assets. (Input) STATICFILES_DIRS=['/var/work/django/frobshop/static'] (Output) STATIC_ROOT=/var/work/django/frobshop/collected_static_assets You have requested to collect static files at the destination location as specified in your settings. This will overwrite existing files! Are you sure you want to do this? yes (aw) $ aws s3 ls assets.ancientwarmth.com PRE admin/ PRE debug_toolbar/ PRE django_extensions/ PRE django_tables2/ PRE oscar/ PRE treebeard/
Invalidating AWS CloudFront Distribution
After running the collectstatic subcommand, the CloudFront distribution must be invalidated before the changes to the deployed assets become visible.
I wrote a bash script called collectstatic that performs the invalidation after calling manage.py collectstatic.
#!/bin/bash
function waitForInvalidation {
echo "Waiting for invalidation $2 to complete."
aws cloudfront wait invalidation-completed \
--distribution-id "$1" \
--id "$2"
echo "Invalidation $2 has completed."
}
function invalidateCloudFrontDistribution {
set -b
DIST_ID="$( aws cloudfront list-distributions \
--query "DistributionList.Items[*].{id:Id,origin:Origins.Items[0].Id}[?origin=='S3-$BUCKET_NAME'].id" \
--output text
)"
JSON="$( aws cloudfront create-invalidation \
--distribution-id "$DIST_ID" \
--paths "/*"
)"
INVALIDATION_ID="$( jq -r .Invalidation.Id <<< "$JSON" )"
waitForInvalidation "$DIST_ID" "$INVALIDATION_ID" &
}
unset FORCE_LOCAL_STORAGE
if [ "$1" == --force-local-storage ]; then
shift
FORCE_LOCAL_STORAGE=--force-local-storage
fi
if [ "$1" ]; then
BUCKET_NAME="$1"
else
BUCKET_NAME=assets.ancientwarmth.com
fi
FORCE_LOCAL_STORAGE_SAVE=$FORCE_LOCAL_STORAGE
# Workaround for https://github.com/jschneier/django-storages/issues/991
# Delete this line when fixed
FORCE_LOCAL_STORAGE=--force-local-storage
./manage.py collectstatic $FORCE_LOCAL_STORAGE \
--verbosity 0 \
--noinput \
--clear
# Workaround for https://github.com/jschneier/django-storages/issues/991
# Delete this command when fixed
aws s3 sync \
--acl public-read \
--delete \
--quiet \
collected_static_assets/ s3://assets.ancientwarmth.com
if [ -z "$FORCE_LOCAL_STORAGE_SAVE" ]; then invalidateCloudFrontDistribution; fi
Here is an example of using it:
(aw) $ bin/collectstatic
Using AWS datastore for assets.
(Input) STATICFILES_DIRS=['/var/work/django/frobshop/static']
(Output) STATIC_ROOT=/var/work/django/frobshop/collected_static_assets
{
"Location": "https://cloudfront.amazonaws.com/2020-05-31/distribution/EIHVJABCDE5K/invalidation/I39XR7TA5LP3NP",
"Invalidation": {
"Id": "I39XR7TA5LP3NP",
"Status": "InProgress",
"CreateTime": "2021-03-15T17:17:54.349Z",
"InvalidationBatch": {
"Paths": {
"Quantity": 1,
"Items": [
"/*"
]
},
"CallerReference": "cli-1615828671-931250"
}
}
}
Finding Assets
It can be confusing to try to understand where an asset is being served from, or why it is not being served,
even when serving from local datastores.
The findstatic subcommand
of manage.py can help with that problem.
findstatic ignores Django data stores and assumes local assets,
just as if the --force-local-storage option is always specified.
findstatic Help
(aw) $ ./manage.py findstatic -h usage: manage.py findstatic [-h] [--first] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks] staticfile [staticfile ...] Finds the absolute paths for the given static file(s). positional arguments: staticfile optional arguments: -h, --help show this help message and exit --first Only return the first match for each static file. --version show program’s version number and exit -v {0,1,2,3}, --verbosity {0,1,2,3} Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output --settings SETTINGS The Python path to a settings module, e.g. "myproject.settings.main". If this isn’t provided, the DJANGO_SETTINGS_MODULE environment variable will be used. --pythonpath PYTHONPATH A directory to add to the Python path, e.g. "/home/djangoprojects/myproject". --traceback Raise on CommandError exceptions --no-color Don’t colorize the command output. --force-color Force colorization of the command output. --skip-checks Skip system checks
Using findstatic
You can discover the absolute path of specific assets, by supplying the load paths.
The following displays the absolute path of the asset loaded by admin/js/core.js:
(aw) $ ./manage.py findstatic admin/js/core.js Found "admin/js/core.js" here: /var/work/django/oscar/lib/python3.8/site-packages/django/contrib/admin/static/admin/js/core.js
You can also obtain the absolute path of an relative asset directory
The following displays the absolute path for assets loaded by admin/js/:
(aw) $ ./manage.py findstatic admin/js Found "admin/js" here: /var/work/django/oscar/lib/python3.8/site-packages/django/contrib/admin/static/admin/js
We can list all the asset files in the absolute directory like this:
(aw) $ ls `./manage.py findstatic admin/js | tail -n 1` SelectBox.js change_form.js popup_response.js SelectFilter2.js collapse.js prepopulate.js actions.js collapse.min.js prepopulate.min.js actions.min.js core.js prepopulate_init.js admin inlines.js urlify.js autocomplete.js inlines.min.js vendor calendar.js jquery.init.js cancel.js nav_sidebar.js