--- a/__pkginfo__.py Thu Dec 22 17:44:50 2011 +0100
+++ b/__pkginfo__.py Thu Dec 22 17:46:55 2011 +0100
@@ -52,7 +52,7 @@
'Twisted': '',
# XXX graphviz
# server dependencies
- 'logilab-database': '>= 1.8.1',
+ 'logilab-database': '>= 1.8.2',
'pysqlite': '>= 2.5.5', # XXX install pysqlite2
}
--- a/debian/control Thu Dec 22 17:44:50 2011 +0100
+++ b/debian/control Thu Dec 22 17:46:55 2011 +0100
@@ -35,7 +35,7 @@
Conflicts: cubicweb-multisources
Replaces: cubicweb-multisources
Provides: cubicweb-multisources
-Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.8.1), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.8.2), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
Recommends: pyro (<< 4.0.0), cubicweb-documentation (= ${source:Version})
Description: server part of the CubicWeb framework
CubicWeb is a semantic web application framework.
--- a/devtools/testlib.py Thu Dec 22 17:44:50 2011 +0100
+++ b/devtools/testlib.py Thu Dec 22 17:46:55 2011 +0100
@@ -605,7 +605,7 @@
dump = json.dumps
args = [dump(arg) for arg in args]
req = self.request(fname=fname, pageid='123', arg=args)
- ctrl = self.vreg['controllers'].select('json', req)
+ ctrl = self.vreg['controllers'].select('ajax', req)
return ctrl.publish(), req
def app_publish(self, req, path='view'):
--- a/doc/book/en/devrepo/datamodel/definition.rst Thu Dec 22 17:44:50 2011 +0100
+++ b/doc/book/en/devrepo/datamodel/definition.rst Thu Dec 22 17:46:55 2011 +0100
@@ -186,6 +186,9 @@
* `default`: default value of the attribute. In case of date types, the values
which could be used correspond to the RQL keywords `TODAY` and `NOW`.
+* `metadata`: Is also accepted as an argument of the attribute contructor. It is
+ not really an attribute property. see `Metadata`_ for details.
+
Properties for `String` attributes:
* `fulltextindexed`: boolean indicating if the attribute is part of
@@ -567,17 +570,41 @@
In any case, identifiers starting with "CW" or "cw" are reserved for
internal use by the framework.
+ .. _Metadata:
+
+ Some attribute using the name of another attribute as prefix are considered
+ metadata. For example, if an EntityType have both a ``data`` and
+ ``data_format`` attribute, ``data_format`` is view as the ``format`` metadata
+ of ``data``. Later the :meth:`cw_attr_metadata` method will allow you to fetch
+ metadata related to an attribute. There are only three valid metadata names:
+ ``format``, ``encoding`` and ``name``.
+
The name of the Python attribute corresponds to the name of the attribute
or the relation in *CubicWeb* application.
An attribute is defined in the schema as follows::
- attr_name = attr_type(properties)
+ attr_name = AttrType(*properties, metadata={})
+
+where
+
+* `AttrType`: is one of the type listed in EntityType_,
+
+* `properties`: is a list of the attribute needs to satisfy (see `Properties`_
+ for more details),
-where `attr_type` is one of the type listed above and `properties` is
-a list of the attribute needs to satisfy (see `Properties`_
-for more details).
+* `metadata`: is a dictionary of meta attributes related to ``attr_name``.
+ Dictionary keys are the name of the meta attribute. Dictionary values
+ attributes objects (like the content of ``AttrType``). For each entry of the
+ metadata dictionary a ``<attr_name>_<key> = <value>`` attribute is
+ automaticaly added to the EntityType. see `Metadata`_ section for details
+ about valid key.
+
+
+ ---
+
+While building your schema
* it is possible to use the attribute `meta` to flag an entity type as a `meta`
(e.g. used to describe/categorize other entities)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devweb/ajax.rst Thu Dec 22 17:46:55 2011 +0100
@@ -0,0 +1,12 @@
+.. _ajax:
+
+Ajax
+----
+
+CubicWeb provides a few helpers to facilitate *javascript <-> python* communications.
+
+You can, for instance, register some python functions that will become
+callable from javascript through ajax calls. All the ajax URLs are handled
+by the ``AjaxController`` controller.
+
+.. automodule:: cubicweb.web.views.ajaxcontroller
--- a/doc/book/en/devweb/controllers.rst Thu Dec 22 17:44:50 2011 +0100
+++ b/doc/book/en/devweb/controllers.rst Thu Dec 22 17:46:55 2011 +0100
@@ -22,10 +22,6 @@
:exc:`NoSelectableObject` errors that may bubble up to its entry point, in an
end-user-friendly way (but other programming errors will slip through)
-* the JSon controller (same module) provides services for Ajax calls,
- typically using JSON as a serialization format for input, and
- sometimes using either JSON or XML for output;
-
* the JSonpController is a wrapper around the ``ViewController`` that
provides jsonp_ services. Padding can be specified with the
``callback`` request parameter. Only *jsonexport* / *ejsonexport*
@@ -36,10 +32,6 @@
* the Login/Logout controllers make effective user login or logout
requests
-.. warning::
-
- JsonController will probably be renamed into AjaxController soon since
- it has nothing to do with json per se.
.. _jsonp: http://en.wikipedia.org/wiki/JSONP
@@ -64,6 +56,13 @@
* the MailBugReport controller (web/views/basecontrollers.py) allows
to quickly have a `reportbug` feature in one's application
+* the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`
+ (:mod:`cubicweb.web.views.ajaxcontroller`) provides
+ services for Ajax calls, typically using JSON as a serialization format
+ for input, and sometimes using either JSON or XML for output. See
+ :ref:`ajax` chapter for more information.
+
+
Registration
++++++++++++
--- a/doc/book/en/devweb/index.rst Thu Dec 22 17:44:50 2011 +0100
+++ b/doc/book/en/devweb/index.rst Thu Dec 22 17:46:55 2011 +0100
@@ -12,6 +12,7 @@
request
views/index
rtags
+ ajax
js
css
edition/index
--- a/doc/book/en/devweb/js.rst Thu Dec 22 17:44:50 2011 +0100
+++ b/doc/book/en/devweb/js.rst Thu Dec 22 17:46:55 2011 +0100
@@ -72,21 +72,22 @@
A simple example with asyncRemoteExec
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-In the python side, we have to extend the ``BaseController``
-class. The ``@jsonize`` decorator ensures that the return value of the
-method is encoded as JSON data. By construction, the JSonController
-inputs everything in JSON format.
+On the python side, we have to define an
+:class:`cubicweb.web.views.ajaxcontroller.AjaxFunction` object. The
+simplest way to do that is to use the
+:func:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator (for more
+details on this, refer to :ref:`ajax`).
.. sourcecode: python
- from cubicweb.web.views.basecontrollers import JSonController, jsonize
+ from cubicweb.web.views.ajaxcontroller import ajaxfunc
- @monkeypatch(JSonController)
- @jsonize
+ # serialize output to json to get it back easily on the javascript side
+ @ajaxfunc(output_type='json')
def js_say_hello(self, name):
return u'hello %s' % name
-In the javascript side, we do the asynchronous call. Notice how it
+On the javascript side, we do the asynchronous call. Notice how it
creates a `deferred` object. Proper treatment of the return value or
error handling has to be done through the addCallback and addErrback
methods.
@@ -131,7 +132,7 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The server side implementation of `reloadComponent` is the
-js_component method of the JSonController.
+:func:`cubicweb.web.views.ajaxcontroller.component` *AjaxFunction* appobject.
The following function implements a two-steps method to delete a
standard bookmark and refresh the UI, while keeping the UI responsive.
@@ -166,7 +167,8 @@
* `url` (mandatory) should be a complete url (typically referencing
- the JSonController, but this is not strictly mandatory)
+ the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`,
+ but this is not strictly mandatory)
* `data` (optional) is a dictionary of values given to the
controller specified through an `url` argument; some keys may have a
@@ -204,25 +206,23 @@
.. sourcecode:: python
- from cubicweb import typed_eid
- from cubicweb.web.views.basecontrollers import JSonController, xhtmlize
+ from cubicweb.web.views.ajaxcontroller import ajaxfunc
- @monkeypatch(JSonController)
- @xhtmlize
+ @ajaxfunc(output_type='xhtml')
def js_frob_status(self, eid, frobname):
- entity = self._cw.entity_from_eid(typed_eid(eid))
+ entity = self._cw.entity_from_eid(eid)
return entity.view('frob', name=frobname)
.. sourcecode:: javascript
- function update_some_div(divid, eid, frobname) {
+ function updateSomeDiv(divid, eid, frobname) {
var params = {fname:'frob_status', eid: eid, frobname:frobname};
jQuery('#'+divid).loadxhtml(JSON_BASE_URL, params, 'post');
}
In this example, the url argument is the base json url of a cube
instance (it should contain something like
-`http://myinstance/json?`). The actual JSonController method name is
+`http://myinstance/ajax?`). The actual AjaxController method name is
encoded in the `params` dictionary using the `fname` key.
A more real-life example
@@ -250,7 +250,7 @@
w(u'</div>')
self._cw.add_onload(u"""
jQuery('#lazy-%(vid)s').bind('%(event)s', function() {
- load_now('#lazy-%(vid)s');});"""
+ loadNow('#lazy-%(vid)s');});"""
% {'event': 'load_%s' % vid, 'vid': vid})
This creates a `div` with a specific event associated to it.
@@ -271,7 +271,7 @@
.. sourcecode:: javascript
- function load_now(eltsel) {
+ function loadNow(eltsel) {
var lazydiv = jQuery(eltsel);
lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl'));
}
@@ -306,18 +306,77 @@
"""trigger an event that will force immediate loading of the view
on dom readyness
"""
- self._cw.add_onload("trigger_load('%s');" % vid)
+ self._cw.add_onload("triggerLoad('%s');" % vid)
The browser-side definition follows.
.. sourcecode:: javascript
- function trigger_load(divid) {
+ function triggerLoad(divid) {
jQuery('#lazy-' + divd).trigger('load_' + divid);
}
-.. XXX userCallback / user_callback
+python/ajax dynamic callbacks
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+CubicWeb provides a way to dynamically register a function and make it
+callable from the javascript side. The typical use case for this is a
+situation where you have everything at hand to implement an action
+(whether it be performing a RQL query or executing a few python
+statements) that you'd like to defer to a user click in the web
+interface. In other words, generate an HTML ``<a href=...`` link that
+would execute your few lines of code.
+
+The trick is to create a python function and store this function in
+the user's session data. You will then be able to access it later.
+While this might sound hard to implement, it's actually quite easy
+thanks to the ``_cw.user_callback()``. This method takes a function,
+registers it and returns a javascript instruction suitable for
+``href`` or ``onclick`` usage. The call is then performed
+asynchronously.
+
+Here's a simplified example taken from the vcreview_ cube that will
+generate a link to change an entity state directly without the
+standard intermediate *comment / validate* step:
+
+.. sourcecode:: python
+
+ def entity_call(self, entity):
+ # [...]
+ def change_state(req, eid):
+ entity = req.entity_from_eid(eid)
+ entity.cw_adapt_to('IWorkflowable').fire_transition('done')
+ url = self._cw.user_callback(change_state, (entity.eid,))
+ self.w(tags.input(type='button', onclick=url, value=self._cw._('mark as done')))
+
+
+The ``change_state`` callback function is registered with
+``self._cw.user_callback()`` which returns the ``url`` value directly
+used for the ``onclick`` attribute of the button. On the javascript
+side, the ``userCallback()`` function is used but you most probably
+won't have to bother with it.
+
+Of course, when dealing with session data, the question of session
+cleaning pops up immediately. If you use ``user_callback()``, the
+registered function will be deleted automatically at some point
+as any other session data. If you want your function to be deleted once
+the web page is unloaded or when the user has clicked once on your link, then
+``_cw.register_onetime_callback()`` is what you need. It behaves as
+``_cw.user_callback()`` but stores the function in page data instead
+of global session data.
+
+
+.. Warning::
+
+ Be careful when registering functions with closures, keep in mind that
+ enclosed data will be kept in memory until the session gets cleared. Also,
+ if you keep entities or any object referecing the current ``req`` object, you
+ might have problems reusing them later because the underlying session
+ might have been closed at the time the callback gets executed.
+
+
+.. _vcreview: http://www.cubicweb.org/project/cubicweb-vcreview
Javascript library: overview
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -356,12 +415,12 @@
.. toctree::
:maxdepth: 1
-
+
js_api/index
Testing javascript
-~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~
You with the ``cubicweb.qunit.QUnitTestCase`` can include standard Qunit tests
inside the python unittest run . You simply have to define a new class that
--- a/schema.py Thu Dec 22 17:44:50 2011 +0100
+++ b/schema.py Thu Dec 22 17:46:55 2011 +0100
@@ -27,7 +27,7 @@
from logilab.common.decorators import cached, clear_cache, monkeypatch
from logilab.common.logging_ext import set_log_methods
-from logilab.common.deprecation import deprecated, class_moved
+from logilab.common.deprecation import deprecated, class_moved, moved
from logilab.common.textutils import splitstrip
from logilab.common.graph import get_cycles
from logilab.common.compat import any
@@ -1241,10 +1241,9 @@
# XXX deprecated
-from yams.buildobjs import RichString
from yams.constraints import StaticVocabularyConstraint
-RichString = class_moved(RichString)
+RichString = moved('yams.buildobjs', 'RichString')
StaticVocabularyConstraint = class_moved(StaticVocabularyConstraint)
FormatConstraint = class_moved(FormatConstraint)
--- a/server/sqlutils.py Thu Dec 22 17:44:50 2011 +0100
+++ b/server/sqlutils.py Thu Dec 22 17:46:55 2011 +0100
@@ -214,31 +214,11 @@
# callback lookup for each *cell* in results when there is nothing to
# lookup
if not column_callbacks:
- return self._process_result(cursor)
+ return self.dbhelper.dbapi_module.process_cursor(cursor, self._dbencoding,
+ Binary)
assert session
return self._cb_process_result(cursor, column_callbacks, session)
- def _process_result(self, cursor):
- # begin bind to locals for optimization
- descr = cursor.description
- encoding = self._dbencoding
- process_value = self._process_value
- binary = Binary
- # /end
- cursor.arraysize = 100
- while True:
- results = cursor.fetchmany()
- if not results:
- break
- for line in results:
- result = []
- for col, value in enumerate(line):
- if value is None:
- result.append(value)
- continue
- result.append(process_value(value, descr[col], encoding, binary))
- yield result
-
def _cb_process_result(self, cursor, column_callbacks, session):
# begin bind to locals for optimization
descr = cursor.description
--- a/server/test/unittest_postgres.py Thu Dec 22 17:44:50 2011 +0100
+++ b/server/test/unittest_postgres.py Thu Dec 22 17:46:55 2011 +0100
@@ -32,7 +32,7 @@
content=u'cubicweb cubicweb')
self.commit()
self.assertEqual(req.execute('Card X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
- [[c1.eid], [c3.eid], [c2.eid]])
+ [(c1.eid,), (c3.eid,), (c2.eid,)])
def test_attr_weight(self):
@@ -49,7 +49,7 @@
content=u'autre chose')
self.commit()
self.assertEqual(req.execute('Card X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
- [[c3.eid], [c1.eid], [c2.eid]])
+ [(c3.eid,), (c1.eid,), (c2.eid,)])
def test_entity_weight(self):
class PersonneIFTIndexableAdapter(IFTIndexableAdapter):
@@ -62,7 +62,7 @@
c3 = req.create_entity('Comment', content=u'cubicweb cubicweb cubicweb', comments=c1)
self.commit()
self.assertEqual(req.execute('Any X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
- [[c1.eid], [c3.eid], [c2.eid]])
+ [(c1.eid,), (c3.eid,), (c2.eid,)])
def test_tz_datetime(self):
--- a/web/application.py Thu Dec 22 17:44:50 2011 +0100
+++ b/web/application.py Thu Dec 22 17:46:55 2011 +0100
@@ -450,7 +450,7 @@
req.remove_header('Etag')
req.reset_message()
req.reset_headers()
- if req.json_request:
+ if req.ajax_request:
raise RemoteCallFailed(unicode(ex))
try:
req.data['ex'] = ex
--- a/web/data/cubicweb.css Thu Dec 22 17:44:50 2011 +0100
+++ b/web/data/cubicweb.css Thu Dec 22 17:46:55 2011 +0100
@@ -76,7 +76,7 @@
letter-spacing: 0.015em;
padding: 0.6em;
margin: 0 2em 1.7em;
- background-color: %(listingHihligthedBgColor)s;
+ background-color: %(listingHighlightedBgColor)s;
border: 1px solid %(listingBorderColor)s;
}
@@ -808,7 +808,7 @@
table.listing input,
table.listing textarea {
- background: %(listingHihligthedBgColor)s;
+ background: %(listingHighlightedBgColor)s;
}
table.htableForm label, table.oneRowTableForm label {
--- a/web/data/cubicweb.iprogress.css Thu Dec 22 17:44:50 2011 +0100
+++ b/web/data/cubicweb.iprogress.css Thu Dec 22 17:46:55 2011 +0100
@@ -62,11 +62,11 @@
}
table.progress tr.highlighted {
- background-color: %(listingHihligthedBgColor)s;
+ background-color: %(listingHighlightedBgColor)s;
}
table.progress tr.highlighted .progressbarback {
- border: 1px solid %(listingHihligthedBgColor)s;
+ border: 1px solid %(listingHighlightedBgColor)s;
}
table.progress .progressbarback {
--- a/web/data/cubicweb.js Thu Dec 22 17:44:50 2011 +0100
+++ b/web/data/cubicweb.js Thu Dec 22 17:46:55 2011 +0100
@@ -84,11 +84,13 @@
},
sortValueExtraction: function (node) {
- var sortvalue = jQuery(node).attr('cubicweb:sortvalue');
- if (sortvalue === undefined) {
- return '';
- }
- return cw.evalJSON(sortvalue);
+ var $node = $(node);
+ var sortvalue = $node.attr('cubicweb:sortvalue');
+ // No metadata found, use cell content as sort key
+ if (sortvalue === undefined) {
+ return $node.text();
+ }
+ return cw.evalJSON(sortvalue);
}
});
--- a/web/data/uiprops.py Thu Dec 22 17:44:50 2011 +0100
+++ b/web/data/uiprops.py Thu Dec 22 17:46:55 2011 +0100
@@ -146,7 +146,7 @@
# table listing & co ###########################################################
listingBorderColor = '#ccc'
listingHeaderBgColor = '#efefef'
-listingHihligthedBgColor = '#fbfbfb'
+listingHighlightedBgColor = '#fbfbfb'
# puce
bulletDownImg = 'url("puce_down.png") 98% 6px no-repeat'
--- a/web/request.py Thu Dec 22 17:44:50 2011 +0100
+++ b/web/request.py Thu Dec 22 17:46:55 2011 +0100
@@ -82,7 +82,7 @@
class CubicWebRequestBase(DBAPIRequest):
"""abstract HTTP request, should be extended according to the HTTP backend"""
- json_request = False # to be set to True by json controllers
+ ajax_request = False # to be set to True by ajax controllers
def __init__(self, vreg, https, form=None):
super(CubicWebRequestBase, self).__init__(vreg)
@@ -122,6 +122,12 @@
self.pageid = pid
@property
+ def json_request(self):
+ warn('[3.15] self._cw.json_request is deprecated, use self._cw.ajax_request instead',
+ DeprecationWarning, stacklevel=2)
+ return self.ajax_request
+
+ @property
def authmode(self):
return self.vreg.config['auth-mode']
--- a/web/test/unittest_views_basecontrollers.py Thu Dec 22 17:44:50 2011 +0100
+++ b/web/test/unittest_views_basecontrollers.py Thu Dec 22 17:46:55 2011 +0100
@@ -20,15 +20,19 @@
from __future__ import with_statement
from logilab.common.testlib import unittest_main, mock_object
+from logilab.common.decorators import monkeypatch
from cubicweb import Binary, NoSelectableObject, ValidationError
from cubicweb.view import STRICT_DOCTYPE
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.utils import json_dumps
from cubicweb.uilib import rql_for_eid
-from cubicweb.web import INTERNAL_FIELD_VALUE, Redirect, RequestError
+from cubicweb.web import INTERNAL_FIELD_VALUE, Redirect, RequestError, RemoteCallFailed
from cubicweb.entities.authobjs import CWUser
from cubicweb.web.views.autoform import get_pending_inserts, get_pending_deletes
+from cubicweb.web.views.basecontrollers import JSonController, xhtmlize, jsonize
+from cubicweb.web.views.ajaxcontroller import ajaxfunc, AjaxFunction
+
u = unicode
def req_form(user):
@@ -557,11 +561,12 @@
-class JSONControllerTC(CubicWebTC):
+class AjaxControllerTC(CubicWebTC):
+ tested_controller = 'ajax'
def ctrl(self, req=None):
req = req or self.request(url='http://whatever.fr/')
- return self.vreg['controllers'].select('json', req)
+ return self.vreg['controllers'].select(self.tested_controller, req)
def setup_database(self):
req = self.request()
@@ -679,8 +684,89 @@
self.assertEqual(self.remote_call('format_date', '2007-01-01 12:00:00')[0],
json_dumps('2007/01/01'))
+ def test_ajaxfunc_noparameter(self):
+ @ajaxfunc
+ def foo(self, x, y):
+ return 'hello'
+ self.assertTrue(issubclass(foo, AjaxFunction))
+ self.assertEqual(foo.__regid__, 'foo')
+ self.assertEqual(foo.check_pageid, False)
+ self.assertEqual(foo.output_type, None)
+ req = self.request()
+ f = foo(req)
+ self.assertEqual(f(12, 13), 'hello')
+
+ def test_ajaxfunc_checkpageid(self):
+ @ajaxfunc( check_pageid=True)
+ def foo(self, x, y):
+ pass
+ self.assertTrue(issubclass(foo, AjaxFunction))
+ self.assertEqual(foo.__regid__, 'foo')
+ self.assertEqual(foo.check_pageid, True)
+ self.assertEqual(foo.output_type, None)
+ # no pageid
+ req = self.request()
+ f = foo(req)
+ self.assertRaises(RemoteCallFailed, f, 12, 13)
+
+ def test_ajaxfunc_json(self):
+ @ajaxfunc(output_type='json')
+ def foo(self, x, y):
+ return x + y
+ self.assertTrue(issubclass(foo, AjaxFunction))
+ self.assertEqual(foo.__regid__, 'foo')
+ self.assertEqual(foo.check_pageid, False)
+ self.assertEqual(foo.output_type, 'json')
+ # no pageid
+ req = self.request()
+ f = foo(req)
+ self.assertEqual(f(12, 13), '25')
+class JSonControllerTC(AjaxControllerTC):
+ # NOTE: this class performs the same tests as AjaxController but with
+ # deprecated 'json' controller (i.e. check backward compatibility)
+ tested_controller = 'json'
+
+ def setUp(self):
+ super(JSonControllerTC, self).setUp()
+ self.exposed_remote_funcs = [fname for fname in dir(JSonController)
+ if fname.startswith('js_')]
+
+ def tearDown(self):
+ super(JSonControllerTC, self).tearDown()
+ for funcname in dir(JSonController):
+ # remove functions added dynamically during tests
+ if funcname.startswith('js_') and funcname not in self.exposed_remote_funcs:
+ delattr(JSonController, funcname)
+
+ def test_monkeypatch_jsoncontroller(self):
+ self.assertRaises(RemoteCallFailed, self.remote_call, 'foo')
+ @monkeypatch(JSonController)
+ def js_foo(self):
+ return u'hello'
+ res, req = self.remote_call('foo')
+ self.assertEqual(res, u'hello')
+
+ def test_monkeypatch_jsoncontroller_xhtmlize(self):
+ self.assertRaises(RemoteCallFailed, self.remote_call, 'foo')
+ @monkeypatch(JSonController)
+ @xhtmlize
+ def js_foo(self):
+ return u'hello'
+ res, req = self.remote_call('foo')
+ self.assertEqual(res,
+ '<?xml version="1.0"?>\n' + STRICT_DOCTYPE +
+ u'<div xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb">hello</div>')
+
+ def test_monkeypatch_jsoncontroller_jsonize(self):
+ self.assertRaises(RemoteCallFailed, self.remote_call, 'foo')
+ @monkeypatch(JSonController)
+ @jsonize
+ def js_foo(self):
+ return 12
+ res, req = self.remote_call('foo')
+ self.assertEqual(res, '12')
if __name__ == '__main__':
unittest_main()
--- a/web/views/actions.py Thu Dec 22 17:44:50 2011 +0100
+++ b/web/views/actions.py Thu Dec 22 17:46:55 2011 +0100
@@ -130,7 +130,7 @@
params = self._cw.form.copy()
for param in ('vid', '__message') + controller.NAV_FORM_PARAMETERS:
params.pop(param, None)
- if self._cw.json_request:
+ if self._cw.ajax_request:
path = 'view'
if self.cw_rset is not None:
params = {'rql': self.cw_rset.printable_rql()}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/ajaxcontroller.py Thu Dec 22 17:46:55 2011 +0100
@@ -0,0 +1,452 @@
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+#
+# (disable pylint msg for client obj access to protected member as in obj._cw)
+# pylint: disable=W0212
+"""The ``ajaxcontroller`` module defines the :class:`AjaxController`
+controller and the ``ajax-funcs`` cubicweb registry.
+
+.. autoclass:: cubicweb.web.views.ajaxcontroller.AjaxController
+ :members:
+
+``ajax-funcs`` registry hosts exposed remote functions, that is
+functions that can be called from the javascript world.
+
+To register a new remote function, either decorate your function
+with the :ref:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator:
+
+.. sourcecode:: python
+
+ from cubicweb.selectors import mactch_user_groups
+ from cubicweb.web.views.ajaxcontroller import ajaxfunc
+
+ @ajaxfunc(output_type='json', selector=match_user_groups('managers'))
+ def list_users(self):
+ return [u for (u,) in self._cw.execute('Any L WHERE U login L')]
+
+or inherit from :class:`cubicwbe.web.views.ajaxcontroller.AjaxFunction` and
+implement the ``__call__`` method:
+
+.. sourcecode:: python
+
+ from cubicweb.web.views.ajaxcontroller import AjaxFunction
+ class ListUser(AjaxFunction):
+ __regid__ = 'list_users' # __regid__ is the name of the exposed function
+ __select__ = match_user_groups('managers')
+ output_type = 'json'
+
+ def __call__(self):
+ return [u for (u, ) in self._cw.execute('Any L WHERE U login L')]
+
+
+.. autoclass:: cubicweb.web.views.ajaxcontroller.AjaxFunction
+ :members:
+
+.. autofunction:: cubicweb.web.views.ajaxcontroller.ajaxfunc
+
+"""
+
+__docformat__ = "restructuredtext en"
+
+from functools import partial
+
+from logilab.common.date import strptime
+from logilab.common.deprecation import deprecated
+
+from cubicweb import ObjectNotFound, NoSelectableObject
+from cubicweb.appobject import AppObject
+from cubicweb.selectors import yes
+from cubicweb.utils import json, json_dumps, UStringIO
+from cubicweb.uilib import exc_message
+from cubicweb.web import RemoteCallFailed, DirectResponse
+from cubicweb.web.controller import Controller
+from cubicweb.web.views import vid_from_rset
+from cubicweb.web.views import basecontrollers
+
+
+def optional_kwargs(extraargs):
+ if extraargs is None:
+ return {}
+ # we receive unicode keys which is not supported by the **syntax
+ return dict((str(key), value) for key, value in extraargs.iteritems())
+
+
+class AjaxController(Controller):
+ """AjaxController handles ajax remote calls from javascript
+
+ The following javascript function call:
+
+ .. sourcecode:: javascript
+
+ var d = asyncRemoteExec('foo', 12, "hello");
+ d.addCallback(function(result) {
+ alert('server response is: ' + result);
+ });
+
+ will generate an ajax HTTP GET on the following url::
+
+ BASE_URL/ajax?fname=foo&arg=12&arg="hello"
+
+ The AjaxController controller will therefore be selected to handle those URLs
+ and will itself select the :class:`cubicweb.web.views.ajaxcontroller.AjaxFunction`
+ matching the *fname* parameter.
+ """
+ __regid__ = 'ajax'
+
+ def publish(self, rset=None):
+ self._cw.ajax_request = True
+ try:
+ fname = self._cw.form['fname']
+ except KeyError:
+ raise RemoteCallFailed('no method specified')
+ try:
+ func = self._cw.vreg['ajax-func'].select(fname, self._cw)
+ except ObjectNotFound:
+ # function not found in the registry, inspect JSonController for
+ # backward compatibility
+ try:
+ func = getattr(basecontrollers.JSonController, 'js_%s' % fname).im_func
+ func = partial(func, self)
+ except AttributeError:
+ raise RemoteCallFailed('no %s method' % fname)
+ else:
+ self.warning('remote function %s found on JSonController, '
+ 'use AjaxFunction / @ajaxfunc instead', fname)
+ except NoSelectableObject:
+ raise RemoteCallFailed('method %s not available in this context'
+ % fname)
+ # no <arg> attribute means the callback takes no argument
+ args = self._cw.form.get('arg', ())
+ if not isinstance(args, (list, tuple)):
+ args = (args,)
+ try:
+ args = [json.loads(arg) for arg in args]
+ except ValueError, exc:
+ self.exception('error while decoding json arguments for '
+ 'js_%s: %s (err: %s)', fname, args, exc)
+ raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
+ try:
+ result = func(*args)
+ except (RemoteCallFailed, DirectResponse):
+ raise
+ except Exception, exc:
+ self.exception('an exception occurred while calling js_%s(%s): %s',
+ fname, args, exc)
+ raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
+ if result is None:
+ return ''
+ # get unicode on @htmlize methods, encoded string on @jsonize methods
+ elif isinstance(result, unicode):
+ return result.encode(self._cw.encoding)
+ return result
+
+class AjaxFunction(AppObject):
+ """
+ Attributes on this base class are:
+
+ :attr: `check_pageid`: make sure the pageid received is valid before proceeding
+ :attr: `output_type`:
+
+ - *None*: no processing, no change on content-type
+
+ - *json*: serialize with `json_dumps` and set *application/json*
+ content-type
+
+ - *xhtml*: wrap result in an XML node and forces HTML / XHTML
+ content-type (use ``_cw.html_content_type()``)
+
+ """
+ __registry__ = 'ajax-func'
+ __select__ = yes()
+ __abstract__ = True
+
+ check_pageid = False
+ output_type = None
+
+ @staticmethod
+ def _rebuild_posted_form(names, values, action=None):
+ form = {}
+ for name, value in zip(names, values):
+ # remove possible __action_xxx inputs
+ if name.startswith('__action'):
+ if action is None:
+ # strip '__action_' to get the actual action name
+ action = name[9:]
+ continue
+ # form.setdefault(name, []).append(value)
+ if name in form:
+ curvalue = form[name]
+ if isinstance(curvalue, list):
+ curvalue.append(value)
+ else:
+ form[name] = [curvalue, value]
+ else:
+ form[name] = value
+ # simulate click on __action_%s button to help the controller
+ if action:
+ form['__action_%s' % action] = u'whatever'
+ return form
+
+ def validate_form(self, action, names, values):
+ self._cw.form = self._rebuild_posted_form(names, values, action)
+ return basecontrollers._validate_form(self._cw, self._cw.vreg)
+
+ def _exec(self, rql, args=None, rocheck=True):
+ """json mode: execute RQL and return resultset as json"""
+ rql = rql.strip()
+ if rql.startswith('rql:'):
+ rql = rql[4:]
+ if rocheck:
+ self._cw.ensure_ro_rql(rql)
+ try:
+ return self._cw.execute(rql, args)
+ except Exception, ex:
+ self.exception("error in _exec(rql=%s): %s", rql, ex)
+ return None
+ return None
+
+ def _call_view(self, view, paginate=False, **kwargs):
+ divid = self._cw.form.get('divid')
+ # we need to call pagination before with the stream set
+ try:
+ stream = view.set_stream()
+ except AttributeError:
+ stream = UStringIO()
+ kwargs['w'] = stream.write
+ assert not paginate
+ if divid == 'pageContent':
+ # ensure divid isn't reused by the view (e.g. table view)
+ del self._cw.form['divid']
+ # mimick main template behaviour
+ stream.write(u'<div id="pageContent">')
+ vtitle = self._cw.form.get('vtitle')
+ if vtitle:
+ stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
+ paginate = True
+ nav_html = UStringIO()
+ if paginate and not view.handle_pagination:
+ view.paginate(w=nav_html.write)
+ stream.write(nav_html.getvalue())
+ if divid == 'pageContent':
+ stream.write(u'<div id="contentmain">')
+ view.render(**kwargs)
+ extresources = self._cw.html_headers.getvalue(skiphead=True)
+ if extresources:
+ stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget ?
+ stream.write(extresources)
+ stream.write(u'</div>\n')
+ if divid == 'pageContent':
+ stream.write(u'</div>%s</div>' % nav_html.getvalue())
+ return stream.getvalue()
+
+
+def _ajaxfunc_factory(implementation, selector=yes(), _output_type=None,
+ _check_pageid=False, regid=None):
+ """converts a standard python function into an AjaxFunction appobject"""
+ class AnAjaxFunc(AjaxFunction):
+ __regid__ = regid or implementation.__name__
+ __select__ = selector
+ output_type = _output_type
+ check_pageid = _check_pageid
+
+ def serialize(self, content):
+ if self.output_type is None:
+ return content
+ elif self.output_type == 'xhtml':
+ self._cw.set_content_type(self._cw.html_content_type())
+ return ''.join((self._cw.document_surrounding_div(),
+ content.strip(), u'</div>'))
+ elif self.output_type == 'json':
+ self._cw.set_content_type('application/json')
+ return json_dumps(content)
+ raise RemoteCallFailed('no serializer found for output type %s'
+ % self.output_type)
+
+ def __call__(self, *args, **kwargs):
+ if self.check_pageid:
+ data = self._cw.session.data.get(self._cw.pageid)
+ if data is None:
+ raise RemoteCallFailed(self._cw._('pageid-not-found'))
+ return self.serialize(implementation(self, *args, **kwargs))
+ AnAjaxFunc.__name__ = implementation.__name__
+ # make sure __module__ refers to the original module otherwise
+ # vreg.register(obj) will ignore ``obj``.
+ AnAjaxFunc.__module__ = implementation.__module__
+ return AnAjaxFunc
+
+
+def ajaxfunc(implementation=None, selector=yes(), output_type=None,
+ check_pageid=False, regid=None):
+ """promote a standard function to an ``AjaxFunction`` appobject.
+
+ All parameters are optional:
+
+ :param selector: a custom selector object if needed, default is ``yes()``
+
+ :param output_type: either None, 'json' or 'xhtml' to customize output
+ content-type. Default is None
+
+ :param check_pageid: whether the function requires a valid `pageid` or not
+ to proceed. Default is False.
+
+ :param regid: a custom __regid__ for the created ``AjaxFunction`` object. Default
+ is to keep the wrapped function name.
+
+ ``ajaxfunc`` can be used both as a standalone decorator:
+
+ .. sourcecode:: python
+
+ @ajaxfunc
+ def my_function(self):
+ return 42
+
+ or as a parametrizable decorator:
+
+ .. sourcecode:: python
+
+ @ajaxfunc(output_type='json')
+ def my_function(self):
+ return 42
+
+ """
+ # if used as a parametrized decorator (e.g. @ajaxfunc(output_type='json'))
+ if implementation is None:
+ def _decorator(func):
+ return _ajaxfunc_factory(func, selector=selector,
+ _output_type=output_type,
+ _check_pageid=check_pageid,
+ regid=regid)
+ return _decorator
+ # else, used as a standalone decorator (i.e. @ajaxfunc)
+ return _ajaxfunc_factory(implementation, selector=selector,
+ _output_type=output_type,
+ _check_pageid=check_pageid, regid=regid)
+
+
+
+###############################################################################
+# Cubicweb remote functions for : #
+# - appobject rendering #
+# - user / page session data management #
+###############################################################################
+@ajaxfunc(output_type='xhtml')
+def view(self):
+ # XXX try to use the page-content template
+ req = self._cw
+ rql = req.form.get('rql')
+ if rql:
+ rset = self._exec(rql)
+ elif 'eid' in req.form:
+ rset = self._cw.eid_rset(req.form['eid'])
+ else:
+ rset = None
+ vid = req.form.get('vid') or vid_from_rset(req, rset, self._cw.vreg.schema)
+ try:
+ viewobj = self._cw.vreg['views'].select(vid, req, rset=rset)
+ except NoSelectableObject:
+ vid = req.form.get('fallbackvid', 'noresult')
+ viewobj = self._cw.vreg['views'].select(vid, req, rset=rset)
+ viewobj.set_http_cache_headers()
+ req.validate_cache()
+ return self._call_view(viewobj, paginate=req.form.pop('paginate', False))
+
+
+@ajaxfunc(output_type='xhtml')
+def component(self, compid, rql, registry='components', extraargs=None):
+ if rql:
+ rset = self._exec(rql)
+ else:
+ rset = None
+ # XXX while it sounds good, addition of the try/except below cause pb:
+ # when filtering using facets return an empty rset, the edition box
+ # isn't anymore selectable, as expected. The pb is that with the
+ # try/except below, we see a "an error occurred" message in the ui, while
+ # we don't see it without it. Proper fix would probably be to deal with
+ # this by allowing facet handling code to tell to js_component that such
+ # error is expected and should'nt be reported.
+ #try:
+ comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset,
+ **optional_kwargs(extraargs))
+ #except NoSelectableObject:
+ # raise RemoteCallFailed('unselectable')
+ return self._call_view(comp, **optional_kwargs(extraargs))
+
+@ajaxfunc(output_type='xhtml')
+def render(self, registry, oid, eid=None,
+ selectargs=None, renderargs=None):
+ if eid is not None:
+ rset = self._cw.eid_rset(eid)
+ # XXX set row=0
+ elif self._cw.form.get('rql'):
+ rset = self._cw.execute(self._cw.form['rql'])
+ else:
+ rset = None
+ viewobj = self._cw.vreg[registry].select(oid, self._cw, rset=rset,
+ **optional_kwargs(selectargs))
+ return self._call_view(viewobj, **optional_kwargs(renderargs))
+
+
+@ajaxfunc(output_type='json')
+def i18n(self, msgids):
+ """returns the translation of `msgid`"""
+ return [self._cw._(msgid) for msgid in msgids]
+
+@ajaxfunc(output_type='json')
+def format_date(self, strdate):
+ """returns the formatted date for `msgid`"""
+ date = strptime(strdate, '%Y-%m-%d %H:%M:%S')
+ return self._cw.format_date(date)
+
+@ajaxfunc(output_type='json')
+def external_resource(self, resource):
+ """returns the URL of the external resource named `resource`"""
+ return self._cw.uiprops[resource]
+
+@ajaxfunc(output_type='json', check_pageid=True)
+def user_callback(self, cbname):
+ """execute the previously registered user callback `cbname`.
+
+ If matching callback is not found, return None
+ """
+ page_data = self._cw.session.data.get(self._cw.pageid, {})
+ try:
+ cb = page_data[cbname]
+ except KeyError:
+ self.warning('unable to find user callback %s', cbname)
+ return None
+ return cb(self._cw)
+
+
+@ajaxfunc
+def unregister_user_callback(self, cbname):
+ """unregister user callback `cbname`"""
+ self._cw.unregister_callback(self._cw.pageid, cbname)
+
+@ajaxfunc
+def unload_page_data(self):
+ """remove user's session data associated to current pageid"""
+ self._cw.session.data.pop(self._cw.pageid, None)
+
+@ajaxfunc(output_type='json')
+@deprecated("[3.13] use jQuery.cookie(cookiename, cookievalue, {path: '/'}) in js land instead")
+def set_cookie(self, cookiename, cookievalue):
+ """generates the Set-Cookie HTTP reponse header corresponding
+ to `cookiename` / `cookievalue`.
+ """
+ cookiename, cookievalue = str(cookiename), str(cookievalue)
+ self._cw.set_cookie(cookiename, cookievalue)
--- a/web/views/autoform.py Thu Dec 22 17:44:50 2011 +0100
+++ b/web/views/autoform.py Thu Dec 22 17:46:55 2011 +0100
@@ -134,10 +134,11 @@
from cubicweb.selectors import (
match_kwargs, match_form_params, non_final_entity,
specified_etype_implements)
-from cubicweb.utils import json_dumps
+from cubicweb.utils import json, json_dumps
from cubicweb.web import (stdmsgs, uicfg, eid_param,
form as f, formwidgets as fw, formfields as ff)
from cubicweb.web.views import forms
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
_AFS = uicfg.autoform_section
_AFFK = uicfg.autoform_field_kwargs
@@ -437,6 +438,70 @@
execute(rql, {'x': subj, 'y': obj})
+# ajax edition helpers ########################################################
+@ajaxfunc(output_type='xhtml', check_pageid=True)
+def inline_creation_form(self, peid, petype, ttype, rtype, role, i18nctx):
+ view = self._cw.vreg['views'].select('inline-creation', self._cw,
+ etype=ttype, rtype=rtype, role=role,
+ peid=peid, petype=petype)
+ return self._call_view(view, i18nctx=i18nctx)
+
+@ajaxfunc(output_type='json')
+def validate_form(self, action, names, values):
+ return self.validate_form(action, names, values)
+
+@ajaxfunc
+def cancel_edition(self, errorurl):
+ """cancelling edition from javascript
+
+ We need to clear associated req's data :
+ - errorurl
+ - pending insertions / deletions
+ """
+ self._cw.cancel_edition(errorurl)
+
+@ajaxfunc(output_type='xhtml')
+def reledit_form(self):
+ req = self._cw
+ args = dict((x, req.form[x])
+ for x in ('formid', 'rtype', 'role', 'reload', 'action'))
+ rset = req.eid_rset(typed_eid(self._cw.form['eid']))
+ try:
+ args['reload'] = json.loads(args['reload'])
+ except ValueError: # not true/false, an absolute url
+ assert args['reload'].startswith('http')
+ view = req.vreg['views'].select('reledit', req, rset=rset, rtype=args['rtype'])
+ return self._call_view(view, **args)
+
+
+def _add_pending(req, eidfrom, rel, eidto, kind):
+ key = 'pending_%s' % kind
+ pendings = req.session.data.setdefault(key, set())
+ pendings.add( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
+
+def _remove_pending(req, eidfrom, rel, eidto, kind):
+ key = 'pending_%s' % kind
+ pendings = req.session.data[key]
+ pendings.remove( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
+
+@ajaxfunc(output_type='json')
+def remove_pending_insert(self, (eidfrom, rel, eidto)):
+ _remove_pending(self._cw, eidfrom, rel, eidto, 'insert')
+
+@ajaxfunc(output_type='json')
+def add_pending_inserts(self, tripletlist):
+ for eidfrom, rel, eidto in tripletlist:
+ _add_pending(self._cw, eidfrom, rel, eidto, 'insert')
+
+@ajaxfunc(output_type='json')
+def remove_pending_delete(self, (eidfrom, rel, eidto)):
+ _remove_pending(self._cw, eidfrom, rel, eidto, 'delete')
+
+@ajaxfunc(output_type='json')
+def add_pending_delete(self, (eidfrom, rel, eidto)):
+ _add_pending(self._cw, eidfrom, rel, eidto, 'delete')
+
+
class GenericRelationsWidget(fw.FieldWidget):
def render(self, form, field, renderer):
--- a/web/views/basecontrollers.py Thu Dec 22 17:44:50 2011 +0100
+++ b/web/views/basecontrollers.py Thu Dec 22 17:46:55 2011 +0100
@@ -22,20 +22,21 @@
__docformat__ = "restructuredtext en"
_ = unicode
-from logilab.common.date import strptime
+from warnings import warn
+
from logilab.common.deprecation import deprecated
from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
AuthenticationError, typed_eid)
-from cubicweb.utils import UStringIO, json, json_dumps
-from cubicweb.uilib import exc_message
-from cubicweb.selectors import authenticated_user, anonymous_user, match_form_params
-from cubicweb.mail import format_mail
-from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse, facet
+from cubicweb.utils import json_dumps
+from cubicweb.selectors import (authenticated_user, anonymous_user,
+ match_form_params)
+from cubicweb.web import Redirect, RemoteCallFailed
from cubicweb.web.controller import Controller
-from cubicweb.web.views import vid_from_rset, formrenderers
+from cubicweb.web.views import vid_from_rset
+@deprecated('jsonize is deprecated, use AjaxFunction appobjects instead')
def jsonize(func):
"""decorator to sets correct content_type and calls `json_dumps` on
results
@@ -46,6 +47,7 @@
wrapper.__name__ = func.__name__
return wrapper
+@deprecated('xhtmlize is deprecated, use AjaxFunction appobjects instead')
def xhtmlize(func):
"""decorator to sets correct content_type and calls `xmlize` on results"""
def wrapper(self, *args, **kwargs):
@@ -56,6 +58,7 @@
wrapper.__name__ = func.__name__
return wrapper
+@deprecated('check_pageid is deprecated, use AjaxFunction appobjects instead')
def check_pageid(func):
"""decorator which checks the given pageid is found in the
user's session data
@@ -234,7 +237,7 @@
</script>""" % (domid, callback, errback, jsargs, cbargs)
def publish(self, rset=None):
- self._cw.json_request = True
+ self._cw.ajax_request = True
# XXX unclear why we have a separated controller here vs
# js_validate_form on the json controller
status, args, entity = _validate_form(self._cw, self._cw.vreg)
@@ -242,339 +245,18 @@
self._cw.encoding)
return self.response(domid, status, args, entity)
-def optional_kwargs(extraargs):
- if extraargs is None:
- return {}
- # we receive unicode keys which is not supported by the **syntax
- return dict((str(key), value) for key, value in extraargs.iteritems())
-
class JSonController(Controller):
__regid__ = 'json'
def publish(self, rset=None):
- """call js_* methods. Expected form keys:
-
- :fname: the method name without the js_ prefix
- :args: arguments list (json)
-
- note: it's the responsability of js_* methods to set the correct
- response content type
- """
- self._cw.json_request = True
- try:
- fname = self._cw.form['fname']
- func = getattr(self, 'js_%s' % fname)
- except KeyError:
- raise RemoteCallFailed('no method specified')
- except AttributeError:
- raise RemoteCallFailed('no %s method' % fname)
- # no <arg> attribute means the callback takes no argument
- args = self._cw.form.get('arg', ())
- if not isinstance(args, (list, tuple)):
- args = (args,)
- try:
- args = [json.loads(arg) for arg in args]
- except ValueError, exc:
- self.exception('error while decoding json arguments for js_%s: %s (err: %s)',
- fname, args, exc)
- raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
- try:
- result = func(*args)
- except (RemoteCallFailed, DirectResponse):
- raise
- except Exception, exc:
- self.exception('an exception occurred while calling js_%s(%s): %s',
- fname, args, exc)
- raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
- if result is None:
- return ''
- # get unicode on @htmlize methods, encoded string on @jsonize methods
- elif isinstance(result, unicode):
- return result.encode(self._cw.encoding)
- return result
-
- def _rebuild_posted_form(self, names, values, action=None):
- form = {}
- for name, value in zip(names, values):
- # remove possible __action_xxx inputs
- if name.startswith('__action'):
- if action is None:
- # strip '__action_' to get the actual action name
- action = name[9:]
- continue
- # form.setdefault(name, []).append(value)
- if name in form:
- curvalue = form[name]
- if isinstance(curvalue, list):
- curvalue.append(value)
- else:
- form[name] = [curvalue, value]
- else:
- form[name] = value
- # simulate click on __action_%s button to help the controller
- if action:
- form['__action_%s' % action] = u'whatever'
- return form
-
- def _exec(self, rql, args=None, rocheck=True):
- """json mode: execute RQL and return resultset as json"""
- rql = rql.strip()
- if rql.startswith('rql:'):
- rql = rql[4:]
- if rocheck:
- self._cw.ensure_ro_rql(rql)
- try:
- return self._cw.execute(rql, args)
- except Exception, ex:
- self.exception("error in _exec(rql=%s): %s", rql, ex)
- return None
- return None
-
- def _call_view(self, view, paginate=False, **kwargs):
- divid = self._cw.form.get('divid')
- # we need to call pagination before with the stream set
- try:
- stream = view.set_stream()
- except AttributeError:
- stream = UStringIO()
- kwargs['w'] = stream.write
- assert not paginate
- if divid == 'pageContent':
- # ensure divid isn't reused by the view (e.g. table view)
- del self._cw.form['divid']
- # mimick main template behaviour
- stream.write(u'<div id="pageContent">')
- vtitle = self._cw.form.get('vtitle')
- if vtitle:
- stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
- paginate = True
- nav_html = UStringIO()
- if paginate and not view.handle_pagination:
- view.paginate(w=nav_html.write)
- stream.write(nav_html.getvalue())
- if divid == 'pageContent':
- stream.write(u'<div id="contentmain">')
- view.render(**kwargs)
- extresources = self._cw.html_headers.getvalue(skiphead=True)
- if extresources:
- stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget ?
- stream.write(extresources)
- stream.write(u'</div>\n')
- if divid == 'pageContent':
- stream.write(u'</div>%s</div>' % nav_html.getvalue())
- return stream.getvalue()
-
- @xhtmlize
- def js_view(self):
- # XXX try to use the page-content template
- req = self._cw
- rql = req.form.get('rql')
- if rql:
- rset = self._exec(rql)
- elif 'eid' in req.form:
- rset = self._cw.eid_rset(req.form['eid'])
- else:
- rset = None
- vid = req.form.get('vid') or vid_from_rset(req, rset, self._cw.vreg.schema)
- try:
- view = self._cw.vreg['views'].select(vid, req, rset=rset)
- except NoSelectableObject:
- vid = req.form.get('fallbackvid', 'noresult')
- view = self._cw.vreg['views'].select(vid, req, rset=rset)
- self.validate_cache(view)
- return self._call_view(view, paginate=req.form.pop('paginate', False))
-
- @xhtmlize
- def js_prop_widget(self, propkey, varname, tabindex=None):
- """specific method for CWProperty handling"""
- entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw)
- entity.eid = varname
- entity['pkey'] = propkey
- form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity)
- form.build_context()
- vfield = form.field_by_name('value')
- renderer = formrenderers.FormRenderer(self._cw)
- return vfield.render(form, renderer, tabindex=tabindex) \
- + renderer.render_help(form, vfield)
-
- @xhtmlize
- def js_component(self, compid, rql, registry='components', extraargs=None):
- if rql:
- rset = self._exec(rql)
- else:
- rset = None
- # XXX while it sounds good, addition of the try/except below cause pb:
- # when filtering using facets return an empty rset, the edition box
- # isn't anymore selectable, as expected. The pb is that with the
- # try/except below, we see a "an error occurred" message in the ui, while
- # we don't see it without it. Proper fix would probably be to deal with
- # this by allowing facet handling code to tell to js_component that such
- # error is expected and should'nt be reported.
- #try:
- comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset,
- **optional_kwargs(extraargs))
- #except NoSelectableObject:
- # raise RemoteCallFailed('unselectable')
- return self._call_view(comp, **optional_kwargs(extraargs))
-
- @xhtmlize
- def js_render(self, registry, oid, eid=None,
- selectargs=None, renderargs=None):
- if eid is not None:
- rset = self._cw.eid_rset(eid)
- # XXX set row=0
- elif self._cw.form.get('rql'):
- rset = self._cw.execute(self._cw.form['rql'])
- else:
- rset = None
- view = self._cw.vreg[registry].select(oid, self._cw, rset=rset,
- **optional_kwargs(selectargs))
- return self._call_view(view, **optional_kwargs(renderargs))
-
- @check_pageid
- @xhtmlize
- def js_inline_creation_form(self, peid, petype, ttype, rtype, role, i18nctx):
- view = self._cw.vreg['views'].select('inline-creation', self._cw,
- etype=ttype, rtype=rtype, role=role,
- peid=peid, petype=petype)
- return self._call_view(view, i18nctx=i18nctx)
-
- @jsonize
- def js_validate_form(self, action, names, values):
- return self.validate_form(action, names, values)
-
- def validate_form(self, action, names, values):
- self._cw.form = self._rebuild_posted_form(names, values, action)
- return _validate_form(self._cw, self._cw.vreg)
-
- @xhtmlize
- def js_reledit_form(self):
- req = self._cw
- args = dict((x, req.form[x])
- for x in ('formid', 'rtype', 'role', 'reload', 'action'))
- rset = req.eid_rset(typed_eid(self._cw.form['eid']))
- try:
- args['reload'] = json.loads(args['reload'])
- except ValueError: # not true/false, an absolute url
- assert args['reload'].startswith('http')
- view = req.vreg['views'].select('reledit', req, rset=rset, rtype=args['rtype'])
- return self._call_view(view, **args)
-
- @jsonize
- def js_i18n(self, msgids):
- """returns the translation of `msgid`"""
- return [self._cw._(msgid) for msgid in msgids]
-
- @jsonize
- def js_format_date(self, strdate):
- """returns the formatted date for `msgid`"""
- date = strptime(strdate, '%Y-%m-%d %H:%M:%S')
- return self._cw.format_date(date)
-
- @jsonize
- def js_external_resource(self, resource):
- """returns the URL of the external resource named `resource`"""
- return self._cw.uiprops[resource]
-
- @check_pageid
- @jsonize
- def js_user_callback(self, cbname):
- page_data = self._cw.session.data.get(self._cw.pageid, {})
- try:
- cb = page_data[cbname]
- except KeyError:
- return None
- return cb(self._cw)
-
- @jsonize
- def js_filter_build_rql(self, names, values):
- form = self._rebuild_posted_form(names, values)
- self._cw.form = form
- builder = facet.FilterRQLBuilder(self._cw)
- return builder.build_rql()
-
- @jsonize
- def js_filter_select_content(self, facetids, rql, mainvar):
- # Union unsupported yet
- select = self._cw.vreg.parse(self._cw, rql).children[0]
- filtered_variable = facet.get_filtered_variable(select, mainvar)
- facet.prepare_select(select, filtered_variable)
- update_map = {}
- for fid in facetids:
- fobj = facet.get_facet(self._cw, fid, select, filtered_variable)
- update_map[fid] = fobj.possible_values()
- return update_map
-
- def js_unregister_user_callback(self, cbname):
- self._cw.unregister_callback(self._cw.pageid, cbname)
-
- def js_unload_page_data(self):
- self._cw.session.data.pop(self._cw.pageid, None)
-
- def js_cancel_edition(self, errorurl):
- """cancelling edition from javascript
-
- We need to clear associated req's data :
- - errorurl
- - pending insertions / deletions
- """
- self._cw.cancel_edition(errorurl)
-
- def js_delete_bookmark(self, beid):
- rql = 'DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s'
- self._cw.execute(rql, {'b': typed_eid(beid), 'u' : self._cw.user.eid})
-
- def js_node_clicked(self, treeid, nodeeid):
- """add/remove eid in treestate cookie"""
- from cubicweb.web.views.treeview import treecookiename
- cookies = self._cw.get_cookie()
- statename = treecookiename(treeid)
- treestate = cookies.get(statename)
- if treestate is None:
- self._cw.set_cookie(statename, nodeeid)
- else:
- marked = set(filter(None, treestate.value.split(':')))
- if nodeeid in marked:
- marked.remove(nodeeid)
- else:
- marked.add(nodeeid)
- self._cw.set_cookie(statename, ':'.join(marked))
-
- @jsonize
- @deprecated("[3.13] use jQuery.cookie(cookiename, cookievalue, {path: '/'}) in js land instead")
- def js_set_cookie(self, cookiename, cookievalue):
- cookiename, cookievalue = str(cookiename), str(cookievalue)
- self._cw.set_cookie(cookiename, cookievalue)
-
- # relations edition stuff ##################################################
-
- def _add_pending(self, eidfrom, rel, eidto, kind):
- key = 'pending_%s' % kind
- pendings = self._cw.session.data.setdefault(key, set())
- pendings.add( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
-
- def _remove_pending(self, eidfrom, rel, eidto, kind):
- key = 'pending_%s' % kind
- pendings = self._cw.session.data[key]
- pendings.remove( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
-
- def js_remove_pending_insert(self, (eidfrom, rel, eidto)):
- self._remove_pending(eidfrom, rel, eidto, 'insert')
-
- def js_add_pending_inserts(self, tripletlist):
- for eidfrom, rel, eidto in tripletlist:
- self._add_pending(eidfrom, rel, eidto, 'insert')
-
- def js_remove_pending_delete(self, (eidfrom, rel, eidto)):
- self._remove_pending(eidfrom, rel, eidto, 'delete')
-
- def js_add_pending_delete(self, (eidfrom, rel, eidto)):
- self._add_pending(eidfrom, rel, eidto, 'delete')
+ warn('[3.15] JSONController is deprecated, use AjaxController instead',
+ DeprecationWarning)
+ ajax_controller = self._cw.vreg['controllers'].select('ajax', self._cw, appli=self.appli)
+ return ajax_controller.publish(rset)
# XXX move to massmailing
-
class MailBugReportController(Controller):
__regid__ = 'reportbug'
__select__ = match_form_params('description')
--- a/web/views/baseviews.py Thu Dec 22 17:44:50 2011 +0100
+++ b/web/views/baseviews.py Thu Dec 22 17:46:55 2011 +0100
@@ -157,7 +157,7 @@
""":__regid__: *incontext*
This view is used whenthe entity should be considered as displayed in its
- context. By default it produces the result of `textincontext` wrapped in a
+ context. By default it produces the result of ``entity.dc_title()`` wrapped in a
link leading to the primary view of the entity.
"""
__regid__ = 'incontext'
@@ -165,18 +165,15 @@
def cell_call(self, row, col):
entity = self.cw_rset.get_entity(row, col)
desc = cut(entity.dc_description(), 50)
- self.w(u'<a href="%s" title="%s">' % (
- xml_escape(entity.absolute_url()), xml_escape(desc)))
- self.w(xml_escape(self._cw.view('textincontext', self.cw_rset,
- row=row, col=col)))
- self.w(u'</a>')
-
+ self.w(u'<a href="%s" title="%s">%s</a>' % (
+ xml_escape(entity.absolute_url()), xml_escape(desc),
+ xml_escape(entity.dc_title())))
class OutOfContextView(EntityView):
""":__regid__: *outofcontext*
This view is used whenthe entity should be considered as displayed out of
- its context. By default it produces the result of `textoutofcontext` wrapped
+ its context. By default it produces the result of ``entity.dc_long_title()`` wrapped
in a link leading to the primary view of the entity.
"""
__regid__ = 'outofcontext'
@@ -184,11 +181,9 @@
def cell_call(self, row, col):
entity = self.cw_rset.get_entity(row, col)
desc = cut(entity.dc_description(), 50)
- self.w(u'<a href="%s" title="%s">' % (
- xml_escape(entity.absolute_url()), xml_escape(desc)))
- self.w(xml_escape(self._cw.view('textoutofcontext', self.cw_rset,
- row=row, col=col)))
- self.w(u'</a>')
+ self.w(u'<a href="%s" title="%s">%s</a>' % (
+ xml_escape(entity.absolute_url()), xml_escape(desc),
+ xml_escape(entity.dc_long_title())))
class OneLineView(EntityView):
@@ -205,9 +200,12 @@
"""the one line view for an entity: linked text view
"""
entity = self.cw_rset.get_entity(row, col)
- self.w(u'<a href="%s">' % xml_escape(entity.absolute_url()))
- self.w(xml_escape(self._cw.view('text', self.cw_rset, row=row, col=col)))
- self.w(u'</a>')
+ desc = cut(entity.dc_description(), 50)
+ title = cut(entity.dc_title(),
+ self._cw.property_value('navigation.short-line-size'))
+ self.w(u'<a href="%s" title="%s">%s</a>' % (
+ xml_escape(entity.absolute_url()), xml_escape(desc),
+ xml_escape(title)))
# text views ###################################################################
--- a/web/views/bookmark.py Thu Dec 22 17:44:50 2011 +0100
+++ b/web/views/bookmark.py Thu Dec 22 17:46:55 2011 +0100
@@ -22,11 +22,12 @@
from logilab.mtconverter import xml_escape
-from cubicweb import Unauthorized
+from cubicweb import Unauthorized, typed_eid
from cubicweb.selectors import is_instance, one_line_rset
from cubicweb.web import (action, component, uicfg, htmlwidgets,
formwidgets as fw)
from cubicweb.web.views import primary
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
_abaa = uicfg.actionbox_appearsin_addmenu
_abaa.tag_subject_of(('*', 'bookmarked_by', '*'), False)
@@ -133,3 +134,8 @@
menu.append(self.link(req._('pick existing bookmarks'), url))
self.append(menu)
self.render_items(w)
+
+@ajaxfunc
+def delete_bookmark(self, beid):
+ rql = 'DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s'
+ self._cw.execute(rql, {'b': typed_eid(beid), 'u' : self._cw.user.eid})
--- a/web/views/cwproperties.py Thu Dec 22 17:44:50 2011 +0100
+++ b/web/views/cwproperties.py Thu Dec 22 17:46:55 2011 +0100
@@ -35,6 +35,7 @@
from cubicweb.web.formwidgets import (Select, TextInput, Button, SubmitButton,
FieldWidget)
from cubicweb.web.views import primary, formrenderers, editcontroller
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
uicfg.primaryview_section.tag_object_of(('*', 'for_user', '*'), 'hidden')
@@ -419,6 +420,20 @@
"""
return 'view', {}
+
+@ajaxfunc(output_type='xhtml')
+def prop_widget(self, propkey, varname, tabindex=None):
+ """specific method for CWProperty handling"""
+ entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw)
+ entity.eid = varname
+ entity['pkey'] = propkey
+ form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity)
+ form.build_context()
+ vfield = form.field_by_name('value')
+ renderer = formrenderers.FormRenderer(self._cw)
+ return vfield.render(form, renderer, tabindex=tabindex) \
+ + renderer.render_help(form, vfield)
+
_afs = uicfg.autoform_section
_afs.tag_subject_of(('*', 'for_user', '*'), 'main', 'hidden')
_afs.tag_object_of(('*', 'for_user', '*'), 'main', 'hidden')
--- a/web/views/editcontroller.py Thu Dec 22 17:44:50 2011 +0100
+++ b/web/views/editcontroller.py Thu Dec 22 17:46:55 2011 +0100
@@ -161,7 +161,7 @@
neweid = entity.eid
except ValidationError, ex:
self._to_create[eid] = ex.entity
- if self._cw.json_request: # XXX (syt) why?
+ if self._cw.ajax_request: # XXX (syt) why?
ex.entity = eid
raise
self._to_create[eid] = neweid
--- a/web/views/facets.py Thu Dec 22 17:44:50 2011 +0100
+++ b/web/views/facets.py Thu Dec 22 17:46:55 2011 +0100
@@ -32,6 +32,7 @@
from cubicweb.uilib import css_em_num_value
from cubicweb.view import AnyRsetView
from cubicweb.web import component, facet as facetbase
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
def facets(req, rset, context, mainvar=None, **kwargs):
"""return the base rql and a list of widgets for facets applying to the
@@ -313,6 +314,28 @@
w(u'</div>')
w(u'</div>\n')
+# python-ajax remote functions used by facet widgets #########################
+
+@ajaxfunc(output_type='json')
+def filter_build_rql(self, names, values):
+ form = self._rebuild_posted_form(names, values)
+ self._cw.form = form
+ builder = facetbase.FilterRQLBuilder(self._cw)
+ return builder.build_rql()
+
+@ajaxfunc(output_type='json')
+def filter_select_content(self, facetids, rql, mainvar):
+ # Union unsupported yet
+ select = self._cw.vreg.parse(self._cw, rql).children[0]
+ filtered_variable = facetbase.get_filtered_variable(select, mainvar)
+ facetbase.prepare_select(select, filtered_variable)
+ update_map = {}
+ for fid in facetids:
+ fobj = facetbase.get_facet(self._cw, fid, select, filtered_variable)
+ update_map[fid] = fobj.possible_values()
+ return update_map
+
+
# facets ######################################################################
--- a/web/views/forms.py Thu Dec 22 17:44:50 2011 +0100
+++ b/web/views/forms.py Thu Dec 22 17:46:55 2011 +0100
@@ -406,7 +406,7 @@
return self.force_session_key
# XXX if this is a json request, suppose we should redirect to the
# entity primary view
- if self._cw.json_request and self.edited_entity.has_eid():
+ if self._cw.ajax_request and self.edited_entity.has_eid():
return '%s#%s' % (self.edited_entity.absolute_url(), self.domid)
# XXX we should not consider some url parameters that may lead to
# different url after a validation error
--- a/web/views/treeview.py Thu Dec 22 17:44:50 2011 +0100
+++ b/web/views/treeview.py Thu Dec 22 17:46:55 2011 +0100
@@ -31,6 +31,7 @@
from cubicweb.view import EntityView
from cubicweb.mixins import _done_init
from cubicweb.web.views import baseviews
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
def treecookiename(treeid):
return str('%s-treestate' % treeid)
@@ -280,3 +281,20 @@
treeid=treeid, initial_load=False, **morekwargs)
w(u'</li>')
+
+
+@ajaxfunc
+def node_clicked(self, treeid, nodeeid):
+ """add/remove eid in treestate cookie"""
+ cookies = self._cw.get_cookie()
+ statename = treecookiename(treeid)
+ treestate = cookies.get(statename)
+ if treestate is None:
+ self._cw.set_cookie(statename, nodeeid)
+ else:
+ marked = set(filter(None, treestate.value.split(':')))
+ if nodeeid in marked:
+ marked.remove(nodeeid)
+ else:
+ marked.add(nodeeid)
+ self._cw.set_cookie(statename, ':'.join(marked))