From 6364a793f1b8052eede28c4ae5fb1140b4932f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Wed, 19 Jul 2017 23:34:01 +0200 Subject: [PATCH 2/2] admin: make form slugs editable by the admin (#15663) --- tests/test_admin_pages.py | 54 +++++++++-- wcs/admin/forms.py | 34 +++++-- wcs/qommon/http_response.py | 4 +- wcs/qommon/static/css/dc2/admin.css | 5 ++ wcs/qommon/static/js/qommon.admin.js | 21 +++++ wcs/qommon/static/js/qommon.geolocation.js | 138 +---------------------------- wcs/qommon/static/js/qommon.slugify.js | 137 ++++++++++++++++++++++++++++ 7 files changed, 241 insertions(+), 152 deletions(-) create mode 100644 wcs/qommon/static/js/qommon.slugify.js diff --git a/tests/test_admin_pages.py b/tests/test_admin_pages.py index c9c99e5b..4bc6f394 100644 --- a/tests/test_admin_pages.py +++ b/tests/test_admin_pages.py @@ -319,17 +319,57 @@ def test_forms_edit(pub): assert_option_display(resp, 'Geolocation', 'Disabled') assert FormDef.get(formdef.id).geolocations is None - # try changing title +def test_form_title_change(pub): + create_superuser(pub) + create_role() + + FormDef.wipe() + formdef = FormDef() + formdef.name = 'form title' + formdef.fields = [] + formdef.store() + + app = login(get_app(pub)) + resp = app.get('/backoffice/forms/1/') resp = resp.click('change title') - assert resp.forms[0]['name'].value == 'form title' - resp.forms[0]['name'] = 'new title' - resp = resp.forms[0].submit() + assert resp.form['name'].value == 'form title' + assert 'data-slug-sync' in resp.body + assert not 'change-nevertheless' in resp.body + resp.form['name'] = 'new title' + resp = resp.form.submit() assert resp.location == 'http://example.net/backoffice/forms/1/' resp = resp.follow() - assert FormDef.get(1).name == 'new title' - assert FormDef.get(1).url_name == 'form-title' - assert FormDef.get(1).internal_identifier == 'new-title' + formdef = FormDef.get(formdef.id) + assert formdef.name == 'new title' + assert formdef.url_name == 'form-title' + assert formdef.internal_identifier == 'new-title' + + resp = app.get('/backoffice/forms/1/') + resp = resp.click('change title') + assert not 'data-slug-sync' in resp.body + assert not 'change-nevertheless' in resp.body + + formdef.data_class()().store() + resp = app.get('/backoffice/forms/1/') + resp = resp.click('change title') + assert 'change-nevertheless' in resp.body + + formdef2 = FormDef() + formdef2.name = 'other title' + formdef2.fields = [] + formdef2.store() + + resp = app.get('/backoffice/forms/%s/' % formdef2.id) + resp = resp.click('change title') + assert resp.form['name'].value == 'other title' + resp.form['url_name'] = formdef.url_name + resp = resp.form.submit() + assert 'This identifier is already used.' in resp.body + + resp.form['url_name'] = 'foobar' + resp = resp.form.submit().follow() + assert FormDef.get(formdef2.id).url_name == 'foobar' def test_form_category(pub): create_superuser(pub) diff --git a/wcs/admin/forms.py b/wcs/admin/forms.py index 1cd19d94..46880511 100644 --- a/wcs/admin/forms.py +++ b/wcs/admin/forms.py @@ -670,8 +670,20 @@ class FormDefPage(Directory): def title(self): form = Form(enctype='multipart/form-data') + kwargs = {} + if self.formdef.url_name == misc.simplify(self.formdef.name): + # if name and url name are in sync, keep them that way + kwargs['data-slug-sync'] = 'url_name' form.add(StringWidget, 'name', title=_('Form Title'), required=True, - size=40, value=self.formdef.name) + size=40, value=self.formdef.name, **kwargs) + + disabled_url_name = bool(self.formdef.data_class().count()) + kwargs = {} + if disabled_url_name: + kwargs['readonly'] = 'readonly' + form.add(ValidatedStringWidget, 'url_name', title=_('Identifier in URLs'), + size=40, required=True, value=self.formdef.url_name, + regex=r'^[a-zA-Z0-9_-]+', **kwargs) form.add_submit('submit', _('Submit')) form.add_submit('cancel', _('Cancel')) if form.get_widget('cancel').parse(): @@ -679,20 +691,28 @@ class FormDefPage(Directory): if form.is_submitted() and not form.has_errors(): new_name = form.get_widget('name').parse() - formdefs_name = [x.name for x in FormDef.select(ignore_errors=True) - if x.id != self.formdef.id] - if new_name in formdefs_name: - form.get_widget('name').set_error(_('This name is already used')) - else: + new_url_name = form.get_widget('url_name').parse() + formdefs = [x for x in FormDef.select(ignore_errors=True) if x.id != self.formdef.id] + if new_name in [x.name for x in formdefs]: + form.get_widget('name').set_error(_('This name is already used.')) + if new_url_name in [x.url_name for x in formdefs]: + form.get_widget('url_name').set_error(_('This identifier is already used.')) + if not form.has_errors(): self.formdef.name = new_name + self.formdef.url_name = new_url_name self.formdef.store() return redirect('.') + if disabled_url_name: + form.widgets.append(HtmlWidget('

%s
' % _( + 'The form identifier should not be modified as there is already some data.'))) + form.widgets.append(HtmlWidget('%s

' % _( + 'I understand the danger, make it editable nevertheless.'))) + get_response().breadcrumb.append( ('title', _('Title')) ) self.html_top(title=self.formdef.name) r = TemplateIO(html=True) r += htmltext('

%s

') % _('Title') - r += htmltext('

%s

') % _('Choose a title for this form') r += form.render() return r.getvalue() diff --git a/wcs/qommon/http_response.py b/wcs/qommon/http_response.py index dc542970..860eed62 100644 --- a/wcs/qommon/http_response.py +++ b/wcs/qommon/http_response.py @@ -84,11 +84,13 @@ class HTTPResponse(quixote.http_response.HTTPResponse): self.add_javascript_code('var QOMMON_ROOT_URL = "%s";\n' % \ get_publisher().get_application_static_files_root_url()) if script_name == 'qommon.geolocation.js': - self.add_javascript(['jquery.js']) + self.add_javascript(['jquery.js', 'qommon.slugify.js']) self.add_javascript_code('var WCS_ROOT_URL = "%s";\n' % \ get_publisher().get_frontoffice_url()) if script_name == 'wcs.listing.js': self.add_javascript(['jquery.js']) + if script_name == 'qommon.admin.js': + self.add_javascript(['jquery.js', 'qommon.slugify.js']) def add_javascript_code(self, code): if not self.javascript_code_parts: diff --git a/wcs/qommon/static/css/dc2/admin.css b/wcs/qommon/static/css/dc2/admin.css index 879d1542..b6183f3f 100644 --- a/wcs/qommon/static/css/dc2/admin.css +++ b/wcs/qommon/static/css/dc2/admin.css @@ -610,6 +610,7 @@ div.widget input[type="text"] { padding: 5px 8px; background: white; color: black; + transition: background ease-out 0.3s; } div.TextWidget textarea:focus, @@ -1611,3 +1612,7 @@ div.leaflet-popup-content p.view-link { a.leaflet-popup-close-button { border: 0; } + +div.widget input[type=text][readonly] { + background: #f0f0f0; +} diff --git a/wcs/qommon/static/js/qommon.admin.js b/wcs/qommon/static/js/qommon.admin.js index 3f72607e..eda24dfd 100644 --- a/wcs/qommon/static/js/qommon.admin.js +++ b/wcs/qommon/static/js/qommon.admin.js @@ -61,6 +61,27 @@ $(function() { return false; }); + /* keep title/slug in sync */ + $('body').delegate('input[data-slug-sync]', 'keyup change paste', + function() { + var $slug_field = $(this).parents('form').find('[name=' + $(this).data('slug-sync') + ']'); + if ($slug_field.prop('readonly')) return; + $slug_field.val($.slugify($(this).val())); + }); + + /* remove readonly attribute from fields */ + $('body').delegate('a.change-nevertheless', 'click', function(e) { + var readonly_fields = $(this).parents('form').find('input[readonly]'); + console.log(readonly_fields); + if (readonly_fields.length) { + console.log('a'); + readonly_fields.prop('readonly', false); + readonly_fields[0].focus(); + } + $(this).parent().hide(); + return false; + }); + /* submission channel */ $('div.submit-channel-selection').show().find('select').on('change', function() { $('input[type=hidden][name=submission_channel]').val($(this).val()); diff --git a/wcs/qommon/static/js/qommon.geolocation.js b/wcs/qommon/static/js/qommon.geolocation.js index b8d7a3a3..3a84544a 100644 --- a/wcs/qommon/static/js/qommon.geolocation.js +++ b/wcs/qommon/static/js/qommon.geolocation.js @@ -1,140 +1,4 @@ $(function() { -/* the slugify code is adapted from the urlify code of django, - * django/contrib/admin/static/admin/js/urlify.js. - */ - -var LATIN_MAP = { - 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE', 'Ç': - 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I', 'Î': 'I', - 'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ö': - 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'Ű': 'U', - 'Ý': 'Y', 'Þ': 'TH', 'Ÿ': 'Y', 'ß': 'ss', 'à':'a', 'á':'a', 'â': 'a', 'ã': - 'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', 'è': 'e', 'é': 'e', 'ê': 'e', - 'ë': 'e', 'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': - 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', - 'ú': 'u', 'û': 'u', 'ü': 'u', 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y' -}; -var LATIN_SYMBOLS_MAP = { - '©':'(c)' -}; -var GREEK_MAP = { - 'α':'a', 'β':'b', 'γ':'g', 'δ':'d', 'ε':'e', 'ζ':'z', 'η':'h', 'θ':'8', - 'ι':'i', 'κ':'k', 'λ':'l', 'μ':'m', 'ν':'n', 'ξ':'3', 'ο':'o', 'π':'p', - 'ρ':'r', 'σ':'s', 'τ':'t', 'υ':'y', 'φ':'f', 'χ':'x', 'ψ':'ps', 'ω':'w', - 'ά':'a', 'έ':'e', 'ί':'i', 'ό':'o', 'ύ':'y', 'ή':'h', 'ώ':'w', 'ς':'s', - 'ϊ':'i', 'ΰ':'y', 'ϋ':'y', 'ΐ':'i', - 'Α':'A', 'Β':'B', 'Γ':'G', 'Δ':'D', 'Ε':'E', 'Ζ':'Z', 'Η':'H', 'Θ':'8', - 'Ι':'I', 'Κ':'K', 'Λ':'L', 'Μ':'M', 'Ν':'N', 'Ξ':'3', 'Ο':'O', 'Π':'P', - 'Ρ':'R', 'Σ':'S', 'Τ':'T', 'Υ':'Y', 'Φ':'F', 'Χ':'X', 'Ψ':'PS', 'Ω':'W', - 'Ά':'A', 'Έ':'E', 'Ί':'I', 'Ό':'O', 'Ύ':'Y', 'Ή':'H', 'Ώ':'W', 'Ϊ':'I', - 'Ϋ':'Y' -}; -var TURKISH_MAP = { - 'ş':'s', 'Ş':'S', 'ı':'i', 'İ':'I', 'ç':'c', 'Ç':'C', 'ü':'u', 'Ü':'U', - 'ö':'o', 'Ö':'O', 'ğ':'g', 'Ğ':'G' -}; -var RUSSIAN_MAP = { - 'а':'a', 'б':'b', 'в':'v', 'г':'g', 'д':'d', 'е':'e', 'ё':'yo', 'ж':'zh', - 'з':'z', 'и':'i', 'й':'j', 'к':'k', 'л':'l', 'м':'m', 'н':'n', 'о':'o', - 'п':'p', 'р':'r', 'с':'s', 'т':'t', 'у':'u', 'ф':'f', 'х':'h', 'ц':'c', - 'ч':'ch', 'ш':'sh', 'щ':'sh', 'ъ':'', 'ы':'y', 'ь':'', 'э':'e', 'ю':'yu', - 'я':'ya', - 'А':'A', 'Б':'B', 'В':'V', 'Г':'G', 'Д':'D', 'Е':'E', 'Ё':'Yo', 'Ж':'Zh', - 'З':'Z', 'И':'I', 'Й':'J', 'К':'K', 'Л':'L', 'М':'M', 'Н':'N', 'О':'O', - 'П':'P', 'Р':'R', 'С':'S', 'Т':'T', 'У':'U', 'Ф':'F', 'Х':'H', 'Ц':'C', - 'Ч':'Ch', 'Ш':'Sh', 'Щ':'Sh', 'Ъ':'', 'Ы':'Y', 'Ь':'', 'Э':'E', 'Ю':'Yu', - 'Я':'Ya' -}; -var UKRAINIAN_MAP = { - 'Є':'Ye', 'І':'I', 'Ї':'Yi', 'Ґ':'G', 'є':'ye', 'і':'i', 'ї':'yi', 'ґ':'g' -}; -var CZECH_MAP = { - 'č':'c', 'ď':'d', 'ě':'e', 'ň': 'n', 'ř':'r', 'š':'s', 'ť':'t', 'ů':'u', - 'ž':'z', 'Č':'C', 'Ď':'D', 'Ě':'E', 'Ň': 'N', 'Ř':'R', 'Š':'S', 'Ť':'T', - 'Ů':'U', 'Ž':'Z' -}; -var POLISH_MAP = { - 'ą':'a', 'ć':'c', 'ę':'e', 'ł':'l', 'ń':'n', 'ó':'o', 'ś':'s', 'ź':'z', - 'ż':'z', 'Ą':'A', 'Ć':'C', 'Ę':'E', 'Ł':'L', 'Ń':'N', 'Ó':'O', 'Ś':'S', - 'Ź':'Z', 'Ż':'Z' -}; -var LATVIAN_MAP = { - 'ā':'a', 'č':'c', 'ē':'e', 'ģ':'g', 'ī':'i', 'ķ':'k', 'ļ':'l', 'ņ':'n', - 'š':'s', 'ū':'u', 'ž':'z', 'Ā':'A', 'Č':'C', 'Ē':'E', 'Ģ':'G', 'Ī':'I', - 'Ķ':'K', 'Ļ':'L', 'Ņ':'N', 'Š':'S', 'Ū':'U', 'Ž':'Z' -}; -var ARABIC_MAP = { - 'أ':'a', 'ب':'b', 'ت':'t', 'ث': 'th', 'ج':'g', 'ح':'h', 'خ':'kh', 'د':'d', - 'ذ':'th', 'ر':'r', 'ز':'z', 'س':'s', 'ش':'sh', 'ص':'s', 'ض':'d', 'ط':'t', - 'ظ':'th', 'ع':'aa', 'غ':'gh', 'ف':'f', 'ق':'k', 'ك':'k', 'ل':'l', 'م':'m', - 'ن':'n', 'ه':'h', 'و':'o', 'ي':'y' -}; -var LITHUANIAN_MAP = { - 'ą':'a', 'č':'c', 'ę':'e', 'ė':'e', 'į':'i', 'š':'s', 'ų':'u', 'ū':'u', - 'ž':'z', - 'Ą':'A', 'Č':'C', 'Ę':'E', 'Ė':'E', 'Į':'I', 'Š':'S', 'Ų':'U', 'Ū':'U', - 'Ž':'Z' -}; -var SERBIAN_MAP = { - 'ђ':'dj', 'ј':'j', 'љ':'lj', 'њ':'nj', 'ћ':'c', 'џ':'dz', 'đ':'dj', - 'Ђ':'Dj', 'Ј':'j', 'Љ':'Lj', 'Њ':'Nj', 'Ћ':'C', 'Џ':'Dz', 'Đ':'Dj' -}; -var AZERBAIJANI_MAP = { - 'ç':'c', 'ə':'e', 'ğ':'g', 'ı':'i', 'ö':'o', 'ş':'s', 'ü':'u', - 'Ç':'C', 'Ə':'E', 'Ğ':'G', 'İ':'I', 'Ö':'O', 'Ş':'S', 'Ü':'U' -}; - -var ALL_DOWNCODE_MAPS = [ - LATIN_MAP, - LATIN_SYMBOLS_MAP, - GREEK_MAP, - TURKISH_MAP, - RUSSIAN_MAP, - UKRAINIAN_MAP, - CZECH_MAP, - POLISH_MAP, - LATVIAN_MAP, - ARABIC_MAP, - LITHUANIAN_MAP, - SERBIAN_MAP, - AZERBAIJANI_MAP -]; - -var Downcoder = { - 'Initialize': function() { - if (Downcoder.map) { // already made - return; - } - Downcoder.map = {}; - Downcoder.chars = []; - for (var i=0; i