backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 27 Jul 2011 20:17:45 +0200
changeset 7721 d313666c171e
parent 7702 73cadb5d0097 (current diff)
parent 7720 4df02855f4b3 (diff)
child 7749 cbb0a0ce3795
backport stable
--- a/.hgtags	Tue Jul 26 19:34:43 2011 +0200
+++ b/.hgtags	Wed Jul 27 20:17:45 2011 +0200
@@ -216,3 +216,5 @@
 cc0578049cbe8b1d40009728e36c17e45da1fc6b cubicweb-debian-version-3.13.1-1
 f9227b9d61835f03163b8133a96da35db37a0c8d cubicweb-version-3.13.2
 9ad5411199e00b2611366439b82f35d7d3285423 cubicweb-debian-version-3.13.2-1
+0e82e7e5a34f57d7239c7a42e48ba4d5e53abab2 cubicweb-version-3.13.3
+fb48c55cb80234bc0164c9bcc0e2cfc428836e5f cubicweb-debian-version-3.13.3-1
--- a/__pkginfo__.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/__pkginfo__.py	Wed Jul 27 20:17:45 2011 +0200
@@ -22,7 +22,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 13, 2)
+numversion = (3, 13, 3)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
--- a/debian/changelog	Tue Jul 26 19:34:43 2011 +0200
+++ b/debian/changelog	Wed Jul 27 20:17:45 2011 +0200
@@ -1,3 +1,9 @@
+cubicweb (3.13.3-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 27 Jul 2011 19:06:16 +0200
+
 cubicweb (3.13.2-1) unstable; urgency=low
 
   * new upstream release
--- a/hooks/__init__.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/hooks/__init__.py	Wed Jul 27 20:17:45 2011 +0200
@@ -57,7 +57,7 @@
                     or not repo.config.source_enabled(source)
                     or not source.config['synchronize']):
                     continue
-                session = repo.internal_session()
+                session = repo.internal_session(safe=True)
                 try:
                     stats = source.pull_data(session)
                     if stats.get('created'):
--- a/hooks/metadata.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/hooks/metadata.py	Wed Jul 27 20:17:45 2011 +0200
@@ -42,8 +42,10 @@
     def __call__(self):
         timestamp = datetime.now()
         edited = self.entity.cw_edited
-        edited.setdefault('creation_date', timestamp)
-        edited.setdefault('modification_date', timestamp)
+        if not edited.get('creation_date'):
+            edited['creation_date'] = timestamp
+        if not edited.get('modification_date'):
+            edited['modification_date'] = timestamp
         if not self._cw.get_shared_data('do-not-insert-cwuri'):
             cwuri = u'%s%s' % (self._cw.base_url(), self.entity.eid)
             edited.setdefault('cwuri', cwuri)
--- a/i18n/de.po	Tue Jul 26 19:34:43 2011 +0200
+++ b/i18n/de.po	Wed Jul 27 20:17:45 2011 +0200
@@ -1914,10 +1914,6 @@
 msgid "cw_schema_object"
 msgstr ""
 
-msgctxt "CWAttribute"
-msgid "cw_schema_object"
-msgstr ""
-
 msgctxt "CWEType"
 msgid "cw_schema_object"
 msgstr ""
--- a/i18n/en.po	Tue Jul 26 19:34:43 2011 +0200
+++ b/i18n/en.po	Wed Jul 27 20:17:45 2011 +0200
@@ -1869,10 +1869,6 @@
 msgid "cw_schema_object"
 msgstr "mapped by"
 
-msgctxt "CWAttribute"
-msgid "cw_schema_object"
-msgstr "mapped by"
-
 msgctxt "CWEType"
 msgid "cw_schema_object"
 msgstr "mapped by"
@@ -4407,3 +4403,7 @@
 #, python-format
 msgid "you should un-inline relation %s which is supported and may be crossed "
 msgstr ""
+
+#~ msgctxt "CWAttribute"
+#~ msgid "cw_schema_object"
+#~ msgstr "mapped by"
--- a/i18n/es.po	Tue Jul 26 19:34:43 2011 +0200
+++ b/i18n/es.po	Wed Jul 27 20:17:45 2011 +0200
@@ -1943,10 +1943,6 @@
 msgid "cw_schema_object"
 msgstr "mapeado por"
 
-msgctxt "CWAttribute"
-msgid "cw_schema_object"
-msgstr "mapeado por"
-
 msgctxt "CWEType"
 msgid "cw_schema_object"
 msgstr "mapeado por"
@@ -4576,3 +4572,7 @@
 msgstr ""
 "usted debe  quitar la puesta en línea de la relación %s que es aceptada y "
 "puede ser cruzada"
