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 |
... | ... | |
3032 | 3033 |
resp = resp.forms[0].submit('submit') |
3033 | 3034 |
assert resp.location == 'http://example.net/backoffice/settings/data-sources/1/' |
3034 | 3035 | |
3036 |
def test_wscalls_new(pub): |
|
3037 |
create_superuser(pub) |
|
3038 |
NamedWsCall.wipe() |
|
3039 |
app = login(get_app(pub)) |
|
3040 | ||
3041 |
# go to the page and cancel |
|
3042 |
resp = app.get('/backoffice/settings/wscalls/') |
|
3043 |
resp = resp.click('New webservice call') |
|
3044 |
resp = resp.forms[0].submit('cancel') |
|
3045 |
assert resp.location == 'http://example.net/backoffice/settings/wscalls/' |
|
3046 | ||
3047 |
# go to the page and add a webservice call |
|
3048 |
resp = app.get('/backoffice/settings/wscalls/') |
|
3049 |
resp = resp.click('New webservice call') |
|
3050 |
resp.form['name'] = 'a new webservice call' |
|
3051 |
resp.form['description'] = 'description' |
|
3052 |
resp.form['request$url'] = 'http://remote.example.net/json' |
|
3053 |
resp = resp.form.submit('submit') |
|
3054 |
assert resp.location == 'http://example.net/backoffice/settings/wscalls/' |
|
3055 |
resp = resp.follow() |
|
3056 |
assert 'a new webservice call' in resp.body |
|
3057 |
resp = resp.click('a new webservice call') |
|
3058 |
assert 'Webservice Call - a new webservice call' in resp.body |
|
3059 |
resp = resp.click('Edit') |
|
3060 |
assert 'Edit webservice call' in resp.body |
|
3061 | ||
3062 |
assert NamedWsCall.get('a_new_webservice_call').name == 'a new webservice call' |
|
3063 | ||
3064 |
def test_wscalls_view(pub): |
|
3065 |
create_superuser(pub) |
|
3066 |
NamedWsCall.wipe() |
|
3067 | ||
3068 |
wscall = NamedWsCall(name='xxx') |
|
3069 |
wscall.description = 'description' |
|
3070 |
wscall.request = { |
|
3071 |
'url': 'http://remote.example.net/json', |
|
3072 |
'request_signature_key': 'xxx', |
|
3073 |
'qs_data': {'a': 'b'}, |
|
3074 |
'method': 'POST', |
|
3075 |
'post_data': {'c': 'd'}, |
|
3076 |
} |
|
3077 |
wscall.store() |
|
3078 | ||
3079 |
app = login(get_app(pub)) |
|
3080 |
resp = app.get('/backoffice/settings/wscalls/%s/' % wscall.id) |
|
3081 |
assert 'http://remote.example.net/json' in resp.body |
|
3082 | ||
3083 |
def test_wscalls_edit(pub): |
|
3084 |
test_wscalls_view(pub) |
|
3085 | ||
3086 |
app = login(get_app(pub)) |
|
3087 | ||
3088 |
resp = app.get('/backoffice/settings/wscalls/xxx/') |
|
3089 |
resp = resp.click(href='edit') |
|
3090 |
assert resp.form['name'].value == 'xxx' |
|
3091 |
assert 'slug' in resp.form.fields |
|
3092 |
resp.form['description'] = 'bla bla bla' |
|
3093 |
resp = resp.form.submit('submit') |
|
3094 |
assert resp.location == 'http://example.net/backoffice/settings/wscalls/xxx/' |
|
3095 |
resp = resp.follow() |
|
3096 | ||
3097 |
assert NamedWsCall.get('xxx').description == 'bla bla bla' |
|
3098 | ||
3099 |
resp = app.get('/backoffice/settings/wscalls/xxx/') |
|
3100 |
resp = resp.click(href='edit') |
|
3101 |
assert resp.form['name'].value == 'xxx' |
|
3102 |
assert 'slug' in resp.form.fields |
|
3103 |
resp.form['slug'] = 'yyy' |
|
3104 |
resp = resp.form.submit('submit') |
|
3105 |
assert resp.location == 'http://example.net/backoffice/settings/wscalls/yyy/' |
|
3106 | ||
3107 | ||
3108 |
def test_wscalls_delete(pub): |
|
3109 |
test_wscalls_view(pub) |
|
3110 |
assert NamedWsCall.count() == 1 |
|
3111 | ||
3112 |
app = login(get_app(pub)) |
|
3113 | ||
3114 |
resp = app.get('/backoffice/settings/wscalls/xxx/') |
|
3115 |
resp = resp.click(href='delete') |
|
3116 |
resp = resp.form.submit('cancel') |
|
3117 |
assert resp.location == 'http://example.net/backoffice/settings/wscalls/' |
|
3118 | ||
3119 |
resp = app.get('/backoffice/settings/wscalls/xxx/') |
|
3120 |
resp = resp.click(href='delete') |
|
3121 |
resp = resp.form.submit('submit') |
|
3122 |
assert resp.location == 'http://example.net/backoffice/settings/wscalls/' |
|
3123 |
assert NamedWsCall.count() == 0 |
|
3124 | ||
3035 | 3125 |
def test_settings_permissions(pub): |
3036 | 3126 |
create_superuser(pub) |
3037 | 3127 |
role1 = create_role() |
tests/test_wscall.py | ||
---|---|---|
1 |
import json |
|
2 |
import mock |
|
3 |
import pytest |
|
4 | ||
5 |
from wcs.qommon.http_request import HTTPRequest |
|
6 |
from wcs.wscalls import NamedWsCall |
|
7 | ||
8 |
from utilities import create_temporary_pub, clean_temporary_pub, http_requests |
|
9 | ||
10 | ||
11 |
@pytest.fixture |
|
12 |
def pub(): |
|
13 |
pub = create_temporary_pub() |
|
14 |
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'}) |
|
15 |
pub.set_app_dir(req) |
|
16 |
pub._set_request(req) |
|
17 |
pub.load_site_options() |
|
18 |
return pub |
|
19 | ||
20 |
def teardown_module(module): |
|
21 |
clean_temporary_pub() |
|
22 | ||
23 | ||
24 |
def test_named_wscall(pub): |
|
25 |
# create object |
|
26 |
wscall = NamedWsCall() |
|
27 |
wscall.name = 'Hello' |
|
28 |
wscall.request = {'url': 'http://example.net', 'qs_data': {'a': 'b'}} |
|
29 |
wscall.store() |
|
30 |
assert wscall.slug == 'hello' |
|
31 | ||
32 |
# get object |
|
33 |
wscall = NamedWsCall.get('hello') |
|
34 |
assert wscall.name == 'Hello' |
|
35 |
assert wscall.request.get('url') == 'http://example.net' |
|
36 |
assert wscall.request.get('qs_data') == {'a': 'b'} |
|
37 | ||
38 |
# create with same name, should get a different slug |
|
39 |
wscall = NamedWsCall() |
|
40 |
wscall.name = 'Hello' |
|
41 |
wscall.request = {'url': 'http://example.net'} |
|
42 |
wscall.store() |
|
43 |
assert wscall.slug == 'hello_1' |
|
44 | ||
45 |
# change slug, shoulg get a new id |
|
46 |
wscall.slug = 'bye' |
|
47 |
wscall.store() |
|
48 |
assert 'bye' in NamedWsCall.keys() |
|
49 |
assert not 'hello_1' in NamedWsCall.keys() |
|
50 | ||
51 |
def test_webservice_substitution_variable(pub): |
|
52 |
NamedWsCall.wipe() |
|
53 | ||
54 |
wscall = NamedWsCall() |
|
55 |
wscall.name = 'Hello world' |
|
56 |
wscall.request = {'url': 'http://remote.example.net/json'} |
|
57 |
wscall.store() |
|
58 |
assert wscall.slug == 'hello_world' |
|
59 | ||
60 |
pub.substitutions.feed(NamedWsCall) |
|
61 |
variables = pub.substitutions.get_context_variables() |
|
62 |
assert variables['webservice'].hello_world == {'foo': 'bar'} |
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 | ||
---|---|---|
1342 | 1342 |
if hint: |
1343 | 1343 |
self.hint = hint |
1344 | 1344 | |
1345 |
def render_content(self): |
|
1346 |
r = TemplateIO(html=True) |
|
1347 |
for name in self.element_names: |
|
1348 |
if name in ('add_element', 'added_elements'): |
|
1349 |
continue |
|
1350 |
key_widget = self.get_widget(name + 'key') |
|
1351 |
value_widget = self.get_widget(name + 'value') |
|
1352 |
r += htmltext('<div class="dict-key">%s</div>' |
|
1353 |
'<div class="dict-separator">: </div>' |
|
1354 |
'<div class="dict-value">%s</div>') % ( |
|
1355 |
key_widget.render(), |
|
1356 |
value_widget.render()) |
|
1357 |
r += htmltext('\n') |
|
1358 |
r += self.get_widget('add_element').render() |
|
1359 |
r += self.get_widget('added_elements').render() |
|
1360 |
return r.getvalue() |
|
1361 | ||
1345 | 1362 |
class TagsWidget(StringWidget): |
1346 | 1363 |
def __init__(self, name, value = None, known_tags = None, **kwargs): |
1347 | 1364 |
StringWidget.__init__(self, name, value, **kwargs) |
wcs/qommon/static/css/dc2/admin.css | ||
---|---|---|
1168 | 1168 |
color: white; |
1169 | 1169 |
} |
1170 | 1170 | |
1171 |
div.WidgetDict div.content div.StringWidget { |
|
1172 |
width: 25%; |
|
1171 |
div.WsCallRequestWidget br { display: none; } |
|
1172 | ||
1173 |
div.WidgetDict div.content div.dict-key { width: 20%; } |
|
1174 |
div.WidgetDict div.content div.dict-value { width: 70%; } |
|
1175 | ||
1176 |
div.WidgetDict div.content div.dict-key div, |
|
1177 |
div.WidgetDict div.content div.dict-value div, |
|
1178 |
div.WidgetDict input |
|
1179 |
{ |
|
1180 |
width: 100%; |
|
1173 | 1181 |
} |
1174 | 1182 | |
1175 |
div.WidgetDict div.content div + div.ComputedExpressionWidget, |
|
1176 |
div.WidgetDict div.content div + div.StringWidget { |
|
1177 |
width: 70%; |
|
1178 |
padding-left: 1em; |
|
1183 |
div.WidgetDict div.content div.widget { |
|
1184 |
margin-bottom: 0.5ex; |
|
1179 | 1185 |
} |
1180 | 1186 | |
1181 |
div.WidgetDict div.content div.content, |
|
1182 |
div.WidgetDict div.content div.content input { |
|
1183 |
width: calc(100% - 1em); |
|
1187 |
div.WidgetDict div.dict-separator { |
|
1188 |
padding: 0 1ex; |
|
1184 | 1189 |
} |
1185 | 1190 | |
1186 | 1191 |
div.SetBackofficeFieldsTableWidget table { |
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 |
... | ... | |
281 | 282 |
get_response().set_content_type('text/plain') |
282 | 283 |
return json.dumps(results) |
283 | 284 | |
285 |
def feed_substitution_parts(self): |
|
286 |
get_publisher().substitutions.feed(get_session()) |
|
287 |
get_publisher().substitutions.feed(get_request().user) |
|
288 |
get_publisher().substitutions.feed(NamedDataSource) |
|
289 |
get_publisher().substitutions.feed(NamedWsCall) |
|
290 | ||
284 | 291 |
def _q_traverse(self, path): |
292 |
self.feed_substitution_parts() |
|
293 | ||
285 | 294 |
response = get_response() |
286 | 295 |
if not hasattr(response, 'filter'): |
287 | 296 |
response.filter = {} |
288 | 297 |
if not hasattr(response, 'breadcrumb'): |
289 | 298 |
response.breadcrumb = [ ('', _('Home')) ] |
290 | 299 | |
291 |
get_publisher().substitutions.feed(get_session()) |
|
292 |
get_publisher().substitutions.feed(get_request().user) |
|
293 |
get_publisher().substitutions.feed(NamedDataSource) |
|
294 | ||
295 | 300 |
if not self.admin: |
296 | 301 |
self.admin = get_publisher().admin_directory_class() |
297 | 302 |
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 | ||
---|---|---|
1546 | 1546 |
value = getattr(self, '%s_parse' % f)(value) |
1547 | 1547 |
setattr(self, f, value) |
1548 | 1548 | |
1549 |
def compute(self, var, do_ezt=True, raises=False): |
|
1549 |
@classmethod |
|
1550 |
def compute(cls, var, do_ezt=True, raises=False): |
|
1550 | 1551 |
if not isinstance(var, basestring): |
1551 | 1552 |
return var |
1552 | 1553 |
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 |
- |