merge with another default heads
authorPierre-Yves David <pierre-yves.david@logilab.fr>
Tue, 12 Mar 2013 12:08:22 +0100
changeset 8725 29e19ca141fc
parent 8724 1beab80aed23 (current diff)
parent 8719 539ed3fb27cb (diff)
child 8727 5bca35901e9b
merge with another default heads
devtools/testlib.py
--- a/cwvreg.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/cwvreg.py	Tue Mar 12 12:08:22 2013 +0100
@@ -197,7 +197,7 @@
 from os.path import join, dirname, realpath
 from warnings import warn
 from datetime import datetime, date, time, timedelta
-from functools import partial
+from functools import partial, reduce
 
 from logilab.common.decorators import cached, clear_cache
 from logilab.common.deprecation import deprecated, class_deprecated
@@ -210,16 +210,25 @@
 from yams.constraints import BASE_CONVERTERS
 
 from cubicweb import (CW_SOFTWARE_ROOT, ETYPE_NAME_MAP, CW_EVENT_MANAGER,
-                      Binary, UnknownProperty, UnknownEid)
+                      onevent, Binary, UnknownProperty, UnknownEid)
 from cubicweb.predicates import (implements, appobject_selectable,
                                  _reset_is_instance_cache)
 
-# backward compat: those modules are now refering to app objects in
-# cw.web.views.uicfg and import * from backward compat. On registry reload, we
-# should pop those modules from the cache so references are properly updated on
-# subsequent reload
-CW_EVENT_MANAGER.bind('before-registry-reload', partial(sys.modules.pop, 'cubicweb.web.uicfg', None))
-CW_EVENT_MANAGER.bind('before-registry-reload', partial(sys.modules.pop, 'cubicweb.web.uihelper', None))
+
+@onevent('before-registry-reload')
+def cleanup_uicfg_compat():
+    """ backward compat: those modules are now refering to app objects in
+    cw.web.views.uicfg and import * from backward compat. On registry reload, we
+    should pop those modules from the cache so references are properly updated on
+    subsequent reload
+    """
+    if 'cubicweb.web' in sys.modules:
+        if getattr(sys.modules['cubicweb.web'], 'uicfg', None):
+            del sys.modules['cubicweb.web'].uicfg
+        if getattr(sys.modules['cubicweb.web'], 'uihelper', None):
+            del sys.modules['cubicweb.web'].uihelper
+    sys.modules.pop('cubicweb.web.uicfg', None)
+    sys.modules.pop('cubicweb.web.uihelper', None)
 
 def use_interfaces(obj):
     """return interfaces required by the given object by searching for
--- a/dbapi.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/dbapi.py	Tue Mar 12 12:08:22 2013 +0100
@@ -103,15 +103,6 @@
     The returned repository may be an in-memory repository or a proxy object
     using a specific RPC method, depending on the given URI (pyro or zmq).
     """
-    try:
-        return _get_repository(uri, config, vreg)
-    except ConnectionError:
-        raise
-    except Exception as exc:
-        raise ConnectionError('cause: %r' % exc)
-
-def _get_repository(uri=None, config=None, vreg=None):
-    """ implements get_repository (see above) """
     if uri is None:
         return _get_inmemory_repo(config, vreg)
 
--- a/devtools/httptest.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/devtools/httptest.py	Tue Mar 12 12:08:22 2013 +0100
@@ -156,7 +156,7 @@
             response = self.web_get('logout')
         self._ident_cookie = None
 
