backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 26 Mar 2010 16:15:41 +0100
changeset 5048 bf8a53a11b6d
parent 5047 ed048e317eae (current diff)
parent 5046 d010f749c21d (diff)
child 5049 1b5c873029a1
backport stable
__pkginfo__.py
cwconfig.py
cwctl.py
devtools/__init__.py
devtools/testlib.py
--- a/__pkginfo__.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/__pkginfo__.py	Fri Mar 26 16:15:41 2010 +0100
@@ -7,7 +7,7 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 7, 1)
+numversion = (3, 7, 2)
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
--- a/_exceptions.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/_exceptions.py	Fri Mar 26 16:15:41 2010 +0100
@@ -45,21 +45,19 @@
 
 class ConnectionError(RepositoryError):
     """raised when a bad connection id is given or when an attempt to establish
-    a connection failed"""
+    a connection failed
+    """
 
 class AuthenticationError(ConnectionError):
-    """raised when a bad connection id is given or when an attempt to establish
-    a connection failed
+    """raised when when an attempt to establish a connection failed do to wrong
+    connection information (login / password or other authentication token)
     """
     def __init__(self, *args, **kwargs):
         super(AuthenticationError, self).__init__(*args)
         self.__dict__.update(kwargs)
 
 class BadConnectionId(ConnectionError):
-    """raised when a bad connection id is given or when an attempt to establish
-    a connection failed"""
-
-BadSessionId = BadConnectionId # XXX bw compat for pyro connections
+    """raised when a bad connection id is given"""
 
 class UnknownEid(RepositoryError):
     """the eid is not defined in the system tables"""
--- a/appobject.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/appobject.py	Fri Mar 26 16:15:41 2010 +0100
@@ -258,7 +258,7 @@
         except AttributeError:
             pdefs = getattr(cls, 'cw_property_defs', {})
         else:
-            warn('property_defs is deprecated, use cw_property_defs in %s'
+            warn('[3.6] property_defs is deprecated, use cw_property_defs in %s'
                  % cls, DeprecationWarning)
         for propid, pdef in pdefs.items():
             pdef = pdef.copy() # may be shared
--- a/cwconfig.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/cwconfig.py	Fri Mar 26 16:15:41 2010 +0100
@@ -687,8 +687,6 @@
 
     # for some commands (creation...) we don't want to initialize gettext
     set_language = True
-    # set this to true to avoid false error message while creating an instance
-    creating = False
     # set this to true to allow somethings which would'nt be possible
     repairing = False
 
--- a/cwctl.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/cwctl.py	Fri Mar 26 16:15:41 2010 +0100
@@ -312,7 +312,6 @@
         cubes = splitstrip(pop_arg(args, 1))
         appid = pop_arg(args)
         # get the configuration and helper
-        cwcfg.creating = True
         config = cwcfg.config_for(appid, configname)
         config.set_language = False
         cubes = config.expand_cubes(cubes)
@@ -861,20 +860,11 @@
     def i18ninstance_instance(appid):
         """recompile instance's messages catalogs"""
         config = cwcfg.config_for(appid)
-        try:
-            config.bootstrap_cubes()
-        except IOError, ex:
-            import errno
-            if ex.errno != errno.ENOENT:
-                raise
-            # bootstrap_cubes files doesn't exist
-            # notify this is not a regular start
-            config.repairing = True
-            # create an in-memory repository, will call config.init_cubes()
-            config.repository()
-        except AttributeError:
+        config.quick_start = True # notify this is not a regular start
+        repo = config.repository()
+        if config._cubes is None:
             # web only config
-            config.init_cubes(config.repository().get_cubes())
+            config.init_cubes(repo.get_cubes())
         errors = config.i18ncompile()
         if errors:
             print '\n'.join(errors)
--- a/cwvreg.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/cwvreg.py	Fri Mar 26 16:15:41 2010 +0100
@@ -21,6 +21,9 @@
 from cubicweb.vregistry import VRegistry, Registry, class_regid
 from cubicweb.rtags import RTAGS
 
+def clear_rtag_objects():
+    for rtag in RTAGS:
+        rtag.clear()
 
 def use_interfaces(obj):
     """return interfaces used by the given object by searching for implements
@@ -262,10 +265,7 @@
         if self.config.mode != 'test':
             # don't clear rtags during test, this may cause breakage with
             # manually imported appobject modules
-            @onevent('before-registry-reload')
-            def clear_rtag_objects():
-                for rtag in RTAGS:
-                    rtag.clear()
+            CW_EVENT_MANAGER.bind('before-registry-reload', clear_rtag_objects)
 
     def setdefault(self, regid):
         try:
--- a/dbapi.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/dbapi.py	Fri Mar 26 16:15:41 2010 +0100
@@ -393,13 +393,13 @@
             pass
 
     def check(self):
-        """raise `BadSessionId` if the connection is no more valid"""
+        """raise `BadConnectionId` if the connection is no more valid"""
         if self._closed is not None:
             raise ProgrammingError('Closed connection')
         self._repo.check_session(self.sessionid)
 
     def set_session_props(self, **props):
-        """raise `BadSessionId` if the connection is no more valid"""
+        """raise `BadConnectionId` if the connection is no more valid"""
         if self._closed is not None:
             raise ProgrammingError('Closed connection')
         self._repo.set_session_props(self.sessionid, props)
--- a/debian/changelog	Fri Mar 26 16:15:16 2010 +0100
+++ b/debian/changelog	Fri Mar 26 16:15:41 2010 +0100
@@ -1,3 +1,9 @@
+cubicweb (3.7.2-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 26 Mar 2010 15:53:01 +0100
+
 cubicweb (3.7.1-1) unstable; urgency=low
 
   * new upstream release
--- a/devtools/__init__.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/devtools/__init__.py	Fri Mar 26 16:15:41 2010 +0100
@@ -81,7 +81,6 @@
     mode = 'test'
     set_language = False
     read_instance_schema = False
-    bootstrap_schema = False
     init_repository = True
     options = cwconfig.merge_options(ServerConfiguration.options + (
         ('anonymous-user',
--- a/devtools/testlib.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/devtools/testlib.py	Fri Mar 26 16:15:41 2010 +0100
@@ -251,7 +251,14 @@
 
     def setUp(self):
         pause_tracing()
-        self._init_repo()
+        previous_failure = self.__class__.__dict__.get('_repo_init_failed')
+        if previous_failure is not None:
+            self.skip('repository is not initialised: %r' % previous_failure)
+        try:
+            self._init_repo()
+        except Exception, ex:
+            self.__class__._repo_init_failed = ex
+            raise
         resume_tracing()
         self.setup_database()
         self.commit()
@@ -495,7 +502,8 @@
             else:
                 cleanup = lambda p: (p[0], unquote(p[1]))
                 params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
-            path = path[len(req.base_url()):]
+            if path.startswith(req.base_url()): # may be relative
+                path = path[len(req.base_url()):]
             return path, params
         else:
             self.fail('expected a Redirect exception')
--- a/entities/schemaobjs.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/entities/schemaobjs.py	Fri Mar 26 16:15:41 2010 +0100
@@ -9,6 +9,8 @@
 
 from logilab.common.decorators import cached
 
+from yams.schema import role_name
+
 from cubicweb import ValidationError
 from cubicweb.schema import ERQLExpression, RRQLExpression
 
@@ -64,13 +66,14 @@
         for rdef in self.reverse_relation_type:
             card = rdef.cardinality[0]
             if not card in '?1':
+                qname = role_name('inlined', 'subject')
                 rtype = self.name
                 stype = rdef.stype
                 otype = rdef.otype
                 msg = self._cw._("can't set inlined=%(inlined)s, "
                                  "%(stype)s %(rtype)s %(otype)s "
                                  "has cardinality=%(card)s")
-                raise ValidationError(self.eid, {'inlined': msg % locals()})
+                raise ValidationError(self.eid, {qname: msg % locals()})
 
     def db_key_name(self):
         """XXX goa specific"""
--- a/entities/test/unittest_wfobjs.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/entities/test/unittest_wfobjs.py	Fri Mar 26 16:15:41 2010 +0100
@@ -39,7 +39,7 @@
         self.commit()
         wf.add_state(u'foo')
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'name': 'workflow already have a state of that name'})
+        self.assertEquals(ex.errors, {'name-subject': 'workflow already have a state of that name'})
         # no pb if not in the same workflow
         wf2 = add_wf(self, 'Company')
         foo = wf2.add_state(u'foo', initial=True)
@@ -49,7 +49,7 @@
         self.commit()
         bar.set_attributes(name=u'foo')
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'name': 'workflow already have a state of that name'})
+        self.assertEquals(ex.errors, {'name-subject': 'workflow already have a state of that name'})
 
     def test_duplicated_transition(self):
         wf = add_wf(self, 'Company')
@@ -58,7 +58,7 @@
         wf.add_transition(u'baz', (foo,), bar, ('managers',))
         wf.add_transition(u'baz', (bar,), foo)
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'name': 'workflow already have a transition of that name'})
+        self.assertEquals(ex.errors, {'name-subject': 'workflow already have a transition of that name'})
         # no pb if not in the same workflow
         wf2 = add_wf(self, 'Company')
         foo = wf.add_state(u'foo', initial=True)
@@ -70,7 +70,7 @@
         self.commit()
         biz.set_attributes(name=u'baz')
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'name': 'workflow already have a transition of that name'})
+        self.assertEquals(ex.errors, {'name-subject': 'workflow already have a transition of that name'})
 
 
 class WorkflowTC(CubicWebTC):
@@ -132,7 +132,7 @@
             ex = self.assertRaises(ValidationError, self.session.execute,
                                'SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
                                {'x': self.user().eid, 's': s.eid}, 'x')
-            self.assertEquals(ex.errors, {'in_state': "state doesn't belong to entity's workflow. "
+            self.assertEquals(ex.errors, {'in_state-subject': "state doesn't belong to entity's workflow. "
                                       "You may want to set a custom workflow for this entity first."})
 
     def test_fire_transition(self):
@@ -175,7 +175,7 @@
         member = req.entity_from_eid(self.member.eid)
         ex = self.assertRaises(ValidationError,
                                member.fire_transition, 'deactivate')
-        self.assertEquals(ex.errors, {'by_transition': "transition may not be fired"})
+        self.assertEquals(ex.errors, {'by_transition-subject': "transition may not be fired"})
         cnx.close()
         cnx = self.login('member')
         req = self.request()
@@ -184,7 +184,7 @@
         cnx.commit()
         ex = self.assertRaises(ValidationError,
                                member.fire_transition, 'activate')
-        self.assertEquals(ex.errors, {'by_transition': "transition may not be fired"})
+        self.assertEquals(ex.errors, {'by_transition-subject': "transition may not be fired"})
 
     def test_fire_transition_owned_by(self):
         self.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", '
@@ -257,7 +257,7 @@
         # subworkflow input transition
         ex = self.assertRaises(ValidationError,
                                self.group.change_state, swfstate1, u'gadget')
-        self.assertEquals(ex.errors, {'to_state': "state doesn't belong to entity's workflow"})
+        self.assertEquals(ex.errors, {'to_state-subject': "state doesn't belong to entity's workflow"})
         self.rollback()
         # force back to state1
         self.group.change_state('state1', u'gadget')
@@ -293,7 +293,7 @@
         mwf.add_wftransition(u'swftr1', swf, state1,
                              [(swfstate2, state2), (swfstate2, state3)])
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'subworkflow_exit': u"can't have multiple exits on the same state"})
+        self.assertEquals(ex.errors, {'subworkflow_exit-subject': u"can't have multiple exits on the same state"})
 
     def test_swf_fire_in_a_row(self):
         # sub-workflow
@@ -406,7 +406,7 @@
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': self.member.eid})
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'custom_workflow': u'workflow has no initial state'})
+        self.assertEquals(ex.errors, {'custom_workflow-subject': u'workflow has no initial state'})
 
     def test_custom_wf_bad_etype(self):
         """try to set a custom workflow which doesn't apply to entity type"""
@@ -415,7 +415,7 @@
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
                      {'wf': wf.eid, 'x': self.member.eid}, 'x')
         ex = self.assertRaises(ValidationError, self.commit)
