0001-admin-make-slug-editable-on-connector-create-view-21.patch
passerelle/base/models.py | ||
---|---|---|
111 | 111 | |
112 | 112 |
class BaseResource(models.Model): |
113 | 113 |
title = models.CharField(max_length=50, verbose_name=_('Title')) |
114 |
description = models.TextField(verbose_name=_('Description')) |
|
115 | 114 |
slug = models.SlugField(verbose_name=_('Identifier'), unique=True) |
115 |
description = models.TextField(verbose_name=_('Description')) |
|
116 | 116 |
users = models.ManyToManyField(ApiUser, blank=True, related_name='+', related_query_name='+') |
117 | 117 |
objects = InheritanceManager() |
118 | 118 |
passerelle/static/js/passerelle.js | ||
---|---|---|
17 | 17 |
var $dialog = $(response).dialog({modal: true, width: 'auto'}); |
18 | 18 |
}); |
19 | 19 |
}); |
20 | ||
21 |
/* keep title/slug in sync, |
|
22 |
this code comes from wcs/qommon/static/js/qommon.admin.js |
|
23 |
*/ |
|
24 |
$('body').delegate('input[data-slug-sync]', 'input change paste', |
|
25 |
function() { |
|
26 |
var $slug_field = $(this).parents('form').find('[name=' + $(this).data('slug-sync') + ']'); |
|
27 |
$slug_field.val($.slugify($(this).val())); |
|
28 |
}); |
|
20 | 29 |
}); |
passerelle/static/js/slugify.js | ||
---|---|---|
1 |
$(function() { |
|
2 |
/* the slugify code is adapted from the urlify code of django, |
|
3 |
* django/contrib/admin/static/admin/js/urlify.js. |
|
4 |
*/ |
|
5 | ||
6 |
var LATIN_MAP = { |
|
7 |
'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE', 'Ç': |
|
8 |
'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I', 'Î': 'I', |
|
9 |
'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ö': |
|
10 |
'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'Ű': 'U', |
|
11 |
'Ý': 'Y', 'Þ': 'TH', 'Ÿ': 'Y', 'ß': 'ss', 'à':'a', 'á':'a', 'â': 'a', 'ã': |
|
12 |
'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', 'è': 'e', 'é': 'e', 'ê': 'e', |
|
13 |
'ë': 'e', 'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': |
|
14 |
'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', |
|
15 |
'ú': 'u', 'û': 'u', 'ü': 'u', 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y' |
|
16 |
}; |
|
17 |
var LATIN_SYMBOLS_MAP = { |
|
18 |
'©':'(c)' |
|
19 |
}; |
|
20 |
var GREEK_MAP = { |
|
21 |
'α':'a', 'β':'b', 'γ':'g', 'δ':'d', 'ε':'e', 'ζ':'z', 'η':'h', 'θ':'8', |
|
22 |
'ι':'i', 'κ':'k', 'λ':'l', 'μ':'m', 'ν':'n', 'ξ':'3', 'ο':'o', 'π':'p', |
|
23 |
'ρ':'r', 'σ':'s', 'τ':'t', 'υ':'y', 'φ':'f', 'χ':'x', 'ψ':'ps', 'ω':'w', |
|
24 |
'ά':'a', 'έ':'e', 'ί':'i', 'ό':'o', 'ύ':'y', 'ή':'h', 'ώ':'w', 'ς':'s', |
|
25 |
'ϊ':'i', 'ΰ':'y', 'ϋ':'y', 'ΐ':'i', |
|
26 |
'Α':'A', 'Β':'B', 'Γ':'G', 'Δ':'D', 'Ε':'E', 'Ζ':'Z', 'Η':'H', 'Θ':'8', |
|
27 |
'Ι':'I', 'Κ':'K', 'Λ':'L', 'Μ':'M', 'Ν':'N', 'Ξ':'3', 'Ο':'O', 'Π':'P', |
|
28 |
'Ρ':'R', 'Σ':'S', 'Τ':'T', 'Υ':'Y', 'Φ':'F', 'Χ':'X', 'Ψ':'PS', 'Ω':'W', |
|
29 |
'Ά':'A', 'Έ':'E', 'Ί':'I', 'Ό':'O', 'Ύ':'Y', 'Ή':'H', 'Ώ':'W', 'Ϊ':'I', |
|
30 |
'Ϋ':'Y' |
|
31 |
}; |
|
32 |
var TURKISH_MAP = { |
|
33 |
'ş':'s', 'Ş':'S', 'ı':'i', 'İ':'I', 'ç':'c', 'Ç':'C', 'ü':'u', 'Ü':'U', |
|
34 |
'ö':'o', 'Ö':'O', 'ğ':'g', 'Ğ':'G' |
|
35 |
}; |
|
36 |
var RUSSIAN_MAP = { |
|
37 |
'а':'a', 'б':'b', 'в':'v', 'г':'g', 'д':'d', 'е':'e', 'ё':'yo', 'ж':'zh', |
|
38 |
'з':'z', 'и':'i', 'й':'j', 'к':'k', 'л':'l', 'м':'m', 'н':'n', 'о':'o', |
|
39 |
'п':'p', 'р':'r', 'с':'s', 'т':'t', 'у':'u', 'ф':'f', 'х':'h', 'ц':'c', |
|
40 |
'ч':'ch', 'ш':'sh', 'щ':'sh', 'ъ':'', 'ы':'y', 'ь':'', 'э':'e', 'ю':'yu', |
|
41 |
'я':'ya', |
|
42 |
'А':'A', 'Б':'B', 'В':'V', 'Г':'G', 'Д':'D', 'Е':'E', 'Ё':'Yo', 'Ж':'Zh', |
|
43 |
'З':'Z', 'И':'I', 'Й':'J', 'К':'K', 'Л':'L', 'М':'M', 'Н':'N', 'О':'O', |
|
44 |
'П':'P', 'Р':'R', 'С':'S', 'Т':'T', 'У':'U', 'Ф':'F', 'Х':'H', 'Ц':'C', |
|
45 |
'Ч':'Ch', 'Ш':'Sh', 'Щ':'Sh', 'Ъ':'', 'Ы':'Y', 'Ь':'', 'Э':'E', 'Ю':'Yu', |
|
46 |
'Я':'Ya' |
|
47 |
}; |
|
48 |
var UKRAINIAN_MAP = { |
|
49 |
'Є':'Ye', 'І':'I', 'Ї':'Yi', 'Ґ':'G', 'є':'ye', 'і':'i', 'ї':'yi', 'ґ':'g' |
|
50 |
}; |
|
51 |
var CZECH_MAP = { |
|
52 |
'č':'c', 'ď':'d', 'ě':'e', 'ň': 'n', 'ř':'r', 'š':'s', 'ť':'t', 'ů':'u', |
|
53 |
'ž':'z', 'Č':'C', 'Ď':'D', 'Ě':'E', 'Ň': 'N', 'Ř':'R', 'Š':'S', 'Ť':'T', |
|
54 |
'Ů':'U', 'Ž':'Z' |
|
55 |
}; |
|
56 |
var POLISH_MAP = { |
|
57 |
'ą':'a', 'ć':'c', 'ę':'e', 'ł':'l', 'ń':'n', 'ó':'o', 'ś':'s', 'ź':'z', |
|
58 |
'ż':'z', 'Ą':'A', 'Ć':'C', 'Ę':'E', 'Ł':'L', 'Ń':'N', 'Ó':'O', 'Ś':'S', |
|
59 |
'Ź':'Z', 'Ż':'Z' |
|
60 |
}; |
|
61 |
var LATVIAN_MAP = { |
|
62 |
'ā':'a', 'č':'c', 'ē':'e', 'ģ':'g', 'ī':'i', 'ķ':'k', 'ļ':'l', 'ņ':'n', |
|
63 |
'š':'s', 'ū':'u', 'ž':'z', 'Ā':'A', 'Č':'C', 'Ē':'E', 'Ģ':'G', 'Ī':'I', |
|
64 |
'Ķ':'K', 'Ļ':'L', 'Ņ':'N', 'Š':'S', 'Ū':'U', 'Ž':'Z' |
|
65 |
}; |
|
66 |
var ARABIC_MAP = { |
|
67 |
'أ':'a', 'ب':'b', 'ت':'t', 'ث': 'th', 'ج':'g', 'ح':'h', 'خ':'kh', 'د':'d', |
|
68 |
'ذ':'th', 'ر':'r', 'ز':'z', 'س':'s', 'ش':'sh', 'ص':'s', 'ض':'d', 'ط':'t', |
|
69 |
'ظ':'th', 'ع':'aa', 'غ':'gh', 'ف':'f', 'ق':'k', 'ك':'k', 'ل':'l', 'م':'m', |
|
70 |
'ن':'n', 'ه':'h', 'و':'o', 'ي':'y' |
|
71 |
}; |
|
72 |
var LITHUANIAN_MAP = { |
|
73 |
'ą':'a', 'č':'c', 'ę':'e', 'ė':'e', 'į':'i', 'š':'s', 'ų':'u', 'ū':'u', |
|
74 |
'ž':'z', |
|
75 |
'Ą':'A', 'Č':'C', 'Ę':'E', 'Ė':'E', 'Į':'I', 'Š':'S', 'Ų':'U', 'Ū':'U', |
|
76 |
'Ž':'Z' |
|
77 |
}; |
|
78 |
var SERBIAN_MAP = { |
|
79 |
'ђ':'dj', 'ј':'j', 'љ':'lj', 'њ':'nj', 'ћ':'c', 'џ':'dz', 'đ':'dj', |
|
80 |
'Ђ':'Dj', 'Ј':'j', 'Љ':'Lj', 'Њ':'Nj', 'Ћ':'C', 'Џ':'Dz', 'Đ':'Dj' |
|
81 |
}; |
|
82 |
var AZERBAIJANI_MAP = { |
|
83 |
'ç':'c', 'ə':'e', 'ğ':'g', 'ı':'i', 'ö':'o', 'ş':'s', 'ü':'u', |
|
84 |
'Ç':'C', 'Ə':'E', 'Ğ':'G', 'İ':'I', 'Ö':'O', 'Ş':'S', 'Ü':'U' |
|
85 |
}; |
|
86 | ||
87 |
var ALL_DOWNCODE_MAPS = [ |
|
88 |
LATIN_MAP, |
|
89 |
LATIN_SYMBOLS_MAP, |
|
90 |
GREEK_MAP, |
|
91 |
TURKISH_MAP, |
|
92 |
RUSSIAN_MAP, |
|
93 |
UKRAINIAN_MAP, |
|
94 |
CZECH_MAP, |
|
95 |
POLISH_MAP, |
|
96 |
LATVIAN_MAP, |
|
97 |
ARABIC_MAP, |
|
98 |
LITHUANIAN_MAP, |
|
99 |
SERBIAN_MAP, |
|
100 |
AZERBAIJANI_MAP |
|
101 |
]; |
|
102 | ||
103 |
var Downcoder = { |
|
104 |
'Initialize': function() { |
|
105 |
if (Downcoder.map) { // already made |
|
106 |
return; |
|
107 |
} |
|
108 |
Downcoder.map = {}; |
|
109 |
Downcoder.chars = []; |
|
110 |
for (var i=0; i<ALL_DOWNCODE_MAPS.length; i++) { |
|
111 |
var lookup = ALL_DOWNCODE_MAPS[i]; |
|
112 |
for (var c in lookup) { |
|
113 |
if (lookup.hasOwnProperty(c)) { |
|
114 |
Downcoder.map[c] = lookup[c]; |
|
115 |
} |
|
116 |
} |
|
117 |
} |
|
118 |
for (var k in Downcoder.map) { |
|
119 |
if (Downcoder.map.hasOwnProperty(k)) { |
|
120 |
Downcoder.chars.push(k); |
|
121 |
} |
|
122 |
} |
|
123 |
Downcoder.regex = new RegExp(Downcoder.chars.join('|'), 'g'); |
|
124 |
} |
|
125 |
}; |
|
126 | ||
127 |
$.slugify = function(value) { |
|
128 |
Downcoder.Initialize(); |
|
129 |
s = value.replace(Downcoder.regex, function(m) {return Downcoder.map[m]; }); |
|
130 |
s = s.replace(/\'/g, ' '); // keep quotes as spaces |
|
131 |
s = s.replace(/[^-\w\s]/g, ''); // remove unneeded chars |
|
132 |
s = s.replace(/^\s+|\s+$/g, ''); // trim leading/trailing spaces |
|
133 |
s = s.replace(/[-\s]+/g, '-'); // convert spaces to hyphens |
|
134 |
s = s.toLowerCase(); // convert to lowercase |
|
135 |
return s; |
|
136 |
} |
|
137 |
}); |
passerelle/templates/passerelle/manage/service_form.html | ||
---|---|---|
1 | 1 |
{% extends "passerelle/manage.html" %} |
2 |
{% load i18n gadjo %} |
|
2 |
{% load i18n gadjo static %} |
|
3 | ||
4 |
{% block extrascripts %} |
|
5 |
<script src="{% static "js/slugify.js" %}"></script> |
|
6 |
{{ block.super }} |
|
7 |
{% endblock %} |
|
3 | 8 | |
4 | 9 |
{% block breadcrumb %} |
5 | 10 |
{{ block.super }} |
passerelle/views.py | ||
---|---|---|
129 | 129 | |
130 | 130 | |
131 | 131 |
class GenericConnectorMixin(object): |
132 |
exclude_fields = ('slug', 'users') |
|
133 | ||
132 | 134 |
def get_connector(self, **kwargs): |
133 | 135 |
return kwargs.get('connector') |
134 | 136 | |
... | ... | |
149 | 151 |
self.form_class = modelform_factory( |
150 | 152 |
self.model, |
151 | 153 |
form=GenericConnectorForm, |
152 |
exclude=('slug', 'users'))
|
|
154 |
exclude=self.exclude_fields)
|
|
153 | 155 |
for field in self.form_class.base_fields.values(): |
154 | 156 |
if isinstance(field.widget, ClearableFileInput): |
155 | 157 |
field.widget.template_with_initial = ''\ |
... | ... | |
179 | 181 | |
180 | 182 |
class GenericCreateConnectorView(GenericConnectorMixin, CreateView): |
181 | 183 |
template_name = 'passerelle/manage/service_form.html' |
184 |
exclude_fields = ('users',) # slug not excluded |
|
182 | 185 | |
183 | 186 |
def form_valid(self, form): |
184 | 187 |
with transaction.atomic(): |
... | ... | |
186 | 189 |
self.object.availability() |
187 | 190 |
return response |
188 | 191 | |
192 |
def init_stuff(self, request, *args, **kwargs): |
|
193 |
super(GenericCreateConnectorView, self).init_stuff(request, *args, **kwargs) |
|
194 |
# tell JS to prepopulate 'slug' field using the 'title' field |
|
195 |
self.form_class.base_fields['title'].widget.attrs['data-slug-sync'] = 'slug' |
|
196 | ||
189 | 197 | |
190 | 198 |
class GenericEditConnectorView(GenericConnectorMixin, UpdateView): |
191 | 199 |
template_name = 'passerelle/manage/service_form.html' |
tests/test_manager.py | ||
---|---|---|
66 | 66 |
assert 'Geographic information system' in resp.body |
67 | 67 |
resp = resp.click('Base Adresse Web Service') |
68 | 68 |
resp.forms[0]['title'] = 'Test Connector' |
69 |
resp.forms[0]['slug'] = 'test-connector' |
|
69 | 70 |
resp.forms[0]['description'] = 'Connector for a simple test' |
70 | 71 |
resp.forms[0]['service_url'] = 'https://api-adresse.data.gouv.fr/' |
71 | 72 |
resp = resp.forms[0].submit() |
... | ... | |
77 | 78 |
resp = app.get('/manage/', status=200) |
78 | 79 |
assert 'Test Connector' in resp.body |
79 | 80 | |
81 |
def test_add_connector_unique_slug(app, admin_user): |
|
82 |
app = login(app) |
|
83 |
resp = app.get('/manage/', status=200) |
|
84 |
resp = resp.click('Add Connector') |
|
85 |
resp = resp.click('Base Adresse Web Service') |
|
86 |
resp.forms[0]['title'] = 'Test Connector' |
|
87 |
resp.forms[0]['slug'] = 'test-connector' |
|
88 |
resp.forms[0]['description'] = 'Connector for a simple test' |
|
89 |
resp.forms[0]['service_url'] = 'https://api-adresse.data.gouv.fr/' |
|
90 |
resp1 = resp.forms[0].submit() |
|
91 |
assert resp1.status_int == 302 |
|
92 | ||
93 |
resp2 = resp.forms[0].submit() |
|
94 |
assert 'There were errors processing your form.' in resp2.body |
|
95 |
assert 'this Identifier already exists.' in resp2.body |
|
96 |
resp.forms[0]['slug'] = 'foo' |
|
97 |
resp2 = resp.forms[0].submit() |
|
98 |
assert resp2.status_int == 302 |
|
99 | ||
80 | 100 |
def test_visit_connectors(app, admin_user): |
81 | 101 |
app = login(app) |
82 | 102 |
resp = app.get('/manage/', status=200) |
83 |
- |