-    def web_get(self, path='', headers=None):
+    def web_request(self, path='', method='GET', body=None, headers=None)
         """Return an httplib.HTTPResponse object for the specified path
 
         Use available credential if available.
@@ -166,12 +166,15 @@
         if self._ident_cookie is not None:
             assert 'Cookie' not in headers
             headers['Cookie'] = self._ident_cookie
-        self._web_test_cnx.request("GET", '/' + path, headers=headers)
+        self._web_test_cnx.request(method, '/' + path, headers=headers, body=body)
         response = self._web_test_cnx.getresponse()
         response.body = response.read() # to chain request
         response.read = lambda : response.body
         return response
 
+    def web_get(self, path='', body=None, headers=None):
+        return self.web_request(path=path, body=body, headers=headers)
+
     def setUp(self):
         super(CubicWebServerTC, self).setUp()
         self.start_server()
--- a/devtools/testlib.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/devtools/testlib.py	Tue Mar 12 12:08:22 2013 +0100
@@ -85,8 +85,7 @@
 
 class JsonValidator(object):
     def parse_string(self, data):
-        json.loads(data)
-        return data
+        return json.loads(data)
 
 # email handling, to test emails sent by an application ########################
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/additionnal_services/index.rst	Tue Mar 12 12:08:22 2013 +0100
@@ -0,0 +1,14 @@
+Additional services
+===================
+
+In this chapter, we introduce services crossing the *web -
+repository - administration* organisation of the first parts of the
+CubicWeb book. Those services can be either proper services (like the
+undo functionality) or mere *topical cross-sections* across CubicWeb.
+
+.. toctree::
+   :maxdepth: 2
+
+   undo
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/additionnal_services/undo.rst	Tue Mar 12 12:08:22 2013 +0100
@@ -0,0 +1,337 @@
+Undoing changes in CubicWeb
+---------------------------
+
+Many desktop applications offer the possibility for the user to
+undo its last changes : this *undo feature* has now been
+integrated into the CubicWeb framework. This document will
+introduce you to the *undo feature* both from the end-user and the
+application developer point of view.
+
+But because a semantic web application and a common desktop
+application are not the same thing at all, especially as far as
+undoing is concerned, we will first introduce *what* is the *undo
+feature* for now.
+
+What's *undoing* in a CubicWeb application
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+What is an *undo feature* is quite intuitive in the context of a
+desktop application. But it is a bit subtler in the context of a
+Semantic Web application. This section introduces some of the main
+differences between a classical desktop and a Semantic Web
+applications to keep in mind in order to state precisely *what we
+want*.
+
+The notion transactions
+```````````````````````
+
+A CubicWeb application acts upon an *Entity-Relationship* model,
+described by a schema. This allows to ensure some data integrity
+properties. It also implies that changes are made by all-or-none
+groups called *transactions*, such that the data integrity is
+preserved whether the transaction is completely applied *or* none
+of it is applied.
+
+A transaction can thus include more actions than just those
+directly required by the main purpose of the user.  For example,
+when a user *just* writes a new blog entry, the underlying
+*transaction* holds several *actions* as illustrated below :
+
+* By admin on 2012/02/17 15:18 - Created Blog entry : Torototo
+
+  #. Created Blog entry : Torototo
+  #. Added relation : Torototo owned by admin
+  #. Added relation : Torototo blog entry of Undo Blog
+  #. Added relation : Torototo in state draft (draft)
+  #. Added relation : Torototo created by admin
+
+Because of the very nature (all-or-none) of the transactions, the
+"undoable stuff" are the transactions and not the actions !
+
+Public and private actions within a transaction
+```````````````````````````````````````````````
+
+Actually, within the *transaction* "Created Blog entry :
+Torototo", two of those *actions* are said to be *public* and
+the others are said to be *private*. *Public* here means that the
+public actions (1 and 3) were directly requested by the end user ;
+whereas *private* means that the other actions (2, 4, 5) were
+triggered "under the hood" to fulfill various requirements for the
+user operation (ensuring integrity, security, ... ).
+
+And because quite a lot of actions can be triggered by a "simple"
+end-user request, most of which the end-user is not (and does not
+need or wish to be) aware, only the so-called public actions will
+appear [1]_ in the description of the an undoable transaction.
+
+* By admin on 2012/02/17 15:18 - Created Blog entry : Torototo
+
+  #. Created Blog entry : Torototo
+  #. Added relation : Torototo blog entry of Undo Blog
+
+But note that both public and private actions will be undone
+together when the transaction is undone.
+
+(In)dependent transactions : the simple case
+````````````````````````````````````````````
+
+A CubicWeb application can be used *simultaneously* by different users
+(whereas a single user works on an given office document at a
+given time), so that there is not always a single history
+time-line in the CubicWeb case. Moreover CubicWeb provides
+security through the mechanism of *permissions* granted to each
+user. This can lead to some transactions *not* being undoable in
+some contexts.
+
+In the simple case two (unprivileged) users Alice and Bob make
+relatively independent changes : then both Alice and Bob can undo
+their changes. But in some case there is a clean dependency
+between Alice's and Bob's actions or between actions of one of
+them. For example let's suppose that :
+
+- Alice has created a blog,
+- then has published a first post inside,
+- then Bob has published a second post in the same blog,
+- and finally Alice has updated its post contents.
+
+Then it is clear that Alice can undo her contents changes and Bob
+can undo his post creation independently. But Alice can not undo
+her post creation while she has not first undone her changes.
+It is also clear that Bob should *not* have the
+permissions to undo any of Alice's transactions.
+
+
+More complex dependencies between transactions
+``````````````````````````````````````````````
+
+But more surprising things can quickly happen. Going back to the
+previous example, Alice *can* undo the creation of the blog after
+Bob has published its post in it ! But this is possible only
+because the schema does not *require* for a post to be in a
+blog. Would the *blog entry of* relation have been mandatory, then
+Alice could not have undone the blog creation because it would
+have broken integrity constraint for Bob's post.
+
+When a user attempts to undo a transaction the system will check
+whether a later transaction has explicit dependency on the
+would-be-undone transaction. In this case the system will not even
+attempt the undo operation and inform the user.
+
+If no such dependency is detected the system will attempt the undo
+operation but it can fail, typically because of integrity
+constraint violations. In such a case the undo operation is
+completely [3]_ rollbacked.
+
+
+The *undo feature* for CubicWeb end-users
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The exposition of the undo feature to the end-user through a Web
+interface is still quite basic and will be improved toward a
+greater usability. But it is already fully functional.  For now
+there are two ways to access the *undo feature* as long as the it
+has been activated in the instance configuration file with the
+option *undo-support=yes*.
+
+Immediately after having done the change to be canceled through
+the **undo** link in the message. This allows to undo an
+hastily action immediately. For example, just after having
+validated the creation of the blog entry *A second blog entry* we
+get the following message, allowing to undo the creation.
+
+.. image:: /images/undo_mesage_w600.png
+   :width: 600px
+   :alt: Screenshot of the undo link in the message
+   :align: center
+
+At any time we can access the **undo-history view** accessible from the
+start-up page.
+
+.. image:: /images/undo_startup-link_w600.png
+   :width: 600px
+   :alt: Screenshot of the startup menu with access to the history view
+   :align: center
+
+This view will provide inspection of the transaction and their (public)
+actions. Each transaction provides its own **undo** link. Only the
+transactions the user has permissions to see and undo will be shown.
+
+.. image:: /images/undo_history-view_w600.png
+   :width: 600px
+   :alt: Screenshot of the undo history main view
+   :align: center
+
+If the user attempts to undo a transaction which can't be undone or
+whose undoing fails, then a message will explain the situation and
+no partial undoing will be left behind.
+
+This is all for the end-user side of the undo mechanism : this is
+quite simple indeed ! Now, in the following section, we are going
+to introduce the developer side of the undo mechanism.
+
+The *undo feature* for CubicWeb application developers
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A word of warning : this section is intended for developers,
+already having some knowledge of what's under CubicWeb's hood. If
+it is not *yet* the case, please refer to CubicWeb documentation
+http://docs.cubicweb.org/ .
+
+Overview
+````````
+
+The core of the undo mechanisms is at work in the *native source*,
+beyond the RQL. This does mean that *transactions* and *actions*
+are *no entities*. Instead they are represented at the SQL level
+and exposed through the *DB-API* supported by the repository
+*Connection* objects.
+
+Once the *undo feature* has been activated in the instance
+configuration file with the option *undo-support=yes*, each
+mutating operation (cf. [2]_) will be recorded in some special SQL
+table along with its associated transaction. Transaction are
+identified by a *txuuid* through which the functions of the
+*DB-API* handle them.
+
+On the web side the last commited transaction *txuuid* is
+remembered in the request's data to allow for imediate undoing
+whereas the *undo-history view* relies upon the *DB-API* to list
+the accessible transactions. The actual undoing is performed by
+the *UndoController* accessible at URL of the form
+`www.my.host/my/instance/undo?txuuid=...`
+
+The repository side
+```````````````````
+
+Please refer to the file `cubicweb/server/sources/native.py` and
+`cubicweb/transaction.py` for the details.
+
+The undoing information is mainly stored in three SQL tables:
+
+`transactions`
+    Stores the txuuid, the user eid and the date-and-time of
+    the transaction. This table is referenced by the two others.
+
+`tx_entity_actions`
+    Stores the undo information for actions on entities.
+
+`tx_relation_actions`
+    Stores the undo information for the actions on relations.
+
+When the undo support is activated, entries are added to those
+tables for each mutating operation on the data repository, and are
+deleted on each transaction undoing.
+
+Those table are accessible through the following methods of the
+repository `Connection` object :
+
+`undoable_transactions`
+    Returns a list of `Transaction` objects accessible to the user
+    and according to the specified filter(s) if any.
+
+`tx_info`
+    Returns a `Transaction` object from a `txuuid`
+
+`undo_transaction`
+    Returns the list of `Action` object for the given `txuuid`.
+
+    NB:  By default it only return *public* actions.
+
+The web side
+````````````
+
+The exposure of the *undo feature* to the end-user through the Web
+interface relies on the *DB-API* introduced above. This implies
+that the *transactions* and *actions* are not *entities* linked by
+*relations* on which the usual views can be applied directly.
+
+That's why the file `cubicweb/web/views/undohistory.py` defines
+some dedicated views to access the undo information :
+
+`UndoHistoryView`
+    This is a *StartupView*, the one accessible from the home
+    page of the instance which list all transactions.
+
+`UndoableTransactionView`
+    This view handles the display of a single `Transaction` object.
+
+`UndoableActionBaseView`
+    This (abstract) base class provides private methods to build
+    the display of actions whatever their nature.
+
+`Undoable[Add|Remove|Create|Delete|Update]ActionView`
+    Those views all inherit from `UndoableActionBaseView` and
+    each handles a specific kind of action.
+
+`UndoableActionPredicate`
+    This predicate is used as a *selector* to pick the appropriate
+    view for actions.
+
+Apart from this main *undo-history view* a `txuuid` is stored in
+the request's data `last_undoable_transaction` in order to allow
+immediate undoing of a hastily validated operation. This is
+handled in `cubicweb/web/application.py` in the `main_publish` and
+`add_undo_link_to_msg` methods for the storing and displaying
+respectively.
+
+Once the undo information is accessible, typically through a
+`txuuid` in an *undo* URL, the actual undo operation can be
+performed by the `UndoController` defined in
+`cubicweb/web/views/basecontrollers.py`. This controller basically
+extracts the `txuuid` and performs a call to `undo_transaction` and
+in case of an undo-specific error, lets the top level publisher
+handle it as a validation error.
+
+
+Conclusion
+~~~~~~~~~~
+
+The undo mechanism relies upon a low level recording of the
+mutating operation on the repository. Those records are accessible
+through some method added to the *DB-API* and exposed to the
+end-user either through a whole history view of through an
+immediate undoing link in the message box.
+
+The undo feature is functional but the interface and configuration
+options are still quite reduced. One major improvement would be to
+be able to filter with a finer grain which transactions or actions
+one wants to see in the *undo-history view*. Another critical
+improvement would be to enable the undo feature on a part only of
+the entity-relationship schema to avoid storing too much useless
+data and reduce the underlying overhead.
+
+But both functionality are related to the strong design choice not
+to represent transactions and actions as entities and
+relations. This has huge benefits in terms of safety and conceptual
+simplicity but prevents from using lots of convenient CubicWeb
+features such as *facets* to access undo information.
+
+Before developing further the undo feature or eventually revising
+this design choice, it appears that some return of experience is
+strongly needed. So don't hesitate to try the undo feature in your
+application and send us some feedback.
+
+
+Notes
+~~~~~
+
+.. [1] The end-user Web interface could be improved to enable
+       user to choose whether he wishes to see private actions.
+
+.. [2] There is only five kind of elementary actions (beyond
+       merely accessing data for reading):
+
+       * **C** : creating an entity
+       * **D** : deleting an entity
+       * **U** : updating an entity attributes
+       * **A** : adding a relation
+       * **R** : removing a relation
+
+.. [3] Meaning none of the actions in the transaction is
+       undone. Depending upon the application, it might make sense
+       to enable *partial* undo. That is to say undo in which some
+       actions could not be undo without preventing to undo the
+       others actions in the transaction (as long as it does not
+       break schema integrity). This is not forbidden by the
+       back-end but is deliberately not supported by the front-end
+       (for now at least).
--- a/doc/book/en/devrepo/migration.rst	Tue Mar 12 12:04:51 2013 +0100
+++ b/doc/book/en/devrepo/migration.rst	Tue Mar 12 12:08:22 2013 +0100
@@ -139,7 +139,7 @@
 * `drop_relation_type(rtype, commit=True)`, removes a relation type and all the
   definitions of this type.
 
-* `rename_relationi_type(oldname, newname, commit=True)`, renames a relation type.
+* `rename_relation_type(oldname, newname, commit=True)`, renames a relation type.
 
 * `add_relation_definition(subjtype, rtype, objtype, commit=True)`, adds a new
   relation definition.
Binary file doc/book/en/images/undo_history-view_w600.png has changed
Binary file doc/book/en/images/undo_mesage_w600.png has changed
Binary file doc/book/en/images/undo_startup-link_w600.png has changed
--- a/doc/book/en/index.rst	Tue Mar 12 12:04:51 2013 +0100
+++ b/doc/book/en/index.rst	Tue Mar 12 12:08:22 2013 +0100
@@ -65,6 +65,7 @@
    :maxdepth: 2
 
    admin/index
+   additionnal_services/index
    annexes/index
 
 See also:
--- a/entity.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/entity.py	Tue Mar 12 12:08:22 2013 +0100
@@ -521,8 +521,8 @@
 
         Example (in a shell session):
 
-        >>> companycls = vreg['etypes'].etype_class(('Company')
-        >>> personcls = vreg['etypes'].etype_class(('Person')
+        >>> companycls = vreg['etypes'].etype_class('Company')
+        >>> personcls = vreg['etypes'].etype_class('Person')
         >>> c = companycls.cw_instantiate(session.execute, name=u'Logilab')
         >>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe',
         ...                              works_for=c)
--- a/etwist/server.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/etwist/server.py	Tue Mar 12 12:08:22 2013 +0100
@@ -31,7 +31,6 @@
 from datetime import date, timedelta
 from urlparse import urlsplit, urlunsplit
 from cgi import FieldStorage, parse_header
-from cStringIO import StringIO
 
 from twisted.internet import reactor, task, threads
 from twisted.internet.defer import maybeDeferred
@@ -40,6 +39,7 @@
 from twisted.web.server import NOT_DONE_YET
 
 
+from logilab.mtconverter import xml_escape
 from logilab.common.decorators import monkeypatch
 
 from cubicweb import (AuthenticationError, ConfigurationError,
@@ -144,9 +144,8 @@
             request.process_multipart()
             return self._render_request(request)
         except Exception:
-            errorstream = StringIO()
-            traceback.print_exc(file=errorstream)
-            return HTTPResponse(stream='<pre>%s</pre>' % errorstream.getvalue(),
+            trace = traceback.format_exc()
+            return HTTPResponse(stream='<pre>%s</pre>' % xml_escape(trace),
                                 code=500, twisted_request=request)
 
     def _render_request(self, request):
--- a/server/repository.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/server/repository.py	Tue Mar 12 12:08:22 2013 +0100
@@ -962,7 +962,7 @@
 
     def close_sessions(self):
         """close every opened sessions"""
-        for sessionid in self._sessions:
+        for sessionid in list(self._sessions):
             try:
                 self.close(sessionid, checkshuttingdown=False)
             except Exception: # XXX BaseException?
--- a/server/serverconfig.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/server/serverconfig.py	Tue Mar 12 12:08:22 2013 +0100
@@ -82,7 +82,9 @@
     """serialize a repository source configuration as text"""
     stream = StringIO()
     optsbysect = list(sconfig.options_by_section())
-    assert len(optsbysect) == 1, 'all options for a source should be in the same group'
+    assert len(optsbysect) == 1, (
+        'all options for a source should be in the same group, got %s'
+        % [x[0] for x in optsbysect])
     lgconfig.ini_format(stream, optsbysect[0][1], encoding)
     return stream.getvalue()
 
--- a/server/sources/ldapfeed.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/server/sources/ldapfeed.py	Tue Mar 12 12:08:22 2013 +0100
@@ -17,6 +17,7 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """cubicweb ldap feed source"""
 
+from cubicweb.cwconfig import merge_options
 from cubicweb.server.sources import datafeed
 from cubicweb.server import ldaputils
 
@@ -30,5 +31,7 @@
     support_entities = {'CWUser': False}
     use_cwuri_as_url = False
 
-    options = datafeed.DataFeedSource.options + ldaputils.LDAPSourceMixIn.options
+    options = merge_options(datafeed.DataFeedSource.options
+                            + ldaputils.LDAPSourceMixIn.options,
+                            optgroup='ldap-source')
 
--- a/server/sources/ldapuser.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/server/sources/ldapuser.py	Tue Mar 12 12:08:22 2013 +0100
@@ -88,9 +88,9 @@
 
     def init(self, activated, source_entity):
         """method called by the repository once ready to handle request"""
+        super(LDAPUserSource, self).init(activated, source_entity)
         if activated:
             self.info('ldap init')
-            self._entity_update(source_entity)
             # set minimum period of 5min 1s (the additional second is to
             # minimize resonnance effet)
             if self.user_rev_attrs['email']:
--- a/server/sources/native.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/server/sources/native.py	Tue Mar 12 12:08:22 2013 +0100
@@ -406,6 +406,7 @@
 
 
     def init(self, activated, source_entity):
+        super(NativeSQLSource, self).init(activated, source_entity)
         self.init_creating(source_entity._cw.cnxset)
         try:
             # test if 'asource' column exists
--- a/server/sources/remoterql.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/server/sources/remoterql.py	Tue Mar 12 12:08:22 2013 +0100
@@ -136,6 +136,7 @@
 
     def init(self, activated, source_entity):
         """method called by the repository once ready to handle request"""
+        super(RemoteSource, self).init(activated, source_entity)
         self.load_mapping(source_entity._cw)
         if activated:
             interval = self.config['synchronization-interval']
--- a/web/data/cubicweb.facets.js	Tue Mar 12 12:04:51 2013 +0100
+++ b/web/data/cubicweb.facets.js	Tue Mar 12 12:08:22 2013 +0100
@@ -11,7 +11,7 @@
 
 
 function copyParam(origparams, newparams, param) {
-    var index = jQuery.inArray(param, origparams[0]);
+    var index = $.inArray(param, origparams[0]);
     if (index > - 1) {
         newparams[param] = origparams[1][index];
     }
@@ -22,14 +22,14 @@
     var names = [];
     var values = [];
     $form.find('.facet').each(function() {
-        var facetName = jQuery(this).find('.facetTitle').attr('cubicweb:facetName');
+        var facetName = $(this).find('.facetTitle').attr('cubicweb:facetName');
         // FacetVocabularyWidget
-        jQuery(this).find('.facetValueSelected').each(function(x) {
+        $(this).find('.facetValueSelected').each(function(x) {
             names.push(facetName);
             values.push(this.getAttribute('cubicweb:value'));
         });
         // FacetStringWidget (e.g. has-text)
-        jQuery(this).find('input:text').each(function(){
+        $(this).find('input:text').each(function(){
             names.push(facetName);
             values.push(this.value);
         });
@@ -51,7 +51,7 @@
 
 // XXX deprecate vidargs once TableView is gone
 function buildRQL(divid, vid, paginate, vidargs) {
-    jQuery(CubicWeb).trigger('facets-content-loading', [divid, vid, paginate, vidargs]);
+    $(CubicWeb).trigger('facets-content-loading', [divid, vid, paginate, vidargs]);
     var $form = $('#' + divid + 'Form');
     var zipped = facetFormContent($form);
     zipped[0].push('facetargs');
@@ -59,7 +59,7 @@
     var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('filter_build_rql', null, zipped[0], zipped[1]));
     d.addCallback(function(result) {
         var rql = result[0];
-        var $bkLink = jQuery('#facetBkLink');
+        var $bkLink = $('#facetBkLink');
         if ($bkLink.length) {
             var bkPath = 'view?rql=' + encodeURIComponent(rql);
             if (vid) {
@@ -68,7 +68,7 @@
             var bkUrl = $bkLink.attr('cubicweb:target') + '&path=' + encodeURIComponent(bkPath);
             $bkLink.attr('href', bkUrl);
         }
-        var $focusLink = jQuery('#focusLink');
+        var $focusLink = $('#focusLink');
         if ($focusLink.length) {
             var url = baseuri()+ 'view?rql=' + encodeURIComponent(rql);
             if (vid) {
@@ -99,20 +99,20 @@
                                      null, 'swap');
         d.addCallback(function() {
             // XXX rql/vid in extraparams
-            jQuery(CubicWeb).trigger('facets-content-loaded', [divid, rql, vid, extraparams]);
+            $(CubicWeb).trigger('facets-content-loaded', [divid, rql, vid, extraparams]);
         });
         if (paginate) {
             // FIXME the edit box might not be displayed in which case we don't
             // know where to put the potential new one, just skip this case for
             // now
-            var $node = jQuery('#edit_box');
+            var $node = $('#edit_box');
             if ($node.length) {
                 $node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', {
                     'rql': rql
                 },
                 'ctxcomponents', 'edit_box'));
             }
-            $node = jQuery('#breadcrumbs');
+            $node = $('#breadcrumbs');
             if ($node.length) {
                 $node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', {
                     'rql': rql
@@ -121,7 +121,7 @@
             }
         }
         var mainvar = null;
-        var index = jQuery.inArray('mainvar', zipped[0]);
+        var index = $.inArray('mainvar', zipped[0]);
         if (index > - 1) {
             mainvar = zipped[1][index];
         }
@@ -134,13 +134,13 @@
                 //$form.find('div[cubicweb\\:facetName="' + facetName + '"] ~ div .facetCheckBox').each(function() {
                 $form.find('div').filter(function () {return $(this).attr('cubicweb:facetName') == facetName}).parent().find('.facetCheckBox').each(function() {
                     var value = this.getAttribute('cubicweb:value');
-                    if (jQuery.inArray(value, values) == -1) {
-                        if (!jQuery(this).hasClass('facetValueDisabled')) {
-                            jQuery(this).addClass('facetValueDisabled');
+                    if ($.inArray(value, values) == -1) {
+                        if (!$(this).hasClass('facetValueDisabled')) {
+                            $(this).addClass('facetValueDisabled');
                         }
                     } else {
-                        if (jQuery(this).hasClass('facetValueDisabled')) {
-                            jQuery(this).removeClass('facetValueDisabled');
+                        if ($(this).hasClass('facetValueDisabled')) {
+                            $(this).removeClass('facetValueDisabled');
                         }
                     }
                 });
@@ -153,8 +153,8 @@
 function initFacetBoxEvents(root) {
     // facetargs : (divid, vid, paginate, extraargs)
     root = root || document;
-    jQuery(root).find('form').each(function() {
-        var form = jQuery(this);
+    $(root).find('form').each(function() {
+        var form = $(this);
         // NOTE: don't evaluate facetargs here but in callbacks since its value
         //       may changes and we must send its value when the callback is
         //       called, not when the page is initialized
@@ -167,19 +167,19 @@
                 return false;
             });
             var divid = jsfacetargs[0];
-            if (jQuery('#'+divid).length) {
+            if ($('#'+divid).length) {
                 var $loadingDiv = $(DIV({id:'facetLoading'},
                                         facetLoadingMsg));
                 $loadingDiv.corner();
-                $(jQuery('#'+divid).get(0).parentNode).append($loadingDiv);
-           }
+                $($('#'+divid).get(0).parentNode).append($loadingDiv);
+            }
             form.find('div.facet').each(function() {
-                var facet = jQuery(this);
+                var facet = $(this);
                 facet.find('div.facetCheckBox').each(function(i) {
                     this.setAttribute('cubicweb:idx', i);
                 });
                 facet.find('div.facetCheckBox').click(function() {
-                    var $this = jQuery(this);
+                    var $this = $(this);
                     // NOTE : add test on the facet operator (i.e. OR, AND)
                     // if ($this.hasClass('facetValueDisabled')){
                     //          return
@@ -189,23 +189,22 @@
                         $this.find('img').each(function(i) {
                             if (this.getAttribute('cubicweb:unselimg')) {
                                 this.setAttribute('src', UNSELECTED_BORDER_IMG);
-                                this.setAttribute('alt', (_("not selected")));
                             }
                             else {
                                 this.setAttribute('src', UNSELECTED_IMG);
-                                this.setAttribute('alt', (_("not selected")));
                             }
+                            this.setAttribute('alt', (_("not selected")));
                         });
                         var index = parseInt($this.attr('cubicweb:idx'));
                         // we dont need to move the element when cubicweb:idx == 0
                         if (index > 0) {
-                            var shift = jQuery.grep(facet.find('.facetValueSelected'), function(n) {
+                            var shift = $.grep(facet.find('.facetValueSelected'), function(n) {
                                 var nindex = parseInt(n.getAttribute('cubicweb:idx'));
                                 return nindex > index;
                             }).length;
                             index += shift;
                             var parent = this.parentNode;
-                            var $insertAfter = jQuery(parent).find('.facetCheckBox:nth(' + index + ')');
+                            var $insertAfter = $(parent).find('.facetCheckBox:nth(' + index + ')');
                             if (! ($insertAfter.length == 1 && shift == 0)) {
                                 // only rearrange element if necessary
                                 $insertAfter.after(this);
@@ -217,10 +216,10 @@
                             lastSelected.after(this);
                         } else {
                             var parent = this.parentNode;
-                            jQuery(parent).prepend(this);
+                            $(parent).prepend(this);
                         }
-                        jQuery(this).addClass('facetValueSelected');
-                        var $img = jQuery(this).find('img');
+                        $(this).addClass('facetValueSelected');
+                        var $img = $(this).find('img');
                         $img.attr('src', SELECTED_IMG).attr('alt', (_("selected")));
                     }
                     buildRQL.apply(null, jsfacetargs);
@@ -237,7 +236,7 @@
                 });
                 facet.find('div.facetTitle.hideFacetBody').click(function() {
                     facet.find('div.facetBody').toggleClass('hidden').toggleClass('opened');
-                    jQuery(this).toggleClass('opened');
+                    $(this).toggleClass('opened');
                 });
 
             });
@@ -250,20 +249,20 @@
 // persistent search (eg crih)
 function reorderFacetsItems(root) {
     root = root || document;
-    jQuery(root).find('form').each(function() {
-        var form = jQuery(this);
+    $(root).find('form').each(function() {
+        var form = $(this);
         if (form.attr('cubicweb:facetargs')) {
             form.find('div.facet').each(function() {
-                var facet = jQuery(this);
+                var facet = $(this);
                 var lastSelected = null;
                 facet.find('div.facetCheckBox').each(function(i) {
-                    var $this = jQuery(this);
+                    var $this = $(this);
                     if ($this.hasClass('facetValueSelected')) {
                         if (lastSelected) {
                             lastSelected.after(this);
                         } else {
                             var parent = this.parentNode;
-                            jQuery(parent).prepend(this);
+                            $(parent).prepend(this);
                         }
                         lastSelected = $this;
                     }
@@ -290,18 +289,18 @@
 // argument or without any argument. If we use `initFacetBoxEvents` as the
 // direct callback on the jQuery.ready event, jQuery will pass some argument of
 // his, so we use this small anonymous function instead.
-jQuery(document).ready(function() {
+$(document).ready(function() {
     initFacetBoxEvents();
-    jQuery(cw).bind('facets-content-loaded', onFacetContentLoaded);
-    jQuery(cw).bind('facets-content-loading', onFacetFiltering);
-    jQuery(cw).bind('facets-content-loading', updateFacetTitles);
+    $(cw).bind('facets-content-loaded', onFacetContentLoaded);
+    $(cw).bind('facets-content-loading', onFacetFiltering);
+    $(cw).bind('facets-content-loading', updateFacetTitles);
 });
 
 function showFacetLoading(parentid) {
     var loadingWidth = 200; // px
     var loadingHeight = 100; // px
-    var $msg = jQuery('#facetLoading');
-    var $parent = jQuery('#' + parentid);
+    var $msg = $('#facetLoading');
+    var $parent = $('#' + parentid);
     var leftPos = $parent.offset().left + ($parent.width() - loadingWidth) / 2;
     $parent.fadeTo('normal', 0.2);
     $msg.css('left', leftPos).show();
@@ -312,11 +311,11 @@
 }
 
 function onFacetContentLoaded(event, divid, rql, vid, extraparams) {
-    jQuery('#facetLoading').hide();
+    $('#facetLoading').hide();
 }
 
-jQuery(document).ready(function () {
-    if (jQuery('div.facetBody').length) {
+$(document).ready(function () {
+    if ($('div.facetBody').length) {
         var $loadingDiv = $(DIV({id:'facetLoading'},
                                 facetLoadingMsg));
         $loadingDiv.corner();
--- a/web/facet.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/web/facet.py	Tue Mar 12 12:08:22 2013 +0100
@@ -49,6 +49,7 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+from functools import reduce
 from warnings import warn
 from copy import deepcopy
 from datetime import datetime, timedelta
--- a/web/formwidgets.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/web/formwidgets.py	Tue Mar 12 12:08:22 2013 +0100
@@ -94,6 +94,7 @@
 """
 __docformat__ = "restructuredtext en"
 
