0001-general-add-new-catalog-of-webservice-calls-usable-i.patch
tests/test_admin_pages.py | ||
---|---|---|
26 | 26 |
from wcs.qommon.template import get_current_theme |
27 | 27 |
from wcs.categories import Category |
28 | 28 |
from wcs.data_sources import NamedDataSource |
29 |
from wcs.wscalls import NamedWsCall |
|
29 | 30 |
from wcs.roles import Role |
30 | 31 |
from wcs.workflows import Workflow, DisplayMessageWorkflowStatusItem, WorkflowCriticalityLevel |
31 | 32 |
from wcs.wf.wscall import WebserviceCallStatusItem |
... | ... | |
2951 | 2952 |
resp = resp.forms[0].submit('submit') |
2952 | 2953 |
assert resp.location == 'http://example.net/backoffice/settings/data-sources/1/' |
2953 | 2954 | |
2955 |
def test_wscalls_new(pub): |
|
2956 |
create_superuser(pub) |
|
2957 |
NamedWsCall.wipe() |
|
2958 |
app = login(get_app(pub)) |
|
2959 | ||
2960 |
# go to the page and cancel |
|
2961 |
resp = app.get('/backoffice/settings/wscalls/') |
|
2962 |
resp = resp.click('New webservice call') |
|
2963 |
resp = resp.forms[0].submit('cancel') |
|
2964 |
assert resp.location == 'http://example.net/backoffice/settings/wscalls/' |
|
2965 | ||
2966 |
# go to the page and add a webservice call |
|
2967 |
resp = app.get('/backoffice/settings/wscalls/') |
|
2968 |
resp = resp.click('New webservice call') |
|
2969 |
resp.form['name'] = 'a new webservice call' |
|
2970 |
resp.form['description'] = 'description' |
|
2971 |
resp.form['request$url'] = 'http://remote.example.net/json' |
|
2972 |
resp = resp.form.submit('submit') |
|
2973 |
assert resp.location == 'http://example.net/backoffice/settings/wscalls/' |
|
2974 |
resp = resp.follow() |
|
2975 |
assert 'a new webservice call' in resp.body |
|
2976 |
resp = resp.click('a new webservice call') |
|
2977 |
assert 'Webservice Call - a new webservice call' in resp.body |
|
2978 |
resp = resp.click('Edit') |
|
2979 |
assert 'Edit webservice call' in resp.body |
|
2980 | ||
2981 |
assert NamedWsCall.get('a_new_webservice_call').name == 'a new webservice call' |
|
2982 | ||
2983 |
def test_wscalls_view(pub): |
|
2984 |
create_superuser(pub) |
|
2985 |
NamedWsCall.wipe() |
|
2986 | ||
2987 |
wscall = NamedWsCall(name='xxx') |
|
2988 |
wscall.description = 'description' |
|
2989 |
wscall.request = { |
|
2990 |
'url': 'http://remote.example.net/json', |
|
2991 |
'request_signature_key': 'xxx', |
|
2992 |
'qs_data': {'a': 'b'}, |
|
2993 |
'method': 'POST', |
|
2994 |
'post_data': {'c': 'd'}, |
|
2995 |
} |
|
2996 |
wscall.store() |
|
2997 | ||
2998 |
app = login(get_app(pub)) |
|
2999 |
resp = app.get('/backoffice/settings/wscalls/%s/' % wscall.id) |
|
3000 |
assert 'http://remote.example.net/json' in resp.body |
|
3001 | ||
3002 |
def test_wscalls_edit(pub): |
|
3003 |
test_wscalls_view(pub) |
|
3004 | ||
3005 |
app = login(get_app(pub)) |
|
3006 | ||
3007 |
resp = app.get('/backoffice/settings/wscalls/xxx/') |
|
3008 |
resp = resp.click(href='edit') |
|
3009 |
assert resp.form['name'].value == 'xxx' |
|
3010 |
assert 'slug' in resp.form.fields |
|
3011 |
resp.form['description'] = 'bla bla bla' |
|
3012 |
resp = resp.form.submit('submit') |
|
3013 |
assert resp.location == 'http://example.net/backoffice/settings/wscalls/xxx/' |
|
3014 |
resp = resp.follow() |
|
3015 | ||
3016 |
assert NamedWsCall.get('xxx').description == 'bla bla bla' |
|
3017 | ||
3018 |
resp = app.get('/backoffice/settings/wscalls/xxx/') |
|
3019 |
resp = resp.click(href='edit') |
|
3020 |
assert resp.form['name'].value == 'xxx' |
|
3021 |
assert 'slug' in resp.form.fields |
|
3022 |
resp.form['slug'] = 'yyy' |
|
3023 |
resp = resp.form.submit('submit') |
|
3024 |
assert resp.location == 'http://example.net/backoffice/settings/wscalls/yyy/' |
|
3025 | ||
3026 | ||
3027 |
def test_wscalls_delete(pub): |
|
3028 |
test_wscalls_view(pub) |
|
3029 |
assert NamedWsCall.count() == 1 |
|
3030 | ||
3031 |
app = login(get_app(pub)) |
|
3032 | ||
3033 |
resp = app.get('/backoffice/settings/wscalls/xxx/') |
|
3034 |
resp = resp.click(href='delete') |
|
3035 |
resp = resp.form.submit('cancel') |
|
3036 |
assert resp.location == 'http://example.net/backoffice/settings/wscalls/' |
|
3037 | ||
3038 |
resp = app.get('/backoffice/settings/wscalls/xxx/') |
|
3039 |
resp = resp.click(href='delete') |
|
3040 |
resp = resp.form.submit('submit') |
|
3041 |
assert resp.location == 'http://example.net/backoffice/settings/wscalls/' |
|
3042 |
assert NamedWsCall.count() == 0 |
|
3043 | ||
2954 | 3044 |
def test_settings_permissions(pub): |
2955 | 3045 |
create_superuser(pub) |
2956 | 3046 |
role1 = create_role() |
wcs/admin/settings.py | ||
---|---|---|
53 | 53 |
from wcs.roles import Role |
54 | 54 | |
55 | 55 |
from .data_sources import NamedDataSourcesDirectory |
56 | ||
56 |
from .wscalls import NamedWsCallsDirectory |
|
57 | 57 | |
58 | 58 |
class UserFormDirectory(Directory): |
59 | 59 |
_q_exports = [''] |
... | ... | |
387 | 387 |
'session', 'download_theme', 'smstest', 'postgresql', |
388 | 388 |
('admin-permissions', 'admin_permissions'), |
389 | 389 |
'theme_preview', 'filetypes', |
390 |
('data-sources', 'data_sources')] |
|
390 |
('data-sources', 'data_sources'), 'wscalls']
|
|
391 | 391 | |
392 | 392 |
emails = EmailsDirectory() |
393 | 393 |
identification = IdentificationDirectory() |
... | ... | |
396 | 396 |
theme_preview = ThemePreviewDirectory() |
397 | 397 |
filetypes = FileTypesDirectory() |
398 | 398 |
data_sources = NamedDataSourcesDirectory() |
399 |
wscalls = NamedWsCallsDirectory() |
|
399 | 400 | |
400 | 401 |
def _q_index(self): |
401 | 402 |
html_top('settings', title = _('Settings')) |
... | ... | |
498 | 499 |
_('File Types'), _('Configure known file types')) |
499 | 500 |
r += htmltext('<dt><a href="data-sources/">%s</a></dt> <dd>%s</dd>') % ( |
500 | 501 |
_('Data sources'), _('Configure data sources')) |
502 |
r += htmltext('<dt><a href="wscalls/">%s</a></dt> <dd>%s</dd>') % ( |
|
503 |
_('Webservice calls'), _('Configure webservice calls')) |
|
501 | 504 |
r += htmltext('</dl>') |
502 | 505 |
r += htmltext('</div>') |
503 | 506 |
wcs/admin/wscalls.py | ||
---|---|---|
1 |
# w.c.s. - web application for online forms |
|
2 |
# Copyright (C) 2005-2016 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software; you can redistribute it and/or modify |
|
5 |
# it under the terms of the GNU General Public License as published by |
|
6 |
# the Free Software Foundation; either version 2 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU General Public License |
|
15 |
# along with this program; if not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from quixote import redirect |
|
18 |
from quixote.directory import Directory |
|
19 |
from quixote.html import TemplateIO, htmltext |
|
20 | ||
21 |
from qommon import errors |
|
22 |
from qommon.form import * |
|
23 |
from qommon.backoffice.menu import html_top |
|
24 |
from wcs.wscalls import NamedWsCall, WsCallRequestWidget |
|
25 | ||
26 |
class NamedWsCallUI(object): |
|
27 |
def __init__(self, wscall): |
|
28 |
self.wscall = wscall |
|
29 |
if self.wscall is None: |
|
30 |
self.wscall = NamedWsCall() |
|
31 | ||
32 |
def get_form(self): |
|
33 |
form = Form(enctype='multipart/form-data', |
|
34 |
advanced_label=_('Additional options')) |
|
35 |
form.add(StringWidget, 'name', title=_('Name'), required=True, size=30, |
|
36 |
value=self.wscall.name) |
|
37 |
form.add(TextWidget, 'description', title=_('Description'), |
|
38 |
cols=40, rows=5, |
|
39 |
value=self.wscall.description) |
|
40 |
if self.wscall.slug: |
|
41 |
form.add(StringWidget, 'slug', |
|
42 |
value=self.wscall.slug, |
|
43 |
title=_('Identifier'), |
|
44 |
hint=_('Beware it is risky to change it'), |
|
45 |
required=True, advanced=True, |
|
46 |
) |
|
47 |
form.add(WsCallRequestWidget, 'request', |
|
48 |
value=self.wscall.request, |
|
49 |
title=_('Request'), required=True) |
|
50 | ||
51 |
form.add_submit('submit', _('Submit')) |
|
52 |
form.add_submit('cancel', _('Cancel')) |
|
53 |
return form |
|
54 | ||
55 |
def submit_form(self, form): |
|
56 |
name = form.get_widget('name').parse() |
|
57 |
if self.wscall.slug: |
|
58 |
slug = form.get_widget('slug').parse() |
|
59 |
else: |
|
60 |
slug = None |
|
61 | ||
62 |
for wscall in NamedWsCall.select(): |
|
63 |
if wscall.id == self.wscall.id: |
|
64 |
continue |
|
65 |
if name == wscall.name: |
|
66 |
form.get_widget('name').set_error(_('This name is already used.')) |
|
67 |
if slug == wscall.slug: |
|
68 |
form.get_widget('slug').set_error(_('This value is already used.')) |
|
69 |
if form.has_errors(): |
|
70 |
raise ValueError() |
|
71 | ||
72 |
self.wscall.name = name |
|
73 |
self.wscall.description = form.get_widget('description').parse() |
|
74 |
self.wscall.request = form.get_widget('request').parse() |
|
75 |
if self.wscall.slug: |
|
76 |
self.wscall.slug = slug |
|
77 |
self.wscall.store() |
|
78 | ||
79 | ||
80 |
class NamedWsCallPage(Directory): |
|
81 |
_q_exports = ['', 'edit', 'delete'] |
|
82 | ||
83 |
def __init__(self, component): |
|
84 |
try: |
|
85 |
self.wscall = NamedWsCall.get(component) |
|
86 |
except KeyError: |
|
87 |
raise errors.TraversalError() |
|
88 |
self.wscall_ui = NamedWsCallUI(self.wscall) |
|
89 |
get_response().breadcrumb.append((component + '/', self.wscall.name)) |
|
90 | ||
91 |
def _q_index(self): |
|
92 |
html_top('wscalls', title=self.wscall.name) |
|
93 |
r = TemplateIO(html=True) |
|
94 |
get_response().filter['sidebar'] = self.get_sidebar() |
|
95 | ||
96 |
r += htmltext('<h2>%s - ') % _('Webservice Call') |
|
97 |
r += self.wscall.name |
|
98 |
r += htmltext('</h2>') |
|
99 | ||
100 |
if self.wscall.description: |
|
101 |
r += htmltext('<div class="bo-block">') |
|
102 |
r += self.wscall.description |
|
103 |
r += htmltext('</div>') |
|
104 | ||
105 |
if self.wscall.request: |
|
106 |
r += htmltext('<div class="bo-block">') |
|
107 |
r += htmltext('<h3>%s</h3>') % _('Parameters') |
|
108 |
r += htmltext('<ul>') |
|
109 |
if self.wscall.request.get('url'): |
|
110 |
r += htmltext('<li>%s %s</li>') % ( |
|
111 |
_('URL:'), |
|
112 |
self.wscall.request.get('url')) |
|
113 |
if self.wscall.request.get('request_signature_key'): |
|
114 |
r += htmltext('<li>%s %s</li>') % ( |
|
115 |
_('Request Signature Key:'), |
|
116 |
self.wscall.request.get('request_signature_key')) |
|
117 |
if self.wscall.request.get('qs_data'): |
|
118 |
r += htmltext('<li>%s<ul>') % _('Query string data:') |
|
119 |
for k, v in self.wscall.request.get('qs_data').items(): |
|
120 |
r += htmltext('<li>%s</li>') % _('%s: %s') % (k, v) |
|
121 |
r += htmltext('</ul></li>') |
|
122 |
r += htmltext('<li>%s %s</li>') % ( |
|
123 |
_('Method:'), |
|
124 |
'POST' if self.wscall.request.get('method') == 'POST' else 'GET') |
|
125 |
if self.wscall.request.get('post_data'): |
|
126 |
r += htmltext('<li>%s<ul>') % _('Post data:') |
|
127 |
for k, v in self.wscall.request.get('post_data').items(): |
|
128 |
r += htmltext('<li>%s</li>') % _('%s: %s') % (k, v) |
|
129 |
r += htmltext('</ul></li>') |
|
130 |
r += htmltext('</ul>') |
|
131 |
r += htmltext('</div>') |
|
132 | ||
133 |
return r.getvalue() |
|
134 | ||
135 |
def get_sidebar(self): |
|
136 |
r = TemplateIO(html=True) |
|
137 |
r += htmltext('<ul id="sidebar-actions">') |
|
138 |
r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit') |
|
139 |
r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete') |
|
140 |
r += htmltext('</ul>') |
|
141 |
return r.getvalue() |
|
142 | ||
143 |
def edit(self): |
|
144 |
form = self.wscall_ui.get_form() |
|
145 |
if form.get_submit() == 'cancel': |
|
146 |
return redirect('.') |
|
147 | ||
148 |
if form.get_submit() == 'submit' and not form.has_errors(): |
|
149 |
try: |
|
150 |
self.wscall_ui.submit_form(form) |
|
151 |
except ValueError: |
|
152 |
pass |
|
153 |
else: |
|
154 |
return redirect('../%s/' % self.wscall.id) |
|
155 | ||
156 |
get_response().breadcrumb.append( ('edit', _('Edit')) ) |
|
157 |
html_top('wscalls', title = _('Edit webservice call')) |
|
158 |
r = TemplateIO(html=True) |
|
159 |
r += htmltext('<h2>%s</h2>') % _('Edit webservice call') |
|
160 |
r += form.render() |
|
161 |
return r.getvalue() |
|
162 | ||
163 |
def delete(self): |
|
164 |
form = Form(enctype='multipart/form-data') |
|
165 |
form.widgets.append(HtmlWidget('<p>%s</p>' % _( |
|
166 |
'You are about to irrevocably delete this data source.'))) |
|
167 |
form.add_submit('delete', _('Submit')) |
|
168 |
form.add_submit('cancel', _('Cancel')) |
|
169 |
if form.get_widget('cancel').parse(): |
|
170 |
return redirect('..') |
|
171 |
if not form.is_submitted() or form.has_errors(): |
|
172 |
get_response().breadcrumb.append(('delete', _('Delete'))) |
|
173 |
html_top('wscalls', title = _('Delete webservice call')) |
|
174 |
r = TemplateIO(html=True) |
|
175 |
r += htmltext('<h2>%s %s</h2>') % (_('Deleting webservice call:'), self.wscall.name) |
|
176 |
r += form.render() |
|
177 |
return r.getvalue() |
|
178 |
else: |
|
179 |
self.wscall.remove_self() |
|
180 |
return redirect('..') |
|
181 | ||
182 | ||
183 |
class NamedWsCallsDirectory(Directory): |
|
184 |
_q_exports = ['', 'new'] |
|
185 | ||
186 |
def _q_traverse(self, path): |
|
187 |
get_response().breadcrumb.append( ('wscalls/', _('Webservice Calls')) ) |
|
188 |
return super(NamedWsCallsDirectory, self)._q_traverse(path) |
|
189 | ||
190 |
def _q_index(self): |
|
191 |
get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'biglist.js', |
|
192 |
'ckeditor/ckeditor.js', 'qommon.wysiwyg.js', 'ckeditor/adapters/jquery.js']) |
|
193 |
get_response().filter['sidebar'] = self.get_sidebar() |
|
194 |
html_top('wscalls', title=_('Webservice Calls')) |
|
195 |
r = TemplateIO(html=True) |
|
196 | ||
197 |
r += htmltext('<div class="bo-block">') |
|
198 |
r += htmltext('<h2>%s</h2>') % _('Webservice Calls') |
|
199 |
r += htmltext('<ul class="biglist" id="wscall-list">') |
|
200 |
wscalls = NamedWsCall.select(order_by='name') |
|
201 |
for wscall in wscalls: |
|
202 |
r += htmltext('<li class="biglistitem" id="itemId_%s">') % wscall.id |
|
203 |
r += htmltext('<strong class="label"><a href="%s/">%s (%s)</a></strong>') % ( |
|
204 |
wscall.id, wscall.name, wscall.slug) |
|
205 |
r += htmltext('<p class="details">') |
|
206 |
r += htmltext('</p></li>') |
|
207 |
r += htmltext('</ul>') |
|
208 |
r += htmltext('</div>') |
|
209 |
return r.getvalue() |
|
210 | ||
211 |
def get_sidebar(self): |
|
212 |
r = TemplateIO(html=True) |
|
213 |
r += htmltext("""<ul id="sidebar-actions"> |
|
214 |
<li><a class="new-item" href="new">%s</a></li> |
|
215 |
</ul>""") % _('New webservice call') |
|
216 | ||
217 |
return r.getvalue() |
|
218 | ||
219 |
def new(self): |
|
220 |
get_response().breadcrumb.append( ('new', _('New')) ) |
|
221 |
wscall_ui = NamedWsCallUI(None) |
|
222 |
form = wscall_ui.get_form() |
|
223 |
if form.get_widget('cancel').parse(): |
|
224 |
return redirect('.') |
|
225 | ||
226 |
if form.get_submit() == 'submit' and not form.has_errors(): |
|
227 |
try: |
|
228 |
wscall_ui.submit_form(form) |
|
229 |
except ValueError: |
|
230 |
pass |
|
231 |
else: |
|
232 |
return redirect('.') |
|
233 | ||
234 |
html_top('wscalls', title = _('New webservice call')) |
|
235 |
r = TemplateIO(html=True) |
|
236 |
r += htmltext('<h2>%s</h2>') % _('New webservice call') |
|
237 |
r += form.render() |
|
238 |
return r.getvalue() |
|
239 | ||
240 |
def _q_lookup(self, component): |
|
241 |
return NamedWsCallPage(component) |
wcs/qommon/form.py | ||
---|---|---|
1341 | 1341 |
if hint: |
1342 | 1342 |
self.hint = hint |
1343 | 1343 | |
1344 |
def render_content(self): |
|
1345 |
r = TemplateIO(html=True) |
|
1346 |
for name in self.element_names: |
|
1347 |
if name in ('add_element', 'added_elements'): |
|
1348 |
continue |
|
1349 |
key_widget = self.get_widget(name + 'key') |
|
1350 |
value_widget = self.get_widget(name + 'value') |
|
1351 |
r += htmltext('<div class="dict-key">%s</div>' |
|
1352 |
'<div class="dict-separator">: </div>' |
|
1353 |
'<div class="dict-value">%s</div>') % ( |
|
1354 |
key_widget.render(), |
|
1355 |
value_widget.render()) |
|
1356 |
r += htmltext('\n') |
|
1357 |
r += self.get_widget('add_element').render() |
|
1358 |
r += self.get_widget('added_elements').render() |
|
1359 |
return r.getvalue() |
|
1360 | ||
1344 | 1361 |
class TagsWidget(StringWidget): |
1345 | 1362 |
def __init__(self, name, value = None, known_tags = None, **kwargs): |
1346 | 1363 |
StringWidget.__init__(self, name, value, **kwargs) |
wcs/qommon/static/css/dc2/admin.css | ||
---|---|---|
1166 | 1166 |
color: white; |
1167 | 1167 |
} |
1168 | 1168 | |
1169 |
div.WidgetDict div.content div.StringWidget { |
|
1170 |
width: 25%; |
|
1169 |
div.WsCallRequestWidget br { display: none; } |
|
1170 | ||
1171 |
div.WidgetDict div.content div.dict-key { width: 20%; } |
|
1172 |
div.WidgetDict div.content div.dict-value { width: 70%; } |
|
1173 | ||
1174 |
div.WidgetDict div.content div.dict-key div, |
|
1175 |
div.WidgetDict div.content div.dict-value div, |
|
1176 |
div.WidgetDict input |
|
1177 |
{ |
|
1178 |
width: 100%; |
|
1171 | 1179 |
} |
1172 | 1180 | |
1173 |
div.WidgetDict div.content div + div.ComputedExpressionWidget, |
|
1174 |
div.WidgetDict div.content div + div.StringWidget { |
|
1175 |
width: 70%; |
|
1176 |
padding-left: 1em; |
|
1181 |
div.WidgetDict div.content div.widget { |
|
1182 |
margin-bottom: 0.5ex; |
|
1177 | 1183 |
} |
1178 | 1184 | |
1179 |
div.WidgetDict div.content div.content, |
|
1180 |
div.WidgetDict div.content div.content input { |
|
1181 |
width: calc(100% - 1em); |
|
1185 |
div.WidgetDict div.dict-separator { |
|
1186 |
padding: 0 1ex; |
|
1182 | 1187 |
} |
1183 | 1188 | |
1184 | 1189 |
div.ComputedExpressionWidget div.content { |
wcs/root.py | ||
---|---|---|
48 | 48 | |
49 | 49 |
from categories import Category |
50 | 50 |
from data_sources import NamedDataSource |
51 |
from wscalls import NamedWsCall |
|
51 | 52 |
from wcs.api import ApiDirectory |
52 | 53 |
from myspace import MyspaceDirectory |
53 | 54 |
from forms.preview import PreviewDirectory |
... | ... | |
282 | 283 |
get_response().set_content_type('text/plain') |
283 | 284 |
return json.dumps(results) |
284 | 285 | |
286 |
def feed_substitution_parts(self): |
|
287 |
get_publisher().substitutions.feed(get_session()) |
|
288 |
get_publisher().substitutions.feed(get_request().user) |
|
289 |
get_publisher().substitutions.feed(NamedDataSource) |
|
290 |
get_publisher().substitutions.feed(NamedWsCall) |
|
291 | ||
285 | 292 |
def _q_traverse(self, path): |
293 |
self.feed_substitution_parts() |
|
294 | ||
286 | 295 |
response = get_response() |
287 | 296 |
if not hasattr(response, 'filter'): |
288 | 297 |
response.filter = {} |
289 | 298 |
if not hasattr(response, 'breadcrumb'): |
290 | 299 |
response.breadcrumb = [ ('', _('Home')) ] |
291 | 300 | |
292 |
get_publisher().substitutions.feed(get_session()) |
|
293 |
get_publisher().substitutions.feed(get_request().user) |
|
294 |
get_publisher().substitutions.feed(NamedDataSource) |
|
295 | ||
296 | 301 |
if not self.admin: |
297 | 302 |
self.admin = get_publisher().admin_directory_class() |
298 | 303 |
wcs/wf/wscall.py | ||
---|---|---|
22 | 22 |
import collections |
23 | 23 |
import mimetypes |
24 | 24 |
from StringIO import StringIO |
25 |
import urllib |
|
26 |
import urlparse |
|
27 | 25 | |
28 | 26 | |
29 | 27 |
from quixote.html import TemplateIO, htmltext |
30 | 28 |
from qommon.errors import ConnectionError |
31 | 29 |
from qommon.form import * |
32 |
from qommon.misc import (http_get_page, http_post_request, get_variadic_url, |
|
33 |
JSONEncoder, json_loads, site_encode) |
|
30 |
from qommon.misc import json_loads, site_encode |
|
34 | 31 |
from wcs.workflows import (WorkflowStatusItem, register_item_class, |
35 | 32 |
AbortActionException, AttachmentEvolutionPart) |
36 |
from wcs.api_utils import sign_url |
|
37 | ||
38 |
TIMEOUT = 30 |
|
33 |
from wcs.wscalls import call_webservice |
|
39 | 34 | |
40 | 35 |
class JournalWsCallErrorPart: #pylint: disable=C1001 |
41 | 36 |
content = None |
... | ... | |
239 | 234 |
if not self.url: |
240 | 235 |
# misconfigured action |
241 | 236 |
return |
242 |
url = self.url |
|
243 |
if '[' in url: |
|
244 |
variables = get_publisher().substitutions.get_context_variables() |
|
245 |
url = get_variadic_url(url, variables) |
|
246 | ||
247 |
if self.qs_data: # merge qs_data into url |
|
248 |
publisher = get_publisher() |
|
249 |
parsed = urlparse.urlparse(url) |
|
250 |
qs = list(urlparse.parse_qsl(parsed.query)) |
|
251 |
for key, value in self.qs_data.iteritems(): |
|
252 |
try: |
|
253 |
value = self.compute(value, raises=True) |
|
254 |
value = str(value) |
|
255 |
except: |
|
256 |
get_publisher().notify_of_exception(sys.exc_info()) |
|
257 |
else: |
|
258 |
key = publisher.sitecharset2utf8(key) |
|
259 |
value = publisher.sitecharset2utf8(value) |
|
260 |
qs.append((key, value)) |
|
261 |
qs = urllib.urlencode(qs) |
|
262 |
url = urlparse.urlunparse(parsed[:4] + (qs,) + parsed[5:6]) |
|
263 | ||
264 |
if self.request_signature_key: |
|
265 |
signature_key = self.compute(self.request_signature_key) |
|
266 |
if signature_key: |
|
267 |
url = sign_url(url, signature_key) |
|
268 | ||
269 |
headers = {'Content-type': 'application/json', |
|
270 |
'Accept': 'application/json'} |
|
271 |
post_data = None # payload |
|
272 | ||
273 |
# if self.post_data exists, post_data is a dict built from it |
|
274 |
if self.method == 'POST' and self.post_data: |
|
275 |
post_data = {} |
|
276 |
for (key, value) in self.post_data.items(): |
|
277 |
try: |
|
278 |
post_data[key] = self.compute(value, raises=True) |
|
279 |
except: |
|
280 |
get_publisher().notify_of_exception(sys.exc_info()) |
|
281 | ||
282 |
# if formdata has to be sent, it's the payload. If post_data exists, |
|
283 |
# it's added in formdata['extra'] |
|
284 |
if self.method == 'POST' and self.post: |
|
285 |
formdata_dict = formdata.get_json_export_dict() |
|
286 |
if post_data is not None: |
|
287 |
formdata_dict['extra'] = post_data |
|
288 |
post_data = formdata_dict |
|
289 | 237 | |
290 | 238 |
try: |
291 |
if self.method == 'POST': |
|
292 |
if post_data: |
|
293 |
post_data = json.dumps(post_data, cls=JSONEncoder, |
|
294 |
encoding=get_publisher().site_charset) |
|
295 |
# increase timeout for huge loads, one second every 65536 |
|
296 |
# bytes, to match a country 512kbps DSL line. |
|
297 |
timeout = TIMEOUT |
|
298 |
timeout += len(post_data) / 65536 |
|
299 |
response, status, data, auth_header = http_post_request( |
|
300 |
url, post_data, headers=headers, timeout=timeout) |
|
301 |
else: |
|
302 |
response, status, data, auth_header = http_get_page( |
|
303 |
url, headers=headers, timeout=TIMEOUT) |
|
239 |
response, status, data = call_webservice( |
|
240 |
url=self.url, |
|
241 |
qs_data=self.qs_data, |
|
242 |
request_signature_key=self.request_signature_key, |
|
243 |
method=self.method, |
|
244 |
post_data=self.post_data, |
|
245 |
post_formdata=self.post, |
|
246 |
formdata=formdata) |
|
304 | 247 |
except ConnectionError as e: |
305 | 248 |
status = 0 |
306 | 249 |
self.action_on_error(self.action_on_network_errors, formdata, |
wcs/workflows.py | ||
---|---|---|
1547 | 1547 |
value = getattr(self, '%s_parse' % f)(value) |
1548 | 1548 |
setattr(self, f, value) |
1549 | 1549 | |
1550 |
def compute(self, var, do_ezt=True, raises=False): |
|
1550 |
@classmethod |
|
1551 |
def compute(cls, var, do_ezt=True, raises=False): |
|
1551 | 1552 |
if not isinstance(var, basestring): |
1552 | 1553 |
return var |
1553 | 1554 |
wcs/wscalls.py | ||
---|---|---|
1 |
# w.c.s. - web application for online forms |
|
2 |
# Copyright (C) 2005-2016 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software; you can redistribute it and/or modify |
|
5 |
# it under the terms of the GNU General Public License as published by |
|
6 |
# the Free Software Foundation; either version 2 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU General Public License |
|
15 |
# along with this program; if not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import collections |
|
18 |
import json |
|
19 |
import sys |
|
20 |
import urllib |
|
21 |
import urlparse |
|
22 |
import xml.etree.ElementTree as ET |
|
23 | ||
24 |
from quixote import get_publisher |
|
25 | ||
26 |
from qommon.misc import (simplify, http_get_page, http_post_request, |
|
27 |
get_variadic_url, JSONEncoder, json_loads) |
|
28 |
from qommon.xml_storage import XmlStorableObject |
|
29 |
from qommon.form import (CompositeWidget, StringWidget, WidgetDict, |
|
30 |
ComputedExpressionWidget, RadiobuttonsWidget, CheckboxWidget) |
|
31 | ||
32 |
from wcs.api_utils import sign_url |
|
33 |
from wcs.workflows import WorkflowStatusItem |
|
34 | ||
35 |
TIMEOUT = 30 |
|
36 | ||
37 |
def call_webservice(url, qs_data=None, request_signature_key=None, |
|
38 |
method=None, post_data=None, post_formdata=None, formdata=None, |
|
39 |
**kwargs): |
|
40 | ||
41 |
if '[' in url: |
|
42 |
variables = get_publisher().substitutions.get_context_variables() |
|
43 |
url = get_variadic_url(url, variables) |
|
44 | ||
45 |
if qs_data: # merge qs_data into url |
|
46 |
publisher = get_publisher() |
|
47 |
parsed = urlparse.urlparse(url) |
|
48 |
qs = list(urlparse.parse_qsl(parsed.query)) |
|
49 |
for key, value in qs_data.iteritems(): |
|
50 |
try: |
|
51 |
value = WorkflowStatusItem.compute(value, raises=True) |
|
52 |
value = str(value) |
|
53 |
except: |
|
54 |
get_publisher().notify_of_exception(sys.exc_info()) |
|
55 |
else: |
|
56 |
key = publisher.sitecharset2utf8(key) |
|
57 |
value = publisher.sitecharset2utf8(value) |
|
58 |
qs.append((key, value)) |
|
59 |
qs = urllib.urlencode(qs) |
|
60 |
url = urlparse.urlunparse(parsed[:4] + (qs,) + parsed[5:6]) |
|
61 | ||
62 |
if request_signature_key: |
|
63 |
signature_key = WorkflowStatusItem.compute(request_signature_key) |
|
64 |
if signature_key: |
|
65 |
url = sign_url(url, signature_key) |
|
66 | ||
67 |
headers = {'Content-type': 'application/json', 'Accept': 'application/json'} |
|
68 |
payload = None |
|
69 | ||
70 |
# if post_data exists, payload is a dict built from it |
|
71 |
if method == 'POST' and post_data: |
|
72 |
payload = {} |
|
73 |
for (key, value) in post_data.items(): |
|
74 |
try: |
|
75 |
payload[key] = WorkflowStatusItem.compute(value, raises=True) |
|
76 |
except: |
|
77 |
get_publisher().notify_of_exception(sys.exc_info()) |
|
78 | ||
79 |
# if formdata has to be sent, it's the payload. If post_data exists, |
|
80 |
# it's added in formdata['extra'] |
|
81 |
if method == 'POST' and post_formdata: |
|
82 |
if formdata: |
|
83 |
formdata_dict = formdata.get_json_export_dict() |
|
84 |
if payload is not None: |
|
85 |
formdata_dict['extra'] = payload |
|
86 |
payload = formdata_dict |
|
87 | ||
88 |
if method == 'POST': |
|
89 |
if payload: |
|
90 |
payload = json.dumps(payload, cls=JSONEncoder, |
|
91 |
encoding=get_publisher().site_charset) |
|
92 |
# increase timeout for huge loads, one second every 65536 |
|
93 |
# bytes, to match a country 512kbps DSL line. |
|
94 |
timeout = TIMEOUT |
|
95 |
timeout += len(payload) / 65536 |
|
96 |
response, status, data, auth_header = http_post_request( |
|
97 |
url, payload, headers=headers, timeout=timeout) |
|
98 |
else: |
|
99 |
response, status, data, auth_header = http_get_page( |
|
100 |
url, headers=headers, timeout=TIMEOUT) |
|
101 |
return (response, status, data) |
|
102 | ||
103 | ||
104 |
class NamedWsCall(XmlStorableObject): |
|
105 |
_names = 'wscalls' |
|
106 |
_xml_tagname = 'wscalls' |
|
107 | ||
108 |
name = None |
|
109 |
slug = None |
|
110 |
description = None |
|
111 |
request = None |
|
112 | ||
113 |
# declarations for serialization |
|
114 |
XML_NODES = [('name', 'str'), ('slug', 'str'), ('description', 'str'), |
|
115 |
('request', 'request'),] |
|
116 | ||
117 |
def __init__(self, name=None): |
|
118 |
XmlStorableObject.__init__(self) |
|
119 |
self.name = name |
|
120 | ||
121 |
@classmethod |
|
122 |
def get_substitution_variables(cls): |
|
123 |
return {'webservices': WsCallsSubstitutionProxy()} |
|
124 | ||
125 |
def export_request_to_xml(self, element, attribute_name, charset): |
|
126 |
request = getattr(self, attribute_name) |
|
127 |
for attr in ('url', 'request_signature_key', 'method'): |
|
128 |
ET.SubElement(element, attr).text = unicode(request.get(attr) or '', charset) |
|
129 |
for attr in ('qs_data', 'post_data'): |
|
130 |
data_element = ET.SubElement(element, attr) |
|
131 |
for k, v in (request.get(attr) or {}).items(): |
|
132 |
sub = ET.SubElement(data_element, 'param') |
|
133 |
sub.attrib['key'] = unicode(k, charset) |
|
134 |
sub.text = unicode(v, charset) |
|
135 |
if request.get('post_formdata'): |
|
136 |
ET.SubElement(element, 'post_formdata') |
|
137 | ||
138 |
def import_request_from_xml(self, element, charset): |
|
139 |
request = {} |
|
140 |
for attr in ('url', 'request_signature_key', 'method'): |
|
141 |
request[attr] = '' |
|
142 |
if element.find(attr) is not None and element.find(attr).text: |
|
143 |
request[attr] = element.find(attr).text.encode(charset) |
|
144 |
for attr in ('qs_data', 'post_data'): |
|
145 |
request[attr] = {} |
|
146 |
data_element = element.find(attr) |
|
147 |
if data_element is None: |
|
148 |
continue |
|
149 |
for param in data_element.findall('param'): |
|
150 |
request[attr][param.attrib['key'].encode(charset)] = param.text.encode(charset) |
|
151 |
request['post_formdata'] = bool(element.find('post_formdata') is not None) |
|
152 |
return request |
|
153 | ||
154 |
def store(self): |
|
155 |
if self.slug is None: |
|
156 |
# set slug if it's not yet there |
|
157 |
self.slug = self.get_new_slug() |
|
158 |
if self.id and self.slug != self.id: |
|
159 |
self.remove_object(self.id) |
|
160 |
self.id = self.slug |
|
161 |
super(NamedWsCall, self).store() |
|
162 | ||
163 |
def get_new_slug(self): |
|
164 |
new_slug = simplify(self.name, space='_') |
|
165 |
base_new_slug = new_slug |
|
166 |
suffix_no = 0 |
|
167 |
while True: |
|
168 |
try: |
|
169 |
obj = self.get(new_slug, ignore_migration=True) |
|
170 |
except KeyError: |
|
171 |
break |
|
172 |
if obj.id == self.id: |
|
173 |
break |
|
174 |
suffix_no += 1 |
|
175 |
new_slug = '%s_%s' % (base_new_slug, suffix_no) |
|
176 |
return new_slug |
|
177 | ||
178 |
@classmethod |
|
179 |
def get_substitution_variables(cls): |
|
180 |
return {'webservice': WsCallsSubstitutionProxy()} |
|
181 | ||
182 |
def call(self): |
|
183 |
(response, status, data) = call_webservice(**self.request) |
|
184 |
return json_loads(data) |
|
185 | ||
186 | ||
187 |
class WsCallsSubstitutionProxy(object): |
|
188 |
def __getattr__(self, attr): |
|
189 |
return NamedWsCall.get(attr).call() |
|
190 | ||
191 | ||
192 |
class WsCallRequestWidget(CompositeWidget): |
|
193 |
def __init__(self, name, value=None, include_post_formdata=False, **kwargs): |
|
194 |
CompositeWidget.__init__(self, name, value, **kwargs) |
|
195 |
self.include_post_formdata = include_post_formdata |
|
196 | ||
197 |
if not value: |
|
198 |
value = {} |
|
199 | ||
200 |
self.add(StringWidget, 'url', title=_('URL'), value=value.get('url'), size=80) |
|
201 |
self.add(ComputedExpressionWidget, 'request_signature_key', |
|
202 |
title=_('Request Signature Key'), |
|
203 |
value=value.get('request_signature_key')) |
|
204 |
self.add(WidgetDict, 'qs_data', |
|
205 |
title=_('Query string data'), |
|
206 |
value=value.get('qs_data') or {}, |
|
207 |
element_value_type=ComputedExpressionWidget) |
|
208 |
methods = collections.OrderedDict( |
|
209 |
[('GET', _('GET')), ('POST', _('POST (JSON)'))]) |
|
210 |
self.add(RadiobuttonsWidget, 'method', |
|
211 |
title=_('Method'), |
|
212 |
options=methods.items(), |
|
213 |
value=value.get('method') or 'GET', |
|
214 |
attrs={'data-dynamic-display-parent': 'true'}) |
|
215 |
method_widget = self.get_widget('method') |
|
216 |
if self.include_post_formdata: |
|
217 |
self.add(CheckboxWidget, 'post_formdata', |
|
218 |
title=_('Post formdata'), |
|
219 |
value=value.get('post_formdata'), |
|
220 |
attrs={ |
|
221 |
'data-dynamic-display-child-of': method_widget.get_name(), |
|
222 |
'data-dynamic-display-value': methods.get('POST'), |
|
223 |
}) |
|
224 |
self.add(WidgetDict, 'post_data', |
|
225 |
title=_('Post data'), |
|
226 |
value=value.get('post_data') or {}, |
|
227 |
element_value_type=ComputedExpressionWidget, |
|
228 |
attrs={ |
|
229 |
'data-dynamic-display-child-of': method_widget.get_name(), |
|
230 |
'data-dynamic-display-value': methods.get('POST'), |
|
231 |
}) |
|
232 | ||
233 |
def _parse(self, request): |
|
234 |
values = {} |
|
235 |
for name in ('url', 'request_signature_key', 'qs_data', 'method', |
|
236 |
'post_formdata', 'post_data'): |
|
237 |
if not self.include_post_formdata and name == 'post_formdata': |
|
238 |
continue |
|
239 |
value = self.get(name) |
|
240 |
if value: |
|
241 |
values[name] = value |
|
242 |
self.value = values or None |
|
0 |
- |