backport stable branch
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 27 May 2009 13:01:32 +0200
changeset 1962 45af1e015683
parent 1956 9865daa96cd7 (current diff)
parent 1961 8b5009e27e2b (diff)
child 1966 87ce7d336393
backport stable branch
web/form.py
--- a/.hgtags	Wed May 27 08:39:16 2009 +0200
+++ b/.hgtags	Wed May 27 13:01:32 2009 +0200
@@ -28,3 +28,11 @@
 0e132fbae9cc5e004f4b79a8b842addad43519a7 cubicweb-debian-version-3_1_4-1
 c14231e3a4f9120e2bb6a1d8690252fff5e48131 cubicweb-version-3_2_0
 c9c492787a8aa1b7916e22eb6498cba1c8fa316c cubicweb-debian-version-3_2_0-1
+634c251dd032894850080c4e5aeb0a4e09f888c0 cubicweb-version-3_2_1
+e784f8847a124a93e5b385d7a92a2772c050fe82 cubicweb-debian-version-3_2_1-1
+6539ce84f04357ef65ccee0896a30997b16a4ece cubicweb-version-3_2_2
+92d1a15f08f7c5fa87643ffb4273d12cb3f41c63 cubicweb-debian-version-3_2_2-1
+6539ce84f04357ef65ccee0896a30997b16a4ece cubicweb-version-3_2_2
+9b21e068fef73c37bcb4e53d006a7bde485f390b cubicweb-version-3_2_2
+92d1a15f08f7c5fa87643ffb4273d12cb3f41c63 cubicweb-debian-version-3_2_2-1
+0e07514264aa1b0b671226f41725ea4c066c210a cubicweb-debian-version-3_2_2-1
--- a/MANIFEST.in	Wed May 27 08:39:16 2009 +0200
+++ b/MANIFEST.in	Wed May 27 13:01:32 2009 +0200
@@ -3,7 +3,7 @@
 include bin/cubicweb-*
 include man/cubicweb-ctl.1
 
-recursive-include doc *.txt *.zargo *.png *.html makefile
+recursive-include doc *.txt *.zargo *.png *.html makefile *.rst
 
 recursive-include misc *
 
--- a/__init__.py	Wed May 27 08:39:16 2009 +0200
+++ b/__init__.py	Wed May 27 13:01:32 2009 +0200
@@ -46,7 +46,7 @@
 
 class Binary(StringIO):
     """customize StringIO to make sure we don't use unicode"""
-    def __init__(self, buf= ''):
+    def __init__(self, buf=''):
         assert isinstance(buf, (str, buffer)), \
                "Binary objects must use raw strings, not %s" % buf.__class__
         StringIO.__init__(self, buf)
--- a/__pkginfo__.py	Wed May 27 08:39:16 2009 +0200
+++ b/__pkginfo__.py	Wed May 27 13:01:32 2009 +0200
@@ -6,7 +6,7 @@
 distname = "cubicweb"
 modname = "cubicweb"
 
-numversion = (3, 2, 0)
+numversion = (3, 2, 2)
 version = '.'.join(str(num) for num in numversion)
 
 license = 'LGPL v2'
--- a/cwconfig.py	Wed May 27 08:39:16 2009 +0200
+++ b/cwconfig.py	Wed May 27 13:01:32 2009 +0200
@@ -11,6 +11,7 @@
 
 """
 __docformat__ = "restructuredtext en"
+_ = unicode
 
 import sys
 import os
@@ -27,7 +28,6 @@
 
 CONFIGURATIONS = []
 
-_ = unicode
 
 class metaconfiguration(type):
     """metaclass to automaticaly register configuration"""
--- a/cwvreg.py	Wed May 27 08:39:16 2009 +0200
+++ b/cwvreg.py	Wed May 27 13:01:32 2009 +0200
@@ -11,7 +11,7 @@
 
 from rql import RQLHelper
 
-from cubicweb import Binary, UnknownProperty, UnknownEid
+from cubicweb import ETYPE_NAME_MAP, Binary, UnknownProperty, UnknownEid
 from cubicweb.vregistry import VRegistry, ObjectNotFound, NoSelectableObject
 from cubicweb.rtags import RTAGS
 
@@ -170,7 +170,10 @@
         # browse ancestors from most specific to most generic and
         # try to find an associated custom entity class
         for baseschema in baseschemas:
-            btype = str(baseschema)
+            try:
+                btype = ETYPE_NAME_MAP[baseschema]
+            except KeyError:
+                btype = str(baseschema)
             try:
                 cls = self.select(self.registry_objects('etypes', btype), etype)
                 break
@@ -372,6 +375,8 @@
         default to a dump of the class registered for 'Any'
         """
         usercls = super(MulCnxCubicWebRegistry, self).etype_class(etype)
+        if etype == 'Any':
+            return usercls
         usercls.e_schema = self.schema.eschema(etype)
         return usercls
 
@@ -385,7 +390,14 @@
             vobject.vreg = self
             vobject.schema = self.schema
             vobject.config = self.config
-        return super(MulCnxCubicWebRegistry, self).select(vobjects, *args, **kwargs)
+        selected = super(MulCnxCubicWebRegistry, self).select(vobjects, *args,
+                                                              **kwargs)
+        # redo the same thing on the instance so it won't use equivalent class
+        # attributes (which may change)
+        selected.vreg = self
+        selected.schema = self.schema
+        selected.config = self.config
+        return selected
 
 from datetime import datetime, date, time, timedelta
 
--- a/dbapi.py	Wed May 27 08:39:16 2009 +0200
+++ b/dbapi.py	Wed May 27 13:01:32 2009 +0200
@@ -13,7 +13,8 @@
 from logging import getLogger
 from time import time, clock
 
-from cubicweb import ConnectionError, RequestSessionMixIn, set_log_methods
+from logilab.common.logging_ext import set_log_methods
+from cubicweb import ETYPE_NAME_MAP, ConnectionError, RequestSessionMixIn
 from cubicweb.cwvreg import CubicWebRegistry, MulCnxCubicWebRegistry
 from cubicweb.cwconfig import CubicWebNoAppConfiguration
 
@@ -101,7 +102,12 @@
             vreg = MulCnxCubicWebRegistry(config, initlog=initlog)
         else:
             vreg = CubicWebRegistry(config, initlog=initlog)
-        vreg.set_schema(repo.get_schema())
+        schema = repo.get_schema()
+        for oldetype, newetype in ETYPE_NAME_MAP.items():
+            if oldetype in schema:
+                print 'aliasing', newetype, 'to', oldetype
+                schema._entities[newetype] = schema._entities[oldetype]
+        vreg.set_schema(schema)
     else:
         vreg = None
     cnx = repo_connect(repo, user, password, cnxprops)
@@ -325,11 +331,6 @@
         self.vreg = None
         # session's data
         self.data = {}
-        # XXX < 3.2 bw compat
-        if 'EUser' in self._repo.get_schema():
-            self._user_etype = 'EUser'
-        else:
-            self._user_etype = 'CWUser'
 
     def __repr__(self):
         if self.anonymous_connection:
@@ -435,9 +436,9 @@
         eid, login, groups, properties = self._repo.user_info(self.sessionid, props)
         if req is None:
             req = self.request()
-        rset = req.eid_rset(eid, self._user_etype)
-        user = self.vreg.etype_class(self._user_etype)(req, rset, row=0, groups=groups,
-                                                       properties=properties)
+        rset = req.eid_rset(eid, 'CWUser')
+        user = self.vreg.etype_class('CWUser')(req, rset, row=0, groups=groups,
+                                               properties=properties)
         user['login'] = login # cache login
         return user
 