-        self.assertEquals(ex.errors, {'custom_workflow': 'workflow isn\'t a workflow for this type'})
+        self.assertEquals(ex.errors, {'custom_workflow-subject': 'workflow isn\'t a workflow for this type'})
 
     def test_del_custom_wf(self):
         """member in some state shared by the new workflow, nothing has to be
@@ -535,7 +535,7 @@
         user = cnx.user(self.session)
         ex = self.assertRaises(ValidationError,
                                user.fire_transition, 'activate')
-        self.assertEquals(self._cleanup_msg(ex.errors['by_transition']),
+        self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
                           u"transition isn't allowed from")
         cnx.close()
 
@@ -544,7 +544,7 @@
         user = cnx.user(self.session)
         ex = self.assertRaises(ValidationError,
                                user.fire_transition, 'dummy')
-        self.assertEquals(self._cleanup_msg(ex.errors['by_transition']),
+        self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
                           u"transition isn't allowed from")
         cnx.close()
 
@@ -557,7 +557,7 @@
         session.set_pool()
         ex = self.assertRaises(ValidationError,
                                user.fire_transition, 'deactivate')
-        self.assertEquals(self._cleanup_msg(ex.errors['by_transition']),
+        self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
                                             u"transition isn't allowed from")
         # get back now
         user.fire_transition('activate')
--- a/goa/goaconfig.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/goa/goaconfig.py	Fri Mar 26 16:15:41 2010 +0100
@@ -86,7 +86,7 @@
     cube_appobject_path = WebConfiguration.cube_appobject_path | ServerConfiguration.cube_appobject_path
 
     # use file system schema
-    bootstrap_schema = read_instance_schema = False
+    read_instance_schema = False
     # schema is not persistent, don't load schema hooks (unavailable)
     schema_hooks = False
     # no user workflow for now
--- a/hooks/integrity.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/hooks/integrity.py	Fri Mar 26 16:15:41 2010 +0100
@@ -10,6 +10,8 @@
 
 from threading import Lock
 
+from yams.schema import role_name
+
 from cubicweb import ValidationError
 from cubicweb.schema import RQLConstraint, RQLUniqueConstraint
 from cubicweb.selectors import implements
@@ -73,7 +75,8 @@
             _ = self.session._
             msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)')
             msg %= {'rtype': _(self.rtype), 'etype': _(etype), 'eid': self.eid}
-            raise ValidationError(self.eid, {self.rtype: msg})
+            qname = role_name(self.rtype, self.role)
+            raise ValidationError(self.eid, {qname: msg})
 
     def commit_event(self):
         pass
@@ -84,12 +87,14 @@
 
 class _CheckSRelationOp(_CheckRequiredRelationOperation):
     """check required subject relation"""
+    role = 'subject'
     def _rql(self):
         return 'Any O WHERE S eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
 
 
 class _CheckORelationOp(_CheckRequiredRelationOperation):
     """check required object relation"""
+    role = 'object'
     def _rql(self):
         return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
 
@@ -225,7 +230,8 @@
                 rset = self._cw.execute(rql, {'val': val})
                 if rset and rset[0][0] != entity.eid:
                     msg = self._cw._('the value "%s" is already used, use another one')
-                    raise ValidationError(entity.eid, {attr: msg % val})
+                    qname = role_name(attr, 'subject')
+                    raise ValidationError(entity.eid, {qname: msg % val})
 
 
 class DontRemoveOwnersGroupHook(IntegrityHook):
@@ -237,12 +243,16 @@
 
     def __call__(self):
         if self.event == 'before_delete_entity' and self.entity.name == 'owners':
-            raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
-        elif self.event == 'before_update_entity' and 'name' in self.entity.edited_attributes:
+            msg = self._cw._('can\'t be deleted')
+            raise ValidationError(self.entity.eid, {None: msg})
+        elif self.event == 'before_update_entity' and \
+                 'name' in self.entity.edited_attributes:
             newname = self.entity.pop('name')
             oldname = self.entity.name
             if oldname == 'owners' and newname != oldname:
-                raise ValidationError(self.entity.eid, {'name': self._cw._('can\'t be changed')})
+                qname = role_name('name', 'subject')
+                msg = self._cw._('can\'t be changed')
+                raise ValidationError(self.entity.eid, {qname: msg})
             self.entity['name'] = newname
 
 
--- a/hooks/syncsession.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/hooks/syncsession.py	Fri Mar 26 16:15:41 2010 +0100
@@ -7,6 +7,7 @@
 """
 __docformat__ = "restructuredtext en"
 
