Commit 40b85384 authored by Sylvain Thénault's avatar Sylvain Thénault
Browse files

[entity] upgrade fetch_[unrelated_]order to benefit from changes introduced in...

[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
parent 3ecd114f6d75
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)
......@@ -67,8 +67,8 @@ cube. Let us begin to study the entities/project.py content.
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 @@ fully and portably expressed at the level of database entities (think
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
......
......@@ -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 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 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): ::
class MyEntity(AnyEntity):
__regid__ = 'MyEntity'
fetch_attrs = ('modification_date', 'name')
@classmethod
def fetch_unrelated_order(cls, attr, var):
if attr == 'name':
return '%s ASC' % var
return None
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`.
For example: ::
class Transition(AnyEntity):
"""..."""
__regid__ = 'Transition'
fetch_attrs, fetch_order = fetch_config(['name'])
Indicates that for the entity type "Transition", you have to pre-load
the attribute `name` and sort by default on this attribute.
* 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 :meth:`cw_fetch_order` and :meth:`cw_fetch_unrelated_order` class methods
are respectively responsible to control how entities will be sorted when:
- retrieving all entities of a given type, or entities related to another
- retrieving a list of entities for use in drop-down lists enabling relations
creation in the editing view of an entity
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:
.. 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:
.. autofunction:: cubicweb.entity.Entity.cw_fetch_order
.. autofunction:: cubicweb.entity.Entity.cw_fetch_unrelated_order
......@@ -138,20 +138,21 @@ There are still a few problems I want to solve...
* 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.
......
......@@ -389,7 +389,7 @@ method, etc...
"""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
......
......@@ -151,21 +151,34 @@ class AnyEntity(Entity):
# 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'):
if pclass is ANYENTITY:
pclass = AnyEntity # AnyEntity and ANYENTITY may be different classes
"""function to ease basic configuration of an entity class ORM. Basic usage
is:
.. sourcecode:: python
class MyEntity(AnyEntity):
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
......@@ -26,14 +26,14 @@ from cubicweb.entities import AnyEntity, fetch_config
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',)
......
......@@ -39,7 +39,7 @@ def mangle_email(address):
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 EmailAddress(AnyEntity):
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 Bookmark(AnyEntity):
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 CWProperty(AnyEntity):
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',
......
......@@ -31,7 +31,7 @@ from cubicweb.entities import AnyEntity, fetch_config
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 CWEType(AnyEntity):
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 CWAttribute(CWRelation):
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 CWConstraint(AnyEntity):
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''
......
......@@ -54,7 +54,7 @@ class _CWSourceCfgMixIn(object):
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 CWSource(_CWSourceCfgMixIn, AnyEntity):
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 CWSourceHostConfig(_CWSourceCfgMixIn, AnyEntity):
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
......
......@@ -183,7 +183,7 @@ class BaseTransition(AnyEntity):
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 SubWorkflowExitPoint(AnyEntity):
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 @@ class TrInfo(AnyEntity):
"""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]
......
......@@ -32,6 +32,7 @@ from rql.nodes import (Not, VariableRef, Constant, make_relation,
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 @@ class Entity(AppObject):
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
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`
"""
return cls.fetch_unrelated_order(attr, var)
cls.cw_fetch_unrelated_order(select, 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_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 @@ class Entity(AppObject):
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
......
# 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 Societe(AnyEntity):
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'
......
......@@ -33,12 +33,12 @@ class EntityTC(CubicWebTC):
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 @@ class EntityTC(CubicWebTC):
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 @@ class EntityTC(CubicWebTC):
"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 '
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment