backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 04 Jan 2011 14:11:54 +0100
changeset 6762 812445504835
parent 6751 02091c91520f (current diff)
parent 6761 84b67ae41a0d (diff)
child 6782 b5d6f5391695
backport stable
server/test/unittest_multisources.py
--- a/devtools/repotest.py	Sat Dec 18 23:12:14 2010 +0100
+++ b/devtools/repotest.py	Tue Jan 04 14:11:54 2011 +0100
@@ -141,7 +141,7 @@
 from rql import RQLHelper
 
 from cubicweb.devtools.fake import FakeRepo, FakeSession
-from cubicweb.server import set_debug
+from cubicweb.server import set_debug, debugged
 from cubicweb.server.querier import QuerierHelper
 from cubicweb.server.session import Session
 from cubicweb.server.sources.rql2sql import SQLGenerator, remove_unused_solutions
@@ -171,6 +171,8 @@
 
     def set_debug(self, debug):
         set_debug(debug)
+    def debugged(self, debug):
+        return debugged(debug)
 
     def _prepare(self, rql):
         #print '******************** prepare', rql
@@ -222,6 +224,8 @@
 
     def set_debug(self, debug):
         set_debug(debug)
+    def debugged(self, debug):
+        return debugged(debug)
 
     def _rqlhelper(self):
         rqlhelper = self.repo.vreg.rqlhelper
--- a/doc/book/en/devrepo/repo/hooks.rst	Sat Dec 18 23:12:14 2010 +0100
+++ b/doc/book/en/devrepo/repo/hooks.rst	Tue Jan 04 14:11:54 2011 +0100
@@ -97,7 +97,7 @@
 
 .. sourcecode:: python
 
-    from cubicweb.server.hook import Hook, Operation, match_rtype
+    from cubicweb.server.hook import Hook, DataOperationMixIn, Operation, match_rtype
 
     def check_cycle(self, session, eid, rtype, role='subject'):
         parents = set([eid])
@@ -110,7 +110,7 @@
             parents.add(parent.eid)
 
 
-    class CheckSubsidiaryCycleOp(Operation):
+    class CheckSubsidiaryCycleOp(DataOperationMixIn, Operation):
 
         def precommit_event(self):
             check_cycle(self.session, self.eidto, 'subsidiary_of')
@@ -131,7 +131,7 @@
 In the above example, our hook will instantiate an operation each time the hook
 is called, i.e. each time the `subsidiary_of` relation is set. There is an
 alternative method to schedule an operation from a hook, using the
-:func:`set_operation` function.
+:func:`get_instance` class method.
 
 .. sourcecode:: python
 
@@ -143,13 +143,12 @@
        __select__ = Hook.__select__ & match_rtype('subsidiary_of')
 
        def __call__(self):
-           set_operation(self._cw, 'subsidiary_cycle_detection', self.eidto,
-                         CheckSubsidiaryCycleOp)
+           CheckSubsidiaryCycleOp.get_instance(self._cw).add_data(self.eidto)
 
    class CheckSubsidiaryCycleOp(Operation):
 
        def precommit_event(self):
-           for eid in self.session.transaction_data['subsidiary_cycle_detection']:
+           for eid in self.get_data():
                check_cycle(self.session, eid, self.rtype)
 
 
@@ -169,30 +168,31 @@
 Reminder
 ~~~~~~~~
 
-Never, ever use the `entity.foo = 42` notation to update an entity. It will not
-work.To updating an entity attribute or relation, uses :meth:`set_attributes` and
+You should never use the `entity.foo = 42` notation to update an
+entity. It will not do what you expect (updating the
+database). Instead, use the :meth:`set_attributes` and
 :meth:`set_relations` methods.
 
 
 How to choose between a before and an after event ?
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-'before_*' hooks give you access to the old attribute (or relation)
-values. You can also hi-jack actually edited stuff in the case of entity
-modification. Needing one of this will definitly guide your choice.
+`before_*` hooks give you access to the old attribute (or relation)
+values. You can also intercept and update edited values in the case of
+entity modification before they reach the database.
 
 Else the question is: should I need to do things before or after the actual
-modification. If the answer is "it doesn't matter", use an 'after' event.
+modification ? If the answer is "it doesn't matter", use an 'after' event.
 
 
 Validation Errors
 ~~~~~~~~~~~~~~~~~
 
-When a hook is responsible to maintain the consistency of the data model detect
-an error, it must use a specific exception named
+When a hook which is responsible to maintain the consistency of the
+data model detects an error, it must use a specific exception named
 :exc:`~cubicweb.ValidationError`. Raising anything but a (subclass of)
-:exc:`~cubicweb.ValidationError` is a programming error. Raising a it entails
-aborting the current transaction.
+:exc:`~cubicweb.ValidationError` is a programming error. Raising it
+entails aborting the current transaction.
 
 This exception is used to convey enough information up to the user
 interface. Hence its constructor is different from the default Exception
@@ -204,6 +204,11 @@
   an end-user facing message (hence properly translated) relating the
   problem.
 
+.. sourcecode:: python
+
+  raise ValidationError(earth.eid, {'sea_level': self._cw._('too high'),
+                                    'temperature': self._cw._('too hot')})
+
 
 Checking for object created/deleted in the current transaction
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -228,7 +233,8 @@
 
 Relations which are defined in the schema as `inlined` (see :ref:`RelationType`
 for details) are inserted in the database at the same time as entity attributes.
-This may have some side effect, for instance when creating entity and setting an
-inlined relation in the same rql query, when 'before_add_relation' for that
-relation will be run, the relation will already exist in the database (it's
-usually not the case).
+
+This may have some side effect, for instance when creating an entity
+and setting an inlined relation in the same rql query, then at
+`before_add_relation` time, the relation will already exist in the
+database (it is otherwise not the case).
--- a/entities/adapters.py	Sat Dec 18 23:12:14 2010 +0100
+++ b/entities/adapters.py	Tue Jan 04 14:11:54 2011 +0100
@@ -484,5 +484,5 @@
     def raise_user_exception(self):
         etype, rtypes = self.exc.args
         msg = self._cw._('violates unique_together constraints (%s)') % (
-            ', '.join(self._cw._(rtypes)))
+            ', '.join([self._cw._(rtype) for rtype in rtypes]))
         raise ValidationError(self.entity.eid, dict((col, msg) for col in rtypes))
--- a/hooks/workflow.py	Sat Dec 18 23:12:14 2010 +0100
+++ b/hooks/workflow.py	Tue Jan 04 14:11:54 2011 +0100
@@ -191,6 +191,8 @@
             msg = session._('mandatory relation')
             raise ValidationError(entity.eid, {qname: msg})
         forentity = session.entity_from_eid(foreid)
+        # see comment in the TrInfo entity definition
+        entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for)
         iworkflowable = forentity.cw_adapt_to('IWorkflowable')
         # then check it has a workflow set, unless we're in the process of changing
         # entity's workflow
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.10.7_Any.py	Tue Jan 04 14:11:54 2011 +0100
@@ -0,0 +1,2 @@
+add_attribute('TrInfo', 'tr_count')
+sync_schema_props_perms('TrInfo')
--- a/schemas/workflow.py	Sat Dec 18 23:12:14 2010 +0100
+++ b/schemas/workflow.py	Tue Jan 04 14:11:54 2011 +0100
@@ -22,7 +22,7 @@
 _ = unicode
 
 from yams.buildobjs import (EntityType, RelationType, SubjectRelation,
-                            RichString, String)
+                            RichString, String, Int)
 from cubicweb.schema import RQLConstraint, RQLUniqueConstraint
 from cubicweb.schemas import (META_ETYPE_PERMS, META_RTYPE_PERMS,
                               HOOKS_RTYPE_PERMS)
@@ -159,13 +159,21 @@
         'delete': (), # XXX should we allow managers to delete TrInfo?
         'update': ('managers', 'owners',),
     }
