Commit 5efd5083 authored by ORD's avatar ORD

Merge pull request #165 from iirob/inverse_references

Added support for Inverse references
parents 60866b82 dd4d25d0
...@@ -37,7 +37,7 @@ def create_folder(parent, *args): ...@@ -37,7 +37,7 @@ def create_folder(parent, *args):
or namespace index, name or namespace index, name
""" """
nodeid, qname = _parse_add_args(*args) nodeid, qname = _parse_add_args(*args)
return node.Node(parent.server, _create_folder(parent.server, parent.nodeid, nodeid, qname)) return node.Node(parent.server, _create_object(parent.server, parent.nodeid, nodeid, qname, ua.ObjectIds.FolderType))
def create_object(parent, *args): def create_object(parent, *args):
...@@ -47,7 +47,7 @@ def create_object(parent, *args): ...@@ -47,7 +47,7 @@ def create_object(parent, *args):
or namespace index, name or namespace index, name
""" """
nodeid, qname = _parse_add_args(*args) nodeid, qname = _parse_add_args(*args)
return node.Node(parent.server, _create_object(parent.server, parent.nodeid, nodeid, qname)) return node.Node(parent.server, _create_object(parent.server, parent.nodeid, nodeid, qname, ua.ObjectIds.BaseObjectType))
def create_property(parent, *args): def create_property(parent, *args):
...@@ -91,37 +91,21 @@ def create_method(parent, *args): ...@@ -91,37 +91,21 @@ def create_method(parent, *args):
outputs = args[4] outputs = args[4]
else: else:
outputs = [] outputs = []
return _create_method(parent, nodeid, qname, callback, inputs, outputs) return node.Node(parent.server, _create_method(parent, nodeid, qname, callback, inputs, outputs))
def _create_folder(server, parentnodeid, nodeid, qname): def _create_object(server, parentnodeid, nodeid, qname, objecttype):
addnode = ua.AddNodesItem() addnode = ua.AddNodesItem()
addnode.RequestedNewNodeId = nodeid addnode.RequestedNewNodeId = nodeid
addnode.BrowseName = qname addnode.BrowseName = qname
addnode.NodeClass = ua.NodeClass.Object addnode.NodeClass = ua.NodeClass.Object
addnode.ParentNodeId = parentnodeid addnode.ParentNodeId = parentnodeid
addnode.ReferenceTypeId = ua.NodeId.from_string("i=35") #TODO: maybe move to address_space.py and implement for all node types?
addnode.TypeDefinition = ua.NodeId.from_string("i=61") if node.Node(server, parentnodeid).get_type_definition() == ua.ObjectIds.FolderType:
attrs = ua.ObjectAttributes() addnode.ReferenceTypeId = ua.NodeId(ua.ObjectIds.Organizes)
attrs.Description = ua.LocalizedText(qname.Name) else:
attrs.DisplayName = ua.LocalizedText(qname.Name) addnode.ReferenceTypeId = ua.NodeId(ua.ObjectIds.HasComponent)
attrs.WriteMask = 0 addnode.TypeDefinition = ua.NodeId(objecttype)
attrs.UserWriteMask = 0
attrs.EventNotifier = 0
addnode.NodeAttributes = attrs
results = server.add_nodes([addnode])
results[0].StatusCode.check()
return results[0].AddedNodeId
def _create_object(server, parentnodeid, nodeid, qname):
addnode = ua.AddNodesItem()
addnode.RequestedNewNodeId = nodeid
addnode.BrowseName = qname
addnode.NodeClass = ua.NodeClass.Object
addnode.ParentNodeId = parentnodeid
addnode.ReferenceTypeId = ua.NodeId.from_string("i=35")
addnode.TypeDefinition = ua.NodeId(ua.ObjectIds.BaseObjectType)
attrs = ua.ObjectAttributes() attrs = ua.ObjectAttributes()
attrs.Description = ua.LocalizedText(qname.Name) attrs.Description = ua.LocalizedText(qname.Name)
attrs.DisplayName = ua.LocalizedText(qname.Name) attrs.DisplayName = ua.LocalizedText(qname.Name)
...@@ -180,7 +164,7 @@ def _create_method(parent, nodeid, qname, callback, inputs, outputs): ...@@ -180,7 +164,7 @@ def _create_method(parent, nodeid, qname, callback, inputs, outputs):
addnode.BrowseName = qname addnode.BrowseName = qname
addnode.NodeClass = ua.NodeClass.Method addnode.NodeClass = ua.NodeClass.Method
addnode.ParentNodeId = parent.nodeid addnode.ParentNodeId = parent.nodeid
addnode.ReferenceTypeId = ua.NodeId.from_string("i=47") addnode.ReferenceTypeId = ua.NodeId(ua.ObjectIds.HasComponent)
#node.TypeDefinition = ua.NodeId(ua.ObjectIds.BaseObjectType) #node.TypeDefinition = ua.NodeId(ua.ObjectIds.BaseObjectType)
attrs = ua.MethodAttributes() attrs = ua.MethodAttributes()
attrs.Description = ua.LocalizedText(qname.Name) attrs.Description = ua.LocalizedText(qname.Name)
......
...@@ -242,12 +242,7 @@ class Node(object): ...@@ -242,12 +242,7 @@ class Node(object):
HasNotifier = 48 HasNotifier = 48
HasOrderedComponent = 49 HasOrderedComponent = 49
""" """
references = self.get_children_descriptions(refs, nodeclassmask) return self.get_referenced_nodes(refs, ua.BrowseDirection.Forward, nodeclassmask)
nodes = []
for desc in references:
node = Node(self.server, desc.NodeId)
nodes.append(node)
return nodes
def get_properties(self): def get_properties(self):
""" """
...@@ -257,11 +252,19 @@ class Node(object): ...@@ -257,11 +252,19 @@ class Node(object):
return self.get_children(refs=ua.ObjectIds.HasProperty, nodeclassmask=ua.NodeClass.Variable) return self.get_children(refs=ua.ObjectIds.HasProperty, nodeclassmask=ua.NodeClass.Variable)
def get_children_descriptions(self, refs=ua.ObjectIds.HierarchicalReferences, nodeclassmask=ua.NodeClass.Unspecified, includesubtypes=True): def get_children_descriptions(self, refs=ua.ObjectIds.HierarchicalReferences, nodeclassmask=ua.NodeClass.Unspecified, includesubtypes=True):
return self.get_references(refs, ua.BrowseDirection.Forward, nodeclassmask, includesubtypes)
def get_references(self, refs=ua.ObjectIds.References, direction=ua.BrowseDirection.Both, nodeclassmask=ua.NodeClass.Unspecified, includesubtypes=True):
""" """
return all attributes of child nodes as UA BrowseResult structs returns references of the node based on specific filter defined with:
refs = ObjectId of the Reference
direction = Browse direction for references
nodeclassmask = filter nodes based on specific class
includesubtypes = If true subtypes of the reference (ref) are also included
""" """
desc = ua.BrowseDescription() desc = ua.BrowseDescription()
desc.BrowseDirection = ua.BrowseDirection.Forward desc.BrowseDirection = direction
desc.ReferenceTypeId = ua.TwoByteNodeId(refs) desc.ReferenceTypeId = ua.TwoByteNodeId(refs)
desc.IncludeSubtypes = includesubtypes desc.IncludeSubtypes = includesubtypes
desc.NodeClassMask = nodeclassmask desc.NodeClassMask = nodeclassmask
...@@ -274,6 +277,36 @@ class Node(object): ...@@ -274,6 +277,36 @@ class Node(object):
results = self.server.browse(params) results = self.server.browse(params)
return results[0].References return results[0].References
def get_referenced_nodes(self, refs=ua.ObjectIds.References, direction=ua.BrowseDirection.Both, nodeclassmask=ua.NodeClass.Unspecified, includesubtypes=True):
"""
returns referenced nodes based on specific filter
Paramters are the same as for get_references
"""
references = self.get_references(refs, direction, nodeclassmask, includesubtypes)
nodes = []
for desc in references:
node = Node(self.server, desc.NodeId)
nodes.append(node)
return nodes
def get_type_definition(self):
"""
returns type definition of the node.
"""
references = self.get_references(refs=ua.ObjectIds.HasTypeDefinition, direction=ua.BrowseDirection.Forward)
if len(references) == 0:
return ua.ObjectIds.BaseObjectType
return references[0].NodeId.Identifier
def get_parent(self):
"""
returns parent of the node.
"""
refs = self.get_references(refs=ua.ObjectIds.HierarchicalReferences, direction=ua.BrowseDirection.Inverse)
return Node(self.server, refs[0].NodeId)
def get_child(self, path): def get_child(self, path):
""" """
get a child specified by its path from this node. get a child specified by its path from this node.
......
...@@ -108,7 +108,8 @@ class ViewService(object): ...@@ -108,7 +108,8 @@ class ViewService(object):
""" """
if ref1.Identifier == ref2.Identifier: if ref1.Identifier == ref2.Identifier:
return True return True
if not subtypes and ref2.Identifier == ua.ObjectIds.HasSubtype: #TODO: Please check the changes here
elif not subtypes:
return False return False
oktypes = self._get_sub_ref(ref1) oktypes = self._get_sub_ref(ref1)
return ref2 in oktypes return ref2 in oktypes
...@@ -117,7 +118,7 @@ class ViewService(object): ...@@ -117,7 +118,7 @@ class ViewService(object):
res = [] res = []
nodedata = self._aspace[ref] nodedata = self._aspace[ref]
for ref in nodedata.references: for ref in nodedata.references:
if ref.ReferenceTypeId.Identifier == ua.ObjectIds.HasSubtype: if ref.ReferenceTypeId.Identifier == ua.ObjectIds.HasSubtype and ref.IsForward:
res.append(ref.NodeId) res.append(ref.NodeId)
res += self._get_sub_ref(ref.NodeId) res += self._get_sub_ref(ref.NodeId)
return res return res
...@@ -127,6 +128,8 @@ class ViewService(object): ...@@ -127,6 +128,8 @@ class ViewService(object):
return True return True
if desc == ua.BrowseDirection.Forward and isforward: if desc == ua.BrowseDirection.Forward and isforward:
return True return True
if desc == ua.BrowseDirection.Inverse and not isforward:
return True
return False return False
def translate_browsepaths_to_nodeids(self, browsepaths): def translate_browsepaths_to_nodeids(self, browsepaths):
...@@ -215,6 +218,9 @@ class NodeManagementService(object): ...@@ -215,6 +218,9 @@ class NodeManagementService(object):
# add requested attrs # add requested attrs
self._add_nodeattributes(item.NodeAttributes, nodedata) self._add_nodeattributes(item.NodeAttributes, nodedata)
# now add our node to db
self._aspace[nodedata.nodeid] = nodedata
if not item.ParentNodeId.is_null(): if not item.ParentNodeId.is_null():
desc = ua.ReferenceDescription() desc = ua.ReferenceDescription()
desc.ReferenceTypeId = item.ReferenceTypeId desc.ReferenceTypeId = item.ReferenceTypeId
...@@ -226,8 +232,13 @@ class NodeManagementService(object): ...@@ -226,8 +232,13 @@ class NodeManagementService(object):
desc.IsForward = True desc.IsForward = True
self._aspace[item.ParentNodeId].references.append(desc) self._aspace[item.ParentNodeId].references.append(desc)
# now add our node to db addref = ua.AddReferencesItem()
self._aspace[nodedata.nodeid] = nodedata addref.ReferenceTypeId = item.ReferenceTypeId
addref.SourceNodeId = nodedata.nodeid
addref.TargetNodeId = item.ParentNodeId
addref.TargetNodeClass = self._aspace[item.ParentNodeId].attributes[ua.AttributeIds.NodeClass].value.Value.Value
addref.IsForward = False
self._add_reference(addref, user)
# add type definition # add type definition
if item.TypeDefinition != ua.NodeId(): if item.TypeDefinition != ua.NodeId():
......
...@@ -86,7 +86,7 @@ class MySubHandler(): ...@@ -86,7 +86,7 @@ class MySubHandler():
class MySubHandler2(): class MySubHandler2():
def __init__(self): def __init__(self):
self.results = [] self.results = []
def datachange_notification(self, node, val, data): def datachange_notification(self, node, val, data):
self.results.append((node, val)) self.results.append((node, val))
...@@ -97,8 +97,8 @@ class MySubHandler2(): ...@@ -97,8 +97,8 @@ class MySubHandler2():
class MySubHandlerCounter(): class MySubHandlerCounter():
def __init__(self): def __init__(self):
self.datachange_count = 0 self.datachange_count = 0
self.event_count = 0 self.event_count = 0
def datachange_notification(self, node, val, data): def datachange_notification(self, node, val, data):
self.datachange_count += 1 self.datachange_count += 1
...@@ -107,8 +107,6 @@ class MySubHandlerCounter(): ...@@ -107,8 +107,6 @@ class MySubHandlerCounter():
self.event_count += 1 self.event_count += 1
class CommonTests(object): class CommonTests(object):
''' '''
...@@ -223,14 +221,42 @@ class CommonTests(object): ...@@ -223,14 +221,42 @@ class CommonTests(object):
self.assertTrue(prop2 in props) self.assertTrue(prop2 in props)
self.assertFalse(var in props) self.assertFalse(var in props)
self.assertFalse(folder in props) self.assertFalse(folder in props)
self.assertFalse(obj2 in props)
all_vars = obj.get_children(nodeclassmask=ua.NodeClass.Variable) all_vars = obj.get_children(nodeclassmask=ua.NodeClass.Variable)
self.assertTrue(prop in all_vars) self.assertTrue(prop in all_vars)
self.assertTrue(var in all_vars) self.assertTrue(var in all_vars)
self.assertFalse(folder in props)
self.assertFalse(obj2 in props)
all_objs = obj.get_children(nodeclassmask=ua.NodeClass.Object) all_objs = obj.get_children(nodeclassmask=ua.NodeClass.Object)
self.assertTrue(folder in all_objs) self.assertTrue(folder in all_objs)
self.assertTrue(obj2 in all_objs) self.assertTrue(obj2 in all_objs)
self.assertFalse(var in all_objs) self.assertFalse(var in all_objs)
def test_browse_references(self):
objects = self.opc.get_objects_node()
folder = objects.add_folder(4, "folder")
childs = objects.get_referenced_nodes(refs=ua.ObjectIds.Organizes, direction=ua.BrowseDirection.Forward, includesubtypes=False)
self.assertTrue(folder in childs)
childs = objects.get_referenced_nodes(refs=ua.ObjectIds.Organizes, direction=ua.BrowseDirection.Both, includesubtypes=False)
self.assertTrue(folder in childs)
childs = objects.get_referenced_nodes(refs=ua.ObjectIds.Organizes, direction=ua.BrowseDirection.Inverse, includesubtypes=False)
self.assertFalse(folder in childs)
parents = folder.get_referenced_nodes(refs=ua.ObjectIds.Organizes, direction=ua.BrowseDirection.Inverse, includesubtypes=False)
self.assertTrue(objects in parents)
parents = folder.get_referenced_nodes(refs=ua.ObjectIds.HierarchicalReferences, direction=ua.BrowseDirection.Inverse, includesubtypes=False)
self.assertFalse(objects in parents)
parents = folder.get_referenced_nodes(refs=ua.ObjectIds.HierarchicalReferences, direction=ua.BrowseDirection.Inverse, includesubtypes=True)
self.assertTrue(objects in parents)
parent = folder.get_parent()
self.assertEqual(parent, objects)
def test_browsename_with_spaces(self): def test_browsename_with_spaces(self):
o = self.opc.get_objects_node() o = self.opc.get_objects_node()
v = o.add_variable(3, 'BNVariable with spaces and %&+?/', 1.3) v = o.add_variable(3, 'BNVariable with spaces and %&+?/', 1.3)
...@@ -387,7 +413,7 @@ class CommonTests(object): ...@@ -387,7 +413,7 @@ class CommonTests(object):
def test_utf8(self): def test_utf8(self):
objects = self.opc.get_objects_node() objects = self.opc.get_objects_node()
utf_string = "æøå@%&" utf_string = "æøå@%&"
bn = ua.QualifiedName(utf_string, 3) bn = ua.QualifiedName(utf_string, 3)
nid = ua.NodeId("æølå", 3) nid = ua.NodeId("æølå", 3)
val = "æøå" val = "æøå"
v = objects.add_variable(nid, bn, val) v = objects.add_variable(nid, bn, val)
...@@ -747,7 +773,7 @@ class CommonTests(object): ...@@ -747,7 +773,7 @@ class CommonTests(object):
o = self.opc.get_objects_node() o = self.opc.get_objects_node()
# subscribe to a variable # subscribe to a variable
startv1 = True startv1 = True
v1 = o.add_variable(3, 'SubscriptionVariableBool', startv1) v1 = o.add_variable(3, 'SubscriptionVariableBool', startv1)
sub = self.opc.create_subscription(100, msclt) sub = self.opc.create_subscription(100, msclt)
handle1 = sub.subscribe_data_change(v1) handle1 = sub.subscribe_data_change(v1)
...@@ -777,9 +803,9 @@ class CommonTests(object): ...@@ -777,9 +803,9 @@ class CommonTests(object):
msclt = MySubHandler2() msclt = MySubHandler2()
o = self.opc.get_objects_node() o = self.opc.get_objects_node()
startv1 = True startv1 = True
v1 = o.add_variable(3, 'SubscriptionVariableMany1', startv1) v1 = o.add_variable(3, 'SubscriptionVariableMany1', startv1)
startv2 = [1.22, 1.65] startv2 = [1.22, 1.65]
v2 = o.add_variable(3, 'SubscriptionVariableMany2', startv2) v2 = o.add_variable(3, 'SubscriptionVariableMany2', startv2)
sub = self.opc.create_subscription(100, msclt) sub = self.opc.create_subscription(100, msclt)
...@@ -787,7 +813,7 @@ class CommonTests(object): ...@@ -787,7 +813,7 @@ class CommonTests(object):
# Now check we get the start values # Now check we get the start values
nodes = [v1, v2] nodes = [v1, v2]
count = 0 count = 0
while not len(msclt.results) > 1: while not len(msclt.results) > 1:
count += 1 count += 1
...@@ -804,7 +830,7 @@ class CommonTests(object): ...@@ -804,7 +830,7 @@ class CommonTests(object):
else: else:
self.fail("Error node {} is neither {} nor {}".format(node, v1, v2)) self.fail("Error node {} is neither {} nor {}".format(node, v1, v2))
sub.delete() sub.delete()
def test_subscribe_server_time(self): def test_subscribe_server_time(self):
msclt = MySubHandler() msclt = MySubHandler()
...@@ -856,14 +882,57 @@ class CommonTests(object): ...@@ -856,14 +882,57 @@ class CommonTests(object):
def test_add_nodes(self): def test_add_nodes(self):
objects = self.opc.get_objects_node() objects = self.opc.get_objects_node()
f = objects.add_folder(3, 'MyFolder') f = objects.add_folder(3, 'MyFolder')
child = objects.get_child("3:MyFolder")
self.assertEqual(child, f)
o = f.add_object(3, 'MyObject')
child = f.get_child("3:MyObject")
self.assertEqual(child, o)
v = f.add_variable(3, 'MyVariable', 6) v = f.add_variable(3, 'MyVariable', 6)
child = f.get_child("3:MyVariable")
self.assertEqual(child, v)
p = f.add_property(3, 'MyProperty', 10) p = f.add_property(3, 'MyProperty', 10)
child = f.get_child("3:MyProperty")
self.assertEqual(child, p)
childs = f.get_children() childs = f.get_children()
self.assertTrue(o in childs)
self.assertTrue(v in childs) self.assertTrue(v in childs)
self.assertTrue(p in childs) self.assertTrue(p in childs)
def test_references_for_added_nodes(self):
objects = self.opc.get_objects_node()
o = objects.add_object(3, 'MyObject')
nodes = objects.get_referenced_nodes(refs=ua.ObjectIds.Organizes, direction=ua.BrowseDirection.Forward, includesubtypes=False)
self.assertTrue(o in nodes)
nodes = o.get_referenced_nodes(refs=ua.ObjectIds.Organizes, direction=ua.BrowseDirection.Inverse, includesubtypes=False)
self.assertTrue(objects in nodes)
self.assertEqual(o.get_parent(), objects)
self.assertEqual(o.get_type_definition(), ua.ObjectIds.BaseObjectType)
o2 = o.add_object(3, 'MySecondObject')
nodes = o.get_referenced_nodes(refs=ua.ObjectIds.HasComponent, direction=ua.BrowseDirection.Forward, includesubtypes=False)
self.assertTrue(o2 in nodes)
nodes = o2.get_referenced_nodes(refs=ua.ObjectIds.HasComponent, direction=ua.BrowseDirection.Inverse, includesubtypes=False)
self.assertTrue(o in nodes)
self.assertEqual(o2.get_parent(), o)
self.assertEqual(o2.get_type_definition(), ua.ObjectIds.BaseObjectType)
v = o.add_variable(3, 'MyVariable', 6)
nodes = o.get_referenced_nodes(refs=ua.ObjectIds.HasComponent, direction=ua.BrowseDirection.Forward, includesubtypes=False)
self.assertTrue(v in nodes)
nodes = v.get_referenced_nodes(refs=ua.ObjectIds.HasComponent, direction=ua.BrowseDirection.Inverse, includesubtypes=False)
self.assertTrue(o in nodes)
self.assertEqual(v.get_parent(), o)
self.assertEqual(v.get_type_definition(), ua.ObjectIds.BaseDataVariableType)
p = o.add_property(3, 'MyProperty', 2)
nodes = o.get_referenced_nodes(refs=ua.ObjectIds.HasProperty, direction=ua.BrowseDirection.Forward, includesubtypes=False)
self.assertTrue(p in nodes)
nodes = p.get_referenced_nodes(refs=ua.ObjectIds.HasProperty, direction=ua.BrowseDirection.Inverse, includesubtypes=False)
self.assertTrue(o in nodes)
self.assertEqual(p.get_parent(), o)
self.assertEqual(p.get_type_definition(), ua.ObjectIds.PropertyType)
def test_get_endpoints(self): def test_get_endpoints(self):
endpoints = self.opc.get_endpoints() endpoints = self.opc.get_endpoints()
self.assertTrue(len(endpoints) > 0) self.assertTrue(len(endpoints) > 0)
self.assertTrue(endpoints[0].EndpointUrl.startswith("opc.tcp://")) self.assertTrue(endpoints[0].EndpointUrl.startswith("opc.tcp://"))
...@@ -6,6 +6,7 @@ from datetime import timedelta ...@@ -6,6 +6,7 @@ from datetime import timedelta
from opcua import Server from opcua import Server
from opcua import Client from opcua import Client
from opcua import ua from opcua import ua
from opcua import uamethod
port_num = 485140 port_num = 485140
...@@ -50,7 +51,7 @@ class TestServer(unittest.TestCase, CommonTests): ...@@ -50,7 +51,7 @@ class TestServer(unittest.TestCase, CommonTests):
self.assertTrue(new_app_uri in [s.ApplicationUri for s in new_servers]) self.assertTrue(new_app_uri in [s.ApplicationUri for s in new_servers])
finally: finally:
client.disconnect() client.disconnect()
def test_find_servers2(self): def test_find_servers2(self):
client = Client(self.discovery.endpoint.geturl()) client = Client(self.discovery.endpoint.geturl())
client.connect() client.connect()
...@@ -136,5 +137,23 @@ class TestServer(unittest.TestCase, CommonTests): ...@@ -136,5 +137,23 @@ class TestServer(unittest.TestCase, CommonTests):
var.set_value(3.0) var.set_value(3.0)
self.srv.iserver.disable_history(var) self.srv.iserver.disable_history(var)
def test_references_for_added_nodes_method(self):
objects = self.opc.get_objects_node()
o = objects.add_object(3, 'MyObject')
nodes = objects.get_referenced_nodes(refs=ua.ObjectIds.Organizes, direction=ua.BrowseDirection.Forward, includesubtypes=False)
self.assertTrue(o in nodes)
nodes = o.get_referenced_nodes(refs=ua.ObjectIds.Organizes, direction=ua.BrowseDirection.Inverse, includesubtypes=False)
self.assertTrue(objects in nodes)
self.assertEqual(o.get_parent(), objects)
self.assertEqual(o.get_type_definition(), ua.ObjectIds.BaseObjectType)
@uamethod
def callback(parent):
return
m = o.add_method(3, 'MyMethod', callback)
nodes = o.get_referenced_nodes(refs=ua.ObjectIds.HasComponent, direction=ua.BrowseDirection.Forward, includesubtypes=False)
self.assertTrue(m in nodes)
nodes = m.get_referenced_nodes(refs=ua.ObjectIds.HasComponent, direction=ua.BrowseDirection.Inverse, includesubtypes=False)
self.assertTrue(o in nodes)
self.assertEqual(m.get_parent(), o)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment