Projet

Général

Profil

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

Frédéric Péters, 21 juin 2016 08:23

Télécharger (35,9 ko)

Voir les différences:

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

 tests/test_admin_pages.py           |  90 ++++++++++++++
 tests/test_wscall.py                |  62 +++++++++
 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 ++++++++++++++++++++++++++++++++++++
 10 files changed, 692 insertions(+), 83 deletions(-)
 create mode 100644 tests/test_wscall.py
 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
......
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
-