-
-    from_state = SubjectRelation('State', cardinality='1*')
-    to_state = SubjectRelation('State', cardinality='1*')
+    # The unique_together constraint ensures that 2 repositories
+    # sharing the db won't be able to fire a transition simultaneously
+    # on the same entity tr_count is filled in the FireTransitionHook
+    # to the number of TrInfo attached to the entity on which we
+    # attempt to fire a transition. In other word, it contains the
+    # rank of the TrInfo for that entity, and the constraint says we
+    # cannot have 2 TrInfo with the same rank.
+    __unique_together__ = [('tr_count', 'wf_info_for')]
+    from_state = SubjectRelation('State', cardinality='1*', inlined=True)
+    to_state = SubjectRelation('State', cardinality='1*', inlined=True)
     # make by_transition optional because we want to allow managers to set
     # entity into an arbitrary state without having to respect wf transition
     by_transition = SubjectRelation('BaseTransition', cardinality='?*')
     comment = RichString(fulltextindexed=True)
+    tr_count = Int(description='autocomputed attribute used to ensure transition coherency')
     # get actor and date time using owned_by and creation_date
 
 class from_state(RelationType):
--- a/server/hook.py	Sat Dec 18 23:12:14 2010 +0100
+++ b/server/hook.py	Tue Jan 04 14:11:54 2011 +0100
@@ -105,17 +105,17 @@
 When called for one of these events, hook will have an `entity` attribute
 containing the entity instance.
 
-* 'before_add_entity', 'before_update_entity':
+* `before_add_entity`, `before_update_entity`:
 
   - on those events, you can check what attributes of the entity are modified in
     `entity.cw_edited` (by definition the database is not yet updated in a before
     event)
 
-  - you are allowed to further modify the entity before database operations,
-    using the dictionary notation. By doing this, you'll avoid the need for a
-    whole new rql query processing, the only difference is that the underlying
-    backend query (eg usually sql) will contains the additional data. For
-    example:
+  - you are allowed to further modify the entity before database
+    operations, using the dictionary notation on `cw_edited`. By doing
+    this, you'll avoid the need for a whole new rql query processing,
+    the only difference is that the underlying backend query (eg
+    usually sql) will contains the additional data. For example:
 
     .. sourcecode:: python
 
@@ -136,17 +136,17 @@
     Similarly, removing an attribute from `cw_edited` will cancel its
     modification.
 
-  - on 'before_update_entity' event, you can access to old and new values in
+  - on `before_update_entity` event, you can access to old and new values in
     this hook, by using `entity.cw_edited.oldnewvalue(attr)`
 
 
-* 'after_add_entity', 'after_update_entity'
+* `after_add_entity`, `after_update_entity`
 
   - on those events, you can still check what attributes of the entity are
     modified in `entity.cw_edited` but you can't get anymore the old value, nor
     modify it.
 
-* 'before_delete_entity', 'after_delete_entity'
+* `before_delete_entity`, `after_delete_entity`
 
   - on those events, the entity has no `cw_edited` set.
 
@@ -158,11 +158,11 @@
 attributes containing respectivly the eid of the subject entity, the relation
 type and the eid of the object entity.
 
-* 'before_add_relation', 'before_delete_relation'
+* `before_add_relation`, `before_delete_relation`
 
   - on those events, you can still get original relation by issuing a rql query
 
-* 'after_add_relation', 'after_delete_relation'
+* `after_add_relation`, `after_delete_relation`
 
 This is an occasion to remind us that relations support the add / delete
 operation, but no update.
@@ -171,8 +171,8 @@
 Non data events
 ~~~~~~~~~~~~~~~
 
