Path:
strictdoc/core/traceability_index.py
Lines:
1321
Non-empty lines:
1159
Non-empty lines covered with requirements:
1159 / 1159 (100.0%)
Functions:
64
Functions covered by requirements:
64 / 64 (100.0%)
1
"""2
@relation(SDOC-SRS-28, scope=file)3
"""4
5
import datetime
6
from copy import copy, deepcopy
7
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
8
9
from strictdoc.backend.sdoc.document_reference import DocumentReference
10
from strictdoc.backend.sdoc.models.anchor import Anchor
11
from strictdoc.backend.sdoc.models.document import SDocDocument
12
from strictdoc.backend.sdoc.models.document_config import DocumentConfig
13
from strictdoc.backend.sdoc.models.grammar_element import GrammarElement
14
from strictdoc.backend.sdoc.models.inline_link import InlineLink
15
from strictdoc.backend.sdoc.models.node import SDocNode
16
from strictdoc.backend.sdoc.models.reference import (
17
ChildReqReference,
18
ParentReqReference,
19
)20
from strictdoc.backend.sdoc.node_filter import NodeFilter
21
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
22
RelationMarkerType,
23
SourceFileTraceabilityInfo,
24
)25
from strictdoc.core.asset_manager import AssetManager
26
from strictdoc.core.constants import GraphLinkType
27
from strictdoc.core.document_iterator import SDocDocumentIterator
28
from strictdoc.core.document_meta import DocumentMeta
29
from strictdoc.core.document_tree import DocumentTree
30
from strictdoc.core.file_dependency_manager import FileDependencyManager
31
from strictdoc.core.file_system.source_tree import SourceFile
32
from strictdoc.core.file_traceability_index import FileTraceabilityIndex
33
from strictdoc.core.graph.abstract_bucket import ALL_EDGES
34
from strictdoc.core.graph_database import GraphDatabase
35
from strictdoc.core.project_config import ProjectConfig
36
from strictdoc.core.transforms.constants import NodeCreationOrder
37
from strictdoc.core.transforms.validation_error import (
38
MultipleValidationErrorAsList,
39
SingleValidationError,
40
)41
from strictdoc.core.tree_cycle_detector import TreeCycleDetector
42
from strictdoc.core.validation_index import ValidationIndex
43
from strictdoc.helpers.cast import assert_cast, assert_optional_cast
44
from strictdoc.helpers.file_modification_time import set_file_modification_time
45
from strictdoc.helpers.mid import MID
46
from strictdoc.helpers.ordered_set import OrderedSet
47
from strictdoc.helpers.paths import SDocRelativePath
48
from strictdoc.helpers.sorting import alphanumeric_sort
49
50
51
class TraceabilityIndex:
52
def __init__(
53
self,
54
document_tree: DocumentTree,
55
document_iterators: Dict[SDocDocument, SDocDocumentIterator],
56
file_traceability_index: FileTraceabilityIndex,
57
graph_database: GraphDatabase,
58
file_dependency_manager: FileDependencyManager,
59
):60
self._document_iterators: Dict[SDocDocument, SDocDocumentIterator] = (
61
document_iterators62
)63
self._file_traceability_index: FileTraceabilityIndex = (
64
file_traceability_index65
)66
self.validation_index: ValidationIndex = ValidationIndex()
67
self.graph_database: GraphDatabase = graph_database
68
self.document_tree: DocumentTree = document_tree
69
self.asset_manager: Optional[AssetManager] = None
70
self.file_dependency_manager: FileDependencyManager = (
71
file_dependency_manager72
)73
self.node_filter: Optional[NodeFilter] = None
74
75
self.index_last_updated = datetime.datetime.today()
76
self.contains_included_documents = False
77
self.strictdoc_last_update: datetime.datetime = (
78
datetime.datetime.fromtimestamp(0)
79
)80
81
# The timestamp is used by HTML/JS for invalidating the search index82
# cache in the IndexedDB database.83
# If no documents have to be re-generated with the second+ run of84
# StrictDoc, this timestamp is set of a modification date of the first85
# SDoc document, see export_static_html_search_index.86
self.search_index_timestamp: datetime.datetime = datetime.datetime.now(
87
datetime.timezone.utc
88
)89
90
@property91
def document_iterators(self) -> Dict[SDocDocument, SDocDocumentIterator]:
92
return self._document_iterators
93
94
def is_small_project(self) -> bool:
95
"""
96
Check if project is small to control feature availability.97
98
This method helps to decide if StrictDoc will precompile Jinja templates99
to Python files or not. Precompilation may take half a second time, so100
it is only worth doing it when a project is relatively large.101
102
Below, making some assumptions about what makes a small or larger103
project.104
"""105
if len(self.document_tree.document_list) >= 3:
106
return False
107
for document_ in self.document_tree.document_list:
108
if len(document_.section_contents) > 5:
109
return False
110
return (
111
self.graph_database.get_count(
112
link_type=GraphLinkType.NODE_TO_PARENT_NODES
113
)114
<= 10
115
)116
117
def can_edit_document(self, document: SDocDocument) -> bool:
118
assert isinstance(document, SDocDocument), document
119
return not document.autogen
120
121
def can_edit_node(self, node: Union[SDocDocument, SDocNode]) -> bool:
122
assert isinstance(node, (SDocDocument, SDocNode)), node
123
if isinstance(node, SDocDocument):
124
return not node.autogen
125
126
if node.get_parent_or_including_document().autogen or node.autogen:
127
return False
128
129
return True
130
131
def can_delete_node(self, node: Union[SDocDocument, SDocNode]) -> bool:
132
assert isinstance(node, (SDocDocument, SDocNode)), node
133
if isinstance(node, SDocDocument):
134
return not node.autogen
135
136
if node.get_parent_or_including_document().autogen:
137
return False
138
139
return not node.is_managed_by_source_code()
140
141
def can_clone_node(self, node: Union[SDocDocument, SDocNode]) -> bool:
142
assert isinstance(node, (SDocDocument, SDocNode)), node
143
if isinstance(node, SDocDocument):
144
return False
145
146
return self.can_delete_node(node)
147
148
def can_add_node(self, node: Union[SDocDocument, SDocNode]) -> bool:
149
assert isinstance(node, (SDocDocument, SDocNode)), node
150
if isinstance(node, SDocDocument):
151
return not node.autogen
152
153
if node.get_parent_or_including_document().autogen or node.autogen:
154
return False
155
156
return True
157
158
def can_insert_next_to_node(
159
self, node: Union[SDocDocument, SDocNode]
160
) -> bool:
161
assert isinstance(node, (SDocDocument, SDocNode)), node
162
if isinstance(node, SDocDocument):
163
return True
164
165
if node.get_parent_or_including_document().autogen:
166
return False
167
168
parent = node.parent
169
if isinstance(parent, SDocNode) and parent.autogen:
170
return False
171
172
return True
173
174
def can_create_node_at(
175
self, reference_node: Union[SDocDocument, SDocNode], whereto: str
176
) -> bool:
177
assert isinstance(reference_node, (SDocDocument, SDocNode)), (
178
reference_node179
)180
assert isinstance(whereto, str), whereto
181
182
if whereto == NodeCreationOrder.CHILD:
183
return self.can_add_node(reference_node)
184
if whereto in (NodeCreationOrder.BEFORE, NodeCreationOrder.AFTER):
185
return self.can_insert_next_to_node(reference_node)
186
return False
187
188
def can_move_node(self, node: Union[SDocDocument, SDocNode]) -> bool:
189
assert isinstance(node, (SDocDocument, SDocNode)), node
190
if isinstance(node, SDocDocument):
191
return not node.autogen
192
193
if node.get_parent_or_including_document().autogen or node.autogen:
194
return False
195
196
return True
197
198
def can_move_node_to(
199
self,
200
moved_node: SDocNode,
201
target_node: Union[SDocDocument, SDocNode],
202
whereto: str,
203
) -> bool:
204
assert isinstance(moved_node, SDocNode), moved_node
205
assert isinstance(target_node, (SDocDocument, SDocNode)), target_node
206
assert isinstance(whereto, str), whereto
207
208
if not self.can_move_node(moved_node):
209
return False
210
211
if (
212
whereto == NodeCreationOrder.CHILD
213
and isinstance(target_node, SDocNode)
214
and not target_node.is_composite
215
):216
return self.can_insert_next_to_node(target_node)
217
218
return self.can_create_node_at(target_node, whereto)
219
220
def has_parent_requirements(self, requirement: SDocNode) -> bool:
221
assert isinstance(requirement, SDocNode)
222
if not isinstance(requirement.reserved_uid, str):
223
return False
224
225
parent_requirements = self.graph_database.get_link_values(
226
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
227
lhs_node=requirement,
228
edge=ALL_EDGES,
229
)230
return len(parent_requirements) > 0
231
232
def has_children_requirements(self, requirement: SDocNode) -> bool:
233
assert isinstance(requirement, SDocNode)
234
if not isinstance(requirement.reserved_uid, str):
235
return False
236
237
children_requirements = self.graph_database.get_link_values(
238
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
239
lhs_node=requirement,
240
edge=ALL_EDGES,
241
)242
return len(children_requirements) > 0
243
244
def has_source_file_reqs(self, source_file_rel_path: str) -> bool:
245
return self._file_traceability_index.has_source_file_reqs(
246
source_file_rel_path247
)248
249
def has_node_connections(self, node_uid: str) -> bool:
250
assert isinstance(node_uid, str), node_uid
251
return self.graph_database.has_any_link(
252
link_type=GraphLinkType.UID_TO_NODE,
253
lhs_node=node_uid,
254
)255
256
def get_node_by_mid(self, node_mid: MID) -> Any:
257
assert isinstance(node_mid, MID), node_mid
258
return self.graph_database.get_link_value(
259
link_type=GraphLinkType.MID_TO_NODE, lhs_node=node_mid
260
)261
262
def get_node_by_mid_weak(self, node_mid: MID) -> Optional[Any]:
263
assert isinstance(node_mid, MID), node_mid
264
return self.graph_database.get_link_value_weak(
265
link_type=GraphLinkType.MID_TO_NODE, lhs_node=node_mid
266
)267
268
def get_file_traceability_index(self) -> FileTraceabilityIndex:
269
return self._file_traceability_index
270
271
def get_document_by_title(
272
self,
273
document_title: str,
274
) -> SDocDocument:
275
for document_ in self.document_tree.document_list:
276
if document_.reserved_title == document_title:
277
return document_
278
raise LookupError(f"Document not found: '{document_title}'")
279
280
def get_document_iterator(
281
self, document: SDocDocument
282
) -> SDocDocumentIterator:
283
return SDocDocumentIterator(
284
document=document, node_filter=self.node_filter
285
)286
287
def get_parent_requirements(self, requirement: SDocNode) -> List[SDocNode]:
288
assert isinstance(requirement, SDocNode)
289
if not isinstance(requirement.reserved_uid, str):
290
return []
291
292
return list(
293
self.graph_database.get_link_values(
294
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
295
lhs_node=requirement,
296
edge=ALL_EDGES,
297
)298
)299
300
def get_parent_relations_with_roles(
301
self, node: SDocNode
302
) -> List[Tuple[SDocNode, Optional[str]]]:
303
assert isinstance(node, SDocNode)
304
305
return list(
306
self.graph_database.get_link_values_with_edges(
307
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
308
lhs_node=node,
309
edge=ALL_EDGES,
310
)311
)312
313
def get_parent_relations_with_role(
314
self, requirement: SDocNode, role: Optional[str]
315
) -> List[Tuple[SDocNode, Optional[str]]]:
316
assert isinstance(requirement, SDocNode)
317
if requirement.reserved_uid is None:
318
return []
319
320
return list(
321
self.graph_database.get_link_values_with_edges(
322
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
323
lhs_node=requirement,
324
edge=role,
325
)326
)327
328
def get_child_relations_with_roles(
329
self, requirement: SDocNode
330
) -> List[Tuple[SDocNode, Optional[str]]]:
331
assert isinstance(requirement, SDocNode)
332
333
return list(
334
self.graph_database.get_link_values_with_edges(
335
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
336
lhs_node=requirement,
337
edge=ALL_EDGES,
338
)339
)340
341
def get_child_relations_with_role(
342
self, requirement: SDocNode, role: Optional[str]
343
) -> List[Tuple[SDocNode, Optional[str]]]:
344
assert isinstance(requirement, SDocNode)
345
if requirement.reserved_uid is None:
346
return []
347
348
return list(
349
self.graph_database.get_link_values_with_edges(
350
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
351
lhs_node=requirement,
352
edge=role,
353
)354
)355
356
def get_children_requirements(
357
self, requirement: SDocNode
358
) -> List[SDocNode]:
359
assert isinstance(requirement, SDocNode)
360
if not isinstance(requirement.reserved_uid, str):
361
return []
362
363
return list(
364
self.graph_database.get_link_values(
365
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
366
lhs_node=requirement,
367
edge=ALL_EDGES,
368
)369
)370
371
def has_tags(self, document: SDocDocument) -> bool:
372
return self.graph_database.has_any_link(
373
link_type=GraphLinkType.DOCUMENT_TO_TAGS,
374
lhs_node=document.reserved_mid,
375
)376
377
def get_counted_tags(
378
self, document: SDocDocument
379
) -> Generator[Tuple[str, int], None, None]:
380
document_tags_or_none = self.graph_database.get_link_value(
381
link_type=GraphLinkType.DOCUMENT_TO_TAGS,
382
lhs_node=document.reserved_mid,
383
)384
document_tags: Dict[str, int] = assert_cast(document_tags_or_none, dict)
385
386
tags = sorted(document_tags.keys(), key=alphanumeric_sort)
387
for tag in tags:
388
yield tag, document_tags[tag]
389
390
def get_requirement_file_links(
391
self, requirement: SDocNode
392
) -> List[Tuple[str, List[RelationMarkerType]]]:
393
return self._file_traceability_index.get_requirement_file_links(
394
requirement395
)396
397
def get_source_file_reqs(
398
self, source_file_rel_path: str
399
) -> Optional[List[SDocNode]]:
400
return self._file_traceability_index.get_source_file_reqs(
401
source_file_rel_path402
)403
404
def get_coverage_info(
405
self, source_file_rel_path: str
406
) -> SourceFileTraceabilityInfo:
407
return self._file_traceability_index.get_coverage_info(
408
source_file_rel_path409
)410
411
def get_coverage_info_weak(
412
self, source_file_rel_path: str
413
) -> Optional[SourceFileTraceabilityInfo]:
414
return self._file_traceability_index.get_coverage_info_weak(
415
source_file_rel_path416
)417
418
def get_node_by_uid(self, uid: str) -> Any:
419
assert isinstance(uid, str) and len(uid) > 0, uid
420
return self.graph_database.get_link_value(
421
link_type=GraphLinkType.UID_TO_NODE, lhs_node=uid
422
)423
424
def get_node_by_uid_weak2(self, uid: str) -> Optional[Any]:
425
"""
426
FIXME: This can likely replace _weak below with no problem.427
"""428
assert isinstance(uid, str) and len(uid) > 0, uid
429
return self.graph_database.get_link_value_weak(
430
link_type=GraphLinkType.UID_TO_NODE, lhs_node=uid
431
)432
433
def get_linkable_node_by_uid(
434
self, uid: str
435
) -> Union[SDocDocument, SDocNode, Anchor]:
436
return assert_cast(
437
self.get_node_by_uid(uid),
438
(SDocDocument, SDocNode, Anchor),
439
)440
441
def get_node_by_uid_weak(
442
self, uid: str
443
) -> Union[SDocDocument, SDocNode, None]:
444
assert isinstance(uid, str), uid
445
for document in self.document_tree.document_list:
446
document_iterator = SDocDocumentIterator(document)
447
for node, _ in document_iterator.all_content(print_fragments=False):
448
if isinstance(node, SDocDocument):
449
if node.config.uid == uid:
450
return node
451
elif isinstance(node, SDocNode):
452
if node.reserved_uid == uid:
453
return node
454
else:
455
raise NotImplementedError
456
return None
457
458
def get_linkable_node_by_uid_weak(
459
self, uid: str
460
) -> Union[SDocDocument, SDocNode, Anchor, None]:
461
return assert_optional_cast(
462
self.graph_database.get_link_value_weak(
463
link_type=GraphLinkType.UID_TO_NODE, lhs_node=uid
464
),465
(SDocDocument, SDocNode, Anchor),
466
)467
468
def get_incoming_links(
469
self, node: Union[SDocDocument, SDocNode, Anchor]
470
) -> Optional[List[InlineLink]]:
471
incoming_links = self.graph_database.get_link_values(
472
link_type=GraphLinkType.NODE_TO_INCOMING_LINKS,
473
lhs_node=node.reserved_mid,
474
)475
if incoming_links is None or len(incoming_links) == 0:
476
return None
477
# FIXME: Should the graph database return OrderedSet or a copied list()?478
return list(incoming_links)
479
480
def get_grammar_element(
481
self, document_uid: str, node_type: str
482
) -> Optional[GrammarElement]:
483
document = self.get_node_by_uid_weak2(document_uid)
484
if isinstance(document, SDocDocument) and document.grammar is not None:
485
return document.grammar.elements_by_type.get(node_type)
486
return None
487
488
def create_traceability_info(
489
self,
490
source_file: SourceFile,
491
traceability_info: SourceFileTraceabilityInfo,
492
) -> None:
493
self._file_traceability_index.create_traceability_info(
494
source_file, traceability_info
495
)496
497
def create_document(self, document: SDocDocument) -> None:
498
assert isinstance(document, SDocDocument)
499
if document.reserved_uid is not None:
500
self.graph_database.create_link(
501
link_type=GraphLinkType.UID_TO_NODE,
502
lhs_node=document.reserved_uid,
503
rhs_node=document,
504
)505
self.graph_database.create_link(
506
link_type=GraphLinkType.MID_TO_NODE,
507
lhs_node=document.reserved_mid,
508
rhs_node=document,
509
)510
511
def create_inline_link(self, new_link: InlineLink) -> None:
512
assert isinstance(new_link, InlineLink)
513
514
# InlineLink points to a section, node or to anchor.515
assert self.graph_database.has_any_link(
516
link_type=GraphLinkType.UID_TO_NODE, lhs_node=new_link.link
517
)518
519
node_or_anchor: Union[SDocDocument, SDocNode, Anchor] = assert_cast(
520
self.graph_database.get_link_value(
521
link_type=GraphLinkType.UID_TO_NODE,
522
lhs_node=new_link.link,
523
),524
(SDocDocument, SDocNode, Anchor),
525
)526
self.graph_database.create_link(
527
link_type=GraphLinkType.NODE_TO_INCOMING_LINKS,
528
lhs_node=node_or_anchor.reserved_mid,
529
rhs_node=new_link,
530
)531
self.graph_database.create_link(
532
link_type=GraphLinkType.MID_TO_NODE,
533
lhs_node=new_link.reserved_mid,
534
rhs_node=new_link,
535
)536
537
def update_last_updated(self) -> None:
538
"""
539
Update the index's last updated date to the current time.540
541
This is a rather broad way of signalling that all documents of the index542
need to be re-generated when they are opened next time. Several UI543
actions use this method to ensure a complete re-generation of all544
documents.545
"""546
self.index_last_updated = datetime.datetime.today()
547
548
def create_requirement(self, requirement: SDocNode) -> None:
549
assert isinstance(requirement, SDocNode), requirement
550
551
self.graph_database.create_link(
552
link_type=GraphLinkType.MID_TO_NODE,
553
lhs_node=requirement.reserved_mid,
554
rhs_node=requirement,
555
)556
if requirement.reserved_uid is not None:
557
self.graph_database.create_link(
558
link_type=GraphLinkType.UID_TO_NODE,
559
lhs_node=requirement.reserved_uid,
560
rhs_node=requirement,
561
)562
563
def update_requirement_uid(
564
self, requirement: SDocNode, old_uid: Optional[str]
565
) -> None:
566
if old_uid is None:
567
if requirement.reserved_uid:
568
self.graph_database.create_link(
569
link_type=GraphLinkType.UID_TO_NODE,
570
lhs_node=requirement.reserved_uid,
571
rhs_node=requirement,
572
)573
return574
575
self.graph_database.delete_link(
576
link_type=GraphLinkType.UID_TO_NODE,
577
lhs_node=old_uid,
578
rhs_node=requirement,
579
)580
581
if requirement.reserved_uid is not None:
582
self.graph_database.create_link(
583
link_type=GraphLinkType.UID_TO_NODE,
584
lhs_node=requirement.reserved_uid,
585
rhs_node=requirement,
586
)587
588
def update_requirement_parent_uid(
589
self, requirement: SDocNode, parent_uid: str, role: Optional[str]
590
) -> None:
591
assert requirement.reserved_uid is not None
592
assert isinstance(parent_uid, str), parent_uid
593
assert role is None or len(role) > 0, role
594
parent_nodes: OrderedSet[SDocNode] = (
595
self.graph_database.get_link_values(
596
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
597
lhs_node=requirement,
598
edge=role,
599
)600
)601
# If a relation to the parent uid through a given role already exists,602
# there is nothing to do.603
if any(node_.reserved_uid == parent_uid for node_ in parent_nodes):
604
return605
606
parent_requirement: SDocNode = self.graph_database.get_link_value(
607
link_type=GraphLinkType.UID_TO_NODE,
608
lhs_node=parent_uid,
609
)610
611
document = assert_cast(requirement.get_document(), SDocDocument)
612
parent_requirement_document = assert_cast(
613
parent_requirement.get_document(), SDocDocument
614
)615
616
self.graph_database.create_link(
617
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
618
lhs_node=requirement,
619
rhs_node=parent_requirement,
620
edge=role,
621
)622
self.graph_database.create_link(
623
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
624
lhs_node=parent_requirement,
625
rhs_node=requirement,
626
edge=role,
627
)628
629
cycle_detector = TreeCycleDetector()
630
631
def parent_cycle_traverse_(node_id: str) -> List[Any]:
632
node = self.graph_database.get_link_value(
633
link_type=GraphLinkType.UID_TO_NODE,
634
lhs_node=node_id,
635
)636
return list(
637
map(
638
lambda node_: node_.reserved_uid,
639
self.graph_database.get_link_values(
640
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
641
lhs_node=node,
642
),643
)644
)645
646
cycle_detector.check_node(
647
requirement.reserved_uid,
648
parent_cycle_traverse_,
649
)650
651
# Mark document and parent document (if different) for re-generation.652
assert document.meta is not None
653
set_file_modification_time(
654
document.meta.input_doc_full_path, datetime.datetime.today()
655
)656
if parent_requirement_document != document:
657
assert parent_requirement_document.meta is not None
658
set_file_modification_time(
659
parent_requirement_document.meta.input_doc_full_path,
660
datetime.datetime.today(),
661
)662
663
def update_node_mid(self, node: SDocNode, new_mid: str) -> None:
664
"""
665
Update a node’s MID identifier and update all parent and child node666
relations to reference this new MID.667
668
The graph database contains relations between nodes. Since these669
relations remain unchanged, the job of this function is only to update670
each node's original .relations, so that they point to the new MID.671
"""672
673
old_mid = node.reserved_mid
674
675
#676
# Update all child nodes.677
#678
child_nodes: OrderedSet[SDocNode] = self.graph_database.get_link_values(
679
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
680
lhs_node=node,
681
edge=ALL_EDGES,
682
)683
for child_node_ in child_nodes:
684
child_node_document = child_node_.get_document()
685
assert child_node_document is not None
686
687
if child_node_document.config.relation_field != "MID":
688
continue689
690
for relation_ in copy(child_node_.relations):
691
if (
692
isinstance(relation_, ParentReqReference)
693
and relation_.ref_uid == old_mid
694
):695
relation_.ref_uid = new_mid
696
697
#698
# Update all parent nodes.699
#700
parent_nodes: OrderedSet[SDocNode] = (
701
self.graph_database.get_link_values(
702
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
703
lhs_node=node,
704
edge=ALL_EDGES,
705
)706
)707
for parent_node_ in parent_nodes:
708
parent_node_document = parent_node_.get_document()
709
assert parent_node_document is not None
710
711
if parent_node_document.config.relation_field != "MID":
712
continue713
714
for relation_ in copy(parent_node_.relations):
715
if (
716
isinstance(relation_, ChildReqReference)
717
and relation_.ref_uid == old_mid
718
):719
relation_.ref_uid = new_mid
720
721
#722
# Update the node itself.723
#724
node.set_field_value(
725
field_name="MID",
726
form_field_index=0,
727
value=new_mid,
728
)729
730
def update_requirement_child_uid(
731
self, requirement: SDocNode, child_uid: str, role: Optional[str]
732
) -> None:
733
assert requirement.reserved_uid is not None
734
assert isinstance(child_uid, str), child_uid
735
assert role is None or len(role) > 0, role
736
child_nodes: OrderedSet[SDocNode] = self.graph_database.get_link_values(
737
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
738
lhs_node=requirement,
739
edge=role,
740
)741
742
# If a relation to the parent uid through a given role already exists,743
# there is nothing to do.744
if any(node_.reserved_uid == child_uid for node_ in child_nodes):
745
return746
child_requirement = self.graph_database.get_link_value(
747
link_type=GraphLinkType.UID_TO_NODE,
748
lhs_node=child_uid,
749
)750
751
document = assert_cast(requirement.get_document(), SDocDocument)
752
child_requirement_document = assert_cast(
753
child_requirement.get_document(), SDocDocument
754
)755
756
self.graph_database.create_link(
757
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
758
lhs_node=child_requirement,
759
rhs_node=requirement,
760
edge=role,
761
)762
self.graph_database.create_link(
763
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
764
lhs_node=requirement,
765
rhs_node=child_requirement,
766
edge=role,
767
)768
769
cycle_detector = TreeCycleDetector()
770
771
def child_cycle_traverse_(node_id: str) -> List[Any]:
772
node = self.graph_database.get_link_value(
773
link_type=GraphLinkType.UID_TO_NODE,
774
lhs_node=node_id,
775
)776
return list(
777
map(
778
lambda node_: node_.reserved_uid,
779
self.graph_database.get_link_values(
780
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
781
lhs_node=node,
782
),783
)784
)785
786
cycle_detector.check_node(
787
requirement.reserved_uid,
788
child_cycle_traverse_,
789
)790
791
# Mark document and parent document (if different) for re-generation.792
assert document.meta is not None
793
set_file_modification_time(
794
document.meta.input_doc_full_path, datetime.datetime.today()
795
)796
if child_requirement_document != document:
797
assert child_requirement_document.meta is not None
798
set_file_modification_time(
799
child_requirement_document.meta.input_doc_full_path,
800
datetime.datetime.today(),
801
)802
803
def update_with_anchor(self, anchor: Anchor) -> None:
804
# By this time, we know that the validations have passed just before.805
existing_anchor: Optional[Anchor] = (
806
self.graph_database.get_link_value_weak(
807
link_type=GraphLinkType.UID_TO_NODE,
808
lhs_node=anchor.value,
809
)810
)811
if existing_anchor is not None:
812
self.graph_database.delete_link(
813
link_type=GraphLinkType.MID_TO_NODE,
814
lhs_node=existing_anchor.mid,
815
rhs_node=existing_anchor,
816
)817
self.graph_database.delete_link(
818
link_type=GraphLinkType.UID_TO_NODE,
819
lhs_node=existing_anchor.value,
820
rhs_node=existing_anchor,
821
)822
823
self.graph_database.create_link(
824
link_type=GraphLinkType.MID_TO_NODE,
825
lhs_node=anchor.mid,
826
rhs_node=anchor,
827
)828
self.graph_database.create_link(
829
link_type=GraphLinkType.UID_TO_NODE,
830
lhs_node=anchor.value,
831
rhs_node=anchor,
832
)833
834
def update_disconnect_two_documents_if_no_links_left(
835
self, document: SDocDocument, other_document: SDocDocument
836
) -> None:
837
assert document != other_document
838
839
for node, _ in self.document_iterators[document].all_content(
840
print_fragments=False
841
):842
if not isinstance(node, SDocNode):
843
continue844
845
requirement_node: SDocNode = node
846
847
# If a requirement has no UID, it cannot contribute to any relation-based848
# connection between any two documents.849
if requirement_node.reserved_uid is None:
850
continue851
852
requirement_parents: OrderedSet[SDocNode] = (
853
self.graph_database.get_link_values(
854
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
855
lhs_node=requirement_node,
856
)857
)858
859
# If at least one parent or child relation points to the other860
# document, terminate, not deleting the link between documents.861
for parent_requirement in requirement_parents:
862
parent_requirement_document: SDocDocument = assert_cast(
863
parent_requirement.get_document(), SDocDocument
864
)865
if parent_requirement_document == other_document:
866
return867
868
requirement_children = self.graph_database.get_link_values(
869
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
870
lhs_node=requirement_node,
871
)872
873
for child_requirement_, _ in requirement_children:
874
if child_requirement_.document == other_document:
875
return876
877
def delete_document(self, document: SDocDocument) -> None:
878
assert isinstance(document, SDocDocument), document
879
880
self.graph_database.delete_link(
881
link_type=GraphLinkType.MID_TO_NODE,
882
lhs_node=document.reserved_mid,
883
rhs_node=document,
884
)885
if document.reserved_uid is not None:
886
self.graph_database.delete_link(
887
link_type=GraphLinkType.UID_TO_NODE,
888
lhs_node=document.reserved_uid,
889
rhs_node=document,
890
)891
892
def delete_requirement(self, requirement: SDocNode) -> None:
893
assert isinstance(requirement, SDocNode), SDocNode
894
895
self.graph_database.delete_link(
896
link_type=GraphLinkType.MID_TO_NODE,
897
lhs_node=requirement.reserved_mid,
898
rhs_node=requirement,
899
)900
if requirement.reserved_uid is not None:
901
self.graph_database.delete_link(
902
link_type=GraphLinkType.UID_TO_NODE,
903
lhs_node=requirement.reserved_uid,
904
rhs_node=requirement,
905
)906
self.graph_database.delete_all_links(
907
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
908
lhs_node=requirement,
909
)910
self.graph_database.delete_all_links(
911
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
912
lhs_node=requirement,
913
)914
915
def remove_requirement_parent_uid(
916
self, requirement: SDocNode, parent_uid: str, role: Optional[str]
917
) -> None:
918
assert requirement.reserved_uid is not None
919
assert isinstance(parent_uid, str), parent_uid
920
assert role is None or len(role) > 0, role
921
922
parent_requirement: SDocNode = self.graph_database.get_link_value(
923
link_type=GraphLinkType.UID_TO_NODE,
924
lhs_node=parent_uid,
925
)926
927
self.graph_database.delete_link(
928
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
929
lhs_node=requirement,
930
rhs_node=parent_requirement,
931
edge=role,
932
)933
self.graph_database.delete_link(
934
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
935
lhs_node=parent_requirement,
936
rhs_node=requirement,
937
edge=role,
938
)939
940
document = assert_cast(requirement.get_document(), SDocDocument)
941
parent_requirement_document = assert_cast(
942
parent_requirement.get_document(), SDocDocument
943
)944
945
# If there are no requirements linking between the documents,946
# remove the link.947
if document != parent_requirement_document:
948
self.update_disconnect_two_documents_if_no_links_left(
949
document, parent_requirement_document
950
)951
952
# Mark document and parent document (if different) for re-generation.953
assert document.meta is not None
954
set_file_modification_time(
955
document.meta.input_doc_full_path, datetime.datetime.today()
956
)957
if parent_requirement_document != document:
958
assert parent_requirement_document.meta is not None
959
set_file_modification_time(
960
parent_requirement_document.meta.input_doc_full_path,
961
datetime.datetime.today(),
962
)963
964
def remove_requirement_child_uid(
965
self, requirement: SDocNode, child_uid: str, role: Optional[str]
966
) -> None:
967
assert requirement.reserved_uid is not None
968
assert isinstance(child_uid, str), child_uid
969
assert role is None or len(role) > 0, role
970
971
child_requirement: SDocNode = self.graph_database.get_link_value(
972
link_type=GraphLinkType.UID_TO_NODE,
973
lhs_node=child_uid,
974
)975
document: SDocDocument = assert_cast(
976
requirement.get_document(), SDocDocument
977
)978
child_requirement_document: SDocDocument = assert_cast(
979
child_requirement.get_document(), SDocDocument
980
)981
982
self.graph_database.delete_link(
983
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
984
lhs_node=requirement,
985
rhs_node=child_requirement,
986
edge=role,
987
)988
self.graph_database.delete_link(
989
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
990
lhs_node=child_requirement,
991
rhs_node=requirement,
992
edge=role,
993
)994
995
# If there are no requirements linking between the documents,996
# remove the link.997
if document != child_requirement_document:
998
self.update_disconnect_two_documents_if_no_links_left(
999
document, child_requirement_document
1000
)1001
1002
# Mark document and parent document (if different) for re-generation.1003
assert document.meta is not None
1004
set_file_modification_time(
1005
document.meta.input_doc_full_path, datetime.datetime.today()
1006
)1007
if child_requirement_document != document:
1008
assert child_requirement_document.meta is not None
1009
set_file_modification_time(
1010
child_requirement_document.meta.input_doc_full_path,
1011
datetime.datetime.today(),
1012
)1013
1014
def remove_inline_link(self, inline_link: InlineLink) -> None:
1015
sections_with_incoming_links = (
1016
self.graph_database.get_link_values_reverse(
1017
link_type=GraphLinkType.NODE_TO_INCOMING_LINKS,
1018
rhs_node=inline_link,
1019
)1020
)1021
1022
for node_with_incoming_links in list(sections_with_incoming_links):
1023
self.graph_database.delete_link(
1024
link_type=GraphLinkType.NODE_TO_INCOMING_LINKS,
1025
lhs_node=node_with_incoming_links,
1026
rhs_node=inline_link,
1027
)1028
1029
self.graph_database.delete_link(
1030
link_type=GraphLinkType.MID_TO_NODE,
1031
lhs_node=inline_link.reserved_mid,
1032
rhs_node=inline_link,
1033
)1034
1035
def remove_anchor_by_uid(self, anchor_uid: str) -> None:
1036
anchor: Anchor = self.graph_database.get_link_value(
1037
link_type=GraphLinkType.UID_TO_NODE,
1038
lhs_node=anchor_uid,
1039
)1040
self.graph_database.delete_link(
1041
link_type=GraphLinkType.MID_TO_NODE,
1042
lhs_node=anchor.mid,
1043
rhs_node=anchor,
1044
)1045
self.graph_database.delete_link(
1046
link_type=GraphLinkType.UID_TO_NODE,
1047
lhs_node=anchor_uid,
1048
rhs_node=anchor,
1049
)1050
1051
def validate_node_against_anchors(
1052
self,
1053
*,
1054
node: Union[SDocDocument, SDocNode, None],
1055
new_anchors: List[Anchor],
1056
) -> None:
1057
assert node is None or isinstance(node, (SDocDocument, SDocNode))
1058
assert isinstance(new_anchors, list)
1059
1060
# Check that this node does not have duplicated anchors.1061
new_anchor_uids = set()
1062
for anchor in new_anchors:
1063
if anchor.value in new_anchor_uids:
1064
raise SingleValidationError(
1065
"A node cannot have two anchors with "1066
f"the same identifier: {anchor.value}."
1067
)1068
new_anchor_uids.add(anchor.value)
1069
1070
# If the node is new, the validation is easier: we just need to make1071
# sure that there are no existing anchors with the UIDs brought1072
# by the new anchors.1073
if node is None:
1074
for anchor_uid in new_anchor_uids:
1075
if self.graph_database.has_any_link(
1076
link_type=GraphLinkType.UID_TO_NODE, lhs_node=anchor_uid
1077
):1078
raise SingleValidationError(
1079
"A node contains an anchor that already exists: "1080
f"{anchor_uid}."
1081
)1082
return1083
1084
# If the node is an existing node, we need to check that:1085
# 1) If some of the new anchors already exist in the project tree, we1086
# need to ensure that they exist in the current node, otherwise we1087
# raise a duplication validation error.1088
# 2) If the new anchors do not contain some of the existing node's1089
# current anchors, this means these anchors are being removed. In1090
# that case, we need to check if these anchors are used by any LINKs,1091
# raising a validation if they do.1092
existing_node_anchor_uids = set()
1093
1094
# FIXME: No test reaches this for Section or Document.1095
assert isinstance(node, SDocNode)
1096
for node_anchor_ in node.get_anchors():
1097
existing_node_anchor_uids.add(node_anchor_.value)
1098
1099
#1100
# Validation 1: Assert all UIDs are either:1101
# a) new1102
# b) exist in this node1103
# c) raise a duplication error.1104
#1105
for anchor_uid in new_anchor_uids:
1106
if (
1107
self.graph_database.has_any_link(
1108
link_type=GraphLinkType.UID_TO_NODE, lhs_node=anchor_uid
1109
)1110
and anchor_uid not in existing_node_anchor_uids
1111
):1112
duplicate_anchor: Anchor = self.graph_database.get_link_value(
1113
link_type=GraphLinkType.UID_TO_NODE, lhs_node=anchor_uid
1114
)1115
node_with_duplicate_anchor: SDocNode = assert_cast(
1116
duplicate_anchor.parent_node(), SDocNode
1117
)1118
raise SingleValidationError(
1119
"Another node contains an anchor with the same UID: "1120
f"{anchor_uid}. {node_with_duplicate_anchor.get_display_node_type()}: "
1121
f"{node_with_duplicate_anchor.get_display_title()}."
1122
)1123
1124
#1125
# Validation 2: Check that removed anchors do not have any incoming1126
# links.1127
#1128
to_be_removed_anchor_uids = existing_node_anchor_uids - new_anchor_uids
1129
for to_be_removed_anchor_uid_ in to_be_removed_anchor_uids:
1130
to_be_removed_anchor: Anchor = self.graph_database.get_link_value(
1131
link_type=GraphLinkType.UID_TO_NODE,
1132
lhs_node=to_be_removed_anchor_uid_,
1133
)1134
incoming_links = self.graph_database.get_link_values(
1135
link_type=GraphLinkType.NODE_TO_INCOMING_LINKS,
1136
lhs_node=to_be_removed_anchor.reserved_mid,
1137
)1138
if incoming_links is not None and len(incoming_links) > 0:
1139
incoming_link: InlineLink = incoming_links[0]
1140
incoming_link_parent_node = incoming_link.parent_node()
1141
raise SingleValidationError(
1142
f"Cannot remove anchor with UID "
1143
f"'{incoming_link.link}' because it has incoming "
1144
f"links. Containing node: "
1145
f"{incoming_link_parent_node.get_display_node_type()}: "
1146
f"'{incoming_link_parent_node.get_display_title()}'."
1147
)1148
1149
def validate_can_remove_node(
1150
self, *, node: Union[SDocNode, Anchor]
1151
) -> None:
1152
incoming_links: Optional[List[InlineLink]] = self.get_incoming_links(
1153
node1154
)1155
if incoming_links is not None and len(incoming_links) > 0:
1156
link_list_message = ", ".join(
1157
map(
1158
lambda l_: (
1159
f"'{l_.parent_node().get_display_title()}' -> '{l_.link}'"
1160
),1161
incoming_links,
1162
)1163
)1164
raise SingleValidationError(
1165
f"Cannot remove node '{node.get_display_title()}' with incoming LINKs from: {link_list_message}."
1166
)1167
if isinstance(node, SDocNode):
1168
child_nodes: List[SDocNode] = self.get_children_requirements(node)
1169
if child_nodes is not None and len(child_nodes) > 0:
1170
nodes_list_message = ", ".join(
1171
map(
1172
lambda n_: "'" + n_.get_display_title() + "'",
1173
child_nodes,
1174
)1175
)1176
raise SingleValidationError(
1177
f"Cannot remove node '{node.get_display_title()}' "
1178
f"with incoming relations from:\n{nodes_list_message}."
1179
)1180
1181
def validate_can_remove_document(self, document: SDocDocument) -> None:
1182
errors: List[str] = []
1183
1184
if document.document_is_included():
1185
including_document = document.get_including_document()
1186
if including_document is not None:
1187
errors.append(
1188
f"Cannot remove document '{document.get_display_title()}' because it is included in document '{including_document.get_display_title()}'."
1189
)1190
else:
1191
errors.append(
1192
f"Cannot remove document '{document.get_display_title()}' because it is included in another document."
1193
)1194
1195
incoming_links: Optional[List[InlineLink]] = self.get_incoming_links(
1196
document1197
)1198
if incoming_links is not None and len(incoming_links) > 0:
1199
link_list_message = ", ".join(
1200
map(
1201
lambda l_: (
1202
f"'{l_.parent_node().get_display_title()}' -> '{l_.link}'"
1203
),1204
incoming_links,
1205
)1206
)1207
errors.append(
1208
f"Cannot remove document '{document.get_display_title()}' with incoming LINKs from: {link_list_message}."
1209
)1210
1211
document_iterator = SDocDocumentIterator(document=document)
1212
for document_node_, _ in document_iterator.all_content(
1213
print_fragments=True
1214
):1215
if not isinstance(document_node_, SDocNode):
1216
continue1217
1218
nodes_with_incoming_links = [
1219
document_node_1220
] + document_node_.get_anchors()
1221
for node_ in nodes_with_incoming_links:
1222
try:
1223
self.validate_can_remove_node(node=node_)
1224
except SingleValidationError as exception_:
1225
errors.append(exception_.args[0])
1226
1227
parent_nodes: List[SDocNode] = self.get_parent_requirements(
1228
document_node_1229
)1230
if len(parent_nodes) > 0:
1231
nodes_list_message = ", ".join(
1232
map(
1233
lambda n_: "'" + n_.get_display_title() + "'",
1234
parent_nodes,
1235
)1236
)1237
errors.append(
1238
f"Cannot remove document '{document_node_.get_display_title()}' "
1239
f"with incoming relations from:\n{nodes_list_message}."
1240
)1241
1242
if len(errors) > 0:
1243
raise MultipleValidationErrorAsList("NOT_RELEVANT", errors)
1244
- "6.7.1. Export to HTML content to PDF (HTML2PDF)" (REQUIREMENT)
1245
def clone_to_bundle_document(
1246
self, project_config: ProjectConfig
1247
) -> Tuple["TraceabilityIndex", SDocDocument]:
1248
"""
1249
Clone this traceability index and create a new bundle document.1250
1251
The only use case for this method is the generation of a bundle document.1252
Since the bundle document does not exist on file system, some parameters1253
are set artificially:1254
- The bundle is assumed to be an input file in the root input folder.1255
- The bundle is generated to the root of the output folder (level=0).1256
- Some variables do not contribute (yet) to the final result, so they1257
are marked as NOT_RELEVANT.1258
1259
@relation(SDOC-SRS-51, scope=function)1260
"""1261
traceability_index_copy = deepcopy(self)
1262
bundle_document = SDocDocument(
1263
mid=None,
1264
title=project_config.project_title,
1265
config=None,
1266
view=None,
1267
grammar=None,
1268
section_contents=[],
1269
is_bundle_document=True,
1270
)1271
bundle_document.config = DocumentConfig.default_config(bundle_document)
1272
1273
if (
1274
project_config.bundle_document_uid is not None
1275
and len(project_config.bundle_document_uid) > 0
1276
):1277
bundle_document.config.uid = project_config.bundle_document_uid
1278
1279
if (
1280
project_config.bundle_document_version is not None
1281
and len(project_config.bundle_document_version) > 0
1282
):1283
bundle_document.config.version = (
1284
project_config.bundle_document_version
1285
)1286
1287
if (
1288
project_config.bundle_document_date is not None
1289
and len(project_config.bundle_document_date) > 0
1290
):1291
bundle_document.config.date = project_config.bundle_document_date
1292
1293
bundle_document.meta = DocumentMeta(
1294
level=0,
1295
file_tree_mount_folder="NOT_RELEVANT",
1296
document_filename="bundle.sdoc",
1297
document_filename_base="bundle",
1298
input_doc_full_path="NOT_RELEVANT",
1299
input_doc_rel_path=SDocRelativePath("bundle.sdoc"),
1300
input_doc_dir_rel_path=SDocRelativePath(""),
1301
input_doc_assets_dir_rel_path=SDocRelativePath("NOT_RELEVANT"),
1302
output_document_dir_full_path=project_config.export_output_html_root,
1303
output_document_dir_rel_path=SDocRelativePath(""),
1304
)1305
traceability_index_copy.document_iterators[bundle_document] = (
1306
SDocDocumentIterator(bundle_document)
1307
)1308
for document_ in traceability_index_copy.document_tree.document_list:
1309
# Ignore all included documents. They are anyway included by1310
# the including documents.1311
if document_.document_is_included():
1312
continue1313
1314
assert document_.ng_including_document_reference is not None
1315
document_.ng_including_document_reference.set_document(
1316
bundle_document1317
)1318
bundle_document.section_contents.append(document_)
1319
traceability_index_copy.document_tree.document_list = [bundle_document]
1320
bundle_document.ng_including_document_reference = DocumentReference()
1321
return traceability_index_copy, bundle_document