[itree] Implement moving a child entities at a given index
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 11 Dec 2017 14:11:50 +0100
changeset 2908 e08c4e3177f5
parent 2907 45a1b86c2805
child 2909 7042ff92abcf
[itree] Implement moving a child entities at a given index this is only unit-tested for now, it will be actually used by later commits.
cubicweb_seda/entities/itree.py
test/test_entities.py
--- a/cubicweb_seda/entities/itree.py	Mon Dec 11 14:03:16 2017 +0100
+++ b/cubicweb_seda/entities/itree.py	Mon Dec 11 14:11:50 2017 +0100
@@ -149,6 +149,29 @@
         return None
 
 
+# Tree ordering management
+# ------------------------
+#
+# when several child entities of the same types are allowed, we want to control
+# their relative order. This is done using the `ordering` attribute of the child
+# entities.
+#
+# Code below handle moving nodes among the tree. As such it has to handle this
+# attribute so that:
+#
+# * move may control order
+#
+# * reparenting will keep consistent order
+#
+# In order to minimize the number of queries and to keep code clearer (avoid
+# transmitting ordering number through json among other), I've (syt) choosen
+# that `ordering` attribute's value:
+#
+# * starts at `1` for the first element,
+#
+# * is a sequential list, i.e. `2` for the 2nd child, `3` for the 3rd child, and
+#   up to `len(children)` for the last one.
+
 ETYPE_PARENT_RTYPE = dict(MULTIPLE_CHILDREN)
 
 
@@ -161,6 +184,38 @@
     return 1 if ordering is None else ordering + 1
 
 
+def move_child_at_index(cnx, parent_eid, child_eid, index, reparenting=False):
+    """Given a parent node and one of its child, move it at `index` position (0 for
+    the first position, and `len(children)` for the last).
+
+    `reparenting` should be true when the child has just been appended, hence
+    it's previous ordering should not be considered.
+    """
+    child = cnx.entity_from_eid(child_eid)
+    assert reparenting or child.ordering not in (index, index + 1), \
+        "moving a child before or after itself doesn't make sense"
+    rtype = ETYPE_PARENT_RTYPE[child.cw_etype]
+    if reparenting:
+        rql = ('SET X ordering XO + 1 WHERE X ordering XO, '
+               'X ordering > %(index)s, '
+               'X {rtype} P, P eid %(p)s'.format(rtype=rtype))
+        order = index + 1
+    elif child.ordering > index:
+        rql = ('SET X ordering XO + 1 WHERE X ordering XO, '
+               'X ordering > %(index)s, X ordering < %(porder)s, '
+               'X {rtype} P, P eid %(p)s'.format(rtype=rtype))
+        order = index + 1
+    else:
+        rql = ('SET X ordering XO - 1 WHERE X ordering XO, '
+               'X ordering <= %(index)s, X ordering > %(porder)s, '
+               'X {rtype} P, P eid %(p)s'.format(rtype=rtype))
+        order = index
+    cnx.execute(rql, {'index': index,
+                      'porder': None if reparenting else child.ordering,
+                      'p': parent_eid}).rows
+    child.cw_set(ordering=order)
+
+
 def reparent(entity, new_parent_eid):
     """Move `entity` as a children of `new_parent_eid`.
     """
--- a/test/test_entities.py	Mon Dec 11 14:03:16 2017 +0100
+++ b/test/test_entities.py	Mon Dec 11 14:11:50 2017 +0100
@@ -26,7 +26,7 @@
 
 from cubicweb_seda.entities import (seda_profile_container_def, simplified_profile,
                                     full_seda2_profile, parent_and_container,
-                                    rule_type_from_etype, custom)
+                                    rule_type_from_etype, custom, itree)
 
 from cubicweb_seda.testutils import create_archive_unit, create_data_object
 
@@ -277,6 +277,41 @@
             self.assertChildren(transfer, [bdo2.eid, bdo.eid, au2.eid, au.eid])
 
 
+class ReorderTC(CubicWebTC):
+
+    def test_reorder_base(self):
+        with self.admin_access.cnx() as cnx:
+            transfer = cnx.create_entity('SEDAArchiveTransfer', title=u'Test')
+            bdo1 = create_data_object(transfer)
+            bdo2 = create_data_object(transfer)
+            bdo3 = create_data_object(transfer)
+            cnx.commit()
+
+            itree.move_child_at_index(cnx, transfer.eid, bdo3.eid, 0)
+            transfer.cw_clear_all_caches()
+            self.assertEqual([(x.eid, x.ordering)
+                              for x in transfer.cw_adapt_to('ITreeBase').iterchildren()],
+                             [(bdo3.eid, 1), (bdo1.eid, 2), (bdo2.eid, 3)])
+
+            itree.move_child_at_index(cnx, transfer.eid, bdo3.eid, 3)
+            transfer.cw_clear_all_caches()
+            self.assertEqual([(x.eid, x.ordering)
+                              for x in transfer.cw_adapt_to('ITreeBase').iterchildren()],
+                             [(bdo1.eid, 1), (bdo2.eid, 2), (bdo3.eid, 3)])
+
+            itree.move_child_at_index(cnx, transfer.eid, bdo1.eid, 2)
+            transfer.cw_clear_all_caches()
+            self.assertEqual([(x.eid, x.ordering)
+                              for x in transfer.cw_adapt_to('ITreeBase').iterchildren()],
+                             [(bdo2.eid, 1), (bdo1.eid, 2), (bdo3.eid, 3)])
+
+            itree.move_child_at_index(cnx, transfer.eid, bdo3.eid, 1)
+            transfer.cw_clear_all_caches()
+            self.assertEqual([(x.eid, x.ordering)
+                              for x in transfer.cw_adapt_to('ITreeBase').iterchildren()],
+                             [(bdo2.eid, 1), (bdo3.eid, 2), (bdo1.eid, 3)])
+
+
 class RuleFromETypeTC(unittest.TestCase):
     def test_rule_from_etype(self):
         for rule_type in ('access', 'appraisal', 'classification',