+
+#~ msgctxt "CWAttribute"
+#~ msgid "cw_schema_object"
+#~ msgstr "mapeado por"
--- a/i18n/fr.po	Tue Jul 26 19:34:43 2011 +0200
+++ b/i18n/fr.po	Wed Jul 27 20:17:45 2011 +0200
@@ -1946,10 +1946,6 @@
 msgid "cw_schema_object"
 msgstr "mappé par"
 
-msgctxt "CWAttribute"
-msgid "cw_schema_object"
-msgstr "mappé par"
-
 msgctxt "CWEType"
 msgid "cw_schema_object"
 msgstr "mappé par"
@@ -4576,3 +4572,7 @@
 msgstr ""
 "vous devriez enlevé la mise en ligne de la relation %s qui est supportée et "
 "peut-être croisée"
+
+#~ msgctxt "CWAttribute"
+#~ msgid "cw_schema_object"
+#~ msgstr "mappé par"
--- a/server/repository.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/repository.py	Wed Jul 27 20:17:45 2011 +0200
@@ -927,14 +927,16 @@
                 nbclosed += 1
         return nbclosed
 
-    def internal_session(self, cnxprops=None):
-        """return a dbapi like connection/cursor using internal user which
-        have every rights on the repository. You'll *have to* commit/rollback
-        or close (rollback implicitly) the session once the job's done, else
-        you'll leak connections set up to the time where no one is
-        available, causing irremediable freeze...
+    def internal_session(self, cnxprops=None, safe=False):
+        """return a dbapi like connection/cursor using internal user which have
+        every rights on the repository. The `safe` argument is a boolean flag
+        telling if integrity hooks should be activated or not.
+
+        *YOU HAVE TO* commit/rollback or close (rollback implicitly) the
+        session once the job's done, else you'll leak connections set up to the
+        time where no one is available, causing irremediable freeze...
         """
-        session = InternalSession(self, cnxprops)
+        session = InternalSession(self, cnxprops, safe)
         session.set_cnxset()
         return session
 
@@ -1030,7 +1032,7 @@
         return extid
 
     def extid2eid(self, source, extid, etype, session=None, insert=True,
-                  sourceparams=None):
+                  complete=True, commit=True, sourceparams=None):
         """Return eid from a local id. If the eid is a negative integer, that
         means the entity is known but has been copied back to the system source
         hence should be ignored.
@@ -1089,15 +1091,16 @@
                 session, extid, etype, eid, sourceparams)
             if source.should_call_hooks:
                 self.hm.call_hooks('before_add_entity', session, entity=entity)
-            # XXX call add_info with complete=False ?
-            self.add_info(session, entity, source, extid)
+            self.add_info(session, entity, source, extid, complete=complete)
             source.after_entity_insertion(session, extid, entity, sourceparams)
             if source.should_call_hooks:
                 self.hm.call_hooks('after_add_entity', session, entity=entity)
-            session.commit(free_cnxset)
+            if commit or free_cnxset:
+                session.commit(free_cnxset)
             return eid
-        except:
-            session.rollback(free_cnxset)
+        except Exception:
+            if commit or free_cnxset:
+                session.rollback(free_cnxset)
             raise
 
     def add_info(self, session, entity, source, extid=None, complete=True):
--- a/server/rqlannotation.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/rqlannotation.py	Wed Jul 27 20:17:45 2011 +0200
@@ -211,16 +211,22 @@
     relation for the rhs variable
     """
     principal = None
+    others = []
     # sort for test predictability
     for rel in sorted(relations, key=lambda x: (x.children[0].name, x.r_type)):
         # only equality relation with a variable as rhs may be principal
         if rel.operator() not in ('=', 'IS') \
                or not isinstance(rel.children[1].children[0], VariableRef) or rel.neged(strict=True):
             continue
+        if rel.optional:
+            others.append(rel)
+            continue
         if rel.scope is rel.stmt:
             return rel
         principal = rel
     if principal is None:
+        if others:
+            return others[0]
         raise BadRQLQuery('unable to find principal in %s' % ', '.join(
             r.as_string() for r in relations))
     return principal
--- a/server/session.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/session.py	Wed Jul 27 20:17:45 2011 +0200
@@ -1276,12 +1276,13 @@
     is_internal_session = True
     running_dbapi_query = False
 
-    def __init__(self, repo, cnxprops=None):
+    def __init__(self, repo, cnxprops=None, safe=False):
         super(InternalSession, self).__init__(InternalManager(), repo, cnxprops,
                                               _id='internal')
         self.user._cw = self # XXX remove when "vreg = user._cw.vreg" hack in entity.py is gone
         self.cnxtype = 'inmemory'
-        self.disable_hook_categories('integrity')
+        if not safe:
+            self.disable_hook_categories('integrity')
 
     @property
     def cnxset(self):
--- a/server/sources/__init__.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/sources/__init__.py	Wed Jul 27 20:17:45 2011 +0200
@@ -25,6 +25,7 @@
 from logging import getLogger
 
 from logilab.common import configuration
+from logilab.common.deprecation import deprecated
 
 from yams.schema import role_name
 
@@ -269,12 +270,6 @@
 
     # external source api ######################################################
 
-    def eid2extid(self, eid, session=None):
-        return self.repo.eid2extid(self, eid, session)
-
-    def extid2eid(self, value, etype, session=None, **kwargs):
-        return self.repo.extid2eid(self, value, etype, session, **kwargs)
-
     def support_entity(self, etype, write=False):
         """return true if the given entity's type is handled by this adapter
         if write is true, return true only if it's a RW support
