Skip to content
Snippets Groups Projects
Commit 7b3cd8f6 authored by Timm Fitschen's avatar Timm Fitschen Committed by Quazgar
Browse files

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.
parent 1ce56524
No related branches found
No related tags found
No related merge requests found
...@@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### ### 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 ### ### Changed ###
......
...@@ -42,11 +42,19 @@ class Version(): ...@@ -42,11 +42,19 @@ class Version():
id : str, optional id : str, optional
See attribute `id`. Default: None See attribute `id`. Default: None
date : str, optional 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 predecessors : list of Version, optional
See attribute `predecessors`. Default: empty list. See attribute `predecessors`. Default: empty list.
successors : list of Version, optional successors : list of Version, optional
See attribute `successors`. Default: empty list. 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 Attributes
---------- ----------
...@@ -55,6 +63,10 @@ class Version(): ...@@ -55,6 +63,10 @@ class Version():
date : str date : str
UTC Timestamp of the version, i.e. the date and time when the entity of UTC Timestamp of the version, i.e. the date and time when the entity of
this version has been inserted or modified. 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 : list of Version
Predecessors are the older entity versions which have been modified Predecessors are the older entity versions which have been modified
into this version. Usually, there is only one predecessor. However, into this version. Usually, there is only one predecessor. However,
...@@ -66,14 +78,64 @@ class Version(): ...@@ -66,14 +78,64 @@ class Version():
is only one successor. However, this API allows that a single entity 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 may co-exist in several versions (e.g. several proposals for the next
entity status). That would result in more than one successor. 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 # 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.id = id
self.date = date self.date = date
self.username = username
self.realm = realm
self.predecessors = predecessors if predecessors is not None else [] self.predecessors = predecessors if predecessors is not None else []
self.successors = successors if successors 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"): def to_xml(self, tag="Version"):
"""Serialize this version to xml. """Serialize this version to xml.
...@@ -99,9 +161,15 @@ class Version(): ...@@ -99,9 +161,15 @@ class Version():
xml.set("id", self.id) xml.set("id", self.id)
if self.date is not None: if self.date is not None:
xml.set("date", self.date) 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: if self.predecessors is not None:
for p in self.predecessors: for p in self.predecessors:
xml.append(p.to_xml(tag="Predecessor")) xml.append(p.to_xml(tag="Predecessor"))
if self.is_head is True:
xml.set("head", "true")
if self.successors is not None: if self.successors is not None:
for s in self.successors: for s in self.successors:
xml.append(s.to_xml(tag="Successor")) xml.append(s.to_xml(tag="Successor"))
...@@ -122,8 +190,9 @@ class Version(): ...@@ -122,8 +190,9 @@ class Version():
Parameters Parameters
---------- ----------
xml : etree.Element xml : etree.Element
A 'Version' xml element, with 'id' and 'date' attributes and A 'Version' xml element, with 'id', possibly 'date', `username`,
'Predecessor' and 'Successor' child elements. `realm`, and `head` attributes as well as 'Predecessor' and
'Successor' child elements.
Returns Returns
------- -------
...@@ -133,6 +202,9 @@ class Version(): ...@@ -133,6 +202,9 @@ class Version():
predecessors = [Version.from_xml(p) for p in xml if p.tag.lower() == "predecessor"] 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"] successors = [Version.from_xml(s) for s in xml if s.tag.lower() == "successor"]
return Version(id=xml.get("id"), date=xml.get("date"), 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) predecessors=predecessors, successors=successors)
def __hash__(self): def __hash__(self):
......
...@@ -27,16 +27,21 @@ from caosdb import Record ...@@ -27,16 +27,21 @@ from caosdb import Record
from caosdb.common.utils import xml2str from caosdb.common.utils import xml2str
from caosdb.common.versioning import Version from caosdb.common.versioning import Version
from .test_property import testrecord from .test_property import testrecord
from lxml import etree
def test_constructor(): def test_constructor():
v = Version(id="1234abcd", date="2020-01-01T20:15:00.000UTC", v = Version(id="1234abcd", date="2020-01-01T20:15:00.000UTC",
username="testuser", realm="CaosDB", is_head=True,
predecessors=[Version(id="2345abdc", predecessors=[Version(id="2345abdc",
date="2020-01-01T20:00:00.000UTC")], date="2020-01-01T20:00:00.000UTC")],
successors=[Version(id="3465abdc", successors=[Version(id="3465abdc",
date="2020-01-01T20:30:00.000UTC")]) date="2020-01-01T20:30:00.000UTC")])
assert v.id == "1234abcd" assert v.id == "1234abcd"
assert v.date == "2020-01-01T20:15:00.000UTC" 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, list)
assert isinstance(v.predecessors[0], Version) assert isinstance(v.predecessors[0], Version)
assert isinstance(v.successors, list) assert isinstance(v.successors, list)
...@@ -48,21 +53,27 @@ def test_constructor(): ...@@ -48,21 +53,27 @@ def test_constructor():
def test_to_xml(): def test_to_xml():
v = test_constructor() v = test_constructor()
xmlstr = xml2str(v.to_xml()) 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' ' <Predecessor id="{pi}" date="{pd}"/>\n'
' <Successor id="{si}" date="{sd}"/>\n' ' <Successor id="{si}" date="{sd}"/>\n'
'</Version>\n').format(i=v.id, d=v.date, '</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, pi=v.predecessors[0].id,
pd=v.predecessors[0].date, pd=v.predecessors[0].date,
si=v.successors[0].id, si=v.successors[0].id,
sd=v.successors[0].date) sd=v.successors[0].date)
xmlstr2 = xml2str(v.to_xml(tag="OtherVersionTag")) 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' ' <Predecessor id="{pi}" date="{pd}"/>\n'
' <Successor id="{si}" date="{sd}"/>\n' ' <Successor id="{si}" date="{sd}"/>\n'
'</OtherVersionTag>\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, pd=v.predecessors[0].date,
si=v.successors[0].id, sd=v.successors[0].date) si=v.successors[0].id, sd=v.successors[0].date)
...@@ -142,3 +153,30 @@ def test_version_serialization(): ...@@ -142,3 +153,30 @@ def test_version_serialization():
# <Record><Version id="test-version" date="asdfsadf"/></Record> # <Record><Version id="test-version" date="asdfsadf"/></Record>
assert "test-version" == r.to_xml().xpath("/Record/Version/@id")[0] assert "test-version" == r.to_xml().xpath("/Record/Version/@id")[0]
assert "asdfsadf" == r.to_xml().xpath("/Record/Version/@date")[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)]
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment