# HG changeset patch # User Sylvain Thénault # Date 1302125059 -7200 # Node ID 79686c864bbfcbb1ccb7b336147e5c5f48fe6098 # Parent 496f51b9215412816843cec3cb1ef9d52ea40803# Parent 7eaef037ea9db1376a39ba195d67f11b78e4d78f backport stable diff -r 7eaef037ea9d -r 79686c864bbf .hgtags --- a/.hgtags Wed Apr 06 23:23:48 2011 +0200 +++ b/.hgtags Wed Apr 06 23:24:19 2011 +0200 @@ -188,3 +188,5 @@ 77318f1ec4aae3523d455e884daf3708c3c79af7 cubicweb-debian-version-3.11.1-1 56ae3cd5f8553678a2b1d4121b61241598d0ca68 cubicweb-version-3.11.2 954b5b51cd9278eb45d66be1967064d01ab08453 cubicweb-debian-version-3.11.2-1 +fd502219eb76f4bfd239d838a498a1d1e8204baf cubicweb-version-3.12.0 +92b56939b7c77bbf443b893c495a20f19bc30702 cubicweb-debian-version-3.12.0-1 diff -r 7eaef037ea9d -r 79686c864bbf __pkginfo__.py --- a/__pkginfo__.py Wed Apr 06 23:23:48 2011 +0200 +++ b/__pkginfo__.py Wed Apr 06 23:24:19 2011 +0200 @@ -22,7 +22,7 @@ modname = distname = "cubicweb" -numversion = (3, 11, 2) +numversion = (3, 12, 0) version = '.'.join(str(num) for num in numversion) description = "a repository of entities / relations for knowledge management" @@ -43,7 +43,7 @@ 'logilab-common': '>= 0.55.2', 'logilab-mtconverter': '>= 0.8.0', 'rql': '>= 0.28.0', - 'yams': '>= 0.30.4', + 'yams': '>= 0.32.0', 'docutils': '>= 0.6', #gettext # for xgettext, msgcat, etc... # web dependancies @@ -52,7 +52,7 @@ 'Twisted': '', # XXX graphviz # server dependencies - 'logilab-database': '>= 1.4.0', + 'logilab-database': '>= 1.5.0', 'pysqlite': '>= 2.5.5', # XXX install pysqlite2 } diff -r 7eaef037ea9d -r 79686c864bbf cwvreg.py --- a/cwvreg.py Wed Apr 06 23:23:48 2011 +0200 +++ b/cwvreg.py Wed Apr 06 23:24:19 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': @@ -835,18 +839,24 @@ return self['views'].select(__vid, req, rset=rset, **kwargs) +import decimal from datetime import datetime, date, time, timedelta -YAMS_TO_PY = { - 'Boolean': bool, +YAMS_TO_PY = { # XXX unify with yams.constraints.BASE_CONVERTERS? 'String' : unicode, + 'Bytes': Binary, 'Password': str, - 'Bytes': Binary, + + 'Boolean': bool, 'Int': int, 'Float': float, - 'Date': date, - 'Datetime': datetime, - 'Time': time, - 'Interval': timedelta, + 'Decimal': decimal.Decimal, + + 'Date': date, + 'Datetime': datetime, + 'TZDatetime': datetime, + 'Time': time, + 'TZTime': time, + 'Interval': timedelta, } diff -r 7eaef037ea9d -r 79686c864bbf debian/changelog --- a/debian/changelog Wed Apr 06 23:23:48 2011 +0200 +++ b/debian/changelog Wed Apr 06 23:24:19 2011 +0200 @@ -1,3 +1,9 @@ +cubicweb (3.12.0-1) unstable; urgency=low + + * new upstream release + + -- Alexandre Fayolle Fri, 01 Apr 2011 15:59:37 +0200 + cubicweb (3.11.2-1) unstable; urgency=low * new upstream release diff -r 7eaef037ea9d -r 79686c864bbf debian/control --- a/debian/control Wed Apr 06 23:23:48 2011 +0200 +++ b/debian/control Wed Apr 06 23:24:19 2011 +0200 @@ -33,7 +33,7 @@ Conflicts: cubicweb-multisources Replaces: cubicweb-multisources Provides: cubicweb-multisources -Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.4.0), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2 +Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.5.0), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2 Recommends: pyro (<< 4.0.0), cubicweb-documentation (= ${source:Version}) Description: server part of the CubicWeb framework CubicWeb is a semantic web application framework. @@ -97,7 +97,7 @@ Package: cubicweb-common Architecture: all XB-Python-Version: ${python:Versions} -Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.55.2), python-yams (>= 0.30.4), python-rql (>= 0.28.0), python-lxml +Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.55.2), python-yams (>= 0.32.0), python-rql (>= 0.28.0), python-lxml Recommends: python-simpletal (>= 4.0), python-crypto Conflicts: cubicweb-core Replaces: cubicweb-core diff -r 7eaef037ea9d -r 79686c864bbf devtools/fill.py --- a/devtools/fill.py Wed Apr 06 23:23:48 2011 +0200 +++ b/devtools/fill.py Wed Apr 06 23:24:19 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. @@ -152,6 +152,8 @@ base = datetime(randint(2000, 2004), randint(1, 12), randint(1, 28), 11, index%60) return self._constrained_generate(entity, attrname, base, timedelta(hours=1), index) + generate_tzdatetime = generate_datetime # XXX implementation should add a timezone + def generate_date(self, entity, attrname, index): """generates a random date (format is 'yyyy-mm-dd')""" base = date(randint(2000, 2010), 1, 1) + timedelta(randint(1, 365)) @@ -166,6 +168,8 @@ """generates a random time (format is ' HH:MM')""" return time(11, index%60) #'11:%02d' % (index % 60) + generate_tztime = generate_time # XXX implementation should add a timezone + def generate_bytes(self, entity, attrname, index, format=None): fakefile = Binary("%s%s" % (attrname, index)) fakefile.filename = u"file_%s" % attrname @@ -441,7 +445,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 7eaef037ea9d -r 79686c864bbf entity.py --- a/entity.py Wed Apr 06 23:23:48 2011 +0200 +++ b/entity.py Wed Apr 06 23:24:19 2011 +0200 @@ -28,7 +28,7 @@ from rql.utils import rqlvar_maker -from cubicweb import Unauthorized, typed_eid +from cubicweb import Unauthorized, typed_eid, neg_role from cubicweb.rset import ResultSet from cubicweb.selectors import yes from cubicweb.appobject import AppObject @@ -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,83 @@ 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 rdef.role_cardinality(neg_role(role)) in '?1': + # if cardinality in '1?', we want a target entity which isn't + # already linked using this relation + if searchedvar == 'S': + restriction = ['NOT S %s ZZ' % rtype] + else: + restriction = ['NOT ZZ %s O' % rtype] + elif self.has_eid(): + # elif we have an eid, we don't want a target entity which is + # already linked to ourself through this relation + restriction = ['NOT S %s O' % rtype] + else: + restriction = [] if self.has_eid(): - restriction = ['NOT S %s O' % rtype, '%s eid %%(x)s' % evar] + restriction += ['%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 = [] 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 +852,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 7eaef037ea9d -r 79686c864bbf misc/migration/3.12.0_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/migration/3.12.0_Any.py Wed Apr 06 23:24:19 2011 +0200 @@ -0,0 +1,2 @@ +add_entity_type('TZDatetime') +add_entity_type('TZTime') diff -r 7eaef037ea9d -r 79686c864bbf rqlrewrite.py --- a/rqlrewrite.py Wed Apr 06 23:23:48 2011 +0200 +++ b/rqlrewrite.py Wed Apr 06 23:24:19 2011 +0200 @@ -257,6 +257,11 @@ insert_scope = None for vi in self.varinfos: scope = vi.get('stinfo', {}).get('scope', self.select) + while True: + negstmt = scope.neged() + if negstmt is None: + break + scope = negstmt.scope if insert_scope is None: insert_scope = scope else: diff -r 7eaef037ea9d -r 79686c864bbf schema.py --- a/schema.py Wed Apr 06 23:23:48 2011 +0200 +++ b/schema.py Wed Apr 06 23:24:19 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 7eaef037ea9d -r 79686c864bbf server/migractions.py --- a/server/migractions.py Wed Apr 06 23:23:48 2011 +0200 +++ b/server/migractions.py Wed Apr 06 23:24:19 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, @@ -757,9 +758,9 @@ targeted type is known """ instschema = self.repo.schema - assert not etype in instschema, \ + eschema = self.fs_schema.eschema(etype) + assert eschema.final or not etype in instschema, \ '%s already defined in the instance schema' % etype - eschema = self.fs_schema.eschema(etype) confirm = self.verbosity >= 2 groupmap = self.group_mapping() cstrtypemap = self.cstrtype_mapping() diff -r 7eaef037ea9d -r 79686c864bbf server/schemaserial.py --- a/server/schemaserial.py Wed Apr 06 23:23:48 2011 +0200 +++ b/server/schemaserial.py Wed Apr 06 23:24:19 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 7eaef037ea9d -r 79686c864bbf server/sources/rql2sql.py --- a/server/sources/rql2sql.py Wed Apr 06 23:23:48 2011 +0200 +++ b/server/sources/rql2sql.py Wed Apr 06 23:24:19 2011 +0200 @@ -50,7 +50,9 @@ __docformat__ = "restructuredtext en" import threading +from datetime import datetime, time +from logilab.common.date import utcdatetime, utctime from logilab.database import FunctionDescr, SQL_FUNCTIONS_REGISTRY from rql import BadRQLQuery, CoercionError @@ -1424,6 +1426,14 @@ _id = value if isinstance(_id, unicode): _id = _id.encode() + # convert timestamp to utc. + # expect SET TiME ZONE to UTC at connection opening time. + # This shouldn't change anything for datetime without TZ. + value = self._args[_id] + if isinstance(value, datetime) and value.tzinfo is not None: + self._query_attrs[_id] = utcdatetime(value) + elif isinstance(value, time) and value.tzinfo is not None: + self._query_attrs[_id] = utctime(value) else: _id = str(id(constant)).replace('-', '', 1) self._query_attrs[_id] = value diff -r 7eaef037ea9d -r 79686c864bbf server/sqlutils.py --- a/server/sqlutils.py Wed Apr 06 23:23:48 2011 +0200 +++ b/server/sqlutils.py Wed Apr 06 23:24:19 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. @@ -25,7 +25,7 @@ from logilab import database as db, common as lgc from logilab.common.shellutils import ProgressBar -from logilab.common.date import todate, todatetime +from logilab.common.date import todate, todatetime, utcdatetime, utctime from logilab.database.sqlgen import SQLGenerator from cubicweb import Binary, ConfigurationError @@ -274,10 +274,15 @@ value = crypt_password(value) value = self._binary(value) # XXX needed for sqlite but I don't think it is for other backends - elif atype == 'Datetime' and isinstance(value, date): + # Note: use is __class__ since issubclass(datetime, date) + elif atype in ('Datetime', 'TZDatetime') and value.__class__ is date: value = todatetime(value) elif atype == 'Date' and isinstance(value, datetime): value = todate(value) + elif atype == 'TZDatetime' and getattr(value, 'tzinfo', None): + value = utcdatetime(value) + elif atype == 'TZTime' and getattr(value, 'tzinfo', None): + value = utctime(value) elif isinstance(value, Binary): value = self._binary(value.getvalue()) attrs[SQL_PREFIX+str(attr)] = value @@ -326,3 +331,10 @@ sqlite_hooks = SQL_CONNECT_HOOKS.setdefault('sqlite', []) sqlite_hooks.append(init_sqlite_connexion) + + +def init_postgres_connexion(cnx): + cnx.cursor().execute('SET TIME ZONE UTC') + +postgres_hooks = SQL_CONNECT_HOOKS.setdefault('postgres', []) +postgres_hooks.append(init_postgres_connexion) diff -r 7eaef037ea9d -r 79686c864bbf server/test/data/schema.py --- a/server/test/data/schema.py Wed Apr 06 23:23:48 2011 +0200 +++ b/server/test/data/schema.py Wed Apr 06 23:24:19 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. @@ -18,7 +18,7 @@ from yams.buildobjs import (EntityType, RelationType, RelationDefinition, SubjectRelation, RichString, String, Int, Float, - Boolean, Datetime) + Boolean, Datetime, TZDatetime) from yams.constraints import SizeConstraint from cubicweb.schema import (WorkflowableEntityType, RQLConstraint, RQLUniqueConstraint, @@ -114,6 +114,7 @@ tel = Int() fax = Int() datenaiss = Datetime() + tzdatenaiss = TZDatetime() test = Boolean(__permissions__={ 'read': ('managers', 'users', 'guests'), 'update': ('managers',), diff -r 7eaef037ea9d -r 79686c864bbf server/test/data/sources_fti --- a/server/test/data/sources_fti Wed Apr 06 23:23:48 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,14 +0,0 @@ -[system] - -db-driver = postgres -db-host = localhost -db-port = -adapter = native -db-name = cw_fti_test -db-encoding = UTF-8 -db-user = syt -db-password = syt - -[admin] -login = admin -password = gingkow diff -r 7eaef037ea9d -r 79686c864bbf server/test/data/sources_postgres --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/server/test/data/sources_postgres Wed Apr 06 23:24:19 2011 +0200 @@ -0,0 +1,14 @@ +[system] + +db-driver = postgres +db-host = localhost +db-port = 5433 +adapter = native +db-name = cw_fti_test +db-encoding = UTF-8 +db-user = syt +db-password = syt + +[admin] +login = admin +password = gingkow diff -r 7eaef037ea9d -r 79686c864bbf server/test/unittest_fti.py --- a/server/test/unittest_fti.py Wed Apr 06 23:23:48 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,68 +0,0 @@ -from __future__ import with_statement - -import socket - -from logilab.common.testlib import SkipTest - -from cubicweb.devtools import ApptestConfiguration -from cubicweb.devtools.testlib import CubicWebTC -from cubicweb.selectors import is_instance -from cubicweb.entities.adapters import IFTIndexableAdapter - -AT_LOGILAB = socket.gethostname().endswith('.logilab.fr') - - -class PostgresFTITC(CubicWebTC): - config = ApptestConfiguration('data', sourcefile='sources_fti') - - @classmethod - def setUpClass(cls): - if not AT_LOGILAB: - raise SkipTest('XXX %s: require logilab configuration' % cls.__name__) - - def test_occurence_count(self): - req = self.request() - c1 = req.create_entity('Card', title=u'c1', - content=u'cubicweb cubicweb cubicweb') - c2 = req.create_entity('Card', title=u'c3', - content=u'cubicweb') - c3 = req.create_entity('Card', title=u'c2', - content=u'cubicweb cubicweb') - self.commit() - self.assertEqual(req.execute('Card X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows, - [[c1.eid], [c3.eid], [c2.eid]]) - - - def test_attr_weight(self): - class CardIFTIndexableAdapter(IFTIndexableAdapter): - __select__ = is_instance('Card') - attr_weight = {'title': 'A'} - with self.temporary_appobjects(CardIFTIndexableAdapter): - req = self.request() - c1 = req.create_entity('Card', title=u'c1', - content=u'cubicweb cubicweb cubicweb') - c2 = req.create_entity('Card', title=u'c2', - content=u'cubicweb cubicweb') - c3 = req.create_entity('Card', title=u'cubicweb', - content=u'autre chose') - self.commit() - self.assertEqual(req.execute('Card X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows, - [[c3.eid], [c1.eid], [c2.eid]]) - - def test_entity_weight(self): - class PersonneIFTIndexableAdapter(IFTIndexableAdapter): - __select__ = is_instance('Personne') - entity_weight = 2.0 - with self.temporary_appobjects(PersonneIFTIndexableAdapter): - req = self.request() - c1 = req.create_entity('Personne', nom=u'c1', prenom=u'cubicweb') - c2 = req.create_entity('Comment', content=u'cubicweb cubicweb', comments=c1) - c3 = req.create_entity('Comment', content=u'cubicweb cubicweb cubicweb', comments=c1) - self.commit() - self.assertEqual(req.execute('Any X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows, - [[c1.eid], [c3.eid], [c2.eid]]) - - -if __name__ == '__main__': - from logilab.common.testlib import unittest_main - unittest_main() diff -r 7eaef037ea9d -r 79686c864bbf server/test/unittest_postgres.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/server/test/unittest_postgres.py Wed Apr 06 23:24:19 2011 +0200 @@ -0,0 +1,77 @@ +from __future__ import with_statement + +import socket +from datetime import datetime + +from logilab.common.testlib import SkipTest + +from cubicweb.devtools import ApptestConfiguration +from cubicweb.devtools.testlib import CubicWebTC +from cubicweb.selectors import is_instance +from cubicweb.entities.adapters import IFTIndexableAdapter + +AT_LOGILAB = socket.gethostname().endswith('.logilab.fr') # XXX + +from unittest_querier import FixedOffset + +class PostgresFTITC(CubicWebTC): + config = ApptestConfiguration('data', sourcefile='sources_postgres') + + @classmethod + def setUpClass(cls): + if not AT_LOGILAB: # XXX here until we can raise SkipTest in setUp to detect we can't connect to the db + raise SkipTest('XXX %s: require logilab configuration' % cls.__name__) + + def test_occurence_count(self): + req = self.request() + c1 = req.create_entity('Card', title=u'c1', + content=u'cubicweb cubicweb cubicweb') + c2 = req.create_entity('Card', title=u'c3', + content=u'cubicweb') + c3 = req.create_entity('Card', title=u'c2', + content=u'cubicweb cubicweb') + self.commit() + self.assertEqual(req.execute('Card X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows, + [[c1.eid], [c3.eid], [c2.eid]]) + + + def test_attr_weight(self): + class CardIFTIndexableAdapter(IFTIndexableAdapter): + __select__ = is_instance('Card') + attr_weight = {'title': 'A'} + with self.temporary_appobjects(CardIFTIndexableAdapter): + req = self.request() + c1 = req.create_entity('Card', title=u'c1', + content=u'cubicweb cubicweb cubicweb') + c2 = req.create_entity('Card', title=u'c2', + content=u'cubicweb cubicweb') + c3 = req.create_entity('Card', title=u'cubicweb', + content=u'autre chose') + self.commit() + self.assertEqual(req.execute('Card X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows, + [[c3.eid], [c1.eid], [c2.eid]]) + + def test_entity_weight(self): + class PersonneIFTIndexableAdapter(IFTIndexableAdapter): + __select__ = is_instance('Personne') + entity_weight = 2.0 + with self.temporary_appobjects(PersonneIFTIndexableAdapter): + req = self.request() + c1 = req.create_entity('Personne', nom=u'c1', prenom=u'cubicweb') + c2 = req.create_entity('Comment', content=u'cubicweb cubicweb', comments=c1) + c3 = req.create_entity('Comment', content=u'cubicweb cubicweb cubicweb', comments=c1) + self.commit() + self.assertEqual(req.execute('Any X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows, + [[c1.eid], [c3.eid], [c2.eid]]) + + + def test_tz_datetime(self): + self.execute("INSERT Personne X: X nom 'bob', X tzdatenaiss %(date)s", + {'date': datetime(1977, 6, 7, 2, 0, tzinfo=FixedOffset(1))}) + datenaiss = self.execute("Any XD WHERE X nom 'bob', X tzdatenaiss XD")[0][0] + self.assertEqual(datenaiss.utctimetuple()[:5], (1977, 6, 7, 1, 0)) + + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() diff -r 7eaef037ea9d -r 79686c864bbf server/test/unittest_querier.py --- a/server/test/unittest_querier.py Wed Apr 06 23:23:48 2011 +0200 +++ b/server/test/unittest_querier.py Wed Apr 06 23:24:19 2011 +0200 @@ -18,7 +18,7 @@ # with CubicWeb. If not, see . """unit tests for modules cubicweb.server.querier and cubicweb.server.ssplanner """ -from datetime import date, datetime +from datetime import date, datetime, timedelta, tzinfo from logilab.common.testlib import TestCase, unittest_main from rql import BadRQLQuery, RQLSyntaxError @@ -32,6 +32,14 @@ from cubicweb.devtools.repotest import tuplify, BaseQuerierTC from unittest_session import Variable +class FixedOffset(tzinfo): + def __init__(self, hours=0): + self.hours = hours + def utcoffset(self, dt): + return timedelta(hours=self.hours) + def dst(self, dt): + return timedelta(0) + # register priority/severity sorting registered procedure from rql.utils import register_function, FunctionDescr @@ -761,18 +769,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') @@ -1213,11 +1223,11 @@ self.assertEqual(rset.description, [('CWUser',)]) def test_update_upassword(self): - cursor = self.pool['system'] rset = self.execute("INSERT CWUser X: X login 'bob', X upassword %(pwd)s", {'pwd': 'toto'}) self.assertEqual(rset.description[0][0], 'CWUser') rset = self.execute("SET X upassword %(pwd)s WHERE X is CWUser, X login 'bob'", {'pwd': 'tutu'}) + cursor = self.pool['system'] cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'" % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX)) passwd = str(cursor.fetchone()[0]) @@ -1227,7 +1237,15 @@ self.assertEqual(len(rset.rows), 1) self.assertEqual(rset.description, [('CWUser',)]) - # non regression tests #################################################### + # ZT datetime tests ######################################################## + + def test_tz_datetime(self): + self.execute("INSERT Personne X: X nom 'bob', X tzdatenaiss %(date)s", + {'date': datetime(1977, 6, 7, 2, 0, tzinfo=FixedOffset(1))}) + datenaiss = self.execute("Any XD WHERE X nom 'bob', X tzdatenaiss XD")[0][0] + self.assertEqual(datenaiss.utctimetuple()[:5], (1977, 6, 7, 1, 0)) + + # non regression tests ##################################################### def test_nonregr_1(self): teid = self.execute("INSERT Tag X: X name 'tag'")[0][0] diff -r 7eaef037ea9d -r 79686c864bbf server/test/unittest_repository.py --- a/server/test/unittest_repository.py Wed Apr 06 23:23:48 2011 +0200 +++ b/server/test/unittest_repository.py Wed Apr 06 23:24:19 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, " @@ -327,7 +328,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 7eaef037ea9d -r 79686c864bbf server/test/unittest_rql2sql.py --- a/server/test/unittest_rql2sql.py Wed Apr 06 23:23:48 2011 +0200 +++ b/server/test/unittest_rql2sql.py Wed Apr 06 23:24:19 2011 +0200 @@ -1225,9 +1225,13 @@ yield self._check, rql, sql def _checkall(self, rql, sql): + if isinstance(rql, tuple): + rql, args = rql + else: + args = None try: rqlst = self._prepare(rql) - r, args, cbs = self.o.generate(rqlst) + r, args, cbs = self.o.generate(rqlst, args) self.assertEqual((r.strip(), args), sql) except Exception, ex: print rql @@ -1239,7 +1243,7 @@ return def test1(self): - self._checkall('Any count(RDEF) WHERE RDEF relation_type X, X eid %(x)s', + self._checkall(('Any count(RDEF) WHERE RDEF relation_type X, X eid %(x)s', {'x': None}), ("""SELECT COUNT(T1.C0) FROM (SELECT _RDEF.cw_eid AS C0 FROM cw_CWAttribute AS _RDEF WHERE _RDEF.cw_relation_type=%(x)s @@ -1250,7 +1254,7 @@ ) def test2(self): - self._checkall('Any X WHERE C comments X, C eid %(x)s', + self._checkall(('Any X WHERE C comments X, C eid %(x)s', {'x': None}), ('''SELECT rel_comments0.eid_to FROM comments_relation AS rel_comments0 WHERE rel_comments0.eid_from=%(x)s''', {}) diff -r 7eaef037ea9d -r 79686c864bbf test/data/schema.py --- a/test/data/schema.py Wed Apr 06 23:23:48 2011 +0200 +++ b/test/data/schema.py Wed Apr 06 23:24:19 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 7eaef037ea9d -r 79686c864bbf test/unittest_entity.py --- a/test/unittest_entity.py Wed Apr 06 23:23:48 2011 +0200 +++ b/test/unittest_entity.py Wed Apr 06 23:24:19 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(ZZ 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,43 +250,80 @@ 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(ZZ 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] - self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE ' - 'NOT EXISTS(S use_email O), S eid %(x)s, ' - 'O is EmailAddress, O address AA, O alias AB, O modification_date AC, ' - 'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)') + self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC ' + 'WHERE NOT EXISTS(ZZ use_email O, ZZ is CWUser), S eid %(x)s, ' + 'O is EmailAddress, O address AA, O alias AB, O modification_date AC, A eid %(B)s, ' + 'EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)') def test_unrelated_rql_security_1_anon(self): self.login('anon') 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 EXISTS(S use_email O), S eid %(x)s, ' - 'O is EmailAddress, O address AA, O alias AB, O modification_date AC, ' - 'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)') + self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC ' + 'WHERE NOT EXISTS(ZZ use_email O, ZZ is CWUser), S eid %(x)s, ' + 'O is EmailAddress, O address AA, O alias AB, O modification_date AC, A eid %(B)s, ' + 'EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)') 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), O eid %(x)s, 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 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)') 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 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 7eaef037ea9d -r 79686c864bbf test/unittest_schema.py --- a/test/unittest_schema.py Wed Apr 06 23:23:48 2011 +0200 +++ b/test/unittest_schema.py Wed Apr 06 23:24:19 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', + 'Tag', 'TZDatetime', 'TZTime', '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 7eaef037ea9d -r 79686c864bbf uilib.py --- a/uilib.py Wed Apr 06 23:23:48 2011 +0200 +++ b/uilib.py Wed Apr 06 23:24:19 2011 +0200 @@ -62,9 +62,9 @@ return value if attrtype == 'Date': return ustrftime(value, req.property_value('ui.date-format')) - if attrtype == 'Time': + if attrtype in ('Time', 'TZTime'): return ustrftime(value, req.property_value('ui.time-format')) - if attrtype == 'Datetime': + if attrtype in ('Datetime', 'TZDatetime'): if displaytime: return ustrftime(value, req.property_value('ui.datetime-format')) return ustrftime(value, req.property_value('ui.date-format')) @@ -72,8 +72,9 @@ if value: return req._('yes') return req._('no') - if attrtype == 'Float': + if attrtype in ('Float', 'Decimal'): value = req.property_value('ui.float-format') % value + # XXX Interval return unicode(value) diff -r 7eaef037ea9d -r 79686c864bbf utils.py --- a/utils.py Wed Apr 06 23:23:48 2011 +0200 +++ b/utils.py Wed Apr 06 23:24:19 2011 +0200 @@ -361,17 +361,43 @@ self.doctype = u'' # xmldecl and html opening tag self.xmldecl = u'\n' % req.encoding - self.htmltag = u'' % (req.lang, req.lang) + self._namespaces = [('xmlns', 'http://www.w3.org/1999/xhtml'), + ('xmlns:cubicweb','http://www.logilab.org/2008/cubicweb')] + self._htmlattrs = [('xml:lang', req.lang), + ('lang', req.lang)] # keep main_stream's reference on req for easier text/html demoting req.main_stream = self + def add_namespace(self, prefix, uri): + self._namespaces.append( (prefix, uri) ) + + def set_namespaces(self, namespaces): + self._namespaces = namespaces + + def add_htmlattr(self, attrname, attrvalue): + self._htmlattrs.append( (attrname, attrvalue) ) + + def set_htmlattrs(self, attrs): + self._htmlattrs = attrs + + def set_doctype(self, doctype, reset_xmldecl=True): + self.doctype = doctype + if reset_xmldecl: + self.xmldecl = u'' + def write(self, data): """StringIO interface: this method will be assigned to self.w """ self.body.write(data) + @property + def htmltag(self): + attrs = ' '.join('%s="%s"' % (attr, xml_escape(value)) + for attr, value in (self._namespaces + self._htmlattrs)) + if attrs: + return '' % attrs + return '' + def getvalue(self): """writes HTML headers, closes tag and writes HTML body""" return u'%s\n%s\n%s\n%s\n%s\n' % (self.xmldecl, self.doctype, @@ -379,7 +405,6 @@ self.head.getvalue(), self.body.getvalue()) - try: # may not be there if cubicweb-web not installed if sys.version_info < (2, 6): diff -r 7eaef037ea9d -r 79686c864bbf web/data/cubicweb.reledit.js --- a/web/data/cubicweb.reledit.js Wed Apr 06 23:23:48 2011 +0200 +++ b/web/data/cubicweb.reledit.js Wed Apr 06 23:24:19 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 7eaef037ea9d -r 79686c864bbf web/formfields.py --- a/web/formfields.py Wed Apr 06 23:23:48 2011 +0200 +++ b/web/formfields.py Wed Apr 06 23:24:19 2011 +0200 @@ -1200,14 +1200,19 @@ FIELDS = { - 'Boolean': BooleanField, + 'String' : StringField, 'Bytes': FileField, - 'Date': DateField, - 'Datetime': DateTimeField, + 'Password': PasswordField, + + 'Boolean': BooleanField, 'Int': IntField, 'Float': FloatField, 'Decimal': StringField, - 'Password': PasswordField, - 'String' : StringField, - 'Time': TimeField, + + 'Date': DateField, + 'Datetime': DateTimeField, + 'TZDatetime': DateTimeField, + 'Time': TimeField, + 'TZTime': TimeField, + # XXX implement 'Interval': TimeIntervalField, } diff -r 7eaef037ea9d -r 79686c864bbf web/request.py --- a/web/request.py Wed Apr 06 23:23:48 2011 +0200 +++ b/web/request.py Wed Apr 06 23:24:19 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 @@ -768,8 +756,16 @@ will display '<[' at the beginning of the page """ self.set_content_type('text/html') - self.main_stream.doctype = TRANSITIONAL_DOCTYPE_NOEXT - self.main_stream.xmldecl = u'' + self.main_stream.set_doctype(TRANSITIONAL_DOCTYPE_NOEXT) + + def set_doctype(self, doctype, reset_xmldecl=True): + """helper method to dynamically change page doctype + + :param doctype: the new doctype, e.g. '' + :param reset_xmldecl: if True, remove the '' + declaration from the page + """ + self.main_stream.set_doctype(doctype, reset_xmldecl) # page data management #################################################### @@ -858,5 +854,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 7eaef037ea9d -r 79686c864bbf web/schemaviewer.py --- a/web/schemaviewer.py Wed Apr 06 23:23:48 2011 +0200 +++ b/web/schemaviewer.py Wed Apr 06 23:24:19 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 7eaef037ea9d -r 79686c864bbf web/test/unittest_application.py --- a/web/test/unittest_application.py Wed Apr 06 23:23:48 2011 +0200 +++ b/web/test/unittest_application.py Wed Apr 06 23:24:19 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. diff -r 7eaef037ea9d -r 79686c864bbf web/test/unittest_form.py --- a/web/test/unittest_form.py Wed Apr 06 23:23:48 2011 +0200 +++ b/web/test/unittest_form.py Wed Apr 06 23:24:19 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 7eaef037ea9d -r 79686c864bbf web/test/unittest_reledit.py --- a/web/test/unittest_reledit.py Wed Apr 06 23:23:48 2011 +0200 +++ b/web/test/unittest_reledit.py Wed Apr 06 23:24:19 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 7eaef037ea9d -r 79686c864bbf web/test/unittest_request.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/test/unittest_request.py Wed Apr 06 23:24:19 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 7eaef037ea9d -r 79686c864bbf web/test/unittest_views_baseviews.py --- a/web/test/unittest_views_baseviews.py Wed Apr 06 23:23:48 2011 +0200 +++ b/web/test/unittest_views_baseviews.py Wed Apr 06 23:24:19 2011 +0200 @@ -16,11 +16,14 @@ # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . +from __future__ import with_statement + from logilab.common.testlib import unittest_main from logilab.mtconverter import html_unescape from cubicweb.devtools.testlib import CubicWebTC from cubicweb.utils import json +from cubicweb.view import StartupView, TRANSITIONAL_DOCTYPE_NOEXT from cubicweb.web.htmlwidgets import TableWidget from cubicweb.web.views import vid_from_rset @@ -125,5 +128,47 @@ self.assertListEqual(got, expected) +class HTMLStreamTests(CubicWebTC): + + def test_set_doctype_reset_xmldecl(self): + """ + tests `cubicweb.web.request.CubicWebRequestBase.set_doctype` + with xmldecl reset + """ + class MyView(StartupView): + __regid__ = 'my-view' + def call(self): + self._cw.set_doctype('') + + with self.temporary_appobjects(MyView): + html_source = self.view('my-view').source + source_lines = [line.strip() for line in html_source.splitlines(False) + if line.strip()] + self.assertListEqual(source_lines[:2], + ['', + '']) + + def test_set_doctype_no_reset_xmldecl(self): + """ + tests `cubicweb.web.request.CubicWebRequestBase.set_doctype` + with no xmldecl reset + """ + html_doctype = TRANSITIONAL_DOCTYPE_NOEXT.strip() + class MyView(StartupView): + __regid__ = 'my-view' + def call(self): + self._cw.set_doctype(html_doctype, reset_xmldecl=False) + self._cw.main_stream.set_namespaces([('xmlns', 'http://www.w3.org/1999/xhtml')]) + self._cw.main_stream.set_htmlattrs([('lang', 'cz')]) + + with self.temporary_appobjects(MyView): + html_source = self.view('my-view').source + source_lines = [line.strip() for line in html_source.splitlines(False) + if line.strip()] + self.assertListEqual(source_lines[:3], + ['', + html_doctype, + '']) + if __name__ == '__main__': unittest_main() diff -r 7eaef037ea9d -r 79686c864bbf web/views/basecontrollers.py --- a/web/views/basecontrollers.py Wed Apr 06 23:23:48 2011 +0200 +++ b/web/views/basecontrollers.py Wed Apr 06 23:24:19 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 7eaef037ea9d -r 79686c864bbf web/views/owl.py --- a/web/views/owl.py Wed Apr 06 23:23:48 2011 +0200 +++ b/web/views/owl.py Wed Apr 06 23:24:19 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. @@ -35,15 +35,19 @@ } OWL_TYPE_MAP = {'String': 'xsd:string', - 'Datetime': 'xsd:dateTime', 'Bytes': 'xsd:byte', - 'Float': 'xsd:float', + 'Password': 'xsd:byte', + 'Boolean': 'xsd:boolean', 'Int': 'xsd:int', + 'Float': 'xsd:float', + 'Decimal' : 'xsd:decimal', + 'Date':'xsd:date', + 'Datetime': 'xsd:dateTime', + 'TZDatetime': 'xsd:dateTime', 'Time': 'xsd:time', - 'Password': 'xsd:byte', - 'Decimal' : 'xsd:decimal', + 'TZTime': 'xsd:time', 'Interval': 'xsd:duration' } diff -r 7eaef037ea9d -r 79686c864bbf web/views/plots.py --- a/web/views/plots.py Wed Apr 06 23:23:48 2011 +0200 +++ b/web/views/plots.py Wed Apr 06 23:24:19 2011 +0200 @@ -47,7 +47,7 @@ @objectify_selector def columns_are_date_then_numbers(cls, req, rset=None, *args, **kwargs): etypes = rset.description[0] - if etypes[0] not in ('Date', 'Datetime'): + if etypes[0] not in ('Date', 'Datetime', 'TZDatetime'): return 0 for etype in etypes[1:]: if etype not in ('Int', 'Float'): diff -r 7eaef037ea9d -r 79686c864bbf web/views/reledit.py --- a/web/views/reledit.py Wed Apr 06 23:23:48 2011 +0200 +++ b/web/views/reledit.py Wed Apr 06 23:24:19 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'