@@ -522,6 +517,15 @@
         pass
 
 
+    @deprecated('[3.13] use repo.eid2extid(source, eid, session)')
+    def eid2extid(self, eid, session=None):
+        return self.repo.eid2extid(self, eid, session)
+
+    @deprecated('[3.13] use extid2eid(source, value, etype, session, **kwargs)')
+    def extid2eid(self, value, etype, session=None, **kwargs):
+        return self.repo.extid2eid(self, value, etype, session, **kwargs)
+
+
 class TrFunc(object):
     """lower, upper"""
     def __init__(self, trname, index, attrname=None):
--- a/server/sources/datafeed.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/sources/datafeed.py	Wed Jul 27 20:17:45 2011 +0200
@@ -147,6 +147,7 @@
         return True
 
     def release_synchronization_lock(self, session):
+        session.set_cnxset()
         session.execute('SET X synchronizing FALSE WHERE X eid %(x)s',
                         {'x': self.eid})
         session.commit()
@@ -220,9 +221,6 @@
         entity.cw_edited['cwuri'] = unicode(lid)
         entity.cw_edited.set_defaults()
         sourceparams['parser'].before_entity_copy(entity, sourceparams)
-        # avoid query to search full-text indexed attributes
-        for attr in entity.e_schema.indexable_attributes():
-            entity.cw_edited.setdefault(attr, u'')
         return entity
 
     def after_entity_insertion(self, session, lid, entity, sourceparams):
@@ -267,15 +265,20 @@
         """return an entity for the given uri. May return None if it should be
         skipped
         """
+        session = self._cw
         # if cwsource is specified and repository has a source with the same
         # name, call extid2eid on that source so entity will be properly seen as
         # coming from this source
-        source = self._cw.repo.sources_by_uri.get(
-            sourceparams.pop('cwsource', None), self.source)
+        source_uri = sourceparams.pop('cwsource', None)
+        if source_uri is not None and source_uri != 'system':
+            source = session.repo.sources_by_uri.get(source_uri, self.source)
+        else:
+            source = self.source
         sourceparams['parser'] = self
         try:
-            eid = source.extid2eid(str(uri), etype, self._cw,
-                                   sourceparams=sourceparams)
+            eid = session.repo.extid2eid(source, str(uri), etype, session,
+                                         complete=False, commit=False,
+                                         sourceparams=sourceparams)
         except ValidationError, ex:
             self.source.error('error while creating %s: %s', etype, ex)
             return None
@@ -285,14 +288,14 @@
             # Don't give etype to entity_from_eid so we get UnknownEid if the
             # entity has been removed
             try:
-                entity = self._cw.entity_from_eid(-eid)
+                entity = session.entity_from_eid(-eid)
             except UnknownEid:
                 return None
             self.notify_updated(entity) # avoid later update from the source's data
             return entity
         if self.sourceuris is not None:
             self.sourceuris.pop(str(uri), None)
-        return self._cw.entity_from_eid(eid, etype)
+        return session.entity_from_eid(eid, etype)
 
     def process(self, url, partialcommit=True):
         """main callback: process the url"""
--- a/server/sources/ldapuser.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/sources/ldapuser.py	Wed Jul 27 20:17:45 2011 +0200
@@ -310,7 +310,7 @@
         except Exception:
             self.error('while trying to authenticate %s', user, exc_info=True)
             raise AuthenticationError()
-        eid = self.extid2eid(user['dn'], 'CWUser', session)
+        eid = self.repo.extid2eid(self, user['dn'], 'CWUser', session)
         if eid < 0:
             # user has been moved away from this source
             raise AuthenticationError()
@@ -423,7 +423,7 @@
             filteredres = []
             for resdict in res:
                 # get sure the entity exists in the system table