--- a/debian/changelog	Wed May 27 08:39:16 2009 +0200
+++ b/debian/changelog	Wed May 27 13:01:32 2009 +0200
@@ -1,3 +1,15 @@
+cubicweb (3.2.2-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Nicolas Chauvat <nicolas.chauvat@logilab.fr>  Wed, 27 May 2009 12:31:49 +0200
+
+cubicweb (3.2.1-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Aurélien Campéas <aurelien.campeas@logilab.fr>  Mon, 25 May 2009 16:45:00 +0200
+
 cubicweb (3.2.0-1) unstable; urgency=low
 
   * new upstream release
--- a/devtools/htmlparser.py	Wed May 27 08:39:16 2009 +0200
+++ b/devtools/htmlparser.py	Wed May 27 13:01:32 2009 +0200
@@ -88,13 +88,13 @@
         self.input_tags = self.find_tag('input')
         self.title_tags = [self.h1_tags, self.h2_tags, self.h3_tags, self.h4_tags]
 
-    def find_tag(self, tag):
+    def find_tag(self, tag, gettext=True):
         """return a list which contains text of all "tag" elements """
         if self.default_ns is None:
             iterstr = ".//%s" % tag
         else:
             iterstr = ".//{%s}%s" % (self.default_ns, tag)
-        if tag in ('a', 'input'):
+        if not gettext or tag in ('a', 'input'):
             return [(elt.text, elt.attrib) for elt in self.etree.iterfind(iterstr)]
         return [u''.join(elt.xpath('.//text()')) for elt in self.etree.iterfind(iterstr)]
 
--- a/misc/migration/bootstrapmigration_repository.py	Wed May 27 08:39:16 2009 +0200
+++ b/misc/migration/bootstrapmigration_repository.py	Wed May 27 13:01:32 2009 +0200
@@ -7,6 +7,15 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
 
+if applcubicwebversion < (3, 2, 2) and cubicwebversion >= (3, 2, 1):
+   from base64 import b64encode
+   for table in ('entities', 'deleted_entities'):
+      for eid, extid in sql('SELECT eid, extid FROM %s WHERE extid is NOT NULL'
+                            % table, ask_confirm=False):
+         sql('UPDATE %s SET extid=%%(extid)s WHERE eid=%%(eid)s' % table,
+             {'extid': b64encode(extid), 'eid': eid}, ask_confirm=False)
+   checkpoint()
+
 if applcubicwebversion < (3, 2, 0) and cubicwebversion >= (3, 2, 0):
    add_cube('card', update_database=False)
 
@@ -24,7 +33,7 @@
     add_relation_type('is_instance_of')
     # fill the relation using an efficient sql query instead of using rql
     sql('INSERT INTO is_instance_of_relation '
-	'  SELECT * from is_relation')
+        '  SELECT * from is_relation')
     checkpoint()
     session.set_shared_data('do-not-insert-is_instance_of', False)
 
--- a/rset.py	Wed May 27 08:39:16 2009 +0200
+++ b/rset.py	Wed May 27 13:01:32 2009 +0200
@@ -380,9 +380,6 @@
             pass
         # build entity instance
         etype = self.description[row][col]
-        if etype == 'EUser':
-            import traceback
-            traceback.printstack()
         entity = self.vreg.etype_class(etype)(req, self, row, col)
         entity.set_eid(eid)
         # cache entity
--- a/schemaviewer.py	Wed May 27 08:39:16 2009 +0200
+++ b/schemaviewer.py	Wed May 27 13:01:32 2009 +0200
@@ -93,10 +93,10 @@
         return data
 
     def eschema_link_url(self, eschema):
-        return self.req.build_url('eetype/%s?vid=eschema' % eschema)
+        return self.req.build_url('cwetype/%s?vid=eschema' % eschema)
 
     def rschema_link_url(self, rschema):
-        return self.req.build_url('ertype/%s?vid=eschema' % rschema)
+        return self.req.build_url('cwrtype/%s?vid=eschema' % rschema)
 
     def possible_views(self, etype):
         rset = self.req.etype_rset(etype)
--- a/selectors.py	Wed May 27 08:39:16 2009 +0200
+++ b/selectors.py	Wed May 27 13:01:32 2009 +0200
@@ -114,7 +114,7 @@
 
 
 def score_interface(cls_or_inst, cls, iface):
-    """Return true if the give object (maybe an instance or class) implements
+    """Return XXX if the give object (maybe an instance or class) implements
     the interface.
     """
     if getattr(iface, '__registry__', None) == 'etypes':
--- a/server/querier.py	Wed May 27 08:39:16 2009 +0200
+++ b/server/querier.py	Wed May 27 13:01:32 2009 +0200
@@ -599,7 +599,8 @@
                 except KeyError:
                     raise QueryError('bad cache key %s (no value)' % key)
                 except TypeError:
-                    raise QueryError('bad cache key %s (value: %r)' % (key, args[key]))
+                    raise QueryError('bad cache key %s (value: %r)' % (
+                        key, args[key]))
                 except UnknownEid:
                     # we want queries such as "Any X WHERE X eid 9999"
                     # return an empty result instead of raising UnknownEid
--- a/server/repository.py	Wed May 27 08:39:16 2009 +0200
+++ b/server/repository.py	Wed May 27 13:01:32 2009 +0200
@@ -636,9 +636,10 @@
             self.exception('unexpected error')
             raise
 
-    def close(self, sessionid):
+    def close(self, sessionid, checkshuttingdown=True):
         """close the session with the given id"""
-        session = self._get_session(sessionid, setpool=True)
+        session = self._get_session(sessionid, setpool=True,
+                                    checkshuttingdown=checkshuttingdown)
         # operation uncommited before close are rollbacked before hook is called
         session.rollback()
         self.hm.call_hooks('session_close', session=session)
@@ -691,7 +692,7 @@
         """close every opened sessions"""
         for sessionid in self._sessions.keys():
             try:
-                self.close(sessionid)
+                self.close(sessionid, checkshuttingdown=False)
             except:
                 self.exception('error while closing session %s' % sessionid)
 
@@ -720,9 +721,9 @@
         session.set_pool()
         return session
 
-    def _get_session(self, sessionid, setpool=False):
+    def _get_session(self, sessionid, setpool=False, checkshuttingdown=True):
         """return the user associated to the given session identifier"""
-        if self._shutting_down:
+        if checkshuttingdown and self._shutting_down:
             raise Exception('Repository is shutting down')
         try:
             session = self._sessions[sessionid]
@@ -793,10 +794,10 @@
             raise UnknownEid(eid)
         return extid
 
-    def extid2eid(self, source, lid, etype, session=None, insert=True,
+    def extid2eid(self, source, extid, etype, session=None, insert=True,
                   recreate=False):
         """get eid from a local id. An eid is attributed if no record is found"""
-        cachekey = (str(lid), source.uri)
+        cachekey = (extid, source.uri)
         try:
             return self._extid_cache[cachekey]
         except KeyError:
@@ -805,17 +806,17 @@
         if session is None:
             session = self.internal_session()
             reset_pool = True
-        eid = self.system_source.extid2eid(session, source, lid)
+        eid = self.system_source.extid2eid(session, source, extid)
         if eid is not None:
             self._extid_cache[cachekey] = eid
-            self._type_source_cache[eid] = (etype, source.uri, lid)
+            self._type_source_cache[eid] = (etype, source.uri, extid)
             if recreate:
-                entity = source.before_entity_insertion(session, lid, etype, eid)
+                entity = source.before_entity_insertion(session, extid, etype, eid)
                 entity._cw_recreating = True
                 if source.should_call_hooks:
                     self.hm.call_hooks('before_add_entity', etype, session, entity)
                 # XXX add fti op ?
-                source.after_entity_insertion(session, lid, entity)
+                source.after_entity_insertion(session, extid, entity)
                 if source.should_call_hooks:
                     self.hm.call_hooks('after_add_entity', etype, session, entity)
             if reset_pool:
@@ -823,7 +824,7 @@
             return eid
         if not insert:
             return
-        # no link between lid and eid, create one using an internal session
+        # no link between extid and eid, create one using an internal session
         # since the current session user may not have required permissions to
         # do necessary stuff and we don't want to commit user session.
         #
@@ -835,13 +836,13 @@
         try:
             eid = self.system_source.create_eid(session)
             self._extid_cache[cachekey] = eid
-            self._type_source_cache[eid] = (etype, source.uri, lid)
-            entity = source.before_entity_insertion(session, lid, etype, eid)
+            self._type_source_cache[eid] = (etype, source.uri, extid)
+            entity = source.before_entity_insertion(session, extid, etype, eid)
             if source.should_call_hooks:
                 self.hm.call_hooks('before_add_entity', etype, session, entity)
             # XXX call add_info with complete=False ?
-            self.add_info(session, entity, source, lid)
-            source.after_entity_insertion(session, lid, entity)
+            self.add_info(session, entity, source, extid)
+            source.after_entity_insertion(session, extid, entity)
             if source.should_call_hooks:
                 self.hm.call_hooks('after_add_entity', etype, session, entity)
             else:
--- a/server/schemaserial.py	Wed May 27 08:39:16 2009 +0200
+++ b/server/schemaserial.py	Wed May 27 13:01:32 2009 +0200
@@ -51,7 +51,7 @@
 def _set_sql_prefix(prefix):
     """3.2.0 migration function: allow to unset/reset SQL_PREFIX"""
     for module in ('checkintegrity', 'migractions', 'schemahooks',
-                   'sources.rql2sql', 'sources.native'):
+                   'sources.rql2sql', 'sources.native', 'sqlutils'):
         try:
             sys.modules['cubicweb.server.%s' % module].SQL_PREFIX = prefix
             print 'changed SQL_PREFIX for %s' % module
--- a/server/sources/ldapuser.py	Wed May 27 08:39:16 2009 +0200
+++ b/server/sources/ldapuser.py	Wed May 27 13:01:32 2009 +0200
@@ -20,6 +20,8 @@
 FOR A PARTICULAR PURPOSE.
 """
 
+from base64 import b64decode
+
 from logilab.common.textutils import get_csv
 from rql.nodes import Relation, VariableRef, Constant, Function
 
@@ -151,7 +153,8 @@
     def init(self):
         """method called by the repository once ready to handle request"""
         self.repo.looping_task(self._interval, self.synchronize)
-        self.repo.looping_task(self._query_cache.ttl.seconds/10, self._query_cache.clear_expired)
+        self.repo.looping_task(self._query_cache.ttl.seconds/10,
+                               self._query_cache.clear_expired)
 
     def synchronize(self):
         """synchronize content known by this repository with content in the
@@ -166,7 +169,8 @@
         try:
             cursor = session.system_sql("SELECT eid, extid FROM entities WHERE "
                                         "source='%s'" % self.uri)
-            for eid, extid in cursor.fetchall():
+            for eid, b64extid in cursor.fetchall():
+                extid = b64decode(b64extid)
                 # if no result found, _search automatically delete entity information
                 res = self._search(session, extid, BASE)
                 if res:
--- a/server/sources/native.py	Wed May 27 08:39:16 2009 +0200
+++ b/server/sources/native.py	Wed May 27 13:01:32 2009 +0200
@@ -1,5 +1,11 @@
 """Adapters for native cubicweb sources.
 
+Notes:
+* extid (aka external id, the primary key of an entity in the external source
+  from which it comes from) are stored in a varchar column encoded as a base64
+  string. This is because it should actually be Bytes but we want an index on
+  it for fast querying.
+  
 :organization: Logilab
 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
@@ -8,6 +14,7 @@
 
 from threading import Lock
 from datetime import datetime
+from base64 import b64decode, b64encode
 
 from logilab.common.cache import Cache
 from logilab.common.configuration import REQUIRED
@@ -234,7 +241,7 @@
                 pwd = rset[0][0]
             except IndexError:
                 raise AuthenticationError('bad login')
-            # passwords are stored using the bytea type, so we get a StringIO
+            # passwords are stored using the Bytes type, so we get a StringIO
             if pwd is not None:
                 args['pwd'] = crypt_password(password, pwd.getvalue()[:2])
         # get eid from login and (crypted) password
@@ -456,22 +463,25 @@
             raise UnknownEid(eid)
         if res is None:
             raise UnknownEid(eid)
+        if res[-1] is not None:
+            if not isinstance(res, list):
+                res = list(res)
+            res[-1] = b64decode(res[-1])
         return res
 
-    def extid2eid(self, session, source, lid):
-        """get eid from a local id. An eid is attributed if no record is found"""
+    def extid2eid(self, session, source, extid):
+        """get eid from an external id. Return None if no record found."""
+        assert isinstance(extid, str)
         cursor = session.system_sql('SELECT eid FROM entities WHERE '
                                     'extid=%(x)s AND source=%(s)s',
-                                    # str() necessary with pg 8.3
-                                    {'x': str(lid), 's': source.uri})
+                                    {'x': b64encode(extid), 's': source.uri})
         # XXX testing rowcount cause strange bug with sqlite, results are there
         #     but rowcount is 0
         #if cursor.rowcount > 0:
         try:
             result = cursor.fetchone()
             if result:
