Skip to content

Commit 146fbe1

Browse files
committed
Track UID_TO_NODE for SDocDocument to resolve LINK to DOCUMENT
We can now use [LINK: DOC] to link to whole documents, where DOC is [DOCUMENT] TITLE: Document UID: DOC in the same or another sdoc.
1 parent ed01b55 commit 146fbe1

File tree

22 files changed

+273
-25
lines changed

22 files changed

+273
-25
lines changed

strictdoc/core/traceability_index.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -354,9 +354,10 @@ def get_node_by_uid_weak2(self, uid: str):
354354

355355
def get_linkable_node_by_uid(
356356
self, uid
357-
) -> Union[SDocNode, SDocSection, Anchor]:
357+
) -> Union[SDocDocument, SDocNode, SDocSection, Anchor]:
358358
return assert_cast(
359-
self.get_node_by_uid(uid), (SDocNode, SDocSection, Anchor)
359+
self.get_node_by_uid(uid),
360+
(SDocDocument, SDocNode, SDocSection, Anchor),
360361
)
361362

362363
def get_node_by_uid_weak(
@@ -383,16 +384,16 @@ def get_node_by_uid_weak(
383384

384385
def get_linkable_node_by_uid_weak(
385386
self, uid
386-
) -> Union[SDocNode, SDocSection, Anchor, None]:
387+
) -> Union[SDocDocument, SDocNode, SDocSection, Anchor, None]:
387388
return assert_optional_cast(
388389
self.graph_database.get_link_value_weak(
389390
link_type=GraphLinkType.UID_TO_NODE, lhs_node=uid
390391
),
391-
(SDocNode, SDocSection, Anchor),
392+
(SDocDocument, SDocNode, SDocSection, Anchor),
392393
)
393394

394395
def get_incoming_links(
395-
self, node: Union[SDocNode, SDocSection, Anchor]
396+
self, node: Union[SDocDocument, SDocNode, SDocSection, Anchor]
396397
) -> Optional[List[InlineLink]]:
397398
incoming_links = self.graph_database.get_link_values_weak(
398399
link_type=GraphLinkType.NODE_TO_INCOMING_LINKS,
@@ -448,6 +449,20 @@ def create_traceability_info(
448449
source_file_rel_path, traceability_info, traceability_index
449450
)
450451

452+
def create_document(self, document: SDocDocument) -> None:
453+
assert isinstance(document, SDocDocument)
454+
if document.reserved_uid is not None:
455+
self.graph_database.create_link(
456+
link_type=GraphLinkType.UID_TO_NODE,
457+
lhs_node=document.reserved_uid,
458+
rhs_node=document,
459+
)
460+
self.graph_database.create_link(
461+
link_type=GraphLinkType.MID_TO_NODE,
462+
lhs_node=document.reserved_mid,
463+
rhs_node=document,
464+
)
465+
451466
def create_section(self, section: SDocSection) -> None:
452467
assert isinstance(section, SDocSection)
453468
if section.reserved_uid is not None:
@@ -469,12 +484,14 @@ def create_inline_link(self, new_link: InlineLink):
469484
if self.graph_database.has_link(
470485
link_type=GraphLinkType.UID_TO_NODE, lhs_node=new_link.link
471486
):
472-
node_or_anchor: Union[SDocNode, SDocSection, Anchor] = assert_cast(
487+
node_or_anchor: Union[
488+
SDocDocument, SDocNode, SDocSection, Anchor
489+
] = assert_cast(
473490
self.graph_database.get_link_value(
474491
link_type=GraphLinkType.UID_TO_NODE,
475492
lhs_node=new_link.link,
476493
),
477-
(SDocNode, SDocSection, Anchor),
494+
(SDocDocument, SDocNode, SDocSection, Anchor),
478495
)
479496
self.graph_database.create_link(
480497
link_type=GraphLinkType.NODE_TO_INCOMING_LINKS,
@@ -767,6 +784,21 @@ def update_disconnect_two_documents_if_no_links_left(
767784
rhs_node=document.reserved_mid,
768785
)
769786

787+
def delete_document(self, document: SDocDocument) -> None:
788+
assert isinstance(document, SDocDocument), document
789+
790+
self.graph_database.delete_link(
791+
link_type=GraphLinkType.MID_TO_NODE,
792+
lhs_node=document.reserved_mid,
793+
rhs_node=document,
794+
)
795+
if document.reserved_uid is not None:
796+
self.graph_database.delete_link(
797+
link_type=GraphLinkType.UID_TO_NODE,
798+
lhs_node=document.reserved_uid,
799+
rhs_node=document,
800+
)
801+
770802
def delete_section(self, section: SDocSection) -> None:
771803
assert isinstance(section, SDocSection), section
772804

strictdoc/core/traceability_index_builder.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,9 @@ def create_from_document_tree(
258258
),
259259
(
260260
GraphLinkType.UID_TO_NODE,
261-
OneToOneDictionary(str, (SDocNode, SDocSection, Anchor)),
261+
OneToOneDictionary(
262+
str, (SDocDocument, SDocNode, SDocSection, Anchor)
263+
),
262264
),
263265
(
264266
GraphLinkType.UID_TO_REQUIREMENT_CONNECTIONS,
@@ -378,7 +380,12 @@ def create_from_document_tree(
378380
lhs_node=document.reserved_mid,
379381
rhs_node=document,
380382
)
381-
# FIXME: Register Document with UID_TO_NODE
383+
if document.uid:
384+
graph_database.create_link(
385+
link_type=GraphLinkType.UID_TO_NODE,
386+
lhs_node=document.uid,
387+
rhs_node=document,
388+
)
382389

383390
document_tags: Dict[str, int] = {}
384391
graph_database.create_link(

strictdoc/core/transforms/update_document_config.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# mypy: disable-error-code="arg-type,no-untyped-call,no-untyped-def"
22
from collections import defaultdict
3-
from typing import Dict, List
3+
from typing import Dict, List, Optional
44

55
from strictdoc.backend.sdoc.models.document import SDocDocument
6+
from strictdoc.backend.sdoc.models.inline_link import InlineLink
67
from strictdoc.core.traceability_index import (
78
TraceabilityIndex,
89
)
@@ -36,11 +37,6 @@ def perform(self):
3637

3738
# Update the document.
3839
document.title = form_object.document_title
39-
document.config.uid = (
40-
form_object.document_uid
41-
if len(form_object.document_uid) > 0
42-
else None
43-
)
4440
document.config.version = (
4541
form_object.document_version
4642
if len(form_object.document_version) > 0
@@ -52,16 +48,48 @@ def perform(self):
5248
else None
5349
)
5450

51+
self.traceability_index.delete_document(document)
52+
53+
document.config.uid = (
54+
form_object.document_uid
55+
if len(form_object.document_uid) > 0
56+
else None
57+
)
58+
59+
self.traceability_index.create_document(document)
60+
5561
def validate(
5662
self,
5763
form_object: DocumentConfigFormObject,
5864
document: SDocDocument,
5965
):
6066
errors: Dict[str, List[str]] = defaultdict(list)
6167
assert isinstance(document, SDocDocument)
68+
6269
if len(form_object.document_title) == 0:
6370
errors["TITLE"].append("Document title must not be empty.")
6471

72+
# Ensure that UID doesn't have any incoming links if it is going to be
73+
# renamed or removed
74+
existing_uid = document.reserved_uid
75+
new_uid = form_object.document_uid
76+
if existing_uid is not None:
77+
if new_uid is None or existing_uid != new_uid:
78+
existing_incoming_links: Optional[List[InlineLink]] = (
79+
self.traceability_index.get_incoming_links(document)
80+
)
81+
if (
82+
existing_incoming_links is not None
83+
and len(existing_incoming_links) > 0
84+
):
85+
errors["UID"].append(
86+
(
87+
"Renaming a node UID when the node has "
88+
"incoming links is not supported yet. "
89+
"Please delete all incoming links first."
90+
),
91+
)
92+
6593
if len(errors):
6694
raise MultipleValidationError(
6795
"Document form has not passed validation.", errors=errors

tests/end2end/helpers/screens/document/screen_document.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,9 @@ def do_drag_first_toc_node_to_the_second(self) -> None:
9898
raise TimeoutError(
9999
"StrictDoc custom timeout: Moving element in the TOC"
100100
)
101+
102+
def do_click_on_tree_document(self, doc_order: int = 1) -> None:
103+
self.test_case.assert_element_not_present("//sdoc-modal", by=By.XPATH)
104+
self.test_case.click_xpath(
105+
f'(//*[@data-testid="tree-document-link"])[{doc_order}]'
106+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[DOCUMENT]
2+
TITLE: Document 1
3+
4+
[REQUIREMENT]
5+
UID: REQ-1
6+
TITLE: Requirement 1
7+
STATEMENT: >>>
8+
Modified. [LINK: DOC-2]
9+
<<<
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[DOCUMENT]
2+
TITLE: Document 2
3+
UID: DOC-2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[DOCUMENT]
2+
TITLE: Document 1
3+
4+
[REQUIREMENT]
5+
UID: REQ-1
6+
TITLE: Requirement 1
7+
STATEMENT: >>>
8+
Nothing here yet.
9+
<<<
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from tests.end2end.e2e_case import E2ECase
2+
from tests.end2end.end2end_test_setup import End2EndTestSetup
3+
from tests.end2end.helpers.components.node.document_root import Form_EditConfig
4+
from tests.end2end.helpers.screens.document.form_edit_requirement import (
5+
Form_EditRequirement,
6+
)
7+
from tests.end2end.helpers.screens.project_index.form_add_document import (
8+
Form_AddDocument,
9+
)
10+
from tests.end2end.helpers.screens.project_index.screen_project_index import (
11+
Screen_ProjectIndex,
12+
)
13+
from tests.end2end.server import SDocTestServer
14+
15+
16+
class Test(E2ECase):
17+
def test(self):
18+
test_setup = End2EndTestSetup(path_to_test_file=__file__)
19+
20+
# Run server.
21+
with SDocTestServer(
22+
input_path=test_setup.path_to_sandbox
23+
) as test_server:
24+
self.open(test_server.get_host_and_port())
25+
26+
screen_project_index = Screen_ProjectIndex(self)
27+
screen_project_index.assert_on_screen()
28+
screen_project_index.assert_contains_document("Document 1")
29+
30+
# Create Document 2 and change UID to DOC-2
31+
form_add_document: Form_AddDocument = (
32+
screen_project_index.do_open_modal_form_add_document()
33+
)
34+
form_add_document.do_fill_in_title("Document 2")
35+
form_add_document.do_fill_in_path("document2.sdoc")
36+
form_add_document.do_form_submit()
37+
38+
screen_document = screen_project_index.do_click_on_the_document(2)
39+
screen_document.assert_on_screen_document()
40+
screen_document.assert_header_document_title("Document 2")
41+
42+
document_node = screen_document.get_root_node()
43+
form_edit_document: Form_EditConfig = (
44+
document_node.do_open_form_edit_config()
45+
)
46+
form_edit_document.do_fill_in_document_uid("DOC-2")
47+
form_edit_document.do_form_submit()
48+
49+
# Go to Document 1 and link requirement statement to DOC-2
50+
screen_document.do_click_on_tree_document(1)
51+
screen_document.assert_header_document_title("Document 1")
52+
53+
req_node_1 = screen_document.get_node(1)
54+
req_node_edit_form: Form_EditRequirement = (
55+
req_node_1.do_open_form_edit_requirement()
56+
)
57+
req_node_edit_form.do_fill_in_field_statement(
58+
"Modified. [LINK: DOC-2]"
59+
)
60+
req_node_edit_form.do_form_submit()
61+
62+
# Click new link and verify it brings us to Document 2
63+
self.click_xpath("//sdoc-node//a[contains(., 'Document 2')]")
64+
screen_document.assert_header_document_title("Document 2")
65+
66+
assert test_setup.compare_sandbox_and_expected_output()

tests/end2end/screens/document/_cross_cutting/LINK_and_ANCHOR/node/_update/_validations/update_node_must_prevent_renaming_or_removing_UID_when_incoming_links/expected_output/document.sdoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
[DOCUMENT]
22
TITLE: Document 1
3+
UID: DOC-1
34

45
[REQUIREMENT]
56
UID: REQ-1
67
STATEMENT: >>>
7-
Hello world
8+
Hello world [LINK: DOC-1]
89
<<<
910

1011
[REQUIREMENT]

tests/end2end/screens/document/_cross_cutting/LINK_and_ANCHOR/node/_update/_validations/update_node_must_prevent_renaming_or_removing_UID_when_incoming_links/input/document.sdoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
[DOCUMENT]
22
TITLE: Document 1
3+
UID: DOC-1
34

45
[REQUIREMENT]
56
UID: REQ-1
67
STATEMENT: >>>
7-
Hello world
8+
Hello world [LINK: DOC-1]
89
<<<
910

1011
[REQUIREMENT]

tests/end2end/screens/document/_cross_cutting/LINK_and_ANCHOR/node/_update/_validations/update_node_must_prevent_renaming_or_removing_UID_when_incoming_links/test_case.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from tests.end2end.e2e_case import E2ECase
22
from tests.end2end.end2end_test_setup import End2EndTestSetup
3+
from tests.end2end.helpers.components.node.document_root import Form_EditConfig
34
from tests.end2end.helpers.screens.document.form_edit_requirement import (
45
Form_EditRequirement,
56
)
@@ -28,6 +29,15 @@ def test(self):
2829
screen_document.assert_on_screen_document()
2930
screen_document.assert_header_document_title("Document 1")
3031

32+
document_node = screen_document.get_root_node()
33+
form_edit_document: Form_EditConfig = (
34+
document_node.do_open_form_edit_config()
35+
)
36+
form_edit_document.do_fill_in_document_uid("DOC-2")
37+
form_edit_document.do_form_submit_and_catch_error(
38+
"Renaming a node UID when the node has incoming links is not supported yet. Please delete all incoming links first."
39+
)
40+
3141
text_node_1 = screen_document.get_node(1)
3242
form_edit_requirement: Form_EditRequirement = (
3343
text_node_1.do_open_form_edit_requirement()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[DOCUMENT]
2+
TITLE: Document 1
3+
UID: DOC-1
4+
5+
[REQUIREMENT]
6+
UID: REQ-2
7+
TITLE: Foo Bar
8+
STATEMENT: >>>
9+
Read [LINK: DOC-1] then [LINK: DOC-2].
10+
<<<
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[DOCUMENT]
2+
TITLE: Document 2
3+
UID: DOC-2
4+
5+
[TEXT]
6+
STATEMENT: >>>
7+
Read [LINK: DOC-2] then [LINK: DOC-1].
8+
<<<
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
RUN: %strictdoc export %S --output-dir Output | filecheck %s --dump-input=fail
2+
CHECK: Published: Document 1
3+
4+
RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input1.html | filecheck %s --dump-input=fail --check-prefix CHECK-HTML-DOC1
5+
6+
CHECK-HTML-DOC1: Read <a href="../11_node_LINK_references_a_document/input1.html#_TOP">🔗&nbsp;Document 1</a> then <a href="../11_node_LINK_references_a_document/input2.html#_TOP">🔗&nbsp;Document 2</a>.
7+
8+
RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input2.html | filecheck %s --dump-input=fail --check-prefix CHECK-HTML-DOC2
9+
10+
CHECK-HTML-DOC2: Read <a href="../11_node_LINK_references_a_document/input2.html#_TOP">🔗&nbsp;Document 2</a> then <a href="../11_node_LINK_references_a_document/input1.html#_TOP">🔗&nbsp;Document 1</a>.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.. _DOC-1:
2+
3+
Document 1
4+
$$$$$$$$$$
5+
6+
.. _REQ-2:
7+
8+
Foo Bar
9+
=======
10+
11+
.. list-table::
12+
:align: left
13+
:header-rows: 0
14+
15+
* - **UID:**
16+
- REQ-2
17+
18+
Read :ref:`Document 1 <DOC-1>` then :ref:`Document 2 <DOC-2>`.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.. _DOC-2:
2+
3+
Document 2
4+
$$$$$$$$$$
5+
6+
Read :ref:`Document 2 <DOC-2>` then :ref:`Document 1 <DOC-1>`.

0 commit comments

Comments
 (0)