-                eid = self.extid2eid(resdict['dn'], 'CWUser', session)
+                eid = self.repo.extid2eid(self, resdict['dn'], 'CWUser', session)
                 for eidfilter in eidfilters:
                     if not eidfilter(eid):
                         break
@@ -537,7 +537,7 @@
             res = cnx.result(all=0)[1]
         except ldap.NO_SUCH_OBJECT:
             self.info('ldap NO SUCH OBJECT')
-            eid = self.extid2eid(base, 'CWUser', session, insert=False)
+            eid = self.repo.extid2eid(self, base, 'CWUser', session, insert=False)
             if eid:
                 self.warning('deleting ldap user with eid %s and dn %s',
                              eid, base)
@@ -646,6 +646,7 @@
     """generate an LDAP filter for a rql query"""
     def __init__(self, source, session, args=None, mainvars=()):
         self.source = source
+        self.repo = source.repo
         self._ldap_attrs = source.user_rev_attrs
         self._base_filters = source.base_filters
         self._session = session
@@ -751,7 +752,7 @@
                           }[rhs.operator]
                 self._eidfilters.append(filter)
                 return
-            dn = self.source.eid2extid(eid, self._session)
+            dn = self.repo.eid2extid(self.source, eid, self._session)
             raise GotDN(dn)
         try:
             filter = '(%s%s)' % (self._ldap_attrs[relation.r_type],
--- a/server/sources/pyrorql.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/sources/pyrorql.py	Wed Jul 27 20:17:45 2011 +0200
@@ -281,8 +281,8 @@
                     continue
             for etype, extid in deleted:
                 try:
-                    eid = self.extid2eid(str(extid), etype, session,
-                                         insert=False)
+                    eid = self.repo.extid2eid(self, str(extid), etype, session,
+                                              insert=False)
                     # entity has been deleted from external repository but is not known here
                     if eid is not None:
                         entity = session.entity_from_eid(eid, etype)
@@ -423,7 +423,7 @@
 
     def _entity_relations_and_kwargs(self, session, entity):
         relations = []
-        kwargs = {'x': self.eid2extid(entity.eid, session)}
+        kwargs = {'x': self.repo.eid2extid(self, entity.eid, session)}
         for key, val in entity.cw_attr_cache.iteritems():
             relations.append('X %s %%(%s)s' % (key, key))
             kwargs[key] = val
@@ -449,15 +449,15 @@
             return
         cu = session.cnxset[self.uri]
         cu.execute('DELETE %s X WHERE X eid %%(x)s' % entity.__regid__,
-                   {'x': self.eid2extid(entity.eid, session)})
+                   {'x': self.repo.eid2extid(self, entity.eid, session)})
         self._query_cache.clear()
 
     def add_relation(self, session, subject, rtype, object):
         """add a relation to the source"""
         cu = session.cnxset[self.uri]
         cu.execute('SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
-                   {'x': self.eid2extid(subject, session),
-                    'y': self.eid2extid(object, session)})
+                   {'x': self.repo.eid2extid(self, subject, session),
+                    'y': self.repo.eid2extid(self, object, session)})
         self._query_cache.clear()
         session.entity_from_eid(subject).cw_clear_all_caches()
         session.entity_from_eid(object).cw_clear_all_caches()
@@ -470,8 +470,8 @@
             return
         cu = session.cnxset[self.uri]
         cu.execute('DELETE X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
-                   {'x': self.eid2extid(subject, session),
-                    'y': self.eid2extid(object, session)})
+                   {'x': self.repo.eid2extid(self, subject, session),
+                    'y': self.repo.eid2extid(self, object, session)})
         self._query_cache.clear()
         session.entity_from_eid(subject).cw_clear_all_caches()
         session.entity_from_eid(object).cw_clear_all_caches()
@@ -481,6 +481,7 @@
     """translate a local rql query to be executed on a distant repository"""
     def __init__(self, source):
         self.source = source
+        self.repo = source.repo
         self.current_operator = None
 
     def _accept_children(self, node):
@@ -676,7 +677,7 @@
 
     def eid2extid(self, eid):
         try:
-            return self.source.eid2extid(eid, self._session)
+            return self.repo.eid2extid(self.source, eid, self._session)
         except UnknownEid:
             operator = self.current_operator
             if operator is not None and operator != '=':
--- a/server/sources/rql2sql.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/sources/rql2sql.py	Wed Jul 27 20:17:45 2011 +0200
@@ -1292,9 +1292,16 @@
                                                relation.r_type)
                 try:
                     self._state.ignore_varmap = True
-                    return '%s%s' % (lhssql, relation.children[1].accept(self))
+                    sql = lhssql + relation.children[1].accept(self)
                 finally:
                     self._state.ignore_varmap = False
