20 """ |
20 """ |
21 |
21 |
22 __docformat__ = "restructuredtext en" |
22 __docformat__ = "restructuredtext en" |
23 _ = unicode |
23 _ = unicode |
24 |
24 |
25 from logilab.common.date import strptime |
25 from warnings import warn |
|
26 |
26 from logilab.common.deprecation import deprecated |
27 from logilab.common.deprecation import deprecated |
27 |
28 |
28 from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError, |
29 from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError, |
29 AuthenticationError, typed_eid) |
30 AuthenticationError, typed_eid) |
30 from cubicweb.utils import UStringIO, json, json_dumps |
31 from cubicweb.utils import json_dumps |
31 from cubicweb.uilib import exc_message |
32 from cubicweb.selectors import (authenticated_user, anonymous_user, |
32 from cubicweb.selectors import authenticated_user, anonymous_user, match_form_params |
33 match_form_params) |
33 from cubicweb.mail import format_mail |
34 from cubicweb.web import Redirect, RemoteCallFailed |
34 from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse, facet |
|
35 from cubicweb.web.controller import Controller |
35 from cubicweb.web.controller import Controller |
36 from cubicweb.web.views import vid_from_rset, formrenderers |
36 from cubicweb.web.views import vid_from_rset |
37 |
37 |
38 |
38 |
|
39 @deprecated('jsonize is deprecated, use AjaxFunction appobjects instead') |
39 def jsonize(func): |
40 def jsonize(func): |
40 """decorator to sets correct content_type and calls `json_dumps` on |
41 """decorator to sets correct content_type and calls `json_dumps` on |
41 results |
42 results |
42 """ |
43 """ |
43 def wrapper(self, *args, **kwargs): |
44 def wrapper(self, *args, **kwargs): |
44 self._cw.set_content_type('application/json') |
45 self._cw.set_content_type('application/json') |
45 return json_dumps(func(self, *args, **kwargs)) |
46 return json_dumps(func(self, *args, **kwargs)) |
46 wrapper.__name__ = func.__name__ |
47 wrapper.__name__ = func.__name__ |
47 return wrapper |
48 return wrapper |
48 |
49 |
|
50 @deprecated('xhtmlize is deprecated, use AjaxFunction appobjects instead') |
49 def xhtmlize(func): |
51 def xhtmlize(func): |
50 """decorator to sets correct content_type and calls `xmlize` on results""" |
52 """decorator to sets correct content_type and calls `xmlize` on results""" |
51 def wrapper(self, *args, **kwargs): |
53 def wrapper(self, *args, **kwargs): |
52 self._cw.set_content_type(self._cw.html_content_type()) |
54 self._cw.set_content_type(self._cw.html_content_type()) |
53 result = func(self, *args, **kwargs) |
55 result = func(self, *args, **kwargs) |
54 return ''.join((self._cw.document_surrounding_div(), result.strip(), |
56 return ''.join((self._cw.document_surrounding_div(), result.strip(), |
55 u'</div>')) |
57 u'</div>')) |
56 wrapper.__name__ = func.__name__ |
58 wrapper.__name__ = func.__name__ |
57 return wrapper |
59 return wrapper |
58 |
60 |
|
61 @deprecated('check_pageid is deprecated, use AjaxFunction appobjects instead') |
59 def check_pageid(func): |
62 def check_pageid(func): |
60 """decorator which checks the given pageid is found in the |
63 """decorator which checks the given pageid is found in the |
61 user's session data |
64 user's session data |
62 """ |
65 """ |
63 def wrapper(self, *args, **kwargs): |
66 def wrapper(self, *args, **kwargs): |
232 return """<script type="text/javascript"> |
235 return """<script type="text/javascript"> |
233 window.parent.handleFormValidationResponse('%s', %s, %s, %s, %s); |
236 window.parent.handleFormValidationResponse('%s', %s, %s, %s, %s); |
234 </script>""" % (domid, callback, errback, jsargs, cbargs) |
237 </script>""" % (domid, callback, errback, jsargs, cbargs) |
235 |
238 |
236 def publish(self, rset=None): |
239 def publish(self, rset=None): |
237 self._cw.json_request = True |
240 self._cw.ajax_request = True |
238 # XXX unclear why we have a separated controller here vs |
241 # XXX unclear why we have a separated controller here vs |
239 # js_validate_form on the json controller |
242 # js_validate_form on the json controller |
240 status, args, entity = _validate_form(self._cw, self._cw.vreg) |
243 status, args, entity = _validate_form(self._cw, self._cw.vreg) |
241 domid = self._cw.form.get('__domid', 'entityForm').encode( |
244 domid = self._cw.form.get('__domid', 'entityForm').encode( |
242 self._cw.encoding) |
245 self._cw.encoding) |
243 return self.response(domid, status, args, entity) |
246 return self.response(domid, status, args, entity) |
244 |
247 |
245 def optional_kwargs(extraargs): |
|
246 if extraargs is None: |
|
247 return {} |
|
248 # we receive unicode keys which is not supported by the **syntax |
|
249 return dict((str(key), value) for key, value in extraargs.iteritems()) |
|
250 |
|
251 |
248 |
252 class JSonController(Controller): |
249 class JSonController(Controller): |
253 __regid__ = 'json' |
250 __regid__ = 'json' |
254 |
251 |
255 def publish(self, rset=None): |
252 def publish(self, rset=None): |
256 """call js_* methods. Expected form keys: |
253 warn('[3.15] JSONController is deprecated, use AjaxController instead', |
257 |
254 DeprecationWarning) |
258 :fname: the method name without the js_ prefix |
255 ajax_controller = self._cw.vreg['controllers'].select('ajax', self._cw, appli=self.appli) |
259 :args: arguments list (json) |
256 return ajax_controller.publish(rset) |
260 |
|
261 note: it's the responsability of js_* methods to set the correct |
|
262 response content type |
|
263 """ |
|
264 self._cw.json_request = True |
|
265 try: |
|
266 fname = self._cw.form['fname'] |
|
267 func = getattr(self, 'js_%s' % fname) |
|
268 except KeyError: |
|
269 raise RemoteCallFailed('no method specified') |
|
270 except AttributeError: |
|
271 raise RemoteCallFailed('no %s method' % fname) |
|
272 # no <arg> attribute means the callback takes no argument |
|
273 args = self._cw.form.get('arg', ()) |
|
274 if not isinstance(args, (list, tuple)): |
|
275 args = (args,) |
|
276 try: |
|
277 args = [json.loads(arg) for arg in args] |
|
278 except ValueError, exc: |
|
279 self.exception('error while decoding json arguments for js_%s: %s (err: %s)', |
|
280 fname, args, exc) |
|
281 raise RemoteCallFailed(exc_message(exc, self._cw.encoding)) |
|
282 try: |
|
283 result = func(*args) |
|
284 except (RemoteCallFailed, DirectResponse): |
|
285 raise |
|
286 except Exception, exc: |
|
287 self.exception('an exception occurred while calling js_%s(%s): %s', |
|
288 fname, args, exc) |
|
289 raise RemoteCallFailed(exc_message(exc, self._cw.encoding)) |
|
290 if result is None: |
|
291 return '' |
|
292 # get unicode on @htmlize methods, encoded string on @jsonize methods |
|
293 elif isinstance(result, unicode): |
|
294 return result.encode(self._cw.encoding) |
|
295 return result |
|
296 |
|
297 def _rebuild_posted_form(self, names, values, action=None): |
|
298 form = {} |
|
299 for name, value in zip(names, values): |
|
300 # remove possible __action_xxx inputs |
|
301 if name.startswith('__action'): |
|
302 if action is None: |
|
303 # strip '__action_' to get the actual action name |
|
304 action = name[9:] |
|
305 continue |
|
306 # form.setdefault(name, []).append(value) |
|
307 if name in form: |
|
308 curvalue = form[name] |
|
309 if isinstance(curvalue, list): |
|
310 curvalue.append(value) |
|
311 else: |
|
312 form[name] = [curvalue, value] |
|
313 else: |
|
314 form[name] = value |
|
315 # simulate click on __action_%s button to help the controller |
|
316 if action: |
|
317 form['__action_%s' % action] = u'whatever' |
|
318 return form |
|
319 |
|
320 def _exec(self, rql, args=None, rocheck=True): |
|
321 """json mode: execute RQL and return resultset as json""" |
|
322 rql = rql.strip() |
|
323 if rql.startswith('rql:'): |
|
324 rql = rql[4:] |
|
325 if rocheck: |
|
326 self._cw.ensure_ro_rql(rql) |
|
327 try: |
|
328 return self._cw.execute(rql, args) |
|
329 except Exception, ex: |
|
330 self.exception("error in _exec(rql=%s): %s", rql, ex) |
|
331 return None |
|
332 return None |
|
333 |
|
334 def _call_view(self, view, paginate=False, **kwargs): |
|
335 divid = self._cw.form.get('divid') |
|
336 # we need to call pagination before with the stream set |
|
337 try: |
|
338 stream = view.set_stream() |
|
339 except AttributeError: |
|
340 stream = UStringIO() |
|
341 kwargs['w'] = stream.write |
|
342 assert not paginate |
|
343 if divid == 'pageContent': |
|
344 # ensure divid isn't reused by the view (e.g. table view) |
|
345 del self._cw.form['divid'] |
|
346 # mimick main template behaviour |
|
347 stream.write(u'<div id="pageContent">') |
|
348 vtitle = self._cw.form.get('vtitle') |
|
349 if vtitle: |
|
350 stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle) |
|
351 paginate = True |
|
352 nav_html = UStringIO() |
|
353 if paginate and not view.handle_pagination: |
|
354 view.paginate(w=nav_html.write) |
|
355 stream.write(nav_html.getvalue()) |
|
356 if divid == 'pageContent': |
|
357 stream.write(u'<div id="contentmain">') |
|
358 view.render(**kwargs) |
|
359 extresources = self._cw.html_headers.getvalue(skiphead=True) |
|
360 if extresources: |
|
361 stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget ? |
|
362 stream.write(extresources) |
|
363 stream.write(u'</div>\n') |
|
364 if divid == 'pageContent': |
|
365 stream.write(u'</div>%s</div>' % nav_html.getvalue()) |
|
366 return stream.getvalue() |
|
367 |
|
368 @xhtmlize |
|
369 def js_view(self): |
|
370 # XXX try to use the page-content template |
|
371 req = self._cw |
|
372 rql = req.form.get('rql') |
|
373 if rql: |
|
374 rset = self._exec(rql) |
|
375 elif 'eid' in req.form: |
|
376 rset = self._cw.eid_rset(req.form['eid']) |
|
377 else: |
|
378 rset = None |
|
379 vid = req.form.get('vid') or vid_from_rset(req, rset, self._cw.vreg.schema) |
|
380 try: |
|
381 view = self._cw.vreg['views'].select(vid, req, rset=rset) |
|
382 except NoSelectableObject: |
|
383 vid = req.form.get('fallbackvid', 'noresult') |
|
384 view = self._cw.vreg['views'].select(vid, req, rset=rset) |
|
385 self.validate_cache(view) |
|
386 return self._call_view(view, paginate=req.form.pop('paginate', False)) |
|
387 |
|
388 @xhtmlize |
|
389 def js_prop_widget(self, propkey, varname, tabindex=None): |
|
390 """specific method for CWProperty handling""" |
|
391 entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw) |
|
392 entity.eid = varname |
|
393 entity['pkey'] = propkey |
|
394 form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity) |
|
395 form.build_context() |
|
396 vfield = form.field_by_name('value') |
|
397 renderer = formrenderers.FormRenderer(self._cw) |
|
398 return vfield.render(form, renderer, tabindex=tabindex) \ |
|
399 + renderer.render_help(form, vfield) |
|
400 |
|
401 @xhtmlize |
|
402 def js_component(self, compid, rql, registry='components', extraargs=None): |
|
403 if rql: |
|
404 rset = self._exec(rql) |
|
405 else: |
|
406 rset = None |
|
407 # XXX while it sounds good, addition of the try/except below cause pb: |
|
408 # when filtering using facets return an empty rset, the edition box |
|
409 # isn't anymore selectable, as expected. The pb is that with the |
|
410 # try/except below, we see a "an error occurred" message in the ui, while |
|
411 # we don't see it without it. Proper fix would probably be to deal with |
|
412 # this by allowing facet handling code to tell to js_component that such |
|
413 # error is expected and should'nt be reported. |
|
414 #try: |
|
415 comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset, |
|
416 **optional_kwargs(extraargs)) |
|
417 #except NoSelectableObject: |
|
418 # raise RemoteCallFailed('unselectable') |
|
419 return self._call_view(comp, **optional_kwargs(extraargs)) |
|
420 |
|
421 @xhtmlize |
|
422 def js_render(self, registry, oid, eid=None, |
|
423 selectargs=None, renderargs=None): |
|
424 if eid is not None: |
|
425 rset = self._cw.eid_rset(eid) |
|
426 # XXX set row=0 |
|
427 elif self._cw.form.get('rql'): |
|
428 rset = self._cw.execute(self._cw.form['rql']) |
|
429 else: |
|
430 rset = None |
|
431 view = self._cw.vreg[registry].select(oid, self._cw, rset=rset, |
|
432 **optional_kwargs(selectargs)) |
|
433 return self._call_view(view, **optional_kwargs(renderargs)) |
|
434 |
|
435 @check_pageid |
|
436 @xhtmlize |
|
437 def js_inline_creation_form(self, peid, petype, ttype, rtype, role, i18nctx): |
|
438 view = self._cw.vreg['views'].select('inline-creation', self._cw, |
|
439 etype=ttype, rtype=rtype, role=role, |
|
440 peid=peid, petype=petype) |
|
441 return self._call_view(view, i18nctx=i18nctx) |
|
442 |
|
443 @jsonize |
|
444 def js_validate_form(self, action, names, values): |
|
445 return self.validate_form(action, names, values) |
|
446 |
|
447 def validate_form(self, action, names, values): |
|
448 self._cw.form = self._rebuild_posted_form(names, values, action) |
|
449 return _validate_form(self._cw, self._cw.vreg) |
|
450 |
|
451 @xhtmlize |
|
452 def js_reledit_form(self): |
|
453 req = self._cw |
|
454 args = dict((x, req.form[x]) |
|
455 for x in ('formid', 'rtype', 'role', 'reload', 'action')) |
|
456 rset = req.eid_rset(typed_eid(self._cw.form['eid'])) |
|
457 try: |
|
458 args['reload'] = json.loads(args['reload']) |
|
459 except ValueError: # not true/false, an absolute url |
|
460 assert args['reload'].startswith('http') |
|
461 view = req.vreg['views'].select('reledit', req, rset=rset, rtype=args['rtype']) |
|
462 return self._call_view(view, **args) |
|
463 |
|
464 @jsonize |
|
465 def js_i18n(self, msgids): |
|
466 """returns the translation of `msgid`""" |
|
467 return [self._cw._(msgid) for msgid in msgids] |
|
468 |
|
469 @jsonize |
|
470 def js_format_date(self, strdate): |
|
471 """returns the formatted date for `msgid`""" |
|
472 date = strptime(strdate, '%Y-%m-%d %H:%M:%S') |
|
473 return self._cw.format_date(date) |
|
474 |
|
475 @jsonize |
|
476 def js_external_resource(self, resource): |
|
477 """returns the URL of the external resource named `resource`""" |
|
478 return self._cw.uiprops[resource] |
|
479 |
|
480 @check_pageid |
|
481 @jsonize |
|
482 def js_user_callback(self, cbname): |
|
483 page_data = self._cw.session.data.get(self._cw.pageid, {}) |
|
484 try: |
|
485 cb = page_data[cbname] |
|
486 except KeyError: |
|
487 return None |
|
488 return cb(self._cw) |
|
489 |
|
490 @jsonize |
|
491 def js_filter_build_rql(self, names, values): |
|
492 form = self._rebuild_posted_form(names, values) |
|
493 self._cw.form = form |
|
494 builder = facet.FilterRQLBuilder(self._cw) |
|
495 return builder.build_rql() |
|
496 |
|
497 @jsonize |
|
498 def js_filter_select_content(self, facetids, rql, mainvar): |
|
499 # Union unsupported yet |
|
500 select = self._cw.vreg.parse(self._cw, rql).children[0] |
|
501 filtered_variable = facet.get_filtered_variable(select, mainvar) |
|
502 facet.prepare_select(select, filtered_variable) |
|
503 update_map = {} |
|
504 for fid in facetids: |
|
505 fobj = facet.get_facet(self._cw, fid, select, filtered_variable) |
|
506 update_map[fid] = fobj.possible_values() |
|
507 return update_map |
|
508 |
|
509 def js_unregister_user_callback(self, cbname): |
|
510 self._cw.unregister_callback(self._cw.pageid, cbname) |
|
511 |
|
512 def js_unload_page_data(self): |
|
513 self._cw.session.data.pop(self._cw.pageid, None) |
|
514 |
|
515 def js_cancel_edition(self, errorurl): |
|
516 """cancelling edition from javascript |
|
517 |
|
518 We need to clear associated req's data : |
|
519 - errorurl |
|
520 - pending insertions / deletions |
|
521 """ |
|
522 self._cw.cancel_edition(errorurl) |
|
523 |
|
524 def js_delete_bookmark(self, beid): |
|
525 rql = 'DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s' |
|
526 self._cw.execute(rql, {'b': typed_eid(beid), 'u' : self._cw.user.eid}) |
|
527 |
|
528 def js_node_clicked(self, treeid, nodeeid): |
|
529 """add/remove eid in treestate cookie""" |
|
530 from cubicweb.web.views.treeview import treecookiename |
|
531 cookies = self._cw.get_cookie() |
|
532 statename = treecookiename(treeid) |
|
533 treestate = cookies.get(statename) |
|
534 if treestate is None: |
|
535 self._cw.set_cookie(statename, nodeeid) |
|
536 else: |
|
537 marked = set(filter(None, treestate.value.split(':'))) |
|
538 if nodeeid in marked: |
|
539 marked.remove(nodeeid) |
|
540 else: |
|
541 marked.add(nodeeid) |
|
542 self._cw.set_cookie(statename, ':'.join(marked)) |
|
543 |
|
544 @jsonize |
|
545 @deprecated("[3.13] use jQuery.cookie(cookiename, cookievalue, {path: '/'}) in js land instead") |
|
546 def js_set_cookie(self, cookiename, cookievalue): |
|
547 cookiename, cookievalue = str(cookiename), str(cookievalue) |
|
548 self._cw.set_cookie(cookiename, cookievalue) |
|
549 |
|
550 # relations edition stuff ################################################## |
|
551 |
|
552 def _add_pending(self, eidfrom, rel, eidto, kind): |
|
553 key = 'pending_%s' % kind |
|
554 pendings = self._cw.session.data.setdefault(key, set()) |
|
555 pendings.add( (typed_eid(eidfrom), rel, typed_eid(eidto)) ) |
|
556 |
|
557 def _remove_pending(self, eidfrom, rel, eidto, kind): |
|
558 key = 'pending_%s' % kind |
|
559 pendings = self._cw.session.data[key] |
|
560 pendings.remove( (typed_eid(eidfrom), rel, typed_eid(eidto)) ) |
|
561 |
|
562 def js_remove_pending_insert(self, (eidfrom, rel, eidto)): |
|
563 self._remove_pending(eidfrom, rel, eidto, 'insert') |
|
564 |
|
565 def js_add_pending_inserts(self, tripletlist): |
|
566 for eidfrom, rel, eidto in tripletlist: |
|
567 self._add_pending(eidfrom, rel, eidto, 'insert') |
|
568 |
|
569 def js_remove_pending_delete(self, (eidfrom, rel, eidto)): |
|
570 self._remove_pending(eidfrom, rel, eidto, 'delete') |
|
571 |
|
572 def js_add_pending_delete(self, (eidfrom, rel, eidto)): |
|
573 self._add_pending(eidfrom, rel, eidto, 'delete') |
|
574 |
257 |
575 |
258 |
576 # XXX move to massmailing |
259 # XXX move to massmailing |
577 |
|
578 class MailBugReportController(Controller): |
260 class MailBugReportController(Controller): |
579 __regid__ = 'reportbug' |
261 __regid__ = 'reportbug' |
580 __select__ = match_form_params('description') |
262 __select__ = match_form_params('description') |
581 |
263 |
582 def publish(self, rset=None): |
264 def publish(self, rset=None): |