-                eid = result[0]
-                return eid
+                return result[0]
         except:
             pass
         return None
@@ -499,8 +509,11 @@
     def add_info(self, session, entity, source, extid=None):
         """add type and source info for an eid into the system table"""
         # begin by inserting eid/type/source/extid into the entities table
-        attrs = {'type': str(entity.e_schema), 'eid': entity.eid,
-                 'extid': extid, 'source': source.uri, 'mtime': datetime.now()}
+        if extid is not None:
+            assert isinstance(extid, str)
+            extid = b64encode(extid)
+        attrs = {'type': entity.id, 'eid': entity.eid, 'extid': extid,
+                 'source': source.uri, 'mtime': datetime.now()}
         session.system_sql(self.sqlgen.insert('entities', attrs), attrs)
 
     def delete_info(self, session, eid, etype, uri, extid):
@@ -510,6 +523,9 @@
         attrs = {'eid': eid}
         session.system_sql(self.sqlgen.delete('entities', attrs), attrs)
         if self.has_deleted_entitites_table:
+            if extid is not None:
+                assert isinstance(extid, str), type(extid)
+                extid = b64encode(extid)
             attrs = {'type': etype, 'eid': eid, 'extid': extid,
                      'source': uri, 'dtime': datetime.now()}
             session.system_sql(self.sqlgen.insert('deleted_entities', attrs), attrs)
