Skip to content

Commit 41d95f8

Browse files
feat: CSP support (#58)
* Add pypi actions * bump version * feat: CSP support * Update test requirements * Update djangocms_attributes_field/widgets.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update test requirements * Bump version * Add media property * Fix linitng * Add tests --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
1 parent 47864a0 commit 41d95f8

File tree

14 files changed

+221
-117
lines changed

14 files changed

+221
-117
lines changed

.github/workflows/test.yml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,22 @@ jobs:
1010
matrix:
1111
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
1212
requirements-file:
13-
[dj42_cms311.txt, dj42_cms41.txt, dj50_cms41.txt, dj51_cms41.txt]
14-
os: [ubuntu-20.04]
13+
[
14+
dj42_cms311.txt,
15+
dj42_cms41.txt,
16+
dj42_cms50.txt,
17+
dj50_cms50.txt,
18+
dj51_cms50.txt,
19+
dj52_cms50.txt
20+
]
21+
os: [ubuntu-latest]
1522
exclude:
1623
- python-version: "3.9"
17-
requirements-file: dj50_cms41.txt
24+
requirements-file: dj50_cms50.txt
1825
- python-version: "3.9"
19-
requirements-file: dj51_cms41.txt
26+
requirements-file: dj51_cms50.txt
27+
- python-version: "3.9"
28+
requirements-file: dj52_cms50.txt
2029

2130
steps:
2231
- uses: actions/checkout@v4

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
Changelog
33
=========
44

5+
4.1.0 (2025-07-15)
6+
==================
7+
8+
* Add CSP support (load JS and CSS from static files if installed in INSTALLED_APPS)
9+
510
4.0.0 (2024-11-19)
611
==================
712

README.rst

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
django CMS Attributes Field
33
===========================
44

5-
|pypi| |coverage| |python| |django| |djangocms| |djangocms4|
5+
|pypi| |coverage| |python| |django| |djangocms|
66

77

88
This project is an opinionated implementation of JSONField for arbitrary HTML
@@ -25,7 +25,8 @@ and provide sensible validation of the keys used.
2525

2626
.. note::
2727

28-
This project is considered 3rd party (no supervision by the `django CMS Association <https://www.django-cms.org/en/about-us/>`_). Join us on `Slack <https://www.django-cms.org/slack/>`_ for more information.
28+
This project is considered 3rd party (no supervision by the `django CMS Association <https://www.django-cms.org/en/about-us/>`_).
29+
Join us on `Discord <https://www.django-cms.org/discord/>`_ for more information.
2930

3031

3132
.. image:: preview.gif
@@ -63,7 +64,7 @@ Installation
6364
For a manual install:
6465

6566
* run ``pip install djangocms-attributes-field``
66-
* add ``djangocms_attributes_field`` to your ``INSTALLED_APPS``
67+
* add ``djangocms_attributes_field`` to your ``INSTALLED_APPS`` (at least for CSP support)
6768
* run ``python manage.py migrate djangocms_attributes_field``
6869

6970

@@ -174,11 +175,13 @@ You can run tests by executing::
174175
:target: http://badge.fury.io/py/djangocms-attributes-field
175176
.. |coverage| image:: https://codecov.io/gh/django-cms/djangocms-attributes-field/branch/master/graph/badge.svg
176177
:target: https://codecov.io/gh/divio/djangocms-attributes-field
177-
.. |python| image:: https://img.shields.io/badge/python-3.8+-blue.svg
178+
179+
.. |python| image:: https://img.shields.io/pypi/pyversions/djangocms-attributes-field
180+
:alt: PyPI - Python Version
178181
:target: https://pypi.org/project/djangocms-attributes-field/
179-
.. |django| image:: https://img.shields.io/badge/django-3.2,%204.0--%204.2-blue.svg
182+
.. |django| image:: https://img.shields.io/pypi/frameworkversions/django/djangocms-attributes-field
183+
:alt: PyPI - Django Versions from Framework Classifiers
180184
:target: https://www.djangoproject.com/
181-
.. |djangocms| image:: https://img.shields.io/badge/django%20CMS-3.9%2B-blue.svg
182-
:target: https://www.django-cms.org/
183-
.. |djangocms4| image:: https://img.shields.io/badge/django%20CMS-4-blue.svg
185+
.. |djangocms| image:: https://img.shields.io/pypi/frameworkversions/django-cms/djangocms-attributes-field
186+
:alt: PyPI - django CMS Versions from Framework Classifiers
184187
:target: https://www.django-cms.org/
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '4.0.0'
1+
__version__ = '4.1.0'
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
body.djangocms-admin-style .delete-attributes-pair,
2+
body.djangocms-admin-style .add-attributes-pair {
3+
border: 1px solid #ddd;
4+
border-radius: 3px;
5+
display: inline-block;
6+
padding: 6px 5px 8px 10px;
7+
line-height: inherit;
8+
}
9+
.delete-attributes-pair, .add-attributes-pair {
10+
line-height: 2;
11+
}
12+
.attributes-pair {
13+
display: table;
14+
table-layout: fixed;
15+
width: 100%;
16+
}
17+
.attributes-pair .field-box:first-child {
18+
width: 25% !important;
19+
display: table-cell !important;
20+
vertical-align: top !important;
21+
float: none !important;
22+
}
23+
.attributes-pair .field-box:last-child {
24+
display: table-cell !important;
25+
vertical-align: top !important;
26+
width: 75% !important;
27+
float: none !important;
28+
}
29+
body:not(.djangocms-admin-style) .attributes-pair .field-box:first-child input {
30+
width: calc(100% - 1.3em);
31+
}
32+
.djangocms-attributes-field .attributes-pair .attributes-value {
33+
width: 60% !important;
34+
width: -webkit-calc(100% - 54px) !important;
35+
width: -moz-calc(100% - 54px) !important;
36+
width: calc(100% - 54px) !important;
37+
}
38+
.delete-attributes-pair {
39+
margin-left: 16px;
40+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
window.addEventListener('load', function () {
2+
(function ($) {
3+
function fixUpIds (fieldGroup) {
4+
fieldGroup.find('.attributes-pair').each(function (idx, value) {
5+
$(value).find('.attributes-key').attr('id', 'field-key-row-' + idx)
6+
.siblings('label').attr('for', 'field-key-row-' + idx);
7+
$(value).find('.attributes-value').attr('id', 'field-value-row-' + idx)
8+
.siblings('label').attr('for', 'field-value-row-' + idx);
9+
});
10+
}
11+
12+
$(function () {
13+
$('.djangocms-attributes-field').each(function () {
14+
const that = $(this);
15+
16+
if (that.data('isAttributesFieldInitialized')) {
17+
return;
18+
}
19+
20+
that.data('isAttributesFieldInitialized', true);
21+
22+
const emptyRow = that.find('.template');
23+
const btnAdd = that.find('.add-attributes-pair');
24+
25+
btnAdd.on('click', function (event) {
26+
event.preventDefault();
27+
emptyRow.before(emptyRow.find('.attributes-pair').clone());
28+
fixUpIds(that);
29+
});
30+
31+
that.on('click', '.delete-attributes-pair', function (event) {
32+
event.preventDefault();
33+
34+
const removeButton = $(this);
35+
36+
removeButton.closest('.attributes-pair').remove();
37+
fixUpIds(that);
38+
});
39+
40+
fixUpIds(that);
41+
});
42+
43+
});
44+
}(django.jQuery));
45+
});

djangocms_attributes_field/widgets.py

Lines changed: 55 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,36 @@
1-
from django.forms import Widget
1+
import os
2+
3+
from django.apps import apps
4+
from django.forms import Media, Widget
25
from django.forms.utils import flatatt
36
from django.utils.html import escape, mark_safe, strip_spaces_between_tags
47
from django.utils.translation import gettext as _
58

9+
# NOTE: Inlining the CSS and JS code allows avoiding to register
10+
# djangocms_attributes_field in INSTALLED_APPS. It will, however,
11+
# potentially conflict with a CSP.
12+
# If "djangocms_attributes_field" is installed, then the media class
13+
# of the widget is used, otherwise the CSS and JS is inlined by reading from
14+
# file system at startup. This way, we support CSPs that do not allow
15+
# inline scripts/styles, but also support projects that historically do not use
16+
# djangocms_attributes_field as an app, but still want to use the widget.
17+
_inline_code = None
18+
19+
def _read_inline_code():
20+
if apps.is_installed('djangocms_attributes_field'):
21+
_inline_code = ""
22+
else:
23+
def _read_static_files():
24+
base_dir = os.path.dirname(os.path.abspath(__file__))
25+
with open(os.path.join(base_dir, 'static/djangocms_attributes_field/widget.js'), 'r', encoding='utf-8') as f:
26+
js_code = f.read()
27+
with open(os.path.join(base_dir, 'static/djangocms_attributes_field/widget.css'), 'r', encoding='utf-8') as f:
28+
css_code = f.read()
29+
return css_code, js_code
30+
31+
_inline_code = "<style>{}</style><script>{}</script>".format(*_read_static_files())
32+
return _inline_code
33+
634

735
class AttributesWidget(Widget):
836
"""
@@ -20,6 +48,31 @@ def __init__(self, *args, **kwargs):
2048
self.sorted = sorted if kwargs.pop('sorted', True) else lambda x: x
2149
super().__init__(*args, **kwargs)
2250

51+
@property
52+
def media(self):
53+
"""
54+
Returns the media required by this widget.
55+
If djangocms_attributes_field is installed, it will use the media class
56+
of the widget, otherwise it will inline the CSS and JS.
57+
"""
58+
59+
global _inline_code
60+
61+
if _inline_code is None:
62+
_inline_code = _read_inline_code()
63+
64+
if _inline_code:
65+
return Media()
66+
else:
67+
return Media(
68+
css={
69+
'all': ('djangocms_attributes_field/widget.css',)
70+
},
71+
js=(
72+
'djangocms_attributes_field/widget.js',
73+
)
74+
)
75+
2376
def _render_row(self, key, value, field_name, key_attrs, val_attrs):
2477
"""
2578
Renders to HTML a single key/value pair row.
@@ -86,103 +139,7 @@ def render(self, name, value, attrs=None, renderer=None):
86139
""".format(
87140
title=_('Add another key/value pair'),
88141
)
89-
output += '</div>'
90-
91-
# NOTE: This is very consciously being inlined into the HTML because
92-
# if we use the Django "class Media()" mechanism to include this JS
93-
# behaviour, then every project that uses any package that uses Django
94-
# CMS Attributes Field will also have to add this package to its
95-
# INSTALLED_APPS. By inlining the JS and CSS here, we avoid this.
96-
output += """
97-
<style>
98-
body.djangocms-admin-style .delete-attributes-pair,
99-
body.djangocms-admin-style .add-attributes-pair {
100-
border: 1px solid #ddd;
101-
border-radius: 3px;
102-
display: inline-block;
103-
padding: 6px 5px 8px 10px;
104-
line-height: inherit;
105-
}
106-
.delete-attributes-pair, .add-attributes-pair {
107-
line-height: 2;
108-
}
109-
.attributes-pair {
110-
display: table;
111-
table-layout: fixed;
112-
width: 100%;
113-
}
114-
.attributes-pair .field-box:first-child {
115-
width: 25% !important;
116-
display: table-cell !important;
117-
vertical-align: top !important;
118-
float: none !important;
119-
}
120-
.attributes-pair .field-box:last-child {
121-
display: table-cell !important;
122-
vertical-align: top !important;
123-
width: 75% !important;
124-
float: none !important;
125-
}
126-
body:not(.djangocms-admin-style) .attributes-pair .field-box:first-child input {
127-
width: calc(100% - 1.3em);
128-
}
129-
.djangocms-attributes-field .attributes-pair .attributes-value {
130-
width: 60% !important;
131-
width: -webkit-calc(100% - 54px) !important;
132-
width: -moz-calc(100% - 54px) !important;
133-
width: calc(100% - 54px) !important;
134-
}
135-
.delete-attributes-pair {
136-
margin-left: 16px;
137-
}
138-
</style>
139-
<script>
140-
(function ($) {
141-
function fixUpIds (fieldGroup) {
142-
fieldGroup.find('.attributes-pair').each(function (idx, value) {
143-
$(value).find('.attributes-key').attr('id', 'field-key-row-' + idx)
144-
.siblings('label').attr('for', 'field-key-row-' + idx);
145-
$(value).find('.attributes-value').attr('id', 'field-value-row-' + idx)
146-
.siblings('label').attr('for', 'field-value-row-' + idx);
147-
});
148-
}
149-
150-
$(function () {
151-
$('.djangocms-attributes-field').each(function () {
152-
var that = $(this);
153-
154-
if (that.data('isAttributesFieldInitialized')) {
155-
return;
156-
}
157-
158-
that.data('isAttributesFieldInitialized', true);
159-
160-
var emptyRow = that.find('.template');
161-
var btnAdd = that.find('.add-attributes-pair');
162-
var btnDelete = that.find('.delete-attributes-pair');
163-
164-
btnAdd.on('click', function (event) {
165-
event.preventDefault();
166-
emptyRow.before(emptyRow.find('.attributes-pair').clone());
167-
fixUpIds(that);
168-
});
169-
170-
that.on('click', '.delete-attributes-pair', function (event) {
171-
event.preventDefault();
172-
173-
var removeButton = $(this);
174-
175-
removeButton.closest('.attributes-pair').remove();
176-
fixUpIds(that);
177-
});
178-
179-
fixUpIds(that);
180-
});
181-
182-
});
183-
}(django.jQuery));
184-
</script>
185-
"""
142+
output += f'</div>{_inline_code}'
186143
return mark_safe(output)
187144

188145
def value_from_datadict(self, data, files, name):

setup.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,20 @@
1717
'Operating System :: OS Independent',
1818
'Programming Language :: Python',
1919
'Programming Language :: Python :: 3',
20-
'Programming Language :: Python :: 3.8',
2120
'Programming Language :: Python :: 3.9',
2221
'Programming Language :: Python :: 3.10',
2322
'Programming Language :: Python :: 3.11',
23+
'Programming Language :: Python :: 3.12',
24+
'Programming Language :: Python :: 3.13',
2425
'Framework :: Django',
25-
'Framework :: Django :: 3.2',
26-
'Framework :: Django :: 4.0',
27-
'Framework :: Django :: 4.1',
2826
'Framework :: Django :: 4.2',
27+
'Framework :: Django :: 5.0',
28+
'Framework :: Django :: 5.1',
29+
'Framework :: Django :: 5.2',
2930
'Framework :: Django CMS',
30-
'Framework :: Django CMS :: 3.9',
31-
'Framework :: Django CMS :: 3.10',
3231
'Framework :: Django CMS :: 3.11',
3332
'Framework :: Django CMS :: 4.1',
33+
'Framework :: Django CMS :: 5.0',
3434
'Topic :: Internet :: WWW/HTTP',
3535
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
3636
'Topic :: Software Development',

tests/requirements/base.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ tox
33
coverage
44
isort
55
flake8
6+
setuptools

tests/requirements/dj42_cms50.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-r base.txt
2+
3+
Django>=4.2,<5.0
4+
django-cms>=5.0,<5.1

0 commit comments

Comments
 (0)