web/views/basecontrollers.py
changeset 8128 0a927fe4541b
parent 8116 6510654269a6
child 8162 d5b02af28125
equal deleted inserted replaced
8125:7070250bf50d 8128:0a927fe4541b
    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):