--- a/server/sources/pyrorql.py	Wed May 27 08:39:16 2009 +0200
+++ b/server/sources/pyrorql.py	Wed May 27 13:01:32 2009 +0200
@@ -8,9 +8,9 @@
 
 import threading
 from os.path import join
-
 from time import mktime
 from datetime import datetime
+from base64 import b64decode
 
 from Pyro.errors import PyroError, ConnectionClosedError
 
@@ -177,7 +177,7 @@
                 try:
                     exturi = cnx.describe(extid)[1]
                     if exturi == 'system' or not exturi in repo.sources_by_uri:
-                        eid = self.extid2eid(extid, etype, session)
+                        eid = self.extid2eid(str(extid), etype, session)
                         rset = session.eid_rset(eid, etype)
                         entity = rset.get_entity(0, 0)
                         entity.complete(entity.e_schema.indexable_attributes())
@@ -188,7 +188,8 @@
                     continue
             for etype, extid in deleted:
                 try:
-                    eid = self.extid2eid(extid, etype, session, insert=False)
+                    eid = self.extid2eid(str(extid), etype, session,
+                                         insert=False)
                     # entity has been deleted from external repository but is not known here
                     if eid is not None:
                         repo.delete_info(session, eid)
@@ -307,7 +308,8 @@
                             etype = descr[rowindex][colindex]
                             exttype, exturi, extid = cnx.describe(row[colindex])
                             if exturi == 'system' or not exturi in self.repo.sources_by_uri:
-                                eid = self.extid2eid(row[colindex], etype, session)
+                                eid = self.extid2eid(str(row[colindex]), etype,
+                                                     session)
                                 row[colindex] = eid
                             else:
                                 # skip this row
@@ -494,7 +496,7 @@
             # XXX what about optional relation or outer NOT EXISTS()
             raise
         except ReplaceByInOperator, ex:
-            rhs = 'IN (%s)' % ','.join(str(eid) for eid in ex.eids)
+            rhs = 'IN (%s)' % ','.join(eid for eid in ex.eids)
         self.need_translation = False
         self.current_operator = None
         if node.optional in ('right', 'both'):
@@ -567,17 +569,15 @@
         except UnknownEid:
             operator = self.current_operator
             if operator is not None and operator != '=':
-                # deal with query like X eid > 12
+                # deal with query like "X eid > 12"
                 #
-                # The problem is
-                # that eid order in the external source may differ from the
-                # local source
+                # The problem is that eid order in the external source may
+                # differ from the local source
                 #
-                # So search for all eids from this
-                # source matching the condition locally and then to replace the
-                # > 12 branch by IN (eids) (XXX we may have to insert a huge
-                # number of eids...)
-                # planner so that
+                # So search for all eids from this source matching the condition
+                # locally and then to replace the "> 12" branch by "IN (eids)"
+                #
+                # XXX we may have to insert a huge number of eids...)
                 sql = "SELECT extid FROM entities WHERE source='%s' AND type IN (%s) AND eid%s%s"
                 etypes = ','.join("'%s'" % etype for etype in self.current_etypes)
                 cu = self._session.system_sql(sql % (self.source.uri, etypes,
@@ -586,6 +586,6 @@
                 # results
                 rows = cu.fetchall()
                 if rows:
-                    raise ReplaceByInOperator((r[0] for r in rows))
+                    raise ReplaceByInOperator((b64decode(r[0]) for r in rows))
             raise
 
--- a/server/sqlutils.py	Wed May 27 08:39:16 2009 +0200
+++ b/server/sqlutils.py	Wed May 27 13:01:32 2009 +0200
@@ -111,13 +111,14 @@
         w(indexer.sql_drop_fti())
         w('')
     w(dropschema2sql(schema, prefix=SQL_PREFIX,
-                     skip_entities=skip_entities, skip_relations=skip_relations))
+                     skip_entities=skip_entities,
+                     skip_relations=skip_relations))
     return '\n'.join(output)
 
 try:
     from mx.DateTime import DateTimeType, DateTimeDeltaType
 except ImportError:
-    DateTimeType, DateTimeDeltaType = None
+    DateTimeType = DateTimeDeltaType = None
 
 class SQLAdapterMixIn(object):
     """Mixin for SQL data sources, getting a connection from a configuration
--- a/server/test/unittest_multisources.py	Wed May 27 08:39:16 2009 +0200
+++ b/server/test/unittest_multisources.py	Wed May 27 13:01:32 2009 +0200
@@ -77,7 +77,7 @@
                            'type': u'Card', 'extid': None})
         externent = rset.get_entity(3, 0)
         metainf = externent.metainformation()
-        self.assertEquals(metainf['source'], {'adapter': 'pyrorql', 'base-url': 'http://extern.org', 'uri': 'extern'})
+        self.assertEquals(metainf['source'], {'adapter': 'pyrorql', 'base-url': 'http://extern.org/', 'uri': 'extern'})
         self.assertEquals(metainf['type'], 'Card')
         self.assert_(metainf['extid'])
         etype = self.execute('Any ETN WHERE X is ET, ET name ETN, X eid %(x)s',
@@ -149,7 +149,7 @@
         self.execute('Any X ORDERBY DUMB_SORT(RF) WHERE X title RF')
 
     def test_in_eid(self):
-        iec1 = self.repo.extid2eid(self.repo.sources_by_uri['extern'], ec1,
+        iec1 = self.repo.extid2eid(self.repo.sources_by_uri['extern'], str(ec1),
                                    'Card', self.session)
         rset = self.execute('Any X WHERE X eid IN (%s, %s)' % (iec1, self.ic1))
         self.assertEquals(sorted(r[0] for r in rset.rows), sorted([iec1, self.ic1]))
--- a/server/test/unittest_querier.py	Wed May 27 08:39:16 2009 +0200
+++ b/server/test/unittest_querier.py	Wed May 27 13:01:32 2009 +0200
@@ -6,7 +6,7 @@
 from logilab.common.testlib import TestCase, unittest_main
 from rql import BadRQLQuery, RQLSyntaxError
 
-from cubicweb import QueryError, Unauthorized
+from cubicweb import QueryError, Unauthorized, Binary
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.server.utils import crypt_password
 from cubicweb.server.sources.native import make_schema
@@ -208,6 +208,13 @@
         # should return an empty result set
         self.failIf(self.execute('Any X WHERE X eid 99999999'))
 
+    def test_bytes_storage(self):
+        feid = self.execute('INSERT File X: X name "foo.pdf", X data_format "text/plain", X data %(data)s',
+                            {'data': Binary("xxx")})[0][0]
+        fdata = self.execute('Any D WHERE X data D, X eid %(x)s', {'x': feid}, 'x')[0][0]
+        self.assertIsInstance(fdata, Binary)
+        self.assertEquals(fdata.getvalue(), 'xxx')
+
     # selection queries tests #################################################
 
     def test_select_1(self):
@@ -470,7 +477,7 @@
         self.assertEquals(rset.rows[0][0], self.ueid)
 
     def test_select_complex_sort(self):
-        self.skip('retry me once http://www.sqlite.org/cvstrac/tktview?tn=3773 is fixed')
+        """need sqlite including http://www.sqlite.org/cvstrac/tktview?tn=3773 fix"""
         rset = self.execute('Any X ORDERBY X,D LIMIT 5 WHERE X creation_date D')
         result = rset.rows
         result.sort()
@@ -1073,7 +1080,7 @@
         cursor = self.pool['system']
         cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
                        % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
-        passwd = cursor.fetchone()[0].getvalue()
+        passwd = str(cursor.fetchone()[0])
         self.assertEquals(passwd, crypt_password('toto', passwd[:2]))
         rset = self.execute("Any X WHERE X is CWUser, X login 'bob', X upassword '%s'" % passwd)
         self.assertEquals(len(rset.rows), 1)
@@ -1087,7 +1094,7 @@
                             {'pwd': 'tutu'})
         cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
                        % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
-        passwd = cursor.fetchone()[0].getvalue()
+        passwd = str(cursor.fetchone()[0])
         self.assertEquals(passwd, crypt_password('tutu', passwd[:2]))
         rset = self.execute("Any X WHERE X is CWUser, X login 'bob', X upassword '%s'" % passwd)
         self.assertEquals(len(rset.rows), 1)
@@ -1212,8 +1219,9 @@
 
         cause: old variable ref inserted into a fresh rqlst copy
         (in RQLSpliter._complex_select_plan)
