# HG changeset patch # User Pierre-Yves David # Date 1363086502 -3600 # Node ID 29e19ca141fcade615a9a6355e3029241d6240d9 # Parent 1beab80aed23cce168d4fc558c811f58106d411d# Parent 539ed3fb27cb2b8dcf816250cafb5ff087c244c0 merge with another default heads diff -r 1beab80aed23 -r 29e19ca141fc cwvreg.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 diff -r 1beab80aed23 -r 29e19ca141fc dbapi.py --- 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) diff -r 1beab80aed23 -r 29e19ca141fc devtools/httptest.py --- 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() diff -r 1beab80aed23 -r 29e19ca141fc devtools/testlib.py --- 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 ######################## diff -r 1beab80aed23 -r 29e19ca141fc doc/book/en/additionnal_services/index.rst --- /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 + + diff -r 1beab80aed23 -r 29e19ca141fc doc/book/en/additionnal_services/undo.rst --- /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). diff -r 1beab80aed23 -r 29e19ca141fc doc/book/en/devrepo/migration.rst --- 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. diff -r 1beab80aed23 -r 29e19ca141fc doc/book/en/images/undo_history-view_w600.png Binary file doc/book/en/images/undo_history-view_w600.png has changed diff -r 1beab80aed23 -r 29e19ca141fc doc/book/en/images/undo_mesage_w600.png Binary file doc/book/en/images/undo_mesage_w600.png has changed diff -r 1beab80aed23 -r 29e19ca141fc doc/book/en/images/undo_startup-link_w600.png Binary file doc/book/en/images/undo_startup-link_w600.png has changed diff -r 1beab80aed23 -r 29e19ca141fc doc/book/en/index.rst --- 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: diff -r 1beab80aed23 -r 29e19ca141fc entity.py --- 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) diff -r 1beab80aed23 -r 29e19ca141fc etwist/server.py --- 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='
%s
' % errorstream.getvalue(), + trace = traceback.format_exc() + return HTTPResponse(stream='
%s
' % xml_escape(trace), code=500, twisted_request=request) def _render_request(self, request): diff -r 1beab80aed23 -r 29e19ca141fc server/repository.py --- 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? diff -r 1beab80aed23 -r 29e19ca141fc server/serverconfig.py --- 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() diff -r 1beab80aed23 -r 29e19ca141fc server/sources/ldapfeed.py --- 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 . """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') diff -r 1beab80aed23 -r 29e19ca141fc server/sources/ldapuser.py --- 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']: diff -r 1beab80aed23 -r 29e19ca141fc server/sources/native.py --- 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 diff -r 1beab80aed23 -r 29e19ca141fc server/sources/remoterql.py --- 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'] diff -r 1beab80aed23 -r 29e19ca141fc web/data/cubicweb.facets.js --- 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(); diff -r 1beab80aed23 -r 29e19ca141fc web/facet.py --- 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 diff -r 1beab80aed23 -r 29e19ca141fc web/formwidgets.py --- 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 diff -r 1beab80aed23 -r 29e19ca141fc web/test/unittest_views_json.py --- 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') diff -r 1beab80aed23 -r 29e19ca141fc web/views/basetemplates.py --- 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'') - 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'\n' % (path, self._cw._('No account? Try public access at %s') % path)) diff -r 1beab80aed23 -r 29e19ca141fc web/views/massmailing.py --- 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)