+from functools import reduce
 from datetime import date
 from warnings import warn
 
--- a/web/test/unittest_views_json.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/web/test/unittest_views_json.py	Tue Mar 12 12:08:22 2013 +0100
@@ -35,14 +35,14 @@
         rset = req.execute('Any GN,COUNT(X) GROUPBY GN ORDERBY GN WHERE X in_group G, G name GN')
         data = self.view('jsonexport', rset)
         self.assertEqual(req.headers_out.getRawHeaders('content-type'), ['application/json'])
-        self.assertEqual(data, '[["guests", 1], ["managers", 1]]')
+        self.assertListEqual(data, [["guests", 1], ["managers", 1]])
 
     def test_json_rsetexport_empty_rset(self):
         req = self.request()
         rset = req.execute('Any X WHERE X is CWUser, X login "foobarbaz"')
         data = self.view('jsonexport', rset)
         self.assertEqual(req.headers_out.getRawHeaders('content-type'), ['application/json'])
-        self.assertEqual(data, '[]')
+        self.assertListEqual(data, [])
 
     def test_json_rsetexport_with_jsonp(self):
         req = self.request()
@@ -68,7 +68,7 @@
     def test_json_ersetexport(self):
         req = self.request()
         rset = req.execute('Any G ORDERBY GN WHERE G is CWGroup, G name GN')