+
+        need sqlite including http://www.sqlite.org/cvstrac/tktview?tn=3773 fix
         """
-        self.skip('retry me once http://www.sqlite.org/cvstrac/tktview?tn=3773 is fixed')
         self.execute('Any X ORDERBY D DESC WHERE X creation_date D')
 
     def test_nonregr_extra_joins(self):
--- a/server/test/unittest_repository.py	Wed May 27 08:39:16 2009 +0200
+++ b/server/test/unittest_repository.py	Wed May 27 13:01:32 2009 +0200
@@ -211,11 +211,8 @@
         t = threading.Thread(target=close_in_a_few_moment)
         t.start()
         try:
-            print 'execute'
             repo.execute(cnxid, 'DELETE CWUser X WHERE X login "toto"')
-            print 'commit'
             repo.commit(cnxid)
-            print 'commited'
         finally:
             t.join()
 
@@ -286,7 +283,8 @@
         repo = self.repo
         cnxid = repo.connect(*self.default_user_password())
         session = repo._get_session(cnxid, setpool=True)
-        self.assertEquals(repo.type_and_source_from_eid(1, session), ('CWGroup', 'system', None))
+        self.assertEquals(repo.type_and_source_from_eid(1, session),
+                          ('CWGroup', 'system', None))
         self.assertEquals(repo.type_from_eid(1, session), 'CWGroup')
         self.assertEquals(repo.source_from_eid(1, session).uri, 'system')
         self.assertEquals(repo.eid2extid(repo.system_source, 1, session), None)
--- a/skeleton/__pkginfo__.py.tmpl	Wed May 27 08:39:16 2009 +0200
+++ b/skeleton/__pkginfo__.py.tmpl	Wed May 27 13:01:32 2009 +0200
@@ -32,7 +32,7 @@
     return [join(dirpath, fname) for fname in _listdir(dirpath)
             if fname[0] != '.' and not fname.endswith('.pyc')
             and not fname.endswith('~')
-            and not isdir(join(dirpath, fname))]¶
+            and not isdir(join(dirpath, fname))]
 
 from glob import glob
 try:
--- a/vregistry.py	Wed May 27 08:39:16 2009 +0200
+++ b/vregistry.py	Wed May 27 13:01:32 2009 +0200
@@ -237,7 +237,7 @@
                 # XXX automatic reloading management
                 try:
                     registry[obj.id].remove(registered)
-                except ValueError:
+                except KeyError:
                     self.warning('can\'t remove %s, no id %s in the %s registry',
                                  removed_id, obj.id, registryname)
                 except ValueError:
@@ -254,11 +254,14 @@
             replaced = replaced.classid()
         registryname = registryname or obj.__registry__
         registry = self.registry(registryname)
-        registered_objs = registry[obj.id]
+        registered_objs = registry.get(obj.id, ())
         for index, registered in enumerate(registered_objs):
             if registered.classid() == replaced:
                 del registry[obj.id][index]
                 break
+        else:
+            self.warning('trying to replace an unregistered view %s by %s',
+                         replaced, obj)
         self.register(obj, registryname=registryname)
 
     # dynamic selection methods ###############################################
Binary file web/data/favicon.ico has changed
--- a/web/form.py	Wed May 27 08:39:16 2009 +0200
+++ b/web/form.py	Wed May 27 13:01:32 2009 +0200
@@ -270,13 +270,21 @@
 
     @iclassmethod
     def field_by_name(cls_or_self, name, role='subject'):
-        """return field with the given name and role"""
+        """return field with the given name and role.
+        Raise FieldNotFound if the field can't be found.
+        """
         for field in cls_or_self._fieldsattr():
             if field.name == name and field.role == role:
                 return field
         raise FieldNotFound(name)
 
     @iclassmethod
+    def fields_by_name(cls_or_self, name, role='subject'):
+        """return a list of fields with the given name and role"""
+        return [field for field in cls_or_self._fieldsattr()
+                if field.name == name and field.role == role]
+
+    @iclassmethod
     def remove_field(cls_or_self, field):
         """remove a field from form class or instance"""
         cls_or_self._fieldsattr().remove(field)
@@ -341,7 +349,9 @@
         # ensure rendervalues is a dict
         if rendervalues is None:
             rendervalues = {}
-        for field in self.fields:
+        # use a copy in case fields are modified while context is build (eg
+        # __linkto handling for instance)
+        for field in self.fields[:]:
             for field in field.actual_fields(self):
                 field.form_init(self)
                 value = self.form_field_display_value(field, rendervalues)
@@ -443,12 +453,13 @@
             self.edited_entity = self.complete_entity(self.row or 0, self.col or 0)
         self.form_add_hidden('__type', eidparam=True)
         self.form_add_hidden('eid')
-        if msg is not None:
+        if msg:
             # If we need to directly attach the new object to another one
+            self.form_add_hidden('__message', msg)
+        if not self.is_subform:
             for linkto in self.req.list_form_param('__linkto'):
                 self.form_add_hidden('__linkto', linkto)
                 msg = '%s %s' % (msg, self.req._('and linked'))
-            self.form_add_hidden('__message', msg)
         # in case of direct instanciation
         self.schema = self.edited_entity.schema
         self.vreg = self.edited_entity.vreg
@@ -478,7 +489,16 @@
     def _req_display_value(self, field):
         value = super(EntityFieldsForm, self)._req_display_value(field)
         if value is None:
-            value = self.edited_entity.linked_to(field.name, field.role) or None
+            value = self.edited_entity.linked_to(field.name, field.role)
+            if value:
+                searchedvalues = ['%s:%s:%s' % (field.name, eid, field.role)
+                                  for eid in value]
+                # remove associated __linkto hidden fields
+                for field in self.fields_by_name('__linkto'):
+                    if field.initial in searchedvalues:
+                        self.remove_field(field)
+            else:
+                value = None
         return value
 
     def _form_field_default_value(self, field, load_bytes):
--- a/web/formrenderers.py	Wed May 27 08:39:16 2009 +0200
+++ b/web/formrenderers.py	Wed May 27 13:01:32 2009 +0200
@@ -83,7 +83,7 @@
         return tags.label(label, **attrs)
 
     def render_help(self, form, field):
-	help = []
+        help = []
         descr = field.help
         if descr:
             help.append('<div class="helper">%s</div>' % form.req._(descr))
--- a/web/test/unittest_form.py	Wed May 27 08:39:16 2009 +0200
+++ b/web/test/unittest_form.py	Wed May 27 13:01:32 2009 +0200
@@ -73,6 +73,21 @@
         form.form_build_context({})
         self.assertEquals(form.form_field_display_value(field, {}), 'toto')
 
+
+    def test_linkto_field_duplication(self):
+        e = self.etype_instance('CWUser')
+        e.eid = 'A'
+        e.req = self.req
+        geid = self.execute('CWGroup X WHERE X name "users"')[0][0]
+        self.req.form['__linkto'] = 'in_group:%s:subject' % geid
+        form = self.vreg.select_object('forms', 'edition', self.req, None, entity=e)
+        form.content_type = 'text/html'
+        pageinfo = self._check_html(form.form_render(), form, template=None)
+        inputs = pageinfo.find_tag('select', False)
+        self.failUnless(any(attrs for t, attrs in inputs if attrs.get('name') == 'in_group:A'))
+        inputs = pageinfo.find_tag('input', False)
+        self.failIf(any(attrs for t, attrs in inputs if attrs.get('name') == '__linkto'))
+
     # form view tests #########################################################
 
     def test_massmailing_formview(self):
--- a/web/test/unittest_viewselector.py	Wed May 27 08:39:16 2009 +0200
+++ b/web/test/unittest_viewselector.py	Wed May 27 13:01:32 2009 +0200
@@ -1,8 +1,6 @@
 # -*- coding: iso-8859-1 -*-
 """XXX rename, split, reorganize this
 """
-from __future__ import with_statement
-
 from logilab.common.testlib import unittest_main
 
 from cubicweb.devtools.apptest import EnvBasedTC
--- a/web/uicfg.py	Wed May 27 08:39:16 2009 +0200
+++ b/web/uicfg.py	Wed May 27 13:01:32 2009 +0200
@@ -66,13 +66,10 @@
 """
 __docformat__ = "restructuredtext en"
 
