1 # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
16 # You should have received a copy of the GNU Lesser General Public License along |
|
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
|
18 # |
|
19 # (disable pylint msg for client obj access to protected member as in obj._cw) |
|
20 # pylint: disable=W0212 |
|
21 """The ``ajaxcontroller`` module defines the :class:`AjaxController` |
|
22 controller and the ``ajax-func`` cubicweb registry. |
|
23 |
|
24 .. autoclass:: cubicweb.web.views.ajaxcontroller.AjaxController |
|
25 :members: |
|
26 |
|
27 ``ajax-funcs`` registry hosts exposed remote functions, that is |
|
28 functions that can be called from the javascript world. |
|
29 |
|
30 To register a new remote function, either decorate your function |
|
31 with the :func:`~cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator: |
|
32 |
|
33 .. sourcecode:: python |
|
34 |
|
35 from cubicweb.predicates import mactch_user_groups |
|
36 from cubicweb.web.views.ajaxcontroller import ajaxfunc |
|
37 |
|
38 @ajaxfunc(output_type='json', selector=match_user_groups('managers')) |
|
39 def list_users(self): |
|
40 return [u for (u,) in self._cw.execute('Any L WHERE U login L')] |
|
41 |
|
42 or inherit from :class:`~cubicweb.web.views.ajaxcontroller.AjaxFunction` and |
|
43 implement the ``__call__`` method: |
|
44 |
|
45 .. sourcecode:: python |
|
46 |
|
47 from cubicweb.web.views.ajaxcontroller import AjaxFunction |
|
48 class ListUser(AjaxFunction): |
|
49 __regid__ = 'list_users' # __regid__ is the name of the exposed function |
|
50 __select__ = match_user_groups('managers') |
|
51 output_type = 'json' |
|
52 |
|
53 def __call__(self): |
|
54 return [u for (u, ) in self._cw.execute('Any L WHERE U login L')] |
|
55 |
|
56 |
|
57 .. autoclass:: cubicweb.web.views.ajaxcontroller.AjaxFunction |
|
58 :members: |
|
59 |
|
60 .. autofunction:: cubicweb.web.views.ajaxcontroller.ajaxfunc |
|
61 |
|
62 """ |
|
63 |
|
64 __docformat__ = "restructuredtext en" |
|
65 |
|
66 from warnings import warn |
|
67 from functools import partial |
|
68 |
|
69 from six import PY2, text_type |
|
70 |
|
71 from logilab.common.date import strptime |
|
72 from logilab.common.registry import yes |
|
73 from logilab.common.deprecation import deprecated |
|
74 |
|
75 from cubicweb import ObjectNotFound, NoSelectableObject |
|
76 from cubicweb.appobject import AppObject |
|
77 from cubicweb.utils import json, json_dumps, UStringIO |
|
78 from cubicweb.uilib import exc_message |
|
79 from cubicweb.web import RemoteCallFailed, DirectResponse |
|
80 from cubicweb.web.controller import Controller |
|
81 from cubicweb.web.views import vid_from_rset |
|
82 from cubicweb.web.views import basecontrollers |
|
83 |
|
84 |
|
85 def optional_kwargs(extraargs): |
|
86 if extraargs is None: |
|
87 return {} |
|
88 # we receive unicode keys which is not supported by the **syntax |
|
89 return dict((str(key), value) for key, value in extraargs.items()) |
|
90 |
|
91 |
|
92 class AjaxController(Controller): |
|
93 """AjaxController handles ajax remote calls from javascript |
|
94 |
|
95 The following javascript function call: |
|
96 |
|
97 .. sourcecode:: javascript |
|
98 |
|
99 var d = asyncRemoteExec('foo', 12, "hello"); |
|
100 d.addCallback(function(result) { |
|
101 alert('server response is: ' + result); |
|
102 }); |
|
103 |
|
104 will generate an ajax HTTP GET on the following url:: |
|
105 |
|
106 BASE_URL/ajax?fname=foo&arg=12&arg="hello" |
|
107 |
|
108 The AjaxController controller will therefore be selected to handle those URLs |
|
109 and will itself select the :class:`cubicweb.web.views.ajaxcontroller.AjaxFunction` |
|
110 matching the *fname* parameter. |
|
111 """ |
|
112 __regid__ = 'ajax' |
|
113 |
|
114 def publish(self, rset=None): |
|
115 self._cw.ajax_request = True |
|
116 try: |
|
117 fname = self._cw.form['fname'] |
|
118 except KeyError: |
|
119 raise RemoteCallFailed('no method specified') |
|
120 # 1/ check first for old-style (JSonController) ajax func for bw compat |
|
121 try: |
|
122 func = getattr(basecontrollers.JSonController, 'js_%s' % fname) |
|
123 if PY2: |
|
124 func = func.__func__ |
|
125 func = partial(func, self) |
|
126 except AttributeError: |
|
127 # 2/ check for new-style (AjaxController) ajax func |
|
128 try: |
|
129 func = self._cw.vreg['ajax-func'].select(fname, self._cw) |
|
130 except ObjectNotFound: |
|
131 raise RemoteCallFailed('no %s method' % fname) |
|
132 else: |
|
133 warn('[3.15] remote function %s found on JSonController, ' |
|
134 'use AjaxFunction / @ajaxfunc instead' % fname, |
|
135 DeprecationWarning, stacklevel=2) |
|
136 # no <arg> attribute means the callback takes no argument |
|
137 args = self._cw.form.get('arg', ()) |
|
138 if not isinstance(args, (list, tuple)): |
|
139 args = (args,) |
|
140 try: |
|
141 args = [json.loads(arg) for arg in args] |
|
142 except ValueError as exc: |
|
143 self.exception('error while decoding json arguments for ' |
|
144 'js_%s: %s (err: %s)', fname, args, exc) |
|
145 raise RemoteCallFailed(exc_message(exc, self._cw.encoding)) |
|
146 try: |
|
147 result = func(*args) |
|
148 except (RemoteCallFailed, DirectResponse): |
|
149 raise |
|
150 except Exception as exc: |
|
151 self.exception('an exception occurred while calling js_%s(%s): %s', |
|
152 fname, args, exc) |
|
153 raise RemoteCallFailed(exc_message(exc, self._cw.encoding)) |
|
154 if result is None: |
|
155 return '' |
|
156 # get unicode on @htmlize methods, encoded string on @jsonize methods |
|
157 elif isinstance(result, text_type): |
|
158 return result.encode(self._cw.encoding) |
|
159 return result |
|
160 |
|
161 class AjaxFunction(AppObject): |
|
162 """ |
|
163 Attributes on this base class are: |
|
164 |
|
165 :attr: `check_pageid`: make sure the pageid received is valid before proceeding |
|
166 :attr: `output_type`: |
|
167 |
|
168 - *None*: no processing, no change on content-type |
|
169 |
|
170 - *json*: serialize with `json_dumps` and set *application/json* |
|
171 content-type |
|
172 |
|
173 - *xhtml*: wrap result in an XML node and forces HTML / XHTML |
|
174 content-type (use ``_cw.html_content_type()``) |
|
175 |
|
176 """ |
|
177 __registry__ = 'ajax-func' |
|
178 __select__ = yes() |
|
179 __abstract__ = True |
|
180 |
|
181 check_pageid = False |
|
182 output_type = None |
|
183 |
|
184 @staticmethod |
|
185 def _rebuild_posted_form(names, values, action=None): |
|
186 form = {} |
|
187 for name, value in zip(names, values): |
|
188 # remove possible __action_xxx inputs |
|
189 if name.startswith('__action'): |
|
190 if action is None: |
|
191 # strip '__action_' to get the actual action name |
|
192 action = name[9:] |
|
193 continue |
|
194 # form.setdefault(name, []).append(value) |
|
195 if name in form: |
|
196 curvalue = form[name] |
|
197 if isinstance(curvalue, list): |
|
198 curvalue.append(value) |
|
199 else: |
|
200 form[name] = [curvalue, value] |
|
201 else: |
|
202 form[name] = value |
|
203 # simulate click on __action_%s button to help the controller |
|
204 if action: |
|
205 form['__action_%s' % action] = u'whatever' |
|
206 return form |
|
207 |
|
208 def validate_form(self, action, names, values): |
|
209 self._cw.form = self._rebuild_posted_form(names, values, action) |
|
210 return basecontrollers._validate_form(self._cw, self._cw.vreg) |
|
211 |
|
212 def _exec(self, rql, args=None, rocheck=True): |
|
213 """json mode: execute RQL and return resultset as json""" |
|
214 rql = rql.strip() |
|
215 if rql.startswith('rql:'): |
|
216 rql = rql[4:] |
|
217 if rocheck: |
|
218 self._cw.ensure_ro_rql(rql) |
|
219 try: |
|
220 return self._cw.execute(rql, args) |
|
221 except Exception as ex: |
|
222 self.exception("error in _exec(rql=%s): %s", rql, ex) |
|
223 return None |
|
224 return None |
|
225 |
|
226 def _call_view(self, view, paginate=False, **kwargs): |
|
227 divid = self._cw.form.get('divid') |
|
228 # we need to call pagination before with the stream set |
|
229 try: |
|
230 stream = view.set_stream() |
|
231 except AttributeError: |
|
232 stream = UStringIO() |
|
233 kwargs['w'] = stream.write |
|
234 assert not paginate |
|
235 if divid == 'pageContent': |
|
236 # ensure divid isn't reused by the view (e.g. table view) |
|
237 del self._cw.form['divid'] |
|
238 # mimick main template behaviour |
|
239 stream.write(u'<div id="pageContent">') |
|
240 vtitle = self._cw.form.get('vtitle') |
|
241 if vtitle: |
|
242 stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle) |
|
243 paginate = True |
|
244 nav_html = UStringIO() |
|
245 if paginate and not view.handle_pagination: |
|
246 view.paginate(w=nav_html.write) |
|
247 stream.write(nav_html.getvalue()) |
|
248 if divid == 'pageContent': |
|
249 stream.write(u'<div id="contentmain">') |
|
250 view.render(**kwargs) |
|
251 extresources = self._cw.html_headers.getvalue(skiphead=True) |
|
252 if extresources: |
|
253 stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget? |
|
254 stream.write(extresources) |
|
255 stream.write(u'</div>\n') |
|
256 if divid == 'pageContent': |
|
257 stream.write(u'</div>%s</div>' % nav_html.getvalue()) |
|
258 return stream.getvalue() |
|
259 |
|
260 |
|
261 def _ajaxfunc_factory(implementation, selector=yes(), _output_type=None, |
|
262 _check_pageid=False, regid=None): |
|
263 """converts a standard python function into an AjaxFunction appobject""" |
|
264 class AnAjaxFunc(AjaxFunction): |
|
265 __regid__ = regid or implementation.__name__ |
|
266 __select__ = selector |
|
267 output_type = _output_type |
|
268 check_pageid = _check_pageid |
|
269 |
|
270 def serialize(self, content): |
|
271 if self.output_type is None: |
|
272 return content |
|
273 elif self.output_type == 'xhtml': |
|
274 self._cw.set_content_type(self._cw.html_content_type()) |
|
275 return ''.join((u'<div>', |
|
276 content.strip(), u'</div>')) |
|
277 elif self.output_type == 'json': |
|
278 self._cw.set_content_type('application/json') |
|
279 return json_dumps(content) |
|
280 raise RemoteCallFailed('no serializer found for output type %s' |
|
281 % self.output_type) |
|
282 |
|
283 def __call__(self, *args, **kwargs): |
|
284 if self.check_pageid: |
|
285 data = self._cw.session.data.get(self._cw.pageid) |
|
286 if data is None: |
|
287 raise RemoteCallFailed(self._cw._('pageid-not-found')) |
|
288 return self.serialize(implementation(self, *args, **kwargs)) |
|
289 |
|
290 AnAjaxFunc.__name__ = implementation.__name__ |
|
291 # make sure __module__ refers to the original module otherwise |
|
292 # vreg.register(obj) will ignore ``obj``. |
|
293 AnAjaxFunc.__module__ = implementation.__module__ |
|
294 # relate the ``implementation`` object to its wrapper appobject |
|
295 # will be used by e.g.: |
|
296 # import base_module |
|
297 # @ajaxfunc |
|
298 # def foo(self): |
|
299 # return 42 |
|
300 # assert foo(object) == 42 |
|
301 # vreg.register_and_replace(foo, base_module.older_foo) |
|
302 implementation.__appobject__ = AnAjaxFunc |
|
303 return implementation |
|
304 |
|
305 |
|
306 def ajaxfunc(implementation=None, selector=yes(), output_type=None, |
|
307 check_pageid=False, regid=None): |
|
308 """promote a standard function to an ``AjaxFunction`` appobject. |
|
309 |
|
310 All parameters are optional: |
|
311 |
|
312 :param selector: a custom selector object if needed, default is ``yes()`` |
|
313 |
|
314 :param output_type: either None, 'json' or 'xhtml' to customize output |
|
315 content-type. Default is None |
|
316 |
|
317 :param check_pageid: whether the function requires a valid `pageid` or not |
|
318 to proceed. Default is False. |
|
319 |
|
320 :param regid: a custom __regid__ for the created ``AjaxFunction`` object. Default |
|
321 is to keep the wrapped function name. |
|
322 |
|
323 ``ajaxfunc`` can be used both as a standalone decorator: |
|
324 |
|
325 .. sourcecode:: python |
|
326 |
|
327 @ajaxfunc |
|
328 def my_function(self): |
|
329 return 42 |
|
330 |
|
331 or as a parametrizable decorator: |
|
332 |
|
333 .. sourcecode:: python |
|
334 |
|
335 @ajaxfunc(output_type='json') |
|
336 def my_function(self): |
|
337 return 42 |
|
338 |
|
339 """ |
|
340 # if used as a parametrized decorator (e.g. @ajaxfunc(output_type='json')) |
|
341 if implementation is None: |
|
342 def _decorator(func): |
|
343 return _ajaxfunc_factory(func, selector=selector, |
|
344 _output_type=output_type, |
|
345 _check_pageid=check_pageid, |
|
346 regid=regid) |
|
347 return _decorator |
|
348 # else, used as a standalone decorator (i.e. @ajaxfunc) |
|
349 return _ajaxfunc_factory(implementation, selector=selector, |
|
350 _output_type=output_type, |
|
351 _check_pageid=check_pageid, regid=regid) |
|
352 |
|
353 |
|
354 |
|
355 ############################################################################### |
|
356 # Cubicweb remote functions for : # |
|
357 # - appobject rendering # |
|
358 # - user / page session data management # |
|
359 ############################################################################### |
|
360 @ajaxfunc(output_type='xhtml') |
|
361 def view(self): |
|
362 # XXX try to use the page-content template |
|
363 req = self._cw |
|
364 rql = req.form.get('rql') |
|
365 if rql: |
|
366 rset = self._exec(rql) |
|
367 elif 'eid' in req.form: |
|
368 rset = self._cw.eid_rset(req.form['eid']) |
|
369 else: |
|
370 rset = None |
|
371 vid = req.form.get('vid') or vid_from_rset(req, rset, self._cw.vreg.schema) |
|
372 try: |
|
373 viewobj = self._cw.vreg['views'].select(vid, req, rset=rset) |
|
374 except NoSelectableObject: |
|
375 vid = req.form.get('fallbackvid', 'noresult') |
|
376 viewobj = self._cw.vreg['views'].select(vid, req, rset=rset) |
|
377 viewobj.set_http_cache_headers() |
|
378 if req.is_client_cache_valid(): |
|
379 return '' |
|
380 return self._call_view(viewobj, paginate=req.form.pop('paginate', False)) |
|
381 |
|
382 |
|
383 @ajaxfunc(output_type='xhtml') |
|
384 def component(self, compid, rql, registry='components', extraargs=None): |
|
385 if rql: |
|
386 rset = self._exec(rql) |
|
387 else: |
|
388 rset = None |
|
389 # XXX while it sounds good, addition of the try/except below cause pb: |
|
390 # when filtering using facets return an empty rset, the edition box |
|
391 # isn't anymore selectable, as expected. The pb is that with the |
|
392 # try/except below, we see a "an error occurred" message in the ui, while |
|
393 # we don't see it without it. Proper fix would probably be to deal with |
|
394 # this by allowing facet handling code to tell to js_component that such |
|
395 # error is expected and should'nt be reported. |
|
396 #try: |
|
397 comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset, |
|
398 **optional_kwargs(extraargs)) |
|
399 #except NoSelectableObject: |
|
400 # raise RemoteCallFailed('unselectable') |
|
401 return self._call_view(comp, **optional_kwargs(extraargs)) |
|
402 |
|
403 @ajaxfunc(output_type='xhtml') |
|
404 def render(self, registry, oid, eid=None, |
|
405 selectargs=None, renderargs=None): |
|
406 if eid is not None: |
|
407 rset = self._cw.eid_rset(eid) |
|
408 # XXX set row=0 |
|
409 elif self._cw.form.get('rql'): |
|
410 rset = self._cw.execute(self._cw.form['rql']) |
|
411 else: |
|
412 rset = None |
|
413 viewobj = self._cw.vreg[registry].select(oid, self._cw, rset=rset, |
|
414 **optional_kwargs(selectargs)) |
|
415 return self._call_view(viewobj, **optional_kwargs(renderargs)) |
|
416 |
|
417 |
|
418 @ajaxfunc(output_type='json') |
|
419 def i18n(self, msgids): |
|
420 """returns the translation of `msgid`""" |
|
421 return [self._cw._(msgid) for msgid in msgids] |
|
422 |
|
423 @ajaxfunc(output_type='json') |
|
424 def format_date(self, strdate): |
|
425 """returns the formatted date for `msgid`""" |
|
426 date = strptime(strdate, '%Y-%m-%d %H:%M:%S') |
|
427 return self._cw.format_date(date) |
|
428 |
|
429 @ajaxfunc(output_type='json') |
|
430 def external_resource(self, resource): |
|
431 """returns the URL of the external resource named `resource`""" |
|
432 return self._cw.uiprops[resource] |
|
433 |
|
434 @ajaxfunc |
|
435 def unload_page_data(self): |
|
436 """remove user's session data associated to current pageid""" |
|
437 self._cw.session.data.pop(self._cw.pageid, None) |
|
438 |
|
439 @ajaxfunc(output_type='json') |
|
440 @deprecated("[3.13] use jQuery.cookie(cookiename, cookievalue, {path: '/'}) in js land instead") |
|
441 def set_cookie(self, cookiename, cookievalue): |
|
442 """generates the Set-Cookie HTTP reponse header corresponding |
|
443 to `cookiename` / `cookievalue`. |
|
444 """ |
|
445 cookiename, cookievalue = str(cookiename), str(cookievalue) |
|
446 self._cw.set_cookie(cookiename, cookievalue) |
|
447 |
|
448 |
|
449 |
|
450 @ajaxfunc |
|
451 def delete_relation(self, rtype, subjeid, objeid): |
|
452 rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % rtype |
|
453 self._cw.execute(rql, {'s': subjeid, 'o': objeid}) |
|
454 |
|
455 @ajaxfunc |
|
456 def add_relation(self, rtype, subjeid, objeid): |
|
457 rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % rtype |
|
458 self._cw.execute(rql, {'s': subjeid, 'o': objeid}) |
|