+                if relation.optional == 'right':
+                    leftalias = self._var_table(principal.children[0].variable)
+                    rightalias = self._var_table(relation.children[0].variable)
+                    self._state.replace_tables_by_outer_join(
+                        leftalias, rightalias, 'LEFT', sql)
+                    return ''
+                return sql
         return ''
 
     def _visit_attribute_relation(self, rel):
@@ -1372,12 +1379,15 @@
 
     def visit_comparison(self, cmp):
         """generate SQL for a comparison"""
+        optional = getattr(cmp, 'optional', None) # rql < 0.30
         if len(cmp.children) == 2:
-            # XXX occurs ?
+            # simplified expression from HAVING clause
             lhs, rhs = cmp.children
         else:
             lhs = None
             rhs = cmp.children[0]
+            assert not optional
+        sql = None
         operator = cmp.operator
         if operator in ('LIKE', 'ILIKE'):
             if operator == 'ILIKE' and not self.dbhelper.ilike_support:
@@ -1385,18 +1395,39 @@
             else:
                 operator = ' %s ' % operator
         elif operator == 'REGEXP':
-            return ' %s' % self.dbhelper.sql_regexp_match_expression(rhs.accept(self))
+            sql = ' %s' % self.dbhelper.sql_regexp_match_expression(rhs.accept(self))
         elif (operator == '=' and isinstance(rhs, Constant)
               and rhs.eval(self._args) is None):
             if lhs is None:
-                return ' IS NULL'
-            return '%s IS NULL' % lhs.accept(self)
+                sql = ' IS NULL'
+            else:
+                sql = '%s IS NULL' % lhs.accept(self)
         elif isinstance(rhs, Function) and rhs.name == 'IN':
             assert operator == '='
             operator = ' '
-        if lhs is None:
-            return '%s%s'% (operator, rhs.accept(self))
-        return '%s%s%s'% (lhs.accept(self), operator, rhs.accept(self))
+        if sql is None:
+            if lhs is None:
+                sql = '%s%s'% (operator, rhs.accept(self))
+            else:
+                sql = '%s%s%s'% (lhs.accept(self), operator, rhs.accept(self))
+        if optional is None:
+            return sql
+        leftvars = cmp.children[0].get_nodes(VariableRef)
+        assert len(leftvars) == 1
+        leftalias = self._var_table(leftvars[0].variable.stinfo['attrvar'])
+        rightvars = cmp.children[1].get_nodes(VariableRef)
+        assert len(rightvars) == 1
+        rightalias = self._var_table(rightvars[0].variable.stinfo['attrvar'])
+        if optional == 'right':
+            self._state.replace_tables_by_outer_join(
+                leftalias, rightalias, 'LEFT', sql)
+        elif optional == 'left':
+            self._state.replace_tables_by_outer_join(
+                rightalias, leftalias, 'LEFT', sql)
+        else:
+            self._state.replace_tables_by_outer_join(
+                leftalias, rightalias, 'FULL', sql)
+        return ''
 
     def visit_mathexpression(self, mexpr):
         """generate SQL for a mathematic expression"""
@@ -1438,15 +1469,15 @@
         value = constant.value
         if constant.type == 'etype':
             return value
-        if constant.type == 'Int' and  isinstance(constant.parent, SortTerm):
-            return value
+        if constant.type == 'Int': # XXX Float?
+            return str(value)
         if constant.type in ('Date', 'Datetime'):
             rel = constant.relation()
             if rel is not None:
                 rel._q_needcast = value
             return self.keyword_map[value]()
         if constant.type == 'Boolean':
-            value = self.dbhelper.boolean_value(value)
+            return str(self.dbhelper.boolean_value(value))
         if constant.type == 'Substitute':
             try:
                 # we may found constant from simplified var in varmap
--- a/server/test/data/migratedapp/schema.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/test/data/migratedapp/schema.py	Wed Jul 27 20:17:45 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.
@@ -17,7 +17,7 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """cw.server.migraction test"""
 from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
