Skip to content

Commit 579a5a0

Browse files
authored
Merge pull request #2273 from thseiler/feature/2230-autocomplete-for-tags
UI: autocomplete-for-tags
2 parents c1683ba + b53ec66 commit 579a5a0

File tree

11 files changed

+476
-37
lines changed

11 files changed

+476
-37
lines changed

strictdoc/backend/sdoc/models/document.py

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,22 @@
1515
SDocDocumentContentIF,
1616
SDocDocumentFromFileIF,
1717
SDocDocumentIF,
18+
SDocElementIF,
1819
SDocNodeIF,
1920
SDocSectionIF,
2021
)
22+
from strictdoc.backend.sdoc.models.node import SDocNode
2123
from strictdoc.backend.sdoc.models.type_system import (
2224
GrammarElementField,
2325
GrammarElementFieldMultipleChoice,
2426
GrammarElementFieldSingleChoice,
27+
GrammarElementFieldTag,
2528
)
2629
from strictdoc.core.document_meta import DocumentMeta
2730
from strictdoc.helpers.auto_described import auto_described
2831
from strictdoc.helpers.cast import assert_cast
2932
from strictdoc.helpers.mid import MID
33+
from strictdoc.helpers.ordered_set import OrderedSet
3034

3135

3236
@auto_described
@@ -79,6 +83,55 @@ def __init__(
7983
SDocDocumentFromFileIF
8084
] = None
8185

86+
def iterate_nodes(
87+
self, element_type: Optional[str] = None
88+
) -> Generator[SDocNodeIF, None, None]:
89+
"""
90+
Iterates over all non-[TEXT] nodes in the document. If element_type
91+
is given, then only nodes of type `element_type` are returned.
92+
Otherwise, all element types are returned.
93+
"""
94+
task_list: List[SDocElementIF] = list(self.section_contents)
95+
while task_list:
96+
node = task_list.pop(0)
97+
98+
if isinstance(node, SDocDocumentFromFileIF):
99+
yield from node.iterate_nodes(element_type)
100+
101+
if isinstance(node, SDocNodeIF):
102+
if node.node_type != "TEXT":
103+
if element_type is None or node.node_type == element_type:
104+
yield node
105+
106+
task_list.extend(node.section_contents)
107+
108+
def has_any_requirements(self) -> bool:
109+
return any(True for _ in self.iterate_nodes())
110+
111+
def collect_options_for_tag(
112+
self, element_type: str, field_name: str
113+
) -> List[str]:
114+
"""
115+
Returns the list of existing options for a tag field in this document.
116+
"""
117+
option_set: OrderedSet[str] = OrderedSet()
118+
119+
for nodeif in self.iterate_nodes(element_type):
120+
node = assert_cast(nodeif, SDocNode)
121+
if field_name in node.ordered_fields_lookup:
122+
node_field = node.ordered_fields_lookup[field_name][0]
123+
field_value = node_field.get_text_value()
124+
if field_value:
125+
options = [
126+
option.strip()
127+
for option in field_value.split(",")
128+
if option.strip()
129+
]
130+
for option in options:
131+
option_set.add(option)
132+
133+
return list(option_set)
134+
82135
@property
83136
def uid(self) -> Optional[str]:
84137
return self.config.uid
@@ -152,21 +205,6 @@ def has_any_toc_nodes(self) -> bool:
152205
return True
153206
return False
154207