+from yams.schema import role_name
 from cubicweb import UnknownProperty, ValidationError, BadConnectionId
 from cubicweb.selectors import implements
 from cubicweb.server import hook
@@ -147,11 +148,13 @@
         try:
             value = session.vreg.typed_value(key, value)
         except UnknownProperty:
+            qname = role_name('pkey', 'subject')
             raise ValidationError(self.entity.eid,
-                                  {'pkey': session._('unknown property key')})
+                                  {qname: session._('unknown property key')})
         except ValueError, ex:
+            qname = role_name('value', 'subject')
             raise ValidationError(self.entity.eid,
-                                  {'value': session._(str(ex))})
+                                  {qname: session._(str(ex))})
         if not session.user.matching_groups('managers'):
             session.add_relation(self.entity.eid, 'for_user', session.user.eid)
         else:
@@ -174,7 +177,8 @@
         except UnknownProperty:
             return
         except ValueError, ex:
-            raise ValidationError(entity.eid, {'value': session._(str(ex))})
+            qname = role_name('value', 'subject')
+            raise ValidationError(entity.eid, {qname: session._(str(ex))})
         if entity.for_user:
             for session_ in get_user_sessions(session.repo, entity.for_user[0].eid):
                 _ChangeCWPropertyOp(session, cwpropdict=session_.user.properties,
@@ -214,8 +218,10 @@
         key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
                                      {'x': eidfrom}, 'x')[0]
         if session.vreg.property_info(key)['sitewide']:
+            qname = role_name('for_user', 'subject')
+            msg = session._("site-wide property can't be set for user")
             raise ValidationError(eidfrom,
-                                  {'for_user': session._("site-wide property can't be set for user")})
+                                  {qname: msg})
         for session_ in get_user_sessions(session.repo, self.eidto):
             _ChangeCWPropertyOp(session, cwpropdict=session_.user.properties,
                               key=key, value=value)
--- a/hooks/test/unittest_hooks.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/hooks/test/unittest_hooks.py	Fri Mar 26 16:15:41 2010 +0100
@@ -102,9 +102,9 @@
                               'WHERE FE name "CWUser", RT name "in_group", TE name "String"')[0][0]
         self.execute('SET X read_permission Y WHERE X eid %(x)s, Y name "managers"',
                      {'x': releid}, 'x')
-        ex = self.assertRaises(ValidationError,
-                               self.commit)
-        self.assertEquals(ex.errors, {'to_entity': 'RQLConstraint O final FALSE failed'})
+        ex = self.assertRaises(ValidationError, self.commit)
+        self.assertEquals(ex.errors,
+                          {'to_entity-object': 'RQLConstraint O final FALSE failed'})
 
     def test_html_tidy_hook(self):
         req = self.request()
@@ -217,23 +217,23 @@
     def test_unexistant_eproperty(self):
         ex = self.assertRaises(ValidationError,
                           self.execute, 'INSERT CWProperty X: X pkey "bla.bla", X value "hop", X for_user U')
-        self.assertEquals(ex.errors, {'pkey': 'unknown property key'})
+        self.assertEquals(ex.errors, {'pkey-subject': 'unknown property key'})
         ex = self.assertRaises(ValidationError,
                           self.execute, 'INSERT CWProperty X: X pkey "bla.bla", X value "hop"')
-        self.assertEquals(ex.errors, {'pkey': 'unknown property key'})
+        self.assertEquals(ex.errors, {'pkey-subject': 'unknown property key'})
 
     def test_site_wide_eproperty(self):
         ex = self.assertRaises(ValidationError,
                                self.execute, 'INSERT CWProperty X: X pkey "ui.site-title", X value "hop", X for_user U')
-        self.assertEquals(ex.errors, {'for_user': "site-wide property can't be set for user"})
+        self.assertEquals(ex.errors, {'for_user-subject': "site-wide property can't be set for user"})
 
     def test_bad_type_eproperty(self):
         ex = self.assertRaises(ValidationError,
                                self.execute, 'INSERT CWProperty X: X pkey "ui.language", X value "hop", X for_user U')
-        self.assertEquals(ex.errors, {'value': u'unauthorized value'})
+        self.assertEquals(ex.errors, {'value-subject': u'unauthorized value'})
         ex = self.assertRaises(ValidationError,
                           self.execute, 'INSERT CWProperty X: X pkey "ui.language", X value "hop"')
-        self.assertEquals(ex.errors, {'value': u'unauthorized value'})
+        self.assertEquals(ex.errors, {'value-subject': u'unauthorized value'})
 
 
 class SchemaHooksTC(CubicWebTC):
@@ -253,7 +253,7 @@
             self.execute('INSERT CWUser X: X login "admin"')
         except ValidationError, ex:
             self.assertIsInstance(ex.entity, int)
