Path:
strictdoc/core/transforms/update_requirement.py
Lines:
523
Non-empty lines:
474
Non-empty lines covered with requirements:
474 / 474 (100.0%)
Functions:
10
Functions covered by requirements:
10 / 10 (100.0%)
1
"""2
@relation(SDOC-SRS-55, scope=file)3
"""4
5
import datetime
6
from copy import copy
7
from dataclasses import dataclass
8
from typing import Dict, List, Optional, Set, Tuple, Union
9
10
from textx import TextXSyntaxError
11
12
from strictdoc.backend.sdoc.document_reference import DocumentReference
13
from strictdoc.backend.sdoc.error_handling import get_textx_syntax_error_message
14
from strictdoc.backend.sdoc.free_text_reader import SDFreeTextReader
15
from strictdoc.backend.sdoc.models.anchor import Anchor
16
from strictdoc.backend.sdoc.models.document import SDocDocument
17
from strictdoc.backend.sdoc.models.free_text import FreeTextContainer
18
from strictdoc.backend.sdoc.models.grammar_element import GrammarElement
19
from strictdoc.backend.sdoc.models.inline_link import InlineLink
20
from strictdoc.backend.sdoc.models.model import SDocDocumentIF, SDocNodeIF
21
from strictdoc.backend.sdoc.models.node import SDocNode, SDocNodeField
22
from strictdoc.backend.sdoc.models.object_factory import SDocObjectFactory
23
from strictdoc.backend.sdoc.models.reference import (
24
Reference,
25
)26
from strictdoc.core.constants import (
27
GraphLinkType,
28
)29
from strictdoc.core.project_config import ProjectConfig
30
from strictdoc.core.traceability_index import (
31
TraceabilityIndex,
32
)33
from strictdoc.core.transforms.constants import NodeCreationOrder
34
from strictdoc.core.transforms.validation_error import (
35
SingleValidationError,
36
)37
from strictdoc.export.html.form_objects.requirement_form_object import (
38
RequirementFormField,
39
RequirementFormFieldType,
40
RequirementFormObject,
41
)42
from strictdoc.export.rst.rst_to_html_fragment_writer import (
43
RstToHtmlFragmentWriter,
44
)45
from strictdoc.helpers.cast import assert_cast
46
from strictdoc.helpers.file_modification_time import set_file_modification_time
47
from strictdoc.helpers.mid import MID
48
49
50
@dataclass51
class CreateNodeInfo:
52
whereto: str
53
requirement_mid: str
54
reference_mid: str
55
56
57
@dataclass58
class UpdateNodeInfo:
59
node_to_update: SDocNode
60
61
62
class UpdateRequirementActionObject:
63
def __init__(self) -> None:
64
self.existing_references_uids: Set[Tuple[str, str, Optional[str]]] = (
65
set()
66
)67
self.reference_ids_to_remove: Set[Tuple[str, str, Optional[str]]] = (
68
set()
69
)70
self.removed_uid_parent_documents_to_update: Set[SDocDocument] = set()
71
# All requirements that have to be updated. This set includes72
# the requirement itself, all links it was linking to73
# (for deleted links) and all links it is linking to now74
# (including new links).75
self.this_document_requirements_to_update: Set[SDocNode] = set()
76
77
78
@dataclass79
class CreateOrUpdateNodeResult:
80
this_document_requirements_to_update: List[SDocNode]
81
82
83
class CreateOrUpdateNodeCommand:
84
def __init__(
85
self,
86
*,
87
form_object: RequirementFormObject,
88
node_info: Union[CreateNodeInfo, UpdateNodeInfo],
89
context_document: SDocDocument,
90
traceability_index: TraceabilityIndex,
91
project_config: ProjectConfig,
92
) -> None:
93
self.form_object: RequirementFormObject = form_object
94
self.node_info: Union[CreateNodeInfo, UpdateNodeInfo] = node_info
95
self.context_document: SDocDocument = context_document
96
self.traceability_index: TraceabilityIndex = traceability_index
97
self.project_config: ProjectConfig = project_config
98
99
def perform(self) -> Optional[CreateOrUpdateNodeResult]:
100
form_object: RequirementFormObject = self.form_object
101
node_to_update_or_none: Optional[SDocNode] = (
102
self.node_info.node_to_update
103
if isinstance(self.node_info, UpdateNodeInfo)
104
else None
105
)106
107
traceability_index: TraceabilityIndex = self.traceability_index
108
109
map_form_to_requirement_fields: Dict[
110
RequirementFormField, Optional[FreeTextContainer]
111
] = {}
112
this_node_unique_anchors: Set[str] = set()
113
for field_bucket_ in form_object.fields.values():
114
field_: RequirementFormField
115
for field_ in field_bucket_:
116
if field_.field_type != RequirementFormFieldType.MULTILINE:
117
continue118
(119
parsed_html,
120
rst_error,
121
) = RstToHtmlFragmentWriter(
122
project_config=self.project_config,
123
context_document=self.context_document,
124
).write_with_validation(field_.field_value)
125
if parsed_html is None:
126
assert rst_error is not None
127
form_object.add_error(field_.field_name, rst_error)
128
else:
129
try:
130
free_text_container: Optional[FreeTextContainer] = (
131
SDFreeTextReader.read(field_.field_value)
132
if len(field_.field_value) > 0
133
else None
134
)135
map_form_to_requirement_fields[field_] = (
136
free_text_container137
)138
if free_text_container is None:
139
continue140
anchors: List[Anchor] = []
141
for part in free_text_container.parts:
142
if isinstance(part, InlineLink):
143
linked_to_node = traceability_index.get_linkable_node_by_uid_weak(
144
part.link
145
)146
if linked_to_node is None:
147
form_object.add_error(
148
field_.field_name,
149
"A LINK points to a node that does "150
f"not exist: '{part.link}'.",
151
)152
elif isinstance(part, Anchor):
153
if part.value in this_node_unique_anchors:
154
form_object.add_error(
155
field_.field_name,
156
"The node fields contain duplicate anchor: "157
f"'{part.value}'.",
158
)159
break160
this_node_unique_anchors.add(part.value)
161
anchors.append(part)
162
else:
163
pass164
if anchors is not None:
165
try:
166
traceability_index.validate_node_against_anchors(
167
node=node_to_update_or_none,
168
new_anchors=anchors,
169
)170
except (
171
SingleValidationError172
) as anchors_validation_error:
173
form_object.add_error(
174
field_.field_name,
175
anchors_validation_error.args[0],
176
)177
except TextXSyntaxError as exception:
178
form_object.add_error(
179
field_.field_name,
180
get_textx_syntax_error_message(exception),
181
)182
183
if form_object.any_errors():
184
return None
185
186
requirement: SDocNode
187
document: SDocDocument
188
existing_uid: Optional[str]
189
existing_node_fields: List[SDocNodeField] = []
190
if isinstance(self.node_info, UpdateNodeInfo):
191
requirement = self.node_info.node_to_update
192
document = assert_cast(requirement.get_document(), SDocDocument)
193
194
existing_uid = requirement.reserved_uid
195
196
existing_node_fields = list(requirement.enumerate_fields())
197
198
# Clearing all existing fields because they will be recreated from199
# scratch from the form data.200
requirement.ordered_fields_lookup.clear()
201
202
self.populate_node_fields_from_form_object(
203
requirement, form_object, map_form_to_requirement_fields
204
)205
206
# Updating Traceability Index: UID.207
traceability_index.update_requirement_uid(
208
requirement=requirement, old_uid=existing_uid
209
)210
else:
211
reference_node: Union[SDocDocument, SDocNode] = (
212
traceability_index.get_node_by_mid(
213
MID(self.node_info.reference_mid)
214
)215
)216
document = traceability_index.get_node_by_mid(
217
MID(form_object.document_mid)
218
)219
parent: Union[SDocDocumentIF, SDocNodeIF]
220
if self.node_info.whereto == NodeCreationOrder.CHILD:
221
parent = reference_node
222
insert_to_idx = len(parent.section_contents)
223
elif self.node_info.whereto == NodeCreationOrder.BEFORE:
224
# Be aware of an edge case besides all normal cases:225
# A reference node can be a root node of an included document.226
if not isinstance(reference_node, SDocDocument):
227
parent = reference_node.parent
228
insert_to_idx = parent.section_contents.index(
229
reference_node230
)231
else:
232
assert (
233
reference_node.ng_including_document_from_file
234
is not None
235
)236
parent = (
237
reference_node.ng_including_document_from_file.parent
238
)239
assert (
240
reference_node.ng_including_document_from_file
241
is not None
242
)243
insert_to_idx = parent.section_contents.index(
244
reference_node.ng_including_document_from_file
245
)246
elif self.node_info.whereto == NodeCreationOrder.AFTER:
247
if isinstance(reference_node, SDocDocument):
248
assert reference_node.document_is_included()
249
assert reference_node.ng_including_document_from_file
250
parent = (
251
reference_node.ng_including_document_from_file.parent
252
)253
insert_to_idx = (
254
parent.section_contents.index(
255
reference_node.ng_including_document_from_file
256
)257
+ 1
258
)259
else:
260
parent = reference_node.parent
261
insert_to_idx = (
262
parent.section_contents.index(reference_node) + 1
263
)264
else:
265
raise NotImplementedError
266
267
# Reset the 'needs generation' flag on all documents.268
for document_ in traceability_index.document_tree.document_list:
269
assert document_.meta is not None
270
set_file_modification_time(
271
document_.meta.input_doc_full_path,
272
datetime.datetime.today(),
273
)274
275
# FIXME: It is better to have a general create_node method because276
# we are dealing with arbitrary nodes, not only Requirement.277
requirement = SDocObjectFactory.create_requirement(
278
parent=parent, node_type=form_object.element_type
279
)280
requirement.reserved_mid = MID(form_object.requirement_mid)
281
if document.config.enable_mid:
282
requirement.mid_permanent = True
283
284
assert document.grammar is not None
285
grammar_element: GrammarElement = document.grammar.elements_by_type[
286
form_object.element_type
287
]288
289
requirement.is_composite = (
290
grammar_element.property_is_composite is True
291
)292
requirement.ng_document_reference = DocumentReference()
293
requirement.ng_document_reference.set_document(document)
294
requirement.ng_including_document_reference = (
295
document.ng_including_document_reference
296
)297
298
parent.section_contents.insert(insert_to_idx, requirement)
299
300
self.populate_node_fields_from_form_object(
301
requirement, form_object, map_form_to_requirement_fields
302
)303
traceability_index.create_requirement(requirement=requirement)
304
305
action_object = UpdateRequirementActionObject()
306
action_object.existing_references_uids.update(
307
requirement.get_requirement_reference_uids()
308
)309
action_object.reference_ids_to_remove = copy(
310
action_object.existing_references_uids
311
)312
action_object.this_document_requirements_to_update = {requirement}
313
314
references: List[Reference] = form_object.get_requirement_relations(
315
requirement316
)317
if len(references) > 0:
318
requirement.relations = references
319
else:
320
requirement.relations = []
321
322
for document_ in traceability_index.document_tree.document_list:
323
assert document_.meta is not None
324
set_file_modification_time(
325
document_.meta.input_doc_full_path, datetime.datetime.today()
326
)327
328
# Updating Traceability Index: Links.329
for reference_field in form_object.reference_fields:
330
ref_uid = reference_field.field_value
331
ref_role: Optional[str] = (
332
reference_field.field_role
333
if reference_field.field_role is not None
334
and len(reference_field.field_role) > 0
335
else None
336
)337
# If a link is in the form, we don't want to remove it.338
if (
339
reference_field.field_type,
340
ref_uid,
341
ref_role,
342
) in action_object.reference_ids_to_remove:
343
action_object.reference_ids_to_remove.remove(
344
(reference_field.field_type, ref_uid, ref_role)
345
)346
# If a link is already in the requirement and traceability index,347
# there is nothing to do.348
if (
349
reference_field.field_type,
350
ref_uid,
351
ref_role,
352
) in action_object.existing_references_uids:
353
continue354
if reference_field.field_type == "Parent":
355
traceability_index.update_requirement_parent_uid(
356
requirement=requirement,
357
parent_uid=ref_uid,
358
role=reference_field.field_role
359
if len(reference_field.field_role) > 0
360
else None,
361
)362
elif reference_field.field_type == "Child":
363
traceability_index.update_requirement_child_uid(
364
requirement=requirement,
365
child_uid=ref_uid,
366
role=reference_field.field_role
367
if len(reference_field.field_role) > 0
368
else None,
369
)370
elif reference_field.field_type == "File":
371
pass372
else:
373
raise AssertionError(f"Must not reach here: {reference_field}")
374
375
# Calculate which documents and requirements have to be regenerated.376
for (
377
_,
378
reference_id_to_remove,
379
_,
380
) in action_object.reference_ids_to_remove:
381
removed_uid_parent_requirement: SDocNode = (
382
traceability_index.graph_database.get_link_value(
383
link_type=GraphLinkType.UID_TO_NODE,
384
lhs_node=reference_id_to_remove,
385
)386
)387
removed_uid_parent_requirement_document = assert_cast(
388
removed_uid_parent_requirement.get_document(), SDocDocument
389
)390
action_object.removed_uid_parent_documents_to_update.add(
391
removed_uid_parent_requirement_document392
)393
# If a link was pointing towards a parent requirement in this394
# document, we will have to re-render it now.395
if removed_uid_parent_requirement_document == document:
396
action_object.this_document_requirements_to_update.add(
397
removed_uid_parent_requirement398
)399
400
for (
401
relation_type_,
402
reference_id_to_remove,
403
reference_id_to_remove_role,
404
) in action_object.reference_ids_to_remove:
405
if relation_type_ == "Parent":
406
traceability_index.remove_requirement_parent_uid(
407
requirement=requirement,
408
parent_uid=reference_id_to_remove,
409
role=reference_id_to_remove_role,
410
)411
elif relation_type_ == "Child":
412
traceability_index.remove_requirement_child_uid(
413
requirement=requirement,
414
child_uid=reference_id_to_remove,
415
role=reference_id_to_remove_role,
416
)417
418
# Rendering back the Turbo template for each changed requirement.419
for reference_field in form_object.reference_fields:
420
if reference_field.field_type not in ("Parent", "Child"):
421
continue422
ref_uid = reference_field.field_value
423
424
node: SDocNode = traceability_index.graph_database.get_link_value(
425
link_type=GraphLinkType.UID_TO_NODE,
426
lhs_node=ref_uid,
427
)428
429
if node.get_document() == document:
430
action_object.this_document_requirements_to_update.add(node)
431
432
self._update_traceability_index_with_links_and_anchors(
433
requirement, existing_node_fields
434
)435
436
document.build_search_index()
437
438
traceability_index.update_last_updated()
439
440
return CreateOrUpdateNodeResult(
441
this_document_requirements_to_update=list(
442
action_object.this_document_requirements_to_update
443
)444
)445
446
def _update_traceability_index_with_links_and_anchors(
447
self, updated_node: SDocNode, existing_node_fields: List[SDocNodeField]
448
) -> None:
449
traceability_index = self.traceability_index
450
451
existing_anchor_uids: Set[str] = set()
452
existing_links: List[InlineLink] = []
453
for node_field_ in existing_node_fields:
454
for part_ in node_field_.parts:
455
if isinstance(part_, Anchor):
456
existing_anchor_uids.add(part_.value)
457
elif isinstance(part_, InlineLink):
458
existing_links.append(part_)
459
460
for existing_link_ in existing_links:
461
traceability_index.remove_inline_link(existing_link_)
462
463
for existing_anchor_uid in existing_anchor_uids:
464
traceability_index.remove_anchor_by_uid(existing_anchor_uid)
465
466
for node_field_ in updated_node.enumerate_fields():
467
for part_ in node_field_.parts:
468
if isinstance(part_, Anchor):
469
# Since this is a new section, we just need to register the470
# new anchor. By this time, we know that there is no471
# existing anchor with this name.472
traceability_index.update_with_anchor(part_)
473
elif isinstance(part_, InlineLink):
474
traceability_index.create_inline_link(part_)
475
476
def populate_node_fields_from_form_object(
477
self,
478
node: SDocNode,
479
form_object: RequirementFormObject,
480
map_form_to_requirement_fields: Dict[
481
RequirementFormField, Optional[FreeTextContainer]
482
],483
) -> None:
484
# FIXME: Leave only one method based on set_field_value().485
for form_field_name, form_fields in form_object.fields.items():
486
for form_field_index, form_field in enumerate(form_fields):
487
if form_field.field_type != RequirementFormFieldType.MULTILINE:
488
node.set_field_value(
489
field_name=form_field_name,
490
form_field_index=form_field_index,
491
value=form_field.field_value,
492
)493
continue494
495
free_text_content: Optional[FreeTextContainer] = (
496
map_form_to_requirement_fields[form_field]
497
)498
requirement_field: Optional[SDocNodeField] = (
499
SDocNodeField(
500
node,
501
field_name=form_field_name,
502
parts=free_text_content.parts,
503
multiline__="true"
504
if form_field.field_type
505
== RequirementFormFieldType.MULTILINE
506
else None,
507
)508
if free_text_content is not None
509
else None
510
)511
node.set_field_value(
512
field_name=form_field_name,
513
form_field_index=form_field_index,
514
value=requirement_field,
515
)516
if (
517
requirement_field is not None
518
and free_text_content is not None
519
):520
for part_ in requirement_field.parts:
521
if isinstance(part_, str):
522
continue523
part_.parent = requirement_field