Projet

Général

Profil

0001-general-add-new-catalog-of-webservice-calls-usable-i.patch

Frédéric Péters, 17 juin 2016 14:36

Télécharger (33,9 ko)

Voir les différences:

Subject: [PATCH] general: add new catalog of webservice calls, usable in
 expressions (#11376)

 tests/test_admin_pages.py           |  90 ++++++++++++++
 wcs/admin/settings.py               |   7 +-
 wcs/admin/wscalls.py                | 241 +++++++++++++++++++++++++++++++++++
 wcs/qommon/form.py                  |  17 +++
 wcs/qommon/static/css/dc2/admin.css |  23 ++--
 wcs/root.py                         |  13 +-
 wcs/wf/wscall.py                    |  77 ++----------
 wcs/workflows.py                    |   3 +-
 wcs/wscalls.py                      | 242 ++++++++++++++++++++++++++++++++++++
 9 files changed, 630 insertions(+), 83 deletions(-)
 create mode 100644 wcs/admin/wscalls.py
 create mode 100644 wcs/wscalls.py
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
-