Skip to content

Commit 2e365eb

Browse files
authored
Merge pull request #2249 from strictdoc-project/stanislaw/nodes_migration
UI: Delete node: Validate the removed node recursively
2 parents 90434d8 + 686c4ac commit 2e365eb

File tree

24 files changed

+357
-32
lines changed

24 files changed

+357
-32
lines changed

strictdoc/core/document_iterator.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,15 @@ def all_content(
4949
) -> Iterator[SDocElementIF]:
5050
root_node = self.document
5151

52-
yield from self._all_content(
52+
yield from self.all_node_content(
5353
root_node,
5454
print_fragments=print_fragments,
5555
print_fragments_from_files=print_fragments_from_files,
5656
level_stack=(),
5757
custom_level=not root_node.config.auto_levels,
5858
)
5959

60-
def _all_content(
60+
def all_node_content(
6161
self,
6262
node: Union[SDocElementIF, DocumentFromFile],
6363
print_fragments: bool = False,
@@ -108,7 +108,7 @@ def get_level_string_(
108108
):
109109
current_number += 1
110110

111-
yield from self._all_content(
111+
yield from self.all_node_content(
112112
assert_cast(
113113
subnode_,
114114
(
@@ -148,7 +148,7 @@ def get_level_string_(
148148
):
149149
current_number += 1
150150

151-
yield from self._all_content(
151+
yield from self.all_node_content(
152152
subnode_,
153153
print_fragments=print_fragments,
154154
print_fragments_from_files=print_fragments_from_files,
@@ -179,7 +179,7 @@ def get_level_string_(
179179
):
180180
current_number += 1
181181

182-
yield from self._all_content(
182+
yield from self.all_node_content(
183183
subnode_,
184184
print_fragments=print_fragments,
185185
print_fragments_from_files=print_fragments_from_files,
@@ -208,7 +208,7 @@ def get_level_string_(
208208
and subnode_.node_type == "TEXT"
209209
):
210210
current_number += 1
211-
yield from self._all_content(
211+
yield from self.all_node_content(
212212
assert_cast(
213213
subnode_,
214214
(
@@ -234,7 +234,7 @@ def get_level_string_(
234234

235235
assert node.resolved_document is not None
236236

237-
yield from self._all_content(
237+
yield from self.all_node_content(
238238
node.resolved_document,
239239
print_fragments=print_fragments,
240240
print_fragments_from_files=print_fragments_from_files,

strictdoc/core/traceability_index.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -968,13 +968,31 @@ def validate_node_against_anchors(
968968
f"'{incoming_link_parent_node.get_display_title()}'."
969969
)
970970

971-
def validate_node_can_remove_uid(self, *, node: Union[SDocNode, Anchor]):
971+
def validate_can_remove_node(
972+
self, *, node: Union[SDocNode, Anchor]
973+
) -> None:
972974
incoming_links: Optional[List[InlineLink]] = self.get_incoming_links(
973975
node
974976
)
975-
if incoming_links is None or len(incoming_links) == 0:
976-
return
977-
raise SingleValidationError("Cannot remove UID with incoming links.")
977+
if incoming_links is not None and len(incoming_links) > 0:
978+
link_list_message = ", ".join(
979+
map(
980+
lambda l_: f"'{l_.parent_node().get_display_title()}' -> '{l_.link}'",
981+
incoming_links,
982+
)
983+
)
984+
raise SingleValidationError(
985+
f"Cannot remove node '{node.get_display_title()}' with incoming LINKs from: {link_list_message}."
986+
)
987+
child_nodes: List[SDocNode] = self.get_children_requirements(node)
988+
if child_nodes is not None and len(child_nodes) > 0:
989+
nodes_list_message = ", ".join(
990+
map(lambda n_: "'" + n_.get_display_title() + "'", child_nodes)
991+
)
992+
raise SingleValidationError(
993+
f"Cannot remove node '{node.get_display_title()}' "
994+
f"with incoming relations from:\n{nodes_list_message}."
995+
)
978996

979997
def validate_section_can_remove_uid(self, *, section: SDocSection):
980998
section_incoming_links: Optional[List[InlineLink]] = (

strictdoc/core/transforms/delete_requirement.py

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
from typing import Union
1+
from typing import List, Union
22

3+
from strictdoc.backend.sdoc.models.document import SDocDocument
34
from strictdoc.backend.sdoc.models.model import (
45
SDocCompositeNodeIF,
56
SDocDocumentIF,
67
SDocSectionIF,
78
)
89
from strictdoc.backend.sdoc.models.node import SDocCompositeNode, SDocNode
10+
from strictdoc.core.document_iterator import DocumentCachingIterator
911
from strictdoc.core.traceability_index import TraceabilityIndex
1012
from strictdoc.core.transforms.validation_error import (
1113
MultipleValidationErrorAsList,
1214
SingleValidationError,
1315
)
16+
from strictdoc.helpers.cast import assert_cast
1417

1518

1619
class DeleteRequirementCommand:
@@ -23,23 +26,28 @@ def __init__(
2326
self.traceability_index: TraceabilityIndex = traceability_index
2427

2528
def validate(self) -> None:
26-
# FIXME:
27-
# 1) For composite requirements, also validate that all child nodes recursively.
28-
# 2) Double-check the case when removing a node, when there are other nodes
29-
# pointing to it with child/parent relations.
30-
nodes_with_incoming_links = [
31-
self.requirement
32-
] + self.requirement.get_anchors()
33-
for node_ in nodes_with_incoming_links:
34-
try:
35-
self.traceability_index.validate_node_can_remove_uid(node=node_)
36-
except SingleValidationError as exception_:
37-
raise MultipleValidationErrorAsList(
38-
"NOT_RELEVANT",
39-
errors=[
40-
"This node cannot be removed because it contains incoming links."
41-
],
42-
) from exception_
29+
errors: List[str] = []
30+
document: SDocDocument = assert_cast(
31+
self.requirement.get_document(), SDocDocument
32+
)
33+
document_iterator = DocumentCachingIterator(document=document)
34+
for document_node_ in document_iterator.all_node_content(
35+
self.requirement,
36+
print_fragments=True,
37+
print_fragments_from_files=True,
38+
):
39+
if not isinstance(document_node_, SDocNode):
40+
continue
41+
nodes_with_incoming_links = [
42+
document_node_
43+
] + document_node_.get_anchors()
44+
for node_ in nodes_with_incoming_links:
45+
try:
46+
self.traceability_index.validate_can_remove_node(node=node_)
47+
except SingleValidationError as exception_:
48+
errors.append(exception_.args[0])
49+
if len(errors) > 0:
50+
raise MultipleValidationErrorAsList("NOT_RELEVANT", errors)
4351

4452
def perform(self) -> None:
4553
self.validate()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
[DOCUMENT]
2+
TITLE: Document 1
3+
4+
[GRAMMAR]
5+
ELEMENTS:
6+
- TAG: SECTION
7+
PROPERTIES:
8+
IS_COMPOSITE: True
9+
VIEW_STYLE: Narrative
10+
FIELDS:
11+
- TITLE: MID
12+
TYPE: String
13+
REQUIRED: False
14+
- TITLE: UID
15+
TYPE: String
16+
REQUIRED: False
17+
- TITLE: TITLE
18+
TYPE: String
19+
REQUIRED: True
20+
- TAG: REQUIREMENT
21+
FIELDS:
22+
- TITLE: UID
23+
TYPE: String
24+
REQUIRED: False
25+
- TITLE: TITLE
26+
TYPE: String
27+
REQUIRED: False
28+
- TITLE: STATEMENT
29+
TYPE: String
30+
REQUIRED: True
31+
RELATIONS:
32+
- TYPE: Parent
33+
34+
[[SECTION]]
35+
UID: SECTION_NESTING
36+
TITLE: Nesting section
37+
38+
[[SECTION]]
39+
UID: SECTION_SUBNESTING
40+
TITLE: Sub-nesting section
41+
42+
[REQUIREMENT]
43+
UID: REQ-1
44+
TITLE: Requirement title #1
45+
STATEMENT: >>>
46+
Requirement statement #1.
47+
<<<
48+
49+
[[/SECTION]]
50+
51+
[[/SECTION]]
52+
53+
[REQUIREMENT]
54+
UID: REQ-2
55+
TITLE: Requirement title #2
56+
STATEMENT: >>>
57+
Requirement statement #2.
58+
[LINK: SECTION_NESTING]
59+
<<<
60+
RELATIONS:
61+
- TYPE: Parent
62+
VALUE: REQ-1
63+
64+
[REQUIREMENT]
65+
UID: REQ-3
66+
TITLE: Requirement title #3
67+
STATEMENT: >>>
68+
Requirement statement #3.
69+
[LINK: SECTION_SUBNESTING]
70+
<<<
71+
RELATIONS:
72+
- TYPE: Parent
73+
VALUE: REQ-1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
[DOCUMENT]
2+
TITLE: Document 1
3+
4+
[GRAMMAR]
5+
ELEMENTS:
6+
- TAG: SECTION
7+
PROPERTIES:
8+
IS_COMPOSITE: True
9+
VIEW_STYLE: Narrative
10+
FIELDS:
11+
- TITLE: MID
12+
TYPE: String
13+
REQUIRED: False
14+
- TITLE: UID
15+
TYPE: String
16+
REQUIRED: False
17+
- TITLE: TITLE
18+
TYPE: String
19+
REQUIRED: True
20+
- TAG: REQUIREMENT
21+
FIELDS:
22+
- TITLE: UID
23+
TYPE: String
24+
REQUIRED: False
25+
- TITLE: TITLE
26+
TYPE: String
27+
REQUIRED: False
28+
- TITLE: STATEMENT
29+
TYPE: String
30+
REQUIRED: True
31+
RELATIONS:
32+
- TYPE: Parent
33+
34+
[[SECTION]]
35+
UID: SECTION_NESTING
36+
TITLE: Nesting section
37+
38+
[[SECTION]]
39+
UID: SECTION_SUBNESTING
40+
TITLE: Sub-nesting section
41+
42+
[REQUIREMENT]
43+
UID: REQ-1
44+
TITLE: Requirement title #1
45+
STATEMENT: >>>
46+
Requirement statement #1.
47+
<<<
48+
49+
[[/SECTION]]
50+
51+
[[/SECTION]]
52+
53+
[REQUIREMENT]
54+
UID: REQ-2
55+
TITLE: Requirement title #2
56+
STATEMENT: >>>
57+
Requirement statement #2.
58+
[LINK: SECTION_NESTING]
59+
<<<
60+
RELATIONS:
61+
- TYPE: Parent
62+
VALUE: REQ-1
63+
64+
[REQUIREMENT]
65+
UID: REQ-3
66+
TITLE: Requirement title #3
67+
STATEMENT: >>>
68+
Requirement statement #3.
69+
[LINK: SECTION_SUBNESTING]
70+
<<<
71+
RELATIONS:
72+
- TYPE: Parent
73+
VALUE: REQ-1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from tests.end2end.e2e_case import E2ECase
2+
from tests.end2end.end2end_test_setup import End2EndTestSetup
3+
from tests.end2end.helpers.screens.project_index.screen_project_index import (
4+
Screen_ProjectIndex,
5+
)
6+
from tests.end2end.server import SDocTestServer
7+
8+
9+
class Test(E2ECase):
10+
def test(self):
11+
test_setup = End2EndTestSetup(path_to_test_file=__file__)
12+
13+
with SDocTestServer(
14+
input_path=test_setup.path_to_sandbox
15+
) as test_server:
16+
self.open(test_server.get_host_and_port())
17+
18+
screen_project_index = Screen_ProjectIndex(self)
19+
20+
screen_project_index.assert_on_screen()
21+
screen_project_index.assert_contains_document("Document 1")
22+
23+
screen_document = screen_project_index.do_click_on_first_document()
24+
25+
screen_document.assert_on_screen_document()
26+
screen_document.assert_header_document_title("Document 1")
27+
28+
requirement = screen_document.get_node(1)
29+
requirement.assert_requirement_title("Nesting section")
30+
requirement.do_delete_node(proceed_with_confirm=False)
31+
32+
screen_document.assert_text(
33+
"Cannot remove node 'Nesting section' with incoming LINKs from: "
34+
"'Requirement title #2' -> 'SECTION_NESTING'."
35+
)
36+
screen_document.assert_text(
37+
"Cannot remove node 'Sub-nesting section' with incoming LINKs from: "
38+
"'Requirement title #3' -> 'SECTION_SUBNESTING'."
39+
)
40+
screen_document.assert_text(
41+
"Cannot remove node 'Requirement title #1' with incoming relations from: "
42+
"'Requirement title #2', 'Requirement title #3'."
43+
)
44+
45+
assert test_setup.compare_sandbox_and_expected_output()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[DOCUMENT]
2+
TITLE: Document 1
3+
4+
[REQUIREMENT]
5+
UID: REQ-1
6+
TITLE: Requirement title #1
7+
STATEMENT: >>>
8+
Requirement statement #1.
9+
<<<
10+
11+
[REQUIREMENT]
12+
UID: REQ-2
13+
TITLE: Requirement title #2
14+
STATEMENT: >>>
15+
Requirement statement #2.
16+
<<<
17+
RELATIONS:
18+
- TYPE: Parent
19+
VALUE: REQ-1
20+
21+
[REQUIREMENT]
22+
UID: REQ-3
23+
TITLE: Requirement title #3
24+
STATEMENT: >>>
25+
Requirement statement #3.
26+
<<<
27+
RELATIONS:
28+
- TYPE: Parent
29+
VALUE: REQ-1

0 commit comments

Comments
 (0)