From 7b3cd8f65d52e3fbf071cf5d71a261b30b7588aa Mon Sep 17 00:00:00 2001 From: Timm Fitschen <t.fitschen@indiscale.com> Date: Fri, 27 Nov 2020 09:45:42 +0000 Subject: [PATCH] Add more functionality and properties to the Version class (get_history()). Currently, the main purpose is automatic testing for the server functionality which is used by the webui. --- CHANGELOG.md | 4 +- src/caosdb/common/versioning.py | 80 +++++++++++++++++++++++++++++++-- unittests/test_versioning.py | 44 ++++++++++++++++-- 3 files changed, 120 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6d7d84f..7ba0d8c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### -* Versioning support (experimental). +* Versioning support (experimental). The version db.Version class can + represents particular entity versions and also the complete history of an + entity. ### Changed ### diff --git a/src/caosdb/common/versioning.py b/src/caosdb/common/versioning.py index bac99b73..2875486a 100644 --- a/src/caosdb/common/versioning.py +++ b/src/caosdb/common/versioning.py @@ -42,11 +42,19 @@ class Version(): id : str, optional See attribute `id`. Default: None date : str, optional - See attribute `data`. Default: None + See attribute `date`. Default: None + username : str, optional + See attribute `username`. Default: None + realm : str, optional + See attribute `realm`. Default: None predecessors : list of Version, optional See attribute `predecessors`. Default: empty list. successors : list of Version, optional See attribute `successors`. Default: empty list. + is_head : bool + See attribute `is_head`. Default: False + is_complete_history : bool + See attribute `is_complete_history`. Default: False Attributes ---------- @@ -55,6 +63,10 @@ class Version(): date : str UTC Timestamp of the version, i.e. the date and time when the entity of this version has been inserted or modified. + username : str + The username of the user who inserted or updated this version. + realm : str + The realm of the user who inserted or updated this version. predecessors : list of Version Predecessors are the older entity versions which have been modified into this version. Usually, there is only one predecessor. However, @@ -66,14 +78,64 @@ class Version(): is only one successor. However, this API allows that a single entity may co-exist in several versions (e.g. several proposals for the next entity status). That would result in more than one successor. + is_head : bool or string + If true, this indicates that this version is the HEAD if true. + Otherwise it is not known whether this is the head or not. Any string + matching "true" (case-insensitively) is regarded as True. + Nota bene: This property should typically be set if the server response + indicated that this is the head version. + is_complete_history : bool or string + If true, this indicates that this version contains the full version + history. That means, that the predecessors and successors have their + respective predecessors and successors attached as well and the tree is + completely available. Any string matching "true" (case-insensitively) + is regarded as True. + Nota bene: This property should typically be set if the server response + indicated that the full version history is included in its response. """ # pylint: disable=redefined-builtin - def __init__(self, id=None, date=None, predecessors=None, successors=None): + def __init__(self, id=None, date=None, username=None, realm=None, + predecessors=None, successors=None, is_head=False, + is_complete_history=False): + """Typically the `predecessors` or `successors` should not "link back" to an existing Version +object.""" self.id = id self.date = date + self.username = username + self.realm = realm self.predecessors = predecessors if predecessors is not None else [] self.successors = successors if successors is not None else [] + self.is_head = str(is_head).lower() == "true" + self.is_complete_history = str(is_complete_history).lower() == "true" + + def get_history(self): + """ Returns a flat list of Version instances representing the history + of the entity. + + The list items are ordered by the relation between the versions, + starting with the oldest version. + + The items in the list have no predecessors or successors attached. + + Note: This method only returns reliable results if + `self.is_complete_history is True` and it will not retrieve the full + version history if it is not present. + + Returns + ------- + list of Version + """ + versions = [] + for p in self.predecessors: + # assuming that predecessors don't have any successors + versions = p.get_history() + versions.append(Version(id=self.id, date=self.date, + username=self.username, realm=self.realm)) + for s in self.successors: + # assuming that successors don't have any predecessors + versions.extend(s.get_history()) + return versions def to_xml(self, tag="Version"): """Serialize this version to xml. @@ -99,9 +161,15 @@ class Version(): xml.set("id", self.id) if self.date is not None: xml.set("date", self.date) + if self.username is not None: + xml.set("username", self.username) + if self.realm is not None: + xml.set("realm", self.realm) if self.predecessors is not None: for p in self.predecessors: xml.append(p.to_xml(tag="Predecessor")) + if self.is_head is True: + xml.set("head", "true") if self.successors is not None: for s in self.successors: xml.append(s.to_xml(tag="Successor")) @@ -122,8 +190,9 @@ class Version(): Parameters ---------- xml : etree.Element - A 'Version' xml element, with 'id' and 'date' attributes and - 'Predecessor' and 'Successor' child elements. + A 'Version' xml element, with 'id', possibly 'date', `username`, + `realm`, and `head` attributes as well as 'Predecessor' and + 'Successor' child elements. Returns ------- @@ -133,6 +202,9 @@ class Version(): predecessors = [Version.from_xml(p) for p in xml if p.tag.lower() == "predecessor"] successors = [Version.from_xml(s) for s in xml if s.tag.lower() == "successor"] return Version(id=xml.get("id"), date=xml.get("date"), + is_head=xml.get("head"), + is_complete_history=xml.get("completeHistory"), + username=xml.get("username"), realm=xml.get("realm"), predecessors=predecessors, successors=successors) def __hash__(self): diff --git a/unittests/test_versioning.py b/unittests/test_versioning.py index d6009e79..5047069c 100644 --- a/unittests/test_versioning.py +++ b/unittests/test_versioning.py @@ -27,16 +27,21 @@ from caosdb import Record from caosdb.common.utils import xml2str from caosdb.common.versioning import Version from .test_property import testrecord +from lxml import etree def test_constructor(): v = Version(id="1234abcd", date="2020-01-01T20:15:00.000UTC", + username="testuser", realm="CaosDB", is_head=True, predecessors=[Version(id="2345abdc", date="2020-01-01T20:00:00.000UTC")], successors=[Version(id="3465abdc", date="2020-01-01T20:30:00.000UTC")]) assert v.id == "1234abcd" assert v.date == "2020-01-01T20:15:00.000UTC" + assert v.username == "testuser" + assert v.realm == "CaosDB" + assert v.is_head is True assert isinstance(v.predecessors, list) assert isinstance(v.predecessors[0], Version) assert isinstance(v.successors, list) @@ -48,21 +53,27 @@ def test_constructor(): def test_to_xml(): v = test_constructor() xmlstr = xml2str(v.to_xml()) - assert xmlstr == ('<Version id="{i}" date="{d}">\n' + assert xmlstr == ('<Version id="{i}" date="{d}" username="{u}" realm="{r}"' + ' head="{h}">\n' ' <Predecessor id="{pi}" date="{pd}"/>\n' ' <Successor id="{si}" date="{sd}"/>\n' '</Version>\n').format(i=v.id, d=v.date, + u=v.username, r=v.realm, + h=str(v.is_head).lower(), pi=v.predecessors[0].id, pd=v.predecessors[0].date, si=v.successors[0].id, sd=v.successors[0].date) xmlstr2 = xml2str(v.to_xml(tag="OtherVersionTag")) - assert xmlstr2 == ('<OtherVersionTag id="{i}" date="{d}">\n' + assert xmlstr2 == ('<OtherVersionTag id="{i}" date="{d}" username="{u}" ' + 'realm="{r}" head="{h}">\n' ' <Predecessor id="{pi}" date="{pd}"/>\n' ' <Successor id="{si}" date="{sd}"/>\n' '</OtherVersionTag>\n' - ).format(i=v.id, d=v.date, pi=v.predecessors[0].id, + ).format(i=v.id, d=v.date, u=v.username, r=v.realm, + h=str(v.is_head).lower(), + pi=v.predecessors[0].id, pd=v.predecessors[0].date, si=v.successors[0].id, sd=v.successors[0].date) @@ -142,3 +153,30 @@ def test_version_serialization(): # <Record><Version id="test-version" date="asdfsadf"/></Record> assert "test-version" == r.to_xml().xpath("/Record/Version/@id")[0] assert "asdfsadf" == r.to_xml().xpath("/Record/Version/@date")[0] + + +def test_get_history(): + xml_str = """ + <Version id="vid6" username="user1" realm="Realm1" date="date6" completeHistory="true"> + <Predecessor id="vid5" username="user1" realm="Realm1" date="date5"> + <Predecessor id="vid4" username="user1" realm="Realm1" date="date4"> + <Predecessor id="vid3" username="user1" realm="Realm1" date="date3"> + <Predecessor id="vid2" username="user1" realm="Realm1" date="date2"> + <Predecessor id="vid1" username="user1" realm="Realm1" date="date1" /> + </Predecessor> + </Predecessor> + </Predecessor> + </Predecessor> + <Successor id="vid7" username="user1" realm="Realm1" date="date7"> + <Successor id="vid8" username="user1" realm="Realm1" date="date8"> + <Successor id="vid9" username="user1" realm="Realm1" date="date9"> + <Successor id="vid10" username="user1" realm="Realm1" date="date10" /> + </Successor> + </Successor> + </Successor> + </Version>""" + version = Version.from_xml(etree.fromstring(xml_str)) + assert version.is_complete_history is True + assert version.get_history() == [Version(id=f"vid{i+1}", date=f"date{i+1}", + username="user1", realm="Realm1") + for i in range(10)] -- GitLab