summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2015-03-24 20:08:12 (GMT)
committerThomas Bruederli <bruederli@kolabsys.com>2015-03-24 20:08:12 (GMT)
commitaa3b0f23dccfb8c8e52fc6f27fb7b99bad42a0b5 (patch)
treee33a9121e4a53ffd66f49fb8c6f94590e4b206a2
parentd222679e9db6abf76578f9b6c5e4ffb96464495c (diff)
downloadpykolab-aa3b0f23dccfb8c8e52fc6f27fb7b99bad42a0b5.tar.gz
Improve object diff computation: ignore order of attribute lists (e.g. attachments, attendees)
-rw-r--r--pykolab/xml/utils.py48
-rw-r--r--tests/unit/test-017-diff.py199
2 files changed, 243 insertions, 4 deletions
diff --git a/pykolab/xml/utils.py b/pykolab/xml/utils.py
index c92c52f..261e33c 100644
--- a/pykolab/xml/utils.py
+++ b/pykolab/xml/utils.py
@@ -253,16 +253,15 @@ def compute_diff(a, b, reduced=False):
aa = [aa]
if not isinstance(bb, list):
bb = [bb]
+
+ (aa, bb) = order_proplists(aa, bb)
index = 0
length = max(len(aa), len(bb))
while index < length:
aai = aa[index] if index < len(aa) else None
bbi = bb[index] if index < len(bb) else None
if not compare_values(aai, bbi):
- if reduced:
- (old, new) = reduce_properties(aai, bbi)
- else:
- (old, new) = (aai, bbi)
+ (old, new) = reduce_properties(aai, bbi) if reduced else (aai, bbi)
diff.append(OrderedDict([('property', prop), ('index', index), ('old', old), ('new', new)]))
index += 1
@@ -277,6 +276,47 @@ def compute_diff(a, b, reduced=False):
return diff
+def order_proplists(a, b):
+ """
+ Orders two lists so that equal entries have the same position
+ """
+ # nothing to be done here
+ if len(a) == 0 and len(b) == 0:
+ return (a, b)
+
+ base = a
+ comp = b
+ flip = False
+
+ if len(a) > len(b):
+ flip = True
+ base = b
+ comp = a
+
+ indices = []
+ top = len(comp) + 1
+ for bb in comp:
+ index = None
+
+ # find a matching entry in base
+ for j, aa in enumerate(base):
+ if compare_values(aa, bb):
+ index = j
+ break
+
+ # move non-matching items to the end of the list
+ if index is None:
+ index = top
+ top += 1
+
+ indices.append(index)
+
+ # do sort by indices
+ indices, comp = zip(*sorted(zip(indices, comp), key=lambda x: x[0]))
+
+ return (comp, base) if flip else (base, comp)
+
+
def compare_values(aa, bb):
ignore_keys = ['rsvp']
if not aa.__class__ == bb.__class__:
diff --git a/tests/unit/test-017-diff.py b/tests/unit/test-017-diff.py
new file mode 100644
index 0000000..6c0a585
--- /dev/null
+++ b/tests/unit/test-017-diff.py
@@ -0,0 +1,199 @@
+import unittest
+
+from pykolab.xml import Todo
+from pykolab.xml.utils import compute_diff
+from pykolab.xml.utils import order_proplists
+
+xml_todo_01 = """<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
+<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0">
+ <vcalendar>
+ <properties>
+ <prodid>
+ <text>Roundcube-libkolab-1.1 Libkolabxml-1.1</text>
+ </prodid>
+ <version>
+ <text>2.0</text>
+ </version>
+ <x-kolab-version>
+ <text>3.1.0</text>
+ </x-kolab-version>
+ </properties>
+ <components>
+ <vtodo>
+ <properties>
+ <uid>
+ <text>E49000485B9EE3299A8870AD6A3E75E5-A4BF5BBB9FEAA271</text>
+ </uid>
+ <created>
+ <date-time>2015-03-25T10:32:18Z</date-time>
+ </created>
+ <dtstamp>
+ <date-time>2015-03-25T16:09:11Z</date-time>
+ </dtstamp>
+ <sequence>
+ <integer>0</integer>
+ </sequence>
+ <summary>
+ <text>Old attachments</text>
+ </summary>
+ <attach>
+ <parameters>
+ <fmttype>
+ <text>image/png</text>
+ </fmttype>
+ <x-label>
+ <text>silhouette.png</text>
+ </x-label>
+ </parameters>
+ <uri>cid:silhouette.1427297477.7514.png</uri>
+ </attach>
+ <attach>
+ <parameters>
+ <fmttype>
+ <text>text/plain</text>
+ </fmttype>
+ <x-label>
+ <text>notes.txt</text>
+ </x-label>
+ </parameters>
+ <uri>cid:notes.1427298885.9012.txt</uri>
+ </attach>
+ <attach>
+ <parameters>
+ <fmttype>
+ <text>image/gif</text>
+ </fmttype>
+ <x-label>
+ <text>landspeeder-lego-icon.gif</text>
+ </x-label>
+ </parameters>
+ <uri>cid:landspeeder-lego-icon.1427299751.8542.gif</uri>
+ </attach>
+ </properties>
+ </vtodo>
+ </components>
+ </vcalendar>
+</icalendar>
+"""
+
+xml_todo_02 = """<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
+<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0">
+ <vcalendar>
+ <properties>
+ <prodid>
+ <text>Roundcube-libkolab-1.1 Libkolabxml-1.1</text>
+ </prodid>
+ <version>
+ <text>2.0</text>
+ </version>
+ <x-kolab-version>
+ <text>3.1.0</text>
+ </x-kolab-version>
+ </properties>
+ <components>
+ <vtodo>
+ <properties>
+ <uid>
+ <text>E49000485B9EE3299A8870AD6A3E75E5-A4BF5BBB9FEAA271</text>
+ </uid>
+ <created>
+ <date-time>2015-03-25T10:32:18Z</date-time>
+ </created>
+ <dtstamp>
+ <date-time>2015-03-25T16:09:53Z</date-time>
+ </dtstamp>
+ <sequence>
+ <integer>1</integer>
+ </sequence>
+ <summary>
+ <text>New attachments</text>
+ </summary>
+ <description>
+ <text>Removed attachment</text>
+ </description>
+ <attach>
+ <parameters>
+ <fmttype>
+ <text>text/plain</text>
+ </fmttype>
+ <x-label>
+ <text>notes.txt</text>
+ </x-label>
+ </parameters>
+ <uri>cid:notes.1427298885.9012.txt</uri>
+ </attach>
+ <attach>
+ <parameters>
+ <fmttype>
+ <text>image/gif</text>
+ </fmttype>
+ <x-label>
+ <text>landspeeder-lego-icon.gif</text>
+ </x-label>
+ </parameters>
+ <uri>cid:landspeeder-lego-icon.1427299751.8542.gif</uri>
+ </attach>
+ </properties>
+ </vtodo>
+ </components>
+ </vcalendar>
+</icalendar>
+"""
+
+class TestComputeDiff(unittest.TestCase):
+
+ def test_000_order_proplists(self):
+ one = {
+ "uri": "cid:one",
+ "label": "one.txt"
+ }
+ two = {
+ "uri": "cid:two",
+ "label": "two.png"
+ }
+ three = {
+ "foo": "three"
+ }
+ four = {
+ "foo": "four"
+ }
+
+ (aa, bb) = order_proplists([one, two, four], [three, two, one, four])
+ self.assertEqual(len(aa), 3)
+ self.assertEqual(len(bb), 4)
+ self.assertEqual(aa[0], bb[0])
+ self.assertEqual(aa[1], bb[1])
+
+ (aa, bb) = order_proplists([four, three, one, two], [two, one])
+ self.assertEqual(len(aa), 4)
+ self.assertEqual(len(bb), 2)
+ self.assertEqual(aa[0], bb[0])
+ self.assertEqual(aa[1], bb[1])
+
+
+ def test_001_attachments(self):
+ old = Todo(from_string=xml_todo_01)
+ new = Todo(from_string=xml_todo_02)
+ diff = compute_diff(old.to_dict(), new.to_dict())
+
+ self.assertEqual(len(diff), 5)
+ self.assertEqual(diff[0]['property'], 'sequence')
+ self.assertEqual(diff[0]['old'], 0)
+ self.assertEqual(diff[0]['new'], 1)
+
+ self.assertEqual(diff[1]['property'], 'description')
+ self.assertEqual(diff[1]['old'], '')
+
+ self.assertEqual(diff[2]['property'], 'summary')
+ self.assertEqual(diff[2]['old'], 'Old attachments')
+ self.assertEqual(diff[2]['new'], 'New attachments')
+
+ self.assertEqual(diff[3]['property'], 'attach')
+ self.assertEqual(diff[3]['new'], None)
+ self.assertEqual(diff[3]['old']['uri'], "cid:silhouette.1427297477.7514.png")
+
+ self.assertEqual(diff[4]['property'], 'lastmodified-date')
+
+
+if __name__ == '__main__':
+ unittest.main() \ No newline at end of file