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%)
- "8.3.3. Selected fields export" (REQUIREMENT)
- "8.3.1. Export to Excel" (REQUIREMENT)
- "8.3.2. Import from Excel" (REQUIREMENT)
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
@staticmethod47
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
@staticmethod70
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
document96
)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
document108
).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
continue115
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 multiple152
# 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_value165
and len(comment_row_value)
166
> columns[field][MAX_WIDTH_KEY]
167
):168
columns[field][MAX_WIDTH_KEY] = len(
169
comment_row_value170
)171
elif field_uc == "RELATIONS":
172
if len(node.relations) > 0:
173
relations_components = []
174
# Using a transition marker to separate175
# 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 and211
# 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
@staticmethod234
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
@staticmethod250
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
@staticmethod259
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
@staticmethod275
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