# HG changeset patch # User Sylvain Thénault # Date 1285944815 -7200 # Node ID 63d5dbaef99985f596110c9b1ad789cfec86c058 # Parent 3f67f7ea56324746274a85424bd3a6afd02825c1 [facets] support for `no_relation` on RelationFacet e.g. to filter en entities *without* a given relation as well as with a particular relation. Proposed or not by default according to relation cardinality. Also, try to guess i18nable properly from the schema by default. diff -r 3f67f7ea5632 -r 63d5dbaef999 __pkginfo__.py --- a/__pkginfo__.py Fri Oct 01 16:07:03 2010 +0200 +++ b/__pkginfo__.py Fri Oct 01 16:53:35 2010 +0200 @@ -42,7 +42,7 @@ __depends__ = { 'logilab-common': '>= 0.51.0', 'logilab-mtconverter': '>= 0.8.0', - 'rql': '>= 0.26.2', + 'rql': '>= 0.27.0', 'yams': '>= 0.30.1', 'docutils': '>= 0.6', #gettext # for xgettext, msgcat, etc... diff -r 3f67f7ea5632 -r 63d5dbaef999 debian/control --- a/debian/control Fri Oct 01 16:07:03 2010 +0200 +++ b/debian/control Fri Oct 01 16:53:35 2010 +0200 @@ -97,7 +97,7 @@ Package: cubicweb-common Architecture: all XB-Python-Version: ${python:Versions} -Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.51.0), python-yams (>= 0.30.1), python-rql (>= 0.26.3), python-lxml +Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.51.0), python-yams (>= 0.30.1), python-rql (>= 0.27.0), python-lxml Recommends: python-simpletal (>= 4.0), python-crypto Conflicts: cubicweb-core Replaces: cubicweb-core diff -r 3f67f7ea5632 -r 63d5dbaef999 web/facet.py --- a/web/facet.py Fri Oct 01 16:07:03 2010 +0200 +++ b/web/facet.py Fri Oct 01 16:53:35 2010 +0200 @@ -55,7 +55,7 @@ from logilab.common.date import datetime2ticks from logilab.common.compat import all -from rql import parse, nodes +from rql import parse, nodes, utils from cubicweb import Unauthorized, typed_eid from cubicweb.schema import display_name @@ -165,18 +165,27 @@ return ovar return None +def _make_relation(rqlst, mainvar, rtype, role): + newvar = rqlst.make_variable() + if role == 'object': + rel = nodes.make_relation(newvar, rtype, (mainvar,), nodes.VariableRef) + else: + rel = nodes.make_relation(mainvar, rtype, (newvar,), nodes.VariableRef) + return newvar, rel + def _add_rtype_relation(rqlst, mainvar, rtype, role): """add a relation relying `mainvar` to entities linked by the `rtype` relation (where `mainvar` has `role`) return the inserted variable for linked entities. """ - newvar = rqlst.make_variable() - if role == 'object': - rqlst.add_relation(newvar, rtype, mainvar) - else: - rqlst.add_relation(mainvar, rtype, newvar) - return newvar + newvar, newrel = _make_relation(rqlst, mainvar, rtype, role) + rqlst.add_restriction(newrel) + return newvar, newrel + +def _add_eid_restr(rel, restrvar, value): + rrel = nodes.make_constant_restriction(restrvar, 'eid', value, 'Int') + rel.parent.replace(rel, nodes.And(rel, rrel)) def _prepare_vocabulary_rqlst(rqlst, mainvar, rtype, role, select_target_entity=True): @@ -187,7 +196,7 @@ * add the new variable to GROUPBY clause if necessary * add the new variable to the selection """ - newvar = _add_rtype_relation(rqlst, mainvar, rtype, role) + newvar = _add_rtype_relation(rqlst, mainvar, rtype, role)[0] if select_target_entity: if rqlst.groupby: rqlst.add_group_var(newvar) @@ -240,7 +249,6 @@ _cleanup_rqlst(rqlst, mainvar) 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) # if query is grouped, we have to add the attribute variable @@ -449,6 +457,10 @@ The relation is defined by the `rtype` and `role` attributes. + The `no_relation` boolean flag tells if a special 'no relation' value should be + added (allowing to filter on entities which *do not* have the relation set). + Default is computed according the relation's cardinality. + The values displayed for related entities will be: * result of calling their `label_vid` view if specified @@ -456,7 +468,8 @@ * else their eid (you usually want something nicer...) When no `label_vid` is set, you will get translated value if `i18nable` is - set. + set. By default, `i18nable` will be set according to the schema, but you can + force its value by setting it has a class attribute. You can filter out target entity types by specifying `target_type` @@ -506,8 +519,6 @@ 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 @@ -520,6 +531,23 @@ _select_target_entity = True title = property(rtype_facet_title) + no_relation_label = '' + + @property + def i18nable(self): + """should label be internationalized""" + if self.target_type: + eschema = self._cw.vreg.schema.eschema(self.target_type) + elif self.role == 'subject': + eschema = self._cw.vreg.schema.rschema(self.rtype).objects()[0] + else: + eschema = self._cw.vreg.schema.rschema(self.rtype).subjects()[0] + return eschema.rdef(self.target_attr).internationalizable + + @property + def no_relation(self): + return (not self._cw.vreg.schema.rschema(self.rtype).final + and self._search_card('?*')) @property def rql_sort(self): @@ -529,6 +557,7 @@ """ 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) """ @@ -555,7 +584,10 @@ rqlst.recover() # don't call rset_vocabulary on empty result set, it may be an empty # *list* (see rqlexec implementation) - return rset and self.rset_vocabulary(rset) + values = rset and self.rset_vocabulary(rset) or [] + if self._include_no_relation(): + values.insert(0, (self._cw._(self.no_relation_label), '')) + return values def possible_values(self): """return a list of possible values (as string since it's used to @@ -572,12 +604,14 @@ 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())] + values = [str(x) for x, in self.rqlexec(rqlst.as_string())] except: - import traceback - traceback.print_exc() + self.exception('while computing values for %s', self) finally: rqlst.recover() + if self._include_no_relation(): + values.append('') + return values def rset_vocabulary(self, rset): if self.i18nable: @@ -585,56 +619,103 @@ else: _ = unicode if self.rql_sort: - return [(_(label), eid) for eid, label in rset] - 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) + if self.label_vid is None: + 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 not self.sortasc: + values = list(reversed(values)) + return values + + def support_and(self): + return self._search_card('+*') + + def add_rql_restrictions(self): + """add restriction for this facet into the rql syntax tree""" + value = self._cw.form.get(self.__regid__) + if value is None: + return + mainvar = self.filtered_variable + restrvar, rel = _add_rtype_relation(self.rqlst, mainvar, self.rtype, + self.role) + if isinstance(value, basestring): + # only one value selected + if value: + self.rqlst.add_eid_restriction(restrvar, value) + else: + rel.parent.replace(rel, nodes.Not(rel)) + elif self.operator == 'OR': + # set_distinct only if rtype cardinality is > 1 + if self.support_and(): + self.rqlst.set_distinct(True) + # multiple ORed values: using IN is fine + if '' in value: + value.remove('') + self._add_not_rel_restr(rel) + _add_eid_restr(rel, restrvar, value) + else: + # multiple values with AND operator + if '' in value: + value.remove('') + self._add_not_rel_restr(rel) + _add_eid_restr(rel, restrvar, value.pop()) + while value: + restrvar, rtrel = _make_relation(self.rqlst, mainvar, + self.rtype, self.role) + _add_eid_restr(rel, restrvar, value.pop()) @cached - def support_and(self): + def _search_card(self, cards): + for rdef in self._iter_rdefs(): + if rdef.role_cardinality(self.role) in cards: + return True + return False + + def _iter_rdefs(self): rschema = self._cw.vreg.schema.rschema(self.rtype) # XXX when called via ajax, no rset to compute possible types possibletypes = self.cw_rset and self.cw_rset.column_types(0) for rdef in rschema.rdefs.itervalues(): if possibletypes is not None: if self.role == 'subject': - if not rdef.subject in possibletypes: + if rdef.subject not in possibletypes: continue - elif not rdef.object in possibletypes: + elif rdef.object not in possibletypes: continue - if rdef.role_cardinality(self.role) in '+*': - return True - return False + if self.target_type is not None: + if self.role == 'subject': + if rdef.object != self.target_type: + continue + elif rdef.subject != self.target_type: + continue + yield rdef - 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) - if isinstance(value, basestring): - # only one value selected - self.rqlst.add_eid_restriction(restrvar, value) - elif self.operator == 'OR': - # 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) + def _include_no_relation(self): + if not self.no_relation: + return False + if self._cw.vreg.schema.rschema(self.rtype).final: + return False + if self.role == 'object': + subj = utils.rqlvar_maker(defined=self.rqlst.defined_vars, + aliases=self.rqlst.aliases).next() + obj = self.filtered_variable.name else: - # multiple values with AND operator - self.rqlst.add_eid_restriction(restrvar, value.pop()) - while value: - restrvar = _add_rtype_relation(self.rqlst, mainvar, self.rtype, self.role) - self.rqlst.add_eid_restriction(restrvar, value.pop()) + subj = self.filtered_variable.name + obj = utils.rqlvar_maker(defined=self.rqlst.defined_vars, + aliases=self.rqlst.aliases).next() + rql = 'Any %s LIMIT 1 WHERE NOT %s %s %s, %s' % ( + self.filtered_variable.name, subj, self.rtype, obj, + self.rqlst.where.as_string()) + return bool(self.rqlexec(rql, self.cw_rset and self.cw_rset.args)) + + def _add_not_rel_restr(self, rel): + nrrel = nodes.Not(_make_relation(self.rqlst, self.filtered_variable, + self.rtype, self.role)[1]) + rel.parent.replace(rel, nodes.Or(nrrel, rel)) class RelationAttributeFacet(RelationFacet): @@ -699,20 +780,25 @@ if not value: return mainvar = self.filtered_variable - restrvar = _add_rtype_relation(self.rqlst, mainvar, self.rtype, self.role) + restrvar = _add_rtype_relation(self.rqlst, mainvar, self.rtype, + self.role)[0] 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) + 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) + 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) + restrvar = _add_rtype_relation(self.rqlst, mainvar, self.rtype, + self.role)[0] + self.rqlst.add_constant_restriction( + restrvar, self.target_attr, value.pop(), + self.attrtype, self.comparator) class AttributeFacet(RelationAttributeFacet): @@ -749,6 +835,16 @@ _select_target_entity = True + @property + def i18nable(self): + """should label be internationalized""" + for rdef in self._iter_rdefs(): + # no 'internationalizable' property for rdef whose object is not a + # String + if not getattr(rdef, 'internationalizable', False): + return False + return True + def vocabulary(self): """return vocabulary for this facet, eg a list of 2-uple (label, value) """ diff -r 3f67f7ea5632 -r 63d5dbaef999 web/test/unittest_facet.py --- a/web/test/unittest_facet.py Fri Oct 01 16:07:03 2010 +0200 +++ b/web/test/unittest_facet.py Fri Oct 01 16:53:35 2010 +0200 @@ -14,26 +14,33 @@ self.assertEqual(rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser') return req, rset, rqlst, mainvar - def test_relation_simple(self): + def _in_group_facet(self, cls=facet.RelationFacet, no_relation=False): req, rset, rqlst, mainvar = self.prepare_rqlst() - f = facet.RelationFacet(req, rset=rset, - rqlst=rqlst.children[0], - filtered_variable=mainvar) + cls.no_relation = no_relation + f = cls(req, rset=rset, rqlst=rqlst.children[0], + filtered_variable=mainvar) + f.__regid__ = 'in_group' 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")')] + groups = [eid for eid, in self.execute('CWGroup G ORDERBY GN ' + 'WHERE G name GN, G name IN ("guests", "managers")')] + return f, groups + + def test_relation_simple(self): + f, (guests, managers) = self._in_group_facet() self.assertEqual(f.vocabulary(), - [(u'guests', guests), (u'managers', managers)]) + [(u'guests', guests), (u'managers', managers)]) # ensure rqlst is left unmodified - self.assertEqual(rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser') + self.assertEqual(f.rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser') #rqlst = rset.syntax_tree() self.assertEqual(f.possible_values(), [str(guests), str(managers)]) # ensure rqlst is left unmodified - self.assertEqual(rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser') - req.form[f.__regid__] = str(guests) + self.assertEqual(f.rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser') + f._cw.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) @@ -72,25 +79,47 @@ self.assertEqual(f.rqlst.as_string(), 'DISTINCT Any GROUPBY X WHERE X in_group G?, G name GN, NOT G name "users", X in_group D, D eid %s' % guests) + def test_relation_no_relation_1(self): + f, (guests, managers) = self._in_group_facet(no_relation=True) + self.assertEqual(f.vocabulary(), + [(u'guests', guests), (u'managers', managers)]) + self.assertEqual(f.possible_values(), + [str(guests), str(managers)]) + f._cw.create_entity('CWUser', login=u'hop', upassword='toto') + self.assertEqual(f.vocabulary(), + [(u'no relation', ''), (u'guests', guests), (u'managers', managers)]) + self.assertEqual(f.possible_values(), + [str(guests), str(managers)]) + f._cw.form[f.__regid__] = '' + f.add_rql_restrictions() + self.assertEqual(f.rqlst.as_string(), + 'DISTINCT Any WHERE X is CWUser, NOT X in_group G') + + def test_relation_no_relation_2(self): + f, (guests, managers) = self._in_group_facet(no_relation=True) + f._cw.form[f.__regid__] = ['', guests] + f.rqlst.save_state() + f.add_rql_restrictions() + self.assertEqual(f.rqlst.as_string(), + 'DISTINCT Any WHERE X is CWUser, (NOT X in_group B) OR (X in_group A, A eid %s)' % guests) + f.rqlst.recover() + self.assertEqual(f.rqlst.as_string(), + 'DISTINCT Any WHERE X is CWUser') + + 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' + f, (guests, managers) = self._in_group_facet(cls=facet.RelationAttributeFacet) self.assertEqual(f.vocabulary(), [(u'guests', u'guests'), (u'managers', u'managers')]) # ensure rqlst is left unmodified - self.assertEqual(rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser') + self.assertEqual(f.rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser') #rqlst = rset.syntax_tree() self.assertEqual(f.possible_values(), ['guests', 'managers']) # ensure rqlst is left unmodified - self.assertEqual(rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser') - req.form[f.__regid__] = 'guests' + self.assertEqual(f.rqlst.as_string(), 'DISTINCT Any WHERE X is CWUser') + f._cw.form[f.__regid__] = 'guests' f.add_rql_restrictions() # selection is cluttered because rqlst has been prepared for facet (it # is not in real life)