-            self.assertEquals(ex.errors, {'login': 'the value "admin" is already used, use another one'})
+            self.assertEquals(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'})
 
 
 if __name__ == '__main__':
--- a/hooks/workflow.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/hooks/workflow.py	Fri Mar 26 16:15:41 2010 +0100
@@ -9,6 +9,8 @@
 
 from datetime import datetime
 
+from yams.schema import role_name
+
 from cubicweb import RepositoryError, ValidationError
 from cubicweb.interfaces import IWorkflowable
 from cubicweb.selectors import implements
@@ -73,8 +75,9 @@
         if mainwf.eid == self.wfeid:
             deststate = mainwf.initial
             if not deststate:
+                qname = role_name('custom_workflow', 'subject')
                 msg = session._('workflow has no initial state')
-                raise ValidationError(entity.eid, {'custom_workflow': msg})
+                raise ValidationError(entity.eid, {qname: msg})
             if mainwf.state_by_eid(entity.current_state.eid):
                 # nothing to do
                 return
@@ -97,8 +100,9 @@
         outputs = set()
         for ep in tr.subworkflow_exit:
             if ep.subwf_state.eid in outputs:
+                qname = role_name('subworkflow_exit', 'subject')
                 msg = self.session._("can't have multiple exits on the same state")
-                raise ValidationError(self.treid, {'subworkflow_exit': msg})
+                raise ValidationError(self.treid, {qname: msg})
             outputs.add(ep.subwf_state.eid)
 
 
@@ -112,6 +116,7 @@
         wftr = forentity.subworkflow_input_transition()
         if wftr is None:
             # inconsistency detected
+            qname = role_name('to_state', 'subject')
             msg = session._("state doesn't belong to entity's current workflow")
             raise ValidationError(self.trinfo.eid, {'to_state': msg})
         tostate = wftr.get_exit_point(forentity, trinfo['to_state'])
@@ -166,8 +171,9 @@
         try:
             foreid = entity['wf_info_for']
         except KeyError:
+            qname = role_name('wf_info_for', 'subject')
             msg = session._('mandatory relation')
-            raise ValidationError(entity.eid, {'wf_info_for': msg})
+            raise ValidationError(entity.eid, {qname: msg})
         forentity = session.entity_from_eid(foreid)
         # then check it has a workflow set, unless we're in the process of changing
         # entity's workflow
@@ -195,41 +201,47 @@
             # no transition set, check user is a manager and destination state
             # is specified (and valid)
             if not cowpowers:
+                qname = role_name('by_transition', 'subject')
                 msg = session._('mandatory relation')
-                raise ValidationError(entity.eid, {'by_transition': msg})
+                raise ValidationError(entity.eid, {qname: msg})
             deststateeid = entity.get('to_state')
             if not deststateeid:
+                qname = role_name('by_transition', 'subject')
                 msg = session._('mandatory relation')
-                raise ValidationError(entity.eid, {'by_transition': msg})
+                raise ValidationError(entity.eid, {qname: msg})
             deststate = wf.state_by_eid(deststateeid)
             if deststate is None:
+                qname = role_name('to_state', 'subject')
                 msg = session._("state doesn't belong to entity's workflow")
-                raise ValidationError(entity.eid, {'to_state': msg})
+                raise ValidationError(entity.eid, {qname: msg})
         else:
             # check transition is valid and allowed, unless we're coming back
             # from subworkflow
             tr = session.entity_from_eid(treid)
             if swtr is None:
+                qname = role_name('by_transition', 'subject')
                 if tr is None:
                     msg = session._("transition doesn't belong to entity's workflow")
-                    raise ValidationError(entity.eid, {'by_transition': msg})
+                    raise ValidationError(entity.eid, {qname: msg})
                 if not tr.has_input_state(fromstate):
                     msg = session._("transition %(tr)s isn't allowed from %(st)s") % {
                         'tr': session._(tr.name), 'st': session._(fromstate.name)}
-                    raise ValidationError(entity.eid, {'by_transition': msg})
+                    raise ValidationError(entity.eid, {qname: msg})
                 if not tr.may_be_fired(foreid):
                     msg = session._("transition may not be fired")
-                    raise ValidationError(entity.eid, {'by_transition': msg})
+                    raise ValidationError(entity.eid, {qname: msg})
             if entity.get('to_state'):
                 deststateeid = entity['to_state']
                 if not cowpowers and deststateeid != tr.destination(forentity).eid:
+                    qname = role_name('by_transition', 'subject')
                     msg = session._("transition isn't allowed")
-                    raise ValidationError(entity.eid, {'by_transition': msg})
+                    raise ValidationError(entity.eid, {qname: msg})
                 if swtr is None:
                     deststate = session.entity_from_eid(deststateeid)
                     if not cowpowers and deststate is None:
+                        qname = role_name('to_state', 'subject')
                         msg = session._("state doesn't belong to entity's workflow")
-                        raise ValidationError(entity.eid, {'to_state': msg})
+                        raise ValidationError(entity.eid, {qname: msg})
             else:
                 deststateeid = tr.destination(forentity).eid
         # everything is ok, add missing information on the trinfo entity
@@ -279,12 +291,14 @@
             if wf.state_by_eid(self.eidto):
                 break
         else:
+            qname = role_name('in_state', 'subject')
             msg = session._("state doesn't belong to entity's workflow. You may "
                             "want to set a custom workflow for this entity first.")
-            raise ValidationError(self.eidfrom, {'in_state': msg})
+            raise ValidationError(self.eidfrom, {qname: msg})
         if entity.current_workflow and wf.eid != entity.current_workflow.eid:
+            qname = role_name('in_state', 'subject')
             msg = session._("state doesn't belong to entity's current workflow")
-            raise ValidationError(self.eidfrom, {'in_state': msg})
+            raise ValidationError(self.eidfrom, {qname: msg})
 
 
 class SetModificationDateOnStateChange(WorkflowHook):
--- a/i18n/en.po	Fri Mar 26 16:15:16 2010 +0100
+++ b/i18n/en.po	Fri Mar 26 16:15:41 2010 +0100
@@ -2514,6 +2514,9 @@
 msgid "incontext"
 msgstr "in-context"
 
+msgid "incorrect captcha value"
+msgstr ""
+
 #, python-format
 msgid "incorrect value (%(value)s) for type \"%(type)s\""
 msgstr ""
@@ -3721,6 +3724,9 @@
 msgid "ui.time-format"
 msgstr "time format"
 
+msgid "unable to check captcha, please try again"
+msgstr ""
+
 msgid "unaccessible"
 msgstr ""
 
--- a/i18n/es.po	Fri Mar 26 16:15:16 2010 +0100
+++ b/i18n/es.po	Fri Mar 26 16:15:41 2010 +0100
@@ -2572,6 +2572,9 @@
 msgid "incontext"
 msgstr "En el contexto"
 
+msgid "incorrect captcha value"
+msgstr ""
+
 #, python-format
 msgid "incorrect value (%(value)s) for type \"%(type)s\""
 msgstr "valor %(value)s incorrecto para el tipo \"%(type)s\""
@@ -3802,6 +3805,9 @@
 msgid "ui.time-format"
 msgstr ""
 
+msgid "unable to check captcha, please try again"
+msgstr ""
+
 msgid "unaccessible"
 msgstr "inaccesible"
 
--- a/i18n/fr.po	Fri Mar 26 16:15:16 2010 +0100
+++ b/i18n/fr.po	Fri Mar 26 16:15:41 2010 +0100
@@ -320,31 +320,32 @@
 "Can't restore %(role)s relation %(rtype)s to entity %(eid)s which is already "
 "linked using this relation."
 msgstr ""
-"Ne peut restaurer la relation %(role)s %(rtype)s vers l'entité %(eid)s qui est "
-"déja lié à une autre entité par cette relation."
+"Ne peut restaurer la relation %(role)s %(rtype)s vers l'entité %(eid)s qui "
+"est déja lié à une autre entité par cette relation."
 
 #, python-format
 msgid ""
 "Can't restore relation %(rtype)s between %(subj)s and %(obj)s, that relation "
 "does not exists anymore in the schema."
 msgstr ""
-"Ne peut restaurer la relation %(rtype)s entre %(subj)s et  %(obj)s, "
-"cette relation n'existe plus dans le schéma."
+"Ne peut restaurer la relation %(rtype)s entre %(subj)s et  %(obj)s, cette "
+"relation n'existe plus dans le schéma."
 
 #, python-format
 msgid ""
 "Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
 "exists anymore in the schema."
 msgstr ""
-"Ne peut restaurer la relation %(rtype)s de l'entité %(eid)s, cette relation"
-"n'existe plus dans le schéma"
+"Ne peut restaurer la relation %(rtype)s de l'entité %(eid)s, cette "
+"relationn'existe plus dans le schéma"
 
 #, python-format
 msgid ""
 "Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
 "anymore."
 msgstr ""
-"Ne peut restaurer la relation %(rtype)s, l'entité %(role)s %(eid)s n'existe plus."
+"Ne peut restaurer la relation %(rtype)s, l'entité %(role)s %(eid)s n'existe "
+"plus."
 
 msgid "Date"
 msgstr "Date"
@@ -2604,6 +2605,9 @@
 msgid "incontext"
 msgstr "dans le contexte"
 
+msgid "incorrect captcha value"
+msgstr "valeur de captcha incorrecte"
+
 #, python-format
 msgid "incorrect value (%(value)s) for type \"%(type)s\""
 msgstr "valeur %(value)s incorrecte pour le type \"%(type)s\""
@@ -3838,6 +3842,9 @@
 msgid "ui.time-format"
 msgstr "format de l'heure"
 
+msgid "unable to check captcha, please try again"
+msgstr "impossible de vérifier le captcha, veuillez réessayer"
+
 msgid "unaccessible"
 msgstr "inaccessible"
 
--- a/schema.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/schema.py	Fri Mar 26 16:15:41 2010 +0100
@@ -21,7 +21,7 @@
 
 from yams import BadSchemaDefinition, buildobjs as ybo
 from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema, \
-     RelationDefinitionSchema, PermissionMixIn
+     RelationDefinitionSchema, PermissionMixIn, role_name
 from yams.constraints import BaseConstraint, FormatConstraint
 from yams.reader import (CONSTRAINTS, PyFileReader, SchemaLoader,
                          obsolete as yobsolete, cleanup_sys_modules)
@@ -682,17 +682,22 @@
             # XXX at this point if both or neither of S and O are in mainvar we
             # dunno if the validation error `occured` 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, {rtype: msg})
+            raise ValidationError(maineid, {qname: msg})
 
     def exec_query(self, session, eidfrom, eidto):
         if eidto is None:
--- a/selectors.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/selectors.py	Fri Mar 26 16:15:41 2010 +0100
@@ -1078,6 +1078,24 @@
             return 1
         return 0
 
+class is_in_state(score_entity):
+    """return 1 if entity is in one of the states given as argument list
+
+    you should use this instead of your own score_entity x: x.state == 'bla'
+    selector to avoid some gotchas:
+
+    * possible views gives a fake entity with no state
+    * you must use the latest tr info, not entity.state for repository side
+      checking of the current state
+    """
+    def __init__(self, *states):
+        def score(entity, states=set(states)):
+            try:
+                return entity.latest_trinfo().new_state.name in states
+            except AttributeError:
+                return None
+        super(is_in_state, self).__init__(score)
+
 
 ## deprecated stuff ############################################################
 
--- a/server/__init__.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/server/__init__.py	Fri Mar 26 16:15:41 2010 +0100
@@ -115,11 +115,7 @@
     from cubicweb.server.sqlutils import sqlexec, sqlschema, sqldropschema
     # configuration to avoid db schema loading and user'state checking
     # on connection
-    read_instance_schema = config.read_instance_schema
-    bootstrap_schema = config.bootstrap_schema
-    config.read_instance_schema = False
     config.creating = True
-    config.bootstrap_schema = True
     config.consider_user_state = False
     config.set_language = False
     # only enable the system source at initialization time + admin which is not
@@ -202,8 +198,6 @@
     repo.shutdown()
     # restore initial configuration
     config.creating = False
-    config.read_instance_schema = read_instance_schema
-    config.bootstrap_schema = bootstrap_schema
     config.consider_user_state = True
     config.set_language = True
     print '-> database for instance %s initialized.' % config.appid
--- a/server/hook.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/server/hook.py	Fri Mar 26 16:15:41 2010 +0100
@@ -19,8 +19,11 @@
 Relation (eg before_add_relation, after_add_relation, before_delete_relation,
 after_delete_relation) all have `eidfrom`, `rtype`, `eidto` attributes.
 
-Server start/stop hooks (eg server_startup, server_shutdown) have a `repo`
-attribute, but *their `_cw` attribute is None*.
+Server start/maintenance/stop hooks (eg server_startup, server_maintenance,
+server_shutdown) have a `repo` attribute, but *their `_cw` attribute is None*.
+The `server_startup` is called on regular startup, while `server_maintenance`
+is called on cubicweb-ctl upgrade or shell commands. `server_shutdown` is
+called anyway.
 
 Backup/restore hooks (eg server_backup, server_restore) have a `repo` and a
 `timestamp` attributes, but *their `_cw` attribute is None*.
@@ -57,7 +60,7 @@
 RELATIONS_HOOKS = set(('before_add_relation',   'after_add_relation' ,
                        'before_delete_relation','after_delete_relation'))
 SYSTEM_HOOKS = set(('server_backup', 'server_restore',
-                    'server_startup', 'server_shutdown',
+                    'server_startup', 'server_maintenance', 'server_shutdown',
                     'session_open', 'session_close'))
 ALL_HOOKS = ENTITIES_HOOKS | RELATIONS_HOOKS | SYSTEM_HOOKS
 
--- a/server/migractions.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/server/migractions.py	Fri Mar 26 16:15:41 2010 +0100
@@ -59,7 +59,6 @@
     def __init__(self, config, schema, interactive=True,
                  repo=None, cnx=None, verbosity=1, connect=True):
         MigrationHelper.__init__(self, config, interactive, verbosity)
-        # no config on shell to a remote instance
         if not interactive:
             assert cnx
             assert repo
@@ -67,11 +66,13 @@
             assert repo
             self._cnx = cnx
             self.repo = repo
-            if config is not None:
-                self.session.data['rebuild-infered'] = False
         elif connect:
             self.repo_connect()
-        if not schema:
+        # no config on shell to a remote instance
+        if config is not None and (cnx or connect):
+            self.session.data['rebuild-infered'] = False
+            self.repo.hm.call_hooks('server_maintenance', repo=self.repo)
+        if not schema and not getattr(config, 'quick_start', False):
             schema = config.load_schema(expand_cubes=True)
         self.fs_schema = schema
         self._synchronized = set()
--- a/server/repository.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/server/repository.py	Fri Mar 26 16:15:41 2010 +0100
@@ -31,6 +31,7 @@
 from logilab.common import flatten
 
 from yams import BadSchemaDefinition
+from yams.schema import role_name
 from rql import RQLSyntaxError
 
 from cubicweb import (CW_SOFTWARE_ROOT, CW_MIGRATION_MAP,
@@ -164,7 +165,7 @@
         if config.open_connections_pools:
             self.open_connections_pools()
 
-    def _boostrap_hook_registry(self):
+    def _bootstrap_hook_registry(self):
         """called during bootstrap since we need the metadata hooks"""
         hooksdirectory = join(CW_SOFTWARE_ROOT, 'hooks')
         self.vreg.init_registration([hooksdirectory])
@@ -175,12 +176,15 @@
         config = self.config
         self._available_pools = Queue.Queue()
         self._available_pools.put_nowait(pool.ConnectionsPool(self.sources))
-        if config.read_instance_schema:
-            # normal start: load the instance schema from the database
-            self.fill_schema()
-        elif config.bootstrap_schema:
-            # usually during repository creation
-            self.warning("set fs instance'schema as bootstrap schema")
+        if config.quick_start:
+            # quick start, usually only to get a minimal repository to get cubes
+            # information (eg dump/restore/
+            config._cubes = ()
+            self.set_schema(config.load_schema(), resetvreg=False)
+            config['connections-pool-size'] = 1
+            config._cubes = None
+        elif config.creating:
+            # repository creation
             config.bootstrap_cubes()
             self.set_schema(config.load_schema(), resetvreg=False)
             # need to load the Any and CWUser entity types
@@ -188,8 +192,11 @@
             self.vreg.init_registration([etdirectory])
             for modname in ('__init__', 'authobjs', 'wfobjs'):
                 self.vreg.load_file(join(etdirectory, '%s.py' % modname),
-                                'cubicweb.entities.%s' % modname)
-            self._boostrap_hook_registry()
+                                    'cubicweb.entities.%s' % modname)
+            self._bootstrap_hook_registry()
+        elif config.read_instance_schema:
+            # normal start: load the instance schema from the database
+            self.fill_schema()
         else:
             # test start: use the file system schema (quicker)
             self.warning("set fs instance'schema")
@@ -218,7 +225,10 @@
             self.pools.append(pool.ConnectionsPool(self.sources))
             self._available_pools.put_nowait(self.pools[-1])
         self._shutting_down = False
-        self.hm = self.vreg['hooks']
+        if config.quick_start:
+            config.init_cubes(self.get_cubes())
+        else:
+            self.hm = self.vreg['hooks']
 
     # internals ###############################################################
 
@@ -267,7 +277,8 @@
         self.set_schema(appschema)
 
     def start_looping_tasks(self):
-        if not (self.config.creating or self.config.repairing):
+        if not (self.config.creating or self.config.repairing
+                or self.config.quick_start):
             # call instance level initialisation hooks
             self.hm.call_hooks('server_startup', repo=self)
             # register a task to cleanup expired session
@@ -335,7 +346,8 @@
             self.info('waiting thread %s...', thread.name)
             thread.join()
             self.info('thread %s finished', thread.name)
-        if not (self.config.creating or self.config.repairing):
+        if not (self.config.creating or self.config.repairing
+                or self.config.quick_start):
             self.hm.call_hooks('server_shutdown', repo=self)
         self.close_sessions()
         while not self._available_pools.empty():
@@ -447,6 +459,7 @@
         """
         versions = self.get_versions(not (self.config.creating
                                           or self.config.repairing
+                                          or self.config.quick_start
                                           or self.config.mode == 'test'))
         cubes = list(versions)
         cubes.remove('cubicweb')
@@ -506,12 +519,13 @@
         finally:
             session.close()
 
+    # XXX protect this method: anonymous should be allowed and registration
+    # plugged
     def register_user(self, login, password, email=None, **kwargs):
         """check a user with the given login exists, if not create it with the
         given password. This method is designed to be used for anonymous
         registration on public web site.
         """
-        # XXX should not be called from web interface
         session = self.internal_session()
         # for consistency, keep same error as unique check hook (although not required)
         errmsg = session._('the value "%s" is already used, use another one')
@@ -519,7 +533,8 @@
             if (session.execute('CWUser X WHERE X login %(login)s', {'login': login})
                 or session.execute('CWUser X WHERE X use_email C, C address %(login)s',
                                    {'login': login})):
-                raise ValidationError(None, {'login': errmsg % login})
+                qname = role_name('login', 'subject')
+                raise ValidationError(None, {qname: errmsg % login})
             # we have to create the user
             user = self.vreg['etypes'].etype_class('CWUser')(session, None)
             if isinstance(password, unicode):
@@ -534,9 +549,11 @@
             if email or '@' in login:
                 d = {'login': login, 'email': email or login}
                 if session.execute('EmailAddress X WHERE X address %(email)s', d):
-                    raise ValidationError(None, {'address': errmsg % d['email']})
+                    qname = role_name('address', 'subject')
+                    raise ValidationError(None, {qname: errmsg % d['email']})
                 session.execute('INSERT EmailAddress X: X address %(email)s, '
-                                'U primary_email X, U use_email X WHERE U login %(login)s', d)
+                                'U primary_email X, U use_email X '
+                                'WHERE U login %(login)s', d)
             session.commit()
         finally:
             session.close()
@@ -610,7 +627,7 @@
             session.reset_pool()
 
     def check_session(self, sessionid):
-        """raise `BadSessionId` if the connection is no more valid"""
+        """raise `BadConnectionId` if the connection is no more valid"""
         self._get_session(sessionid, setpool=False)
 
     def get_shared_data(self, sessionid, key, default=None, pop=False):
--- a/server/serverconfig.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/server/serverconfig.py	Fri Mar 26 16:15:41 2010 +0100
@@ -194,8 +194,11 @@
 
     # read the schema from the database
     read_instance_schema = True
-    bootstrap_schema = True
-
+    # set to true while creating an instance
+    creating = False
+    # set this to true to get a minimal repository, for instance to get cubes
+    # information on commands such as i18ninstance, db-restore, etc...
+    quick_start = False
     # check user's state at login time
     consider_user_state = True
 
--- a/server/serverctl.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/server/serverctl.py	Fri Mar 26 16:15:41 2010 +0100
@@ -389,7 +389,7 @@
             get_connection(
                 system['db-driver'], database=system['db-name'],
                 host=system.get('db-host'), port=system.get('db-port'),
-                user=system.get('db-user'), password=system.get('db-password'), 
+                user=system.get('db-user'), password=system.get('db-password'),
                 **extra)
         except Exception, ex:
             raise ConfigurationError(
@@ -572,17 +572,16 @@
 
 def _local_dump(appid, output):
     config = ServerConfiguration.config_for(appid)
-    # schema=1 to avoid unnecessary schema loading
-    mih = config.migration_handler(connect=False, schema=1, verbosity=1)
+    config.quick_start = True
+    mih = config.migration_handler(connect=False, verbosity=1)
     mih.backup_database(output, askconfirm=False)
     mih.shutdown()
 
 def _local_restore(appid, backupfile, drop, systemonly=True):
     config = ServerConfiguration.config_for(appid)
     config.verbosity = 1 # else we won't be asked for confirmation on problems
-    config.repairing = 1 # don't check versions
-    # schema=1 to avoid unnecessary schema loading
-    mih = config.migration_handler(connect=False, schema=1, verbosity=1)
+    config.quick_start = True
+    mih = config.migration_handler(connect=False, verbosity=1)
     mih.restore_database(backupfile, drop, systemonly, askconfirm=False)
     repo = mih.repo_connect()
     # version of the database
--- a/server/session.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/server/session.py	Fri Mar 26 16:15:41 2010 +0100
@@ -553,7 +553,7 @@
     def _touch(self):
         """update latest session usage timestamp and reset mode to read"""
         self.timestamp = time()
-        self.local_perm_cache.clear()
+        self.local_perm_cache.clear() # XXX simply move in transaction_data, no?
         self._threaddata.mode = self.default_mode
 
     # shared data handling ###################################################
--- a/server/test/unittest_hook.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/server/test/unittest_hook.py	Fri Mar 26 16:15:41 2010 +0100
@@ -92,19 +92,19 @@
         class _Hook(hook.Hook):
             events = ('before_add_entiti',)
         ex = self.assertRaises(Exception, self.o.register, _Hook)
-        self.assertEquals(str(ex), 'bad event before_add_entiti on unittest_hook._Hook')
+        self.assertEquals(str(ex), 'bad event before_add_entiti on %s._Hook' % __name__)
 
     def test_register_bad_hook2(self):
         class _Hook(hook.Hook):
             events = None
         ex = self.assertRaises(Exception, self.o.register, _Hook)
-        self.assertEquals(str(ex), 'bad .events attribute None on unittest_hook._Hook')
+        self.assertEquals(str(ex), 'bad .events attribute None on %s._Hook' % __name__)
 
     def test_register_bad_hook3(self):
         class _Hook(hook.Hook):
             events = 'before_add_entity'
         ex = self.assertRaises(Exception, self.o.register, _Hook)
-        self.assertEquals(str(ex), 'bad event b on unittest_hook._Hook')
+        self.assertEquals(str(ex), 'bad event b on %s._Hook' % __name__)
 
     def test_call_hook(self):
         self.o.register(AddAnyHook)
--- a/server/test/unittest_ldapuser.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/server/test/unittest_ldapuser.py	Fri Mar 26 16:15:41 2010 +0100
@@ -15,7 +15,7 @@
 
 from cubicweb.server.sources.ldapuser import *
 
-if socket.gethostbyname('ldap1').startswith('172'):
+if '17.1' in socket.gethostbyname('ldap1'):
     SYT = 'syt'
     ADIM = 'adim'
 else:
--- a/web/captcha.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/web/captcha.py	Fri Mar 26 16:15:41 2010 +0100
@@ -17,7 +17,7 @@
 from time import time
 
 from cubicweb import tags
-from cubicweb.web import formwidgets as fw
+from cubicweb.web import ProcessFormError, formwidgets as fw
 
 
 def pil_captcha(text, fontfile, fontsize):
@@ -63,7 +63,22 @@
 class CaptchaWidget(fw.TextInput):
     def render(self, form, field, renderer=None):
         # t=int(time()*100) to make sure img is not cached
-        src = form._cw.build_url('view', vid='captcha', t=int(time()*100))
+        src = form._cw.build_url('view', vid='captcha', t=int(time()*100),
+                                 captchakey=field.input_name(form))
         img = tags.img(src=src, alt=u'captcha')
         img = u'<div class="captcha">%s</div>' % img
         return img + super(CaptchaWidget, self).render(form, field, renderer)
+
+    def process_field_data(self, form, field):
+        captcha = form._cw.get_session_data(field.input_name(form), None,
+                                            pop=True)
+        val = super(CaptchaWidget, self).process_field_data(form, field)
+        if val is None:
+            return val # required will be checked by field
+        if captcha is None:
+            msg = form._cw._('unable to check captcha, please try again')
+            raise ProcessFormError(msg)
+        elif val.lower() != captcha.lower():
+            msg = form._cw._('incorrect captcha value')
+            raise ProcessFormError(msg)
+        return val
--- a/web/controller.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/web/controller.py	Fri Mar 26 16:15:41 2010 +0100
@@ -136,6 +136,9 @@
             params.update(newparams)
             newparams = params
         elif self._edited_entity:
+            # clear caches in case some attribute participating to the rest path
+            # has been modified
+            self._edited_entity.clear_all_caches()
             path = self._edited_entity.rest_path()
         else:
             path = 'view'
--- a/web/data/cubicweb.edition.js	Fri Mar 26 16:15:16 2010 +0100
+++ b/web/data/cubicweb.edition.js	Fri Mar 26 16:15:41 2010 +0100
@@ -322,7 +322,7 @@
 
 function _clearPreviousErrors(formid) {
     jQuery('#' + formid + 'ErrorMessage').remove();
-    jQuery('#' + formid + ' span.error').remove();
+    jQuery('#' + formid + ' span.errorMsg').remove();
     jQuery('#' + formid + ' .error').removeClass('error');
 }
 
@@ -334,6 +334,7 @@
 	var fieldid = fieldname + ':' + eid;
 	var suffixes = ['', '-subject', '-object'];
 	var found = false;
+	// XXX remove suffixes at some point
 	for (var i=0, length=suffixes.length; i<length;i++) {
 	    var field = jqNode(fieldname + suffixes[i] + ':' + eid);
 	    if (field && getNodeAttribute(field, 'type') != 'hidden') {
@@ -341,7 +342,7 @@
 		    firsterrfield = 'err-' + fieldid;
 		}
 		addElementClass(field, 'error');
-		var span = SPAN({'id': 'err-' + fieldid, 'class': "error"}, errmsg);
+		var span = SPAN({'id': 'err-' + fieldid, 'class': "errorMsg"}, errmsg);
 		field.before(span);
 		found = true;
 		break;
--- a/web/data/cubicweb.form.css	Fri Mar 26 16:15:16 2010 +0100
+++ b/web/data/cubicweb.form.css	Fri Mar 26 16:15:41 2010 +0100
@@ -192,11 +192,14 @@
   background-color: #eeedd9;
 }
 
-input.error {
+.error input { /* error added by the form renderer */
+  background: transparent url("error.png") 100% 50% no-repeat;
+}
+input.error { /* error added by javascript */
   background: transparent url("error.png") 100% 50% no-repeat;
 }
 
-span.error {
+span.errorMsg {
   display: block;
   font-weight: bold;
   color: #ed0d0d;
--- a/web/formfields.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/web/formfields.py	Fri Mar 26 16:15:41 2010 +0100
@@ -15,7 +15,7 @@
 from logilab.mtconverter import xml_escape
 from logilab.common.date import ustrftime
 
-from yams.schema import KNOWN_METAATTRIBUTES
+from yams.schema import KNOWN_METAATTRIBUTES, role_name
 from yams.constraints import (SizeConstraint, StaticVocabularyConstraint,
                               FormatConstraint)
 
@@ -214,7 +214,7 @@
     def role_name(self):
         """return <field.name>-<field.role> if role is specified, else field.name"""
         if self.role is not None:
-            return '%s-%s' % (self.name, self.role)
+            return role_name(self.name, self.role)
         return self.name
 
     def dom_id(self, form, suffix=None):
@@ -381,7 +381,10 @@
         for field in self.actual_fields(form):
             if field is self:
                 try:
-                    yield field, field.process_form_value(form)
+                    value = field.process_form_value(form)
+                    if value is None and field.required:
+                        raise ProcessFormError(form._cw._("required field"))
+                    yield field, value
                 except UnmodifiedField:
                     continue
             else:
--- a/web/test/unittest_application.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/web/test/unittest_application.py	Fri Mar 26 16:15:41 2010 +0100
@@ -186,36 +186,66 @@
         self.assertEquals(values['eid'], eid)
         error = forminfo['error']
         self.assertEquals(error.entity, user.eid)
-        self.assertEquals(error.errors['login'], 'required attribute')
+        self.assertEquals(error.errors['login-subject'], 'required field')
 
 
-    def test_validation_error_dont_loose_subentity_data(self):
+    def test_validation_error_dont_loose_subentity_data_ctrl(self):
         """test creation of two linked entities
+
+        error occurs on the web controller
         """
         req = self.request()
-        form = {'eid': ['X', 'Y'], '__maineid': 'X',
-                '__type:X': 'CWUser', '_cw_edited_fields:X': 'login-subject,surname-subject',
-                # missing required field
-                'login-subject:X': u'',
-                'surname-subject:X': u'Mr Ouaoua',
-                # but email address is set
-                '__type:Y': 'EmailAddress', '_cw_edited_fields:Y': 'address-subject,alias-subject,use_email-object',
-                'address-subject:Y': u'bougloup@logilab.fr',
-                'alias-subject:Y': u'',
-                'use_email-object:Y': 'X',
-                # necessary to get validation error handling
-                '__errorurl': 'view?vid=edition...',
-                }
-        req.form = form
-        # monkey patch edited_eid to ensure both entities are edited, not only X
-        req.edited_eids = lambda : ('Y', 'X')
+        # set Y before X to ensure both entities are edited, not only X
+        req.form = {'eid': ['Y', 'X'], '__maineid': 'X',
+                    '__type:X': 'CWUser', '_cw_edited_fields:X': 'login-subject',
+                    # missing required field
+                    'login-subject:X': u'',
+                    # but email address is set
+                    '__type:Y': 'EmailAddress', '_cw_edited_fields:Y': 'address-subject',
+                    'address-subject:Y': u'bougloup@logilab.fr',
+                    'use_email-object:Y': 'X',
+                    # necessary to get validation error handling
+                    '__errorurl': 'view?vid=edition...',
+                    }
         path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
         forminfo = req.get_session_data('view?vid=edition...')
         self.assertEquals(set(forminfo['eidmap']), set('XY'))
+        self.assertEquals(forminfo['eidmap']['X'], None)
+        self.assertIsInstance(forminfo['eidmap']['Y'], int)
+        self.assertEquals(forminfo['error'].entity, 'X')
+        self.assertEquals(forminfo['error'].errors,
+                          {'login-subject': 'required field'})
+        self.assertEquals(forminfo['values'], req.form)
+
+
+    def test_validation_error_dont_loose_subentity_data_repo(self):
+        """test creation of two linked entities
+
+        error occurs on the repository
+        """
+        req = self.request()
+        # set Y before X to ensure both entities are edited, not only X
+        req.form = {'eid': ['Y', 'X'], '__maineid': 'X',
+                    '__type:X': 'CWUser', '_cw_edited_fields:X': 'login-subject,upassword-subject',
+                    # already existent user
+                    'login-subject:X': u'admin',
+                    'upassword-subject:X': u'admin', 'upassword-subject-confirm:X': u'admin',
+                    '__type:Y': 'EmailAddress', '_cw_edited_fields:Y': 'address-subject',
+                    'address-subject:Y': u'bougloup@logilab.fr',
+                    'use_email-object:Y': 'X',
+                    # necessary to get validation error handling
+                    '__errorurl': 'view?vid=edition...',
+                    }
+        path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
+        forminfo = req.get_session_data('view?vid=edition...')
+        self.assertEquals(set(forminfo['eidmap']), set('XY'))
+        self.assertIsInstance(forminfo['eidmap']['X'], int)
+        self.assertIsInstance(forminfo['eidmap']['Y'], int)
         self.assertEquals(forminfo['error'].entity, forminfo['eidmap']['X'])
-        self.assertEquals(forminfo['error'].errors, {'login': 'required attribute',
-                                                     'upassword': 'required attribute'})
-        self.assertEquals(forminfo['values'], form)
+        self.assertEquals(forminfo['error'].errors,
+                          {'login-subject': u'the value "admin" is already used, use another one'})
+        self.assertEquals(forminfo['values'], req.form)
+
 
     def _test_cleaned(self, kwargs, injected, cleaned):
         req = self.request(**kwargs)
--- a/web/test/unittest_pdf.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/web/test/unittest_pdf.py	Fri Mar 26 16:15:41 2010 +0100
@@ -36,3 +36,8 @@
         self.assertEquals( len(output), len(reference) )
         # cut begin & end 'cause they contain variyng data
         self.assertTextEquals(output[150:1500], reference[150:1500])
+
+if __name__ == '__main__':
+    from logilab.common.testlib import unittest_main
+    unittest_main()
+
--- a/web/test/unittest_views_basecontrollers.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/web/test/unittest_views_basecontrollers.py	Fri Mar 26 16:15:41 2010 +0100
@@ -51,7 +51,7 @@
                     'upassword-subject-confirm:X': u'toto',
                     }
         ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEquals(ex.errors, {'login': 'the value "admin" is already used, use another one'})
+        self.assertEquals(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'})
 
     def test_user_editing_itself(self):
         """checking that a manager user can edit itself
@@ -219,7 +219,7 @@
                     'described_by_test-subject:X': u(feid),
                 }
         ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEquals(ex.errors, {'amount': 'value [0;100] constraint failed for value -10'})
