|
1
|
|
|
import json |
|
2
|
|
|
import notations |
|
3
|
|
|
|
|
4
|
|
|
from django.contrib.auth.models import User |
|
5
|
|
|
from django.db import models |
|
6
|
|
|
from django.db.models.aggregates import Max |
|
7
|
|
|
|
|
8
|
|
|
import pyxb.utils.domutils |
|
9
|
|
|
try: |
|
10
|
|
|
from .xml_fuzztree import FuzzTree as XmlFuzzTree, Namespace as XmlFuzzTreeNamespace, CreateFromDocument as fuzzTreeFromXml |
|
11
|
|
|
from .xml_faulttree import FaultTree as XmlFaultTree, Namespace as XmlFaultTreeNamespace, CreateFromDocument as faultTreeFromXml |
|
12
|
|
|
from .node_rendering import tikz_shapes |
|
13
|
|
|
except Exception: |
|
14
|
|
|
print "ERROR: Perform a build process first." |
|
15
|
|
|
exit(-1) |
|
16
|
|
|
from defusedxml.ElementTree import fromstring as parseXml |
|
17
|
|
|
|
|
18
|
|
|
from .project import Project |
|
19
|
|
|
|
|
20
|
|
|
import logging |
|
21
|
|
|
logger = logging.getLogger('ore') |
|
22
|
|
|
|
|
23
|
|
|
|
|
24
|
|
|
class Graph(models.Model): |
|
25
|
|
|
|
|
26
|
|
|
""" |
|
27
|
|
|
Class: Graph |
|
28
|
|
|
|
|
29
|
|
|
This class models a generic graph that is suitable for any diagram notation. It basically serves a container for |
|
30
|
|
|
its contained nodes and edges. Additionally, it provides functionality for serializing it. |
|
31
|
|
|
|
|
32
|
|
|
Fields: |
|
33
|
|
|
{str} kind - unique identifier that indicates the graph's notation (e.g. fuzztree). Must be an |
|
34
|
|
|
element of the set of available notations (See also: <notations>) |
|
35
|
|
|
{str} name - the name of the graph |
|
36
|
|
|
{User} owner - a link to the owner of the graph |
|
37
|
|
|
{Project} project - the project corresponding to the graph |
|
38
|
|
|
{const datetime} created - timestamp of the moment of graph creation (default: now) |
|
39
|
|
|
{bool} deleted - flag indicating whether this graph was deleted or not. Simplifies restoration of the |
|
40
|
|
|
graph if needed by toggling this member (default: False) |
|
41
|
|
|
{JSON} graph_issues - |
|
42
|
|
|
""" |
|
43
|
|
|
class Meta: |
|
44
|
|
|
app_label = 'ore' |
|
45
|
|
|
|
|
46
|
|
|
kind = models.CharField(max_length=127, choices=notations.choices) |
|
47
|
|
|
name = models.CharField(max_length=255) |
|
48
|
|
|
owner = models.ForeignKey(User, related_name='graphs') |
|
49
|
|
|
project = models.ForeignKey(Project, related_name='graphs') |
|
50
|
|
|
created = models.DateTimeField(auto_now_add=True, editable=False) |
|
51
|
|
|
modified = models.DateTimeField(auto_now=True) |
|
52
|
|
|
deleted = models.BooleanField(default=False) |
|
53
|
|
|
read_only = models.BooleanField(default=False) |
|
54
|
|
|
|
|
55
|
|
|
def __unicode__(self): |
|
56
|
|
|
return unicode( |
|
|
|
|
|
|
57
|
|
|
'%s%s' % ('[DELETED] ' if self.deleted else '', self.name)) |
|
58
|
|
|
|
|
59
|
|
|
def ensure_default_nodes(self): |
|
60
|
|
|
""" |
|
61
|
|
|
Add nodes that are contained in this kind of graph by default, |
|
62
|
|
|
in case they are missing. |
|
63
|
|
|
""" |
|
64
|
|
|
notation = notations.by_kind[self.kind] |
|
65
|
|
|
if 'defaults' in notation: |
|
66
|
|
|
from .node import Node |
|
67
|
|
|
for index, default_node in enumerate( |
|
68
|
|
|
notation['defaults']['nodes']): |
|
69
|
|
|
default_node.update({'properties': {}}) |
|
70
|
|
|
# use index as node client ID |
|
71
|
|
|
# this is unique since all other client IDs are time stamps |
|
72
|
|
|
node, created = Node.objects.get_or_create(kind=default_node['kind'], |
|
73
|
|
|
graph=self, deleted=False, |
|
74
|
|
|
defaults={'client_id': int(index), |
|
75
|
|
|
'x': default_node['x'], |
|
76
|
|
|
'y': default_node['y'] |
|
77
|
|
|
} |
|
78
|
|
|
) |
|
79
|
|
|
if created: |
|
80
|
|
|
node.save() |
|
81
|
|
|
|
|
82
|
|
|
def top_node(self): |
|
83
|
|
|
""" |
|
84
|
|
|
Method: top_node |
|
85
|
|
|
|
|
86
|
|
|
Return the top node of this graph, if applicable for the given type. |
|
87
|
|
|
|
|
88
|
|
|
Returns: |
|
89
|
|
|
{Node} instance |
|
90
|
|
|
""" |
|
91
|
|
|
if self.kind in {'faulttree', 'fuzztree'}: |
|
92
|
|
|
return self.nodes.all().get(kind='topEvent') |
|
93
|
|
|
else: |
|
94
|
|
|
return None |
|
95
|
|
|
|
|
96
|
|
|
def to_json(self, use_value_dict=False): |
|
97
|
|
|
""" |
|
98
|
|
|
Method: to_json |
|
99
|
|
|
|
|
100
|
|
|
Serializes the graph into a JSON object. |
|
101
|
|
|
|
|
102
|
|
|
Returns: |
|
103
|
|
|
{dict} the graph in JSON representation |
|
104
|
|
|
""" |
|
105
|
|
|
return json.dumps(self.to_dict(use_value_dict)) |
|
106
|
|
|
|
|
107
|
|
|
def to_dict(self, use_value_dict=False): |
|
108
|
|
|
""" |
|
109
|
|
|
Method: to_dict |
|
110
|
|
|
|
|
111
|
|
|
Encodes the whole graph as dictionary. |
|
112
|
|
|
|
|
113
|
|
|
Returns: |
|
114
|
|
|
{dict} the graph as dictionary |
|
115
|
|
|
""" |
|
116
|
|
|
node_set = self.nodes.filter(deleted=False) |
|
117
|
|
|
edge_set = self.edges.filter(deleted=False) |
|
118
|
|
|
group_set = self.groups.filter(deleted=False) |
|
119
|
|
|
nodes = [node.to_dict(use_value_dict) for node in node_set] |
|
120
|
|
|
edges = [edge.to_dict(use_value_dict) for edge in edge_set] |
|
121
|
|
|
groups = [group.to_dict(use_value_dict) for group in group_set] |
|
122
|
|
|
|
|
123
|
|
|
node_seed = self.nodes.aggregate(Max('client_id'))['client_id__max'] |
|
124
|
|
|
edge_seed = self.edges.aggregate(Max('client_id'))['client_id__max'] |
|
125
|
|
|
group_seed = self.groups.aggregate(Max('client_id'))['client_id__max'] |
|
126
|
|
|
|
|
127
|
|
|
return { |
|
128
|
|
|
'id': self.pk, |
|
129
|
|
|
'seed': max(node_seed, edge_seed, group_seed), |
|
130
|
|
|
'name': self.name, |
|
131
|
|
|
'type': self.kind, |
|
132
|
|
|
'readOnly': self.read_only, |
|
133
|
|
|
'nodes': nodes, |
|
134
|
|
|
'edges': edges, |
|
135
|
|
|
'nodeGroups': groups |
|
136
|
|
|
} |
|
137
|
|
|
|
|
138
|
|
|
def to_bool_term(self): |
|
139
|
|
|
root = self.nodes.get(kind__exact='topEvent') |
|
140
|
|
|
return root.to_bool_term() |
|
141
|
|
|
|
|
142
|
|
|
def to_graphml(self): |
|
143
|
|
|
graphKindData = ' <data key="kind">%s</data>\n' % (self.kind) |
|
144
|
|
|
if self.kind in {'faulttree', 'fuzztree'}: |
|
145
|
|
|
missionData = ' <data key="missionTime">%d</data>\n' % ( |
|
146
|
|
|
self.top_node().get_property('missionTime'),) |
|
147
|
|
|
else: |
|
148
|
|
|
missionData = '' |
|
149
|
|
|
|
|
150
|
|
|
return ''.join([ |
|
151
|
|
|
'<?xml version="1.0" encoding="utf-8"?>\n' |
|
152
|
|
|
'<graphml xmlns="http://graphml.graphdrawing.org/xmlns"\n' |
|
153
|
|
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' |
|
154
|
|
|
' xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns\n' |
|
155
|
|
|
' http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">\n' |
|
156
|
|
|
' <graph id="graph" edgedefault="directed">\n', |
|
157
|
|
|
notations.graphml_keys[self.kind], |
|
158
|
|
|
'\n', |
|
159
|
|
|
graphKindData, |
|
160
|
|
|
missionData] + |
|
161
|
|
|
[node.to_graphml() for node in self.nodes.filter(deleted=False)] + |
|
162
|
|
|
[edge.to_graphml() for edge in self.edges.filter(deleted=False)] + |
|
163
|
|
|
|
|
164
|
|
|
[' </graph>\n' |
|
165
|
|
|
'</graphml>\n' |
|
166
|
|
|
]) |
|
167
|
|
|
|
|
168
|
|
|
def to_tikz(self): |
|
169
|
|
|
""" |
|
170
|
|
|
Method: to_tikz |
|
171
|
|
|
Translates the graph into a LaTex TIKZ representation. |
|
172
|
|
|
|
|
173
|
|
|
Returns: |
|
174
|
|
|
{string} The TIKZ representation of the graph |
|
175
|
|
|
""" |
|
176
|
|
|
# Latex preambel |
|
177
|
|
|
result = """ |
|
178
|
|
|
\\documentclass{article} |
|
179
|
|
|
\\usepackage[landscape, top=1in, bottom=1in, left=1in, right=1in]{geometry} |
|
180
|
|
|
\\usepackage{helvet} |
|
181
|
|
|
\\usepackage{adjustbox} |
|
182
|
|
|
\\renewcommand{\\familydefault}{\\sfdefault} |
|
183
|
|
|
\\usepackage{tikz} |
|
184
|
|
|
\\usetikzlibrary{positioning, trees, svg.path} |
|
185
|
|
|
\\tikzset{shapeStyle/.style={inner sep=0em, outer sep=0em}} |
|
186
|
|
|
\\tikzset{shapeStyleDashed/.style={inner sep=0em, outer sep=0em, dashed, dash pattern=on 4.2 off 1.4}} |
|
187
|
|
|
\\tikzset{mirrorStyle/.style={fill=white,text width=50pt, below, align=center, inner sep=0.2em, outer sep=0.3em}} |
|
188
|
|
|
\\tikzset{fork edge/.style={line width=1.4, to path={|- ([shift={(\\tikztotarget)}] +0pt,+18pt) -| (\\tikztotarget) }}} |
|
189
|
|
|
\\begin{document} |
|
190
|
|
|
\\pagestyle{empty} |
|
191
|
|
|
""" |
|
192
|
|
|
result += tikz_shapes + """ |
|
193
|
|
|
\\begin{figure} |
|
194
|
|
|
\\begin{adjustbox}{max size={\\textwidth}{\\textheight}} |
|
195
|
|
|
\\begin{tikzpicture}[auto, trim left] |
|
196
|
|
|
""" |
|
197
|
|
|
# Find most left node and takes it's x coordinate as start offset |
|
198
|
|
|
# This basically shifts the whole tree to the left border |
|
199
|
|
|
minx = self.nodes.aggregate(min_x=models.Min('x'))['min_x'] |
|
200
|
|
|
# Find root node and start from there |
|
201
|
|
|
# Use the TOP node Y coordinate as starting point at the upper border |
|
202
|
|
|
# Note: (0,0) is the upper left corder in TiKZ, but the lower left in |
|
203
|
|
|
# the DB |
|
204
|
|
|
top_event = self.nodes.get(kind='topEvent') |
|
205
|
|
|
result += top_event.to_tikz(x_offset=-minx, y_offset=top_event.y) |
|
206
|
|
|
# result += top_event.to_tikz_tree() |
|
207
|
|
|
result += """ |
|
208
|
|
|
\\end{tikzpicture} |
|
209
|
|
|
\\end{adjustbox} |
|
210
|
|
|
\\end{figure} |
|
211
|
|
|
\\end{document} |
|
212
|
|
|
""" |
|
213
|
|
|
return result |
|
214
|
|
|
|
|
215
|
|
|
def to_xml(self, xmltype=None): |
|
216
|
|
|
""" |
|
217
|
|
|
Method: to_xml |
|
218
|
|
|
Serializes the graph into its XML representation. |
|
219
|
|
|
|
|
220
|
|
|
Returns: |
|
221
|
|
|
{string} The XML representation of the graph |
|
222
|
|
|
""" |
|
223
|
|
|
bds = pyxb.utils.domutils.BindingDOMSupport() |
|
224
|
|
|
if xmltype: |
|
225
|
|
|
kind = xmltype |
|
226
|
|
|
else: |
|
227
|
|
|
kind = self.kind |
|
228
|
|
|
if kind == "fuzztree": |
|
229
|
|
|
tree = XmlFuzzTree(name=self.name, id=self.pk) |
|
230
|
|
|
bds.DeclareNamespace(XmlFuzzTreeNamespace, 'fuzzTree') |
|
231
|
|
|
elif kind == "faulttree": |
|
232
|
|
|
tree = XmlFaultTree(name=self.name, id=self.pk) |
|
233
|
|
|
bds.DeclareNamespace(XmlFaultTreeNamespace, 'faultTree') |
|
234
|
|
|
else: |
|
235
|
|
|
raise ValueError('No XML support for this graph type.') |
|
236
|
|
|
|
|
237
|
|
|
# Find root node and start from there |
|
238
|
|
|
top_event = self.nodes.get(kind='topEvent') |
|
239
|
|
|
tree.topEvent = top_event.to_xml(kind) |
|
240
|
|
|
|
|
241
|
|
|
dom = tree.toDOM(bds) |
|
242
|
|
|
return dom.toprettyxml() |
|
243
|
|
|
|
|
244
|
|
|
def from_xml(self, xml): |
|
245
|
|
|
''' Fill this graph with the information gathered from the XML.''' |
|
246
|
|
|
from .node import Node |
|
247
|
|
|
if self.kind == "fuzztree": |
|
248
|
|
|
tree = fuzzTreeFromXml(xml) |
|
249
|
|
|
elif self.kind == "faulttree": |
|
250
|
|
|
tree = faultTreeFromXml(xml) |
|
251
|
|
|
else: |
|
252
|
|
|
raise ValueError('No XML support for this graph type.') |
|
253
|
|
|
|
|
254
|
|
|
self.name = tree.name |
|
255
|
|
|
self.save() |
|
256
|
|
|
top = Node(graph=self) |
|
257
|
|
|
top.load_xml(tree.topEvent) |
|
258
|
|
|
|
|
259
|
|
|
def from_graphml(self, graphml): |
|
260
|
|
|
''' |
|
261
|
|
|
Parses the given GraphML with the DefusedXML library, for better security. |
|
262
|
|
|
''' |
|
263
|
|
|
from .node import Node, new_client_id |
|
264
|
|
|
from .edge import Edge |
|
265
|
|
|
dom = parseXml(graphml) |
|
266
|
|
|
graph = dom.find('{http://graphml.graphdrawing.org/xmlns}graph') |
|
267
|
|
|
if graph is None: |
|
268
|
|
|
raise Exception( |
|
269
|
|
|
'Could not find <graph> element in the input data.') |
|
270
|
|
|
if graph.get('edgedefault') != 'directed': |
|
271
|
|
|
raise Exception( |
|
272
|
|
|
'Only GraphML documents with directed edges are supported.') |
|
273
|
|
|
# Determine graph type, in order to check for the right format rules |
|
274
|
|
|
graph_kind_element = graph.find( |
|
275
|
|
|
"{http://graphml.graphdrawing.org/xmlns}data[@key='kind']") |
|
276
|
|
|
if graph_kind_element is None: |
|
277
|
|
|
raise Exception( |
|
278
|
|
|
'Missing <data> element for graph kind declaration.') |
|
279
|
|
|
graph_kind = graph_kind_element.text |
|
280
|
|
|
if graph_kind not in notations.graphml_node_data: |
|
281
|
|
|
raise Exception('Invalid graph kind declaration.') |
|
282
|
|
|
else: |
|
283
|
|
|
self.kind = graph_kind |
|
284
|
|
|
# Save graph object to get a valid, which is needed in the node refs |
|
285
|
|
|
self.save() |
|
286
|
|
|
# go through all GraphML nodes and create them as model nodes for this |
|
287
|
|
|
# graph |
|
288
|
|
|
id_map = {} # GraphML node ID's to node model PK's |
|
289
|
|
|
for gml_node in graph.iter( |
|
290
|
|
|
'{http://graphml.graphdrawing.org/xmlns}node'): |
|
291
|
|
|
gml_node_id = gml_node.get('id') |
|
292
|
|
|
node = Node(graph=self, client_id=int(gml_node_id)) |
|
293
|
|
|
node.save() # get a valid pk |
|
294
|
|
|
id_map[gml_node_id] = node.pk |
|
295
|
|
|
# Scan all data elements for the node |
|
296
|
|
|
for gml_node_data in gml_node.iter( |
|
297
|
|
|
'{http://graphml.graphdrawing.org/xmlns}data'): |
|
298
|
|
|
name = gml_node_data.get('key') |
|
299
|
|
|
value = gml_node_data.text |
|
300
|
|
|
if name not in notations.graphml_node_data[graph_kind]: |
|
301
|
|
|
raise Exception("Invalid graph node element '%s'" % name) |
|
302
|
|
|
# set according node properties |
|
303
|
|
|
if name == 'kind': |
|
304
|
|
|
node.kind = value |
|
305
|
|
|
node.save() |
|
306
|
|
|
else: |
|
307
|
|
|
node.set_attr(name, value) |
|
308
|
|
|
# logger.debug("New node from GraphML import: "+str(node.to_dict())) |
|
309
|
|
|
# Graph properties belong to the TOP node |
|
310
|
|
|
# GraphML files without graph <data> properties may refer to a graph type that has no |
|
311
|
|
|
# TOP node concept |
|
312
|
|
|
for data in graph.findall( |
|
313
|
|
|
'{http://graphml.graphdrawing.org/xmlns}data'): |
|
314
|
|
|
name = data.get('key') |
|
315
|
|
|
if name != 'kind': # already handled above |
|
316
|
|
|
if name not in notations.graphml_graph_data[graph_kind]: |
|
317
|
|
|
raise Exception("Invalid graph data element '%s'" % name) |
|
318
|
|
|
logger.debug( |
|
319
|
|
|
"Setting attribute %s to %s on top node" % |
|
320
|
|
|
(name, data.text)) |
|
321
|
|
|
self.top_node().set_attr( |
|
322
|
|
|
name, |
|
323
|
|
|
data.text) # Fetch TOP node here, not existent in RBD's |
|
324
|
|
|
|
|
325
|
|
|
# go through all GraphML edges and create them as model edges for this |
|
326
|
|
|
# graph |
|
327
|
|
|
for gml_edge in graph.iter( |
|
328
|
|
|
'{http://graphml.graphdrawing.org/xmlns}edge'): |
|
329
|
|
|
source = Node.objects.get(pk=id_map[gml_edge.get('source')]) |
|
330
|
|
|
target = Node.objects.get(pk=id_map[gml_edge.get('target')]) |
|
331
|
|
|
edge = Edge( |
|
332
|
|
|
graph=self, |
|
333
|
|
|
source=source, |
|
334
|
|
|
target=target, |
|
335
|
|
|
client_id=new_client_id()) |
|
336
|
|
|
edge.save() |
|
337
|
|
|
|
|
338
|
|
|
def copy_values(self, other): |
|
339
|
|
|
# copy all nodes, groups and their properties |
|
340
|
|
|
node_cache = {} |
|
341
|
|
|
|
|
342
|
|
|
for node in other.nodes.all(): |
|
343
|
|
|
# first cache the old node's properties |
|
344
|
|
|
properties = node.properties.all() |
|
345
|
|
|
|
|
346
|
|
|
# create node copy by overwriting the ID field |
|
347
|
|
|
old_id = node.pk |
|
348
|
|
|
node.pk = None |
|
349
|
|
|
node.graph = self |
|
350
|
|
|
node.save() |
|
351
|
|
|
node_cache[old_id] = node |
|
352
|
|
|
|
|
353
|
|
|
# now save the property objects for the new node |
|
354
|
|
|
for prop in properties: |
|
355
|
|
|
prop.pk = None |
|
356
|
|
|
prop.node = node |
|
357
|
|
|
prop.save() |
|
358
|
|
|
|
|
359
|
|
|
for edge in other.edges.all(): |
|
360
|
|
|
properties = edge.properties.all() |
|
361
|
|
|
edge.pk = None |
|
362
|
|
|
edge.source = node_cache[edge.source.pk] |
|
363
|
|
|
edge.target = node_cache[edge.target.pk] |
|
364
|
|
|
edge.graph = self |
|
365
|
|
|
edge.save() |
|
366
|
|
|
|
|
367
|
|
|
# now save the property objects for the new edge |
|
368
|
|
|
for prop in properties: |
|
369
|
|
|
prop.pk = None |
|
370
|
|
|
prop.edge = edge |
|
371
|
|
|
prop.save() |
|
372
|
|
|
|
|
373
|
|
|
from .node_group import NodeGroup |
|
374
|
|
|
for group in other.groups.all(): |
|
375
|
|
|
# create group copy by overwriting the ID field |
|
376
|
|
|
newgroup = NodeGroup(graph=self) |
|
377
|
|
|
newgroup.save() # prepare M2M field |
|
378
|
|
|
for node in group.nodes.all(): |
|
379
|
|
|
newgroup.nodes.add(node_cache[node.pk]) |
|
380
|
|
|
newgroup.save() |
|
381
|
|
|
|
|
382
|
|
|
# now save the property objects for the new group |
|
383
|
|
|
for prop in group.properties.all(): |
|
384
|
|
|
prop.pk = None |
|
385
|
|
|
prop.node_group = newgroup |
|
386
|
|
|
prop.save() |
|
387
|
|
|
|
|
388
|
|
|
self.read_only = other.read_only |
|
389
|
|
|
self.save() |
|
390
|
|
|
|
|
391
|
|
|
def delete_configurations(self): |
|
392
|
|
|
""" |
|
393
|
|
|
Deletes all informations about configurations of this graph. |
|
394
|
|
|
""" |
|
395
|
|
|
self.configurations.all().delete() |
|
396
|
|
|
|
|
397
|
|
|
def delete_results(self, kind): |
|
398
|
|
|
''' |
|
399
|
|
|
Deletes all graph analysis results of a particular kind. |
|
400
|
|
|
''' |
|
401
|
|
|
old_results = self.results.filter(kind=kind).all() |
|
402
|
|
|
logger.debug( |
|
403
|
|
|
"Deleting %u old results of kind %s" % |
|
404
|
|
|
(len(old_results), kind)) |
|
405
|
|
|
old_results.delete() |
|
406
|
|
|
|
|
407
|
|
|
def same_as(self, graph): |
|
408
|
|
|
''' |
|
409
|
|
|
Checks if this graph is equal to the given one in terms of nodes and their properties. |
|
410
|
|
|
This is a very expensive operation that is only intended for testing purposes. |
|
411
|
|
|
Comparing the edges is not easily possible, since the source / target ID's in the edge |
|
412
|
|
|
instances would need to be mapped between original and copy. |
|
413
|
|
|
''' |
|
414
|
|
|
|
|
415
|
|
|
def error_reporting(): |
|
416
|
|
|
logger.debug("Graphs are differing:") |
|
417
|
|
|
text1 = self.to_graphml() |
|
418
|
|
|
text2 = graph.to_graphml() |
|
419
|
|
|
import difflib |
|
420
|
|
|
difflines = difflib.unified_diff(text1, text2) |
|
421
|
|
|
logger.debug('\n'.join(difflines)) |
|
422
|
|
|
|
|
423
|
|
|
for my_node in self.nodes.all().filter(deleted=False): |
|
424
|
|
|
found_match = False |
|
425
|
|
|
for their_node in graph.nodes.all().filter(deleted=False): |
|
426
|
|
|
logger.debug( |
|
427
|
|
|
"Checking our %s (%u) against %s (%u)" % |
|
428
|
|
|
(str(my_node), my_node.pk, str(their_node), their_node.pk)) |
|
429
|
|
|
if my_node.same_as(their_node): |
|
430
|
|
|
logger.debug("Match") |
|
431
|
|
|
found_match = True |
|
432
|
|
|
break |
|
433
|
|
|
if not found_match: |
|
434
|
|
|
logger.debug( |
|
435
|
|
|
"Couldn't find a match for node %s" % |
|
436
|
|
|
(str(my_node))) |
|
437
|
|
|
error_reporting() |
|
438
|
|
|
return False |
|
439
|
|
|
|
|
440
|
|
|
for my_group in self.groups.all().filter(deleted=False): |
|
441
|
|
|
found_match = False |
|
442
|
|
|
for their_group in graph.groups.all().filter(deleted=False): |
|
443
|
|
|
if my_group.same_as(their_group): |
|
444
|
|
|
found_match = True |
|
445
|
|
|
break |
|
446
|
|
|
if not found_match: |
|
447
|
|
|
error_reporting() |
|
448
|
|
|
return False |
|
449
|
|
|
|
|
450
|
|
|
return True |
|
451
|
|
|
|