plain 0.1.1__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/assets/README.md +18 -37
- plain/assets/__init__.py +0 -6
- plain/assets/compile.py +111 -0
- plain/assets/finders.py +26 -218
- plain/assets/fingerprints.py +38 -0
- plain/assets/urls.py +31 -0
- plain/assets/views.py +263 -0
- plain/cli/cli.py +68 -5
- plain/csrf/README.md +12 -0
- plain/packages/config.py +5 -5
- plain/packages/registry.py +1 -7
- plain/preflight/urls.py +0 -10
- plain/runtime/README.md +0 -1
- plain/runtime/__init__.py +1 -1
- plain/runtime/global_settings.py +7 -14
- plain/runtime/user_settings.py +0 -49
- plain/templates/jinja/globals.py +1 -1
- plain/test/__init__.py +0 -8
- plain/test/client.py +36 -16
- plain/views/base.py +5 -3
- plain/views/errors.py +7 -0
- {plain-0.1.1.dist-info → plain-0.2.0.dist-info}/LICENSE +0 -24
- {plain-0.1.1.dist-info → plain-0.2.0.dist-info}/METADATA +1 -1
- {plain-0.1.1.dist-info → plain-0.2.0.dist-info}/RECORD +26 -36
- plain/assets/preflight.py +0 -14
- plain/assets/storage.py +0 -916
- plain/assets/utils.py +0 -52
- plain/assets/whitenoise/__init__.py +0 -5
- plain/assets/whitenoise/base.py +0 -259
- plain/assets/whitenoise/compress.py +0 -189
- plain/assets/whitenoise/media_types.py +0 -137
- plain/assets/whitenoise/middleware.py +0 -197
- plain/assets/whitenoise/responders.py +0 -286
- plain/assets/whitenoise/storage.py +0 -178
- plain/assets/whitenoise/string_utils.py +0 -13
- plain/internal/legacy/management/commands/__init__.py +0 -0
- plain/internal/legacy/management/commands/collectstatic.py +0 -297
- plain/test/utils.py +0 -255
- {plain-0.1.1.dist-info → plain-0.2.0.dist-info}/WHEEL +0 -0
- {plain-0.1.1.dist-info → plain-0.2.0.dist-info}/entry_points.txt +0 -0
plain/assets/README.md
CHANGED
@@ -1,13 +1,26 @@
|
|
1
1
|
# Assets
|
2
2
|
|
3
|
-
Serve static assets (CSS, JS, images, etc.)
|
3
|
+
Serve static assets (CSS, JS, images, etc.) directly from your app.
|
4
4
|
|
5
|
-
The default behavior is for Plain to serve assets
|
6
|
-
This is based on [whitenoise](http://whitenoise.evans.io/en/stable/).
|
5
|
+
The default behavior is for Plain to serve its own assets through a view. This behaves in a way similar to [Whitenoise](https://whitenoise.readthedocs.io/en/latest/).
|
7
6
|
|
8
7
|
## Usage
|
9
8
|
|
10
|
-
|
9
|
+
To include assests in your app, put them either in `app/assets` or `app/<package>/assets`.
|
10
|
+
|
11
|
+
Then include the `plain.assets.urls` in your `urls.py`:
|
12
|
+
|
13
|
+
```python
|
14
|
+
# app/urls.py
|
15
|
+
from plain.urls import include, path
|
16
|
+
import plain.assets.urls
|
17
|
+
|
18
|
+
|
19
|
+
urlpatterns = [
|
20
|
+
path("assets/", include(plain.assets.urls)),
|
21
|
+
# ...
|
22
|
+
]
|
23
|
+
```
|
11
24
|
|
12
25
|
Then in your template you can use the `asset()` function to get the URL.
|
13
26
|
|
@@ -18,39 +31,7 @@ Then in your template you can use the `asset()` function to get the URL.
|
|
18
31
|
If you ever need to reference an asset directly in Python code, you can use the `get_asset_url()` function.
|
19
32
|
|
20
33
|
```python
|
21
|
-
from plain.assets import get_asset_url
|
34
|
+
from plain.assets.urls import get_asset_url
|
22
35
|
|
23
36
|
print(get_asset_url("css/style.css"))
|
24
37
|
```
|
25
|
-
|
26
|
-
## Settings
|
27
|
-
|
28
|
-
These are the default settings related to assets handling.
|
29
|
-
|
30
|
-
```python
|
31
|
-
# app/settings.py
|
32
|
-
MIDDLEWARE = [
|
33
|
-
"plain.middleware.security.SecurityMiddleware",
|
34
|
-
"plain.assets.whitenoise.middleware.WhiteNoiseMiddleware", # <--
|
35
|
-
"plain.middleware.common.CommonMiddleware",
|
36
|
-
"plain.csrf.middleware.CsrfViewMiddleware",
|
37
|
-
"plain.middleware.clickjacking.XFrameOptionsMiddleware",
|
38
|
-
]
|
39
|
-
|
40
|
-
ASSETS_BACKEND = "plain.assets.whitenoise.storage.CompressedManifestStaticFilesStorage"
|
41
|
-
|
42
|
-
# List of finder classes that know how to find assets files in
|
43
|
-
# various locations.
|
44
|
-
ASSETS_FINDERS = [
|
45
|
-
"plain.assets.finders.FileSystemFinder",
|
46
|
-
"plain.assets.finders.PackageDirectoriesFinder",
|
47
|
-
]
|
48
|
-
|
49
|
-
# Absolute path to the directory assets files should be collected to.
|
50
|
-
# Example: "/var/www/example.com/assets/"
|
51
|
-
ASSETS_ROOT = PLAIN_TEMP_PATH / "assets_collected"
|
52
|
-
|
53
|
-
# URL that handles the assets files served from ASSETS_ROOT.
|
54
|
-
# Example: "http://example.com/assets/", "http://assets.example.com/"
|
55
|
-
ASSETS_URL = "/assets/"
|
56
|
-
```
|
plain/assets/__init__.py
CHANGED
plain/assets/compile.py
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
import gzip
|
2
|
+
import hashlib
|
3
|
+
import os
|
4
|
+
import shutil
|
5
|
+
|
6
|
+
from .finders import find_assets
|
7
|
+
from .fingerprints import AssetsFingerprintsManifest
|
8
|
+
|
9
|
+
FINGERPRINT_LENGTH = 7
|
10
|
+
|
11
|
+
SKIP_COMPRESS_EXTENSIONS = (
|
12
|
+
# Images
|
13
|
+
"jpg",
|
14
|
+
"jpeg",
|
15
|
+
"png",
|
16
|
+
"gif",
|
17
|
+
"webp",
|
18
|
+
# Compressed files
|
19
|
+
"zip",
|
20
|
+
"gz",
|
21
|
+
"tgz",
|
22
|
+
"bz2",
|
23
|
+
"tbz",
|
24
|
+
"xz",
|
25
|
+
"br",
|
26
|
+
# Fonts
|
27
|
+
"woff",
|
28
|
+
"woff2",
|
29
|
+
# Video
|
30
|
+
"3gp",
|
31
|
+
"3gpp",
|
32
|
+
"asf",
|
33
|
+
"avi",
|
34
|
+
"m4v",
|
35
|
+
"mov",
|
36
|
+
"mp4",
|
37
|
+
"mpeg",
|
38
|
+
"mpg",
|
39
|
+
"webm",
|
40
|
+
"wmv",
|
41
|
+
)
|
42
|
+
|
43
|
+
|
44
|
+
def compile_assets(*, target_dir, keep_original, fingerprint, compress):
|
45
|
+
manifest = AssetsFingerprintsManifest()
|
46
|
+
|
47
|
+
for url_path, asset in find_assets().items():
|
48
|
+
resolved_path, compiled_paths = compile_asset(
|
49
|
+
asset=asset,
|
50
|
+
target_dir=target_dir,
|
51
|
+
keep_original=keep_original,
|
52
|
+
fingerprint=fingerprint,
|
53
|
+
compress=compress,
|
54
|
+
)
|
55
|
+
if resolved_path != url_path:
|
56
|
+
manifest[url_path] = resolved_path
|
57
|
+
|
58
|
+
yield url_path, resolved_path, compiled_paths
|
59
|
+
|
60
|
+
if manifest:
|
61
|
+
manifest.save()
|
62
|
+
|
63
|
+
|
64
|
+
def compile_asset(*, asset, target_dir, keep_original, fingerprint, compress):
|
65
|
+
"""
|
66
|
+
Compile an asset to multiple output paths.
|
67
|
+
"""
|
68
|
+
compiled_paths = []
|
69
|
+
|
70
|
+
# The expected destination for the original asset
|
71
|
+
target_path = os.path.join(target_dir, asset.url_path)
|
72
|
+
|
73
|
+
# Keep track of where the final, resolved asset ends up
|
74
|
+
resolved_url_path = asset.url_path
|
75
|
+
|
76
|
+
# Make sure all the expected directories exist
|
77
|
+
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
78
|
+
|
79
|
+
base, extension = os.path.splitext(asset.url_path)
|
80
|
+
|
81
|
+
# First, copy the original asset over
|
82
|
+
if keep_original:
|
83
|
+
shutil.copy(asset.absolute_path, target_path)
|
84
|
+
compiled_paths.append(target_path)
|
85
|
+
|
86
|
+
if fingerprint:
|
87
|
+
# Fingerprint it with an md5 hash
|
88
|
+
# (maybe need a setting with fnmatch patterns for files to NOT fingerprint?
|
89
|
+
# that would allow pre-fingerprinted files to be used as-is, and keep source maps etc in tact)
|
90
|
+
with open(asset.absolute_path, "rb") as f:
|
91
|
+
content = f.read()
|
92
|
+
fingerprint_hash = hashlib.md5(content, usedforsecurity=False).hexdigest()[
|
93
|
+
:FINGERPRINT_LENGTH
|
94
|
+
]
|
95
|
+
|
96
|
+
fingerprinted_basename = f"{base}.{fingerprint_hash}{extension}"
|
97
|
+
fingerprinted_path = os.path.join(target_dir, fingerprinted_basename)
|
98
|
+
shutil.copy(asset.absolute_path, fingerprinted_path)
|
99
|
+
compiled_paths.append(fingerprinted_path)
|
100
|
+
|
101
|
+
resolved_url_path = os.path.relpath(fingerprinted_path, target_dir)
|
102
|
+
|
103
|
+
if compress and extension not in SKIP_COMPRESS_EXTENSIONS:
|
104
|
+
for path in compiled_paths.copy():
|
105
|
+
gzip_path = f"{path}.gz"
|
106
|
+
with gzip.GzipFile(gzip_path, "wb") as f:
|
107
|
+
with open(path, "rb") as f2:
|
108
|
+
f.write(f2.read())
|
109
|
+
compiled_paths.append(gzip_path)
|
110
|
+
|
111
|
+
return resolved_url_path, compiled_paths
|
plain/assets/finders.py
CHANGED
@@ -1,233 +1,41 @@
|
|
1
|
-
import functools
|
2
1
|
import os
|
3
2
|
|
4
|
-
from plain.assets import utils
|
5
|
-
from plain.assets.storage import FileSystemStorage
|
6
|
-
from plain.exceptions import ImproperlyConfigured
|
7
3
|
from plain.packages import packages
|
8
4
|
from plain.runtime import APP_PATH
|
9
|
-
from plain.utils._os import safe_join
|
10
|
-
from plain.utils.module_loading import import_string
|
11
|
-
|
12
|
-
# To keep track on which directories the finder has searched the static files.
|
13
|
-
searched_locations = []
|
14
|
-
|
15
5
|
|
16
6
|
APP_ASSETS_DIR = APP_PATH / "assets"
|
17
7
|
|
18
|
-
|
19
|
-
class BaseFinder:
|
20
|
-
"""
|
21
|
-
A base file finder to be used for custom assets finder classes.
|
22
|
-
"""
|
23
|
-
|
24
|
-
def check(self, **kwargs):
|
25
|
-
raise NotImplementedError(
|
26
|
-
"subclasses may provide a check() method to verify the finder is "
|
27
|
-
"configured correctly."
|
28
|
-
)
|
29
|
-
|
30
|
-
def find(self, path, all=False):
|
31
|
-
"""
|
32
|
-
Given a relative file path, find an absolute file path.
|
33
|
-
|
34
|
-
If the ``all`` parameter is False (default) return only the first found
|
35
|
-
file path; if True, return a list of all found files paths.
|
36
|
-
"""
|
37
|
-
raise NotImplementedError(
|
38
|
-
"subclasses of BaseFinder must provide a find() method"
|
39
|
-
)
|
40
|
-
|
41
|
-
def list(self, ignore_patterns):
|
42
|
-
"""
|
43
|
-
Given an optional list of paths to ignore, return a two item iterable
|
44
|
-
consisting of the relative path and storage instance.
|
45
|
-
"""
|
46
|
-
raise NotImplementedError(
|
47
|
-
"subclasses of BaseFinder must provide a list() method"
|
48
|
-
)
|
49
|
-
|
50
|
-
|
51
|
-
class FileSystemFinder(BaseFinder):
|
52
|
-
"""
|
53
|
-
A static files finder that looks in "static"
|
54
|
-
"""
|
55
|
-
|
56
|
-
def __init__(self, package_names=None, *args, **kwargs):
|
57
|
-
# List of locations with static files
|
58
|
-
self.locations = []
|
59
|
-
# Maps dir paths to an appropriate storage instance
|
60
|
-
self.storages = {}
|
61
|
-
|
62
|
-
root = APP_ASSETS_DIR
|
63
|
-
|
64
|
-
if isinstance(root, list | tuple):
|
65
|
-
prefix, root = root
|
66
|
-
else:
|
67
|
-
prefix = ""
|
68
|
-
if (prefix, root) not in self.locations:
|
69
|
-
self.locations.append((prefix, root))
|
70
|
-
for prefix, root in self.locations:
|
71
|
-
filesystem_storage = FileSystemStorage(location=root)
|
72
|
-
filesystem_storage.prefix = prefix
|
73
|
-
self.storages[root] = filesystem_storage
|
74
|
-
super().__init__(*args, **kwargs)
|
75
|
-
|
76
|
-
# def check(self, **kwargs):
|
77
|
-
# errors = []
|
78
|
-
# if settings.ASSETS_ROOT and os.path.abspath(
|
79
|
-
# settings.ASSETS_ROOT
|
80
|
-
# ) == os.path.abspath(self.path):
|
81
|
-
# errors.append(
|
82
|
-
# Error(
|
83
|
-
# "The STATICFILES_DIR setting should not contain the "
|
84
|
-
# "ASSETS_ROOT setting.",
|
85
|
-
# id="assets.E002",
|
86
|
-
# )
|
87
|
-
# )
|
88
|
-
# return errors
|
89
|
-
|
90
|
-
def find(self, path, all=False):
|
91
|
-
matches = []
|
92
|
-
for prefix, root in self.locations:
|
93
|
-
if root not in searched_locations:
|
94
|
-
searched_locations.append(root)
|
95
|
-
matched_path = self.find_location(root, path, prefix)
|
96
|
-
if matched_path:
|
97
|
-
if not all:
|
98
|
-
return matched_path
|
99
|
-
matches.append(matched_path)
|
100
|
-
return matches
|
101
|
-
|
102
|
-
def find_location(self, root, path, prefix=None):
|
103
|
-
"""
|
104
|
-
Find a requested static file in a location and return the found
|
105
|
-
absolute path (or ``None`` if no match).
|
106
|
-
"""
|
107
|
-
if prefix:
|
108
|
-
prefix = f"{prefix}{os.sep}"
|
109
|
-
if not path.startswith(prefix):
|
110
|
-
return None
|
111
|
-
path = path.removeprefix(prefix)
|
112
|
-
path = safe_join(root, path)
|
113
|
-
if os.path.exists(path):
|
114
|
-
return path
|
115
|
-
|
116
|
-
def list(self, ignore_patterns):
|
117
|
-
"""
|
118
|
-
List all files in all locations.
|
119
|
-
"""
|
120
|
-
for prefix, root in self.locations:
|
121
|
-
# Skip nonexistent directories.
|
122
|
-
if os.path.isdir(root):
|
123
|
-
storage = self.storages[root]
|
124
|
-
for path in utils.get_files(storage, ignore_patterns):
|
125
|
-
yield path, storage
|
126
|
-
|
127
|
-
|
128
|
-
class PackageDirectoriesFinder(BaseFinder):
|
129
|
-
"""
|
130
|
-
A static files finder that looks in the directory of each app as
|
131
|
-
specified in the source_dir attribute.
|
132
|
-
"""
|
133
|
-
|
134
|
-
storage_class = FileSystemStorage
|
135
|
-
source_dir = "assets"
|
136
|
-
|
137
|
-
def __init__(self, package_names=None, *args, **kwargs):
|
138
|
-
# The list of packages that are handled
|
139
|
-
self.packages = []
|
140
|
-
# Mapping of app names to storage instances
|
141
|
-
self.storages = {}
|
142
|
-
package_configs = packages.get_package_configs()
|
143
|
-
if package_names:
|
144
|
-
package_names = set(package_names)
|
145
|
-
package_configs = [ac for ac in package_configs if ac.name in package_names]
|
146
|
-
for package_config in package_configs:
|
147
|
-
app_storage = self.storage_class(
|
148
|
-
os.path.join(package_config.path, self.source_dir)
|
149
|
-
)
|
150
|
-
if os.path.isdir(app_storage.location):
|
151
|
-
self.storages[package_config.name] = app_storage
|
152
|
-
if package_config.name not in self.packages:
|
153
|
-
self.packages.append(package_config.name)
|
154
|
-
super().__init__(*args, **kwargs)
|
155
|
-
|
156
|
-
def list(self, ignore_patterns):
|
157
|
-
"""
|
158
|
-
List all files in all app storages.
|
159
|
-
"""
|
160
|
-
for storage in self.storages.values():
|
161
|
-
if storage.exists(""): # check if storage location exists
|
162
|
-
for path in utils.get_files(storage, ignore_patterns):
|
163
|
-
yield path, storage
|
164
|
-
|
165
|
-
def find(self, path, all=False):
|
166
|
-
"""
|
167
|
-
Look for files in the app directories.
|
168
|
-
"""
|
169
|
-
matches = []
|
170
|
-
for app in self.packages:
|
171
|
-
app_location = self.storages[app].location
|
172
|
-
if app_location not in searched_locations:
|
173
|
-
searched_locations.append(app_location)
|
174
|
-
match = self.find_in_app(app, path)
|
175
|
-
if match:
|
176
|
-
if not all:
|
177
|
-
return match
|
178
|
-
matches.append(match)
|
179
|
-
return matches
|
180
|
-
|
181
|
-
def find_in_app(self, app, path):
|
182
|
-
"""
|
183
|
-
Find a requested static file in an app's static locations.
|
184
|
-
"""
|
185
|
-
storage = self.storages.get(app)
|
186
|
-
# Only try to find a file if the source dir actually exists.
|
187
|
-
if storage and storage.exists(path):
|
188
|
-
matched_path = storage.path(path)
|
189
|
-
if matched_path:
|
190
|
-
return matched_path
|
8
|
+
SKIP_ASSETS = (".DS_Store", ".gitignore")
|
191
9
|
|
192
10
|
|
193
|
-
def
|
194
|
-
|
195
|
-
Find a static file with the given path using all enabled finders.
|
11
|
+
def find_assets():
|
12
|
+
assets_map = {}
|
196
13
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
matches = []
|
202
|
-
for finder in get_finders():
|
203
|
-
result = finder.find(path, all=all)
|
204
|
-
if not all and result:
|
205
|
-
return result
|
206
|
-
if not isinstance(result, list | tuple):
|
207
|
-
result = [result]
|
208
|
-
matches.extend(result)
|
209
|
-
if matches:
|
210
|
-
return matches
|
211
|
-
# No match.
|
212
|
-
return [] if all else None
|
14
|
+
class Asset:
|
15
|
+
def __init__(self, *, url_path, absolute_path):
|
16
|
+
self.url_path = url_path
|
17
|
+
self.absolute_path = absolute_path
|
213
18
|
|
19
|
+
def __str__(self):
|
20
|
+
return self.url_path
|
214
21
|
|
215
|
-
def
|
216
|
-
|
22
|
+
def iter_directory(path):
|
23
|
+
for root, _, files in os.walk(path):
|
24
|
+
for f in files:
|
25
|
+
if f in SKIP_ASSETS:
|
26
|
+
continue
|
27
|
+
abs_path = os.path.join(root, f)
|
28
|
+
url_path = os.path.relpath(abs_path, path)
|
29
|
+
yield url_path, abs_path
|
217
30
|
|
218
|
-
|
219
|
-
|
31
|
+
# Iterate the installed package assets, in order
|
32
|
+
for pkg in packages.get_package_configs():
|
33
|
+
pkg_assets_dir = os.path.join(pkg.path, "assets")
|
34
|
+
for url_path, abs_path in iter_directory(pkg_assets_dir):
|
35
|
+
assets_map[url_path] = Asset(url_path=url_path, absolute_path=abs_path)
|
220
36
|
|
37
|
+
# The app/assets take priority over everything
|
38
|
+
for url_path, abs_path in iter_directory(APP_ASSETS_DIR):
|
39
|
+
assets_map[url_path] = Asset(url_path=url_path, absolute_path=abs_path)
|
221
40
|
|
222
|
-
|
223
|
-
def get_finder(import_path):
|
224
|
-
"""
|
225
|
-
Import the assets finder class described by import_path, where
|
226
|
-
import_path is the full Python path to the class.
|
227
|
-
"""
|
228
|
-
Finder = import_string(import_path)
|
229
|
-
if not issubclass(Finder, BaseFinder):
|
230
|
-
raise ImproperlyConfigured(
|
231
|
-
f'Finder "{Finder}" is not a subclass of "{BaseFinder}"'
|
232
|
-
)
|
233
|
-
return Finder()
|
41
|
+
return assets_map
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import json
|
2
|
+
from functools import cache
|
3
|
+
|
4
|
+
from plain.runtime import settings
|
5
|
+
|
6
|
+
|
7
|
+
class AssetsFingerprintsManifest(dict):
|
8
|
+
def __init__(self):
|
9
|
+
self.path = settings.PLAIN_TEMP_PATH / "assets" / "fingerprints.json"
|
10
|
+
|
11
|
+
def load(self):
|
12
|
+
if self.path.exists():
|
13
|
+
with open(self.path) as f:
|
14
|
+
self.update(json.load(f))
|
15
|
+
|
16
|
+
def save(self):
|
17
|
+
with open(self.path, "w") as f:
|
18
|
+
json.dump(self, f, indent=2)
|
19
|
+
|
20
|
+
|
21
|
+
@cache
|
22
|
+
def _get_manifest():
|
23
|
+
"""
|
24
|
+
A cached function for loading the asset fingerprints manifest,
|
25
|
+
so we don't have to keep loading it from disk over and over.
|
26
|
+
"""
|
27
|
+
manifest = AssetsFingerprintsManifest()
|
28
|
+
manifest.load()
|
29
|
+
return manifest
|
30
|
+
|
31
|
+
|
32
|
+
def get_fingerprinted_url_path(url_path):
|
33
|
+
"""
|
34
|
+
Get the final fingerprinted path for an asset URL path.
|
35
|
+
"""
|
36
|
+
manifest = _get_manifest()
|
37
|
+
if url_path in manifest:
|
38
|
+
return manifest[url_path]
|
plain/assets/urls.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
from plain.runtime import settings
|
2
|
+
from plain.urls import path, reverse
|
3
|
+
|
4
|
+
from .fingerprints import get_fingerprinted_url_path
|
5
|
+
from .views import AssetView
|
6
|
+
|
7
|
+
default_namespace = "assets"
|
8
|
+
|
9
|
+
|
10
|
+
def get_asset_url(url_path):
|
11
|
+
if settings.DEBUG:
|
12
|
+
# In debug, we only ever use the original URL path.
|
13
|
+
resolved_url_path = url_path
|
14
|
+
else:
|
15
|
+
# If a fingerprinted URL path is available, use that.
|
16
|
+
if fingerprinted_url_path := get_fingerprinted_url_path(url_path):
|
17
|
+
resolved_url_path = fingerprinted_url_path
|
18
|
+
else:
|
19
|
+
resolved_url_path = url_path
|
20
|
+
|
21
|
+
# If a base url is set (i.e. a CDN),
|
22
|
+
# then do a simple join to get the full URL.
|
23
|
+
if settings.ASSETS_BASE_URL:
|
24
|
+
return settings.ASSETS_BASE_URL + resolved_url_path
|
25
|
+
|
26
|
+
return reverse(default_namespace + ":asset", kwargs={"path": resolved_url_path})
|
27
|
+
|
28
|
+
|
29
|
+
urlpatterns = [
|
30
|
+
path("<path:path>", AssetView, name="asset"),
|
31
|
+
]
|