+        self.assertEquals(ex.errors, {'amount-subject': 'value [0;100] constraint failed for value -10'})
         req = self.request()
         req.form = {'eid': ['X'],
                     '__type:X': 'Salesterm',
@@ -228,7 +228,7 @@
                     'described_by_test-subject:X': u(feid),
                     }
         ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEquals(ex.errors, {'amount': 'value [0;100] constraint failed for value 110'})
+        self.assertEquals(ex.errors, {'amount-subject': 'value [0;100] constraint failed for value 110'})
         req = self.request()
         req.form = {'eid': ['X'],
                     '__type:X': 'Salesterm',
@@ -430,7 +430,7 @@
                     'use_email-object:Y': 'X',
                     }
         ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
-        self.assertEquals(ex.errors, {'address': u'required attribute'})
+        self.assertEquals(ex.errors, {'address-subject': u'required field'})
 
     def test_nonregr_copy(self):
         user = self.user()
--- a/web/views/editcontroller.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/web/views/editcontroller.py	Fri Mar 26 16:15:41 2010 +0100
@@ -17,6 +17,11 @@
 from cubicweb.web import INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, ProcessFormError
 from cubicweb.web.views import basecontrollers, autoform
 
+def valerror_eid(eid):
+    try:
+        return typed_eid(eid)
+    except (ValueError, TypeError):
+        return eid
 
 class RqlQuery(object):
     def __init__(self):
