Path:
strictdoc/core/transforms/update_document_config.py
Lines:
196
Non-empty lines:
179
Non-empty lines covered with requirements:
179 / 179 (100.0%)
Functions:
5
Functions covered by requirements:
5 / 5 (100.0%)
1
"""2
@relation(SDOC-SRS-57, scope=file)3
"""4
5
import re
6
from collections import defaultdict
7
from typing import Dict, List, Optional
8
9
from strictdoc.backend.sdoc.free_text_reader import SDFreeTextReader
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 (
13
DocumentCustomMetadata,
14
DocumentCustomMetadataKeyValuePair,
15
)16
from strictdoc.backend.sdoc.models.inline_link import InlineLink
17
from strictdoc.core.traceability_index import (
18
TraceabilityIndex,
19
)20
from strictdoc.core.transforms.validation_error import (
21
MultipleValidationError,
22
)23
from strictdoc.export.html.form_objects.document_config_form_object import (
24
DocumentConfigFormObject,
25
)26
27
DOCUMENT_CUSTOM_METADATA_KEY_RE = re.compile(r"[a-zA-Z_][a-zA-Z0-9_-]*")
28
29
30
class UpdateDocumentConfigTransform:
31
def __init__(
32
self,
33
form_object: DocumentConfigFormObject,
34
document: SDocDocument,
35
traceability_index: TraceabilityIndex,
36
) -> None:
37
self.form_object: DocumentConfigFormObject = form_object
38
self.document: SDocDocument = document
39
self.traceability_index: TraceabilityIndex = traceability_index
40
41
def perform(self) -> None:
42
form_object = self.form_object
43
document = self.document
44
45
try:
46
self.validate(form_object, document)
47
except MultipleValidationError:
48
raise49
50
# Update the document.51
document.title = form_object.document_title
52
document.config.version = (
53
form_object.document_version
54
if form_object.document_version is not None
55
and len(form_object.document_version) > 0
56
else None
57
)58
document.config.classification = (
59
form_object.document_classification
60
if form_object.document_classification is not None
61
and len(form_object.document_classification) > 0
62
else None
63
)64
document.config.requirement_prefix = (
65
form_object.document_requirement_prefix
66
if form_object.document_requirement_prefix is not None
67
and len(form_object.document_requirement_prefix) > 0
68
else None
69
)70
existing_custom_metadata = document.config.custom_metadata
71
if len(form_object.custom_metadata_fields):
72
entries = [
73
DocumentCustomMetadataKeyValuePair(
74
key=field.field_name,
75
parts=SDFreeTextReader.read(field.field_value).parts,
76
)77
for field in form_object.custom_metadata_fields
78
]79
self._update_traceability_index_with_metadata_links(
80
existing_custom_metadata, entries
81
)82
new_custom_metadata = DocumentCustomMetadata(
83
parent=document.config, entries=entries
84
)85
for entry_ in entries:
86
entry_.parent = new_custom_metadata
87
document.config.custom_metadata = new_custom_metadata
88
else:
89
self._update_traceability_index_with_metadata_links(
90
existing_custom_metadata, []
91
)92
document.config.custom_metadata = None
93
94
self.traceability_index.delete_document(document)
95
96
document.config.uid = (
97
form_object.document_uid
98
if form_object.document_uid is not None
99
and len(form_object.document_uid) > 0
100
else None
101
)102
103
self.traceability_index.create_document(document)
104
105
def _update_traceability_index_with_metadata_links(
106
self,
107
existing_custom_metadata: Optional[DocumentCustomMetadata],
108
new_entries: List[DocumentCustomMetadataKeyValuePair],
109
) -> None:
110
if existing_custom_metadata is not None:
111
for existing_entry_ in existing_custom_metadata.entries:
112
for part_ in existing_entry_.parts:
113
if isinstance(part_, InlineLink):
114
self.traceability_index.remove_inline_link(part_)
115
for new_entry_ in new_entries:
116
for part_ in new_entry_.parts:
117
if isinstance(part_, InlineLink):
118
self.traceability_index.create_inline_link(part_)
119
120
def validate(
121
self,
122
form_object: DocumentConfigFormObject,
123
document: SDocDocument,
124
) -> None:
125
errors: Dict[str, List[str]] = defaultdict(list)
126
assert isinstance(document, SDocDocument)
127
128
if len(form_object.document_title) == 0:
129
errors["TITLE"].append("Document title must not be empty.")
130
131
# Ensure that UID doesn't have any incoming links if it is going to be132
# renamed or removed.133
existing_uid = document.reserved_uid
134
new_uid = form_object.document_uid
135
if existing_uid is not None:
136
if new_uid is None or existing_uid != new_uid:
137
existing_incoming_links: Optional[List[InlineLink]] = (
138
self.traceability_index.get_incoming_links(document)
139
)140
if (
141
existing_incoming_links is not None
142
and len(existing_incoming_links) > 0
143
):144
errors["UID"].append(
145
(146
"Renaming a node UID when the node has "147
"incoming links is not supported yet. "148
"Please delete all incoming links first."149
),150
)151
152
for metadata_field in form_object.custom_metadata_fields:
153
metadata_error_key = f"METADATA[{metadata_field.field_mid}]"
154
if len(metadata_field.field_name) == 0:
155
errors[metadata_error_key].append("Key must not be empty.")
156
elif (
157
DOCUMENT_CUSTOM_METADATA_KEY_RE.fullmatch(
158
metadata_field.field_name
159
)160
is None
161
):162
# Keep this validation aligned with DocumentCustomMetadataKey163
# in the SDOC grammar so saved documents remain parseable.164
errors[metadata_error_key].append(
165
"Key must start with an ASCII letter or underscore and "166
"contain only ASCII letters, digits, underscores, or "167
"hyphens."168
)169
if len(metadata_field.field_value) == 0:
170
errors[metadata_error_key].append("Value must not be empty.")
171
else:
172
free_text_container = SDFreeTextReader.read(
173
metadata_field.field_value
174
)175
for part_ in free_text_container.parts:
176
if isinstance(part_, InlineLink):
177
if (
178
self.traceability_index.get_linkable_node_by_uid_weak(
179
part_.link
180
)181
is None
182
):183
errors[metadata_error_key].append(
184
"METADATA value references a UID "185
f"that does not exist: {part_.link}."
186
)187
elif isinstance(part_, Anchor):
188
errors[metadata_error_key].append(
189
"METADATA values do not support "190
"[ANCHOR: ...] markers, only [LINK: UID]."191
)192
193
if len(errors):
194
raise MultipleValidationError(
195
"Document form has not passed validation.", errors=errors
196
)