+from cubicweb import neg_role
 from cubicweb.rtags import RelationTags, RelationTagsBool, RelationTagsSet
 from cubicweb.web import formwidgets
 
-# primary view configuration ##################################################
-
-def dual_role(role):
-    return role == 'subject' and 'object' or 'subject'
 
 def card_from_role(card, role):
     if role == 'subject':
@@ -80,10 +77,12 @@
     assert role in ('object', 'sobject'), repr(role)
     return card[1]
 
+# primary view configuration ##################################################
+
 def init_primaryview_section(rtag, sschema, rschema, oschema, role):
     if rtag.get(sschema, rschema, oschema, role) is None:
         card = card_from_role(rschema.rproperty(sschema, oschema, 'cardinality'), role)
-        composed = rschema.rproperty(sschema, oschema, 'composite') == dual_role(role)
+        composed = rschema.rproperty(sschema, oschema, 'composite') == neg_role(role)
         if rschema.is_final():
             if rschema.meta or oschema.type in ('Password', 'Bytes'):
                 section = 'hidden'
--- a/web/views/basecomponents.py	Wed May 27 08:39:16 2009 +0200
+++ b/web/views/basecomponents.py	Wed May 27 13:01:32 2009 +0200
@@ -8,6 +8,7 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
 __docformat__ = "restructuredtext en"
+_ = unicode
 
 from rql import parse
 
@@ -18,8 +19,6 @@
 from cubicweb.web.htmlwidgets import (MenuWidget, PopupBoxMenu, BoxSeparator,
                                       BoxLink)
 
-_ = unicode
-
 VISIBLE_PROP_DEF = {
     _('visible'):  dict(type='Boolean', default=False,
                         help=_('display the component or not')),
--- a/web/views/editcontroller.py	Wed May 27 08:39:16 2009 +0200
+++ b/web/views/editcontroller.py	Wed May 27 13:01:32 2009 +0200
@@ -5,6 +5,7 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
 __docformat__ = "restructuredtext en"
+
 from decimal import Decimal
 
 from rql.utils import rqlvar_maker
@@ -228,6 +229,9 @@
                         formparams['name'] = value[0]
                         self.relations.append('X name %(name)s')
                     value = val
+            else:
+                # no specified value, skip
+                return
         elif value is not None:
             if attrtype in ('Date', 'Datetime', 'Time'):
                 try:
--- a/web/views/management.py	Wed May 27 08:39:16 2009 +0200
+++ b/web/views/management.py	Wed May 27 13:01:32 2009 +0200
@@ -23,6 +23,7 @@
 
 class SecurityViewMixIn(object):
     """display security information for a given schema """
+
     def schema_definition(self, eschema, link=True,  access_types=None):
         w = self.w
         _ = self.req._
@@ -40,7 +41,7 @@
             for trad, group in sorted(groups):
                 if link:
                     l.append(u'<a href="%s" class="%s">%s</a><br/>' % (
-                    self.build_url('egroup/%s' % group), group, trad))
+                    self.build_url('cwgroup/%s' % group), group, trad))
                 else:
                     l.append(u'<div class="%s">%s</div>' % (group, trad))
             w(u'<td>%s</td>' % u''.join(l))
@@ -68,6 +69,7 @@
     __select__ = EntityView.__select__ & authenticated_user()
 
     title = _('security')
+
     def call(self):
         self.w(u'<div id="progress">%s</div>' % self.req._('validating...'))
         super(SecurityManagementView, self).call()