@@ -110,7 +115,7 @@
         self._cw.remove_pending_operations()
         if self.errors:
             errors = dict((f.name, unicode(ex)) for f, ex in self.errors)
-            raise ValidationError(form.get('__maineid'), errors)
+            raise ValidationError(valerror_eid(form.get('__maineid')), errors)
 
     def _insert_entity(self, etype, eid, rqlquery):
         rql = rqlquery.insert_query(etype)
@@ -166,7 +171,7 @@
                     self.handle_formfield(form, field, rqlquery)
         if self.errors:
             errors = dict((f.role_name(), unicode(ex)) for f, ex in self.errors)
-            raise ValidationError(entity.eid, errors)
+            raise ValidationError(valerror_eid(entity.eid), errors)
         if eid is None: # creation or copy
             entity.eid = self._insert_entity(etype, formparams['eid'], rqlquery)
         elif rqlquery.edited: # edition of an existant entity
--- a/web/views/editviews.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/web/views/editviews.py	Fri Mar 26 16:15:41 2010 +0100
@@ -15,7 +15,7 @@
 from cubicweb.view import EntityView, StartupView
 from cubicweb.selectors import (one_line_rset, non_final_entity,
                                 match_search_state)
-from cubicweb.web import httpcache, captcha
+from cubicweb.web import httpcache
 from cubicweb.web.views import baseviews, linksearch_select_url
 
 