-        data = json.loads(self.view('ejsonexport', rset))
+        data = self.view('ejsonexport', rset)
         self.assertEqual(req.headers_out.getRawHeaders('content-type'), ['application/json'])
         self.assertEqual(data[0]['name'], 'guests')
         self.assertEqual(data[1]['name'], 'managers')
--- a/web/views/basetemplates.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/web/views/basetemplates.py	Tue Mar 12 12:08:22 2013 +0100
@@ -498,7 +498,7 @@
         if config['auth-mode'] != 'http':
             self.login_form(id) # Cookie authentication
         w(u'</div>')
-        if self._cw.https and config.anonymous_user()[0]:
+        if self._cw.https and config.anonymous_user()[0] and config['https-deny-anonymous']:
             path = xml_escape(config['base-url'] + self._cw.relative_path())
             w(u'<div class="loginMessage"><a href="%s">%s</a></div>\n'
               % (path, self._cw._('No account? Try public access at %s') % path))
--- a/web/views/massmailing.py	Tue Mar 12 12:04:51 2013 +0100
+++ b/web/views/massmailing.py	Tue Mar 12 12:08:22 2013 +0100
@@ -21,6 +21,7 @@
 _ = unicode
 
 import operator
+from functools import reduce
 
 from cubicweb.predicates import (is_instance, authenticated_user,
                                 adaptable, match_form_params)