-Hooks called on server start/maintenance/stop event (eg 'server_startup',
-'server_maintenance', 'server_shutdown') have a `repo` attribute, but *their
+Hooks called on server start/maintenance/stop event (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.
@@ -180,7 +180,7 @@
 Hooks called on backup/restore event (eg 'server_backup', 'server_restore') have
 a `repo` and a `timestamp` attributes, but *their `_cw` attribute is None*.
 
-Hooks called on session event (eg 'session_open', 'session_close') have no
+Hooks called on session event (eg `session_open`, `session_close`) have no
 special attribute.
 
 
@@ -400,7 +400,7 @@
 
     .. Note::
 
-      Do not forget to extend the base class selectors as in ::
+      Do not forget to extend the base class selectors as in:
 
       .. sourcecode:: python
 
@@ -609,7 +609,7 @@
     An operation is triggered on connections pool events related to
     commit / rollback transations. Possible events are:
 
-    * 'precommit':
+    * `precommit`:
 
       the transaction is being prepared for commit. You can freely do any heavy
       computation, raise an exception if the commit can't go. or even add some
@@ -618,13 +618,13 @@
       instance), you'll have to support the 'revertprecommit' event to revert
       things by yourself
 
-    * 'revertprecommit':
+    * `revertprecommit`:
 
       if an operation failed while being pre-commited, this event is triggered
       for all operations which had their 'precommit' event already fired to let
       them revert things (including the operation which made the commit fail)
 
-    * 'rollback':
+    * `rollback`:
 
       the transaction has been either rollbacked either:
 
@@ -632,7 +632,7 @@
        * a 'precommit' event failed, in which case all operations are rollbacked
          once 'revertprecommit'' has been called
 
-    * 'postcommit':
+    * `postcommit`:
 
       the transaction is over. All the ORM entities accessed by the earlier
       transaction are invalid. If you need to work on the database, you need to
@@ -642,7 +642,7 @@
     For an operation to support an event, one has to implement the `<event
     name>_event` method with no arguments.
 
-    Notice order of operations may be important, and is controlled according to
+    The order of operations may be important, and is controlled according to
     the insert_index's method output (whose implementation vary according to the
     base hook class used).
     """
@@ -736,6 +736,7 @@
     `value`, since handling operations becomes costly on massive data import.
 
     Usage looks like:
+
     .. sourcecode:: python
 
         class MyEntityHook(Hook):
--- a/server/msplanner.py	Sat Dec 18 23:12:14 2010 +0100
+++ b/server/msplanner.py	Tue Jan 04 14:11:54 2011 +0100
@@ -856,6 +856,25 @@
                 needsel = set()
                 if not self._sourcesterms:
                     terms += scope.defined_vars.values() + scope.aliases.values()
+                    if isinstance(term, Relation) and len(sources) > 1:
+                        variants = set()
+                        partterms = [term]
+                        for vref in term.get_nodes(VariableRef):
+                            if not vref.variable._q_invariant:
+                                variants.add(vref.name)
+                        if len(variants) == 2:
+                            # we need an extra-step to fetch relations from each source
+                            # before a join with prefetched inputs
+                            # (see test_crossed_relation_noeid_needattr in
+                            #  unittest_msplanner / unittest_multisources)
+                            needsel2 = needsel.copy()
+                            needsel2.update(variants)
+                            lhs, rhs = term.get_variable_parts()
+                            steps.append( (sources, [term, getattr(lhs, 'variable', lhs),
+                                                     getattr(rhs, 'variable', rhs)],
+                                           solindices, scope,
+                                           needsel2, False) )
+                            sources = [self.system_source]
                     final = True
                 else:
                     # suppose this is a final step until the contrary is proven
@@ -1310,8 +1329,8 @@
                 # in this case we have to merge input maps before call to
                 # filter so already processed restriction are correctly
                 # removed
-                solsinputmaps = ppi.merge_input_maps(solindices,
-                                                     complete=not (final and multifinal))
+                solsinputmaps = ppi.merge_input_maps(
+                    solindices, complete=not (final and multifinal))
                 for solindices, inputmap in solsinputmaps:
                     minrqlst, insertedvars = vfilter.filter(
                         sources, terms, scope, set(solindices), needsel, final)
@@ -1328,7 +1347,8 @@
                 minrqlst, insertedvars = vfilter.filter(
                     sources, terms, scope, solindices, needsel, final)
                 if final:
-                    solsinputmaps = ppi.merge_input_maps(solindices)
+                    solsinputmaps = ppi.merge_input_maps(
+                        solindices, complete=not (final and multifinal))
                     if len(solsinputmaps) > 1:
                         refrqlst = minrqlst
                     for solindices, inputmap in solsinputmaps:
@@ -1548,7 +1568,7 @@
 
     def visit_relation(self, node, newroot, terms):
         if not node.is_types_restriction():
-            if node in self.skip and self.solindices.issubset(self.skip[node]):
+            if not node in terms and node in self.skip and self.solindices.issubset(self.skip[node]):
                 if not self.schema.rschema(node.r_type).final:
                     # can't really skip the relation if one variable is selected
                     # and only referenced by this relation
--- a/server/test/data/extern_mapping.py	Sat Dec 18 23:12:14 2010 +0100
+++ b/server/test/data/extern_mapping.py	Tue Jan 04 14:11:54 2011 +0100
@@ -15,8 +15,9 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
+"""mapping file for source used in unittest_multisources.py"""
 
-"""
 support_entities = {'Card': True, 'Affaire': True, 'State': True}
 support_relations = {'in_state': True, 'documented_by': True, 'multisource_inlined_rel': True}
+
+cross_relations = set( ('documented_by',) )
--- a/server/test/unittest_msplanner.py	Sat Dec 18 23:12:14 2010 +0100
+++ b/server/test/unittest_msplanner.py	Tue Jan 04 14:11:54 2011 +0100
@@ -18,6 +18,7 @@
 
 from logilab.common.decorators import clear_cache
 
+from yams.buildobjs import RelationDefinition
 from rql import BadRQLQuery
 
 from cubicweb.devtools import init_test_database
@@ -1607,20 +1608,84 @@
                     ('FetchStep',  [('Any Y,T WHERE Y type T, Y is Note', [{'T': 'String', 'Y': 'Note'}])],
                      [self.cards, self.system], None,
                      {'T': 'table1.C1', 'Y': 'table1.C0', 'Y.type': 'table1.C1'},  []),
-                    ('UnionStep', None,  None,
-                     [('OneFetchStep', [('Any X,Y,T WHERE X multisource_crossed_rel Y, Y type T, X type T, X is Note, Y is Note',
-                                         [{'T': 'String', 'X': 'Note', 'Y': 'Note'}])],
-                       None, None, [self.cards], None,
-                       []),
-                      ('OneFetchStep', [('Any X,Y,T WHERE X multisource_crossed_rel Y, Y type T, X type T, X is Note, Y is Note',
-                                         [{'T': 'String', 'X': 'Note', 'Y': 'Note'}])],
-                       None, None, [self.system],
-                       {'T': 'table1.C1', 'X': 'table0.C0', 'X.type': 'table0.C1',
-                        'Y': 'table1.C0', 'Y.type': 'table1.C1'},
-                       [])]
-                     )],
+                    ('FetchStep', [('Any X,Y WHERE X multisource_crossed_rel Y, X is Note, Y is Note',
+                                    [{'X': 'Note', 'Y': 'Note'}])],
+                     [self.cards, self.system], None,
+                     {'X': 'table2.C0', 'Y': 'table2.C1'},
+                     []),
+                    ('OneFetchStep', [('Any X,Y,T WHERE X multisource_crossed_rel Y, Y type T, X type T, '
+                                       'X is Note, Y is Note, Y identity A, X identity B, A is Note, B is Note',
+                                       [{u'A': 'Note', u'B': 'Note', 'T': 'String', 'X': 'Note', 'Y': 'Note'}])],
+                     None, None,
+                     [self.system],
+                     {'A': 'table1.C0',
+                      'B': 'table0.C0',
+                      'T': 'table1.C1',
+                      'X': 'table2.C0',
+                      'X.type': 'table0.C1',
+                      'Y': 'table2.C1',
+                      'Y.type': 'table1.C1'},
+                     []),
+                    ],
                     {'x': 999999,})
 