@@ -95,17 +95,23 @@
         else:
             super(EditableFinalView, self).cell_call(row, col, props)
 
-
-class CaptchaView(StartupView):
-    __regid__ = 'captcha'
+try:
+    from cubicweb.web import captcha
+except ImportError:
+    # PIL not installed
+    pass
+else:
+    class CaptchaView(StartupView):
+        __regid__ = 'captcha'
 
-    http_cache_manager = httpcache.NoHTTPCacheManager
-    binary = True
-    templatable = False
-    content_type = 'image/jpg'
+        http_cache_manager = httpcache.NoHTTPCacheManager
+        binary = True
+        templatable = False
+        content_type = 'image/jpg'
 
-    def call(self):
-        text, data = captcha.captcha(self._cw.vreg.config['captcha-font-file'],
-                                     self._cw.vreg.config['captcha-font-size'])
-        self._cw.set_session_data('captcha', text)
-        self.w(data.read())
+        def call(self):
+            text, data = captcha.captcha(self._cw.vreg.config['captcha-font-file'],
+                                         self._cw.vreg.config['captcha-font-size'])
+            key = self._cw.form.get('captchakey', 'captcha')
+            self._cw.set_session_data(key, text)
+            self.w(data.read())
--- a/web/views/formrenderers.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/web/views/formrenderers.py	Fri Mar 26 16:15:41 2010 +0100
@@ -224,6 +224,8 @@
                     w(u' class="error"')
                 w(u'>')
                 w(field.render(form, self))
