[entity] upgrade fetch_[unrelated_]order to benefit from changes introduced in 3.14 (closes #1942758)
of rql generation parts of the ORM now based on rql syntax tree. This allows more powerful and
flexible sort control by giving them the syntax tree instead of manipulating string.
Also:
* prefix new methods by 'cw_'
* fix cases that currently crash in 3.14 due to the refactoring
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/3.14.rst Thu Sep 22 16:12:23 2011 +0200
@@ -0,0 +1,25 @@
+API changes in cubicweb 3.14
+----------------------------
+
+* `Entity.fetch_order` and `Entity.fetch_unrelated_order` class methods have been
+ replaced by `Entity.cw_fetch_order` and `Entity.cw_fetch_unrelated_order` with
+ a different prototype:
+
+ - instead of taking (attr, var) as two string argument, they now take (select,
+ attr, var) where select is the rql syntax tree beinx constructed and var the
+ variable *node*.
+
+ - instead of returning some string to be inserted in the ORDERBY clause, it has
+ to modify the syntax tree
+
+ Backward compat is kept with proper warning, BESIDE cases below:
+
+ - custom order method return **something else the a variable name with or
+ without the sorting order** (e.g. cases where you sort on the value of a
+ registered procedure as it was done in the tracker for instance). In such
+ case, an error is logged telling that this sorting is ignored until API
+ upgrade.
+
+ - client code use direct access to one of those methods on an entity (no code
+ known to do that)
+
--- a/doc/book/en/devrepo/entityclasses/application-logic.rst Thu Sep 22 09:56:20 2011 +0200
+++ b/doc/book/en/devrepo/entityclasses/application-logic.rst Thu Sep 22 16:12:23 2011 +0200
@@ -67,8 +67,8 @@
class Project(AnyEntity):
__regid__ = 'Project'
- fetch_attrs, fetch_order = fetch_config(('name', 'description',
- 'description_format', 'summary'))
+ fetch_attrs, cw_fetch_order = fetch_config(('name', 'description',
+ 'description_format', 'summary'))
TICKET_DEFAULT_STATE_RESTR = 'S name IN ("created","identified","released","scheduled")'
@@ -95,11 +95,9 @@
about the transitive closure of the child relation). This is a further
argument to implement it at entity class level.
-The fetch_attrs, fetch_order class attributes are parameters of the
-`ORM`_ layer. They tell which attributes should be loaded at once on
-entity object instantiation (by default, only the eid is known, other
-attributes are loaded on demand), and which attribute is to be used to
-order the .related() and .unrelated() methods output.
+`fetch_attrs` configures which attributes should be prefetched when using ORM
+methods retrieving entity of this type. In a same manner, the `cw_fetch_order` is
+a class method allowing to control sort order. More on this in :ref:FetchAttrs.
We can observe the big TICKET_DEFAULT_STATE_RESTR is a pure
application domain piece of data. There is, of course, no limitation
--- a/doc/book/en/devrepo/entityclasses/load-sort.rst Thu Sep 22 09:56:20 2011 +0200
+++ b/doc/book/en/devrepo/entityclasses/load-sort.rst Thu Sep 22 16:12:23 2011 +0200
@@ -4,50 +4,36 @@
Loaded attributes and default sorting management
````````````````````````````````````````````````
-* The class attribute `fetch_attrs` allows to define in an entity class a list
- of names of attributes or relations that should be automatically loaded when
- entities of this type are fetched from the database. In the case of relations,
- we are limited to *subject of cardinality `?` or `1`* relations.
+* The class attribute `fetch_attrs` allows to define in an entity class a list of
+ names of attributes that should be automatically loaded when entities of this
+ type are fetched from the database using ORM methods retrieving entity of this
+ type (such as :meth:`related` and :meth:`unrelated`). You can also put relation
+ names in there, but we are limited to *subject relations of cardinality `?` or
+ `1`*.
-* The class method `fetch_order(attr, var)` expects an attribute (or relation)
- name as a parameter and a variable name, and it should return a string
- to use in the requirement `ORDERBY` of an RQL query to automatically
- sort the list of entities of such type according to this attribute, or
- `None` if we do not want to sort on the attribute given in the parameter.
- By default, the entities are sorted according to their creation date.
+* The :meth:`cw_fetch_order` and :meth:`cw_fetch_unrelated_order` class methods
+ are respectively responsible to control how entities will be sorted when:
-* The class method `fetch_unrelated_order(attr, var)` is similar to
- the method `fetch_order` except that it is essentially used to
- control the sorting of drop-down lists enabling relations creation
- in the editing view of an entity. The default implementation uses
- the modification date. Here's how to adapt it for one entity (sort
- on the name attribute): ::
+ - retrieving all entities of a given type, or entities related to another
- class MyEntity(AnyEntity):
- __regid__ = 'MyEntity'
- fetch_attrs = ('modification_date', 'name')
+ - retrieving a list of entities for use in drop-down lists enabling relations
+ creation in the editing view of an entity
- @classmethod
- def fetch_unrelated_order(cls, attr, var):
- if attr == 'name':
- return '%s ASC' % var
- return None
+By default entities will be listed on their modification date descending,
+i.e. you'll get entities recently modified first. While this is usually a good
+default in drop-down list, you'll probably want to change `cw_fetch_order`.
+This may easily be done using the :func:`~cubicweb.entities.fetch_config`
+function, which simplifies the definition of attributes to load and sorting by
+returning a list of attributes to pre-load (considering automatically the
+attributes of `AnyEntity`) and a sorting function as described below:
-The function `fetch_config(fetchattrs, mainattr=None)` simplifies the
-definition of the attributes to load and the sorting by returning a
-list of attributes to pre-load (considering automatically the
-attributes of `AnyEntity`) and a sorting function based on the main
-attribute (the second parameter if specified, otherwise the first
-attribute from the list `fetchattrs`). This function is defined in
-`cubicweb.entities`.
+.. autofunction:: cubicweb.entities.fetch_config
+
+In you want something else (such as sorting on the result of a registered
+procedure), here is the prototype of those methods:
-For example: ::
+.. autofunction:: cubicweb.entity.Entity.cw_fetch_order
- class Transition(AnyEntity):
- """..."""
- __regid__ = 'Transition'
- fetch_attrs, fetch_order = fetch_config(['name'])
+.. autofunction:: cubicweb.entity.Entity.cw_fetch_unrelated_order
-Indicates that for the entity type "Transition", you have to pre-load
-the attribute `name` and sort by default on this attribute.
--- a/doc/book/en/tutorials/advanced/part04_ui-base.rst Thu Sep 22 09:56:20 2011 +0200
+++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst Thu Sep 22 16:12:23 2011 +0200
@@ -138,20 +138,21 @@
* Also, when viewing an image, there is no clue about the folder to which this
image belongs to.
-I will first try to explain the ordering problem. By default, when accessing related
-entities by using the ORM's API, you should get them ordered according to the target's
-class `fetch_order`. If we take a look at the file cube'schema, we can see:
+I will first try to explain the ordering problem. By default, when accessing
+related entities by using the ORM's API, you should get them ordered according to
+the target's class `cw_fetch_order`. If we take a look at the file cube'schema,
+we can see:
.. sourcecode:: python
-
class File(AnyEntity):
"""customized class for File entities"""
__regid__ = 'File'
- fetch_attrs, fetch_order = fetch_config(['data_name', 'title'])
+ fetch_attrs, cw_fetch_order = fetch_config(['data_name', 'title'])
+
-By default, `fetch_config` will return a `fetch_order` method that will order on
-the first attribute in the list. So, we could expect to get files ordered by
+By default, `fetch_config` will return a `cw_fetch_order` method that will order
+on the first attribute in the list. So, we could expect to get files ordered by
their name. But we don't. What's up doc ?
The problem is that files are related to folder using the `filed_under` relation.
--- a/doc/book/en/tutorials/base/customizing-the-application.rst Thu Sep 22 09:56:20 2011 +0200
+++ b/doc/book/en/tutorials/base/customizing-the-application.rst Thu Sep 22 16:12:23 2011 +0200
@@ -389,7 +389,7 @@
"""customized class for Community entities"""
__regid__ = 'Community'
- fetch_attrs, fetch_order = fetch_config(['name'])
+ fetch_attrs, cw_fetch_order = fetch_config(['name'])
def dc_title(self):
return self.name
--- a/entities/__init__.py Thu Sep 22 09:56:20 2011 +0200
+++ b/entities/__init__.py Thu Sep 22 16:12:23 2011 +0200
@@ -151,21 +151,34 @@
# server side helpers #####################################################
-# XXX: store a reference to the AnyEntity class since it is hijacked in goa
-# configuration and we need the actual reference to avoid infinite loops
-# in mro
-ANYENTITY = AnyEntity
+def fetch_config(fetchattrs, mainattr=None, pclass=AnyEntity, order='ASC'):
+ """function to ease basic configuration of an entity class ORM. Basic usage
+ is:
+
+ .. sourcecode:: python
+
+ class MyEntity(AnyEntity):
-def fetch_config(fetchattrs, mainattr=None, pclass=AnyEntity, order='ASC'):
- if pclass is ANYENTITY:
- pclass = AnyEntity # AnyEntity and ANYENTITY may be different classes
+ fetch_attrs, cw_fetch_order = fetch_config(['attr1', 'attr2'])
+ # uncomment line below if you want the same sorting for 'unrelated' entities
+ # cw_fetch_unrelated_order = cw_fetch_order
+
+ Using this, when using ORM methods retrieving this type of entity, 'attr1'
+ and 'attr2' will be automatically prefetched and results will be sorted on
+ 'attr1' ascending (ie the first attribute in the list).
+
+ This function will automatically add to fetched attributes those defined in
+ parent class given using the `pclass` argument.
+
+ Also, You can use `mainattr` and `order` argument to have a different
+ sorting.
+ """
if pclass is not None:
fetchattrs += pclass.fetch_attrs
if mainattr is None:
mainattr = fetchattrs[0]
@classmethod
- def fetch_order(cls, attr, var):
+ def fetch_order(cls, select, attr, var):
if attr == mainattr:
- return '%s %s' % (var, order)
- return None
+ select.add_sort_var(var, order=='ASC')
return fetchattrs, fetch_order
--- a/entities/authobjs.py Thu Sep 22 09:56:20 2011 +0200
+++ b/entities/authobjs.py Thu Sep 22 16:12:23 2011 +0200
@@ -26,14 +26,14 @@
class CWGroup(AnyEntity):
__regid__ = 'CWGroup'
- fetch_attrs, fetch_order = fetch_config(['name'])
- fetch_unrelated_order = fetch_order
+ fetch_attrs, cw_fetch_order = fetch_config(['name'])
+ cw_fetch_unrelated_order = cw_fetch_order
class CWUser(AnyEntity):
__regid__ = 'CWUser'
- fetch_attrs, fetch_order = fetch_config(['login', 'firstname', 'surname'])
- fetch_unrelated_order = fetch_order
+ fetch_attrs, cw_fetch_order = fetch_config(['login', 'firstname', 'surname'])
+ cw_fetch_unrelated_order = cw_fetch_order
# used by repository to check if the user can log in or not
AUTHENTICABLE_STATES = ('activated',)
--- a/entities/lib.py Thu Sep 22 09:56:20 2011 +0200
+++ b/entities/lib.py Thu Sep 22 16:12:23 2011 +0200
@@ -39,7 +39,7 @@
class EmailAddress(AnyEntity):
__regid__ = 'EmailAddress'
- fetch_attrs, fetch_order = fetch_config(['address', 'alias'])
+ fetch_attrs, cw_fetch_order = fetch_config(['address', 'alias'])
rest_attr = 'eid'
def dc_title(self):
@@ -94,7 +94,7 @@
class Bookmark(AnyEntity):
"""customized class for Bookmark entities"""
__regid__ = 'Bookmark'
- fetch_attrs, fetch_order = fetch_config(['title', 'path'])
+ fetch_attrs, cw_fetch_order = fetch_config(['title', 'path'])
def actual_url(self):
url = self._cw.build_url(self.path)
@@ -114,7 +114,7 @@
class CWProperty(AnyEntity):
__regid__ = 'CWProperty'
- fetch_attrs, fetch_order = fetch_config(['pkey', 'value'])
+ fetch_attrs, cw_fetch_order = fetch_config(['pkey', 'value'])
rest_attr = 'pkey'
def typed_value(self):
@@ -130,7 +130,7 @@
class CWCache(AnyEntity):
"""Cache"""
__regid__ = 'CWCache'
- fetch_attrs, fetch_order = fetch_config(['name'])
+ fetch_attrs, cw_fetch_order = fetch_config(['name'])
def touch(self):
self._cw.execute('SET X timestamp %(t)s WHERE X eid %(x)s',
--- a/entities/schemaobjs.py Thu Sep 22 09:56:20 2011 +0200
+++ b/entities/schemaobjs.py Thu Sep 22 16:12:23 2011 +0200
@@ -31,7 +31,7 @@
class CWEType(AnyEntity):
__regid__ = 'CWEType'
- fetch_attrs, fetch_order = fetch_config(['name'])
+ fetch_attrs, cw_fetch_order = fetch_config(['name'])
def dc_title(self):
return u'%s (%s)' % (self.name, self._cw._(self.name))
@@ -48,7 +48,7 @@
class CWRType(AnyEntity):
__regid__ = 'CWRType'
- fetch_attrs, fetch_order = fetch_config(['name'])
+ fetch_attrs, cw_fetch_order = fetch_config(['name'])
def dc_title(self):
return u'%s (%s)' % (self.name, self._cw._(self.name))
@@ -139,7 +139,7 @@
class CWConstraint(AnyEntity):
__regid__ = 'CWConstraint'
- fetch_attrs, fetch_order = fetch_config(['value'])
+ fetch_attrs, cw_fetch_order = fetch_config(['value'])
def dc_title(self):
return '%s(%s)' % (self.cstrtype[0].name, self.value or u'')
@@ -151,7 +151,7 @@
class RQLExpression(AnyEntity):
__regid__ = 'RQLExpression'
- fetch_attrs, fetch_order = fetch_config(['exprtype', 'mainvars', 'expression'])
+ fetch_attrs, cw_fetch_order = fetch_config(['exprtype', 'mainvars', 'expression'])
def dc_title(self):
return self.expression or u''
--- a/entities/sources.py Thu Sep 22 09:56:20 2011 +0200
+++ b/entities/sources.py Thu Sep 22 16:12:23 2011 +0200
@@ -54,7 +54,7 @@
class CWSource(_CWSourceCfgMixIn, AnyEntity):
__regid__ = 'CWSource'
- fetch_attrs, fetch_order = fetch_config(['name', 'type'])
+ fetch_attrs, cw_fetch_order = fetch_config(['name', 'type'])
@property
def host_config(self):
@@ -107,7 +107,7 @@
class CWSourceHostConfig(_CWSourceCfgMixIn, AnyEntity):
__regid__ = 'CWSourceHostConfig'
- fetch_attrs, fetch_order = fetch_config(['match_host', 'config'])
+ fetch_attrs, cw_fetch_order = fetch_config(['match_host', 'config'])
@property
def cwsource(self):
@@ -119,7 +119,7 @@
class CWSourceSchemaConfig(AnyEntity):
__regid__ = 'CWSourceSchemaConfig'
- fetch_attrs, fetch_order = fetch_config(['cw_for_source', 'cw_schema', 'options'])
+ fetch_attrs, cw_fetch_order = fetch_config(['cw_for_source', 'cw_schema', 'options'])
def dc_title(self):
return self._cw._(self.__regid__) + ' #%s' % self.eid
--- a/entities/wfobjs.py Thu Sep 22 09:56:20 2011 +0200
+++ b/entities/wfobjs.py Thu Sep 22 16:12:23 2011 +0200
@@ -183,7 +183,7 @@
fired by the logged user
"""
__regid__ = 'BaseTransition'
- fetch_attrs, fetch_order = fetch_config(['name', 'type'])
+ fetch_attrs, cw_fetch_order = fetch_config(['name', 'type'])
def __init__(self, *args, **kwargs):
if self.__regid__ == 'BaseTransition':
@@ -347,7 +347,7 @@
class State(AnyEntity):
"""customized class for State entities"""
__regid__ = 'State'
- fetch_attrs, fetch_order = fetch_config(['name'])
+ fetch_attrs, cw_fetch_order = fetch_config(['name'])
rest_attr = 'eid'
@property
@@ -360,8 +360,8 @@
"""customized class for Transition information entities
"""
__regid__ = 'TrInfo'
- fetch_attrs, fetch_order = fetch_config(['creation_date', 'comment'],
- pclass=None) # don't want modification_date
+ fetch_attrs, cw_fetch_order = fetch_config(['creation_date', 'comment'],
+ pclass=None) # don't want modification_date
@property
def for_entity(self):
return self.wf_info_for[0]
--- a/entity.py Thu Sep 22 09:56:20 2011 +0200
+++ b/entity.py Thu Sep 22 16:12:23 2011 +0200
@@ -32,6 +32,7 @@
Relation as RqlRelation)
from cubicweb import Unauthorized, typed_eid, neg_role
+from cubicweb.utils import support_args
from cubicweb.rset import ResultSet
from cubicweb.selectors import yes
from cubicweb.appobject import AppObject
@@ -140,22 +141,59 @@
cls.info('plugged %s mixins on %s', mixins, cls)
fetch_attrs = ('modification_date',)
- @classmethod
- def fetch_order(cls, attr, var):
- """class method used to control sort order when multiple entities of
- this type are fetched
- """
- return cls.fetch_unrelated_order(attr, var)
@classmethod
- def fetch_unrelated_order(cls, attr, var):
- """class method used to control sort order when multiple entities of
- this type are fetched to use in edition (eg propose them to create a
+ def cw_fetch_order(cls, select, attr, var):
+ """This class method may be used to control sort order when multiple
+ entities of this type are fetched through ORM methods. Its arguments
+ are:
+
+ * `select`, the RQL syntax tree
+
+ * `attr`, the attribute being watched
+
+ * `var`, the variable through which this attribute's value may be
+ accessed in the query
+
+ When you want to do some sorting on the given attribute, you should
+ modify the syntax tree accordingly. For instance:
+
+ .. sourcecode:: python
+
+ from rql import nodes
+
+ class Version(AnyEntity):
+ __regid__ = 'Version'
+
+ fetch_attrs = ('num', 'description', 'in_state')
+
+ @classmethod
+ def cw_fetch_order(cls, select, attr, var):
+ if attr == 'num':
+ func = nodes.Function('version_sort_value')
+ func.append(nodes.variable_ref(var))
+ sterm = nodes.SortTerm(func, asc=False)
+ select.add_sort_term(sterm)
+
+ The default implementation call
+ :meth:`~cubicweb.entity.Entity.cw_fetch_unrelated_order`
+ """
+ cls.cw_fetch_unrelated_order(select, attr, var)
+
+ @classmethod
+ def cw_fetch_unrelated_order(cls, select, attr, var):
+ """This class method may be used to control sort order when multiple entities of
+ this type are fetched to use in edition (e.g. propose them to create a
new relation on an edited entity).
+
+ See :meth:`~cubicweb.entity.Entity.cw_fetch_unrelated_order` for a
+ description of its arguments and usage.
+
+ By default entities will be listed on their modification date descending,
+ i.e. you'll get entities recently modified first.
"""
if attr == 'modification_date':
- return '%s DESC' % var
- return None
+ select.add_sort_var(var, asc=False)
@classmethod
def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
@@ -276,10 +314,34 @@
etypecls._fetch_restrictions(var, select, fetchattrs,
user, ordermethod, visited=visited)
if ordermethod is not None:
- orderterm = getattr(cls, ordermethod)(attr, var.name)
- if orderterm:
- var, order = orderterm.split()
- select.add_sort_var(select.get_variable(var), order=='ASC')
+ try:
+ cmeth = getattr(cls, ordermethod)
+ warn('[3.14] %s %s class method should be renamed to cw_%s'
+ % (cls.__regid__, ordermethod, ordermethod),
+ DeprecationWarning)
+ except AttributeError:
+ cmeth = getattr(cls, 'cw_' + ordermethod)
+ if support_args(cmeth, 'select'):
+ cmeth(select, attr, var)
+ else:
+ warn('[3.14] %s should now take (select, attr, var) and '
+ 'modify the syntax tree when desired instead of '
+ 'returning something' % cmeth, DeprecationWarning)
+ orderterm = cmeth(attr, var.name)
+ if orderterm is not None:
+ try:
+ var, order = orderterm.split()
+ except ValueError:
+ if '(' in orderterm:
+ self.error('ignore %s until %s is upgraded',
+ orderterm, cmeth)
+ orderterm = None
+ elif not ' ' in orderterm.strip():
+ var = orderterm
+ order = 'ASC'
+ if orderterm is not None:
+ select.add_sort_var(select.get_variable(var),
+ order=='ASC')
@classmethod
@cached
--- a/test/data/entities.py Thu Sep 22 09:56:20 2011 +0200
+++ b/test/data/entities.py Thu Sep 22 16:12:23 2011 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -15,9 +15,7 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""
-"""
from cubicweb.entities import AnyEntity, fetch_config
class Societe(AnyEntity):
@@ -27,7 +25,7 @@
class Personne(Societe):
"""customized class forne Person entities"""
__regid__ = 'Personne'
- fetch_attrs, fetch_order = fetch_config(['nom', 'prenom'])
+ fetch_attrs, cw_fetch_order = fetch_config(['nom', 'prenom'])
rest_attr = 'nom'
--- a/test/unittest_entity.py Thu Sep 22 09:56:20 2011 +0200
+++ b/test/unittest_entity.py Thu Sep 22 16:12:23 2011 +0200
@@ -33,12 +33,12 @@
super(EntityTC, self).setUp()
self.backup_dict = {}
for cls in self.vreg['etypes'].iter_classes():
- self.backup_dict[cls] = (cls.fetch_attrs, cls.fetch_order)
+ self.backup_dict[cls] = (cls.fetch_attrs, cls.cw_fetch_order)
def tearDown(self):
super(EntityTC, self).tearDown()
for cls in self.vreg['etypes'].iter_classes():
- cls.fetch_attrs, cls.fetch_order = self.backup_dict[cls]
+ cls.fetch_attrs, cls.cw_fetch_order = self.backup_dict[cls]
def test_boolean_value(self):
e = self.vreg['etypes'].etype_class('CWUser')(self.request())
@@ -228,9 +228,9 @@
Note = self.vreg['etypes'].etype_class('Note')
SubNote = self.vreg['etypes'].etype_class('SubNote')
self.assertTrue(issubclass(self.vreg['etypes'].etype_class('SubNote'), Note))
- Personne.fetch_attrs, Personne.fetch_order = fetch_config(('nom', 'type'))
- Note.fetch_attrs, Note.fetch_order = fetch_config(('type',))
- SubNote.fetch_attrs, SubNote.fetch_order = fetch_config(('type',))
+ Personne.fetch_attrs, Personne.cw_fetch_order = fetch_config(('nom', 'type'))
+ Note.fetch_attrs, Note.cw_fetch_order = fetch_config(('type',))
+ SubNote.fetch_attrs, SubNote.cw_fetch_order = fetch_config(('type',))
p = self.request().create_entity('Personne', nom=u'pouet')
self.assertEqual(p.cw_related_rql('evaluee'),
'Any X,AA,AB ORDERBY AA WHERE E eid %(x)s, E evaluee X, '
@@ -241,7 +241,7 @@
"Any X,AA ORDERBY AB DESC WHERE E eid %(x)s, X evaluee E, "
"X is IN('Personne', 'Societe'), X nom AA, "
"X modification_date AB")
- Personne.fetch_attrs, Personne.fetch_order = fetch_config(('nom', ))
+ Personne.fetch_attrs, Personne.cw_fetch_order = fetch_config(('nom', ))
# XXX
self.assertEqual(p.cw_related_rql('evaluee'),
'Any X,AA ORDERBY AA DESC '