StrictDoc Documentation
strictdoc/backend/excel/export/excel_generator.py
Source file coverage
Path:
strictdoc/backend/excel/export/excel_generator.py
Lines:
281
Non-empty lines:
250
Non-empty lines covered with requirements:
250 / 250 (100.0%)
Functions:
7
Functions covered by requirements:
7 / 7 (100.0%)
1
"""
2
@relation(SDOC-SRS-134, scope=file)
3
"""
4
 
5
import os
6
from pathlib import Path
7
from typing import Dict, List
8
 
9
import xlsxwriter
10
from xlsxwriter.workbook import Workbook
11
from xlsxwriter.worksheet import Worksheet
12
 
13
from strictdoc.backend.sdoc.models.document import SDocDocument
14
from strictdoc.backend.sdoc.models.grammar_element import ReferenceType
15
from strictdoc.backend.sdoc.models.model import (
16
    SDocIteratedElementIF,
17
)
18
from strictdoc.backend.sdoc.models.node import SDocNode
19
from strictdoc.backend.sdoc.models.reference import (
20
    FileReference,
21
    ParentReqReference,
22
)
23
from strictdoc.core.project_config import ProjectConfig
24
from strictdoc.core.traceability_index import TraceabilityIndex
25
from strictdoc.helpers.cast import assert_cast
26
 
27
EXCEL_SHEET_NAME = "Requirements"
28
MAX_WIDTH = 75
29
HEADER_MARGIN = 3
30
MAX_WIDTH_KEY = "max_width"
31
COLUMN_HEADER_KEY = "header"
32
PARENT_COLUMN_HEADER_LABEL = "PARENT"
33
EXPORT_COLUMNS = [
34
    {
35
        "name": "uid",
36
        "header": "UID",
37
    },
38
    {
39
        "name": "statement",
40
        "header": "STATEMENT",
41
    },
42
]
43
 
44
 
45
class ExcelGenerator:
46
    @staticmethod
47
    def export_tree(
48
        traceability_index: TraceabilityIndex,
49
        output_excel_root: str,
50
        project_config: ProjectConfig,
51
    ) -> None:
52
        Path(output_excel_root).mkdir(parents=True, exist_ok=True)
53
 
54
        document: SDocDocument
55
        for document in traceability_index.document_tree.document_list:
56
            assert document.meta is not None
57
 
58
            document_out_file_name = (
59
                f"{document.meta.document_filename_base}.xlsx"
60
            )
61
            document_out_file = os.path.join(
62
                output_excel_root, document_out_file_name
63
            )
64
 
65
            ExcelGenerator._export_single_document(
66
                document, traceability_index, document_out_file, project_config
67
            )
68
 
69
    @staticmethod
70
    def _export_single_document(
71
        document: SDocDocument,
72
        traceability_index: TraceabilityIndex,
73
        document_out_file: str,
74
        project_config: ProjectConfig,
75
    ) -> None:
76
        with xlsxwriter.Workbook(document_out_file) as workbook:
77
            worksheet = workbook.add_worksheet(name=EXCEL_SHEET_NAME)
78
            workbook.set_properties(
79
                {
80
                    "title": project_config.project_title,
81
                    "comments": "Created with StrictDoc.",
82
                }
83
            )
84
 
85
            # Header row.
86
            row = 1
87
 
88
            fields = project_config.excel_export_fields
89
 
90
            # FIXME: Check if all fields are defined by the DocumentGrammar.
91
            assert fields is not None
92
            columns = ExcelGenerator._init_columns_width(fields)
93
 
94
            document_iterator = traceability_index.get_document_iterator(
95
                document
96
            )
97
            all_nodes = list(
98
                map(
99
                    lambda node_with_context_: node_with_context_[0],
100
                    document_iterator.all_content(print_fragments=False),
101
                )
102
            )
103
            req_uid_rows = ExcelGenerator._lookup_refs(all_nodes)
104
 
105
            if len(req_uid_rows):
106
                for node, _ in traceability_index.get_document_iterator(
107
                    document
108
                ).all_content(print_fragments=False):
109
                    if (
110
                        not isinstance(node, SDocNode)
111
                        or node.reserved_uid is None
112
                    ):
113
                        # Only export the requirements with UID.
114
                        continue
115
 
116
                    for idx, field in enumerate(fields, start=0):
117
                        field_uc = field.upper()
118
 
119
                        # Special treatment for ParentReqReference and Comments.
120
                        if field_uc in (
121
                            "RELATIONS:PARENT",
122
                            "PARENT",
123
                            "PARENTS",
124
                        ):
125
                            parent_refs = node.get_requirement_references(
126
                                ReferenceType.PARENT
127
                            )
128
                            if len(parent_refs) > 0:
129
                                # FIXME: Allow multiple parent refs.
130
                                ref = assert_cast(
131
                                    parent_refs[0], ParentReqReference
132
                                )
133
                                columns[field][MAX_WIDTH_KEY] = max(
134
                                    len(ref.ref_uid),
135
                                    columns[field][MAX_WIDTH_KEY],
136
                                )
137
                                if ref.ref_uid in req_uid_rows:
138
                                    worksheet.write_url(
139
                                        row,
140
                                        idx,
141
                                        (
142
                                            "internal:"
143
                                            f"'{EXCEL_SHEET_NAME}'"
144
                                            f"!A{req_uid_rows[ref.ref_uid]}"
145
                                        ),
146
                                        string=ref.ref_uid,
147
                                    )
148
                                else:
149
                                    worksheet.write(row, idx, ref.ref_uid)
150
                        elif field_uc in ("COMMENT", "COMMENTS"):
151
                            # Using a transition marker to separate multiple
152
                            # comments.
153
                            comment_fields = node.get_comment_fields()
154
                            if len(comment_fields) > 0:
155
                                comment_row_value: str = ""
156
                                for comment_field_ in comment_fields:
157
                                    if len(comment_row_value) > 0:
158
                                        comment_row_value += "\n----------\n"
159
                                    comment_row_value += (
160
                                        comment_field_.get_text_value()
161
                                    )
162
                                worksheet.write(row, idx, comment_row_value)
163
                                if (
164
                                    comment_row_value
165
                                    and len(comment_row_value)
166
                                    > columns[field][MAX_WIDTH_KEY]
167
                                ):
168
                                    columns[field][MAX_WIDTH_KEY] = len(
169
                                        comment_row_value
170
                                    )
171
                        elif field_uc == "RELATIONS":
172
                            if len(node.relations) > 0:
173
                                relations_components = []
174
                                # Using a transition marker to separate
175
                                # multiple references.
176
                                for relation_ in node.relations:
177
                                    if isinstance(
178
                                        relation_, ParentReqReference
179
                                    ):
180
                                        relations_components.append(
181
                                            relation_.ref_type
182
                                            + ": "
183
                                            + relation_.ref_uid
184
                                        )
185
                                    elif isinstance(relation_, FileReference):
186
                                        relations_components.append(
187
                                            relation_.ref_type
188
                                            + ": "
189
                                            + relation_.get_posix_path()
190
                                        )
191
                                relations_row_value: str = (
192
                                    "\n----------\n".join(relations_components)
193
                                )
194
                                worksheet.write(row, idx, relations_row_value)
195
                                value_len = len(relations_row_value)
196
                                columns[field][MAX_WIDTH_KEY] = max(
197
                                    value_len, columns[field][MAX_WIDTH_KEY]
198
                                )
199
                        elif field_uc in node.ordered_fields_lookup.keys():
200
                            req_field = node.ordered_fields_lookup[field_uc][0]
201
                            value: str = req_field.get_text_value()
202
                            worksheet.write(row, idx, value)
203
                            value_len = len(value)
204
                            columns[field][MAX_WIDTH_KEY] = max(
205
                                value_len, columns[field][MAX_WIDTH_KEY]
206
                            )
207
 
208
                    row += 1
209
 
210
                # Add a table around all this data, allowing filtering and
211
                # ordering in Excel.
212
                worksheet.add_table(
213
                    0,
214
                    0,
215
                    row - 1,
216
                    len(fields) - 1,
217
                    {"columns": ExcelGenerator._init_headers(fields)},
218
                )
219
 
220
                # Enforce columns width.
221
                ExcelGenerator._set_columns_width(
222
                    workbook, worksheet, columns, fields
223
                )
224
            else:
225
                # No requirement with UID.
226
                print(  # noqa: T201
227
                    "No requirement with UID, nothing to export into excel"
228
                )
229
 
230
        if row == 1:
231
            os.unlink(document_out_file)
232
 
233
    @staticmethod
234
    def _lookup_refs(
235
        document_contents: List[SDocIteratedElementIF],
236
    ) -> Dict[str, int]:
237
        refs: Dict[str, int] = {}
238
        row = 1
239
 
240
        for content_node in document_contents:
241
            if isinstance(content_node, SDocNode):
242
                if content_node.reserved_uid:
243
                    # Only export the requirements with uid, allowing tracking.
244
                    row += 1
245
                    refs[content_node.reserved_uid] = row
246
 
247
        return refs
248
 
249
    @staticmethod
250
    def _init_columns_width(fields: List[str]) -> Dict[str, Dict[str, int]]:
251
        columns: Dict[str, Dict[str, int]] = {}
252
 
253
        for field in fields:
254
            columns[field] = {}
255
            columns[field][MAX_WIDTH_KEY] = len(field) + HEADER_MARGIN
256
        return columns
257
 
258
    @staticmethod
259
    def _set_columns_width(
260
        workbook: Workbook,
261
        worksheet: Worksheet,
262
        columns: Dict[str, Dict[str, int]],
263
        fields: List[str],
264
    ) -> None:
265
        cell_format_text_wrap = workbook.add_format()
266
        cell_format_text_wrap.set_text_wrap()
267
 
268
        for idx, field in enumerate(fields, start=0):
269
            if columns[field][MAX_WIDTH_KEY] > MAX_WIDTH:
270
                worksheet.set_column(idx, idx, MAX_WIDTH, cell_format_text_wrap)
271
            else:
272
                worksheet.set_column(idx, idx, columns[field][MAX_WIDTH_KEY])
273
 
274
    @staticmethod
275
    def _init_headers(fields: List[str]) -> List[Dict[str, str]]:
276
        headers: List[Dict[str, str]] = []
277
 
278
        for field in fields:
279
            headers.append({"header": field.upper()})
280
 
281
        return headers