-                            SubjectRelation,
+                            SubjectRelation, Bytes,
                             RichString, String, Int, Boolean, Datetime, Date)
 from yams.constraints import SizeConstraint, UniqueConstraint
 from cubicweb.schema import (WorkflowableEntityType, RQLConstraint,
@@ -36,6 +36,7 @@
     sujet = String(fulltextindexed=True,
                  constraints=[SizeConstraint(256)])
     concerne = SubjectRelation('Societe')
+    opt_attr = Bytes()
 
 class concerne(RelationType):
     __permissions__ = {
--- a/server/test/data/schema.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/test/data/schema.py	Wed Jul 27 20:17:45 2011 +0200
@@ -24,9 +24,6 @@
                              RQLConstraint, RQLUniqueConstraint,
                              ERQLExpression, RRQLExpression)
 
-class BFSSTestable(EntityType):
-    opt_attr = Bytes()
-
 class Affaire(WorkflowableEntityType):
     __permissions__ = {
         'read':   ('managers',
@@ -45,6 +42,7 @@
 
     duration = Int()
     invoiced = Float()
+    opt_attr = Bytes()
 
     depends_on = SubjectRelation('Affaire')
     require_permission = SubjectRelation('CWPermission')
--- a/server/test/unittest_datafeed.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/test/unittest_datafeed.py	Wed Jul 27 20:17:45 2011 +0200
@@ -96,9 +96,8 @@
 
         # test_delete_source
         req = self.request()
-        with self.debugged('DBG_RQL'):
-            req.execute('DELETE CWSource S WHERE S name "myfeed"')
-            self.commit()
+        req.execute('DELETE CWSource S WHERE S name "myfeed"')
+        self.commit()
         self.failIf(self.execute('Card X WHERE X title "cubicweb.org"'))
         self.failIf(self.execute('Any X WHERE X has_text "cubicweb.org"'))
 
--- a/server/test/unittest_ldapuser.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/test/unittest_ldapuser.py	Wed Jul 27 20:17:45 2011 +0200
@@ -61,7 +61,7 @@
         # no such user
         raise AuthenticationError()
     # don't check upassword !
-    return self.extid2eid(user['dn'], 'CWUser', session)
+    return self.repo.extid2eid(self, user['dn'], 'CWUser', session)
 
 def setUpModule(*args):
     create_slapd_configuration(LDAPUserSourceTC.config)
--- a/server/test/unittest_querier.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/test/unittest_querier.py	Wed Jul 27 20:17:45 2011 +0200
@@ -800,6 +800,12 @@
                                                            'Password', 'String',
                                                            'TZDatetime', 'TZTime',
                                                            'Time'])
+        req = self.session
+        req.create_entity('Personne', nom=u'louis', test=True)
+        self.assertEqual(len(req.execute('Any X WHERE X test %(val)s', {'val': True})), 1)
+        self.assertEqual(len(req.execute('Any X WHERE X test TRUE')), 1)
+        self.assertEqual(len(req.execute('Any X WHERE X test %(val)s', {'val': False})), 0)
+        self.assertEqual(len(req.execute('Any X WHERE X test FALSE')), 0)
 
     def test_select_constant(self):
         rset = self.execute('Any X, "toto" ORDERBY X WHERE X is CWGroup')
--- a/server/test/unittest_repository.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/test/unittest_repository.py	Wed Jul 27 20:17:45 2011 +0200
@@ -68,7 +68,7 @@
                 namecol, table, finalcol))
             self.assertEqual(cu.fetchall(), [])
             cu = self.session.system_sql('SELECT %s FROM %s WHERE %s=%%(final)s ORDER BY %s'
-                                         % (namecol, table, finalcol, namecol), {'final': 'TRUE'})
+                                         % (namecol, table, finalcol, namecol), {'final': True})
             self.assertEqual(cu.fetchall(), [(u'BigInt',), (u'Boolean',), (u'Bytes',),
                                              (u'Date',), (u'Datetime',),
                                              (u'Decimal',),(u'Float',),
--- a/server/test/unittest_rql2sql.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/test/unittest_rql2sql.py	Wed Jul 27 20:17:45 2011 +0200
@@ -807,6 +807,11 @@
 
 
 OUTER_JOIN = [
+
+    ('Any U,G WHERE U login L, G name L?, G is CWGroup',
+     '''SELECT _U.cw_eid, _G.cw_eid
+FROM cw_CWUser AS _U LEFT OUTER JOIN cw_CWGroup AS _G ON (_G.cw_name=_U.cw_login)'''),
+
     ('Any X,S WHERE X travaille S?',
      '''SELECT _X.cw_eid, rel_travaille0.eid_to
 FROM cw_Personne AS _X LEFT OUTER JOIN travaille_relation AS rel_travaille0 ON (rel_travaille0.eid_from=_X.cw_eid)'''
@@ -969,6 +974,18 @@
      '''SELECT _CFG.cw_ecrit_par, _CALIBCFG.cw_eid, _CFG.cw_eid
 FROM cw_Note AS _CFG LEFT OUTER JOIN cw_Note AS _CALIBCFG ON (_CALIBCFG.cw_ecrit_par=_CFG.cw_ecrit_par)
 WHERE _CFG.cw_ecrit_par=1'''),
+
+    ('Any U,G WHERE U login UL, G name GL, G is CWGroup HAVING UPPER(UL)=UPPER(GL)?',
+     '''SELECT _U.cw_eid, _G.cw_eid
+FROM cw_CWUser AS _U LEFT OUTER JOIN cw_CWGroup AS _G ON (UPPER(_U.cw_login)=UPPER(_G.cw_name))'''),
+
+    ('Any U,G WHERE U login UL, G name GL, G is CWGroup HAVING UPPER(UL)?=UPPER(GL)',
+     '''SELECT _U.cw_eid, _G.cw_eid
+FROM cw_CWGroup AS _G LEFT OUTER JOIN cw_CWUser AS _U ON (UPPER(_U.cw_login)=UPPER(_G.cw_name))'''),
+
+    ('Any U,G WHERE U login UL, G name GL, G is CWGroup HAVING UPPER(UL)?=UPPER(GL)?',
+     '''SELECT _U.cw_eid, _G.cw_eid
+FROM cw_CWUser AS _U FULL OUTER JOIN cw_CWGroup AS _G ON (UPPER(_U.cw_login)=UPPER(_G.cw_name))'''),
     ]
 
 VIRTUAL_VARS = [
@@ -1871,7 +1888,7 @@
     backend = 'sqlite'
 
     def _norm_sql(self, sql):
-        return sql.strip().replace(' ILIKE ', ' LIKE ')
+        return sql.strip().replace(' ILIKE ', ' LIKE ').replace('TRUE', '1').replace('FALSE', '0')
 
     def test_date_extraction(self):
         self._check("Any MONTH(D) WHERE P is Personne, P creation_date D",
--- a/server/test/unittest_storage.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/server/test/unittest_storage.py	Wed Jul 27 20:17:45 2011 +0200
@@ -259,7 +259,7 @@
 
     @tag('update', 'NULL')
     def test_bfss_update_to_None(self):
-        f = self.session.create_entity('BFSSTestable', opt_attr=Binary('toto'))
+        f = self.session.create_entity('Affaire', opt_attr=Binary('toto'))
         self.session.commit()
         self.session.set_pool()
         f.set_attributes(opt_attr=None)
--- a/sobjects/parsers.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/sobjects/parsers.py	Wed Jul 27 20:17:45 2011 +0200
@@ -75,7 +75,10 @@
             if rschema == 'eid':
                 continue
             attrtype = eschema.destination(rschema)
-            typeddict[rschema.type] = converters[attrtype](stringdict[rschema])
+            value = stringdict[rschema]
+            if value is not None:
+                value = converters[attrtype](value)
+            typeddict[rschema.type] = value
     return typeddict
 
 def rtype_role_rql(rtype, role):
@@ -244,7 +247,7 @@
             except ValueError:
                 return url + '?' + self._cw.build_url_params(**params)
             try:
-                etype = self._cw.vreg.case_insensitive_etypes[etype]
+                etype = self._cw.vreg.case_insensitive_etypes[etype.lower()]
             except KeyError:
                 return url + '?' + self._cw.build_url_params(**params)
         if add_relations:
@@ -294,9 +297,12 @@
                 # relation
                 related = rels.setdefault(role, {}).setdefault(child.tag, [])
                 related += [ritem for ritem, _ in self.parser.parse_etree(child)]
-            else:
+            elif child.text:
                 # attribute
                 item[child.tag] = unicode(child.text)
+            else:
+                # None attribute (empty tag)
+                item[child.tag] = None
         return item, rels
 
 
--- a/sobjects/test/unittest_parsers.py	Tue Jul 26 19:34:43 2011 +0200
+++ b/sobjects/test/unittest_parsers.py	Wed Jul 27 20:17:45 2011 +0200
@@ -40,7 +40,7 @@
 
 BASEXML = ''.join(u'''
 <rset size="1">
- <CWUser eid="5" cwuri="http://pouet.org/5">
+ <CWUser eid="5" cwuri="http://pouet.org/5" cwsource="system">
   <login>sthenault</login>
   <upassword>toto</upassword>
   <last_login_time>2011-01-25 14:14:06</last_login_time>
@@ -113,20 +113,28 @@
   <last_login_time>2011-01-25 14:14:06</last_login_time>
   <creation_date>2010-01-22 10:27:59</creation_date>
   <modification_date>2011-01-25 14:14:06</modification_date>
+  <in_group role="subject">
+    <CWGroup cwuri="http://pouet.org/7" eid="7"/>
+  </in_group>
  </CWUser>
 </rset>
 '''.splitlines()
 )
+
+
 class CWEntityXMLParserTC(CubicWebTC):
+    """/!\ this test use a pre-setup database /!\, if you modify above xml,
+    REMOVE THE DATABASE TEMPLATE else it won't be considered
+    """
     test_db_id = 'xmlparser'
     @classmethod
     def pre_setup_database(cls, session, config):
-        source = session.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
+        myfeed = session.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
                                    parser=u'cw.entityxml', url=BASEXML)
-        session.create_entity('CWSource', name=u'myotherfeed', type=u'datafeed',
-                              parser=u'cw.entityxml', url=OTHERXML)
+        myotherfeed = session.create_entity('CWSource', name=u'myotherfeed', type=u'datafeed',
+                                            parser=u'cw.entityxml', url=OTHERXML)
         session.commit()
-        source.init_mapping([(('CWUser', 'use_email', '*'),
+        myfeed.init_mapping([(('CWUser', 'use_email', '*'),
                               u'role=subject\naction=copy'),
                              (('CWUser', 'in_group', '*'),
                               u'role=subject\naction=link\nlinkattr=name'),
@@ -135,11 +143,18 @@
                              (('*', 'tags', 'CWUser'),
                               u'role=object\naction=link-or-create\nlinkattr=name'),
                             ])
+        myotherfeed.init_mapping([(('CWUser', 'in_group', '*'),
+                                   u'role=subject\naction=link\nlinkattr=name'),
+                                  (('CWUser', 'in_state', '*'),
+                                   u'role=subject\naction=link\nlinkattr=name'),
+                                  ])
         session.create_entity('Tag', name=u'hop')
 
     def test_complete_url(self):
         dfsource = self.repo.sources_by_uri['myfeed']
         parser = dfsource._get_parser(self.session)
+        self.assertEqual(parser.complete_url('http://www.cubicweb.org/CWUser'),
+                         'http://www.cubicweb.org/CWUser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject&vid=xml')
         self.assertEqual(parser.complete_url('http://www.cubicweb.org/cwuser'),
                          'http://www.cubicweb.org/cwuser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject&vid=xml')
         self.assertEqual(parser.complete_url('http://www.cubicweb.org/cwuser?vid=rdf&relation=hop'),
@@ -164,7 +179,7 @@
                                  (u'EmailAddress', {})]
                              }
                           })
-        session = self.repo.internal_session()
+        session = self.repo.internal_session(safe=True)
         stats = dfsource.pull_data(session, force=True, raise_on_error=True)
         self.assertEqual(sorted(stats.keys()), ['created', 'updated'])
         self.assertEqual(len(stats['created']), 2)
@@ -248,7 +263,7 @@
 
     def test_external_entity(self):
         dfsource = self.repo.sources_by_uri['myotherfeed']
-        session = self.repo.internal_session()
+        session = self.repo.internal_session(safe=True)
         stats = dfsource.pull_data(session, force=True, raise_on_error=True)
         user = self.execute('CWUser X WHERE X login "sthenault"').get_entity(0, 0)
         self.assertEqual(user.creation_date, datetime(2010, 01, 22, 10, 27, 59))
@@ -256,6 +271,33 @@
         self.assertEqual(user.cwuri, 'http://pouet.org/5')
         self.assertEqual(user.cw_source[0].name, 'myfeed')
 
+    def test_noerror_missing_fti_attribute(self):
+        dfsource = self.repo.sources_by_uri['myfeed']
+        session = self.repo.internal_session(safe=True)
+        parser = dfsource._get_parser(session)
+        dfsource.process_urls(parser, ['''
+<rset size="1">
+ <Card eid="50" cwuri="http://pouet.org/50" cwsource="system">
+  <title>how-to</title>
+ </Card>
+</rset>
+'''], raise_on_error=True)
+
+    def test_noerror_unspecified_date(self):
+        dfsource = self.repo.sources_by_uri['myfeed']
+        session = self.repo.internal_session(safe=True)
+        parser = dfsource._get_parser(session)
+        dfsource.process_urls(parser, ['''
+<rset size="1">
+ <Card eid="50" cwuri="http://pouet.org/50" cwsource="system">
+  <title>how-to</title>
+  <content>how-to</content>
+  <synopsis>how-to</synopsis>
+  <creation_date/>
+ </Card>
+</rset>
+'''], raise_on_error=True)
+
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
     unittest_main()
--- a/web/data/cubicweb.css	Tue Jul 26 19:34:43 2011 +0200
+++ b/web/data/cubicweb.css	Wed Jul 27 20:17:45 2011 +0200
@@ -309,7 +309,7 @@
 }
 
 div#contentmain{
-  margin-top: %(pageContentPadding)s
+  margin-top: %(pageContentPadding)s;
 }
 
 /*FIXME */
@@ -923,7 +923,7 @@
 
 ul.startup li,
 ul.section li {
-  margin-left: 0px
+  margin-left: 0px;
 }
 
 ul.simple li,