155-
def has_any_requirements(self) -> bool:
156-
task_list: List[SDocDocumentContentIF] = list(self.section_contents)
157-
while len(task_list) > 0:
158-
section_or_requirement = task_list.pop(0)
159-
if isinstance(section_or_requirement, SDocDocumentFromFileIF):
160-
if section_or_requirement.has_any_requirements():
161-
return True
162-
continue
163-
if isinstance(section_or_requirement, SDocNodeIF):
164-
if section_or_requirement.node_type == "TEXT":
165-
continue
166-
return True
167-
task_list.extend(section_or_requirement.section_contents)
168-
return False
169-
170208
def get_display_title(
171209
self,
172210
include_toc_number: bool = True, # noqa: ARG002
@@ -223,7 +261,7 @@ def get_grammar_element_field_for(
223261
field: GrammarElementField = element.fields_map[field_name]
224262
return field
225263

226-
def get_options_for_choice(
264+
def get_options_for_field(
227265
self, element_type: str, field_name: str
228266
) -> List[str]:
229267
"""
@@ -237,4 +275,8 @@ def get_options_for_choice(
237275
field, GrammarElementFieldMultipleChoice
238276
):
239277
return field.options
278+
279+
if isinstance(field, GrammarElementFieldTag):
280+
return self.collect_options_for_tag(element_type, field_name)
281+
240282
raise AssertionError(f"Must not reach here: {field}")

strictdoc/backend/sdoc/models/document_from_file.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
@relation(SDOC-SRS-109, scope=file)
33
"""
44

5-
from typing import List, Optional, Union
5+
from typing import Generator, List, Optional, Union
66

77
from strictdoc.backend.sdoc.document_reference import DocumentReference
88
from strictdoc.backend.sdoc.models.document import SDocDocument
@@ -34,21 +34,32 @@ def __init__(
3434
self.resolved_full_path_to_document_file: Optional[str] = None
3535
self.resolved_document: Optional[SDocDocumentIF] = None
3636

37-
def has_any_requirements(self) -> bool:
37+
def iterate_nodes(
38+
self, element_type: Optional[str] = None
39+
) -> Generator[SDocNodeIF, None, None]:
40+
"""
41+
Iterates over all non-[TEXT] nodes in the document. If element_type
42+
is given, then only nodes of type `element_type` are returned.
43+
Otherwise, all element types are returned.
44+
"""
45+
3846
task_list: List[SDocElementIF] = list(self.section_contents)
39-
while len(task_list) > 0:
40-
element = task_list.pop(0)
41-
if isinstance(element, SDocNodeIF):
42-
if element.is_normative_node():
43-
return True
44-
continue
45-
46-
if isinstance(element, SDocDocumentFromFileIF):
47-
if element.has_any_requirements():
48-
return True
49-
50-
task_list.extend(element.section_contents)
51-
return False
47+
while task_list:
48+
node = task_list.pop(0)
49+
50+
if isinstance(node, SDocDocumentFromFileIF):
51+
yield from node.iterate_nodes(element_type)
52+
53+
if isinstance(node, SDocNodeIF):
54+
if node.node_type == "TEXT":
55+
continue
56+
if element_type is None or node.node_type == element_type:
57+
yield node
58+
59+
task_list.extend(node.section_contents)
60+
61+
def has_any_requirements(self) -> bool:
62+
return any(True for _ in self.iterate_nodes())
5263

5364
@property
5465
def section_contents(self) -> List[SDocDocumentIF]:

strictdoc/backend/sdoc/models/model.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ class SDocDocumentFromFileIF(ABC):
101101
ng_resolved_custom_level: Optional[str]
102102

103103
@abstractmethod
104-
def has_any_requirements(self) -> bool:
104+
def iterate_nodes(
105+
self,
106+
element_type: Optional[str] = None,
107+
) -> Generator[SDocNodeIF, None, None]:
105108
raise NotImplementedError # pragma: no cover
106109

107110
@property

strictdoc/export/html/form_objects/requirement_form_object.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,14 @@ def is_autocompletable(self) -> bool:
7777
return self.field_gef_type in (
7878
RequirementFieldType.SINGLE_CHOICE,
7979
RequirementFieldType.MULTIPLE_CHOICE,
80+
RequirementFieldType.TAG,
8081
)
8182

8283
def is_multiplechoice(self) -> bool:
83-
return self.field_gef_type == RequirementFieldType.MULTIPLE_CHOICE
84+
return self.field_gef_type in (
85+
RequirementFieldType.MULTIPLE_CHOICE,
86+
RequirementFieldType.TAG,
87+
)
8488

8589
def get_input_field_name(self):
8690
return f"requirement[fields][{self.field_mid}][value]"

strictdoc/server/routers/main_router.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2772,7 +2772,7 @@ def get_autocomplete_field_results(
27722772
field_name: Optional[str] = None,
27732773
):
27742774
"""
2775-
Returns matches of possible values of a SingleChoice or MultiChoice field.
2775+
Returns matches of possible values of a SingleChoice, MultiChoice or tag field.
27762776
The field is identified by the document_mid, the element_type, and the field_name.
27772777
"""
27782778
output = ""
@@ -2787,7 +2787,7 @@ def get_autocomplete_field_results(
27872787
)
27882788
)
27892789
if document:
2790-
all_options = document.get_options_for_choice(
2790+
all_options = document.get_options_for_field(
27912791
element_type, field_name
27922792
)
27932793
field: GrammarElementField = (
@@ -2796,8 +2796,11 @@ def get_autocomplete_field_results(
27962796
)
27972797
)
27982798

2799-
if field.gef_type == RequirementFieldType.MULTIPLE_CHOICE:
2800-
# MultipleChoice: We split the query into its parts:
2799+
if field.gef_type in (
2800+
RequirementFieldType.MULTIPLE_CHOICE,
2801+
RequirementFieldType.TAG,
2802+
):
2803+
# MultipleChoice/Tag: We split the query into its parts:
28012804
#
28022805
# Example User input: "Some Value, Another Value, Yet ano|".
28032806
# parts = ['some value', 'another value', 'yet ano'] # noqa: ERA001
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
[DOCUMENT]
2+
TITLE: Document 1
3+
4+
[GRAMMAR]
5+
ELEMENTS:
6+
- TAG: TEXT
7+
FIELDS:
8+
- TITLE: UID
9+
TYPE: String
10+
REQUIRED: False
11+
- TITLE: STATEMENT
12+
TYPE: String
13+
REQUIRED: True
14+
- TAG: REQUIREMENT
15+
FIELDS:
16+
- TITLE: UID
17+
TYPE: String
18+
REQUIRED: False
19+
- TITLE: LEVEL
20+
TYPE: String
21+
REQUIRED: False
22+
- TITLE: TAGS
23+
TYPE: Tag
24+
REQUIRED: False
25+
- TITLE: TITLE
26+
TYPE: String
27+
REQUIRED: False
28+
- TITLE: STATEMENT
29+
TYPE: String
30+
REQUIRED: False
31+
- TITLE: RATIONALE
32+
TYPE: String
33+
REQUIRED: False
34+
- TITLE: COMMENT
35+
TYPE: String
36+
REQUIRED: False
37+
RELATIONS:
38+
- TYPE: Parent
39+
- TYPE: File
40+
41+
[REQUIREMENT]
42+
UID: REQ-1
43+
TAGS: Tag1
44+
TITLE: Requirement 1 ABC
45+
STATEMENT: >>>
46+
Shall test foo.
47+
<<<
48+
49+
[REQUIREMENT]
50+
UID: REQ-2
51+
TAGS: Tag2
52+
TITLE: Requirement 2 XYZ
53+
STATEMENT: >>>
54+
Shall test bar.
55+
<<<
56+
57+
[REQUIREMENT]
58+
UID: REQ-3
59+
TAGS: Tag1, Tag2
60+
TITLE: New Requirement
61+
STATEMENT: >>>
62+
Shall test foo 2.
63+
<<<
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
[DOCUMENT]
2+
TITLE: Document 1
3+
4+
[GRAMMAR]
5+
ELEMENTS:
6+
- TAG: TEXT
7+
FIELDS:
8+
- TITLE: UID
9+
TYPE: String
10+
REQUIRED: False
11+
- TITLE: STATEMENT
12+
TYPE: String
13+
REQUIRED: True
14+
- TAG: REQUIREMENT
15+
FIELDS:
16+
- TITLE: UID
17+
TYPE: String
18+
REQUIRED: False
19+
- TITLE: LEVEL
20+
TYPE: String
21+
REQUIRED: False
22+
- TITLE: TAGS
23+
TYPE: Tag
24+
REQUIRED: False
25+
- TITLE: TITLE
26+
TYPE: String
27+
REQUIRED: False
28+
- TITLE: STATEMENT
29+
TYPE: String
30+
REQUIRED: False
31+
- TITLE: RATIONALE
32+
TYPE: String
33+
REQUIRED: False
34+
- TITLE: COMMENT
35+
TYPE: String
36+
REQUIRED: False
37+
RELATIONS:
38+
- TYPE: Parent
39+
- TYPE: File
40+
41+
[REQUIREMENT]
42+
UID: REQ-1
43+
TAGS: Tag1
44+
TITLE: Requirement 1 ABC
45+
STATEMENT: >>>
46+
Shall test foo.
47+
<<<
48+
49+
[REQUIREMENT]
50+
UID: REQ-2
51+
TITLE: Requirement 2 XYZ
52+
STATEMENT: >>>
53+
Shall test bar.
54+
<<<
55+

0 commit comments

Comments
 (0)