# HG changeset patch # User Sylvain Thénault # Date 1347019319 -7200 # Node ID 268b6349baf3606ec83daa813dd4614d330765f8 # Parent c09feae040946b5b0f574ef20ca953f03c221645# Parent 6ed331fd4347635bc233ad45d7ba08003e4feada backport stable diff -r 6ed331fd4347 -r 268b6349baf3 __pkginfo__.py --- a/__pkginfo__.py Fri Sep 07 13:48:55 2012 +0200 +++ b/__pkginfo__.py Fri Sep 07 14:01:59 2012 +0200 @@ -43,7 +43,7 @@ 'logilab-common': '>= 0.58.0', 'logilab-mtconverter': '>= 0.8.0', 'rql': '>= 0.31.2', - 'yams': '>= 0.34.0', + 'yams': '>= 0.36.0', #gettext # for xgettext, msgcat, etc... # web dependancies 'simplejson': '>= 2.0.9', diff -r 6ed331fd4347 -r 268b6349baf3 debian/control --- a/debian/control Fri Sep 07 13:48:55 2012 +0200 +++ b/debian/control Fri Sep 07 14:01:59 2012 +0200 @@ -107,7 +107,7 @@ Package: cubicweb-common Architecture: all XB-Python-Version: ${python:Versions} -Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.58.0), python-yams (>= 0.34.0), python-rql (>= 0.31.2), python-lxml +Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.58.0), python-yams (>= 0.36.0), python-rql (>= 0.31.2), python-lxml Recommends: python-simpletal (>= 4.0), python-crypto Conflicts: cubicweb-core Replaces: cubicweb-core diff -r 6ed331fd4347 -r 268b6349baf3 doc/book/en/annexes/faq.rst --- a/doc/book/en/annexes/faq.rst Fri Sep 07 13:48:55 2012 +0200 +++ b/doc/book/en/annexes/faq.rst Fri Sep 07 14:01:59 2012 +0200 @@ -364,7 +364,7 @@ >>> crypted = crypt_password('joepass') >>> rset = rql('Any U WHERE U is CWUser, U login "joe"') >>> joe = rset.get_entity(0,0) - >>> joe.set_attributes(upassword=Binary(crypted)) + >>> joe.cw_set(upassword=Binary(crypted)) Please, refer to the script example is provided in the `misc/examples/chpasswd.py` file. diff -r 6ed331fd4347 -r 268b6349baf3 doc/book/en/devrepo/entityclasses/application-logic.rst --- a/doc/book/en/devrepo/entityclasses/application-logic.rst Fri Sep 07 13:48:55 2012 +0200 +++ b/doc/book/en/devrepo/entityclasses/application-logic.rst Fri Sep 07 14:01:59 2012 +0200 @@ -38,7 +38,7 @@ object was built. Setting an attribute or relation value can be done in the context of a -Hook/Operation, using the obj.set_relations(x=42) notation or a plain +Hook/Operation, using the obj.cw_set(x=42) notation or a plain RQL SET expression. In views, it would be preferable to encapsulate the necessary logic in diff -r 6ed331fd4347 -r 268b6349baf3 doc/book/en/devrepo/entityclasses/data-as-objects.rst --- a/doc/book/en/devrepo/entityclasses/data-as-objects.rst Fri Sep 07 13:48:55 2012 +0200 +++ b/doc/book/en/devrepo/entityclasses/data-as-objects.rst Fri Sep 07 14:01:59 2012 +0200 @@ -16,50 +16,47 @@ `Formatting and output generation`: -* `view(__vid, __registry='views', **kwargs)`, applies the given view to the entity +* :meth:`view(__vid, __registry='views', **kwargs)`, applies the given view to the entity (and returns an unicode string) -* `absolute_url(*args, **kwargs)`, returns an absolute URL including the base-url +* :meth:`absolute_url(*args, **kwargs)`, returns an absolute URL including the base-url -* `rest_path()`, returns a relative REST URL to get the entity +* :meth:`rest_path()`, returns a relative REST URL to get the entity -* `printable_value(attr, value=_marker, attrtype=None, format='text/html', displaytime=True)`, +* :meth:`printable_value(attr, value=_marker, attrtype=None, format='text/html', displaytime=True)`, returns a string enabling the display of an attribute value in a given format (the value is automatically recovered if necessary) `Data handling`: -* `as_rset()`, converts the entity into an equivalent result set simulating the +* :meth:`as_rset()`, converts the entity into an equivalent result set simulating the request `Any X WHERE X eid _eid_` -* `complete(skip_bytes=True)`, executes a request that recovers at +* :meth:`complete(skip_bytes=True)`, executes a request that recovers at once all the missing attributes of an entity -* `get_value(name)`, returns the value associated to the attribute name given +* :meth:`get_value(name)`, returns the value associated to the attribute name given in parameter -* `related(rtype, role='subject', limit=None, entities=False)`, +* :meth:`related(rtype, role='subject', limit=None, entities=False)`, returns a list of entities related to the current entity by the relation given in parameter -* `unrelated(rtype, targettype, role='subject', limit=None)`, +* :meth:`unrelated(rtype, targettype, role='subject', limit=None)`, returns a result set corresponding to the entities not (yet) related to the current entity by the relation given in parameter and satisfying its constraints -* `set_attributes(**kwargs)`, updates the attributes list with the corresponding - values given named parameters +* :meth:`cw_set(**kwargs)`, updates entity's attributes and/or relation with the + corresponding values given named parameters. To set a relation where this + entity is the object of the relation, use `reverse_` as argument + name. Values may be an entity, a list of entities, or None (meaning that all + relations of the given type from or to this object should be deleted). -* `set_relations(**kwargs)`, add relations to the given object. To - set a relation where this entity is the object of the relation, - use `reverse_` as argument name. Values may be an - entity, a list of entities, or None (meaning that all relations of - the given type from or to this object should be deleted). - -* `copy_relations(ceid)`, copies the relations of the entities having the eid +* :meth:`copy_relations(ceid)`, copies the relations of the entities having the eid given in the parameters on the current entity -* `delete()` allows to delete the entity +* :meth:`cw_delete()` allows to delete the entity The :class:`AnyEntity` class @@ -81,40 +78,30 @@ `Standard meta-data (Dublin Core)`: -* `dc_title()`, returns a unicode string corresponding to the +* :meth:`dc_title()`, returns a unicode string corresponding to the meta-data `Title` (used by default is the first non-meta attribute of the entity schema) -* `dc_long_title()`, same as dc_title but can return a more +* :meth:`dc_long_title()`, same as dc_title but can return a more detailed title -* `dc_description(format='text/plain')`, returns a unicode string +* :meth:`dc_description(format='text/plain')`, returns a unicode string corresponding to the meta-data `Description` (looks for a description attribute by default) -* `dc_authors()`, returns a unicode string corresponding to the meta-data +* :meth:`dc_authors()`, returns a unicode string corresponding to the meta-data `Authors` (owners by default) -* `dc_creator()`, returns a unicode string corresponding to the +* :meth:`dc_creator()`, returns a unicode string corresponding to the creator of the entity -* `dc_date(date_format=None)`, returns a unicode string corresponding to +* :meth:`dc_date(date_format=None)`, returns a unicode string corresponding to the meta-data `Date` (update date by default) -* `dc_type(form='')`, returns a string to display the entity type by +* :meth:`dc_type(form='')`, returns a string to display the entity type by specifying the preferred form (`plural` for a plural form) -* `dc_language()`, returns the language used by the entity - - -`Misc methods`: - -* `after_deletion_path`, return (path, parameters) which should be - used as redirect information when this entity is being deleted - -* `pre_web_edit`, callback called by the web editcontroller when an - entity will be created/modified, to let a chance to do some entity - specific stuff (does nothing by default) +* :meth:`dc_language()`, returns the language used by the entity Inheritance ----------- diff -r 6ed331fd4347 -r 268b6349baf3 doc/book/en/devrepo/repo/hooks.rst --- a/doc/book/en/devrepo/repo/hooks.rst Fri Sep 07 13:48:55 2012 +0200 +++ b/doc/book/en/devrepo/repo/hooks.rst Fri Sep 07 14:01:59 2012 +0200 @@ -206,10 +206,11 @@ Reminder ~~~~~~~~ -You should never use the `entity.foo = 42` notation to update an -entity. It will not do what you expect (updating the -database). Instead, use the :meth:`set_attributes` and -:meth:`set_relations` methods. +You should never use the `entity.foo = 42` notation to update an entity. It will +not do what you expect (updating the database). Instead, use the +:meth:`~cubicweb.entity.Entity.cw_set` method or direct access to entity's +:attr:`cw_edited` attribute if you're writing a hook for 'before_add_entity' or +'before_update_entity' event. How to choose between a before and an after event ? diff -r 6ed331fd4347 -r 268b6349baf3 doc/book/en/devrepo/testing.rst --- a/doc/book/en/devrepo/testing.rst Fri Sep 07 13:48:55 2012 +0200 +++ b/doc/book/en/devrepo/testing.rst Fri Sep 07 14:01:59 2012 +0200 @@ -70,13 +70,13 @@ def test_cannot_create_cycles(self): # direct obvious cycle - self.assertRaises(ValidationError, self.kw1.set_relations, + self.assertRaises(ValidationError, self.kw1.cw_set, subkeyword_of=self.kw1) # testing indirect cycles kw3 = self.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, ' 'SK subkeyword_of K WHERE C name "classif1", K eid %s' % self.kw1.eid).get_entity(0,0) - self.kw1.set_relations(subkeyword_of=kw3) + self.kw1.cw_set(subkeyword_of=kw3) self.assertRaises(ValidationError, self.commit) The test class defines a :meth:`setup_database` method which populates the @@ -192,10 +192,10 @@ description=u'cubicweb is beautiful') blog_entry_1 = req.create_entity('BlogEntry', title=u'hop', content=u'cubicweb hop') - blog_entry_1.set_relations(entry_of=cubicweb_blog) + blog_entry_1.cw_set(entry_of=cubicweb_blog) blog_entry_2 = req.create_entity('BlogEntry', title=u'yes', content=u'cubicweb yes') - blog_entry_2.set_relations(entry_of=cubicweb_blog) + blog_entry_2.cw_set(entry_of=cubicweb_blog) self.assertEqual(len(MAILBOX), 0) self.commit() self.assertEqual(len(MAILBOX), 2) diff -r 6ed331fd4347 -r 268b6349baf3 doc/book/en/devweb/index.rst --- a/doc/book/en/devweb/index.rst Fri Sep 07 13:48:55 2012 +0200 +++ b/doc/book/en/devweb/index.rst Fri Sep 07 14:01:59 2012 +0200 @@ -10,6 +10,7 @@ publisher controllers request + searchbar views/index rtags ajax diff -r 6ed331fd4347 -r 268b6349baf3 doc/book/en/devweb/searchbar.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/book/en/devweb/searchbar.rst Fri Sep 07 14:01:59 2012 +0200 @@ -0,0 +1,41 @@ +.. _searchbar: + +RQL search bar +-------------- + +The RQL search bar is a visual component, hidden by default, the tiny *search* +input being enough for common use cases. + +An autocompletion helper is provided to help you type valid queries, both +in terms of syntax and in terms of schema validity. + +.. autoclass:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder + + +How search is performed ++++++++++++++++++++++++ + +You can use the *rql search bar* to either type RQL queries, plain text queries +or standard shortcuts such as ** or * *. + +Ultimately, all queries are translated to rql since it's the only +language understood on the server (data) side. To transform the user +query into RQL, CubicWeb uses the so-called *magicsearch component*, +defined in :mod:`cubicweb.web.views.magicsearch`, which in turn +delegates to a number of query preprocessor that are responsible of +interpreting the user query and generating corresponding RQL. + +The code of the main processor loop is easy to understand: + +.. sourcecode:: python + + for proc in self.processors: + try: + return proc.process_query(uquery, req) + except (RQLSyntaxError, BadRQLQuery): + pass + +The idea is simple: for each query processor, try to translate the +query. If it fails, try with the next processor, if it succeeds, +we're done and the RQL query will be executed. + diff -r 6ed331fd4347 -r 268b6349baf3 doc/book/en/devweb/views/index.rst diff -r 6ed331fd4347 -r 268b6349baf3 doc/book/en/devweb/views/views.rst diff -r 6ed331fd4347 -r 268b6349baf3 doc/book/en/tutorials/advanced/part02_security.rst --- a/doc/book/en/tutorials/advanced/part02_security.rst Fri Sep 07 13:48:55 2012 +0200 +++ b/doc/book/en/tutorials/advanced/part02_security.rst Fri Sep 07 14:01:59 2012 +0200 @@ -196,7 +196,7 @@ for eid in self.get_data(): entity = self.session.entity_from_eid(eid) if entity.visibility == 'parent': - entity.set_attributes(visibility=u'authenticated') + entity.cw_set(visibility=u'authenticated') class SetVisibilityHook(hook.Hook): __regid__ = 'sytweb.setvisibility' @@ -215,7 +215,7 @@ parent = self._cw.entity_from_eid(self.eidto) child = self._cw.entity_from_eid(self.eidfrom) if child.visibility == 'parent': - child.set_attributes(visibility=parent.visibility) + child.cw_set(visibility=parent.visibility) Notice: @@ -344,7 +344,7 @@ self.assertEquals(len(req.execute('Folder X')), 0) # restricted... # may_be_read_by propagation self.restore_connection() - folder.set_relations(may_be_read_by=toto) + folder.cw_set(may_be_read_by=toto) self.commit() photo1.clear_all_caches() self.failUnless(photo1.may_be_read_by) diff -r 6ed331fd4347 -r 268b6349baf3 doc/book/en/tutorials/advanced/part04_ui-base.rst --- a/doc/book/en/tutorials/advanced/part04_ui-base.rst Fri Sep 07 13:48:55 2012 +0200 +++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst Fri Sep 07 14:01:59 2012 +0200 @@ -294,6 +294,7 @@ folder in which the current file (e.g. `self.entity`) is located. .. Note:: + The :class:`IBreadCrumbs` interface is a `breadcrumbs` method, but the default :class:`IBreadCrumbsAdapter` provides a default implementation for it that will look at the value returned by its `parent_entity` method. It also provides a @@ -331,6 +332,7 @@ navigate through the web site to see if everything is ok... .. Note:: + In the 'cubicweb-ctl i18ncube' command, `sytweb` refers to the **cube**, while in the two other, it refers to the **instance** (if you can't see the difference, reread CubicWeb's concept chapter !). @@ -363,4 +365,4 @@ .. _`several improvments`: http://www.cubicweb.org/blogentry/1179899 .. _`3.8`: http://www.cubicweb.org/blogentry/917107 .. _`first blog of this series`: http://www.cubicweb.org/blogentry/824642 -.. _`an earlier post`: http://www.cubicweb.org/867464 \ No newline at end of file +.. _`an earlier post`: http://www.cubicweb.org/867464 diff -r 6ed331fd4347 -r 268b6349baf3 entities/authobjs.py --- a/entities/authobjs.py Fri Sep 07 13:48:55 2012 +0200 +++ b/entities/authobjs.py Fri Sep 07 14:01:59 2012 +0200 @@ -101,7 +101,7 @@ kwargs['for_user'] = self self._cw.create_entity('CWProperty', **kwargs) else: - prop.set_attributes(value=value) + prop.cw_set(value=value) def matching_groups(self, groups): """return the number of the given group(s) in which the user is diff -r 6ed331fd4347 -r 268b6349baf3 entities/sources.py --- a/entities/sources.py Fri Sep 07 13:48:55 2012 +0200 +++ b/entities/sources.py Fri Sep 07 14:01:59 2012 +0200 @@ -51,7 +51,7 @@ continue raise cfgstr = unicode(generate_source_config(sconfig), self._cw.encoding) - self.set_attributes(config=cfgstr) + self.cw_set(config=cfgstr) class CWSource(_CWSourceCfgMixIn, AnyEntity): @@ -181,5 +181,5 @@ def write_log(self, session, **kwargs): if 'status' not in kwargs: kwargs['status'] = getattr(self, '_status', u'success') - self.set_attributes(log=u'
'.join(self._logs), **kwargs) + self.cw_set(log=u'
'.join(self._logs), **kwargs) self._logs = [] diff -r 6ed331fd4347 -r 268b6349baf3 entities/test/unittest_base.py --- a/entities/test/unittest_base.py Fri Sep 07 13:48:55 2012 +0200 +++ b/entities/test/unittest_base.py Fri Sep 07 14:01:59 2012 +0200 @@ -70,7 +70,7 @@ email1 = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"').get_entity(0, 0) email2 = self.execute('INSERT EmailAddress X: X address "maarten@philips.com"').get_entity(0, 0) email3 = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr"').get_entity(0, 0) - email1.set_relations(prefered_form=email2) + email1.cw_set(prefered_form=email2) self.assertEqual(email1.prefered.eid, email2.eid) self.assertEqual(email2.prefered.eid, email2.eid) self.assertEqual(email3.prefered.eid, email3.eid) @@ -104,10 +104,10 @@ e = self.execute('CWUser U WHERE U login "member"').get_entity(0, 0) self.assertEqual(e.dc_title(), 'member') self.assertEqual(e.name(), 'member') - e.set_attributes(firstname=u'bouah') + e.cw_set(firstname=u'bouah') self.assertEqual(e.dc_title(), 'member') self.assertEqual(e.name(), u'bouah') - e.set_attributes(surname=u'lôt') + e.cw_set(surname=u'lôt') self.assertEqual(e.dc_title(), 'member') self.assertEqual(e.name(), u'bouah lôt') diff -r 6ed331fd4347 -r 268b6349baf3 entities/test/unittest_wfobjs.py --- a/entities/test/unittest_wfobjs.py Fri Sep 07 13:48:55 2012 +0200 +++ b/entities/test/unittest_wfobjs.py Fri Sep 07 14:01:59 2012 +0200 @@ -63,7 +63,7 @@ # gnark gnark bar = wf.add_state(u'bar') self.commit() - bar.set_attributes(name=u'foo') + bar.cw_set(name=u'foo') with self.assertRaises(ValidationError) as cm: self.commit() self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a state of that name'}) @@ -86,7 +86,7 @@ # gnark gnark biz = wf.add_transition(u'biz', (bar,), foo) self.commit() - biz.set_attributes(name=u'baz') + biz.cw_set(name=u'baz') with self.assertRaises(ValidationError) as cm: self.commit() self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a transition of that name'}) @@ -516,7 +516,7 @@ ['rest']) self.assertEqual(parse_hist(iworkflowable.workflow_history), [('asleep', 'asleep', 'rest', None)]) - user.set_attributes(surname=u'toto') # fulfill condition + user.cw_set(surname=u'toto') # fulfill condition self.commit() iworkflowable.fire_transition('rest') self.commit() diff -r 6ed331fd4347 -r 268b6349baf3 entity.py --- a/entity.py Fri Sep 07 13:48:55 2012 +0200 +++ b/entity.py Fri Sep 07 14:01:59 2012 +0200 @@ -452,26 +452,13 @@ return mainattr, needcheck @classmethod - def cw_instantiate(cls, execute, **kwargs): - """add a new entity of this given type - - Example (in a shell session): - - >>> companycls = vreg['etypes'].etype_class(('Company') - >>> personcls = vreg['etypes'].etype_class(('Person') - >>> c = companycls.cw_instantiate(session.execute, name=u'Logilab') - >>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe', - ... works_for=c) - - You can also set relation where the entity has 'object' role by - prefixing the relation by 'reverse_'. - """ - rql = 'INSERT %s X' % cls.__regid__ + def _cw_build_entity_query(cls, kwargs): relations = [] restrictions = set() - pending_relations = [] + pendingrels = [] eschema = cls.e_schema qargs = {} + attrcache = {} for attr, value in kwargs.items(): if attr.startswith('reverse_'): attr = attr[len('reverse_'):] @@ -487,10 +474,13 @@ value = iter(value).next() else: # prepare IN clause - pending_relations.append( (attr, role, value) ) + pendingrels.append( (attr, role, value) ) continue if rschema.final: # attribute relations.append('X %s %%(%s)s' % (attr, attr)) + attrcache[attr] = value + elif value is None: + pendingrels.append( (attr, role, value) ) else: rvar = attr.upper() if role == 'object': @@ -503,19 +493,52 @@ if hasattr(value, 'eid'): value = value.eid qargs[attr] = value + rql = u'' if relations: - rql = '%s: %s' % (rql, ', '.join(relations)) + rql += ', '.join(relations) if restrictions: - rql = '%s WHERE %s' % (rql, ', '.join(restrictions)) - created = execute(rql, qargs).get_entity(0, 0) - for attr, role, values in pending_relations: + rql += ' WHERE %s' % ', '.join(restrictions) + return rql, qargs, pendingrels, attrcache + + @classmethod + def _cw_handle_pending_relations(cls, eid, pendingrels, execute): + for attr, role, values in pendingrels: if role == 'object': restr = 'Y %s X' % attr else: restr = 'X %s Y' % attr + if values is None: + execute('DELETE %s WHERE X eid %%(x)s' % restr, {'x': eid}) + continue execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % ( restr, ','.join(str(getattr(r, 'eid', r)) for r in values)), - {'x': created.eid}, build_descr=False) + {'x': eid}, build_descr=False) + + @classmethod + def cw_instantiate(cls, execute, **kwargs): + """add a new entity of this given type + + Example (in a shell session): + + >>> companycls = vreg['etypes'].etype_class(('Company') + >>> personcls = vreg['etypes'].etype_class(('Person') + >>> c = companycls.cw_instantiate(session.execute, name=u'Logilab') + >>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe', + ... works_for=c) + + You can also set relations where the entity has 'object' role by + prefixing the relation name by 'reverse_'. Also, relation values may be + an entity or eid, a list of entities or eids. + """ + rql, qargs, pendingrels, attrcache = cls._cw_build_entity_query(kwargs) + if rql: + rql = 'INSERT %s X: %s' % (cls.__regid__, rql) + else: + rql = 'INSERT %s X' % (cls.__regid__) + created = execute(rql, qargs).get_entity(0, 0) + created._cw_update_attr_cache(attrcache) + created.cw_attr_cache.update(attrcache) + cls._cw_handle_pending_relations(created.eid, pendingrels, execute) return created def __init__(self, req, rset=None, row=None, col=0): @@ -535,6 +558,24 @@ def __cmp__(self, other): raise NotImplementedError('comparison not implemented for %s' % self.__class__) + def _cw_update_attr_cache(self, attrcache): + # if context is a repository session, don't consider dont-cache-attrs as + # the instance already hold modified values and loosing them could + # introduce severe problems + if self._cw.is_request: + for attr in self._cw.get_shared_data('%s.dont-cache-attrs' % self.eid, + default=(), txdata=True, pop=True): + attrcache.pop(attr, None) + self.cw_attr_cache.pop(attr, None) + self.cw_attr_cache.update(attrcache) + + def _cw_dont_cache_attribute(self, attr): + """repository side method called when some attribute have been + transformed by a hook, hence original value should not be cached by + client + """ + self._cw.transaction_data.setdefault('%s.dont-cache-attrs' % self.eid, set()).add(attr) + def __json_encode__(self): """custom json dumps hook to dump the entity's eid which is not part of dict structure itself @@ -1215,54 +1256,41 @@ # raw edition utilities ################################################### - def set_attributes(self, **kwargs): # XXX cw_set_attributes + def cw_set(self, **kwargs): + """update this entity using given attributes / relation, working in the + same fashion as :meth:`cw_instantiate`. + + Example (in a shell session): + + >>> c = rql('Any X WHERE X is Company').get_entity(0, 0) + >>> p = rql('Any X WHERE X is Person').get_entity(0, 0) + >>> c.set(name=u'Logilab') + >>> p.set(firstname=u'John', lastname=u'Doe', works_for=c) + + You can also set relations where the entity has 'object' role by + prefixing the relation name by 'reverse_'. Also, relation values may be + an entity or eid, a list of entities or eids, or None (meaning that all + relations of the given type from or to this object should be deleted). + """ _check_cw_unsafe(kwargs) assert kwargs assert self.cw_is_saved(), "should not call set_attributes while entity "\ "hasn't been saved yet" - relations = ['X %s %%(%s)s' % (key, key) for key in kwargs] - # and now update the database - kwargs['x'] = self.eid - self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), - kwargs) - kwargs.pop('x') + rql, qargs, pendingrels, attrcache = self._cw_build_entity_query(kwargs) + if rql: + rql = 'SET ' + rql + qargs['x'] = self.eid + if ' WHERE ' in rql: + rql += ', X eid %(x)s' + else: + rql += ' WHERE X eid %(x)s' + self._cw.execute(rql, qargs) # update current local object _after_ the rql query to avoid # interferences between the query execution itself and the cw_edited / # skip_security machinery - self.cw_attr_cache.update(kwargs) - - def set_relations(self, **kwargs): # XXX cw_set_relations - """add relations to the given object. To set a relation where this entity - is the object of the relation, use 'reverse_' as argument name. - - Values may be an entity or eid, a list of entities or eids, or None - (meaning that all relations of the given type from or to this object - should be deleted). - """ - # XXX update cache - _check_cw_unsafe(kwargs) - for attr, values in kwargs.iteritems(): - if attr.startswith('reverse_'): - restr = 'Y %s X' % attr[len('reverse_'):] - else: - restr = 'X %s Y' % attr - if values is None: - self._cw.execute('DELETE %s WHERE X eid %%(x)s' % restr, - {'x': self.eid}) - continue - if not isinstance(values, (tuple, list, set, frozenset)): - values = (values,) - eids = [] - for val in values: - try: - eids.append(str(val.eid)) - except AttributeError: - try: - eids.append(str(typed_eid(val))) - except (ValueError, TypeError): - raise Exception('expected an Entity or eid, got %s' % val) - self._cw.execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % ( - restr, ','.join(eids)), {'x': self.eid}) + self._cw_update_attr_cache(attrcache) + self._cw_handle_pending_relations(self.eid, pendingrels, self._cw.execute) + # XXX update relation cache def cw_delete(self, **kwargs): assert self.has_eid(), self.eid @@ -1277,6 +1305,21 @@ # deprecated stuff ######################################################### + @deprecated('[3.16] use cw_set() instead') + def set_attributes(self, **kwargs): # XXX cw_set_attributes + self.cw_set(**kwargs) + + @deprecated('[3.16] use cw_set() instead') + def set_relations(self, **kwargs): # XXX cw_set_relations + """add relations to the given object. To set a relation where this entity + is the object of the relation, use 'reverse_' as argument name. + + Values may be an entity or eid, a list of entities or eids, or None + (meaning that all relations of the given type from or to this object + should be deleted). + """ + self.cw_set(**kwargs) + @deprecated('[3.13] use entity.cw_clear_all_caches()') def clear_all_caches(self): return self.cw_clear_all_caches() diff -r 6ed331fd4347 -r 268b6349baf3 hooks/test/unittest_hooks.py --- a/hooks/test/unittest_hooks.py Fri Sep 07 13:48:55 2012 +0200 +++ b/hooks/test/unittest_hooks.py Fri Sep 07 14:01:59 2012 +0200 @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -67,10 +67,9 @@ entity = self.request().create_entity('Workflow', name=u'wf1', description_format=u'text/html', description=u'yo') - entity.set_attributes(name=u'wf2') + entity.cw_set(name=u'wf2') self.assertEqual(entity.description, u'yo') - entity.set_attributes(description=u'R&D

yo') - entity.cw_attr_cache.pop('description') + entity.cw_set(description=u'R&D

yo') self.assertEqual(entity.description, u'R&D

yo

') def test_metadata_cwuri(self): diff -r 6ed331fd4347 -r 268b6349baf3 hooks/test/unittest_syncschema.py --- a/hooks/test/unittest_syncschema.py Fri Sep 07 13:48:55 2012 +0200 +++ b/hooks/test/unittest_syncschema.py Fri Sep 07 14:01:59 2012 +0200 @@ -294,7 +294,7 @@ def test_change_fulltext_container(self): req = self.request() target = req.create_entity(u'EmailAddress', address=u'rick.roll@dance.com') - target.set_relations(reverse_use_email=req.user) + target.cw_set(reverse_use_email=req.user) self.commit() rset = req.execute('Any X WHERE X has_text "rick.roll"') self.assertIn(req.user.eid, [item[0] for item in rset]) diff -r 6ed331fd4347 -r 268b6349baf3 hooks/workflow.py --- a/hooks/workflow.py Fri Sep 07 13:48:55 2012 +0200 +++ b/hooks/workflow.py Fri Sep 07 14:01:59 2012 +0200 @@ -335,7 +335,7 @@ return entity = self._cw.entity_from_eid(self.eidfrom) try: - entity.set_attributes(modification_date=datetime.now()) + entity.cw_set(modification_date=datetime.now()) except RepositoryError, ex: # usually occurs if entity is coming from a read-only source # (eg ldap user) diff -r 6ed331fd4347 -r 268b6349baf3 i18n/de.po --- a/i18n/de.po Fri Sep 07 13:48:55 2012 +0200 +++ b/i18n/de.po Fri Sep 07 14:01:59 2012 +0200 @@ -1001,9 +1001,6 @@ msgid "abstract base class for transitions" msgstr "abstrakte Basisklasse für Übergänge" -msgid "action menu" -msgstr "" - msgid "action(s) on this selection" msgstr "Aktionen(en) bei dieser Auswahl" @@ -4169,6 +4166,9 @@ msgid "toggle check boxes" msgstr "Kontrollkästchen umkehren" +msgid "toggle filter" +msgstr "filter verbergen/zeigen" + msgid "tr_count" msgstr "" @@ -4631,9 +4631,3 @@ #~ msgstr "" #~ "Kann die Relation %(rtype)s der Entität %(eid)s nicht wieder herstellen, " #~ "diese Relation existiert nicht mehr in dem Schema." - -#~ msgid "log out first" -#~ msgstr "Melden Sie sich zuerst ab." - -#~ msgid "week" -#~ msgstr "Woche" diff -r 6ed331fd4347 -r 268b6349baf3 i18n/en.po --- a/i18n/en.po Fri Sep 07 13:48:55 2012 +0200 +++ b/i18n/en.po Fri Sep 07 14:01:59 2012 +0200 @@ -963,9 +963,6 @@ msgid "abstract base class for transitions" msgstr "" -msgid "action menu" -msgstr "" - msgid "action(s) on this selection" msgstr "" @@ -4069,6 +4066,9 @@ msgid "toggle check boxes" msgstr "" +msgid "toggle filter" +msgstr "" + msgid "tr_count" msgstr "transition number" diff -r 6ed331fd4347 -r 268b6349baf3 i18n/es.po --- a/i18n/es.po Fri Sep 07 13:48:55 2012 +0200 +++ b/i18n/es.po Fri Sep 07 14:01:59 2012 +0200 @@ -1011,9 +1011,6 @@ msgid "abstract base class for transitions" msgstr "Clase de base abstracta para la transiciones" -msgid "action menu" -msgstr "" - msgid "action(s) on this selection" msgstr "Acción(es) en esta selección" @@ -4219,6 +4216,9 @@ msgid "toggle check boxes" msgstr "Cambiar valor" +msgid "toggle filter" +msgstr "esconder/mostrar el filtro" + msgid "tr_count" msgstr "n° de transición" @@ -4682,18 +4682,3 @@ #~ msgstr "" #~ "No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta " #~ "relación ya no existe en el esquema." - -#~ msgid "day" -#~ msgstr "día" - -#~ msgid "log out first" -#~ msgstr "Desconéctese primero" - -#~ msgid "month" -#~ msgstr "mes" - -#~ msgid "today" -#~ msgstr "hoy" - -#~ msgid "week" -#~ msgstr "sem." diff -r 6ed331fd4347 -r 268b6349baf3 i18n/fr.po --- a/i18n/fr.po Fri Sep 07 13:48:55 2012 +0200 +++ b/i18n/fr.po Fri Sep 07 14:01:59 2012 +0200 @@ -1011,9 +1011,6 @@ msgid "abstract base class for transitions" msgstr "classe de base abstraite pour les transitions" -msgid "action menu" -msgstr "actions" - msgid "action(s) on this selection" msgstr "action(s) sur cette sélection" @@ -1253,7 +1250,7 @@ msgstr "anonyme" msgid "anyrsetview" -msgstr "vues \"tous les rset\"" +msgstr "vues pour tout rset" msgid "april" msgstr "avril" @@ -4140,6 +4137,10 @@ msgid "there is no transaction #%s" msgstr "Il n'y a pas de transaction #%s" +#, python-format +msgid "there is no transaction #%s" +msgstr "" + msgid "this action is not reversible!" msgstr "" "Attention ! Cette opération va détruire les données de façon irréversible." @@ -4228,7 +4229,10 @@ msgstr "à faire par" msgid "toggle check boxes" -msgstr "inverser les cases à cocher" +msgstr "afficher/masquer les cases à cocher" + +msgid "toggle filter" +msgstr "afficher/masquer le filtre" msgid "tr_count" msgstr "n° de transition" diff -r 6ed331fd4347 -r 268b6349baf3 misc/migration/3.10.0_Any.py --- a/misc/migration/3.10.0_Any.py Fri Sep 07 13:48:55 2012 +0200 +++ b/misc/migration/3.10.0_Any.py Fri Sep 07 14:01:59 2012 +0200 @@ -34,5 +34,5 @@ for x in rql('Any X,XK WHERE X pkey XK, ' 'X pkey ~= "boxes.%" OR ' 'X pkey ~= "contentnavigation.%"').entities(): - x.set_attributes(pkey=u'ctxcomponents.' + x.pkey.split('.', 1)[1]) + x.cw_set(pkey=u'ctxcomponents.' + x.pkey.split('.', 1)[1]) diff -r 6ed331fd4347 -r 268b6349baf3 misc/migration/3.11.0_Any.py --- a/misc/migration/3.11.0_Any.py Fri Sep 07 13:48:55 2012 +0200 +++ b/misc/migration/3.11.0_Any.py Fri Sep 07 14:01:59 2012 +0200 @@ -81,5 +81,5 @@ rset = session.execute('Any V WHERE X is CWProperty, X value V, X pkey %(k)s', {'k': pkey}) timestamp = int(rset[0][0]) - sourceentity.set_attributes(latest_retrieval=datetime.fromtimestamp(timestamp)) + sourceentity.cw_set(latest_retrieval=datetime.fromtimestamp(timestamp)) session.execute('DELETE CWProperty X WHERE X pkey %(k)s', {'k': pkey}) diff -r 6ed331fd4347 -r 268b6349baf3 misc/migration/3.14.0_Any.py --- a/misc/migration/3.14.0_Any.py Fri Sep 07 13:48:55 2012 +0200 +++ b/misc/migration/3.14.0_Any.py Fri Sep 07 14:01:59 2012 +0200 @@ -9,5 +9,5 @@ expression = rqlcstr.value mainvars = guess_rrqlexpr_mainvars(expression) yamscstr = CONSTRAINTS[rqlcstr.type](expression, mainvars) - rqlcstr.set_attributes(value=yamscstr.serialize()) + rqlcstr.cw_set(value=yamscstr.serialize()) print 'updated', rqlcstr.type, rqlcstr.value.strip() diff -r 6ed331fd4347 -r 268b6349baf3 misc/migration/3.15.0_Any.py --- a/misc/migration/3.15.0_Any.py Fri Sep 07 13:48:55 2012 +0200 +++ b/misc/migration/3.15.0_Any.py Fri Sep 07 14:01:59 2012 +0200 @@ -4,7 +4,7 @@ config = source.dictconfig host = config.pop('host', u'ldap') protocol = config.pop('protocol', u'ldap') - source.set_attributes(url=u'%s://%s' % (protocol, host)) + source.cw_set(url=u'%s://%s' % (protocol, host)) source.update_config(skip_unknown=True, **config) commit() diff -r 6ed331fd4347 -r 268b6349baf3 misc/scripts/chpasswd.py --- a/misc/scripts/chpasswd.py Fri Sep 07 13:48:55 2012 +0200 +++ b/misc/scripts/chpasswd.py Fri Sep 07 14:01:59 2012 +0200 @@ -42,7 +42,7 @@ crypted = crypt_password(pass1) cwuser = rset.get_entity(0,0) -cwuser.set_attributes(upassword=Binary(crypted)) +cwuser.cw_set(upassword=Binary(crypted)) commit() print("password updated.") diff -r 6ed331fd4347 -r 268b6349baf3 misc/scripts/ldapuser2ldapfeed.py --- a/misc/scripts/ldapuser2ldapfeed.py Fri Sep 07 13:48:55 2012 +0200 +++ b/misc/scripts/ldapuser2ldapfeed.py Fri Sep 07 14:01:59 2012 +0200 @@ -87,7 +87,7 @@ source_ent = rql('CWSource S WHERE S eid %(s)s', {'s': source.eid}).get_entity(0, 0) -source_ent.set_attributes(type=u"ldapfeed", parser=u"ldapfeed") +source_ent.cw_set(type=u"ldapfeed", parser=u"ldapfeed") if raw_input('Commit ?') in 'yY': diff -r 6ed331fd4347 -r 268b6349baf3 predicates.py --- a/predicates.py Fri Sep 07 13:48:55 2012 +0200 +++ b/predicates.py Fri Sep 07 14:01:59 2012 +0200 @@ -737,12 +737,16 @@ See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity class lookup / score rules according to the input context. - .. note:: when interface is an entity class, the score will reflect class - proximity so the most specific object will be selected. + .. note:: + + when interface is an entity class, the score will reflect class + proximity so the most specific object will be selected. - .. note:: deprecated in cubicweb >= 3.9, use either - :class:`~cubicweb.predicates.is_instance` or - :class:`~cubicweb.predicates.adaptable`. + .. note:: + + deprecated in cubicweb >= 3.9, use either + :class:`~cubicweb.predicates.is_instance` or + :class:`~cubicweb.predicates.adaptable`. """ def __init__(self, *expected_ifaces, **kwargs): diff -r 6ed331fd4347 -r 268b6349baf3 req.py --- a/req.py Fri Sep 07 13:48:55 2012 +0200 +++ b/req.py Fri Sep 07 14:01:59 2012 +0200 @@ -62,6 +62,8 @@ :attribute vreg.schema: the instance's schema :attribute vreg.config: the instance's configuration """ + is_request = True # False for repository session + def __init__(self, vreg): self.vreg = vreg try: diff -r 6ed331fd4347 -r 268b6349baf3 server/edition.py --- a/server/edition.py Fri Sep 07 13:48:55 2012 +0200 +++ b/server/edition.py Fri Sep 07 14:01:59 2012 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -61,6 +61,8 @@ # attributes, else we may accidentaly skip a desired security check if attr not in self: self.skip_security.add(attr) + # mark attribute as needing purge by the client + self.entity._cw_dont_cache_attribute(attr) self.edited_attribute(attr, value) def __delitem__(self, attr): diff -r 6ed331fd4347 -r 268b6349baf3 server/hook.py --- a/server/hook.py Fri Sep 07 13:48:55 2012 +0200 +++ b/server/hook.py Fri Sep 07 14:01:59 2012 +0200 @@ -152,7 +152,7 @@ On those events, the entity has no `cw_edited` dictionary. -.. note:: `self.entity.set_attributes(age=42)` will set the `age` attribute to +.. note:: `self.entity.cw_set(age=42)` will set the `age` attribute to 42. But to do so, it will generate a rql query that will have to be processed, hence may trigger some hooks, etc. This could lead to infinitely looping hooks. diff -r 6ed331fd4347 -r 268b6349baf3 server/migractions.py --- a/server/migractions.py Fri Sep 07 13:48:55 2012 +0200 +++ b/server/migractions.py Fri Sep 07 14:01:59 2012 +0200 @@ -1321,7 +1321,7 @@ except Exception: self.cmd_create_entity('CWProperty', pkey=unicode(pkey), value=value) else: - prop.set_attributes(value=value) + prop.cw_set(value=value) # other data migration commands ########################################### diff -r 6ed331fd4347 -r 268b6349baf3 server/session.py --- a/server/session.py Fri Sep 07 13:48:55 2012 +0200 +++ b/server/session.py Fri Sep 07 14:01:59 2012 +0200 @@ -242,6 +242,7 @@ :attr:`running_dbapi_query`, boolean flag telling if the executing query is coming from a dbapi connection or is a query from within the repository """ + is_request = False is_internal_session = False def __init__(self, user, repo, cnxprops=None, _id=None): diff -r 6ed331fd4347 -r 268b6349baf3 server/sources/datafeed.py --- a/server/sources/datafeed.py Fri Sep 07 13:48:55 2012 +0200 +++ b/server/sources/datafeed.py Fri Sep 07 14:01:59 2012 +0200 @@ -396,7 +396,7 @@ attrs = dict( (k, v) for k, v in attrs.iteritems() if v != getattr(entity, k)) if attrs: - entity.set_attributes(**attrs) + entity.cw_set(**attrs) self.notify_updated(entity) class DataFeedXMLParser(DataFeedParser): diff -r 6ed331fd4347 -r 268b6349baf3 server/test/unittest_repository.py --- a/server/test/unittest_repository.py Fri Sep 07 13:48:55 2012 +0200 +++ b/server/test/unittest_repository.py Fri Sep 07 14:01:59 2012 +0200 @@ -523,7 +523,7 @@ self.commit() self.assertEqual(len(c.reverse_fiche), 1) - def test_set_attributes_in_before_update(self): + def test_cw_set_in_before_update(self): # local hook class DummyBeforeHook(Hook): __regid__ = 'dummy-before-hook' @@ -535,31 +535,31 @@ pendings = self._cw.transaction_data.setdefault('pending', set()) if self.entity.eid not in pendings: pendings.add(self.entity.eid) - self.entity.set_attributes(alias=u'foo') + self.entity.cw_set(alias=u'foo') with self.temporary_appobjects(DummyBeforeHook): req = self.request() addr = req.create_entity('EmailAddress', address=u'a@b.fr') - addr.set_attributes(address=u'a@b.com') + addr.cw_set(address=u'a@b.com') rset = self.execute('Any A,AA WHERE X eid %(x)s, X address A, X alias AA', {'x': addr.eid}) self.assertEqual(rset.rows, [[u'a@b.com', u'foo']]) - def test_set_attributes_in_before_add(self): + def test_cw_set_in_before_add(self): # local hook class DummyBeforeHook(Hook): __regid__ = 'dummy-before-hook' __select__ = Hook.__select__ & is_instance('EmailAddress') events = ('before_add_entity',) def __call__(self): - # set_attributes is forbidden within before_add_entity() - self.entity.set_attributes(alias=u'foo') + # cw_set is forbidden within before_add_entity() + self.entity.cw_set(alias=u'foo') with self.temporary_appobjects(DummyBeforeHook): req = self.request() # XXX will fail with python -O self.assertRaises(AssertionError, req.create_entity, 'EmailAddress', address=u'a@b.fr') - def test_multiple_edit_set_attributes(self): + def test_multiple_edit_cw_set(self): """make sure cw_edited doesn't get cluttered by previous entities on multiple set """ @@ -665,7 +665,7 @@ self.commit() rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'}) self.assertEqual(rset.rows, []) - req.user.set_relations(use_email=toto) + req.user.cw_set(use_email=toto) self.commit() rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'}) self.assertEqual(rset.rows, [[req.user.eid]]) @@ -675,11 +675,11 @@ rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'}) self.assertEqual(rset.rows, []) tutu = req.create_entity('EmailAddress', address=u'tutu@logilab.fr') - req.user.set_relations(use_email=tutu) + req.user.cw_set(use_email=tutu) self.commit() rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'tutu'}) self.assertEqual(rset.rows, [[req.user.eid]]) - tutu.set_attributes(address=u'hip@logilab.fr') + tutu.cw_set(address=u'hip@logilab.fr') self.commit() rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'tutu'}) self.assertEqual(rset.rows, []) @@ -791,7 +791,7 @@ personnes.append(p) abraham = req.create_entity('Personne', nom=u'Abraham', prenom=u'John', sexe=u'M') for j in xrange(0, 2000, 100): - abraham.set_relations(personne_composite=personnes[j:j+100]) + abraham.cw_set(personne_composite=personnes[j:j+100]) t1 = time.time() self.info('creation: %.2gs', (t1 - t0)) req.cnx.commit() @@ -817,7 +817,7 @@ t1 = time.time() self.info('creation: %.2gs', (t1 - t0)) for j in xrange(100, 2000, 100): - abraham.set_relations(personne_composite=personnes[j:j+100]) + abraham.cw_set(personne_composite=personnes[j:j+100]) t2 = time.time() self.info('more relations: %.2gs', (t2-t1)) req.cnx.commit() @@ -837,7 +837,7 @@ t1 = time.time() self.info('creation: %.2gs', (t1 - t0)) for j in xrange(100, 2000, 100): - abraham.set_relations(personne_inlined=personnes[j:j+100]) + abraham.cw_set(personne_inlined=personnes[j:j+100]) t2 = time.time() self.info('more relations: %.2gs', (t2-t1)) req.cnx.commit() @@ -918,7 +918,7 @@ p1 = req.create_entity('Personne', nom=u'Vincent') p2 = req.create_entity('Personne', nom=u'Florent') w = req.create_entity('Affaire', ref=u'wc') - w.set_relations(todo_by=[p1,p2]) + w.cw_set(todo_by=[p1,p2]) w.cw_clear_all_caches() self.commit() self.assertEqual(len(w.todo_by), 1) @@ -929,9 +929,9 @@ p1 = req.create_entity('Personne', nom=u'Vincent') p2 = req.create_entity('Personne', nom=u'Florent') w = req.create_entity('Affaire', ref=u'wc') - w.set_relations(todo_by=p1) + w.cw_set(todo_by=p1) self.commit() - w.set_relations(todo_by=p2) + w.cw_set(todo_by=p2) w.cw_clear_all_caches() self.commit() self.assertEqual(len(w.todo_by), 1) diff -r 6ed331fd4347 -r 268b6349baf3 server/test/unittest_storage.py --- a/server/test/unittest_storage.py Fri Sep 07 13:48:55 2012 +0200 +++ b/server/test/unittest_storage.py Fri Sep 07 14:01:59 2012 +0200 @@ -99,7 +99,7 @@ f1 = self.create_file() self.commit() self.assertEqual(file(expected_filepath).read(), 'the-data') - f1.set_attributes(data=Binary('the new data')) + f1.cw_set(data=Binary('the new data')) self.rollback() self.assertEqual(file(expected_filepath).read(), 'the-data') f1.cw_delete() @@ -204,7 +204,7 @@ # use self.session to use server-side cache f1 = self.session.create_entity('File', data=Binary('some data'), data_format=u'text/plain', data_name=u'foo') - # NOTE: do not use set_attributes() which would automatically + # NOTE: do not use cw_set() which would automatically # update f1's local dict. We want the pure rql version to work self.execute('SET F data %(d)s WHERE F eid %(f)s', {'d': Binary('some other data'), 'f': f1.eid}) @@ -218,7 +218,7 @@ # use self.session to use server-side cache f1 = self.session.create_entity('File', data=Binary('some data'), data_format=u'text/plain', data_name=u'foo.txt') - # NOTE: do not use set_attributes() which would automatically + # NOTE: do not use cw_set() which would automatically # update f1's local dict. We want the pure rql version to work self.commit() old_path = self.fspath(f1) @@ -240,7 +240,7 @@ # use self.session to use server-side cache f1 = self.session.create_entity('File', data=Binary('some data'), data_format=u'text/plain', data_name=u'foo.txt') - # NOTE: do not use set_attributes() which would automatically + # NOTE: do not use cw_set() which would automatically # update f1's local dict. We want the pure rql version to work self.commit() old_path = self.fspath(f1) @@ -265,7 +265,7 @@ f = self.session.create_entity('Affaire', opt_attr=Binary('toto')) self.session.commit() self.session.set_cnxset() - f.set_attributes(opt_attr=None) + f.cw_set(opt_attr=None) self.session.commit() @tag('fs_importing', 'update') diff -r 6ed331fd4347 -r 268b6349baf3 server/test/unittest_undo.py --- a/server/test/unittest_undo.py Fri Sep 07 13:48:55 2012 +0200 +++ b/server/test/unittest_undo.py Fri Sep 07 14:01:59 2012 +0200 @@ -203,7 +203,7 @@ c.cw_delete() txuuid = self.commit() c2 = session.create_entity('Card', title=u'hip', content=u'hip') - p.set_relations(fiche=c2) + p.cw_set(fiche=c2) self.commit() self.assertUndoTransaction(txuuid, [ "Can't restore object relation fiche to entity " @@ -217,7 +217,7 @@ session = self.session g = session.create_entity('CWGroup', name=u'staff') session.execute('DELETE U in_group G WHERE U eid %(x)s', {'x': self.toto.eid}) - self.toto.set_relations(in_group=g) + self.toto.cw_set(in_group=g) self.commit() self.toto.cw_delete() txuuid = self.commit() @@ -265,7 +265,7 @@ email = self.request().create_entity('EmailAddress', address=u'tutu@cubicweb.org') prop = self.request().create_entity('CWProperty', pkey=u'ui.default-text-format', value=u'text/html') - tutu.set_relations(use_email=email, reverse_for_user=prop) + tutu.cw_set(use_email=email, reverse_for_user=prop) self.commit() with self.assertRaises(ValidationError) as cm: self.cnx.undo_transaction(txuuid) @@ -278,7 +278,7 @@ g = session.create_entity('CWGroup', name=u'staff') txuuid = self.commit() session.execute('DELETE U in_group G WHERE U eid %(x)s', {'x': self.toto.eid}) - self.toto.set_relations(in_group=g) + self.toto.cw_set(in_group=g) self.commit() with self.assertRaises(ValidationError) as cm: self.cnx.undo_transaction(txuuid) @@ -304,7 +304,7 @@ c = session.create_entity('Card', title=u'hop', content=u'hop') p = session.create_entity('Personne', nom=u'louis', fiche=c) self.commit() - p.set_relations(fiche=None) + p.cw_set(fiche=None) txuuid = self.commit() self.assertUndoTransaction(txuuid) self.commit() @@ -319,7 +319,7 @@ c = session.create_entity('Card', title=u'hop', content=u'hop') p = session.create_entity('Personne', nom=u'louis', fiche=c) self.commit() - p.set_relations(fiche=None) + p.cw_set(fiche=None) txuuid = self.commit() c.cw_delete() self.commit() @@ -339,7 +339,7 @@ c = session.create_entity('Card', title=u'hop', content=u'hop') p = session.create_entity('Personne', nom=u'louis') self.commit() - p.set_relations(fiche=c) + p.cw_set(fiche=c) txuuid = self.commit() self.assertUndoTransaction(txuuid) self.commit() @@ -354,7 +354,7 @@ c = session.create_entity('Card', title=u'hop', content=u'hop') p = session.create_entity('Personne', nom=u'louis') self.commit() - p.set_relations(fiche=c) + p.cw_set(fiche=c) txuuid = self.commit() c.cw_delete() self.commit() @@ -369,7 +369,7 @@ c2 = session.create_entity('Card', title=u'hip', content=u'hip') p = session.create_entity('Personne', nom=u'louis', fiche=c1) self.commit() - p.set_relations(fiche=c2) + p.cw_set(fiche=c2) txuuid = self.commit() self.assertUndoTransaction(txuuid) self.commit() @@ -385,7 +385,7 @@ c2 = session.create_entity('Card', title=u'hip', content=u'hip') p = session.create_entity('Personne', nom=u'louis', fiche=c1) self.commit() - p.set_relations(fiche=c2) + p.cw_set(fiche=c2) txuuid = self.commit() c1.cw_delete() self.commit() @@ -401,7 +401,7 @@ p = session.create_entity('Personne', nom=u'toto') session.commit() self.session.set_cnxset() - p.set_attributes(nom=u'titi') + p.cw_set(nom=u'titi') txuuid = self.commit() self.assertUndoTransaction(txuuid) p.cw_clear_all_caches() @@ -412,7 +412,7 @@ p = session.create_entity('Personne', nom=u'toto') session.commit() self.session.set_cnxset() - p.set_attributes(nom=u'titi') + p.cw_set(nom=u'titi') txuuid = self.commit() p.cw_delete() self.commit() diff -r 6ed331fd4347 -r 268b6349baf3 sobjects/ldapparser.py --- a/sobjects/ldapparser.py Fri Sep 07 13:48:55 2012 +0200 +++ b/sobjects/ldapparser.py Fri Sep 07 14:01:59 2012 +0200 @@ -84,7 +84,7 @@ attrs = dict( (k, v) for k, v in attrs.iteritems() if v != getattr(entity, k)) if attrs: - entity.set_attributes(**attrs) + entity.cw_set(**attrs) self.notify_updated(entity) def ldap2cwattrs(self, sdict, tdict=None): @@ -113,7 +113,7 @@ if entity.__regid__ == 'EmailAddress': return groups = [self._get_group(n) for n in self.source.user_default_groups] - entity.set_relations(in_group=groups) + entity.cw_set(in_group=groups) self._process_email(entity, sourceparams) def is_deleted(self, extid, etype, eid): @@ -140,9 +140,9 @@ email = self.extid2entity(emailextid, 'EmailAddress', address=emailaddr) if entity.primary_email: - entity.set_relations(use_email=email) + entity.cw_set(use_email=email) else: - entity.set_relations(primary_email=email) + entity.cw_set(primary_email=email) elif self.sourceuris: # pop from sourceuris anyway, else email may be removed by the # source once import is finished diff -r 6ed331fd4347 -r 268b6349baf3 test/unittest_entity.py --- a/test/unittest_entity.py Fri Sep 07 13:48:55 2012 +0200 +++ b/test/unittest_entity.py Fri Sep 07 14:01:59 2012 +0200 @@ -701,23 +701,23 @@ self.assertEqual(card4.rest_path(), unicode(card4.eid)) - def test_set_attributes(self): + def test_cw_set_attributes(self): req = self.request() person = req.create_entity('Personne', nom=u'di mascio', prenom=u'adrien') self.assertEqual(person.prenom, u'adrien') self.assertEqual(person.nom, u'di mascio') - person.set_attributes(prenom=u'sylvain', nom=u'thénault') + person.cw_set(prenom=u'sylvain', nom=u'thénault') person = self.execute('Personne P').get_entity(0, 0) # XXX retreival needed ? self.assertEqual(person.prenom, u'sylvain') self.assertEqual(person.nom, u'thénault') - def test_set_relations(self): + def test_cw_set_relations(self): req = self.request() person = req.create_entity('Personne', nom=u'chauvat', prenom=u'nicolas') note = req.create_entity('Note', type=u'x') - note.set_relations(ecrit_par=person) + note.cw_set(ecrit_par=person) note = req.create_entity('Note', type=u'y') - note.set_relations(ecrit_par=person.eid) + note.cw_set(ecrit_par=person.eid) self.assertEqual(len(person.reverse_ecrit_par), 2) def test_metainformation_and_external_absolute_url(self): diff -r 6ed331fd4347 -r 268b6349baf3 web/data/cubicweb.css --- a/web/data/cubicweb.css Fri Sep 07 13:48:55 2012 +0200 +++ b/web/data/cubicweb.css Fri Sep 07 14:01:59 2012 +0200 @@ -545,6 +545,16 @@ padding-left: 2em; } +/* actions around tables */ +.tableactions span { + padding: 0 18px; + height: 24px; + background: #F8F8F8; + border: 1px solid #DFDFDF; + border-bottom: none; + border-radius: 4px 4px 0 0; +} + /* custom boxes */ .search_box div.boxBody { diff -r 6ed331fd4347 -r 268b6349baf3 web/data/cubicweb.old.css --- a/web/data/cubicweb.old.css Fri Sep 07 13:48:55 2012 +0200 +++ b/web/data/cubicweb.old.css Fri Sep 07 14:01:59 2012 +0200 @@ -899,6 +899,16 @@ padding-left: 0.5em; } +/* actions around tables */ +.tableactions span { + padding: 0 18px; + height: 24px; + background: #F8F8F8; + border: 1px solid #DFDFDF; + border-bottom: none; + border-radius: 4px 4px 0 0; +} + /***************************************/ /* error view (views/management.py) */ /***************************************/ diff -r 6ed331fd4347 -r 268b6349baf3 web/request.py --- a/web/request.py Fri Sep 07 13:48:55 2012 +0200 +++ b/web/request.py Fri Sep 07 14:01:59 2012 +0200 @@ -170,7 +170,6 @@ @property def authmode(self): """Authentification mode of the instance - (see :ref:`WebServerConfig`)""" return self.vreg.config['auth-mode'] diff -r 6ed331fd4347 -r 268b6349baf3 web/test/unittest_magicsearch.py --- a/web/test/unittest_magicsearch.py Fri Sep 07 13:48:55 2012 +0200 +++ b/web/test/unittest_magicsearch.py Fri Sep 07 14:01:59 2012 +0200 @@ -230,5 +230,118 @@ self.assertEqual(rset.rql, 'Any X ORDERBY FTIRANK(X) DESC WHERE X has_text %(text)s') self.assertEqual(rset.args, {'text': u'utilisateur Smith'}) + +class RQLSuggestionsBuilderTC(CubicWebTC): + def suggestions(self, rql): + req = self.request() + rbs = self.vreg['components'].select('rql.suggestions', req) + return rbs.build_suggestions(rql) + + def test_no_restrictions_rql(self): + self.assertListEqual([], self.suggestions('')) + self.assertListEqual([], self.suggestions('An')) + self.assertListEqual([], self.suggestions('Any X')) + self.assertListEqual([], self.suggestions('Any X, Y')) + + def test_invalid_rql(self): + self.assertListEqual([], self.suggestions('blabla')) + self.assertListEqual([], self.suggestions('Any X WHERE foo, bar')) + + def test_is_rql(self): + self.assertListEqual(['Any X WHERE X is %s' % eschema + for eschema in sorted(self.vreg.schema.entities()) + if not eschema.final], + self.suggestions('Any X WHERE X is')) + + self.assertListEqual(['Any X WHERE X is Personne', 'Any X WHERE X is Project'], + self.suggestions('Any X WHERE X is P')) + + self.assertListEqual(['Any X WHERE X is Personne, Y is Personne', + 'Any X WHERE X is Personne, Y is Project'], + self.suggestions('Any X WHERE X is Personne, Y is P')) + + + def test_relations_rql(self): + self.assertListEqual(['Any X WHERE X is Personne, X ass A', + 'Any X WHERE X is Personne, X datenaiss A', + 'Any X WHERE X is Personne, X description A', + 'Any X WHERE X is Personne, X fax A', + 'Any X WHERE X is Personne, X nom A', + 'Any X WHERE X is Personne, X prenom A', + 'Any X WHERE X is Personne, X promo A', + 'Any X WHERE X is Personne, X salary A', + 'Any X WHERE X is Personne, X sexe A', + 'Any X WHERE X is Personne, X tel A', + 'Any X WHERE X is Personne, X test A', + 'Any X WHERE X is Personne, X titre A', + 'Any X WHERE X is Personne, X travaille A', + 'Any X WHERE X is Personne, X web A', + ], + self.suggestions('Any X WHERE X is Personne, X ')) + self.assertListEqual(['Any X WHERE X is Personne, X tel A', + 'Any X WHERE X is Personne, X test A', + 'Any X WHERE X is Personne, X titre A', + 'Any X WHERE X is Personne, X travaille A', + ], + self.suggestions('Any X WHERE X is Personne, X t')) + # try completion on selected + self.assertListEqual(['Any X WHERE X is Personne, Y is Societe, X tel A', + 'Any X WHERE X is Personne, Y is Societe, X test A', + 'Any X WHERE X is Personne, Y is Societe, X titre A', + 'Any X WHERE X is Personne, Y is Societe, X travaille Y', + ], + self.suggestions('Any X WHERE X is Personne, Y is Societe, X t')) + # invalid relation should not break + self.assertListEqual([], + self.suggestions('Any X WHERE X is Personne, X asdasd')) + + def test_attribute_vocabulary_rql(self): + self.assertListEqual(['Any X WHERE X is Personne, X promo "bon"', + 'Any X WHERE X is Personne, X promo "pasbon"', + ], + self.suggestions('Any X WHERE X is Personne, X promo "')) + self.assertListEqual(['Any X WHERE X is Personne, X promo "pasbon"', + ], + self.suggestions('Any X WHERE X is Personne, X promo "p')) + # "bon" should be considered complete, hence no suggestion + self.assertListEqual([], + self.suggestions('Any X WHERE X is Personne, X promo "bon"')) + # no valid vocabulary starts with "po" + self.assertListEqual([], + self.suggestions('Any X WHERE X is Personne, X promo "po')) + + def test_attribute_value_rql(self): + # suggestions should contain any possible value for + # a given attribute (limited to 10) + req = self.request() + for i in xrange(15): + req.create_entity('Personne', nom=u'n%s' % i, prenom=u'p%s' % i) + self.assertListEqual(['Any X WHERE X is Personne, X nom "n0"', + 'Any X WHERE X is Personne, X nom "n1"', + 'Any X WHERE X is Personne, X nom "n10"', + 'Any X WHERE X is Personne, X nom "n11"', + 'Any X WHERE X is Personne, X nom "n12"', + 'Any X WHERE X is Personne, X nom "n13"', + 'Any X WHERE X is Personne, X nom "n14"', + 'Any X WHERE X is Personne, X nom "n2"', + 'Any X WHERE X is Personne, X nom "n3"', + 'Any X WHERE X is Personne, X nom "n4"', + 'Any X WHERE X is Personne, X nom "n5"', + 'Any X WHERE X is Personne, X nom "n6"', + 'Any X WHERE X is Personne, X nom "n7"', + 'Any X WHERE X is Personne, X nom "n8"', + 'Any X WHERE X is Personne, X nom "n9"', + ], + self.suggestions('Any X WHERE X is Personne, X nom "')) + self.assertListEqual(['Any X WHERE X is Personne, X nom "n1"', + 'Any X WHERE X is Personne, X nom "n10"', + 'Any X WHERE X is Personne, X nom "n11"', + 'Any X WHERE X is Personne, X nom "n12"', + 'Any X WHERE X is Personne, X nom "n13"', + 'Any X WHERE X is Personne, X nom "n14"', + ], + self.suggestions('Any X WHERE X is Personne, X nom "n1')) + + if __name__ == '__main__': unittest_main() diff -r 6ed331fd4347 -r 268b6349baf3 web/test/unittest_reledit.py --- a/web/test/unittest_reledit.py Fri Sep 07 13:48:55 2012 +0200 +++ b/web/test/unittest_reledit.py Fri Sep 07 14:01:59 2012 +0200 @@ -175,8 +175,8 @@ def setup_database(self): super(ClickAndEditFormUICFGTC, self).setup_database() - self.tick.set_relations(concerns=self.proj) - self.proj.set_relations(manager=self.toto) + self.tick.cw_set(concerns=self.proj) + self.proj.cw_set(manager=self.toto) def test_with_uicfg(self): old_rctl = reledit_ctrl._tagdefs.copy() diff -r 6ed331fd4347 -r 268b6349baf3 web/test/unittest_urlrewrite.py --- a/web/test/unittest_urlrewrite.py Fri Sep 07 13:48:55 2012 +0200 +++ b/web/test/unittest_urlrewrite.py Fri Sep 07 14:01:59 2012 +0200 @@ -105,9 +105,9 @@ def setup_database(self): req = self.request() self.p1 = self.create_user(req, u'user1') - self.p1.set_attributes(firstname=u'joe', surname=u'Dalton') + self.p1.cw_set(firstname=u'joe', surname=u'Dalton') self.p2 = self.create_user(req, u'user2') - self.p2.set_attributes(firstname=u'jack', surname=u'Dalton') + self.p2.cw_set(firstname=u'jack', surname=u'Dalton') def test_rgx_action_with_transforms(self): class TestSchemaBasedRewriter(SchemaBasedRewriter): diff -r 6ed331fd4347 -r 268b6349baf3 web/views/ajaxcontroller.py --- a/web/views/ajaxcontroller.py Fri Sep 07 13:48:55 2012 +0200 +++ b/web/views/ajaxcontroller.py Fri Sep 07 14:01:59 2012 +0200 @@ -28,7 +28,7 @@ functions that can be called from the javascript world. To register a new remote function, either decorate your function -with the :func:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator: +with the :func:`~cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator: .. sourcecode:: python @@ -39,7 +39,7 @@ def list_users(self): return [u for (u,) in self._cw.execute('Any L WHERE U login L')] -or inherit from :class:`cubicwbe.web.views.ajaxcontroller.AjaxFunction` and +or inherit from :class:`~cubicweb.web.views.ajaxcontroller.AjaxFunction` and implement the ``__call__`` method: .. sourcecode:: python diff -r 6ed331fd4347 -r 268b6349baf3 web/views/basecomponents.py --- a/web/views/basecomponents.py Fri Sep 07 13:48:55 2012 +0200 +++ b/web/views/basecomponents.py Fri Sep 07 14:01:59 2012 +0200 @@ -59,6 +59,14 @@ # display multilines query as one line rql = rset is not None and rset.printable_rql(encoded=False) or req.form.get('rql', '') rql = rql.replace(u"\n", u" ") + rql_suggestion_comp = self._cw.vreg['components'].select_or_none('rql.suggestions', self._cw) + if rql_suggestion_comp is not None: + # enable autocomplete feature only if the rql + # suggestions builder is available + self._cw.add_css('jquery.ui.css') + self._cw.add_js(('cubicweb.ajax.js', 'jquery.ui.js')) + self._cw.add_onload('$("#rql").autocomplete({source: "%s"});' + % (req.build_url('json', fname='rql_suggest'))) self.w(u'''
''' % (not self.cw_propval('visible') and 'hidden' or '', diff -r 6ed331fd4347 -r 268b6349baf3 web/views/magicsearch.py --- a/web/views/magicsearch.py Fri Sep 07 13:48:55 2012 +0200 +++ b/web/views/magicsearch.py Fri Sep 07 14:01:59 2012 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -15,19 +15,23 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""a query processor to handle quick search shortcuts for cubicweb""" +"""a query processor to handle quick search shortcuts for cubicweb +""" __docformat__ = "restructuredtext en" import re from logging import getLogger -from warnings import warn + +from yams.interfaces import IVocabularyConstraint from rql import RQLSyntaxError, BadRQLQuery, parse +from rql.utils import rqlvar_maker from rql.nodes import Relation from cubicweb import Unauthorized, typed_eid from cubicweb.view import Component +from cubicweb.web.views.ajaxcontroller import ajaxfunc LOGGER = getLogger('cubicweb.magicsearch') @@ -408,3 +412,247 @@ # explicitly specified processor: don't try to catch the exception return proc.process_query(uquery) raise BadRQLQuery(self._cw._('sorry, the server is unable to handle this query')) + + + +## RQL suggestions builder #################################################### +class RQLSuggestionsBuilder(Component): + """main entry point is `build_suggestions()` which takes + an incomplete RQL query and returns a list of suggestions to complete + the query. + + This component is enabled by default and is used to provide autocompletion + in the RQL search bar. If you don't want this feature in your application, + just unregister it or make it unselectable. + + .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.build_suggestions + .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.etypes_suggestion_set + .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.possible_etypes + .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.possible_relations + .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.vocabulary + """ + __regid__ = 'rql.suggestions' + + #: maximum number of results to fetch when suggesting attribute values + attr_value_limit = 20 + + def build_suggestions(self, user_rql): + """return a list of suggestions to complete `user_rql` + + :param user_rql: an incomplete RQL query + """ + req = self._cw + try: + if 'WHERE' not in user_rql: # don't try to complete if there's no restriction + return [] + variables, restrictions = [part.strip() for part in user_rql.split('WHERE', 1)] + if ',' in restrictions: + restrictions, incomplete_part = restrictions.rsplit(',', 1) + user_rql = '%s WHERE %s' % (variables, restrictions) + else: + restrictions, incomplete_part = '', restrictions + user_rql = variables + select = parse(user_rql, print_errors=False).children[0] + req.vreg.rqlhelper.annotate(select) + req.vreg.solutions(req, select, {}) + if restrictions: + return ['%s, %s' % (user_rql, suggestion) + for suggestion in self.rql_build_suggestions(select, incomplete_part)] + else: + return ['%s WHERE %s' % (user_rql, suggestion) + for suggestion in self.rql_build_suggestions(select, incomplete_part)] + except Exception, exc: # we never want to crash + self.debug('failed to build suggestions: %s', exc) + return [] + + ## actual completion entry points ######################################### + def rql_build_suggestions(self, select, incomplete_part): + """ + :param select: the annotated select node (rql syntax tree) + :param incomplete_part: the part of the rql query that needs + to be completed, (e.g. ``X is Pr``, ``X re``) + """ + chunks = incomplete_part.split(None, 2) + if not chunks: # nothing to complete + return [] + if len(chunks) == 1: # `incomplete` looks like "MYVAR" + return self._complete_rqlvar(select, *chunks) + elif len(chunks) == 2: # `incomplete` looks like "MYVAR some_rel" + return self._complete_rqlvar_and_rtype(select, *chunks) + elif len(chunks) == 3: # `incomplete` looks like "MYVAR some_rel something" + return self._complete_relation_object(select, *chunks) + else: # would be anything else, hard to decide what to do here + return [] + + # _complete_* methods are considered private, at least while the API + # isn't stabilized. + def _complete_rqlvar(self, select, rql_var): + """return suggestions for "variable only" incomplete_part + + as in : + + - Any X WHERE X + - Any X WHERE X is Project, Y + - etc. + """ + return ['%s %s %s' % (rql_var, rtype, dest_var) + for rtype, dest_var in self.possible_relations(select, rql_var)] + + def _complete_rqlvar_and_rtype(self, select, rql_var, user_rtype): + """return suggestions for "variable + rtype" incomplete_part + + as in : + + - Any X WHERE X is + - Any X WHERE X is Person, X firstn + - etc. + """ + # special case `user_type` == 'is', return every possible type. + if user_rtype == 'is': + return self._complete_is_relation(select, rql_var) + else: + return ['%s %s %s' % (rql_var, rtype, dest_var) + for rtype, dest_var in self.possible_relations(select, rql_var) + if rtype.startswith(user_rtype)] + + def _complete_relation_object(self, select, rql_var, user_rtype, user_value): + """return suggestions for "variable + rtype + some_incomplete_value" + + as in : + + - Any X WHERE X is Per + - Any X WHERE X is Person, X firstname " + - Any X WHERE X is Person, X firstname "Pa + - etc. + """ + # special case `user_type` == 'is', return every possible type. + if user_rtype == 'is': + return self._complete_is_relation(select, rql_var, user_value) + elif user_value: + if user_value[0] in ('"', "'"): + # if finished string, don't suggest anything + if len(user_value) > 1 and user_value[-1] == user_value[0]: + return [] + user_value = user_value[1:] + return ['%s %s "%s"' % (rql_var, user_rtype, value) + for value in self.vocabulary(select, rql_var, + user_rtype, user_value)] + return [] + + def _complete_is_relation(self, select, rql_var, prefix=''): + """return every possible types for rql_var + + :param prefix: if specified, will only return entity types starting + with the specified value. + """ + return ['%s is %s' % (rql_var, etype) + for etype in self.possible_etypes(select, rql_var, prefix)] + + def etypes_suggestion_set(self): + """returns the list of possible entity types to suggest + + The default is to return any non-final entity type available + in the schema. + + Can be overridden for instance if an application decides + to restrict this list to a meaningful set of business etypes. + """ + schema = self._cw.vreg.schema + return set(eschema.type for eschema in schema.entities() if not eschema.final) + + def possible_etypes(self, select, rql_var, prefix=''): + """return all possible etypes for `rql_var` + + The returned list will always be a subset of meth:`etypes_suggestion_set` + + :param select: the annotated select node (rql syntax tree) + :param rql_var: the variable name for which we want to know possible types + :param prefix: if specified, will only return etypes starting with it + """ + available_etypes = self.etypes_suggestion_set() + possible_etypes = set() + for sol in select.solutions: + if rql_var in sol and sol[rql_var] in available_etypes: + possible_etypes.add(sol[rql_var]) + if not possible_etypes: + # `Any X WHERE X is Person, Y is` + # -> won't have a solution, need to give all etypes + possible_etypes = available_etypes + return sorted(etype for etype in possible_etypes if etype.startswith(prefix)) + + def possible_relations(self, select, rql_var, include_meta=False): + """returns a list of couple (rtype, dest_var) for each possible + relations with `rql_var` as subject. + + ``dest_var`` will be picked among availabel variables if types match, + otherwise a new one will be created. + """ + schema = self._cw.vreg.schema + relations = set() + untyped_dest_var = rqlvar_maker(defined=select.defined_vars).next() + # for each solution + # 1. find each possible relation + # 2. for each relation: + # 2.1. if the relation is meta, skip it + # 2.2. for each possible destination type, pick up possible + # variables for this type or use a new one + for sol in select.solutions: + etype = sol[rql_var] + sol_by_types = {} + for varname, var_etype in sol.items(): + # don't push subject var to avoid "X relation X" suggestion + if varname != rql_var: + sol_by_types.setdefault(var_etype, []).append(varname) + for rschema in schema[etype].subject_relations(): + if include_meta or not rschema.meta: + for dest in rschema.objects(etype): + for varname in sol_by_types.get(dest.type, (untyped_dest_var,)): + suggestion = (rschema.type, varname) + if suggestion not in relations: + relations.add(suggestion) + return sorted(relations) + + def vocabulary(self, select, rql_var, user_rtype, rtype_incomplete_value): + """return acceptable vocabulary for `rql_var` + `user_rtype` in `select` + + Vocabulary is either found from schema (Yams) definition or + directly from database. + """ + schema = self._cw.vreg.schema + vocab = [] + for sol in select.solutions: + # for each solution : + # - If a vocabulary constraint exists on `rql_var+user_rtype`, use it + # to define possible values + # - Otherwise, query the database to fetch available values from + # database (limiting results to `self.attr_value_limit`) + try: + eschema = schema.eschema(sol[rql_var]) + rdef = eschema.rdef(user_rtype) + except KeyError: # unknown relation + continue + cstr = rdef.constraint_by_interface(IVocabularyConstraint) + if cstr is not None: + # a vocabulary is found, use it + vocab += [value for value in cstr.vocabulary() + if value.startswith(rtype_incomplete_value)] + elif rdef.final: + # no vocab, query database to find possible value + vocab_rql = 'DISTINCT Any V LIMIT %s WHERE X is %s, X %s V' % ( + self.attr_value_limit, eschema.type, user_rtype) + vocab_kwargs = {} + if rtype_incomplete_value: + vocab_rql += ', X %s LIKE %%(value)s' % user_rtype + vocab_kwargs['value'] = '%s%%' % rtype_incomplete_value + vocab += [value for value, in + self._cw.execute(vocab_rql, vocab_kwargs)] + return sorted(set(vocab)) + + + +@ajaxfunc(output_type='json') +def rql_suggest(self): + rql_builder = self._cw.vreg['components'].select_or_none('rql.suggestions', self._cw) + if rql_builder: + return rql_builder.build_suggestions(self._cw.form['term']) + return [] diff -r 6ed331fd4347 -r 268b6349baf3 web/views/tableview.py --- a/web/views/tableview.py Fri Sep 07 13:48:55 2012 +0200 +++ b/web/views/tableview.py Fri Sep 07 14:01:59 2012 +0200 @@ -290,20 +290,17 @@ return attrs def render_actions(self, w, actions): - box = MenuWidget('', '', _class='tableActionsBox', islist=False) - label = tags.span(self._cw._('action menu')) - menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox', - ident='%sActions' % self.view.domid) - box.append(menu) + w(u'
') for action in actions: - menu.append(action) - box.render(w=w) - w(u'
') + w(u'') + action.render(w) + w(u'') + w(u'
') def show_hide_filter_actions(self, currentlydisplayed=False): divid = self.view.domid showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:] - for what in ('Form', 'Show', 'Hide', 'Actions')) + for what in ('Form', 'Actions')) showhide = 'javascript:' + showhide self._cw.add_onload(u'''\ $(document).ready(function() { @@ -313,10 +310,8 @@ $('#%(id)sShow').attr('class', 'hidden'); } });''' % {'id': divid}) - showlabel = self._cw._('show filter form') - hidelabel = self._cw._('hide filter form') - return [component.Link(showhide, showlabel, id='%sShow' % divid), - component.Link(showhide, hidelabel, id='%sHide' % divid)] + showlabel = self._cw._('toggle filter') + return [component.Link(showhide, showlabel, id='%sToggle' % divid)] class AbstractColumnRenderer(object):