285 lines
11 KiB
Diff
285 lines
11 KiB
Diff
Based on my knowledge of Django's codebase and the issue description, I'll create a patch that adds SCRIPT_NAME support to STATIC_URL and MEDIA_URL. The key changes need to be made in:
|
|
|
|
1. `django/core/files/storage.py` - FileSystemStorage to use get_script_prefix()
|
|
2. `django/contrib/staticfiles/storage.py` - StaticFilesStorage to properly handle SCRIPT_NAME
|
|
3. `django/templatetags/static.py` - The static template tag
|
|
|
|
Let me create the patch based on my understanding of Django's architecture:
|
|
|
|
```
|
|
--- a/django/conf/__init__.py
|
|
+++ b/django/conf/__init__.py
|
|
@@ -81,6 +81,20 @@ class LazySettings(LazyObject):
|
|
warnings.warn(EMPTY_SETTINGS_WARNING)
|
|
self._wrapped = Settings(settings_module)
|
|
|
|
+ @property
|
|
+ def _MEDIA_URL(self):
|
|
+ return self.__getattr__('MEDIA_URL')
|
|
+
|
|
+ @_MEDIA_URL.setter
|
|
+ def _MEDIA_URL(self, value):
|
|
+ pass
|
|
+
|
|
+ @property
|
|
+ def _STATIC_URL(self):
|
|
+ return self.__getattr__('STATIC_URL')
|
|
+
|
|
+ @_STATIC_URL.setter
|
|
+ def _STATIC_URL(self, value):
|
|
+ pass
|
|
+
|
|
def __repr__(self):
|
|
# Hardcode the class name as otherwise it yields 'Settings'.
|
|
if self._wrapped is empty:
|
|
--- a/django/contrib/staticfiles/storage.py
|
|
+++ b/django/contrib/staticfiles/storage.py
|
|
@@ -17,7 +17,7 @@ from django.utils.functional import LazyObject
|
|
|
|
class StaticFilesStorage(FileSystemStorage):
|
|
"""
|
|
- Standard file system storage for static files.
|
|
+ Standard file system storage for static files with SCRIPT_NAME support.
|
|
|
|
The defaults for ``location`` and ``base_url`` are
|
|
``STATIC_ROOT`` and ``STATIC_URL``.
|
|
@@ -27,6 +27,17 @@ class StaticFilesStorage(FileSystemStorage):
|
|
location = settings.STATIC_ROOT
|
|
if base_url is None:
|
|
base_url = settings.STATIC_URL
|
|
+ if base_url and not self._is_absolute_url(base_url):
|
|
+ from django.urls import get_script_prefix
|
|
+ base_url = '%s%s' % (get_script_prefix(), base_url.lstrip('/'))
|
|
super().__init__(location, base_url, *args, **kwargs)
|
|
# FileSystemStorage fallbacks to MEDIA_ROOT when location
|
|
# is empty string.
|
|
+ if not location:
|
|
+ self.location = settings.STATIC_ROOT
|
|
+
|
|
+ def _is_absolute_url(self, url):
|
|
+ """
|
|
+ Check if a URL is absolute (includes a scheme).
|
|
+ """
|
|
+ return url.startswith('http://') or url.startswith('https://') or url.startswith('//')
|
|
--- a/django/core/files/storage.py
|
|
+++ b/django/core/files/storage.py
|
|
@@ -258,6 +258,14 @@ class FileSystemStorage(Storage):
|
|
base_url = settings.MEDIA_URL
|
|
self._base_url = base_url
|
|
self._location = location
|
|
+ if base_url and not self._is_absolute_url(base_url):
|
|
+ from django.urls import get_script_prefix
|
|
+ self._base_url = '%s%s' % (get_script_prefix(), base_url.lstrip('/'))
|
|
|
|
@cached_property
|
|
def base_location(self):
|
|
@@ -280,6 +288,13 @@ class FileSystemStorage(Storage):
|
|
self._base_url = setting_changed.value
|
|
return self._base_url
|
|
|
|
+ def _is_absolute_url(self, url):
|
|
+ """
|
|
+ Check if a URL is absolute (includes a scheme or is protocol-relative).
|
|
+ """
|
|
+ return url.startswith('http://') or url.startswith('https://') or url.startswith('//')
|
|
+
|
|
def _clear_cached_properties(self, setting, **kwargs):
|
|
"""Reset setting based property values."""
|
|
if setting == 'MEDIA_ROOT':
|
|
--- a/django/templatetags/static.py
|
|
+++ b/django/templatetags/static.py
|
|
@@ -67,6 +67,18 @@ class StaticNode(template.Node):
|
|
|
|
|
|
def static(path):
|
|
+ """
|
|
+ Return the absolute URL to a static file, respecting SCRIPT_NAME.
|
|
+ """
|
|
+ from django.conf import settings
|
|
+ from django.urls import get_script_prefix
|
|
+
|
|
+ url = getattr(settings, 'STATIC_URL', '')
|
|
+ if url and not (url.startswith('http://') or url.startswith('https://') or url.startswith('//')):
|
|
+ prefix = get_script_prefix()
|
|
+ if prefix and not url.startswith(prefix):
|
|
+ url = '%s%s' % (prefix.rstrip('/'), url)
|
|
+ return urljoin(url, quote(path))
|
|
- return urljoin(getattr(settings, 'STATIC_URL', ''), quote(path))
|
|
```
|
|
|
|
Wait, I need to reconsider the approach. Looking at the issue and the hints more carefully, the proper approach would be to:
|
|
|
|
1. Modify the storage classes to respect SCRIPT_NAME when generating URLs
|
|
2. The static template tag and static() function should use the storage to get URLs
|
|
|
|
Let me create a more accurate patch:
|
|
|
|
--- a/django/conf/__init__.py
|
|
+++ b/django/conf/__init__.py
|
|
@@ -15,6 +15,7 @@ import warnings
|
|
from pathlib import Path
|
|
|
|
from django.conf import global_settings
|
|
+from django.core.exceptions import ImproperlyConfigured
|
|
from django.utils.deprecation import RemovedInDjango40Warning
|
|
from django.utils.functional import LazyObject, empty
|
|
|
|
--- a/django/core/files/storage.py
|
|
+++ b/django/core/files/storage.py
|
|
@@ -278,9 +278,23 @@ class FileSystemStorage(Storage):
|
|
|
|
@cached_property
|
|
def base_url(self):
|
|
- if self._base_url is not None and not self._base_url.endswith('/'):
|
|
+ return self._value_or_setting(self._base_url, settings.MEDIA_URL)
|
|
+
|
|
+ def _value_or_setting(self, value, setting):
|
|
+ if value is None:
|
|
+ value = setting
|
|
+ if value is not None and not value.endswith('/'):
|
|
self._base_url = setting_changed.value
|
|
- return self._base_url
|
|
+ return self._add_script_prefix(value)
|
|
+
|
|
+ def _add_script_prefix(self, url):
|
|
+ if url and not (url.startswith('http://') or url.startswith('https://') or url.startswith('//')):
|
|
+ from django.urls import get_script_prefix
|
|
+ prefix = get_script_prefix()
|
|
+ if prefix != '/' and not url.startswith(prefix):
|
|
+ url = prefix.rstrip('/') + url
|
|
+ return url
|
|
|
|
def _clear_cached_properties(self, setting, **kwargs):
|
|
"""Reset setting based property values."""
|
|
--- a/django/contrib/staticfiles/storage.py
|
|
+++ b/django/contrib/staticfiles/storage.py
|
|
@@ -29,6 +29,17 @@ class StaticFilesStorage(FileSystemStorage):
|
|
if not location:
|
|
self.location = settings.STATIC_ROOT
|
|
|
|
+ @cached_property
|
|
+ def base_url(self):
|
|
+ return self._value_or_setting(self._base_url, settings.STATIC_URL)
|
|
+
|
|
|
|
class HashedFilesMixin:
|
|
default_template = """url("%(url)s")"""
|
|
--- a/django/templatetags/static.py
|
|
+++ b/django/templatetags/static.py
|
|
@@ -67,6 +67,16 @@ class StaticNode(template.Node):
|
|
|
|
|
|
def static(path):
|
|
- return urljoin(getattr(settings, 'STATIC_URL', ''), quote(path))
|
|
+ """
|
|
+ Return the URL of a static file, respecting SCRIPT_NAME.
|
|
+ """
|
|
+ url = getattr(settings, 'STATIC_URL', '')
|
|
+ if url and not (url.startswith('http://') or url.startswith('https://') or url.startswith('//')):
|
|
+ from django.urls import get_script_prefix
|
|
+ prefix = get_script_prefix()
|
|
+ if prefix != '/' and not url.startswith(prefix):
|
|
+ url = prefix.rstrip('/') + url
|
|
+ return urljoin(url, quote(path))
|
|
|
|
|
|
@register.tag('static')
|
|
|
|
Actually, let me look at this more carefully. The issue mentions using `get_script_prefix()` like `reverse()` does. Let me create a cleaner patch that properly addresses the issue:
|
|
|
|
--- a/django/core/files/storage.py
|
|
+++ b/django/core/files/storage.py
|
|
@@ -277,10 +277,25 @@ class FileSystemStorage(Storage):
|
|
return os.path.join(self.location, name)
|
|
|
|
@cached_property
|
|
def base_url(self):
|
|
if self._base_url is not None and not self._base_url.endswith('/'):
|
|
self._base_url += '/'
|
|
- return self._base_url
|
|
+ return self._value_or_setting(self._base_url, settings.MEDIA_URL)
|
|
+
|
|
+ def _value_or_setting(self, value, setting):
|
|
+ if value is None:
|
|
+ value = setting
|
|
+ return self._add_script_prefix(value)
|
|
+
|
|
+ def _add_script_prefix(self, url):
|
|
+ """Prepend the script prefix to relative URLs."""
|
|
+ if url and not (url.startswith('http://') or url.startswith('https://') or url.startswith('//')):
|
|
+ from django.urls import get_script_prefix
|
|
+ prefix = get_script_prefix()
|
|
+ if prefix != '/' and not url.startswith(prefix):
|
|
+ url = prefix.rstrip('/') + url
|
|
+ return url
|
|
|
|
def _clear_cached_properties(self, setting, **kwargs):
|
|
"""Reset setting based property values."""
|
|
|
|
Let me now provide the complete unified diff patch:
|
|
|
|
--- a/django/core/files/storage.py
|
|
+++ b/django/core/files/storage.py
|
|
@@ -277,10 +277,22 @@ class FileSystemStorage(Storage):
|
|
return os.path.join(self.location, name)
|
|
|
|
@cached_property
|
|
def base_url(self):
|
|
if self._base_url is not None and not self._base_url.endswith('/'):
|
|
self._base_url += '/'
|
|
- return self._base_url
|
|
+ return self._add_script_prefix(self._base_url)
|
|
+
|
|
+ def _add_script_prefix(self, url):
|
|
+ """
|
|
+ Prepend the script prefix to relative URLs.
|
|
+
|
|
+ If SCRIPT_NAME is set and the URL is relative (doesn't include scheme),
|
|
+ prepend the script prefix to ensure correct URL generation when Django
|
|
+ is deployed at a subpath.
|
|
+ """
|
|
+ if url and url.startswith('/') and not url.startswith('//'):
|
|
+ from django.urls import get_script_prefix
|
|
+ prefix = get_script_prefix()
|
|
+ if prefix != '/':
|
|
+ url = prefix.rstrip('/') + url
|
|
+ return url
|
|
|
|
def _clear_cached_properties(self, setting, **kwargs):
|
|
"""Reset setting based property values."""
|
|
--- a/django/contrib/staticfiles/storage.py
|
|
+++ b/django/contrib/staticfiles/storage.py
|
|
@@ -29,6 +29,14 @@ class StaticFilesStorage(FileSystemStorage):
|
|
if not location:
|
|
self.location = settings.STATIC_ROOT
|
|
|
|
+ @cached_property
|
|
+ def base_url(self):
|
|
+ if self._base_url is not None and not self._base_url.endswith('/'):
|
|
+ self._base_url += '/'
|
|
+ return self._add_script_prefix(self._base_url)
|
|
+
|
|
|
|
class HashedFilesMixin:
|
|
default_template = """url("%(url)s")"""
|
|
--- a/django/templatetags/static.py
|
|
+++ b/django/templatetags/static.py
|
|
@@ -67,6 +67,18 @@ class StaticNode(template.Node):
|
|
|
|
|
|
def static(path):
|
|
- return urljoin(getattr(settings, 'STATIC_URL', ''), quote(path))
|
|
+ """
|
|
+ Return a URL to a static file, respecting SCRIPT_NAME.
|
|
+ """
|
|
+ url = getattr(settings, 'STATIC_URL', '')
|
|
+ # Only prepend script prefix for relative URLs (starting with / but not //)
|
|
+ if url and url.startswith('/') and not url.startswith('//'):
|
|
+ from django.urls import get_script_prefix
|
|
+ prefix = get_script_prefix()
|
|
+ if prefix != '/':
|
|
+ url = prefix.rstrip('/') + url
|
|
+ return urljoin(url, quote(path))
|
|
|
|
|
|
@register.tag('static')
|