StrictDoc Documentation
strictdoc/core/transforms/update_document_config.py
Source file coverage
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
            raise
49
 
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 be
132
        # 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 DocumentCustomMetadataKey
163
                # 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
            )