+                if error:
+                    self.render_error(w, error)
                 if self.display_help:
                     w(self.render_help(form, field))
                 w(u'</td></tr>')
@@ -241,7 +243,7 @@
 
     def render_error(self, w, err):
         """return validation error for widget's field, if any"""
-        w(u'<span class="error">%s</span>' % err)
+        w(u'<span class="errorMsg">%s</span>' % err)
 
 
 
--- a/web/views/iprogress.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/web/views/iprogress.py	Fri Mar 26 16:15:41 2010 +0100
@@ -261,8 +261,7 @@
         self._cw.html_headers.add_onload('draw_progressbar("canvas%s", %i, %i, %i, "%s");' %
                                          (cid,
                                           int(100.*done/maxi), int(100.*(done+todo)/maxi),
-                                          int(100.*budget/maxi), color),
-                                         jsoncall=self._cw.json_request)
+                                          int(100.*budget/maxi), color))
         self.w(u'%s<br/>'
                u'<canvas class="progressbar" id="canvas%s" width="100" height="10"></canvas>'
                % (short_title.replace(' ','&nbsp;'), cid))
--- a/web/views/plots.py	Fri Mar 26 16:15:16 2010 +0100
+++ b/web/views/plots.py	Fri Mar 26 16:15:41 2010 +0100
@@ -115,8 +115,7 @@
                                     {'plotdefs': '\n'.join(plotdefs),
                                      'figid': figid,
                                      'plotdata': ','.join(plotdata),
-                                     'mode': self.timemode and "'time'" or 'null'},
-                                    jsoncall=req.json_request)
+                                     'mode': self.timemode and "'time'" or 'null'})
 
 
 class PlotView(baseviews.AnyRsetView):