# HG changeset patch # User Sylvain Thénault # Date 1284656195 -7200 # Node ID 42079f752a9c5232c6751eee07d5ed9cfc70ac43 # Parent c4a70a5dd1447e4c3d9a8427d6b394f5dc1c0b43# Parent 7bbdc3b6e9efeeeb890d79c1b2f5efa1634cfcaa backport stable into default diff -r c4a70a5dd144 -r 42079f752a9c .hgtags --- a/.hgtags Tue Sep 14 08:48:44 2010 +0200 +++ b/.hgtags Thu Sep 16 18:56:35 2010 +0200 @@ -151,3 +151,5 @@ 12038ca95f0fff2205f7ee029f5602d192118aec cubicweb-debian-version-3.9.5-1 d37428222a6325583be958d7c7fe7c595115663d cubicweb-version-3.9.6 7d2cab567735a17cab391c1a7f1bbe39118308a2 cubicweb-debian-version-3.9.6-1 +de588e756f4fbe9c53c72159c6b96580a36d3fa6 cubicweb-version-3.9.7 +1c01f9dffd64d507863c9f8f68e3585b7aa24374 cubicweb-debian-version-3.9.7-1 diff -r c4a70a5dd144 -r 42079f752a9c __pkginfo__.py --- a/__pkginfo__.py Tue Sep 14 08:48:44 2010 +0200 +++ b/__pkginfo__.py Thu Sep 16 18:56:35 2010 +0200 @@ -22,7 +22,7 @@ modname = distname = "cubicweb" -numversion = (3, 9, 6) +numversion = (3, 9, 7) version = '.'.join(str(num) for num in numversion) description = "a repository of entities / relations for knowledge management" @@ -57,7 +57,7 @@ } __recommends__ = { - 'Pyro': '>= 3.9.1', + 'Pyro': '>= 3.9.1, < 4.0.0', 'PIL': '', # for captcha 'pycrypto': '', # for crypto extensions 'fyzz': '>= 0.1.0', # for sparql diff -r c4a70a5dd144 -r 42079f752a9c dbapi.py --- a/dbapi.py Tue Sep 14 08:48:44 2010 +0200 +++ b/dbapi.py Thu Sep 16 18:56:35 2010 +0200 @@ -457,6 +457,12 @@ time() - tstart, clock() - cstart)) return rset +def check_not_closed(func): + def decorator(self, *args, **kwargs): + if self._closed is not None: + raise ProgrammingError('Closed connection') + return func(self, *args, **kwargs) + return decorator class Connection(object): """DB-API 2.0 compatible Connection object for CubicWeb @@ -497,59 +503,15 @@ self.rollback() return False #propagate the exception - def _txid(self, cursor=None): # XXX could now handle various isolation level! - # return a dict as bw compat trick - return {'txid': currentThread().getName()} - - def request(self): - return DBAPIRequest(self.vreg, DBAPISession(self)) - - def check(self): - """raise `BadConnectionId` if the connection is no more valid, else - return its latest activity timestamp. - """ - if self._closed is not None: - raise ProgrammingError('Closed connection') - return self._repo.check_session(self.sessionid) - - def set_session_props(self, **props): - """raise `BadConnectionId` if the connection is no more valid""" - if self._closed is not None: - raise ProgrammingError('Closed connection') - self._repo.set_session_props(self.sessionid, props) - - def get_shared_data(self, key, default=None, pop=False, txdata=False): - """return value associated to key in the session's data dictionary or - session's transaction's data if `txdata` is true. - - If pop is True, value will be removed from the dictionnary. + def __del__(self): + """close the remote connection if necessary""" + if self._closed is None and self._close_on_del: + try: + self.close() + except: + pass - If key isn't defined in the dictionnary, value specified by the - `default` argument will be returned. - """ - if self._closed is not None: - raise ProgrammingError('Closed connection') - return self._repo.get_shared_data(self.sessionid, key, default, pop, txdata) - - def set_shared_data(self, key, value, txdata=False): - """set value associated to `key` in shared data - - if `txdata` is true, the value will be added to the repository session's - transaction's data which are cleared on commit/rollback of the current - transaction. - """ - if self._closed is not None: - raise ProgrammingError('Closed connection') - return self._repo.set_shared_data(self.sessionid, key, value, txdata) - - def get_schema(self): - """Return the schema currently used by the repository. - - This is NOT part of the DB-API. - """ - if self._closed is not None: - raise ProgrammingError('Closed connection') - return self._repo.get_schema() + # connection initialization methods ######################################## def load_appobjects(self, cubes=_MARKER, subpath=None, expand=True): config = self.vreg.config @@ -605,20 +567,18 @@ if sitetitle is not None: self.vreg['propertydefs']['ui.site-title'] = {'default': sitetitle} + @check_not_closed def source_defs(self): """Return the definition of sources used by the repository. This is NOT part of the DB-API. """ - if self._closed is not None: - raise ProgrammingError('Closed connection') return self._repo.source_defs() + @check_not_closed def user(self, req=None, props=None): """return the User object associated to this connection""" # cnx validity is checked by the call to .user_info - if self._closed is not None: - raise ProgrammingError('Closed connection') eid, login, groups, properties = self._repo.user_info(self.sessionid, props) if req is None: @@ -634,19 +594,103 @@ user.cw_attr_cache['login'] = login # cache login return user - def __del__(self): - """close the remote connection if necessary""" - if self._closed is None and self._close_on_del: - try: - self.close() - except: - pass + @check_not_closed + def check(self): + """raise `BadConnectionId` if the connection is no more valid, else + return its latest activity timestamp. + """ + self._repo.check_session(self.sessionid) + + def _txid(self, cursor=None): # XXX could now handle various isolation level! + # return a dict as bw compat trick + return {'txid': currentThread().getName()} + + def request(self): + return DBAPIRequest(self.vreg, DBAPISession(self)) + + # session data methods ##################################################### + + @check_not_closed + def set_session_props(self, **props): + """raise `BadConnectionId` if the connection is no more valid""" + self._repo.set_session_props(self.sessionid, props) + + @check_not_closed + def get_shared_data(self, key, default=None, pop=False, txdata=False): + """return value associated to key in the session's data dictionary or + session's transaction's data if `txdata` is true. + + If pop is True, value will be removed from the dictionnary. + If key isn't defined in the dictionnary, value specified by the + `default` argument will be returned. + """ + return self._repo.get_shared_data(self.sessionid, key, default, pop, txdata) + + @check_not_closed + def set_shared_data(self, key, value, txdata=False): + """set value associated to `key` in shared data + + if `txdata` is true, the value will be added to the repository + session's query data which are cleared on commit/rollback of the current + transaction. + """ + return self._repo.set_shared_data(self.sessionid, key, value, txdata) + + # meta-data accessors ###################################################### + + @check_not_closed + def get_schema(self): + """Return the schema currently used by the repository.""" + return self._repo.get_schema() + + @check_not_closed + def get_option_value(self, option): + """return the value for `option` in the repository configuration.""" + return self._repo.get_option_value(option) + + @check_not_closed def describe(self, eid): - if self._closed is not None: - raise ProgrammingError('Closed connection') return self._repo.describe(self.sessionid, eid, **self._txid()) + # db-api like interface #################################################### + + @check_not_closed + def commit(self): + """Commit pending transaction for this connection to the repository. + + may raises `Unauthorized` or `ValidationError` if we attempted to do + something we're not allowed to for security or integrity reason. + + If the transaction is undoable, a transaction id will be returned. + """ + return self._repo.commit(self.sessionid, **self._txid()) + + @check_not_closed + def rollback(self): + """This method is optional since not all databases provide transaction + support. + + In case a database does provide transactions this method causes the the + database to roll back to the start of any pending transaction. Closing + a connection without committing the changes first will cause an implicit + rollback to be performed. + """ + self._repo.rollback(self.sessionid, **self._txid()) + + @check_not_closed + def cursor(self, req=None): + """Return a new Cursor Object using the connection. + + On pyro connection, you should get cursor after calling if + load_appobjects method if desired (which you should call if you intend + to use ORM abilities). + """ + if req is None: + req = self.request() + return self.cursor_class(self, self._repo, req=req) + + @check_not_closed def close(self): """Close the connection now (rather than whenever __del__ is called). @@ -656,52 +700,13 @@ connection. Note that closing a connection without committing the changes first will cause an implicit rollback to be performed. """ - if self._closed: - raise ProgrammingError('Connection is already closed') self._repo.close(self.sessionid, **self._txid()) del self._repo # necessary for proper garbage collection self._closed = 1 - def commit(self): - """Commit pending transaction for this connection to the repository. - - may raises `Unauthorized` or `ValidationError` if we attempted to do - something we're not allowed to for security or integrity reason. - - If the transaction is undoable, a transaction id will be returned. - """ - if not self._closed is None: - raise ProgrammingError('Connection is already closed') - return self._repo.commit(self.sessionid, **self._txid()) - - def rollback(self): - """This method is optional since not all databases provide transaction - support. - - In case a database does provide transactions this method causes the the - database to roll back to the start of any pending transaction. Closing - a connection without committing the changes first will cause an implicit - rollback to be performed. - """ - if not self._closed is None: - raise ProgrammingError('Connection is already closed') - self._repo.rollback(self.sessionid, **self._txid()) - - def cursor(self, req=None): - """Return a new Cursor Object using the connection. - - On pyro connection, you should get cursor after calling if - load_appobjects method if desired (which you should call if you intend - to use ORM abilities). - """ - if self._closed is not None: - raise ProgrammingError('Can\'t get cursor on closed connection') - if req is None: - req = self.request() - return self.cursor_class(self, self._repo, req=req) - # undo support ############################################################ + @check_not_closed def undoable_transactions(self, ueid=None, req=None, **actionfilters): """Return a list of undoable transaction objects by the connection's user, ordered by descendant transaction time. @@ -734,6 +739,7 @@ txinfo.req = req return txinfos + @check_not_closed def transaction_info(self, txuuid, req=None): """Return transaction object for the given uid. @@ -748,6 +754,7 @@ txinfo.req = req return txinfo + @check_not_closed def transaction_actions(self, txuuid, public=True): """Return an ordered list of action effectued during that transaction. @@ -761,6 +768,7 @@ return self._repo.transaction_actions(self.sessionid, txuuid, public, **self._txid()) + @check_not_closed def undo_transaction(self, txuuid): """Undo the given transaction. Return potential restoration errors. diff -r c4a70a5dd144 -r 42079f752a9c debian/changelog --- a/debian/changelog Tue Sep 14 08:48:44 2010 +0200 +++ b/debian/changelog Thu Sep 16 18:56:35 2010 +0200 @@ -1,3 +1,9 @@ +cubicweb (3.9.7-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault Thu, 16 Sep 2010 15:41:51 +0200 + cubicweb (3.9.6-1) unstable; urgency=low * new upstream release diff -r c4a70a5dd144 -r 42079f752a9c doc/book/README --- a/doc/book/README Tue Sep 14 08:48:44 2010 +0200 +++ b/doc/book/README Thu Sep 16 18:56:35 2010 +0200 @@ -46,9 +46,26 @@ .. [foot note] the foot note content +Cross references +================ -XXX -* lien vers cw.cwconfig.CW_CUBES_PATH par ex. +To arbitrary section +-------------------- + +:ref:`identifier` ou :ref:`label ` + +Label required of referencing node which as no title, else the node's title will be used. -automodule, autofunction, automethod, autofunction +To API objects +-------------- +See the autodoc sphinx extension documentation. Quick overview: + +* ref to a class: :class:`cubicweb.devtools.testlib.AutomaticWebTest` + +* if you can to see only the class name in the generated documentation, add a ~: + :class:`~cubicweb.devtools.testlib.AutomaticWebTest` + +* you can also use :mod: (module), :exc: (exception), :func: (function), :meth: (method)... + +* syntax explained above to specify label explicitly may also be used diff -r c4a70a5dd144 -r 42079f752a9c doc/book/en/devrepo/migration.rst --- a/doc/book/en/devrepo/migration.rst Tue Sep 14 08:48:44 2010 +0200 +++ b/doc/book/en/devrepo/migration.rst Thu Sep 16 18:56:35 2010 +0200 @@ -91,6 +91,24 @@ * `session`, repository session object +New cube dependencies +--------------------- + +If your code depends on some new cubes, you have to add them in a migration +script by using: + +* `add_cube(cube, update_database=True)`, add a cube. +* `add_cubes(cubes, update_database=True)`, add a list of cubes. + +The `update_database` parameter is telling if the database schema +should be updated or if only the relevant persistent property should be +inserted (for the case where a new cube has been extracted from an +existing one, so the new cube schema is actually already in there). + +If some of the added cubes are already used by an instance, they'll simply be +silently skipped. + + Schema migration ---------------- The following functions for schema migration are available in `repository` diff -r c4a70a5dd144 -r 42079f752a9c doc/book/en/devrepo/testing.rst --- a/doc/book/en/devrepo/testing.rst Tue Sep 14 08:48:44 2010 +0200 +++ b/doc/book/en/devrepo/testing.rst Thu Sep 16 18:56:35 2010 +0200 @@ -184,15 +184,70 @@ mail = MAILBOX[1] self.assertEquals(mail.subject, '[data] yes') +Visible actions tests +````````````````````` + +It is easy to write unit tests to test actions which are visible to +user or to a category of users. Let's take an example in the +`conference cube`_. + +.. _`conference cube`: http://www.cubicweb.org/project/cubicweb-conference +.. sourcecode:: python + + class ConferenceActionsTC(CubicWebTC): + + def setup_database(self): + self.conf = self.create_entity('Conference', + title=u'my conf', + url_id=u'conf', + start_on=date(2010, 1, 27), + end_on = date(2010, 1, 29), + call_open=True, + reverse_is_chair_at=chair, + reverse_is_reviewer_at=reviewer) + + def test_admin(self): + req = self.request() + rset = req.execute('Any C WHERE C is Conference') + self.assertListEquals(self.pactions(req, rset), + [('workflow', workflow.WorkflowActions), + ('edit', confactions.ModifyAction), + ('managepermission', actions.ManagePermissionsAction), + ('addrelated', actions.AddRelatedActions), + ('delete', actions.DeleteAction), + ('generate_badge_action', badges.GenerateBadgeAction), + ('addtalkinconf', confactions.AddTalkInConferenceAction) + ]) + self.assertListEquals(self.action_submenu(req, rset, 'addrelated'), + [(u'add Track in_conf Conference object', + u'http://testing.fr/cubicweb/add/Track' + u'?__linkto=in_conf%%3A%(conf)s%%3Asubject&' + u'__redirectpath=conference%%2Fconf&' + u'__redirectvid=' % {'conf': self.conf.eid}), + ]) + +You just have to execute a rql query corresponding to the view you want to test, +and to compare the result of +:meth:`~cubicweb.devtools.testlib.CubicWebTC.pactions` with the list of actions +that must be visible in the interface. This is a list of tuples. The first +element is the action's `__regid__`, the second the action's class. + +To test actions in submenu, you just have to test the result of +:meth:`~cubicweb.devtools.testlib.CubicWebTC.action_submenu` method. The last +parameter of the method is the action's category. The result is a list of +tuples. The first element is the action's title, and the second element the +action's url. + + .. _automatic_views_tests: Automatic views testing ----------------------- -This is done automatically with the AutomaticWebTest class. At cube -creation time, the mycube/test/test_mycube.py file contains such a -test. The code here has to be uncommented to be usable, without -further modification. +This is done automatically with the :class:`cubicweb.devtools.testlib.AutomaticWebTest` +class. At cube creation time, the mycube/test/test_mycube.py file +contains such a test. The code here has to be uncommented to be +usable, without further modification. The ``auto_populate`` method uses a smart algorithm to create pseudo-random data in the database, thus enabling the views to be @@ -212,6 +267,11 @@ auto_populate cannot guess by itself; these must yield resultsets against which views may be selected. +.. warning:: + + Take care to not let the imported `AutomaticWebTest` in your test module + namespace, else both your subclass *and* this parent class will be run. + Testing on a real-life database ------------------------------- diff -r c4a70a5dd144 -r 42079f752a9c doc/book/en/devweb/views/reledit.rst --- a/doc/book/en/devweb/views/reledit.rst Tue Sep 14 08:48:44 2010 +0200 +++ b/doc/book/en/devweb/views/reledit.rst Thu Sep 16 18:56:35 2010 +0200 @@ -70,12 +70,23 @@ controlled using the reledit_ctrl rtag, defined in :mod:`cubicweb.web.uicfg`. -This rtag provides three control variables: +This rtag provides four control variables: -* ``default_value`` -* ``reload``, to specificy if edition of the relation entails a full page - reload, which defaults to False -* ``noedit``, to explicitly inhibit edition +* ``default_value``: alternative default value + The default value is what is shown when there is no value. +* ``reload``: boolean, eid (to reload to) or function taking subject + and returning bool/eid This is useful when editing a relation (or + attribute) that impacts the url or another parts of the current + displayed page. Defaults to false. +* ``rvid``: alternative view id (as str) for relation or composite + edition Default is 'incontext' or 'csv' depending on the + cardinality. They can also be statically changed by subclassing + ClickAndEditFormView and redefining _one_rvid (resp. _many_rvid). +* ``edit_target``: 'rtype' (to edit the relation) or 'related' (to + edit the related entity) This controls whether to edit the relation + or the target entity of the relation. Currently only one-to-one + relations support target entity edition. By default, the 'related' + option is taken whenever the relation is composite and one-to-one. Let's see how to use these controls. @@ -86,15 +97,13 @@ reledit_ctrl.tag_attribute(('Company', 'name'), {'reload': lambda x:x.eid, 'default_value': xml_escape(u'')}) - reledit_ctrl.tag_object_of(('*', 'boss', 'Person'), {'noedit': True}) + reledit_ctrl.tag_object_of(('*', 'boss', 'Person'), {'edit_target': 'related'}) The `default_value` needs to be an xml escaped unicode string. -The `noedit` attribute is convenient to programmatically disable some -relation edition on views that apply it systematically (the prime -example being the primary view). Here we use it to forbid changing the -`boss` relation from a `Person` side (as it could have unwanted -effects). +The `edit_target` tag on the `boss` relation being set to `related` will +ensure edition of the `Person` entity instead (using a standard +automatic form) of the association of Company and Person. Finally, the `reload` key accepts either a boolean, an eid or an unicode string representing an url. If an eid is provided, it will be diff -r c4a70a5dd144 -r 42079f752a9c entity.py --- a/entity.py Tue Sep 14 08:48:44 2010 +0200 +++ b/entity.py Thu Sep 16 18:56:35 2010 +0200 @@ -222,9 +222,10 @@ destcls._fetch_restrictions(var, varmaker, destcls.fetch_attrs, selection, orderby, restrictions, user, ordermethod, visited=visited) - orderterm = getattr(cls, ordermethod)(attr, var) - if orderterm: - orderby.append(orderterm) + if ordermethod is not None: + orderterm = getattr(cls, ordermethod)(attr, var) + if orderterm: + orderby.append(orderterm) return selection, orderby, restrictions @classmethod diff -r c4a70a5dd144 -r 42079f752a9c etwist/service.py --- a/etwist/service.py Tue Sep 14 08:48:44 2010 +0200 +++ b/etwist/service.py Thu Sep 16 18:56:35 2010 +0200 @@ -72,8 +72,9 @@ # create the site config = cwcfg.config_for(self.instance) config.init_log(force=True) + config.debugmode = False logger.info('starting cubicweb instance %s ', self.instance) - root_resource = CubicWebRootResource(config, False) + root_resource = CubicWebRootResource(config) website = server.Site(root_resource) # serve it via standard HTTP on port set in the configuration port = config['port'] or 8080 diff -r c4a70a5dd144 -r 42079f752a9c ext/rest.py --- a/ext/rest.py Tue Sep 14 08:48:44 2010 +0200 +++ b/ext/rest.py Thu Sep 16 18:56:35 2010 +0200 @@ -242,8 +242,14 @@ data = data.translate(ESC_CAR_TABLE) settings = {'input_encoding': encoding, 'output_encoding': 'unicode', 'warning_stream': StringIO(), + 'traceback': True, # don't sys.exit + 'stylesheet': None, # don't try to embed stylesheet (may cause + # obscure bug due to docutils computing + # relative path according to the directory + # used *at import time* # dunno what's the max, severe is 4, and we never want a crash - # (though try/except may be a better option...) + # (though try/except may be a better option...). May be the + # above traceback option will avoid this? 'halt_level': 10, } if context: diff -r c4a70a5dd144 -r 42079f752a9c hooks/syncschema.py --- a/hooks/syncschema.py Tue Sep 14 08:48:44 2010 +0200 +++ b/hooks/syncschema.py Thu Sep 16 18:56:35 2010 +0200 @@ -34,8 +34,8 @@ from cubicweb import ValidationError from cubicweb.selectors import is_instance -from cubicweb.schema import (META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS, - ETYPE_NAME_MAP, display_name) +from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES, + CONSTRAINTS, ETYPE_NAME_MAP, display_name) from cubicweb.server import hook, schemaserial as ss from cubicweb.server.sqlutils import SQL_PREFIX @@ -52,16 +52,9 @@ } # core entity and relation types which can't be removed -CORE_ETYPES = list(BASE_TYPES) + ['CWEType', 'CWRType', 'CWUser', 'CWGroup', - 'CWConstraint', 'CWAttribute', 'CWRelation'] -CORE_RTYPES = ['eid', 'creation_date', 'modification_date', 'cwuri', - 'login', 'upassword', 'name', - 'is', 'instanceof', 'owned_by', 'created_by', 'in_group', - 'relation_type', 'from_entity', 'to_entity', - 'constrainted_by', - 'read_permission', 'add_permission', - 'delete_permission', 'updated_permission', - ] +CORE_TYPES = BASE_TYPES | SCHEMA_TYPES | META_RTYPES | set( + ('CWUser', 'CWGroup','login', 'upassword', 'name', 'in_group')) + def get_constraints(session, entity): constraints = [] @@ -873,7 +866,7 @@ def __call__(self): # final entities can't be deleted, don't care about that name = self.entity.name - if name in CORE_ETYPES: + if name in CORE_TYPES: raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')}) # delete every entities of this type if not name in ETYPE_NAME_MAP: @@ -939,7 +932,7 @@ def __call__(self): name = self.entity.name - if name in CORE_RTYPES: + if name in CORE_TYPES: raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')}) # delete relation definitions using this relation type self._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s', diff -r c4a70a5dd144 -r 42079f752a9c i18n/en.po --- a/i18n/en.po Tue Sep 14 08:48:44 2010 +0200 +++ b/i18n/en.po Thu Sep 16 18:56:35 2010 +0200 @@ -5,7 +5,7 @@ msgstr "" "Project-Id-Version: 2.0\n" "POT-Creation-Date: 2006-01-12 17:35+CET\n" -"PO-Revision-Date: 2010-05-16 18:58+0200\n" +"PO-Revision-Date: 2010-09-15 14:55+0200\n" "Last-Translator: Sylvain Thenault \n" "Language-Team: English \n" "Language: en\n" @@ -317,6 +317,12 @@ msgid "CWRelation_plural" msgstr "Relations" +msgid "CWUniqueTogetherConstraint" +msgstr "" + +msgid "CWUniqueTogetherConstraint_plural" +msgstr "" + msgid "CWUser" msgstr "User" @@ -496,6 +502,9 @@ msgid "New CWRelation" msgstr "New relation" +msgid "New CWUniqueTogetherConstraint" +msgstr "" + msgid "New CWUser" msgstr "New user" @@ -686,6 +695,9 @@ msgid "This CWRelation" msgstr "This relation" +msgid "This CWUniqueTogetherConstraint" +msgstr "" + msgid "This CWUser" msgstr "This user" @@ -880,6 +892,9 @@ msgid "add CWRelation relation_type CWRType object" msgstr "relation definition" +msgid "add CWUniqueTogetherConstraint constraint_of CWEType object" +msgstr "" + msgid "add CWUser in_group CWGroup object" msgstr "user" @@ -1436,6 +1451,20 @@ msgid "constraint factory" msgstr "" +msgid "constraint_of" +msgstr "" + +msgctxt "CWUniqueTogetherConstraint" +msgid "constraint_of" +msgstr "" + +msgid "constraint_of_object" +msgstr "" + +msgctxt "CWEType" +msgid "constraint_of_object" +msgstr "" + msgid "constraints" msgstr "" @@ -1559,6 +1588,11 @@ msgid "creating CWRelation (CWRelation relation_type CWRType %(linkto)s)" msgstr "creating relation %(linkto)s" +msgid "" +"creating CWUniqueTogetherConstraint (CWUniqueTogetherConstraint " +"constraint_of CWEType %(linkto)s)" +msgstr "" + msgid "creating CWUser (CWUser in_group CWGroup %(linkto)s)" msgstr "creating a new user in group %(linkto)s" @@ -1800,6 +1834,9 @@ msgid "define how we get out from a sub-workflow" msgstr "" +msgid "defines a sql-level multicolumn unique index" +msgstr "" + msgid "" "defines what's the property is applied for. You must select this first to be " "able to set value" @@ -3116,9 +3153,27 @@ msgid "relation_type_object" msgstr "relation definitions" +msgid "relations" +msgstr "" + +msgctxt "CWUniqueTogetherConstraint" +msgid "relations" +msgstr "" + msgid "relations deleted" msgstr "" +msgid "relations_object" +msgstr "" + +msgctxt "CWAttribute" +msgid "relations_object" +msgstr "" + +msgctxt "CWRelation" +msgid "relations_object" +msgstr "" + msgid "relative url of the bookmarked page" msgstr "" @@ -3898,6 +3953,10 @@ msgid "view_index" msgstr "index" +#, python-format +msgid "violates unique_together constraints (%s)" +msgstr "violates unique_together constraints (%s)" + msgid "visible" msgstr "" diff -r c4a70a5dd144 -r 42079f752a9c i18n/es.po --- a/i18n/es.po Tue Sep 14 08:48:44 2010 +0200 +++ b/i18n/es.po Thu Sep 16 18:56:35 2010 +0200 @@ -326,6 +326,12 @@ msgid "CWRelation_plural" msgstr "Relaciones" +msgid "CWUniqueTogetherConstraint" +msgstr "" + +msgid "CWUniqueTogetherConstraint_plural" +msgstr "" + msgid "CWUser" msgstr "Usuario" @@ -517,6 +523,9 @@ msgid "New CWRelation" msgstr "Nueva definición de relación final" +msgid "New CWUniqueTogetherConstraint" +msgstr "" + msgid "New CWUser" msgstr "Agregar usuario" @@ -707,6 +716,9 @@ msgid "This CWRelation" msgstr "Esta definición de relación no final" +msgid "This CWUniqueTogetherConstraint" +msgstr "" + msgid "This CWUser" msgstr "Este usuario" @@ -922,6 +934,9 @@ msgid "add CWRelation relation_type CWRType object" msgstr "Definición de relación" +msgid "add CWUniqueTogetherConstraint constraint_of CWEType object" +msgstr "" + msgid "add CWUser in_group CWGroup object" msgstr "Usuario" @@ -1487,6 +1502,20 @@ msgid "constraint factory" msgstr "Fábrica de restricciones" +msgid "constraint_of" +msgstr "" + +msgctxt "CWUniqueTogetherConstraint" +msgid "constraint_of" +msgstr "" + +msgid "constraint_of_object" +msgstr "" + +msgctxt "CWEType" +msgid "constraint_of_object" +msgstr "" + msgid "constraints" msgstr "Restricciones" @@ -1619,6 +1648,11 @@ msgid "creating CWRelation (CWRelation relation_type CWRType %(linkto)s)" msgstr "Creación de la relación %(linkto)s" +msgid "" +"creating CWUniqueTogetherConstraint (CWUniqueTogetherConstraint " +"constraint_of CWEType %(linkto)s)" +msgstr "" + msgid "creating CWUser (CWUser in_group CWGroup %(linkto)s)" msgstr "Creación de un usuario para agregar al grupo %(linkto)s" @@ -1873,6 +1907,9 @@ msgid "define how we get out from a sub-workflow" msgstr "Define como salir de un sub-Workflow" +msgid "defines a sql-level multicolumn unique index" +msgstr "" + msgid "" "defines what's the property is applied for. You must select this first to be " "able to set value" @@ -3231,9 +3268,27 @@ msgid "relation_type_object" msgstr "Definición de Relaciones" +msgid "relations" +msgstr "" + +msgctxt "CWUniqueTogetherConstraint" +msgid "relations" +msgstr "" + msgid "relations deleted" msgstr "Relaciones Eliminadas" +msgid "relations_object" +msgstr "" + +msgctxt "CWAttribute" +msgid "relations_object" +msgstr "" + +msgctxt "CWRelation" +msgid "relations_object" +msgstr "" + msgid "relative url of the bookmarked page" msgstr "Url relativa de la página" @@ -4035,6 +4090,10 @@ msgid "view_index" msgstr "Inicio" +#, python-format +msgid "violates unique_together constraints (%s)" +msgstr "" + msgid "visible" msgstr "Visible" diff -r c4a70a5dd144 -r 42079f752a9c i18n/fr.po --- a/i18n/fr.po Tue Sep 14 08:48:44 2010 +0200 +++ b/i18n/fr.po Thu Sep 16 18:56:35 2010 +0200 @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: cubicweb 2.46.0\n" -"PO-Revision-Date: 2010-09-06 21:44+0200\n" +"PO-Revision-Date: 2010-09-15 15:12+0200\n" "Last-Translator: Logilab Team \n" "Language-Team: fr \n" "Language: \n" @@ -37,7 +37,7 @@ msgstr " de l'état %(fromstate)s vers l'état %(tostate)s\n" msgid " :" -msgstr "" +msgstr " :" #, python-format msgid "%(attr)s set to %(newvalue)s" @@ -324,6 +324,12 @@ msgid "CWRelation_plural" msgstr "Relations" +msgid "CWUniqueTogetherConstraint" +msgstr "Contrainte unique_together" + +msgid "CWUniqueTogetherConstraint_plural" +msgstr "Contraintes unique_together" + msgid "CWUser" msgstr "Utilisateur" @@ -515,6 +521,9 @@ msgid "New CWRelation" msgstr "Nouvelle définition de relation non finale" +msgid "New CWUniqueTogetherConstraint" +msgstr "Nouvelle contrainte unique_together" + msgid "New CWUser" msgstr "Nouvel utilisateur" @@ -705,6 +714,9 @@ msgid "This CWRelation" msgstr "Cette définition de relation" +msgid "This CWUniqueTogetherConstraint" +msgstr "Cette contrainte unique_together" + msgid "This CWUser" msgstr "Cet utilisateur" @@ -920,6 +932,9 @@ msgid "add CWRelation relation_type CWRType object" msgstr "définition de relation" +msgid "add CWUniqueTogetherConstraint constraint_of CWEType object" +msgstr "contrainte unique_together" + msgid "add CWUser in_group CWGroup object" msgstr "utilisateur" @@ -1487,6 +1502,20 @@ msgid "constraint factory" msgstr "fabrique de contraintes" +msgid "constraint_of" +msgstr "contrainte de" + +msgctxt "CWUniqueTogetherConstraint" +msgid "constraint_of" +msgstr "contrainte de" + +msgid "constraint_of_object" +msgstr "contraint par" + +msgctxt "CWEType" +msgid "constraint_of_object" +msgstr "contraint par" + msgid "constraints" msgstr "contraintes" @@ -1546,7 +1575,9 @@ msgstr "contexte où ce composant doit être affiché" msgid "context where this facet should be displayed, leave empty for both" -msgstr "contexte où cette facette doit être affichée. Laissez ce champ vide pour l'avoir dans les deux." +msgstr "" +"contexte où cette facette doit être affichée. Laissez ce champ vide pour " +"l'avoir dans les deux." msgid "control subject entity's relations order" msgstr "contrôle l'ordre des relations de l'entité sujet" @@ -1619,6 +1650,11 @@ msgid "creating CWRelation (CWRelation relation_type CWRType %(linkto)s)" msgstr "création relation %(linkto)s" +msgid "" +"creating CWUniqueTogetherConstraint (CWUniqueTogetherConstraint " +"constraint_of CWEType %(linkto)s)" +msgstr "création d'une contrainte unique_together sur %(linkto)s" + msgid "creating CWUser (CWUser in_group CWGroup %(linkto)s)" msgstr "création d'un utilisateur à rajouter au groupe %(linkto)s" @@ -1869,6 +1905,9 @@ msgid "define how we get out from a sub-workflow" msgstr "définit comment sortir d'un sous-workflow" +msgid "defines a sql-level multicolumn unique index" +msgstr "définit un index SQL unique sur plusieurs colonnes" + msgid "" "defines what's the property is applied for. You must select this first to be " "able to set value" @@ -3229,9 +3268,27 @@ msgid "relation_type_object" msgstr "définition" +msgid "relations" +msgstr "relations" + +msgctxt "CWUniqueTogetherConstraint" +msgid "relations" +msgstr "relations" + msgid "relations deleted" msgstr "relations supprimées" +msgid "relations_object" +msgstr "relations de" + +msgctxt "CWAttribute" +msgid "relations_object" +msgstr "contraint par" + +msgctxt "CWRelation" +msgid "relations_object" +msgstr "contraint par" + msgid "relative url of the bookmarked page" msgstr "url relative de la page" @@ -4032,6 +4089,10 @@ msgid "view_index" msgstr "accueil" +#, python-format +msgid "violates unique_together constraints (%s)" +msgstr "violation de contrainte unique_together (%s)" + msgid "visible" msgstr "visible" diff -r c4a70a5dd144 -r 42079f752a9c rtags.py --- a/rtags.py Tue Sep 14 08:48:44 2010 +0200 +++ b/rtags.py Thu Sep 16 18:56:35 2010 +0200 @@ -34,8 +34,6 @@ * ``tag_subject_of`` tag a relation in the subject's context * ``tag_object_of`` tag a relation in the object's context * ``tag_attribute`` shortcut for tag_subject_of - - """ __docformat__ = "restructuredtext en" @@ -212,4 +210,27 @@ _allowed_values = frozenset((True, False)) +class NoTargetRelationTagsDict(RelationTagsDict): + + @property + def name(self): + return self.__class__.name + + def tag_subject_of(self, key, tag): + subj, rtype, obj = key + if obj != '*': + self.warning('using explict target type in %s.tag_subject_of() ' + 'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)', + self.name, subj, rtype, subj, rtype, obj) + super(NoTargetRelationTagsDict, self).tag_subject_of((subj, rtype, '*'), tag) + + def tag_object_of(self, key, tag): + subj, rtype, obj = key + if subj != '*': + self.warning('using explict subject type in %s.tag_object_of() ' + 'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)', + self.name, rtype, obj, subj, rtype, obj) + super(NoTargetRelationTagsDict, self).tag_object_of(('*', rtype, obj), tag) + + set_log_methods(RelationTags, logging.getLogger('cubicweb.rtags')) diff -r c4a70a5dd144 -r 42079f752a9c schema.py --- a/schema.py Tue Sep 14 08:48:44 2010 +0200 +++ b/schema.py Thu Sep 16 18:56:35 2010 +0200 @@ -62,6 +62,8 @@ 'relation_type', 'from_entity', 'to_entity', 'constrained_by', 'cstrtype', 'constraint_of', 'relations', + 'read_permission', 'add_permission', + 'delete_permission', 'update_permission', )) WORKFLOW_TYPES = set(('Transition', 'State', 'TrInfo', 'Workflow', @@ -613,6 +615,7 @@ class BaseRQLConstraint(BaseConstraint): """base class for rql constraints """ + distinct_query = None def __init__(self, restriction, mainvars=None): self.restriction = normalize_expression(restriction) @@ -652,8 +655,12 @@ pass # this is a vocabulary constraint, not enforce XXX why? def __str__(self): - return '%s(Any %s WHERE %s)' % (self.__class__.__name__, self.mainvars, - self.restriction) + if self.distinct_query: + selop = 'Any' + else: + selop = 'DISTINCT Any' + return '%s(%s %s WHERE %s)' % (self.__class__.__name__, selop, + self.mainvars, self.restriction) def __repr__(self): return '<%s @%#x>' % (self.__str__(), id(self)) @@ -745,13 +752,14 @@ class RQLUniqueConstraint(RepoEnforcedRQLConstraintMixIn, BaseRQLConstraint): """the unique rql constraint check that the result of the query isn't - greater than one - """ - distinct_query = True + greater than one. - # XXX turns mainvars into a required argument in __init__, since we've no - # way to guess it correctly (eg if using S,O or U the constraint will - # always be satisfied since we've to use a DISTINCT query) + You *must* specify mainvars when instantiating the constraint since there is + no way to guess it correctly (e.g. if using S,O or U the constraint will + always be satisfied because we've to use a DISTINCT query). + """ + # XXX turns mainvars into a required argument in __init__ + distinct_query = True def match_condition(self, session, eidfrom, eidto): return len(self.exec_query(session, eidfrom, eidto)) <= 1 diff -r c4a70a5dd144 -r 42079f752a9c server/hook.py --- a/server/hook.py Tue Sep 14 08:48:44 2010 +0200 +++ b/server/hook.py Thu Sep 16 18:56:35 2010 +0200 @@ -248,7 +248,7 @@ from itertools import chain from logilab.common.decorators import classproperty -from logilab.common.deprecation import deprecated +from logilab.common.deprecation import deprecated, class_renamed from logilab.common.logging_ext import set_log_methods from cubicweb import RegistryNotFound @@ -480,15 +480,19 @@ set_log_methods(Hook, getLogger('cubicweb.hook')) -# base classes for relation propagation ######################################## +# abtract hooks for relation propagation ####################################### +# See example usage in hooks of the nosylist cube -class PropagateSubjectRelationHook(Hook): +class PropagateRelationHook(Hook): """propagate some `main_rtype` relation on entities linked as object of `subject_relations` or as subject of `object_relations` (the watched relations). This hook ensure that when one of the watched relation is added, the `main_rtype` relation is added to the target entity of the relation. + + You usually want to use the :class:`match_rtype_sets` selector on concret + classes. """ events = ('after_add_relation',) @@ -514,56 +518,77 @@ {'x': meid, 'e': seid}) -class PropagateSubjectRelationAddHook(Hook): - """propagate to entities at the end of watched relations when a `main_rtype` - relation is added +class PropagateRelationAddHook(Hook): + """Propagate to entities at the end of watched relations when a `main_rtype` + relation is added. + + `subject_relations` and `object_relations` attributes should be specified on + subclasses and are usually shared references with attributes of the same + name on :class:`PropagateRelationHook`. + + Because of those shared references, you can use `skip_subject_relations` and + `skip_object_relations` attributes when you don't want to propagate to + entities linked through some particular relations. """ events = ('after_add_relation',) - # to set in concrete class + # to set in concrete class (mandatory) subject_relations = None object_relations = None + # to set in concrete class (optionaly) + skip_subject_relations = () + skip_object_relations = () def __call__(self): eschema = self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0]) execute = self._cw.execute for rel in self.subject_relations: - if rel in eschema.subjrels: + if rel in eschema.subjrels and not rel in self.skip_subject_relations: execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' 'X %s R, NOT R %s P' % (self.rtype, rel, self.rtype), {'x': self.eidfrom, 'p': self.eidto}) for rel in self.object_relations: - if rel in eschema.objrels: + if rel in eschema.objrels and not rel in self.skip_object_relations: execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' 'R %s X, NOT R %s P' % (self.rtype, rel, self.rtype), {'x': self.eidfrom, 'p': self.eidto}) -class PropagateSubjectRelationDelHook(Hook): - """propagate to entities at the end of watched relations when a `main_rtype` - relation is deleted +class PropagateRelationDelHook(PropagateRelationAddHook): + """Propagate to entities at the end of watched relations when a `main_rtype` + relation is deleted. + + This is the opposite of the :class:`PropagateRelationAddHook`, see its + documentation for how to use this class. """ events = ('after_delete_relation',) - # to set in concrete class - subject_relations = None - object_relations = None - def __call__(self): eschema = self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0]) execute = self._cw.execute for rel in self.subject_relations: - if rel in eschema.subjrels: + if rel in eschema.subjrels and not rel in self.skip_subject_relations: execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' 'X %s R' % (self.rtype, rel), {'x': self.eidfrom, 'p': self.eidto}) for rel in self.object_relations: - if rel in eschema.objrels: + if rel in eschema.objrels and not rel in self.skip_object_relations: execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, ' 'R %s X' % (self.rtype, rel), {'x': self.eidfrom, 'p': self.eidto}) +PropagateSubjectRelationHook = class_renamed( + 'PropagateSubjectRelationHook', PropagateRelationHook, + '[3.9] PropagateSubjectRelationHook has been renamed to PropagateRelationHook') +PropagateSubjectRelationAddHook = class_renamed( + 'PropagateSubjectRelationAddHook', PropagateRelationAddHook, + '[3.9] PropagateSubjectRelationAddHook has been renamed to PropagateRelationAddHook') +PropagateSubjectRelationDelHook = class_renamed( + 'PropagateSubjectRelationDelHook', PropagateRelationDelHook, + '[3.9] PropagateSubjectRelationDelHook has been renamed to PropagateRelationDelHook') + + # abstract classes for operation ############################################### class Operation(object): diff -r c4a70a5dd144 -r 42079f752a9c server/repository.py --- a/server/repository.py Tue Sep 14 08:48:44 2010 +0200 +++ b/server/repository.py Thu Sep 16 18:56:35 2010 +0200 @@ -413,6 +413,11 @@ # public (dbapi) interface ################################################ def stats(self): # XXX restrict to managers session? + """Return a dictionary containing some statistics about the repository + resources usage. + + This is a public method, not requiring a session id. + """ results = {} querier = self.querier source = self.system_source @@ -435,8 +440,9 @@ return results def get_schema(self): - """return the instance schema. This is a public method, not - requiring a session id + """Return the instance schema. + + This is a public method, not requiring a session id. """ try: # necessary to support pickling used by pyro @@ -446,8 +452,9 @@ self.schema.__hashmode__ = None def get_cubes(self): - """return the list of cubes used by this instance. This is a - public method, not requiring a session id. + """Return the list of cubes used by this instance. + + This is a public method, not requiring a session id. """ versions = self.get_versions(not (self.config.creating or self.config.repairing @@ -457,11 +464,20 @@ cubes.remove('cubicweb') return cubes + def get_option_value(self, option): + """Return the value for `option` in the configuration. + + This is a public method, not requiring a session id. + """ + # XXX we may want to check we don't give sensible information + return self.config[option] + @cached def get_versions(self, checkversions=False): - """return the a dictionary containing cubes used by this instance - as key with their version as value, including cubicweb version. This is a - public method, not requiring a session id. + """Return the a dictionary containing cubes used by this instance + as key with their version as value, including cubicweb version. + + This is a public method, not requiring a session id. """ from logilab.common.changelog import Version vcconf = {} @@ -491,6 +507,11 @@ @cached def source_defs(self): + """Return the a dictionary containing source uris as value and a + dictionary describing each source as value. + + This is a public method, not requiring a session id. + """ sources = self.config.sources().copy() # remove manager information sources.pop('admin', None) @@ -502,7 +523,10 @@ return sources def properties(self): - """return a result set containing system wide properties""" + """Return a result set containing system wide properties. + + This is a public method, not requiring a session id. + """ session = self.internal_session() try: # don't use session.execute, we don't want rset.req set diff -r c4a70a5dd144 -r 42079f752a9c server/sources/native.py --- a/server/sources/native.py Tue Sep 14 08:48:44 2010 +0200 +++ b/server/sources/native.py Thu Sep 16 18:56:35 2010 +0200 @@ -677,6 +677,11 @@ etype = elements[0] rtypes = elements[1:] raise UniqueTogetherError(etype, rtypes) + mo = re.search('columns (.*) are not unique', arg) + if mo is not None: # sqlite in use + rtypes = [c.strip().lstrip('cw_') for c in mo.group(1).split(',')] + etype = '???' + raise UniqueTogetherError(etype, rtypes) raise return cursor diff -r c4a70a5dd144 -r 42079f752a9c server/test/data/schema.py --- a/server/test/data/schema.py Tue Sep 14 08:48:44 2010 +0200 +++ b/server/test/data/schema.py Thu Sep 16 18:56:35 2010 +0200 @@ -20,7 +20,8 @@ SubjectRelation, RichString, String, Int, Float, Boolean, Datetime) from yams.constraints import SizeConstraint -from cubicweb.schema import (WorkflowableEntityType, RQLConstraint, +from cubicweb.schema import (WorkflowableEntityType, + RQLConstraint, RQLUniqueConstraint, ERQLExpression, RRQLExpression) class Affaire(WorkflowableEntityType): @@ -94,7 +95,10 @@ migrated_from = SubjectRelation('Note') attachment = SubjectRelation('File') - inline1 = SubjectRelation('Affaire', inlined=True, cardinality='?*') + inline1 = SubjectRelation('Affaire', inlined=True, cardinality='?*', + constraints=[RQLUniqueConstraint('S type T, S inline1 A1, A1 todo_by C, ' + 'Y type T, Y inline1 A2, A2 todo_by C', + 'S,Y')]) todo_by = SubjectRelation('CWUser') class Personne(EntityType): diff -r c4a70a5dd144 -r 42079f752a9c server/test/unittest_repository.py --- a/server/test/unittest_repository.py Tue Sep 14 08:48:44 2010 +0200 +++ b/server/test/unittest_repository.py Thu Sep 16 18:56:35 2010 +0200 @@ -627,6 +627,18 @@ self.assertEquals(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp), ('after_add_relation', eidn, 'ecrit_par', eidp)]) + def test_unique_contraint(self): + req = self.request() + toto = req.create_entity('Personne', nom=u'toto') + a01 = req.create_entity('Affaire', ref=u'A01', todo_by=toto) + req.cnx.commit() + req = self.request() + req.create_entity('Note', type=u'todo', inline1=a01) + req.cnx.commit() + req = self.request() + req.create_entity('Note', type=u'todo', inline1=a01) + ex = self.assertRaises(ValidationError, req.cnx.commit) + self.assertEquals(ex.errors, {'inline1-subject': u'RQLUniqueConstraint S type T, S inline1 A1, A1 todo_by C, Y type T, Y inline1 A2, A2 todo_by C failed'}) if __name__ == '__main__': unittest_main() diff -r c4a70a5dd144 -r 42079f752a9c web/application.py --- a/web/application.py Tue Sep 14 08:48:44 2010 +0200 +++ b/web/application.py Thu Sep 16 18:56:35 2010 +0200 @@ -420,11 +420,12 @@ self.validation_error_handler(req, ex) except (Unauthorized, BadRQLQuery, RequestError), ex: self.error_handler(req, ex, tb=False) - except Exception, ex: + except BaseException, ex: self.error_handler(req, ex, tb=True) except: self.critical('Catch all triggered!!!') self.exception('this is what happened') + result = 'oops' finally: if req.cnx and not commited: try: diff -r c4a70a5dd144 -r 42079f752a9c web/component.py --- a/web/component.py Tue Sep 14 08:48:44 2010 +0200 +++ b/web/component.py Thu Sep 16 18:56:35 2010 +0200 @@ -27,6 +27,7 @@ from cubicweb import Unauthorized, role, tags from cubicweb.uilib import js, domid +from cubicweb.utils import json_dumps from cubicweb.view import ReloadableMixIn, Component from cubicweb.selectors import (no_cnx, paginated_rset, one_line_rset, non_final_entity, partial_relation_possible, @@ -106,17 +107,23 @@ if view is not None and hasattr(view, 'page_navigation_url'): url = view.page_navigation_url(self, path, params) elif path == 'json': - rql = params.pop('rql', self.cw_rset.printable_rql()) - # latest 'true' used for 'swap' mode - url = 'javascript: %s' % (js.replacePageChunk( - params.get('divid', 'pageContent'), rql, - params.pop('vid', None), params)) + url = self.ajax_page_url(**params) else: url = self._cw.build_url(path, **params) return url + def ajax_page_url(self, **params): + divid = params.setdefault('divid', 'pageContent') + params['rql'] = self.cw_rset.printable_rql() + return "javascript: $(%s).loadxhtml('json', %s, 'get', 'swap')" % ( + json_dumps('#'+divid), js.ajaxFuncArgs('view', params)) + def page_link(self, path, params, start, stop, content): url = xml_escape(self.page_url(path, params, start, stop)) + # XXX hack to avoid opening a new page containing the evaluation of the + # js expression on ajax call + if url.startswith('javascript:'): + url += '; noop();' if start == self.starting_from: return self.selected_page_link_templ % (url, content, content) return self.page_link_templ % (url, content, content) diff -r c4a70a5dd144 -r 42079f752a9c web/data/cubicweb.css --- a/web/data/cubicweb.css Tue Sep 14 08:48:44 2010 +0200 +++ b/web/data/cubicweb.css Thu Sep 16 18:56:35 2010 +0200 @@ -917,6 +917,14 @@ } /********************************/ +/* placement of alt. view icons */ +/********************************/ + +.otherView { + float: right; +} + +/********************************/ /* rest related classes */ /********************************/ @@ -928,6 +936,18 @@ margin-right: 1.5em; } +/******************************/ +/* reledit */ +/******************************/ + +.releditField { + display: inline; +} + +.releditForm { + display:none; +} + /********************************/ /* overwite other css here */ /********************************/ @@ -955,4 +975,3 @@ margin-left: 0.5em; vertical-align: bottom; } - diff -r c4a70a5dd144 -r 42079f752a9c web/data/cubicweb.edition.js --- a/web/data/cubicweb.edition.js Tue Sep 14 08:48:44 2010 +0200 +++ b/web/data/cubicweb.edition.js Thu Sep 16 18:56:35 2010 +0200 @@ -317,7 +317,7 @@ form.insertBefore(insertBefore).slideDown('fast'); updateInlinedEntitiesCounters(rtype, role); reorderTabindex(null, $(insertBefore).closest('form')[0]); - jQuery(CubicWeb).trigger('inlinedform-added', form); + jQuery(cw).trigger('inlinedform-added', form); // if the inlined form contains a file input, we must force // the form enctype to multipart/form-data if (form.find('input:file').length) { diff -r c4a70a5dd144 -r 42079f752a9c web/data/cubicweb.old.css --- a/web/data/cubicweb.old.css Tue Sep 14 08:48:44 2010 +0200 +++ b/web/data/cubicweb.old.css Thu Sep 16 18:56:35 2010 +0200 @@ -901,3 +901,24 @@ border-color:#edecd2 #cfceb7 #cfceb7 #edecd2; background: #fffff8 url("button.png") bottom left repeat-x; } + +/********************************/ +/* placement of alt. view icons */ +/********************************/ + +.otherView { + float: right; +} + + +/******************************/ +/* reledit */ +/******************************/ + +.releditField { + display: inline; +} + +.releditForm { + display:none; +} diff -r c4a70a5dd144 -r 42079f752a9c web/data/cubicweb.reledit.js --- a/web/data/cubicweb.reledit.js Tue Sep 14 08:48:44 2010 +0200 +++ b/web/data/cubicweb.reledit.js Thu Sep 16 18:56:35 2010 +0200 @@ -53,6 +53,7 @@ } } jQuery('#'+params.divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, params, 'post'); + jQuery(cw).trigger('reledit-reloaded', params); }, /* called by reledit forms to submit changes diff -r c4a70a5dd144 -r 42079f752a9c web/facet.py --- a/web/facet.py Tue Sep 14 08:48:44 2010 +0200 +++ b/web/facet.py Thu Sep 16 18:56:35 2010 +0200 @@ -515,6 +515,7 @@ def title(self): return display_name(self._cw, self.rtype, form=self.role) + @property def rql_sort(self): """return true if we can handle sorting in the rql query. E.g. if sortfunc is set or if we have not to transform the returned value (eg no diff -r c4a70a5dd144 -r 42079f752a9c web/formwidgets.py --- a/web/formwidgets.py Tue Sep 14 08:48:44 2010 +0200 +++ b/web/formwidgets.py Thu Sep 16 18:56:35 2010 +0200 @@ -653,10 +653,16 @@ timestr = req.form.get(field.input_name(form, 'time')).strip() or None if datestr is None: return None - date = todatetime(req.parse_datetime(datestr, 'Date')) + try: + date = todatetime(req.parse_datetime(datestr, 'Date')) + except ValueError, exc: + raise ProcessFormError(unicode(exc)) if timestr is None: return date - time = req.parse_datetime(timestr, 'Time') + try: + time = req.parse_datetime(timestr, 'Time') + except ValueError, exc: + raise ProcessFormError(unicode(exc)) return date.replace(hour=time.hour, minute=time.minute, second=time.second) diff -r c4a70a5dd144 -r 42079f752a9c web/request.py --- a/web/request.py Tue Sep 14 08:48:44 2010 +0200 +++ b/web/request.py Thu Sep 16 18:56:35 2010 +0200 @@ -99,9 +99,6 @@ # search state: 'normal' or 'linksearch' (eg searching for an object # to create a relation with another) self.search_state = ('normal',) - # tabindex generator - self.tabindexgen = count(1) - self.next_tabindex = self.tabindexgen.next # page id, set by htmlheader template self.pageid = None self._set_pageid() @@ -131,6 +128,13 @@ """ return self.set_varmaker() + def _get_tabindex_func(self): + nextfunc = self.get_page_data('nexttabfunc') + if nextfunc is None: + nextfunc = count(1).next + self.set_page_data('nexttabfunc', nextfunc) + return nextfunc + def set_varmaker(self): varmaker = self.get_page_data('rql_varmaker') if varmaker is None: @@ -143,6 +147,8 @@ or an anonymous connection is open """ super(CubicWebRequestBase, self).set_session(session, user) + # tabindex generator + self.next_tabindex = self._get_tabindex_func() # set request language vreg = self.vreg if self.user: diff -r c4a70a5dd144 -r 42079f752a9c web/test/data/schema.py --- a/web/test/data/schema.py Tue Sep 14 08:48:44 2010 +0200 +++ b/web/test/data/schema.py Thu Sep 16 18:56:35 2010 +0200 @@ -75,3 +75,19 @@ cp = String(maxsize=12) ville= String(maxsize=32) +# enough relations to cover most reledit use cases +class Project(EntityType): + title = String(maxsize=32, required=True, fulltextindexed=True) + long_desc = SubjectRelation('Blog', composite='subject', cardinality='?*') + manager = SubjectRelation('Personne', cardinality='?*') + +class composite_card11_2ttypes(RelationDefinition): + subject = 'Project' + object = ('File', 'Blog') + composite = 'subject' + cardinality = '?*' + +class Ticket(EntityType): + title = String(maxsize=32, required=True, fulltextindexed=True) + concerns = SubjectRelation('Project', composite='object') + diff -r c4a70a5dd144 -r 42079f752a9c web/test/unittest_reledit.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/test/unittest_reledit.py Thu Sep 16 18:56:35 2010 +0200 @@ -0,0 +1,223 @@ +# copyright 2003-2010 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 . +""" +mainly regression-preventing tests for reledit/doreledit views +""" + +from cubicweb.devtools.testlib import CubicWebTC +from cubicweb.web.uicfg import reledit_ctrl + +class ReleditMixinTC(object): + + def setup_database(self): + self.req = self.request() + self.proj = self.req.create_entity('Project', title=u'cubicweb-world-domination') + self.tick = self.req.create_entity('Ticket', title=u'write the code') + self.toto = self.req.create_entity('Personne', nom=u'Toto') + +class ClickAndEditFormTC(ReleditMixinTC, CubicWebTC): + + def test_default_config(self): + reledit = {'title': """
cubicweb-world-domination
""", + 'long_desc': """
<long_desc not specified>
""", + 'manager': """
<manager not specified>
""", + 'composite_card11_2ttypes': """<composite_card11_2ttypes not specified>""", + 'concerns': """<concerns_object not specified>"""} + + for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True): + if rschema not in reledit: + continue + rtype = rschema.type + self.assertTextEquals(reledit[rtype], self.proj.view('reledit', rtype=rtype, role=role), rtype) + + def test_default_forms(self): + doreledit = {'title': """
cubicweb-world-domination
+
+ + + + + + + + + + + + + + + +
+ + + +
+ +
+ + + + + + + +
+
+
""", + + 'long_desc': """
<long_desc not specified>
+
+ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+ +
+ +
+ +
+ + + + + + + +
+
+
""", + + 'manager': """
<manager not specified>
+
+ + + + + + + + + + + + + + + + + +
+ + + +
+ +
+ + + + + + + +
+
+
""", + 'composite_card11_2ttypes': """<composite_card11_2ttypes not specified>""", + 'concerns': """<concerns_object not specified>""" + } + for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True): + if rschema not in doreledit: + continue + rtype = rschema.type + self.assertTextEquals(doreledit[rtype], + self.proj.view('doreledit', rtype=rtype, role=role, + formid='edition' if rtype == 'long_desc' else 'base'), + rtype) + +class ClickAndEditFormUICFGTC(ReleditMixinTC, CubicWebTC): + + def setup_database(self): + super(ClickAndEditFormUICFGTC, self).setup_database() + self.tick.set_relations(concerns=self.proj) + self.proj.set_relations(manager=self.toto) + + def test_with_uicfg(self): + old_rctl = reledit_ctrl._tagdefs.copy() + reledit_ctrl.tag_attribute(('Project', 'title'), + {'default_value': '', 'reload': True}) + reledit_ctrl.tag_subject_of(('Project', 'long_desc', '*'), + {'reload': True, 'edit_target': 'rtype', + 'default_value': u'<long_desc is required>'}) + reledit_ctrl.tag_subject_of(('Project', 'manager', '*'), + {'edit_target': 'related'}) + reledit_ctrl.tag_subject_of(('Project', 'composite_card11_2ttypes', '*'), + {'edit_target': 'related'}) + reledit_ctrl.tag_object_of(('Ticket', 'concerns', 'Project'), + {'edit_target': 'rtype'}) + reledit = { + 'title': """<div id="title-subject-917-reledit" onmouseout="jQuery('#title-subject-917').addClass('hidden')" onmouseover="jQuery('#title-subject-917').removeClass('hidden')" class="releditField"><div id="title-subject-917-value" class="editableFieldValue">cubicweb-world-domination</div><div id="title-subject-917" class="editableField hidden"><div id="title-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', 917, 'title', 'subject', 'title-subject-917', true, '', '<title is required>');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""", + 'long_desc': """<div id="long_desc-subject-917-reledit" onmouseout="jQuery('#long_desc-subject-917').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-917').removeClass('hidden')" class="releditField"><div id="long_desc-subject-917-value" class="editableFieldValue"><long_desc is required></div><div id="long_desc-subject-917" class="editableField hidden"><div id="long_desc-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', 917, 'long_desc', 'subject', 'long_desc-subject-917', true, 'incontext', '<long_desc is required>');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""", + 'manager': """<div id="manager-subject-917-reledit" onmouseout="jQuery('#manager-subject-917').addClass('hidden')" onmouseover="jQuery('#manager-subject-917').removeClass('hidden')" class="releditField"><div id="manager-subject-917-value" class="editableFieldValue"><a href="http://testing.fr/cubicweb/personne/919" title="">Toto</a></div><div id="manager-subject-917" class="editableField hidden"><div id="manager-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('edition', 917, 'manager', 'subject', 'manager-subject-917', false, 'incontext', '&lt;manager not specified&gt;');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div><div id="manager-subject-917-delete" class="editableField" onclick="cw.reledit.loadInlineEditionForm('deleteconf', 917, 'manager', 'subject', 'manager-subject-917', false, 'incontext', '&lt;manager not specified&gt;');" title="click to delete this value"><img title="click to delete this value" src="data/cancel.png" alt="click to delete this value"/></div></div></div>""", + 'composite_card11_2ttypes': """<composite_card11_2ttypes not specified>""", + 'concerns': """<div id="concerns-object-917-reledit" onmouseout="jQuery('#concerns-object-917').addClass('hidden')" onmouseover="jQuery('#concerns-object-917').removeClass('hidden')" class="releditField"><div id="concerns-object-917-value" class="editableFieldValue"><a href="http://testing.fr/cubicweb/ticket/918" title="">write the code</a></div><div id="concerns-object-917" class="editableField hidden"><div id="concerns-object-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', 917, 'concerns', 'object', 'concerns-object-917', false, 'csv', '&lt;concerns_object not specified&gt;');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""" + } + for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True): + if rschema not in reledit: + continue + rtype = rschema.type + self.assertTextEquals(reledit[rtype], + self.proj.view('reledit', rtype=rtype, role=role), + rtype) + reledit_ctrl.clear() + reledit_ctrl._tagdefs.update(old_rctl) diff -r c4a70a5dd144 -r 42079f752a9c web/uicfg.py --- a/web/uicfg.py Tue Sep 14 08:48:44 2010 +0200 +++ b/web/uicfg.py Thu Sep 16 18:56:35 2010 +0200 @@ -53,7 +53,8 @@ from cubicweb import neg_role from cubicweb.rtags import (RelationTags, RelationTagsBool, RelationTagsSet, - RelationTagsDict, register_rtag, _ensure_str_key) + RelationTagsDict, NoTargetRelationTagsDict, + register_rtag, _ensure_str_key) from cubicweb.schema import META_RTYPES @@ -83,27 +84,11 @@ 'sideboxes', 'hidden'))) -class DisplayCtrlRelationTags(RelationTagsDict): +class DisplayCtrlRelationTags(NoTargetRelationTagsDict): def __init__(self, *args, **kwargs): super(DisplayCtrlRelationTags, self).__init__(*args, **kwargs) self.counter = 0 - def tag_subject_of(self, key, tag): - subj, rtype, obj = key - if obj != '*': - self.warning('using explict target type in display_ctrl.tag_subject_of() ' - 'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)', - subj, rtype, subj, rtype, obj) - super(DisplayCtrlRelationTags, self).tag_subject_of((subj, rtype, '*'), tag) - - def tag_object_of(self, key, tag): - subj, rtype, obj = key - if subj != '*': - self.warning('using explict subject type in display_ctrl.tag_object_of() ' - 'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)', - rtype, obj, subj, rtype, obj) - super(DisplayCtrlRelationTags, self).tag_object_of(('*', rtype, obj), tag) - def init_primaryview_display_ctrl(rtag, sschema, rschema, oschema, role): if role == 'subject': oschema = '*' @@ -378,7 +363,7 @@ autoform_field = RelationTags('autoform_field') # relations'field explicit kwargs (given to field's __init__) -autoform_field_kwargs = RelationTagsDict() +autoform_field_kwargs = RelationTagsDict('autoform_field_kwargs') # set of tags of the form <action>_on_new on relations. <action> is a @@ -386,31 +371,49 @@ # permissions checking is by-passed and supposed to be ok autoform_permissions_overrides = RelationTagsSet('autoform_permissions_overrides') -class _ReleditTags(RelationTagsDict): - _keys = frozenset('reload default_value noedit'.split()) - - def tag_subject_of(self, key, *args, **kwargs): - subj, rtype, obj = key - if obj != '*': - self.warning('using explict target type in display_ctrl.tag_subject_of() ' - 'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)', - subj, rtype, subj, rtype, obj) - super(_ReleditTags, self).tag_subject_of(key, *args, **kwargs) - - def tag_object_of(self, key, *args, **kwargs): - subj, rtype, obj = key - if subj != '*': - self.warning('using explict subject type in display_ctrl.tag_object_of() ' - 'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)', - rtype, obj, subj, rtype, obj) - super(_ReleditTags, self).tag_object_of(key, *args, **kwargs) +class ReleditTags(NoTargetRelationTagsDict): + """ + default_value: alternative default value + The default value is what is shown when there is no value. + reload: boolean, eid (to reload to) or function taking subject and returning bool/eid + This is useful when editing a relation (or attribute) that impacts the url + or another parts of the current displayed page. Defaults to False. + rvid: alternative view id (as str) for relation or composite edition + Default is 'incontext' or 'csv' depending on the cardinality. They can also be + statically changed by subclassing ClickAndEditFormView and redefining _one_rvid + (resp. _many_rvid). + edit_target: 'rtype' (to edit the relation) or 'related' (to edit the related entity) + This controls whether to edit the relation or the target entity of the relation. + Currently only one-to-one relations support target entity edition. By default, + the 'related' option is taken whenever the relation is composite and one-to-one. + """ + _keys = frozenset('default_value reload rvid edit_target'.split()) def tag_relation(self, key, tag): for tagkey in tag.iterkeys(): assert tagkey in self._keys, 'tag %r not in accepted tags: %r' % (tag, self._keys) - return super(_ReleditTags, self).tag_relation(key, tag) + return super(ReleditTags, self).tag_relation(key, tag) -reledit_ctrl = _ReleditTags('reledit') +def init_reledit_ctrl(rtag, sschema, rschema, oschema, role): + if rschema.final: + return + composite = rschema.rdef(sschema, oschema).composite == role + if role == 'subject': + oschema = '*' + else: + sschema = '*' + values = rtag.get(sschema, rschema, oschema, role) + edittarget = values.get('edit_target') + if edittarget not in (None, 'rtype', 'related'): + rtag.warning('reledit: wrong value for edit_target on relation %s: %s', + rschema, edittarget) + edittarget = None + if not edittarget: + edittarget = 'related' if composite else 'rtype' + rtag.tag_relation((sschema, rschema, oschema, role), + {'edit_target': edittarget}) + +reledit_ctrl = ReleditTags('reledit', init_reledit_ctrl) # boxes.EditBox configuration ################################################# diff -r c4a70a5dd144 -r 42079f752a9c web/views/__init__.py --- a/web/views/__init__.py Tue Sep 14 08:48:44 2010 +0200 +++ b/web/views/__init__.py Thu Sep 16 18:56:35 2010 +0200 @@ -84,7 +84,7 @@ return VID_BY_MIMETYPE[mimetype] nb_rows = len(rset) # empty resultset - if nb_rows == 0 : + if nb_rows == 0: return 'noresult' # entity result set if not schema.eschema(rset.description[0][0]).final: diff -r c4a70a5dd144 -r 42079f752a9c web/views/basecontrollers.py --- a/web/views/basecontrollers.py Tue Sep 14 08:48:44 2010 +0200 +++ b/web/views/basecontrollers.py Thu Sep 16 18:56:35 2010 +0200 @@ -260,9 +260,8 @@ def optional_kwargs(extraargs): if extraargs is None: return {} - else: # we receive unicode keys which is not supported by the **syntax - return dict((str(key), value) - for key, value in extraargs.items()) + # 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' @@ -334,6 +333,9 @@ 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: @@ -344,7 +346,7 @@ return None def _call_view(self, view, paginate=False, **kwargs): - divid = self._cw.form.get('divid', 'pageContent') + divid = self._cw.form.get('divid') # we need to call pagination before with the stream set try: stream = view.set_stream() @@ -352,23 +354,26 @@ 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 if paginate: - if divid == 'pageContent': - # mimick main template behaviour - stream.write(u'<div id="pageContent">') - vtitle = self._cw.form.get('vtitle') - if vtitle: - stream.write(u'<div class="vtitle">%s</div>\n' % vtitle) view.paginate() - if divid == 'pageContent': - stream.write(u'<div id="contentmain">') + 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 paginate and divid == 'pageContent': + if divid == 'pageContent': stream.write(u'</div></div>') return stream.getvalue() @@ -390,7 +395,7 @@ 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.get('paginate')) + return self._call_view(view, paginate=req.form.pop('paginate', False)) @xhtmlize def js_prop_widget(self, propkey, varname, tabindex=None): @@ -411,11 +416,6 @@ rset = self._exec(rql) else: rset = None - if extraargs is None: - extraargs = {} - else: # we receive unicode keys which is not supported by the **syntax - extraargs = dict((str(key), value) - for key, value in extraargs.items()) # 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 @@ -425,21 +425,22 @@ # error is expected and should'nt be reported. #try: comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset, - **extraargs) + **optional_kwargs(extraargs)) #except NoSelectableObject: # raise RemoteCallFailed('unselectable') return self._call_view(comp, **extraargs) @xhtmlize - def js_render(self, registry, oid, eid=None, selectargs=None, renderargs=None): + def js_render(self, registry, oid, eid=None, + selectargs=None, renderargs=None): if eid is not None: rset = self._cw.eid_rset(eid) elif self._cw.form.get('rql'): rset = self._cw.execute(self._cw.form['rql']) else: rset = None - selectargs = optional_kwargs(selectargs) - view = self._cw.vreg[registry].select(oid, self._cw, rset=rset, **selectargs) + view = self._cw.vreg[registry].select(oid, self._cw, rset=rset, + **optional_kwargs(selectargs)) return self._call_view(view, **optional_kwargs(renderargs)) @check_pageid diff -r c4a70a5dd144 -r 42079f752a9c web/views/forms.py --- a/web/views/forms.py Tue Sep 14 08:48:44 2010 +0200 +++ b/web/views/forms.py Thu Sep 16 18:56:35 2010 +0200 @@ -188,7 +188,7 @@ if self.formvalues is not None: return # already built self.formvalues = formvalues or {} - # use a copy in case fields are modified while context is build (eg + # use a copy in case fields are modified while context is built (eg # __linkto handling for instance) for field in self.fields[:]: for field in field.actual_fields(self): diff -r c4a70a5dd144 -r 42079f752a9c web/views/primary.py --- a/web/views/primary.py Tue Sep 14 08:48:44 2010 +0200 +++ b/web/views/primary.py Thu Sep 16 18:56:35 2010 +0200 @@ -24,7 +24,7 @@ from logilab.mtconverter import xml_escape -from cubicweb import Unauthorized +from cubicweb import Unauthorized, NoSelectableObject from cubicweb.utils import support_args from cubicweb.selectors import match_kwargs from cubicweb.view import EntityView @@ -161,13 +161,19 @@ def render_entity_relations(self, entity): for rschema, tschemas, role, dispctrl in self._section_def(entity, 'relations'): if rschema.final or dispctrl.get('rtypevid'): + vid = dispctrl.get('vid', 'reledit') + try: + rview = self._cw.vreg['views'].select( + vid, self._cw, rset=entity.cw_rset, row=entity.cw_row, + col=entity.cw_col, dispctrl=dispctrl, rtype=rschema, role=role) + except NoSelectableObject: + continue self.w(u'<div class="section">') label = self._rel_label(entity, rschema, role, dispctrl) if label: self.w(u'<h4>%s</h4>' % label) - vid = dispctrl.get('vid', 'reledit') - entity.view(vid, rtype=rschema.type, role=role, w=self.w, - initargs={'dispctrl': dispctrl}) + rview.render(row=entity.cw_row, col=entity.cw_col, w=self.w, + rtype=rschema.type, role=role) self.w(u'</div>') continue rset = self._relation_rset(entity, rschema, role, dispctrl) @@ -248,10 +254,7 @@ if section == where: matchtschemas.append(tschema) if matchtschemas: - # XXX pick the latest dispctrl - dispctrl = self.display_ctrl.etype_get(eschema, rschema, role, - matchtschemas[-1]) - + dispctrl = self.display_ctrl.etype_get(eschema, rschema, role, '*') rdefs.append( (rschema, matchtschemas, role, dispctrl) ) return sorted(rdefs, key=lambda x: x[-1]['order']) @@ -338,6 +341,28 @@ if url: self.w(u'<a href="%s">%s</a>' % (url, url)) +class AttributeView(EntityView): + """use this view on an entity as an alternative to more sophisticated + views such as reledit. + + Ex. usage: + + uicfg.primaryview_display_ctrl.tag_attribute(('Foo', 'bar'), {'vid': 'attribute'}) + """ + __regid__ = 'attribute' + __select__ = EntityView.__select__ & match_kwargs('rtype') + + def cell_call(self, row, col, rtype, **kwargs): + entity = self.cw_rset.get_entity(row, col) + if self._cw.vreg.schema.rschema(rtype).final: + self.w(entity.printable_value(rtype)) + else: + dispctrl = uicfg.primaryview_display_ctrl.etype_get( + entity.e_schema, rtype, kwargs['role'], '*') + rset = entity.related(rtype, role) + if rset: + self.wview('autolimited', rset, initargs={'dispctrl': dispctrl}) + ## default primary ui configuration ########################################### diff -r c4a70a5dd144 -r 42079f752a9c web/views/reledit.py --- a/web/views/reledit.py Tue Sep 14 08:48:44 2010 +0200 +++ b/web/views/reledit.py Thu Sep 16 18:56:35 2010 +0200 @@ -21,6 +21,7 @@ import copy from logilab.mtconverter import xml_escape +from logilab.common.deprecation import deprecated from cubicweb import neg_role from cubicweb.schema import display_name @@ -39,8 +40,8 @@ return u'' def append_field(self, *args): pass - def field_by_name(self, rtype, role, eschema=None): - return None + +rctrl = uicfg.reledit_ctrl class ClickAndEditFormView(EntityView): __regid__ = 'doreledit' @@ -60,8 +61,11 @@ _editzonemsg = _('click to edit this field') # default relation vids according to cardinality + # can be changed per rtype using reledit_ctrl rtag _one_rvid = 'incontext' _many_rvid = 'csv' + # renderer + _form_renderer_id = 'base' def cell_call(self, row, col, rtype=None, role='subject', reload=False, # controls reloading the whole page after change @@ -69,89 +73,98 @@ # function taking the subject entity & returning a boolean or an eid rvid=None, # vid to be applied to other side of rtype (non final relations only) default_value=None, - formid=None + formid='base' ): """display field to edit entity's `rtype` relation on click""" assert rtype assert role in ('subject', 'object'), '%s is not an acceptable role value' % role - if self.__regid__ == 'doreledit': - assert formid - self._cw.add_js('cubicweb.reledit.js') - if formid: - self._cw.add_js('cubicweb.edition.js') self._cw.add_css('cubicweb.form.css') + self._cw.add_js('cubicweb.reledit.js', 'cubicweb.edition.js') entity = self.cw_rset.get_entity(row, col) rschema = self._cw.vreg.schema[rtype] + self._rules = rctrl.etype_get(entity.e_schema.type, rschema.type, role, '*') reload = self._compute_reload(entity, rschema, role, reload) default_value = self._compute_default_value(entity, rschema, role, default_value) divid = self._build_divid(rtype, role, entity.eid) if rschema.final: - self._handle_attributes(entity, rschema, role, divid, reload, default_value) + self._handle_attribute(entity, rschema, role, divid, reload, default_value) else: - self._handle_relations(entity, rschema, role, divid, reload, default_value, formid) + if self._is_composite(): + self._handle_composite(entity, rschema, role, divid, reload, default_value, formid) + else: + self._handle_relation(entity, rschema, role, divid, reload, default_value, formid) - def _handle_attributes(self, entity, rschema, role, divid, reload, default_value): + def _handle_attribute(self, entity, rschema, role, divid, reload, default_value): rtype = rschema.type value = entity.printable_value(rtype) - form, renderer = self._build_form(entity, rtype, role, divid, 'base', - default_value, reload) - if not self._should_edit_attribute(entity, rschema, form): + if not self._should_edit_attribute(entity, rschema): self.w(value) return + + display_label, related_entity = self._prepare_form(entity, rtype, role) + form, renderer = self._build_form(entity, rtype, role, divid, 'base', default_value, + reload, display_label, related_entity) value = value or default_value - field = form.field_by_name(rtype, role, entity.e_schema) - form.append_field(field) self.view_form(divid, value, form, renderer) - def _handle_relations(self, entity, rschema, role, divid, reload, default_value, formid): - rtype = rschema.type - rvid = self._compute_best_vid(entity.e_schema, rschema, role) - related_rset = entity.related(rtype, role) + def _compute_formid_value(self, entity, rschema, role, default_value, rvid, formid): + related_rset = entity.related(rschema.type, role) if related_rset: value = self._cw.view(rvid, related_rset) else: value = default_value - ttypes = self._compute_ttypes(rschema, role) + if not self._should_edit_relation(entity, rschema, role): + return None, value + return formid, value + + def _handle_relation(self, entity, rschema, role, divid, reload, default_value, formid): + rvid = self._compute_best_vid(entity.e_schema, rschema, role) + formid, value = self._compute_formid_value(entity, rschema, role, default_value, rvid, formid) + if formid is None: + return self.w(value) - if not self._should_edit_relation(entity, rschema, role): - self.w(value) - return - # this is for attribute-like composites (1 target type, 1 related entity at most) + rtype = rschema.type + display_label, related_entity = self._prepare_form(entity, rtype, role) + form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value, reload, + display_label, related_entity, dict(vid=rvid)) + self.view_form(divid, value, form, renderer) + + def _handle_composite(self, entity, rschema, role, divid, reload, default_value, formid): + # this is for attribute-like composites (1 target type, 1 related entity at most, for now) + ttypes = self._compute_ttypes(rschema, role) + related_rset = entity.related(rschema.type, role) add_related = self._may_add_related(related_rset, entity, rschema, role, ttypes) edit_related = self._may_edit_related_entity(related_rset, entity, rschema, role, ttypes) delete_related = edit_related and self._may_delete_related(related_rset, entity, rschema, role) - # compute formid - if len(ttypes) > 1: # redundant safety belt - formid = 'base' - else: - afs = uicfg.autoform_section.etype_get(entity.e_schema, rschema, role, ttypes[0]) - # is there an afs spec that says we should edit - # the rschema as an attribute ? - if afs and 'main_attributes' in afs: - formid = 'base' + + rvid = self._compute_best_vid(entity.e_schema, rschema, role) + formid, value = self._compute_formid_value(entity, rschema, role, default_value, rvid, formid) + if formid is None or not (edit_related or add_related): + # till we learn to handle cases where not (edit_related or add_related) + self.w(value) + return - form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value, - reload, dict(vid=rvid), - edit_related, add_related and ttypes[0]) - if formid == 'base': - field = form.field_by_name(rtype, role, entity.e_schema) - form.append_field(field) - self.view_form(divid, value, form, renderer, edit_related, - delete_related, add_related) - + rtype = rschema.type + ttype = ttypes[0] + _fdata = self._prepare_composite_form(entity, rtype, role, edit_related, add_related and ttype) + display_label, related_entity = _fdata + form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value, reload, + display_label, related_entity, dict(vid=rvid)) + self.view_form(divid, value, form, renderer, + edit_related, add_related, delete_related) def _compute_best_vid(self, eschema, rschema, role): + rvid = self._one_rvid if eschema.rdef(rschema, role).role_cardinality(role) in '+*': - return self._many_rvid - return self._one_rvid + rvid = self._many_rvid + return self._rules.get('rvid', rvid) def _compute_ttypes(self, rschema, role): dual_role = neg_role(role) return getattr(rschema, '%ss' % dual_role)() def _compute_reload(self, entity, rschema, role, reload): - rule = uicfg.reledit_ctrl.etype_get(entity.e_schema.type, rschema.type, role, '*') - ctrl_reload = rule.get('reload', reload) + ctrl_reload = self._rules.get('reload', reload) if callable(ctrl_reload): ctrl_reload = ctrl_reload(entity) if isinstance(ctrl_reload, int) and ctrl_reload > 1: # not True/False @@ -160,8 +173,7 @@ def _compute_default_value(self, entity, rschema, role, default_value): etype = entity.e_schema.type - rule = uicfg.reledit_ctrl.etype_get(etype, rschema.type, role, '*') - ctrl_default = rule.get('default_value', default_value) + ctrl_default = self._rules.get('default_value', default_value) if ctrl_default: return ctrl_default if default_value is None: @@ -169,47 +181,42 @@ display_name(self._cw, rschema.type, role)) return default_value - def _is_composite(self, eschema, rschema, role): - return eschema.rdef(rschema, role).composite == role + def _is_composite(self): + return self._rules.get('edit_target') == 'related' def _may_add_related(self, related_rset, entity, rschema, role, ttypes): """ ok for attribute-like composite entities """ - if self._is_composite(entity.e_schema, rschema, role): - if len(ttypes) > 1: # wrong cardinality: do not handle - return False - rdef = rschema.role_rdef(entity.e_schema, ttypes[0], role) - card = rdef.role_cardinality(role) - if related_rset and card in '?1': - return False - if role == 'subject': - kwargs = {'fromeid': entity.eid} - else: - kwargs = {'toeid': entity.eid} - if rdef.has_perm(self._cw, 'add', **kwargs): - return True - return False + if len(ttypes) > 1: # many etypes: learn how to do it + return False + rdef = rschema.role_rdef(entity.e_schema, ttypes[0], role) + card = rdef.role_cardinality(role) + if related_rset or card not in '?1': + return False + if role == 'subject': + kwargs = {'fromeid': entity.eid} + else: + kwargs = {'toeid': entity.eid} + return rdef.has_perm(self._cw, 'add', **kwargs) def _may_edit_related_entity(self, related_rset, entity, rschema, role, ttypes): """ controls the edition of the related entity """ - if entity.e_schema.rdef(rschema, role).role_cardinality(role) not in '?1': - return False - if len(related_rset.rows) != 1: + if len(ttypes) > 1 or len(related_rset.rows) != 1: return False - if len(ttypes) > 1: - return False - if not self._is_composite(entity.e_schema, rschema, role): + if entity.e_schema.rdef(rschema, role).role_cardinality(role) not in '?1': return False return related_rset.get_entity(0, 0).cw_has_perm('update') def _may_delete_related(self, related_rset, entity, rschema, role): - # we assume may_edit_related - kwargs = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid} - if not rschema.has_perm(self._cw, 'delete', **kwargs): + # we assume may_edit_related, only 1 related entity + if not related_rset: return False - for related_entity in related_rset.entities(): - if not related_entity.cw_has_perm('delete'): - return False - return True + rentity = related_rset.get_entity(0, 0) + if role == 'subject': + kwargs = {'fromeid': entity.eid, 'toeid': rentity.eid} + else: + kwargs = {'fromeid': rentity.eid, 'toeid': entity.eid} + # NOTE: should be sufficient given a well built schema/security + return rschema.has_perm(self._cw, 'delete', **kwargs) def _build_edit_zone(self): return self._editzone % {'msg' : xml_escape(_(self._cw._(self._editzonemsg)))} @@ -234,32 +241,43 @@ event_args.update(extradata) return event_args - def _build_form(self, entity, rtype, role, divid, formid, default_value, reload, - extradata=None, edit_related=False, add_related=False, **formargs): - event_args = self._build_args(entity, rtype, role, formid, default_value, - reload, extradata) - cancelclick = self._cancelclick % divid + def _prepare_form(self, entity, _rtype, role): + display_label = False + related_entity = entity + return display_label, related_entity + + def _prepare_composite_form(self, entity, rtype, role, edit_related, add_related): if edit_related and not add_related: - display_fields = None display_label = True related_entity = entity.related(rtype, role).get_entity(0, 0) - self._cw.form['eid'] = related_entity.eid elif add_related: - display_fields = None display_label = True _new_entity = self._cw.vreg['etypes'].etype_class(add_related)(self._cw) _new_entity.eid = self._cw.varmaker.next() related_entity = _new_entity + # XXX see forms.py ~ 276 and entities.linked_to method + # is there another way ? self._cw.form['__linkto'] = '%s:%s:%s' % (rtype, entity.eid, neg_role(role)) - else: # base case: edition/attribute relation - display_fields = [(rtype, role)] - display_label = False - related_entity = entity + return display_label, related_entity + + def _build_renderer(self, related_entity, display_label): + return self._cw.vreg['formrenderers'].select( + self._form_renderer_id, self._cw, entity=related_entity, + display_label=display_label, + table_class='attributeForm' if display_label else '', + display_help=False, button_bar_class='buttonbar', + display_progress_div=False) + + def _build_form(self, entity, rtype, role, divid, formid, default_value, reload, + display_label, related_entity, extradata=None, **formargs): + event_args = self._build_args(entity, rtype, role, formid, default_value, + reload, extradata) + cancelclick = self._cancelclick % divid form = self._cw.vreg['forms'].select( - formid, self._cw, rset=related_entity.as_rset(), entity=related_entity, domid='%s-form' % divid, - display_fields=display_fields, formtype='inlined', - action=self._cw.build_url('validateform?__onsuccess=window.parent.cw.reledit.onSuccess'), - cwtarget='eformframe', cssstyle='display: none', + formid, self._cw, rset=related_entity.as_rset(), entity=related_entity, + domid='%s-form' % divid, formtype='inlined', + action=self._cw.build_url('validateform', __onsuccess='window.parent.cw.reledit.onSuccess'), + cwtarget='eformframe', cssclass='releditForm', **formargs) # pass reledit arguments for pname, pvalue in event_args.iteritems(): @@ -279,53 +297,37 @@ form.form_buttons = [SubmitButton(), Button(stdmsgs.BUTTON_CANCEL, onclick=cancelclick)] form.event_args = event_args - renderer = self._cw.vreg['formrenderers'].select( - 'base', self._cw, entity=related_entity, display_label=display_label, - display_help=False, table_class='', - button_bar_class='buttonbar', display_progress_div=False) - return form, renderer + if formid == 'base': + field = form.field_by_name(rtype, role, entity.e_schema) + form.append_field(field) + return form, self._build_renderer(related_entity, display_label) - def _should_edit_attribute(self, entity, rschema, form): - # examine rtags - noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rschema.type, 'subject').get('noedit', False) - if noedit: - return False + def _should_edit_attribute(self, entity, rschema): rdef = entity.e_schema.rdef(rschema) - afs = uicfg.autoform_section.etype_get(entity.__regid__, rschema, 'subject', rdef.object) - if 'main_hidden' in afs: - return False # check permissions if not entity.cw_has_perm('update'): return False rdef = entity.e_schema.rdef(rschema) - if not rdef.has_perm(self._cw, 'update', eid=entity.eid): - return False - # XXX ? - try: - form.field_by_name(str(rschema), 'subject', entity.e_schema) - except FieldNotFound: - return False - return True + return rdef.has_perm(self._cw, 'update', eid=entity.eid) + + should_edit_attributes = deprecated('[3.9] should_edit_attributes is deprecated,' + ' use _should_edit_attribute instead', + _should_edit_attribute) def _should_edit_relation(self, entity, rschema, role): - # examine rtags - rtype = rschema.type - noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rtype, role).get('noedit', False) - if noedit: - return False - rdef = entity.e_schema.rdef(rschema, role) - afs = uicfg.autoform_section.etype_get( - entity.__regid__, rschema, role, rdef.object) - if 'main_hidden' in afs: - return False - perm_args = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid} + eeid = entity.eid + perm_args = {'fromeid': eeid} if role == 'subject' else {'toeid': eeid} return rschema.has_perm(self._cw, 'add', **perm_args) - def view_form(self, divid, value, form=None, renderer=None, - edit_related=False, delete_related=False, add_related=False): + should_edit_relations = deprecated('[3.9] should_edit_relations is deprecated,' + ' use _should_edit_relation instead', + _should_edit_relation) + + def _open_form_wrapper(self, divid, value, form, renderer, + _edit_related, _add_related, _delete_related): w = self.w - w(u'<div id="%(id)s-reledit" onmouseout="%(out)s" onmouseover="%(over)s">' % - {'id': divid, + w(u'<div id="%(id)s-reledit" onmouseout="%(out)s" onmouseover="%(over)s" class="%(css)s">' % + {'id': divid, 'css': 'releditField', 'out': "jQuery('#%s').addClass('hidden')" % divid, 'over': "jQuery('#%s').removeClass('hidden')" % divid}) w(u'<div id="%s-value" class="editableFieldValue">' % divid) @@ -333,33 +335,54 @@ w(u'</div>') w(form.render(renderer=renderer)) w(u'<div id="%s" class="editableField hidden">' % divid) - args = form.event_args.copy() - if not add_related: # excludes edition - args['formid'] = 'edition' + + def _edit_action(self, divid, args, edit_related, add_related, _delete_related): + if not add_related: # currently, excludes edition + w = self.w + args['formid'] = 'edition' if edit_related else 'base' w(u'<div id="%s-update" class="editableField" onclick="%s" title="%s">' % (divid, xml_escape(self._onclick % args), self._cw._(self._editzonemsg))) w(self._build_edit_zone()) w(u'</div>') - else: - args['formid'] = 'edition' + + def _add_action(self, divid, args, _edit_related, add_related, _delete_related): + if add_related: + w = self.w + args['formid'] = 'edition' if add_related else 'base' w(u'<div id="%s-add" class="editableField" onclick="%s" title="%s">' % (divid, xml_escape(self._onclick % args), self._cw._(self._addmsg))) w(self._build_add_zone()) w(u'</div>') + + def _del_action(self, divid, args, _edit_related, _add_related, delete_related): if delete_related: + w = self.w args['formid'] = 'deleteconf' w(u'<div id="%s-delete" class="editableField" onclick="%s" title="%s">' % (divid, xml_escape(self._onclick % args), self._cw._(self._deletemsg))) w(self._build_delete_zone()) w(u'</div>') - w(u'</div>') - w(u'</div>') + + def _close_form_wrapper(self): + self.w(u'</div>') + self.w(u'</div>') + + def view_form(self, divid, value, form=None, renderer=None, + edit_related=False, add_related=False, delete_related=False): + self._open_form_wrapper(divid, value, form, renderer, + edit_related, add_related, delete_related) + args = form.event_args.copy() + self._edit_action(divid, args, edit_related, add_related, delete_related) + self._add_action(divid, args, edit_related, add_related, delete_related) + self._del_action(divid, args, edit_related, add_related, delete_related) + self._close_form_wrapper() + class AutoClickAndEditFormView(ClickAndEditFormView): __regid__ = 'reledit' def _build_form(self, entity, rtype, role, divid, formid, default_value, reload, - extradata=None, edit_related=False, add_related=False, **formargs): + display_label, related_entity, extradata=None, **formargs): event_args = self._build_args(entity, rtype, role, 'base', default_value, reload, extradata) form = _DummyForm() diff -r c4a70a5dd144 -r 42079f752a9c web/views/sessions.py diff -r c4a70a5dd144 -r 42079f752a9c web/views/tableview.py --- a/web/views/tableview.py Tue Sep 14 08:48:44 2010 +0200 +++ b/web/views/tableview.py Thu Sep 16 18:56:35 2010 +0200 @@ -140,11 +140,11 @@ if mainindex is None: displayfilter, displayactions = False, False else: - if displayfilter is None and 'displayfilter' in req.form: + if displayfilter is None and req.form.get('displayfilter'): displayfilter = True if req.form['displayfilter'] == 'shown': hidden = False - if displayactions is None and 'displayactions' in req.form: + if displayactions is None and req.form.get('displayactions'): displayactions = True displaycols = self.displaycols(displaycols, headers) fromformfilter = 'fromformfilter' in req.form @@ -187,14 +187,9 @@ def page_navigation_url(self, navcomp, path, params): if hasattr(self, 'divid'): - divid = self.divid - else: - divid = params.get('divid', 'pageContent'), - rql = params.pop('rql', self.cw_rset.printable_rql()) - # latest 'true' used for 'swap' mode - return 'javascript: replacePageChunk(%s, %s, %s, %s, true)' % ( - json_dumps(divid), json_dumps(rql), json_dumps(self.__regid__), - json_dumps(params)) + params['divid'] = self.divid + params['vid'] = self.__regid__ + return navcomp.ajax_page_url(**params) def show_hide_actions(self, divid, currentlydisplayed=False): showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:] @@ -324,7 +319,8 @@ title = None def call(self, title=None, subvid=None, headers=None, divid=None, - paginate=False, displaycols=None, displayactions=None, mainindex=None): + paginate=False, displaycols=None, displayactions=None, + mainindex=None): """Dumps a table displaying a composite query""" try: actrql = self._cw.form['actualrql'] @@ -347,7 +343,8 @@ mainindex = self.main_var_index() if mainindex is not None: actions = self.form_filter(divid, displaycols, displayactions, - displayfilter=True, paginate=paginate, hidden=True) + displayfilter=True, paginate=paginate, + hidden=True) else: actions = () if not subvid and 'subvid' in self._cw.form: diff -r c4a70a5dd144 -r 42079f752a9c web/views/urlpublishing.py --- a/web/views/urlpublishing.py Tue Sep 14 08:48:44 2010 +0200 +++ b/web/views/urlpublishing.py Thu Sep 16 18:56:35 2010 +0200 @@ -176,18 +176,27 @@ else: attrname = cls._rest_attr_info()[0] value = req.url_unquote(parts.pop(0)) - rset = self.attr_rset(req, etype, attrname, value) - else: - rset = self.cls_rset(req, cls) + return self.handle_etype_attr(req, cls, attrname, value) + return self.handle_etype(req, cls) + + def set_vid_for_rset(self, req, cls, rset):# cls is there to ease overriding if rset.rowcount == 0: raise NotFound() + # we've to set a default vid here, since vid_from_rset may try to use a + # table view if fetch_rql include some non final relation + if rset.rowcount == 1: + req.form.setdefault('vid', 'primary') + else: # rset.rowcount >= 1 + req.form.setdefault('vid', 'sameetypelist') + + def handle_etype(self, req, cls): + rset = req.execute(cls.fetch_rql(req.user)) + self.set_vid_for_rset(req, cls, rset) return None, rset - def cls_rset(self, req, cls): - return req.execute(cls.fetch_rql(req.user)) - - def attr_rset(self, req, etype, attrname, value): - rql = u'Any X WHERE X is %s, X %s %%(x)s' % (etype, attrname) + def handle_etype_attr(self, req, cls, attrname, value): + rql = cls.fetch_rql(req.user, ['X %s %%(x)s' % (attrname)], + mainvar='X', ordermethod=None) if attrname == 'eid': try: rset = req.execute(rql, {'x': typed_eid(value)}) @@ -196,7 +205,8 @@ raise PathDontMatch() else: rset = req.execute(rql, {'x': value}) - return rset + self.set_vid_for_rset(req, cls, rset) + return None, rset class URLRewriteEvaluator(URLPathEvaluator):