From f8861d5e9210ce4af8c5571aa068b95c7757f49c Mon Sep 17 00:00:00 2001
From: Timm Fitschen <t.fitschen@indiscale.com>
Date: Tue, 17 Nov 2020 01:32:41 +0100
Subject: [PATCH] WIP: FSM

---
 src/caosdb/__init__.py      |  1 +
 src/caosdb/common/models.py | 16 ++++++++++++++--
 src/caosdb/common/state.py  | 33 +++++++++++++++++++++++++++++++++
 unittests/test_state.py     | 27 +++++++++++++++++++++++++++
 4 files changed, 75 insertions(+), 2 deletions(-)
 create mode 100644 src/caosdb/common/state.py
 create mode 100644 unittests/test_state.py

diff --git a/src/caosdb/__init__.py b/src/caosdb/__init__.py
index 75847bb1..d59c60db 100644
--- a/src/caosdb/__init__.py
+++ b/src/caosdb/__init__.py
@@ -31,6 +31,7 @@ import caosdb.apiutils
 from caosdb.common import administration
 from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER,
                                     REFERENCE, TEXT, LIST)
+from caosdb.common.state import State
 # Import of the basic  API classes:
 from caosdb.common.models import (ACL, ALL, FIX, NONE, OBLIGATORY, RECOMMENDED,
                                   SUGGESTED, Container, DropOffBox, Entity,
diff --git a/src/caosdb/common/models.py b/src/caosdb/common/models.py
index a5adf6af..eb625610 100644
--- a/src/caosdb/common/models.py
+++ b/src/caosdb/common/models.py
@@ -43,6 +43,7 @@ from warnings import warn
 from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER,
                                     LIST, REFERENCE, TEXT, is_reference)
 from caosdb.common.versioning import Version
+from caosdb.common.state import State
 from caosdb.common.utils import uuid, xml2str
 from caosdb.configuration import get_config
 from caosdb.connection.connection import get_connection
@@ -109,6 +110,7 @@ class Entity(object):
         self.name = name
         self.description = description
         self.id = id
+        self.state = None
 
     @property
     def version(self):
@@ -907,6 +909,9 @@ class Entity(object):
         if self.acl is not None:
             xml.append(self.acl.to_xml())
 
+        if self.state is not None:
+            xml.append(self.state.to_xml())
+
         return xml
 
     @staticmethod
@@ -951,6 +956,8 @@ class Entity(object):
                 entity.add_message(child)
             elif isinstance(child, Version):
                 entity.version = child
+            elif isinstance(child, State):
+                entity.state = child
             elif child is None or hasattr(child, "encode"):
                 vals.append(child)
             elif isinstance(child, Entity):
@@ -1070,7 +1077,7 @@ class Entity(object):
             flags=flags)[0]
 
     def update(self, strict=False, raise_exception_on_error=True,
-               unique=True, flags=None):
+               unique=True, flags=None, sync=True):
         """Update this entity.
 
         There are two possible work-flows to perform this update:
@@ -1104,6 +1111,7 @@ class Entity(object):
 
         return Container().append(self).update(
             strict=strict,
+            sync=sync,
             raise_exception_on_error=raise_exception_on_error,
             unique=unique,
             flags=flags)[0]
@@ -1239,6 +1247,7 @@ class QueryTemplate():
         self.is_valid = lambda: False
         self.is_deleted = lambda: False
         self.version = None
+        self.state = None
 
     def retrieve(self, strict=True, raise_exception_on_error=True,
                  unique=True, sync=True, flags=None):
@@ -2215,6 +2224,7 @@ def _basic_sync(e_local, e_remote):
     e_local.is_valid = e_remote.is_valid
     e_local.is_deleted = e_remote.is_deleted
     e_local.version = e_remote.version
+    e_local.state = e_remote.state
 
     if hasattr(e_remote, "query"):
         e_local.query = e_remote.query
@@ -2828,7 +2838,7 @@ class Container(list):
         entity_url_segments = [_ENTITY_URI_SEGMENT, "&".join(id_str)]
 
         _log_request("DELETE: " + str(entity_url_segments) +
-                     ("?" + flags if flags is not None else ''))
+                     ("?" + str(flags) if flags is not None else ''))
 
         http_response = c.delete(entity_url_segments, query_dict=flags)
         cresp = Container._response_to_entities(http_response)
@@ -3850,6 +3860,8 @@ def _parse_single_xml_element(elem):
         return entity
     elif elem.tag.lower() == "version":
         return Version.from_xml(elem)
+    elif elem.tag.lower() == "state":
+        return State.from_xml(elem)
     elif elem.tag.lower() == "emptystring":
         return ""
     elif elem.tag.lower() == "value":
diff --git a/src/caosdb/common/state.py b/src/caosdb/common/state.py
new file mode 100644
index 00000000..431675fb
--- /dev/null
+++ b/src/caosdb/common/state.py
@@ -0,0 +1,33 @@
+from lxml import etree
+class State:
+
+    def __init__(self, model, name):
+        self.name = name
+        self.model = model
+
+    def __eq__(self, other):
+        return (other is not None
+                and hasattr(other, "model")
+                and hasattr(other, "name")
+                and self.name == other.name
+                and self.model == other.model)
+
+    def __hash__(self):
+        return hash(self.name) + hash(self.model)
+
+    def __repr__(self):
+        return f"State('{self.model}', '{self.name}')"
+
+    def to_xml(self):
+        xml = etree.Element("State")
+        if self.name is not None:
+            xml.set("name", self.name)
+        if self.model is not None:
+            xml.set("model", self.model)
+        return xml
+
+    @staticmethod
+    def from_xml(xml):
+        name = xml.get("name")
+        model = xml.get("model")
+        return State(name=name, model=model)
diff --git a/unittests/test_state.py b/unittests/test_state.py
new file mode 100644
index 00000000..7fc16363
--- /dev/null
+++ b/unittests/test_state.py
@@ -0,0 +1,27 @@
+import caosdb as db
+from caosdb import State
+from caosdb.common.models import parse_xml
+from lxml import etree
+
+def test_state_xml():
+    state = State(model="model1", name="state1")
+    xml = etree.tostring(state.to_xml())
+
+    assert xml == b'<State name="state1" model="model1"/>'
+    state = State.from_xml(etree.fromstring(xml))
+    assert state.name == "state1"
+    assert state.model == "model1"
+
+    assert xml == etree.tostring(state.to_xml())
+
+def test_entity_xml():
+    r = db.Record()
+    assert r.state is None
+    r.state = State(model="model1", name="state1")
+
+    xml = etree.tostring(r.to_xml())
+    assert xml == b'<Record><State name="state1" model="model1"/></Record>'
+
+    r = parse_xml(xml)
+    assert r.state == State(model="model1", name="state1")
+
-- 
GitLab