+    def test_crossed_relation_noeid_needattr(self):
+        # http://www.cubicweb.org/ticket/1382452
+        self._test('DISTINCT Any DEP WHERE DEP is Note, P type "cubicweb-foo", P multisource_crossed_rel DEP, DEP type LIKE "cubicweb%"',
+                   [('FetchStep', [(u'Any DEP WHERE DEP type LIKE "cubicweb%", DEP is Note',
+                                    [{'DEP': 'Note'}])],
+                     [self.cards, self.system], None,
+                     {'DEP': 'table0.C0'},
+                     []),
+                    ('FetchStep', [(u'Any P WHERE P type "cubicweb-foo", P is Note', [{'P': 'Note'}])],
+                     [self.cards, self.system], None, {'P': 'table1.C0'},
+                     []),
+                    ('FetchStep', [('Any DEP,P WHERE P multisource_crossed_rel DEP, DEP is Note, P is Note',
+                                    [{'DEP': 'Note', 'P': 'Note'}])],
+                     [self.cards, self.system], None, {'DEP': 'table2.C0', 'P': 'table2.C1'},
+                     []),
+                    ('OneFetchStep',
+                     [('DISTINCT Any DEP WHERE P multisource_crossed_rel DEP, DEP is Note, '
+                       'P is Note, DEP identity A, P identity B, A is Note, B is Note',
+                       [{u'A': 'Note', u'B': 'Note', 'DEP': 'Note', 'P': 'Note'}])],
+                     None, None, [self.system],
+                     {'A': 'table0.C0', 'B': 'table1.C0', 'DEP': 'table2.C0', 'P': 'table2.C1'},
+                     [])])
+
+    def test_crossed_relation_noeid_invariant(self):
+        # see comment in http://www.cubicweb.org/ticket/1382452
+        self.schema.add_relation_def(
+            RelationDefinition(subject='Note', name='multisource_crossed_rel', object='Affaire'))
+        self.repo.set_schema(self.schema)
+        try:
+            self._test('DISTINCT Any P,DEP WHERE P type "cubicweb-foo", P multisource_crossed_rel DEP',
+                       [('FetchStep',
+                         [('Any DEP WHERE DEP is Note', [{'DEP': 'Note'}])],
+                         [self.cards, self.system], None, {'DEP': 'table0.C0'}, []),
+                        ('FetchStep',
+                         [(u'Any P WHERE P type "cubicweb-foo", P is Note', [{'P': 'Note'}])],
+                         [self.cards, self.system], None, {'P': 'table1.C0'}, []),
+                        ('UnionStep', None, None,
+                         [('OneFetchStep',
+                           [('DISTINCT Any P,DEP WHERE P multisource_crossed_rel DEP, DEP is Note, P is Note',
+                             [{'DEP': 'Note', 'P': 'Note'}])],
+                           None, None, [self.cards], None, []),
+                          ('OneFetchStep',
+                           [('DISTINCT Any P,DEP WHERE P multisource_crossed_rel DEP, DEP is Note, P is Note',
+                             [{'DEP': 'Note', 'P': 'Note'}])],
+                           None, None, [self.system],
+                           {'DEP': 'table0.C0', 'P': 'table1.C0'},
+                           []),
+                          ('OneFetchStep',
+                           [('DISTINCT Any P,DEP WHERE P multisource_crossed_rel DEP, DEP is Affaire, P is Note',
+                             [{'DEP': 'Affaire', 'P': 'Note'}])],
+                           None, None, [self.system], {'P': 'table1.C0'},
+                           [])])
+                        ])
+        finally:
+            self.schema.del_relation_def('Note', 'multisource_crossed_rel', 'Affaire')
+            self.repo.set_schema(self.schema)
+
     # edition queries tests ###################################################
 
     def test_insert_simplified_var_1(self):
--- a/server/test/unittest_multisources.py	Sat Dec 18 23:12:14 2010 +0100
+++ b/server/test/unittest_multisources.py	Tue Jan 04 14:11:54 2011 +0100
@@ -86,7 +86,9 @@
     TestServerConfiguration.no_sqlite_wrap = False
 
 class TwoSourcesTC(CubicWebTC):
-
+    """Main repo -> extern-multi -> extern
+                  \-------------/
+    """
     @classmethod
     def _refresh_repo(cls):
         super(TwoSourcesTC, cls)._refresh_repo()
@@ -333,6 +335,19 @@
         cu.execute('DELETE Card X WHERE X eid %(x)s', {'x':ceid})
         cnx3.commit()
 
+    def test_crossed_relation_noeid_needattr(self):
+        """http://www.cubicweb.org/ticket/1382452"""
+        aff1 = self.sexecute('INSERT Affaire X: X ref "AFFREF"')[0][0]
+        # link within extern source
+        ec1 = self.sexecute('Card X WHERE X wikiid "zzz"')[0][0]
+        self.sexecute('SET A documented_by C WHERE E eid %(a)s, C eid %(c)s',
+                      {'a': aff1, 'c': ec1})
+        # link from system to extern source
+        self.sexecute('SET A documented_by C WHERE E eid %(a)s, C eid %(c)s',
+                      {'a': aff1, 'c': self.ic2})
+        rset = self.sexecute('DISTINCT Any DEP WHERE P ref "AFFREF", P documented_by DEP, DEP wikiid LIKE "z%"')
+        self.assertEqual(sorted(rset.rows), [[ec1], [self.ic2]])
+
     def test_nonregr1(self):
         ueid = self.session.user.eid
         affaire = self.sexecute('Affaire X WHERE X ref "AFFREF"').get_entity(0, 0)
--- a/view.py	Sat Dec 18 23:12:14 2010 +0100
+++ b/view.py	Tue Jan 04 14:11:54 2011 +0100
@@ -40,6 +40,11 @@
 NOINDEX = u'<meta name="ROBOTS" content="NOINDEX" />'
 NOFOLLOW = u'<meta name="ROBOTS" content="NOFOLLOW" />'
 
+
+CW_XMLNS = '''[
+  <!ATTLIST html xmlns:cubicweb CDATA  #FIXED \'http://www.logilab.org/2008/cubicweb\'  >
+  ]
+'''
 CW_XHTML_EXTENSIONS = '''[
   <!ATTLIST html xmlns:cubicweb CDATA  #FIXED \'http://www.logilab.org/2008/cubicweb\'  >
 
@@ -81,9 +86,9 @@
   "> ] '''
 
 TRANSITIONAL_DOCTYPE = u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" %s>\n' % CW_XHTML_EXTENSIONS
-TRANSITIONAL_DOCTYPE_NOEXT = u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n'
+TRANSITIONAL_DOCTYPE_NOEXT = u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" %s>\n' % CW_XMLNS
 STRICT_DOCTYPE = u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" %s>\n' % CW_XHTML_EXTENSIONS
-STRICT_DOCTYPE_NOEXT = u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
+STRICT_DOCTYPE_NOEXT = u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" %s>\n' % CW_XMLNS
 
 # base view object ############################################################
 
--- a/web/views/reledit.py	Sat Dec 18 23:12:14 2010 +0100
+++ b/web/views/reledit.py	Tue Jan 04 14:11:54 2011 +0100
@@ -78,7 +78,7 @@
         assert rtype
         assert role in ('subject', 'object'), '%s is not an acceptable role value' % role
         self._cw.add_css('cubicweb.form.css')
-        self._cw.add_js('cubicweb.reledit.js', 'cubicweb.edition.js')
+        self._cw.add_js(('cubicweb.reledit.js', 'cubicweb.edition.js', 'cubicweb.ajax.js'))
         entity = self.cw_rset.get_entity(row, col)
         rschema = self._cw.vreg.schema[rtype]
         self._rules = rctrl.etype_get(entity.e_schema.type, rschema.type, role, '*')