Skip to content

Commit 61d77df

Browse files
authored
Fixes #19615: Properly set version request parameter for static files in S3 (#20455)
1 parent 24a83ac commit 61d77df

File tree

3 files changed

+107
-4
lines changed

3 files changed

+107
-4
lines changed

netbox/templates/base/base.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
{# Initialize color mode #}
2727
<script
2828
type="text/javascript"
29-
src="{% static 'setmode.js' %}?v={{ settings.RELEASE.version }}"
29+
src="{% static_with_params 'setmode.js' v=settings.RELEASE.version %}"
3030
onerror="window.location='{% url 'media_failure' %}?filename=setmode.js'">
3131
</script>
3232
<script type="text/javascript">
@@ -39,12 +39,12 @@
3939
{# Static resources #}
4040
<link
4141
rel="stylesheet"
42-
href="{% static 'netbox-external.css'%}?v={{ settings.RELEASE.version }}"
42+
href="{% static_with_params 'netbox-external.css' v=settings.RELEASE.version %}"
4343
onerror="window.location='{% url 'media_failure' %}?filename=netbox-external.css'"
4444
/>
4545
<link
4646
rel="stylesheet"
47-
href="{% static 'netbox.css'%}?v={{ settings.RELEASE.version }}"
47+
href="{% static_with_params 'netbox.css' v=settings.RELEASE.version %}"
4848
onerror="window.location='{% url 'media_failure' %}?filename=netbox.css'"
4949
/>
5050
<link rel="icon" type="image/png" href="{% static 'netbox.ico' %}" />
@@ -53,7 +53,7 @@
5353
{# Javascript #}
5454
<script
5555
type="text/javascript"
56-
src="{% static 'netbox.js' %}?v={{ settings.RELEASE.version }}"
56+
src="{% static_with_params 'netbox.js' v=settings.RELEASE.version %}"
5757
onerror="window.location='{% url 'media_failure' %}?filename=netbox.js'">
5858
</script>
5959
{% django_htmx_script %}

netbox/utilities/templatetags/builtins/tags.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import logging
2+
13
from django import template
4+
from django.templatetags.static import static
25
from django.utils.safestring import mark_safe
6+
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
37

48
from extras.choices import CustomFieldTypeChoices
59
from utilities.querydict import dict_to_querydict
@@ -11,6 +15,7 @@
1115
'customfield_value',
1216
'htmx_table',
1317
'formaction',
18+
'static_with_params',
1419
'tag',
1520
)
1621

@@ -127,3 +132,53 @@ def formaction(context):
127132
if context.get('htmx_navigation', False):
128133
return mark_safe('hx-push-url="true" hx-post')
129134
return 'formaction'
135+
136+
137+
@register.simple_tag
138+
def static_with_params(path, **params):
139+
"""
140+
Generate a static URL with properly appended query parameters.
141+
142+
The original Django static tag doesn't properly handle appending new parameters to URLs
143+
that already contain query parameters, which can result in malformed URLs with double
144+
question marks. This template tag handles the case where static files are served from
145+
AWS S3 or other CDNs that automatically append query parameters to URLs.
146+
147+
This implementation correctly appends new parameters to existing URLs and checks for
148+
parameter conflicts. A warning will be logged if any of the provided parameters
149+
conflict with existing parameters in the URL.
150+
151+
Args:
152+
path: The static file path (e.g., 'setmode.js')
153+
**params: Query parameters to append (e.g., v='4.3.1')
154+
155+
Returns:
156+
A properly formatted URL with query parameters.
157+
158+
Note:
159+
If any provided parameters conflict with existing URL parameters, a warning
160+
will be logged and the new parameter value will override the existing one.
161+
"""
162+
# Get the base static URL
163+
static_url = static(path)
164+
165+
# Parse the URL to extract existing query parameters
166+
parsed = urlparse(static_url)
167+
existing_params = parse_qs(parsed.query)
168+
169+
# Check for duplicate parameters and log warnings
170+
logger = logging.getLogger('netbox.utilities.templatetags.tags')
171+
for key, value in params.items():
172+
if key in existing_params:
173+
logger.warning(
174+
f"Parameter '{key}' already exists in static URL '{static_url}' "
175+
f"with value(s) {existing_params[key]}, overwriting with '{value}'"
176+
)
177+
existing_params[key] = [str(value)]
178+
179+
# Rebuild the query string
180+
new_query = urlencode(existing_params, doseq=True)
181+
182+
# Reconstruct the URL with the new query string
183+
new_parsed = parsed._replace(query=new_query)
184+
return urlunparse(new_parsed)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from unittest.mock import patch
2+
3+
from django.test import TestCase, override_settings
4+
5+
from utilities.templatetags.builtins.tags import static_with_params
6+
7+
8+
class StaticWithParamsTest(TestCase):
9+
"""
10+
Test the static_with_params template tag functionality.
11+
"""
12+
13+
def test_static_with_params_basic(self):
14+
"""Test basic parameter appending to static URL."""
15+
result = static_with_params('test.js', v='1.0.0')
16+
self.assertIn('test.js', result)
17+
self.assertIn('v=1.0.0', result)
18+
19+
@override_settings(STATIC_URL='https://cdn.example.com/static/')
20+
def test_static_with_params_existing_query_params(self):
21+
"""Test appending parameters to URL that already has query parameters."""
22+
# Mock the static() function to return a URL with existing query parameters
23+
with patch('utilities.templatetags.builtins.tags.static') as mock_static:
24+
mock_static.return_value = 'https://cdn.example.com/static/test.js?existing=param'
25+
26+
result = static_with_params('test.js', v='1.0.0')
27+
28+
# Should contain both existing and new parameters
29+
self.assertIn('existing=param', result)
30+
self.assertIn('v=1.0.0', result)
31+
# Should not have double question marks
32+
self.assertEqual(result.count('?'), 1)
33+
34+
@override_settings(STATIC_URL='https://cdn.example.com/static/')
35+
def test_static_with_params_duplicate_parameter_warning(self):
36+
"""Test that a warning is logged when parameters conflict."""
37+
with patch('utilities.templatetags.builtins.tags.static') as mock_static:
38+
mock_static.return_value = 'https://cdn.example.com/static/test.js?v=old_version'
39+
40+
with self.assertLogs('netbox.utilities.templatetags.tags', level='WARNING') as cm:
41+
result = static_with_params('test.js', v='new_version')
42+
43+
# Check that warning was logged
44+
self.assertIn("Parameter 'v' already exists", cm.output[0])
45+
46+
# Check that new parameter value is used
47+
self.assertIn('v=new_version', result)
48+
self.assertNotIn('v=old_version', result)

0 commit comments

Comments
 (0)