1- from django .forms import Widget
1+ import os
2+
3+ from django .apps import apps
4+ from django .forms import Media , Widget
25from django .forms .utils import flatatt
36from django .utils .html import escape , mark_safe , strip_spaces_between_tags
47from 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
735class 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 ):
0 commit comments