diff --git a/.gitignore b/.gitignore index b3ba0b7b..302584e6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,9 @@ htmlcov/ node_modules/ .next/ example/db.sqlite3 + +# Emacs Temp/Autosaves +*~ +\#*\# +.#*.* +*_flymake* \ No newline at end of file diff --git a/djedi/admin/api.py b/djedi/admin/api.py index e18d0560..223853fb 100644 --- a/djedi/admin/api.py +++ b/djedi/admin/api.py @@ -1,4 +1,6 @@ from collections import defaultdict +from djedi.plugins.base import DjediPlugin + from django.core.exceptions import PermissionDenied from django.http import HttpResponse, Http404, HttpResponseBadRequest from django.utils.http import urlunquote @@ -193,11 +195,16 @@ def get(self, request, uri): try: uri = self.decode_uri(uri) uri = URI(uri) - plugins.resolve(uri) + plugin = plugins.resolve(uri) + plugin_context = self.get_context_data(uri=uri) + + if isinstance(plugin, DjediPlugin): + plugin_context = plugin.get_editor_context(**plugin_context) + except UnknownPlugin: raise Http404 else: - return self.render_plugin(request, self.get_context_data(uri=uri)) + return self.render_plugin(request, plugin_context) @never_cache def post(self, request, uri): diff --git a/djedi/plugins/base.py b/djedi/plugins/base.py new file mode 100644 index 00000000..d6796236 --- /dev/null +++ b/djedi/plugins/base.py @@ -0,0 +1,9 @@ +from cio.plugins.base import BasePlugin + + +class DjediPlugin(BasePlugin): + def get_editor_context(self, **kwargs): + """ + Returns custom context + """ + return kwargs diff --git a/djedi/plugins/form.py b/djedi/plugins/form.py new file mode 100644 index 00000000..e2ba83ae --- /dev/null +++ b/djedi/plugins/form.py @@ -0,0 +1,67 @@ +import json +from djedi.plugins.base import DjediPlugin +from django import forms + + +def deprefix(s): + # Remove prefix (anything including and before __) + return s.rpartition('__')[-1] + + +def get_custom_render_widget(cls): + class CustomRenderWidget(cls): + def render(self, *args, **kwargs): + name = kwargs.pop("name", None) + + if not name: + name = args[0] + args = args[1:] + + name = deprefix(name) + + return super(CustomRenderWidget, self).render( + "data[%s]" % name, + *args, + **kwargs + ) + + return CustomRenderWidget + + +class BaseEditorForm(forms.Form): + def __init__(self, *args, **kwargs): + super(BaseEditorForm, self).__init__(*args, **kwargs) + + for field in list(self.fields.keys()): + self.fields[field].widget.__class__ = get_custom_render_widget( + self.fields[field].widget.__class__ + ) + + +class FormsBasePlugin(DjediPlugin): + ext = None + + @property + def forms(self): + return {} + + def get_editor_context(self, **context): + context.update( + {"forms": { + tab: form() + for tab, form in self.forms.items() + }} + ) + + return context + + def save(self, data, dumps=True): + data = self.collect_forms_data(data) + return json.dumps(data) if dumps else data + + def collect_forms_data(self, data): + return { + deprefix(field): data.get(deprefix(field)) + for tab, form in self.forms.items() + for field in form.base_fields.keys() + } diff --git a/djedi/plugins/img.py b/djedi/plugins/img.py index ea5f4c91..69debb15 100644 --- a/djedi/plugins/img.py +++ b/djedi/plugins/img.py @@ -1,16 +1,44 @@ import json import six from django.utils.html import escape -from cio.plugins.base import BasePlugin from django.core.files.uploadedfile import InMemoryUploadedFile +from django import forms from hashlib import sha1 from os import path +from .form import FormsBasePlugin, BaseEditorForm -class ImagePluginBase(BasePlugin): +class DataForm(BaseEditorForm): + data__id = forms.CharField( + label="ID", + max_length=255, + required=False, + widget=forms.TextInput(attrs={"class": "form-control"}), + ) + + data__alt = forms.CharField( + label="Alt text", + max_length=255, + required=False, + widget=forms.TextInput(attrs={"class": "form-control"}), + ) + + data__class = forms.CharField( + label="Class", + max_length=255, + required=False, + widget=forms.TextInput(attrs={"class": "form-control"}), + ) + + +class ImagePluginBase(FormsBasePlugin): ext = 'img' + @property + def forms(self): + return {'HTML': DataForm} + def _open(self, filename): raise NotImplementedError @@ -101,14 +129,12 @@ def save(self, data): if file: file.close() - content = { + content = super(ImagePluginBase, self).save(data, dumps=False) + content.update({ 'filename': filename, 'width': width, 'height': height, - 'id': data.get('id') or None, - 'class': data.get('class') or None, - 'alt': data.get('alt') or None - } + }) return json.dumps(content) @@ -126,23 +152,27 @@ def render(self, data): 'width': 160, 'height': 90 } + if data: url = data.get('url') - width = data.get('width') or 0 - height = data.get('height') or 0 - alt = data.get('alt') or '' - tag_id = data.get('id') - tag_class = data.get('class') if url: attrs['src'] = url - attrs['alt'] = alt + + width = data.get('width') or 0 + height = data.get('height') or 0 if width and height: attrs['width'] = width attrs['height'] = height - if tag_id: - attrs['id'] = tag_id - if tag_class: - attrs['class'] = tag_class + + attrs['alt'] = data.get('alt') or '' + + attr_id = data.get('id') + if attr_id: + attrs['id'] = attr_id + + attr_class = data.get('class') + if attr_class: + attrs['class'] = attr_class html_attrs = (u'{0}="{1}"'.format(attr, escape(attrs[attr])) for attr in sorted(attrs.keys())) return u''.format(u' '.join(html_attrs)) diff --git a/djedi/static/djedi/plugins/img/js/img.coffee b/djedi/static/djedi/plugins/img/js/img.coffee index 1f0ae0df..29b407e1 100644 --- a/djedi/static/djedi/plugins/img/js/img.coffee +++ b/djedi/static/djedi/plugins/img/js/img.coffee @@ -375,13 +375,19 @@ class window.ImageEditor extends window.Editor @firstRender = false updateForm: (data) -> + # Hardcoded fields $("input[name='data[filename]']").val data.filename + $("input[name='data[crop]']").val '' $("input[name='data[width]']").val data.width $("input[name='data[height]']").val data.height - $("input[name='data[crop]']").val '' - $("input[name='data[id]']").val data.id - $("input[name='data[class]']").val data.class - $("input[name='data[alt]']").val data.alt + delete data.filename + delete data.width + delete data.height + + # Form fields + for k, v of data + $("input[name='data[#{k}]']").val v + @ratioButton.removeClass 'active' renderThumbnail: (url) -> diff --git a/djedi/static/djedi/plugins/img/js/img.js b/djedi/static/djedi/plugins/img/js/img.js index 990aed44..419e8b4e 100644 --- a/djedi/static/djedi/plugins/img/js/img.js +++ b/djedi/static/djedi/plugins/img/js/img.js @@ -425,13 +425,18 @@ }; ImageEditor.prototype.updateForm = function(data) { + var k, v; $("input[name='data[filename]']").val(data.filename); + $("input[name='data[crop]']").val(''); $("input[name='data[width]']").val(data.width); $("input[name='data[height]']").val(data.height); - $("input[name='data[crop]']").val(''); - $("input[name='data[id]']").val(data.id); - $("input[name='data[class]']").val(data["class"]); - $("input[name='data[alt]']").val(data.alt); + delete data.filename; + delete data.width; + delete data.height; + for (k in data) { + v = data[k]; + $("input[name='data[" + k + "]']").val(v); + } return this.ratioButton.removeClass('active'); }; diff --git a/djedi/templates/djedi/plugins/img/editor.html b/djedi/templates/djedi/plugins/img/editor.html index 4b94ff3c..5769f38b 100644 --- a/djedi/templates/djedi/plugins/img/editor.html +++ b/djedi/templates/djedi/plugins/img/editor.html @@ -9,7 +9,9 @@ {% block tabs %} {% endblock tabs %} @@ -29,28 +31,21 @@ -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
+ + {% for tab, form in forms.items %} +
+
+ {% for field in form %} +
+ +
+ {{ field }} +
+
+ {% endfor %} +
+
+ {% endfor %} {% endblock editor %} diff --git a/djedi/tests/test_rest.py b/djedi/tests/test_rest.py index 404ff5ea..080af6c3 100644 --- a/djedi/tests/test_rest.py +++ b/djedi/tests/test_rest.py @@ -11,6 +11,7 @@ from cio.backends.exceptions import NodeDoesNotExist, PersistenceError from cio.plugins import plugins from cio.utils.uri import URI +from djedi.plugins.form import BaseEditorForm from djedi.tests.base import ClientTest, DjediTest, UserMixin from djedi.utils.encoding import smart_unicode @@ -201,6 +202,7 @@ def test_render(self): 'width': '64', 'height': '64' })}) + self.assertEqual(response.status_code, 200) self.assertEqual(smart_unicode(response.content), u'') @@ -214,7 +216,19 @@ def test_editor(self): for ext in plugins: response = self.get('cms.editor', 'sv-se@page/title.' + ext) self.assertEqual(response.status_code, 200) - assert set(response.context_data.keys()) == set(('THEME', 'VERSION', 'uri',)) + if ext == 'img': + assert set(response.context_data.keys()) == set(('THEME', 'VERSION', 'uri', 'forms')) + assert 'HTML' in response.context_data['forms'] + assert isinstance(response.context_data['forms']['HTML'], BaseEditorForm) + + self.assertListEqual( + ["data__id", "data__alt", "data__class"], + list(response.context_data['forms']['HTML'].fields.keys()) + ) + + else: + assert set(response.context_data.keys()) == set(('THEME', 'VERSION', 'uri',)) + self.assertNotIn(b'document.domain', response.content) with cio.conf.settings(XSS_DOMAIN='foobar.se'): @@ -222,6 +236,16 @@ def test_editor(self): self.assertEqual(response.status_code, 200) self.assertIn(b'document.domain = "foobar.se"', response.content) + def test_image_dataform(self): + from djedi.plugins.img import DataForm + + data_form = DataForm() + html = data_form.as_table() + + self.assertTrue('name="data[alt]"' in html) + self.assertTrue('name="data[class]"' in html) + self.assertTrue('name="data[id]"' in html) + def test_upload(self): tests_dir = os.path.dirname(os.path.abspath(__file__)) image_path = os.path.join(tests_dir, 'assets', 'image.png') @@ -235,7 +259,9 @@ def test_upload(self): 'data[alt]': u'Zwitter', 'meta[comment]': u'VW' } + response = self.post('api', 'i18n://sv-se@header/logo.img', form) + self.assertEqual(response.status_code, 200) with open(image_path, "rb") as image: diff --git a/tox.ini b/tox.ini index 7c3a3e2e..4bf144b9 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,8 @@ envlist = py27-django{ 15, 16, 17, 18, 19, 110, 111 }, py35-django{ 18, 19, 110, 111, 20, 21, 22 }, py36-django{ 111, 20, 21, 22 }, - py37-django{ 111, 20, 21, 22 } + py37-django{ 111, 20, 21, 22 }, + py38-django{ 111, 20, 21, 22 } [testenv] passenv = COVERAGE_FILE