# HG changeset patch # User Alexandre Fayolle # Date 1301661496 -7200 # Node ID b817d44cb606ff68e75865c8ddab74f55f69bc1b # Parent 93a19c1831aac2e1eab183bb85717565c3c5119c# Parent d6d905d0344fc4a116886fd8cd96aa7d176f2cf2 merged test fix from stable diff -r d6d905d0344f -r b817d44cb606 cwvreg.py --- a/cwvreg.py Fri Apr 01 14:26:18 2011 +0200 +++ b/cwvreg.py Fri Apr 01 14:38:16 2011 +0200 @@ -312,6 +312,10 @@ kwargs['clear'] = True super(ETypeRegistry, self).register(obj, **kwargs) + def iter_classes(self): + for etype in self.vreg.schema.entities(): + yield self.etype_class(etype) + @cached def parent_classes(self, etype): if etype == 'Any': diff -r d6d905d0344f -r b817d44cb606 devtools/fill.py --- a/devtools/fill.py Fri Apr 01 14:26:18 2011 +0200 +++ b/devtools/fill.py Fri Apr 01 14:38:16 2011 +0200 @@ -1,5 +1,5 @@ # -*- coding: iso-8859-1 -*- -# 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. @@ -441,7 +441,7 @@ constraints = [c for c in rdef.constraints if isinstance(c, RQLConstraint)] if constraints: - restrictions = ', '.join(c.restriction for c in constraints) + restrictions = ', '.join(c.expression for c in constraints) q += ', %s' % restrictions # restrict object eids if possible # XXX the attempt to restrict below in completely wrong diff -r d6d905d0344f -r b817d44cb606 entity.py --- a/entity.py Fri Apr 01 14:26:18 2011 +0200 +++ b/entity.py Fri Apr 01 14:38:16 2011 +0200 @@ -157,6 +157,7 @@ def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X', settype=True, ordermethod='fetch_order'): """return a rql to fetch all entities of the class type""" + # XXX update api and implementation to AST manipulation (see unrelated rql) restrictions = restriction or [] if settype: restrictions.append('%s is %s' % (mainvar, cls.__regid__)) @@ -165,6 +166,7 @@ selection = [mainvar] orderby = [] # start from 26 to avoid possible conflicts with X + # XXX not enough to be sure it'll be no conflicts varmaker = rqlvar_maker(index=26) cls._fetch_restrictions(mainvar, varmaker, fetchattrs, selection, orderby, restrictions, user, ordermethod) @@ -752,7 +754,7 @@ # generic vocabulary methods ############################################## def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None, - vocabconstraints=True): + vocabconstraints=True): """build a rql to fetch `targettype` entities unrelated to this entity using (rtype, role) relation. @@ -762,58 +764,77 @@ ordermethod = ordermethod or 'fetch_unrelated_order' if isinstance(rtype, basestring): rtype = self._cw.vreg.schema.rschema(rtype) + rdef = rtype.role_rdef(self.e_schema, targettype, role) + rewriter = RQLRewriter(self._cw) + # initialize some variables according to the `role` of `self` in the + # relation: + # * variable for myself (`evar`) and searched entities (`searchvedvar`) + # * entity type of the subject (`subjtype`) and of the object + # (`objtype`) of the relation if role == 'subject': evar, searchedvar = 'S', 'O' subjtype, objtype = self.e_schema, targettype else: searchedvar, evar = 'S', 'O' objtype, subjtype = self.e_schema, targettype + # initialize some variables according to `self` existance if self.has_eid(): - restriction = ['NOT S %s O' % rtype, '%s eid %%(x)s' % evar] + restriction = ['NOT S %s O' % rtype] + if rdef.role_cardinality(role) not in '?1': + # if cardinality in '1?', don't add restriction on eid + restriction.append('%s eid %%(x)s' % evar) args = {'x': self.eid} if role == 'subject': - securitycheck_args = {'fromeid': self.eid} + sec_check_args = {'fromeid': self.eid} else: - securitycheck_args = {'toeid': self.eid} + sec_check_args = {'toeid': self.eid} + existant = None # instead of 'SO', improve perfs else: - restriction = [] + if rdef.role_cardinality(role) in '?1': + restriction = ['NOT S %s O' % rtype] + else: + restriction = [] args = {} - securitycheck_args = {} - rdef = rtype.role_rdef(self.e_schema, targettype, role) - insertsecurity = (rdef.has_local_role('add') and not - rdef.has_perm(self._cw, 'add', **securitycheck_args)) - # XXX consider constraint.mainvars to check if constraint apply + sec_check_args = {} + existant = searchedvar + # retreive entity class for targettype to compute base rql + etypecls = self._cw.vreg['etypes'].etype_class(targettype) + rql = etypecls.fetch_rql(self._cw.user, restriction, + mainvar=searchedvar, ordermethod=ordermethod) + select = self._cw.vreg.parse(self._cw, rql, args).children[0] + # insert RQL expressions for schema constraints into the rql syntax tree if vocabconstraints: # RQLConstraint is a subclass for RQLVocabularyConstraint, so they # will be included as well - restriction += [cstr.restriction for cstr in rdef.constraints - if isinstance(cstr, RQLVocabularyConstraint)] + cstrcls = RQLVocabularyConstraint else: - restriction += [cstr.restriction for cstr in rdef.constraints - if isinstance(cstr, RQLConstraint)] - etypecls = self._cw.vreg['etypes'].etype_class(targettype) - rql = etypecls.fetch_rql(self._cw.user, restriction, - mainvar=searchedvar, ordermethod=ordermethod) + cstrcls = RQLConstraint + for cstr in rdef.constraints: + # consider constraint.mainvars to check if constraint apply + if isinstance(cstr, cstrcls) and searchedvar in cstr.mainvars: + if not self.has_eid() and evar in cstr.mainvars: + continue + # compute a varmap suitable to RQLRewriter.rewrite argument + varmap = dict((v, v) for v in 'SO' if v in select.defined_vars + and v in cstr.mainvars) + # rewrite constraint by constraint since we want a AND between + # expressions. + rewriter.rewrite(select, [(varmap, (cstr,))], select.solutions, + args, existant) + # insert security RQL expressions granting the permission to 'add' the + # relation into the rql syntax tree, if necessary + rqlexprs = rdef.get_rqlexprs('add') + if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args): + # compute a varmap suitable to RQLRewriter.rewrite argument + varmap = dict((v, v) for v in 'SO' if v in select.defined_vars) + # rewrite all expressions at once since we want a OR between them. + rewriter.rewrite(select, [(varmap, rqlexprs)], select.solutions, + args, existant) # ensure we have an order defined - if not ' ORDERBY ' in rql: - before, after = rql.split(' WHERE ', 1) - rql = '%s ORDERBY %s WHERE %s' % (before, searchedvar, after) - if insertsecurity: - rqlexprs = rdef.get_rqlexprs('add') - rewriter = RQLRewriter(self._cw) - rqlst = self._cw.vreg.parse(self._cw, rql, args) - if not self.has_eid(): - existant = searchedvar - else: - existant = None # instead of 'SO', improve perfs - for select in rqlst.children: - varmap = {} - for var in 'SO': - if var in select.defined_vars: - varmap[var] = var - rewriter.rewrite(select, [(varmap, rqlexprs)], - select.solutions, args, existant) - rql = rqlst.as_string() + if not select.orderby: + select.add_sort_var(select.defined_vars[searchedvar]) + # we're done, turn the rql syntax tree as a string + rql = select.as_string() return rql, args def unrelated(self, rtype, targettype, role='subject', limit=None, @@ -825,6 +846,7 @@ rql, args = self.cw_unrelated_rql(rtype, targettype, role, ordermethod) except Unauthorized: return self._cw.empty_rset() + # XXX should be set in unrelated rql when manipulating the AST if limit is not None: before, after = rql.split(' WHERE ', 1) rql = '%s LIMIT %s WHERE %s' % (before, limit, after) diff -r d6d905d0344f -r b817d44cb606 schema.py --- a/schema.py Fri Apr 01 14:26:18 2011 +0200 +++ b/schema.py Fri Apr 01 14:38:16 2011 +0200 @@ -28,6 +28,7 @@ from logilab.common.decorators import cached, clear_cache, monkeypatch from logilab.common.logging_ext import set_log_methods from logilab.common.deprecation import deprecated, class_moved +from logilab.common.textutils import splitstrip from logilab.common.graph import get_cycles from logilab.common.compat import any @@ -179,35 +180,6 @@ __builtins__['display_name'] = deprecated('[3.4] display_name should be imported from cubicweb.schema')(display_name) -# rql expression utilities function ############################################ - -def guess_rrqlexpr_mainvars(expression): - defined = set(split_expression(expression)) - mainvars = [] - if 'S' in defined: - mainvars.append('S') - if 'O' in defined: - mainvars.append('O') - if 'U' in defined: - mainvars.append('U') - if not mainvars: - raise Exception('unable to guess selection variables') - return ','.join(sorted(mainvars)) - -def split_expression(rqlstring): - for expr in rqlstring.split(','): - for noparen in expr.split('('): - for word in noparen.split(): - yield word - -def normalize_expression(rqlstring): - """normalize an rql expression to ease schema synchronization (avoid - suppressing and reinserting an expression if only a space has been added/removed - for instance) - """ - return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(',')) - - # Schema objects definition ################################################### def ERSchema_display_name(self, req, form='', context=None): @@ -640,175 +612,57 @@ def schema_by_eid(self, eid): return self._eid_index[eid] - -# Possible constraints ######################################################## - -class BaseRQLConstraint(BaseConstraint): - """base class for rql constraints - """ - distinct_query = None - - def __init__(self, restriction, mainvars=None): - self.restriction = normalize_expression(restriction) - if mainvars is None: - mainvars = guess_rrqlexpr_mainvars(restriction) - else: - normmainvars = [] - for mainvar in mainvars.split(','): - mainvar = mainvar.strip() - if not mainvar.isalpha(): - raise Exception('bad mainvars %s' % mainvars) - normmainvars.append(mainvar) - assert mainvars, 'bad mainvars %s' % mainvars - mainvars = ','.join(sorted(normmainvars)) - self.mainvars = mainvars - - def serialize(self): - # start with a comma for bw compat, see below - return ';' + self.mainvars + ';' + self.restriction - - @classmethod - def deserialize(cls, value): - # XXX < 3.5.10 bw compat - if not value.startswith(';'): - return cls(value) - _, mainvars, restriction = value.split(';', 2) - return cls(restriction, mainvars) - - def check(self, entity, rtype, value): - """return true if the value satisfy the constraint, else false""" - # implemented as a hook in the repository - return 1 - - def repo_check(self, session, eidfrom, rtype, eidto): - """raise ValidationError if the relation doesn't satisfy the constraint - """ - pass # this is a vocabulary constraint, not enforce XXX why? - - def __str__(self): - if self.distinct_query: - selop = 'Any' - else: - selop = 'DISTINCT Any' - return '%s(%s %s WHERE %s)' % (self.__class__.__name__, selop, - self.mainvars, self.restriction) - - def __repr__(self): - return '<%s @%#x>' % (self.__str__(), id(self)) - - -class RQLVocabularyConstraint(BaseRQLConstraint): - """the rql vocabulary constraint : - - limit the proposed values to a set of entities returned by a rql query, - but this is not enforced at the repository level - - restriction is additional rql restriction that will be added to - a predefined query, where the S and O variables respectivly represent - the subject and the object of the relation - - mainvars is a string that should be used as selection variable (eg - `'Any %s WHERE ...' % mainvars`). If not specified, an attempt will be - done to guess it according to variable used in the expression. - """ - - -class RepoEnforcedRQLConstraintMixIn(object): +# Bases for manipulating RQL in schema ######################################### - def __init__(self, restriction, mainvars=None, msg=None): - super(RepoEnforcedRQLConstraintMixIn, self).__init__(restriction, mainvars) - self.msg = msg - - def serialize(self): - # start with a semicolon for bw compat, see below - return ';%s;%s\n%s' % (self.mainvars, self.restriction, - self.msg or '') - - def deserialize(cls, value): - # XXX < 3.5.10 bw compat - if not value.startswith(';'): - return cls(value) - value, msg = value.split('\n', 1) - _, mainvars, restriction = value.split(';', 2) - return cls(restriction, mainvars, msg) - deserialize = classmethod(deserialize) +def guess_rrqlexpr_mainvars(expression): + defined = set(split_expression(expression)) + mainvars = set() + if 'S' in defined: + mainvars.add('S') + if 'O' in defined: + mainvars.add('O') + if 'U' in defined: + mainvars.add('U') + if not mainvars: + raise Exception('unable to guess selection variables') + return mainvars - def repo_check(self, session, eidfrom, rtype, eidto=None): - """raise ValidationError if the relation doesn't satisfy the constraint - """ - if not self.match_condition(session, eidfrom, eidto): - # XXX at this point if both or neither of S and O are in mainvar we - # dunno if the validation error `occurred` on eidfrom or eidto (from - # user interface point of view) - # - # possible enhancement: check entity being created, it's probably - # the main eid unless this is a composite relation - if eidto is None or 'S' in self.mainvars or not 'O' in self.mainvars: - maineid = eidfrom - qname = role_name(rtype, 'subject') - else: - maineid = eidto - qname = role_name(rtype, 'object') - if self.msg: - msg = session._(self.msg) - else: - msg = '%(constraint)s %(restriction)s failed' % { - 'constraint': session._(self.type()), - 'restriction': self.restriction} - raise ValidationError(maineid, {qname: msg}) +def split_expression(rqlstring): + for expr in rqlstring.split(','): + for noparen in expr.split('('): + for word in noparen.split(): + yield word - def exec_query(self, session, eidfrom, eidto): - if eidto is None: - # checking constraint for an attribute relation - restriction = 'S eid %(s)s, ' + self.restriction - args = {'s': eidfrom} - else: - restriction = 'S eid %(s)s, O eid %(o)s, ' + self.restriction - args = {'s': eidfrom, 'o': eidto} - rql = 'Any %s WHERE %s' % (self.mainvars, restriction) - if self.distinct_query: - rql = 'DISTINCT ' + rql - return session.execute(rql, args, build_descr=False) - - -class RQLConstraint(RepoEnforcedRQLConstraintMixIn, RQLVocabularyConstraint): - """the rql constraint is similar to the RQLVocabularyConstraint but - are also enforced at the repository level +def normalize_expression(rqlstring): + """normalize an rql expression to ease schema synchronization (avoid + suppressing and reinserting an expression if only a space has been + added/removed for instance) """ - distinct_query = False - - def match_condition(self, session, eidfrom, eidto): - return self.exec_query(session, eidfrom, eidto) - - -class RQLUniqueConstraint(RepoEnforcedRQLConstraintMixIn, BaseRQLConstraint): - """the unique rql constraint check that the result of the query isn't - greater than one. - - You *must* specify mainvars when instantiating the constraint since there is - no way to guess it correctly (e.g. if using S,O or U the constraint will - always be satisfied because we've to use a DISTINCT query). - """ - # XXX turns mainvars into a required argument in __init__ - distinct_query = True - - def match_condition(self, session, eidfrom, eidto): - return len(self.exec_query(session, eidfrom, eidto)) <= 1 + return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(',')) class RQLExpression(object): + """Base class for RQL expression used in schema (constraints and + permissions) + """ + # these are overridden by set_log_methods below + # only defining here to prevent pylint from complaining + info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None def __init__(self, expression, mainvars, eid): self.eid = eid # eid of the entity representing this rql expression - if not isinstance(mainvars, unicode): - mainvars = unicode(mainvars) + assert mainvars, 'bad mainvars %s' % mainvars + if isinstance(mainvars, basestring): + mainvars = set(splitstrip(mainvars)) + elif not isinstance(mainvars, set): + mainvars = set(mainvars) self.mainvars = mainvars self.expression = normalize_expression(expression) try: self.rqlst = parse(self.full_rql, print_errors=False).children[0] except RQLSyntaxError: raise RQLSyntaxError(expression) - for mainvar in mainvars.split(','): + for mainvar in mainvars: if len(self.rqlst.defined_vars[mainvar].references()) <= 2: _LOGGER.warn('You did not use the %s variable in your RQL ' 'expression %s', mainvar, self) @@ -832,6 +686,8 @@ def __setstate__(self, state): self.__init__(*state) + # permission rql expression specific stuff ################################# + @cached def transform_has_permission(self): found = None @@ -942,12 +798,10 @@ @property def minimal_rql(self): - return 'Any %s WHERE %s' % (self.mainvars, self.expression) + return 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)), + self.expression) - # these are overridden by set_log_methods below - # only defining here to prevent pylint from complaining - info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None - +# rql expressions for use in permission definition ############################# class ERQLExpression(RQLExpression): def __init__(self, expression, mainvars=None, eid=None): @@ -1024,12 +878,153 @@ kwargs['o'] = toeid return self._check(session, **kwargs) + # in yams, default 'update' perm for attributes granted to managers and owners. # Within cw, we want to default to users who may edit the entity holding the # attribute. ybo.DEFAULT_ATTRPERMS['update'] = ( 'managers', ERQLExpression('U has_update_permission X')) +# additional cw specific constraints ########################################### + +class BaseRQLConstraint(RRQLExpression, BaseConstraint): + """base class for rql constraints""" + distinct_query = None + + def serialize(self): + # start with a comma for bw compat,see below + return ';' + ','.join(sorted(self.mainvars)) + ';' + self.expression + + @classmethod + def deserialize(cls, value): + # XXX < 3.5.10 bw compat + if not value.startswith(';'): + return cls(value) + _, mainvars, expression = value.split(';', 2) + return cls(expression, mainvars) + + def check(self, entity, rtype, value): + """return true if the value satisfy the constraint, else false""" + # implemented as a hook in the repository + return 1 + + def __str__(self): + if self.distinct_query: + selop = 'Any' + else: + selop = 'DISTINCT Any' + return '%s(%s %s WHERE %s)' % (self.__class__.__name__, selop, + ','.join(sorted(self.mainvars)), + self.expression) + + def __repr__(self): + return '<%s @%#x>' % (self.__str__(), id(self)) + + +class RQLVocabularyConstraint(BaseRQLConstraint): + """the rql vocabulary constraint: + + limit the proposed values to a set of entities returned by a rql query, + but this is not enforced at the repository level + + `expression` is additional rql restriction that will be added to + a predefined query, where the S and O variables respectivly represent + the subject and the object of the relation + + `mainvars` is a set of variables that should be used as selection variable + (eg `'Any %s WHERE ...' % mainvars`). If not specified, an attempt will be + done to guess it according to variable used in the expression. + """ + + def repo_check(self, session, eidfrom, rtype, eidto): + """raise ValidationError if the relation doesn't satisfy the constraint + """ + pass # this is a vocabulary constraint, not enforce + + +class RepoEnforcedRQLConstraintMixIn(object): + + def __init__(self, expression, mainvars=None, msg=None): + super(RepoEnforcedRQLConstraintMixIn, self).__init__(expression, mainvars) + self.msg = msg + + def serialize(self): + # start with a semicolon for bw compat, see below + return ';%s;%s\n%s' % (','.join(sorted(self.mainvars)), self.expression, + self.msg or '') + + def deserialize(cls, value): + # XXX < 3.5.10 bw compat + if not value.startswith(';'): + return cls(value) + value, msg = value.split('\n', 1) + _, mainvars, expression = value.split(';', 2) + return cls(expression, mainvars, msg) + deserialize = classmethod(deserialize) + + def repo_check(self, session, eidfrom, rtype, eidto=None): + """raise ValidationError if the relation doesn't satisfy the constraint + """ + if not self.match_condition(session, eidfrom, eidto): + # XXX at this point if both or neither of S and O are in mainvar we + # dunno if the validation error `occurred` on eidfrom or eidto (from + # user interface point of view) + # + # possible enhancement: check entity being created, it's probably + # the main eid unless this is a composite relation + if eidto is None or 'S' in self.mainvars or not 'O' in self.mainvars: + maineid = eidfrom + qname = role_name(rtype, 'subject') + else: + maineid = eidto + qname = role_name(rtype, 'object') + if self.msg: + msg = session._(self.msg) + else: + msg = '%(constraint)s %(expression)s failed' % { + 'constraint': session._(self.type()), + 'expression': self.expression} + raise ValidationError(maineid, {qname: msg}) + + def exec_query(self, session, eidfrom, eidto): + if eidto is None: + # checking constraint for an attribute relation + expression = 'S eid %(s)s, ' + self.expression + args = {'s': eidfrom} + else: + expression = 'S eid %(s)s, O eid %(o)s, ' + self.expression + args = {'s': eidfrom, 'o': eidto} + rql = 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)), expression) + if self.distinct_query: + rql = 'DISTINCT ' + rql + return session.execute(rql, args, build_descr=False) + + +class RQLConstraint(RepoEnforcedRQLConstraintMixIn, RQLVocabularyConstraint): + """the rql constraint is similar to the RQLVocabularyConstraint but + are also enforced at the repository level + """ + distinct_query = False + + def match_condition(self, session, eidfrom, eidto): + return self.exec_query(session, eidfrom, eidto) + + +class RQLUniqueConstraint(RepoEnforcedRQLConstraintMixIn, BaseRQLConstraint): + """the unique rql constraint check that the result of the query isn't + greater than one. + + You *must* specify `mainvars` when instantiating the constraint since there + is no way to guess it correctly (e.g. if using S,O or U the constraint will + always be satisfied because we've to use a DISTINCT query). + """ + # XXX turns mainvars into a required argument in __init__ + distinct_query = True + + def match_condition(self, session, eidfrom, eidto): + return len(self.exec_query(session, eidfrom, eidto)) <= 1 + + # workflow extensions ######################################################### from yams.buildobjs import _add_relation as yams_add_relation diff -r d6d905d0344f -r b817d44cb606 server/migractions.py --- a/server/migractions.py Fri Apr 01 14:26:18 2011 +0200 +++ b/server/migractions.py Fri Apr 01 14:38:16 2011 +0200 @@ -438,7 +438,8 @@ 'X expression %%(expr)s, X mainvars %%(vars)s, T %s X ' 'WHERE T eid %%(x)s' % perm, {'expr': expr, 'exprtype': exprtype, - 'vars': expression.mainvars, 'x': teid}, + 'vars': u','.join(sorted(expression.mainvars)), + 'x': teid}, ask_confirm=False) def _synchronize_rschema(self, rtype, syncrdefs=True, diff -r d6d905d0344f -r b817d44cb606 server/schemaserial.py --- a/server/schemaserial.py Fri Apr 01 14:26:18 2011 +0200 +++ b/server/schemaserial.py Fri Apr 01 14:38:16 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. @@ -564,7 +564,7 @@ yield ('INSERT RQLExpression E: E expression %%(e)s, E exprtype %%(t)s, ' 'E mainvars %%(v)s, X %s_permission E WHERE X eid %%(x)s' % action, {'e': unicode(rqlexpr.expression), - 'v': unicode(rqlexpr.mainvars), + 'v': unicode(','.join(sorted(rqlexpr.mainvars))), 't': unicode(rqlexpr.__class__.__name__)}) # update functions diff -r d6d905d0344f -r b817d44cb606 server/test/unittest_querier.py --- a/server/test/unittest_querier.py Fri Apr 01 14:26:18 2011 +0200 +++ b/server/test/unittest_querier.py Fri Apr 01 14:38:16 2011 +0200 @@ -761,18 +761,20 @@ rset = self.execute('Any N WHERE X is CWEType, X name N, X final %(val)s', {'val': True}) self.assertEqual(sorted(r[0] for r in rset.rows), ['Boolean', 'Bytes', - 'Date', 'Datetime', - 'Decimal', 'Float', - 'Int', 'Interval', - 'Password', 'String', - 'Time']) + 'Date', 'Datetime', + 'Decimal', 'Float', + 'Int', 'Interval', + 'Password', 'String', + 'TZDatetime', 'TZTime', + 'Time']) rset = self.execute('Any N WHERE X is CWEType, X name N, X final TRUE') self.assertEqual(sorted(r[0] for r in rset.rows), ['Boolean', 'Bytes', - 'Date', 'Datetime', - 'Decimal', 'Float', - 'Int', 'Interval', - 'Password', 'String', - 'Time']) + 'Date', 'Datetime', + 'Decimal', 'Float', + 'Int', 'Interval', + 'Password', 'String', + 'TZDatetime', 'TZTime', + 'Time']) def test_select_constant(self): rset = self.execute('Any X, "toto" ORDERBY X WHERE X is CWGroup') diff -r d6d905d0344f -r b817d44cb606 server/test/unittest_repository.py --- a/server/test/unittest_repository.py Fri Apr 01 14:26:18 2011 +0200 +++ b/server/test/unittest_repository.py Fri Apr 01 14:38:16 2011 +0200 @@ -69,11 +69,12 @@ cu = self.session.system_sql('SELECT %s FROM %s WHERE %s=%%(final)s ORDER BY %s' % (namecol, table, finalcol, namecol), {'final': 'TRUE'}) self.assertEqual(cu.fetchall(), [(u'Boolean',), (u'Bytes',), - (u'Date',), (u'Datetime',), - (u'Decimal',),(u'Float',), - (u'Int',), - (u'Interval',), (u'Password',), - (u'String',), (u'Time',)]) + (u'Date',), (u'Datetime',), + (u'Decimal',),(u'Float',), + (u'Int',), + (u'Interval',), (u'Password',), + (u'String',), + (u'TZDatetime',), (u'TZTime',), (u'Time',)]) sql = ("SELECT etype.cw_eid, etype.cw_name, cstr.cw_eid, rel.eid_to " "FROM cw_CWUniqueTogetherConstraint as cstr, " " relations_relation as rel, " @@ -319,7 +320,7 @@ self.assertEqual(len(constraints), 1) cstr = constraints[0] self.assert_(isinstance(cstr, RQLConstraint)) - self.assertEqual(cstr.restriction, 'O final TRUE') + self.assertEqual(cstr.expression, 'O final TRUE') ownedby = schema.rschema('owned_by') self.assertEqual(ownedby.objects('CWEType'), ('CWUser',)) diff -r d6d905d0344f -r b817d44cb606 test/data/schema.py --- a/test/data/schema.py Fri Apr 01 14:26:18 2011 +0200 +++ b/test/data/schema.py Fri Apr 01 14:38:16 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,13 +15,11 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -""" - -""" from yams.buildobjs import (EntityType, String, SubjectRelation, RelationDefinition) -from cubicweb.schema import WorkflowableEntityType +from cubicweb.schema import (WorkflowableEntityType, + RQLConstraint, RQLVocabularyConstraint) class Personne(EntityType): nom = String(required=True) @@ -29,7 +27,14 @@ type = String() travaille = SubjectRelation('Societe') evaluee = SubjectRelation(('Note', 'Personne')) - connait = SubjectRelation('Personne', symmetric=True) + connait = SubjectRelation( + 'Personne', symmetric=True, + constraints=[ + RQLConstraint('NOT S identity O'), + # conflicting constraints, see cw_unrelated_rql tests in + # unittest_entity.py + RQLVocabularyConstraint('NOT (S connait P, P nom "toto")'), + RQLVocabularyConstraint('S travaille P, P nom "tutu"')]) class Societe(EntityType): nom = String() diff -r d6d905d0344f -r b817d44cb606 test/unittest_entity.py --- a/test/unittest_entity.py Fri Apr 01 14:26:18 2011 +0200 +++ b/test/unittest_entity.py Fri Apr 01 14:38:16 2011 +0200 @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# 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. @@ -19,7 +19,7 @@ """unit tests for cubicweb.web.views.entities module""" from datetime import datetime - +from logilab.common import tempattr from cubicweb import Binary, Unauthorized from cubicweb.devtools.testlib import CubicWebTC from cubicweb.mttransforms import HAS_TAL @@ -29,6 +29,17 @@ class EntityTC(CubicWebTC): + def setUp(self): + 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) + + 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] + def test_boolean_value(self): e = self.vreg['etypes'].etype_class('CWUser')(self.request()) self.failUnless(e) @@ -136,17 +147,19 @@ Note = self.vreg['etypes'].etype_class('Note') peschema = Personne.e_schema seschema = Societe.e_schema - peschema.subjrels['travaille'].rdef(peschema, seschema).cardinality = '1*' - peschema.subjrels['connait'].rdef(peschema, peschema).cardinality = '11' - peschema.subjrels['evaluee'].rdef(peschema, Note.e_schema).cardinality = '1*' - seschema.subjrels['evaluee'].rdef(seschema, Note.e_schema).cardinality = '1*' - # testing basic fetch_attrs attribute - self.assertEqual(Personne.fetch_rql(user), - 'Any X,AA,AB,AC ORDERBY AA ASC ' - 'WHERE X is Personne, X nom AA, X prenom AB, X modification_date AC') - pfetch_attrs = Personne.fetch_attrs - sfetch_attrs = Societe.fetch_attrs + torestore = [] + for rdef, card in [(peschema.subjrels['travaille'].rdef(peschema, seschema), '1*'), + (peschema.subjrels['connait'].rdef(peschema, peschema), '11'), + (peschema.subjrels['evaluee'].rdef(peschema, Note.e_schema), '1*'), + (seschema.subjrels['evaluee'].rdef(seschema, Note.e_schema), '1*')]: + cm = tempattr(rdef, 'cardinality', card) + cm.__enter__() + torestore.append(cm) try: + # testing basic fetch_attrs attribute + self.assertEqual(Personne.fetch_rql(user), + 'Any X,AA,AB,AC ORDERBY AA ASC ' + 'WHERE X is Personne, X nom AA, X prenom AB, X modification_date AC') # testing unknown attributes Personne.fetch_attrs = ('bloug', 'beep') self.assertEqual(Personne.fetch_rql(user), 'Any X WHERE X is Personne') @@ -185,8 +198,9 @@ 'Any X,AA,AB ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB') # XXX test unauthorized attribute finally: - Personne.fetch_attrs = pfetch_attrs - Societe.fetch_attrs = sfetch_attrs + # fetch_attrs restored by generic tearDown + for cm in torestore: + cm.__exit__(None, None, None) def test_related_rql_base(self): Personne = self.vreg['etypes'].etype_class('Personne') @@ -227,7 +241,7 @@ user = self.request().user rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0] self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC ' - 'WHERE NOT S use_email O, S eid %(x)s, ' + 'WHERE NOT EXISTS(S use_email O), S eid %(x)s, ' 'O is EmailAddress, O address AA, O alias AB, O modification_date AC') def test_unrelated_rql_security_1_user(self): @@ -236,7 +250,7 @@ user = self.request().user rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0] self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC ' - 'WHERE NOT S use_email O, S eid %(x)s, ' + 'WHERE NOT EXISTS(S use_email O), S eid %(x)s, ' 'O is EmailAddress, O address AA, O alias AB, O modification_date AC') user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0) rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0] @@ -257,22 +271,59 @@ def test_unrelated_rql_security_2(self): email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0) rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0] - self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA ASC ' - 'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, S login AA, S firstname AB, S surname AC, S modification_date AD') + self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA ' + 'WHERE NOT EXISTS(S use_email O), S is CWUser, ' + 'S login AA, S firstname AB, S surname AC, S modification_date AD') self.login('anon') email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0) rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0] self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA ' - 'WHERE NOT EXISTS(S use_email O), O eid %(x)s, S is CWUser, S login AA, S firstname AB, S surname AC, S modification_date AD, ' - 'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)') + 'WHERE NOT EXISTS(S use_email O, O is EmailAddress), S is CWUser, ' + 'S login AA, S firstname AB, S surname AC, S modification_date AD, ' + 'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)') def test_unrelated_rql_security_nonexistant(self): self.login('anon') email = self.vreg['etypes'].etype_class('EmailAddress')(self.request()) rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0] self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA ' - 'WHERE S is CWUser, S login AA, S firstname AB, S surname AC, S modification_date AD, ' - 'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)') + 'WHERE NOT EXISTS(S use_email O, O is EmailAddress), S is CWUser, ' + 'S login AA, S firstname AB, S surname AC, S modification_date AD, ' + 'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)') + + def test_unrelated_rql_constraints_creation_subject(self): + person = self.vreg['etypes'].etype_class('Personne')(self.request()) + rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0] + self.assertEqual( + rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE ' + 'O is Personne, O nom AA, O prenom AB, O modification_date AC') + + def test_unrelated_rql_constraints_creation_object(self): + person = self.vreg['etypes'].etype_class('Personne')(self.request()) + rql = person.cw_unrelated_rql('connait', 'Personne', 'object')[0] + self.assertEqual( + rql, 'Any S,AA,AB,AC ORDERBY AC DESC WHERE ' + 'S is Personne, S nom AA, S prenom AB, S modification_date AC, ' + 'NOT (S connait A, A nom "toto"), A is Personne, EXISTS(S travaille B, B nom "tutu")') + + def test_unrelated_rql_constraints_edition_subject(self): + person = self.request().create_entity('Personne', nom=u'sylvain') + rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0] + self.assertEqual( + rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE ' + 'NOT EXISTS(S connait O), S eid %(x)s, O is Personne, ' + 'O nom AA, O prenom AB, O modification_date AC, ' + 'NOT S identity O') + + def test_unrelated_rql_constraints_edition_object(self): + person = self.request().create_entity('Personne', nom=u'sylvain') + rql = person.cw_unrelated_rql('connait', 'Personne', 'object')[0] + self.assertEqual( + rql, 'Any S,AA,AB,AC ORDERBY AC DESC WHERE ' + 'NOT EXISTS(S connait O), O eid %(x)s, S is Personne, ' + 'S nom AA, S prenom AB, S modification_date AC, ' + 'NOT S identity O, NOT (S connait A, A nom "toto"), ' + 'EXISTS(S travaille B, B nom "tutu")') def test_unrelated_base(self): req = self.request() diff -r d6d905d0344f -r b817d44cb606 test/unittest_schema.py --- a/test/unittest_schema.py Fri Apr 01 14:26:18 2011 +0200 +++ b/test/unittest_schema.py Fri Apr 01 14:38:16 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. @@ -169,7 +169,7 @@ 'Password', 'Personne', 'RQLExpression', 'Societe', 'State', 'StateFull', 'String', 'SubNote', 'SubWorkflowExitPoint', - 'Tag', 'Time', 'Transition', 'TrInfo', + 'TZDatetime', 'TZTime', 'Tag', 'Time', 'Transition', 'TrInfo', 'Workflow', 'WorkflowTransition'] self.assertListEqual(sorted(expected_entities), entities) relations = sorted([str(r) for r in schema.relations()]) @@ -239,7 +239,7 @@ self.failUnlessEqual(len(constraints), 1, constraints) constraint = constraints[0] self.failUnless(isinstance(constraint, RQLConstraint)) - self.failUnlessEqual(constraint.restriction, 'O final TRUE') + self.failUnlessEqual(constraint.expression, 'O final TRUE') def test_fulltext_container(self): schema = loader.load(config) @@ -315,7 +315,7 @@ class GuessRrqlExprMainVarsTC(TestCase): def test_exists(self): mainvars = guess_rrqlexpr_mainvars(normalize_expression('NOT EXISTS(O team_competition C, C level < 3)')) - self.assertEqual(mainvars, 'O') + self.assertEqual(mainvars, set(['O'])) if __name__ == '__main__': diff -r d6d905d0344f -r b817d44cb606 web/data/cubicweb.reledit.js --- a/web/data/cubicweb.reledit.js Fri Apr 01 14:26:18 2011 +0200 +++ b/web/data/cubicweb.reledit.js Fri Apr 01 14:38:16 2011 +0200 @@ -18,6 +18,7 @@ cleanupAfterCancel: function (divid) { jQuery('#appMsg').hide(); jQuery('div.errorMessage').remove(); + // plus re-set inline style ? jQuery('#' + divid).show(); jQuery('#' + divid + '-value').show(); jQuery('#' + divid + '-form').hide(); @@ -63,9 +64,9 @@ * @param reload: boolean to reload page if true (when changing URL dependant data) * @param default_value : value if the field is empty */ - loadInlineEditionForm: function(formid, eid, rtype, role, divid, reload, vid) { + loadInlineEditionForm: function(formid, eid, rtype, role, divid, reload, vid, action) { var args = {fname: 'reledit_form', rtype: rtype, role: role, - pageid: pageid, + pageid: pageid, action: action, eid: eid, divid: divid, formid: formid, reload: reload, vid: vid}; var d = jQuery('#'+divid+'-reledit').loadxhtml(JSON_BASE_URL, args, 'post'); diff -r d6d905d0344f -r b817d44cb606 web/request.py --- a/web/request.py Fri Apr 01 14:26:18 2011 +0200 +++ b/web/request.py Fri Apr 01 14:38:16 2011 +0200 @@ -734,26 +734,14 @@ return None, None def parse_accept_header(self, header): - """returns an ordered list of preferred languages""" + """returns an ordered list of accepted values""" + try: + value_parser, value_sort_key = ACCEPT_HEADER_PARSER[header.lower()] + except KeyError: + value_parser = value_sort_key = None accepteds = self.get_header(header, '') - values = [] - for info in accepteds.split(','): - try: - value, scores = info.split(';', 1) - except ValueError: - value = info - score = 1.0 - else: - for score in scores.split(';'): - try: - scorekey, scoreval = score.split('=') - if scorekey == 'q': # XXX 'level' - score = float(scoreval) - except ValueError: - continue - values.append((score, value)) - values.sort(reverse=True) - return (value for (score, value) in values) + values = _parse_accept_header(accepteds, value_parser, value_sort_key) + return (raw_value for (raw_value, parsed_value, score) in values) def header_if_modified_since(self): """If the HTTP header If-modified-since is set, return the equivalent @@ -858,5 +846,91 @@ self.parse_accept_header('Accept-Language')] + +## HTTP-accept parsers / utilies ############################################## +def _mimetype_sort_key(accept_info): + """accepted mimetypes must be sorted by : + + 1/ highest score first + 2/ most specific mimetype first, e.g. : + - 'text/html level=1' is more specific 'text/html' + - 'text/html' is more specific than 'text/*' + - 'text/*' itself more specific than '*/*' + + """ + raw_value, (media_type, media_subtype, media_type_params), score = accept_info + # FIXME: handle '+' in media_subtype ? (should xhtml+xml have a + # higher precedence than xml ?) + if media_subtype == '*': + score -= 0.0001 + if media_type == '*': + score -= 0.0001 + return 1./score, media_type, media_subtype, 1./(1+len(media_type_params)) + +def _charset_sort_key(accept_info): + """accepted mimetypes must be sorted by : + + 1/ highest score first + 2/ most specific charset first, e.g. : + - 'utf-8' is more specific than '*' + """ + raw_value, value, score = accept_info + if value == '*': + score -= 0.0001 + return 1./score, value + +def _parse_accept_header(raw_header, value_parser=None, value_sort_key=None): + """returns an ordered list accepted types + + returned value is a list of 2-tuple (value, score), ordered + by score. Exact type of `value` will depend on what `value_parser` + will reutrn. if `value_parser` is None, then the raw value, as found + in the http header, is used. + """ + if value_sort_key is None: + value_sort_key = lambda infos: 1./infos[-1] + values = [] + for info in raw_header.split(','): + score = 1.0 + other_params = {} + try: + value, infodef = info.split(';', 1) + except ValueError: + value = info + else: + for info in infodef.split(';'): + try: + infokey, infoval = info.split('=') + if infokey == 'q': # XXX 'level' + score = float(infoval) + continue + except ValueError: + continue + other_params[infokey] = infoval + parsed_value = value_parser(value, other_params) if value_parser else value + values.append( (value.strip(), parsed_value, score) ) + values.sort(key=value_sort_key) + return values + + +def _mimetype_parser(value, other_params): + """return a 3-tuple + (type, subtype, type_params) corresponding to the mimetype definition + e.g. : for 'text/*', `mimetypeinfo` will be ('text', '*', {}), for + 'text/html;level=1', `mimetypeinfo` will be ('text', '*', {'level': '1'}) + """ + try: + media_type, media_subtype = value.strip().split('/') + except ValueError: # safety belt : '/' should always be present + media_type = value.strip() + media_subtype = '*' + return (media_type, media_subtype, other_params) + + +ACCEPT_HEADER_PARSER = { + 'accept': (_mimetype_parser, _mimetype_sort_key), + 'accept-charset': (None, _charset_sort_key), + } + from cubicweb import set_log_methods set_log_methods(CubicWebRequestBase, LOGGER) diff -r d6d905d0344f -r b817d44cb606 web/schemaviewer.py --- a/web/schemaviewer.py Fri Apr 01 14:26:18 2011 +0200 +++ b/web/schemaviewer.py Fri Apr 01 14:38:16 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,8 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""an helper class to display CubicWeb schema using ureports +"""an helper class to display CubicWeb schema using ureports""" -""" __docformat__ = "restructuredtext en" _ = unicode @@ -217,7 +216,7 @@ if val is None: val = '' elif prop == 'constraints': - val = ', '.join([c.restriction for c in val]) + val = ', '.join([c.expression for c in val]) elif isinstance(val, dict): for key, value in val.iteritems(): if isinstance(value, (list, tuple)): diff -r d6d905d0344f -r b817d44cb606 web/test/unittest_form.py --- a/web/test/unittest_form.py Fri Apr 01 14:26:18 2011 +0200 +++ b/web/test/unittest_form.py Fri Apr 01 14:38:16 2011 +0200 @@ -104,9 +104,9 @@ def test_reledit_composite_field(self): rset = self.execute('INSERT BlogEntry X: X title "cubicweb.org", X content "hop"') - form = self.vreg['views'].select('doreledit', self.request(), + form = self.vreg['views'].select('reledit', self.request(), rset=rset, row=0, rtype='content') - data = form.render(row=0, rtype='content', formid='base') + data = form.render(row=0, rtype='content', formid='base', action='edit_rtype') self.failUnless('content_format' in data) # form view tests ######################################################### diff -r d6d905d0344f -r b817d44cb606 web/test/unittest_reledit.py --- a/web/test/unittest_reledit.py Fri Apr 01 14:26:18 2011 +0200 +++ b/web/test/unittest_reledit.py Fri Apr 01 14:38:16 2011 +0200 @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . """ -mainly regression-preventing tests for reledit/doreledit views +mainly regression-preventing tests for reledit views """ from cubicweb.devtools.testlib import CubicWebTC @@ -33,9 +33,9 @@ class ClickAndEditFormTC(ReleditMixinTC, CubicWebTC): def test_default_config(self): - reledit = {'title': """
cubicweb-world-domination
""", - 'long_desc': """
<not specified>
""", - 'manager': """
<not specified>
""", + reledit = {'title': '''
cubicweb-world-domination
''', + 'long_desc': '''
<not specified>
''', + 'manager': '''
<not specified>
''', 'composite_card11_2ttypes': """<not specified>""", 'concerns': """<not specified>"""} @@ -44,9 +44,11 @@ continue rtype = rschema.type self.assertMultiLineEqual(reledit[rtype] % {'eid': self.proj.eid}, - self.proj.view('reledit', rtype=rtype, role=role), rtype) + self.proj.view('reledit', rtype=rtype, role=role), + rtype) def test_default_forms(self): + self.skip('Need to check if this test should still run post reledit/doreledit merge') doreledit = {'title': """
cubicweb-world-domination
@@ -190,11 +192,11 @@ reledit_ctrl.tag_object_of(('Ticket', 'concerns', 'Project'), {'edit_target': 'rtype'}) reledit = { - 'title': """
cubicweb-world-domination
""", - 'long_desc': """
<long_desc is required>
""", - 'manager': """""", + 'title': """
cubicweb-world-domination
""", + 'long_desc': """
<long_desc is required>
""", + 'manager': """""", 'composite_card11_2ttypes': """<not specified>""", - 'concerns': """""" + 'concerns': """""" } for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True): if rschema not in reledit: diff -r d6d905d0344f -r b817d44cb606 web/test/unittest_request.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/test/unittest_request.py Fri Apr 01 14:38:16 2011 +0200 @@ -0,0 +1,69 @@ +"""misc. unittests for utility functions +""" + +from logilab.common.testlib import TestCase, unittest_main + +from functools import partial + +from cubicweb.web.request import (_parse_accept_header, + _mimetype_sort_key, _mimetype_parser, _charset_sort_key) + + + +class AcceptParserTC(TestCase): + + def test_parse_accept(self): + parse_accept_header = partial(_parse_accept_header, + value_parser=_mimetype_parser, + value_sort_key=_mimetype_sort_key) + # compare scores + self.assertEqual(parse_accept_header("audio/*;q=0.2, audio/basic"), + [( ('audio/basic', ('audio', 'basic', {}), 1.0 ) ), + ( ('audio/*', ('audio', '*', {}), 0.2 ) )]) + self.assertEqual(parse_accept_header("text/plain;q=0.5, text/html, text/x-dvi;q=0.8, text/x-c"), + [( ('text/html', ('text', 'html', {}), 1.0 ) ), + ( ('text/x-c', ('text', 'x-c', {}), 1.0 ) ), + ( ('text/x-dvi', ('text', 'x-dvi', {}), 0.8 ) ), + ( ('text/plain', ('text', 'plain', {}), 0.5 ) )]) + # compare mimetype precedence for a same given score + self.assertEqual(parse_accept_header("audio/*, audio/basic"), + [( ('audio/basic', ('audio', 'basic', {}), 1.0 ) ), + ( ('audio/*', ('audio', '*', {}), 1.0 ) )]) + self.assertEqual(parse_accept_header("text/*, text/html, text/html;level=1, */*"), + [( ('text/html', ('text', 'html', {'level': '1'}), 1.0 ) ), + ( ('text/html', ('text', 'html', {}), 1.0 ) ), + ( ('text/*', ('text', '*', {}), 1.0 ) ), + ( ('*/*', ('*', '*', {}), 1.0 ) )]) + # free party + self.assertEqual(parse_accept_header("text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5"), + [( ('text/html', ('text', 'html', {'level': '1'}), 1.0 ) ), + ( ('text/html', ('text', 'html', {}), 0.7 ) ), + ( ('*/*', ('*', '*', {}), 0.5 ) ), + ( ('text/html', ('text', 'html', {'level': '2'}), 0.4 ) ), + ( ('text/*', ('text', '*', {}), 0.3 ) ) + ]) + # chrome sample header + self.assertEqual(parse_accept_header("application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"), + [( ('application/xhtml+xml', ('application', 'xhtml+xml', {}), 1.0 ) ), + ( ('application/xml', ('application', 'xml', {}), 1.0 ) ), + ( ('image/png', ('image', 'png', {}), 1.0 ) ), + ( ('text/html', ('text', 'html', {}), 0.9 ) ), + ( ('text/plain', ('text', 'plain', {}), 0.8 ) ), + ( ('*/*', ('*', '*', {}), 0.5 ) ), + ]) + + def test_parse_accept_language(self): + self.assertEqual(_parse_accept_header('fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3'), + [('fr', 'fr', 1.0), ('fr-fr', 'fr-fr', 0.8), + ('en-us', 'en-us', 0.5), ('en', 'en', 0.3)]) + + def test_parse_accept_charset(self): + parse_accept_header = partial(_parse_accept_header, + value_sort_key=_charset_sort_key) + self.assertEqual(parse_accept_header('ISO-8859-1,utf-8;q=0.7,*;q=0.7'), + [('ISO-8859-1', 'ISO-8859-1', 1.0), + ('utf-8', 'utf-8', 0.7), + ('*', '*', 0.7)]) + +if __name__ == '__main__': + unittest_main() diff -r d6d905d0344f -r b817d44cb606 web/views/basecontrollers.py --- a/web/views/basecontrollers.py Fri Apr 01 14:26:18 2011 +0200 +++ b/web/views/basecontrollers.py Fri Apr 01 14:38:16 2011 +0200 @@ -455,13 +455,13 @@ def js_reledit_form(self): req = self._cw args = dict((x, req.form[x]) - for x in ('formid', 'rtype', 'role', 'reload')) + for x in ('formid', 'rtype', 'role', 'reload', 'action')) rset = req.eid_rset(typed_eid(self._cw.form['eid'])) try: args['reload'] = json.loads(args['reload']) except ValueError: # not true/false, an absolute url assert args['reload'].startswith('http') - view = req.vreg['views'].select('doreledit', req, rset=rset, rtype=args['rtype']) + view = req.vreg['views'].select('reledit', req, rset=rset, rtype=args['rtype']) return self._call_view(view, **args) @jsonize diff -r d6d905d0344f -r b817d44cb606 web/views/reledit.py --- a/web/views/reledit.py Fri Apr 01 14:26:18 2011 +0200 +++ b/web/views/reledit.py Fri Apr 01 14:38:16 2011 +0200 @@ -15,7 +15,9 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""the 'reledit' feature (eg edit attribute/relation from primary view)""" +"""edit entity attributes/relations from any view, without going to the entity +form +""" __docformat__ = "restructuredtext en" _ = unicode @@ -24,7 +26,8 @@ from warnings import warn from logilab.mtconverter import xml_escape -from logilab.common.deprecation import deprecated +from logilab.common.deprecation import deprecated, class_renamed +from logilab.common.decorators import cached from cubicweb import neg_role from cubicweb.schema import display_name @@ -43,16 +46,18 @@ return u'' def append_field(self, *args): pass + def add_hidden(self, *args): + pass rctrl = uicfg.reledit_ctrl -class ClickAndEditFormView(EntityView): - __regid__ = 'doreledit' +class AutoClickAndEditFormView(EntityView): + __regid__ = 'reledit' __select__ = non_final_entity() & match_kwargs('rtype') # ui side continuations _onclick = (u"cw.reledit.loadInlineEditionForm('%(formid)s', %(eid)s, '%(rtype)s', '%(role)s', " - "'%(divid)s', %(reload)s, '%(vid)s');") + "'%(divid)s', %(reload)s, '%(vid)s', '%(action)s');") _cancelclick = "cw.reledit.cleanupAfterCancel('%s')" # ui side actions/buttons @@ -75,93 +80,84 @@ # function taking the subject entity & returning a boolean or an eid rvid=None, # vid to be applied to other side of rtype (non final relations only) default_value=None, - formid='base' + formid='base', + action=None ): """display field to edit entity's `rtype` relation on click""" assert rtype - assert role in ('subject', 'object'), '%s is not an acceptable role value' % role self._cw.add_css('cubicweb.form.css') self._cw.add_js(('cubicweb.reledit.js', 'cubicweb.edition.js', 'cubicweb.ajax.js')) - entity = self.cw_rset.get_entity(row, col) + self.entity = self.cw_rset.get_entity(row, col) rschema = self._cw.vreg.schema[rtype] - self._rules = rctrl.etype_get(entity.e_schema.type, rschema.type, role, '*') + self._rules = rctrl.etype_get(self.entity.e_schema.type, rschema.type, role, '*') if rvid is not None or default_value is not None: warn('[3.9] specifying rvid/default_value on select is deprecated, ' 'reledit_ctrl rtag to control this' % self, DeprecationWarning) - reload = self._compute_reload(entity, rschema, role, reload) - divid = self._build_divid(rtype, role, entity.eid) + reload = self._compute_reload(rschema, role, reload) + divid = self._build_divid(rtype, role, self.entity.eid) if rschema.final: - self._handle_attribute(entity, rschema, role, divid, reload) + self._handle_attribute(rschema, role, divid, reload, action) else: if self._is_composite(): - self._handle_composite(entity, rschema, role, divid, reload, formid) + self._handle_composite(rschema, role, divid, reload, formid, action) else: - self._handle_relation(entity, rschema, role, divid, reload, formid) + self._handle_relation(rschema, role, divid, reload, formid, action) - def _handle_attribute(self, entity, rschema, role, divid, reload): - rtype = rschema.type - value = entity.printable_value(rtype) - if not self._should_edit_attribute(entity, rschema): + def _handle_attribute(self, rschema, role, divid, reload, action): + value = self.entity.printable_value(rschema.type) + if not self._should_edit_attribute(rschema): self.w(value) return - display_label, related_entity = self._prepare_form(entity, rtype, role) - form, renderer = self._build_form(entity, rtype, role, divid, 'base', - reload, display_label, related_entity) + form, renderer = self._build_form(self.entity, rschema, role, divid, 'base', reload, action) value = value or self._compute_default_value(rschema, role) self.view_form(divid, value, form, renderer) - def _compute_formid_value(self, entity, rschema, role, rvid, formid): - related_rset = entity.related(rschema.type, role) + def _compute_formid_value(self, rschema, role, rvid, formid): + related_rset = self.entity.related(rschema.type, role) if related_rset: value = self._cw.view(rvid, related_rset) else: value = self._compute_default_value(rschema, role) - if not self._should_edit_relation(entity, rschema, role): + if not self._should_edit_relation(rschema, role): return None, value return formid, value - def _handle_relation(self, entity, rschema, role, divid, reload, formid): + def _handle_relation(self, rschema, role, divid, reload, formid, action): rvid = self._rules.get('rvid', 'autolimited') - formid, value = self._compute_formid_value(entity, rschema, role, rvid, formid) + formid, value = self._compute_formid_value(rschema, role, rvid, formid) if formid is None: return self.w(value) - rtype = rschema.type - display_label, related_entity = self._prepare_form(entity, rtype, role) - form, renderer = self._build_form(entity, rtype, role, divid, formid, reload, - display_label, related_entity, dict(vid=rvid)) + form, renderer = self._build_form(self.entity, rschema, role, divid, formid, + reload, action, dict(vid=rvid)) self.view_form(divid, value, form, renderer) - def _handle_composite(self, entity, rschema, role, divid, reload, formid): + def _handle_composite(self, rschema, role, divid, reload, formid, action): # this is for attribute-like composites (1 target type, 1 related entity at most, for now) - ttypes = self._compute_ttypes(rschema, role) + entity = self.entity related_rset = entity.related(rschema.type, role) - add_related = self._may_add_related(related_rset, entity, rschema, role, ttypes) - edit_related = self._may_edit_related_entity(related_rset, entity, rschema, role, ttypes) - delete_related = edit_related and self._may_delete_related(related_rset, entity, rschema, role) + add_related = self._may_add_related(related_rset, rschema, role) + edit_related = self._may_edit_related_entity(related_rset, rschema, role) + delete_related = edit_related and self._may_delete_related(related_rset, rschema, role) rvid = self._rules.get('rvid', 'autolimited') - formid, value = self._compute_formid_value(entity, rschema, role, rvid, formid) + formid, value = self._compute_formid_value(rschema, role, rvid, formid) if formid is None or not (edit_related or add_related): # till we learn to handle cases where not (edit_related or add_related) self.w(value) return - rtype = rschema.type - ttype = ttypes[0] - _fdata = self._prepare_composite_form(entity, rtype, role, edit_related, - add_related and ttype) - display_label, related_entity = _fdata - form, renderer = self._build_form(entity, rtype, role, divid, formid, reload, - display_label, related_entity, dict(vid=rvid)) + form, renderer = self._build_form(entity, rschema, role, divid, formid, + reload, action, dict(vid=rvid)) self.view_form(divid, value, form, renderer, edit_related, add_related, delete_related) + @cached def _compute_ttypes(self, rschema, role): dual_role = neg_role(role) return getattr(rschema, '%ss' % dual_role)() - def _compute_reload(self, entity, rschema, role, reload): + def _compute_reload(self, rschema, role, reload): ctrl_reload = self._rules.get('reload', reload) if callable(ctrl_reload): - ctrl_reload = ctrl_reload(entity) + ctrl_reload = ctrl_reload(self.entity) if isinstance(ctrl_reload, int) and ctrl_reload > 1: # not True/False ctrl_reload = self._cw.build_url(ctrl_reload) return ctrl_reload @@ -179,33 +175,36 @@ def _is_composite(self): return self._rules.get('edit_target') == 'related' - def _may_add_related(self, related_rset, entity, rschema, role, ttypes): + def _may_add_related(self, related_rset, rschema, role): """ ok for attribute-like composite entities """ + ttypes = self._compute_ttypes(rschema, role) if len(ttypes) > 1: # many etypes: learn how to do it return False - rdef = rschema.role_rdef(entity.e_schema, ttypes[0], role) + rdef = rschema.role_rdef(self.entity.e_schema, ttypes[0], role) card = rdef.role_cardinality(role) if related_rset or card not in '?1': return False if role == 'subject': - kwargs = {'fromeid': entity.eid} + kwargs = {'fromeid': self.entity.eid} else: - kwargs = {'toeid': entity.eid} + kwargs = {'toeid': self.entity.eid} return rdef.has_perm(self._cw, 'add', **kwargs) - def _may_edit_related_entity(self, related_rset, entity, rschema, role, ttypes): + def _may_edit_related_entity(self, related_rset, rschema, role): """ controls the edition of the related entity """ + ttypes = self._compute_ttypes(rschema, role) if len(ttypes) > 1 or len(related_rset.rows) != 1: return False - if entity.e_schema.rdef(rschema, role).role_cardinality(role) not in '?1': + if self.entity.e_schema.rdef(rschema, role).role_cardinality(role) not in '?1': return False return related_rset.get_entity(0, 0).cw_has_perm('update') - def _may_delete_related(self, related_rset, entity, rschema, role): + def _may_delete_related(self, related_rset, rschema, role): # we assume may_edit_related, only 1 related entity if not related_rset: return False rentity = related_rset.get_entity(0, 0) + entity = self.entity if role == 'subject': kwargs = {'fromeid': entity.eid, 'toeid': rentity.eid} else: @@ -230,33 +229,33 @@ """ builds an id for the root div of a reledit widget """ return '%s-%s-%s' % (rtype, role, entity_eid) - def _build_args(self, entity, rtype, role, formid, reload, + def _build_args(self, entity, rtype, role, formid, reload, action, extradata=None): divid = self._build_divid(rtype, role, entity.eid) event_args = {'divid' : divid, 'eid' : entity.eid, 'rtype' : rtype, 'formid': formid, - 'reload' : json_dumps(reload), + 'reload' : json_dumps(reload), 'action': action, 'role' : role, 'vid' : u''} if extradata: event_args.update(extradata) return event_args - def _prepare_form(self, entity, _rtype, role): - display_label = False - related_entity = entity - return display_label, related_entity - - def _prepare_composite_form(self, entity, rtype, role, edit_related, add_related): - display_label = True - if edit_related and not add_related: - related_entity = entity.related(rtype, role).get_entity(0, 0) - elif add_related: - _new_entity = self._cw.vreg['etypes'].etype_class(add_related)(self._cw) + def _prepare_form(self, entity, rschema, role, action): + assert action in ('edit_rtype', 'edit_related', 'add', 'delete'), action + if action == 'edit_rtype': + return False, entity + label = True + if action in ('edit_related', 'delete'): + edit_entity = entity.related(rschema, role).get_entity(0, 0) + elif action == 'add': + add_etype = self._compute_ttypes(rschema, role)[0] + _new_entity = self._cw.vreg['etypes'].etype_class(add_etype)(self._cw) _new_entity.eid = self._cw.varmaker.next() - related_entity = _new_entity + edit_entity = _new_entity # XXX see forms.py ~ 276 and entities.linked_to method # is there another way ? - self._cw.form['__linkto'] = '%s:%s:%s' % (rtype, entity.eid, neg_role(role)) - return display_label, related_entity + self._cw.form['__linkto'] = '%s:%s:%s' % (rschema, entity.eid, neg_role(role)) + assert edit_entity + return label, edit_entity def _build_renderer(self, related_entity, display_label): return self._cw.vreg['formrenderers'].select( @@ -266,13 +265,18 @@ display_help=False, button_bar_class='buttonbar', display_progress_div=False) - def _build_form(self, entity, rtype, role, divid, formid, reload, - display_label, related_entity, extradata=None, **formargs): - event_args = self._build_args(entity, rtype, role, formid, - reload, extradata) + def _build_form(self, entity, rschema, role, divid, formid, reload, action, + extradata=None, **formargs): + rtype = rschema.type + event_args = self._build_args(entity, rtype, role, formid, reload, action, extradata) + if not action: + form = _DummyForm() + form.event_args = event_args + return form, None + label, edit_entity = self._prepare_form(entity, rschema, role, action) cancelclick = self._cancelclick % divid form = self._cw.vreg['forms'].select( - formid, self._cw, rset=related_entity.as_rset(), entity=related_entity, + formid, self._cw, rset=edit_entity.as_rset(), entity=edit_entity, domid='%s-form' % divid, formtype='inlined', action=self._cw.build_url('validateform', __onsuccess='window.parent.cw.reledit.onSuccess'), cwtarget='eformframe', cssclass='releditForm', @@ -298,9 +302,10 @@ if formid == 'base': field = form.field_by_name(rtype, role, entity.e_schema) form.append_field(field) - return form, self._build_renderer(related_entity, display_label) + return form, self._build_renderer(edit_entity, label) - def _should_edit_attribute(self, entity, rschema): + def _should_edit_attribute(self, rschema): + entity = self.entity rdef = entity.e_schema.rdef(rschema) # check permissions if not entity.cw_has_perm('update'): @@ -312,8 +317,8 @@ ' use _should_edit_attribute instead', _should_edit_attribute) - def _should_edit_relation(self, entity, rschema, role): - eeid = entity.eid + def _should_edit_relation(self, rschema, role): + eeid = self.entity.eid perm_args = {'fromeid': eeid} if role == 'subject' else {'toeid': eeid} return rschema.has_perm(self._cw, 'add', **perm_args) @@ -335,9 +340,11 @@ w(u'