--- a/web/facet.py Wed Aug 18 10:36:02 2010 +0200
+++ b/web/facet.py Wed Aug 18 13:58:12 2010 +0200
@@ -145,7 +145,8 @@
rqlst.add_relation(mainvar, rtype, newvar)
return newvar
-def _prepare_vocabulary_rqlst(rqlst, mainvar, rtype, role):
+def _prepare_vocabulary_rqlst(rqlst, mainvar, rtype, role,
+ select_target_entity=True):
"""prepare a syntax tree to generate a filter vocabulary rql using the given
relation:
* create a variable to filter on this relation
@@ -154,9 +155,10 @@
* add the new variable to the selection
"""
newvar = _add_rtype_relation(rqlst, mainvar, rtype, role)
- if rqlst.groupby:
- rqlst.add_group_var(newvar)
- rqlst.add_selected(newvar)
+ if select_target_entity:
+ if rqlst.groupby:
+ rqlst.add_group_var(newvar)
+ rqlst.add_selected(newvar)
# add is restriction if necessary
if mainvar.stinfo['typerel'] is None:
etypes = frozenset(sol[mainvar.name] for sol in rqlst.solutions)
@@ -191,7 +193,8 @@
rqlst.add_sort_term(term)
def insert_attr_select_relation(rqlst, mainvar, rtype, role, attrname,
- sortfuncname=None, sortasc=True):
+ sortfuncname=None, sortasc=True,
+ select_target_entity=True):
"""modify a syntax tree to :
* link a new variable to `mainvar` through `rtype` (where mainvar has `role`)
* retrieve only the newly inserted variable and its `attrname`
@@ -202,7 +205,8 @@
* no sort if `sortasc` is None
"""
_cleanup_rqlst(rqlst, mainvar)
- var = _prepare_vocabulary_rqlst(rqlst, mainvar, rtype, role)
+ var = _prepare_vocabulary_rqlst(rqlst, mainvar, rtype, role,
+ select_target_entity)
# not found, create one
attrvar = rqlst.make_variable()
rqlst.add_relation(var, attrname, attrvar)
@@ -354,12 +358,39 @@
class RelationFacet(VocabularyFacet):
+ """Base facet to filter some entities according to other entities to which
+ they are related. Create concret facet by inheriting from this class an then
+ configuring it by setting class attribute described below.
+
+ The relation is defined by the `rtype` and `role` attributes.
+
+ The values displayed for related entities will be:
+
+ * result of calling their `label_vid` view if specified
+ * else their `target_attr` attribute value if specified
+ * else their eid (you usually want something nicer...)
+
+ When no `label_vid` is set, you will get translated value if `i18nable` is
+ set.
+
+ You can filter out target entity types by specifying `target_type`
+
+ By default, vocabulary will be displayed sorted on `target_attr` value in an
+ ascending way. You can control sorting with:
+
+ * `sortfunc`: set this to a stored procedure name if you want to sort on the
+ result of this function's result instead of direct value
+
+ * `sortasc`: boolean flag to control ascendant/descendant sorting
+ """
__select__ = partial_relation_possible() & match_context_prop()
# class attributes to configure the relation facet
rtype = None
role = 'subject'
target_attr = 'eid'
target_type = None
+ # should value be internationalized (XXX could be guessed from the schema)
+ i18nable = True
# set this to a stored procedure name if you want to sort on the result of
# this function's result instead of direct value
sortfunc = None
@@ -368,24 +399,34 @@
# if you want to call a view on the entity instead of using `target_attr`
label_vid = None
+ # internal purpose
+ _select_target_entity = True
+
@property
def title(self):
return display_name(self._cw, self.rtype, form=self.role)
+ def rql_sort(self):
+ """return true if we can handle sorting in the rql query. E.g. if
+ sortfunc is set or if we have not to transform the returned value (eg no
+ label_vid and not i18nable)
+ """
+ return self.sortfunc is not None or (self.label_vid is None
+ and not self.i18nable)
def vocabulary(self):
"""return vocabulary for this facet, eg a list of 2-uple (label, value)
"""
rqlst = self.rqlst
rqlst.save_state()
- if self.label_vid is not None and self.sortfunc is None:
+ if self.rql_sort:
+ sort = self.sortasc
+ else:
sort = None # will be sorted on label
- else:
- sort = self.sortasc
try:
mainvar = self.filtered_variable
var = insert_attr_select_relation(
rqlst, mainvar, self.rtype, self.role, self.target_attr,
- self.sortfunc, sort)
+ self.sortfunc, sort, self._select_target_entity)
if self.target_type is not None:
rqlst.add_type_restriction(var, self.target_type)
try:
@@ -408,20 +449,37 @@
rqlst.save_state()
try:
_cleanup_rqlst(rqlst, self.filtered_variable)
- _prepare_vocabulary_rqlst(rqlst, self.filtered_variable, self.rtype, self.role)
+ if self._select_target_entity:
+ _prepare_vocabulary_rqlst(rqlst, self.filtered_variable, self.rtype,
+ self.role, select_target_entity=True)
+ else:
+ insert_attr_select_relation(
+ rqlst, self.filtered_variable, self.rtype, self.role, self.target_attr,
+ select_target_entity=False)
return [str(x) for x, in self.rqlexec(rqlst.as_string())]
+ except:
+ import traceback
+ traceback.print_exc()
finally:
rqlst.recover()
def rset_vocabulary(self, rset):
- if self.label_vid is None:
+ if self.i18nable:
_ = self._cw._
+ else:
+ _ = unicode
+ if self.rql_sort:
return [(_(label), eid) for eid, label in rset]
- if self.sortfunc is None:
- return sorted((entity.view(self.label_vid), entity.eid)
- for entity in rset.entities())
- return [(entity.view(self.label_vid), entity.eid)
- for entity in rset.entities()]
+ if self.label_vid is None:
+ assert self.i18nable
+ values = [(_(label), eid) for eid, label in rset]
+ else:
+ values = [(entity.view(self.label_vid), entity.eid)
+ for entity in rset.entities()]
+ values = sorted(values)
+ if self.sortasc:
+ return values
+ return reversed(values)
@cached
def support_and(self):
@@ -450,10 +508,10 @@
# only one value selected
self.rqlst.add_eid_restriction(restrvar, value)
elif self.operator == 'OR':
- # multiple values with OR operator
# set_distinct only if rtype cardinality is > 1
if self.support_and():
self.rqlst.set_distinct(True)
+ # multiple ORed values: using IN is fine
self.rqlst.add_eid_restriction(restrvar, value)
else:
# multiple values with AND operator
@@ -463,12 +521,66 @@
self.rqlst.add_eid_restriction(restrvar, value.pop())
-class AttributeFacet(RelationFacet):
+class RelationAttributeFacet(RelationFacet):
+ """Base facet to filter some entities according to an attribute of other
+ entities to which they are related. Most things work similarly as
+ :class:`RelationFacet`, except that:
+
+ * `label_vid` doesn't make sense here
+
+ * you should specify the attribute type using `attrtype` if it's not a
+ String
+
+ * you can specify a comparison operator using `comparator`
+ """
+ _select_target_entity = False
# attribute type
attrtype = 'String'
# type of comparison: default is an exact match on the attribute value
comparator = '=' # could be '<', '<=', '>', '>='
- i18nable = True
+
+ def rset_vocabulary(self, rset):
+ if self.i18nable:
+ _ = self._cw._
+ else:
+ _ = unicode
+ if self.rql_sort:
+ return [(_(value), value) for value, in rset]
+ values = [(_(value), value) for value, in rset]
+ if self.sortasc:
+ return sorted(values)
+ return reversed(sorted(values))
+
+ def add_rql_restrictions(self):
+ """add restriction for this facet into the rql syntax tree"""
+ value = self._cw.form.get(self.__regid__)
+ if not value:
+ return
+ mainvar = self.filtered_variable
+ restrvar = _add_rtype_relation(self.rqlst, mainvar, self.rtype, self.role)
+ self.rqlst.set_distinct(True)
+ if isinstance(value, basestring) or self.operator == 'OR':
+ # only one value selected or multiple ORed values: using IN is fine
+ self.rqlst.add_constant_restriction(restrvar, self.target_attr, value,
+ self.attrtype, self.comparator)
+ else:
+ # multiple values with AND operator
+ self.rqlst.add_constant_restriction(restrvar, self.target_attr, value.pop(),
+ self.attrtype, self.comparator)
+ while value:
+ restrvar = _add_rtype_relation(self.rqlst, mainvar, self.rtype, self.role)
+ self.rqlst.add_constant_restriction(restrvar, self.target_attr, value.pop(),
+ self.attrtype, self.comparator)
+
+
+class AttributeFacet(RelationAttributeFacet):
+ """Base facet to filter some entities according one of their attribute.
+ Configuration is mostly similarly as :class:`RelationAttributeFacet`, except that:
+
+ * `target_attr` doesn't make sense here (you specify the attribute using `rtype`
+ * `role` neither, it's systematically 'subject'
+ """
+ _select_target_entity = True
def vocabulary(self):
"""return vocabulary for this facet, eg a list of 2-uple (label, value)
@@ -492,13 +604,6 @@
# *list* (see rqlexec implementation)
return rset and self.rset_vocabulary(rset)
- def rset_vocabulary(self, rset):
- if self.i18nable:
- _ = self._cw._
- else:
- _ = unicode
- return [(_(value), value) for value, in rset]
-
def support_and(self):
return False
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_facet.py Wed Aug 18 13:58:12 2010 +0200
@@ -0,0 +1,88 @@
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.web import facet
+
+class BaseFacetTC(CubicWebTC):
+
+ def prepare_rqlst(self):
+ req = self.request()
+ rset = self.execute('CWUser X')
+ rqlst = rset.syntax_tree().copy()
+ req.vreg.rqlhelper.annotate(rqlst)
+ mainvar, baserql = facet.prepare_facets_rqlst(rqlst, rset.args)
+ self.assertEquals(mainvar.name, 'X')
+ self.assertEquals(baserql, 'Any X WHERE X is CWUser')
+ self.assertEquals(rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser')
+ return req, rset, rqlst, mainvar
+
+ def test_relation(self):
+ req, rset, rqlst, mainvar = self.prepare_rqlst()
+ f = facet.RelationFacet(req, rset=rset,
+ rqlst=rqlst.children[0],
+ filtered_variable=mainvar)
+ f.rtype = 'in_group'
+ f.role = 'subject'
+ f.target_attr = 'name'
+ guests, managers = [eid for eid, in self.execute('CWGroup G ORDERBY GN '
+ 'WHERE G name GN, G name IN ("guests", "managers")')]
+ self.assertEquals(f.vocabulary(),
+ [(u'guests', guests), (u'managers', managers)])
+ # ensure rqlst is left unmodified
+ self.assertEquals(rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser')
+ #rqlst = rset.syntax_tree()
+ self.assertEquals(f.possible_values(),
+ [str(guests), str(managers)])
+ # ensure rqlst is left unmodified
+ self.assertEquals(rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser')
+ req.form[f.__regid__] = str(guests)
+ f.add_rql_restrictions()
+ # selection is cluttered because rqlst has been prepared for facet (it
+ # is not in real life)
+ self.assertEquals(f.rqlst.as_string(),
+ 'DISTINCT Any WHERE X is CWUser, X in_group D, D eid %s' % guests)
+
+ def test_relationattribute(self):
+ req, rset, rqlst, mainvar = self.prepare_rqlst()
+ f = facet.RelationAttributeFacet(req, rset=rset,
+ rqlst=rqlst.children[0],
+ filtered_variable=mainvar)
+ f.rtype = 'in_group'
+ f.role = 'subject'
+ f.target_attr = 'name'
+ self.assertEquals(f.vocabulary(),
+ [(u'guests', u'guests'), (u'managers', u'managers')])
+ # ensure rqlst is left unmodified
+ self.assertEquals(rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser')
+ #rqlst = rset.syntax_tree()
+ self.assertEquals(f.possible_values(),
+ ['guests', 'managers'])
+ # ensure rqlst is left unmodified
+ self.assertEquals(rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser')
+ req.form[f.__regid__] = 'guests'
+ f.add_rql_restrictions()
+ # selection is cluttered because rqlst has been prepared for facet (it
+ # is not in real life)
+ self.assertEquals(f.rqlst.as_string(),
+ "DISTINCT Any WHERE X is CWUser, X in_group E, E name 'guests'")
+
+
+ def test_attribute(self):
+ req, rset, rqlst, mainvar = self.prepare_rqlst()
+ f = facet.AttributeFacet(req, rset=rset,
+ rqlst=rqlst.children[0],
+ filtered_variable=mainvar)
+ f.rtype = 'login'
+ self.assertEquals(f.vocabulary(),
+ [(u'admin', u'admin'), (u'anon', u'anon')])
+ # ensure rqlst is left unmodified
+ self.assertEquals(rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser')
+ #rqlst = rset.syntax_tree()
+ self.assertEquals(f.possible_values(),
+ ['admin', 'anon'])
+ # ensure rqlst is left unmodified
+ self.assertEquals(rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser')
+ req.form[f.__regid__] = 'admin'
+ f.add_rql_restrictions()
+ # selection is cluttered because rqlst has been prepared for facet (it
+ # is not in real life)
+ self.assertEquals(f.rqlst.as_string(),
+ "DISTINCT Any WHERE X is CWUser, X login 'admin'")