Path:
strictdoc/export/html/form_objects/grammar_element_form_object.py
Lines:
534
Non-empty lines:
475
Non-empty lines covered with requirements:
475 / 475 (100.0%)
Functions:
32
Functions covered by requirements:
32 / 32 (100.0%)
1
"""2
@relation(SDOC-SRS-56, scope=file)3
"""4
5
from typing import List, Optional, Set, Tuple
6
7
from jinja2 import Template
8
from markupsafe import Markup
9
from starlette.datastructures import FormData
10
11
from strictdoc.backend.sdoc.models.document import SDocDocument
12
from strictdoc.backend.sdoc.models.document_grammar import (
13
DocumentGrammar,
14
)15
from strictdoc.backend.sdoc.models.grammar_element import (
16
GrammarElement,
17
GrammarElementField,
18
GrammarElementFieldString,
19
GrammarElementFieldType,
20
GrammarElementRelationChild,
21
GrammarElementRelationFile,
22
GrammarElementRelationParent,
23
GrammarElementRelationType,
24
)25
from strictdoc.backend.sdoc.models.model import RequirementFieldName
26
from strictdoc.core.project_config import ProjectConfig
27
from strictdoc.export.html.form_objects.form_object import (
28
RowWithCustomFieldFormObject,
29
RowWithRelationFormObject,
30
RowWithReservedFieldFormObject,
31
)32
from strictdoc.export.html.html_templates import JinjaEnvironment
33
from strictdoc.helpers.auto_described import auto_described
34
from strictdoc.helpers.cast import assert_cast
35
from strictdoc.helpers.form_data import ParsedFormData, parse_form_data
36
from strictdoc.helpers.mid import MID
37
from strictdoc.helpers.string import is_uppercase_underscore_string
38
from strictdoc.server.error_object import ErrorObject
39
from strictdoc.server.helpers.turbo import render_turbo_stream
40
41
42
def is_reserved_field(field_name: str) -> bool:
43
return field_name in (
44
RequirementFieldName.UID,
45
RequirementFieldName.TITLE,
46
RequirementFieldName.STATEMENT,
47
RequirementFieldName.RATIONALE,
48
RequirementFieldName.COMMENT,
49
)50
51
52
@auto_described53
class GrammarFormField:
54
def __init__(
55
self,
56
field_mid: str,
57
field_name: str,
58
field_human_title: Optional[str],
59
field_required: bool,
60
reserved: bool,
61
):62
self.field_mid: str = field_mid
63
self.field_name: str = field_name
64
self.field_human_title: Optional[str] = field_human_title
65
self.field_required: bool = field_required
66
self.reserved: bool = reserved
67
68
@staticmethod69
def create_from_grammar_field(
70
*, grammar_field: GrammarElementField
71
) -> "GrammarFormField":
72
reserved = is_reserved_field(grammar_field.title)
73
return GrammarFormField(
74
field_mid=grammar_field.mid,
75
field_name=grammar_field.title,
76
field_human_title=grammar_field.human_title,
77
field_required=grammar_field.required,
78
reserved=reserved,
79
)80
81
def get_input_field_name(self) -> str:
82
return f"document_grammar_field[{self.field_mid}][field_name]"
83
84
def get_input_field_human_title(self) -> str:
85
return f"document_grammar_field[{self.field_mid}][field_human_title]"
86
87
def get_input_field_required(self) -> str:
88
return f"document_grammar_field[{self.field_mid}][field_required]"
89
90
def get_input_field_required_value(self) -> str:
91
return "true" if self.field_required else "false"
92
93
94
@auto_described95
class GrammarFormRelation:
96
def __init__(
97
self,
98
relation_mid: str,
99
relation_type: str,
100
relation_role: Optional[str],
101
):102
self.relation_mid: str = relation_mid
103
self.relation_type: str = relation_type
104
self.relation_role: str = (
105
relation_role if relation_role is not None else ""
106
)107
108
def relation_type_input_name(self) -> str:
109
return f"document_grammar_relation[{self.relation_mid}][type]"
110
111
def relation_role_input_name(self) -> str:
112
return f"document_grammar_relation[{self.relation_mid}][role]"
113
114
115
@auto_described116
class GrammarElementFormObject(ErrorObject):
117
def __init__(
118
self,
119
*,
120
document_mid: str,
121
element_mid: str,
122
element_name: str,
123
is_composite: Optional[bool],
124
prefix: Optional[str],
125
view_style: Optional[str],
126
fields: List[GrammarFormField],
127
relations: List[GrammarFormRelation],
128
project_config: ProjectConfig,
129
jinja_environment: JinjaEnvironment,
130
):131
assert isinstance(document_mid, str), document_mid
132
super().__init__()
133
self.document_mid = document_mid
134
self.element_mid: str = element_mid
135
self.element_name: str = element_name
136
self.is_composite: Optional[bool] = is_composite
137
self.prefix: Optional[str] = prefix
138
self.view_style: Optional[str] = view_style
139
self.fields: List[GrammarFormField] = fields
140
self.relations: List[GrammarFormRelation] = relations
141
self.project_config: ProjectConfig = project_config
142
self.jinja_environment: JinjaEnvironment = jinja_environment
143
144
@staticmethod145
def create_from_request(
146
*,
147
document: SDocDocument,
148
request_form_data: FormData,
149
project_config: ProjectConfig,
150
jinja_environment: JinjaEnvironment,
151
) -> "GrammarElementFormObject":
152
assert document.grammar is not None
153
154
form_object_fields: List[GrammarFormField] = []
155
form_object_relations: List[GrammarFormRelation] = []
156
request_form_data_as_list = [
157
(field_name, field_value)
158
for field_name, field_value in request_form_data.multi_items()
159
]160
request_form_dict: ParsedFormData = assert_cast(
161
parse_form_data(request_form_data_as_list), dict
162
)163
164
element_mid = assert_cast(request_form_dict["element_mid"], str)
165
166
element_is_composite = request_form_dict["is_composite"]
167
element_prefix = request_form_dict["prefix"]
168
element_view_style = request_form_dict["view_style"]
169
170
#171
# Grammar fields.172
#173
document_grammar_fields = assert_cast(
174
request_form_dict["document_grammar_field"], dict
175
)176
for field_mid, field_dict in document_grammar_fields.items():
177
assert isinstance(field_dict, dict), type(field_dict)
178
179
field_name = assert_cast(field_dict["field_name"], str)
180
field_human_title = field_dict.get("field_human_title")
181
if field_human_title is not None:
182
field_human_title = field_human_title.strip()
183
if len(field_human_title) == 0:
184
field_human_title = None
185
field_required_string = field_dict.get("field_required")
186
field_required = field_required_string == "true"
187
188
form_object_field = GrammarFormField(
189
field_mid=field_mid,
190
field_name=field_name,
191
field_human_title=field_human_title,
192
field_required=field_required,
193
reserved=is_reserved_field(field_name),
194
)195
form_object_fields.append(form_object_field)
196
197
#198
# Grammar relations.199
#200
document_grammar_relations = assert_cast(
201
request_form_dict.get("document_grammar_relation", {}), dict
202
)203
for field_mid, field_dict in document_grammar_relations.items():
204
assert isinstance(field_dict, dict), type(field_dict)
205
206
field_type = assert_cast(field_dict["type"], str)
207
field_role = assert_cast(field_dict["role"].strip(), str)
208
209
form_object_relation = GrammarFormRelation(
210
relation_mid=field_mid,
211
relation_type=field_type,
212
relation_role=field_role,
213
)214
form_object_relations.append(form_object_relation)
215
216
element: GrammarElement = document.grammar.get_element_by_mid(
217
element_mid218
)219
220
form_object = GrammarElementFormObject(
221
document_mid=document.reserved_mid,
222
element_mid=element_mid,
223
element_name=element.tag,
224
is_composite=(
225
True226
if element_is_composite == "true"
227
else False
228
if element_is_composite == "false"
229
else None
230
),231
prefix=assert_cast(element_prefix, str),
232
view_style=assert_cast(element_view_style, str),
233
fields=form_object_fields,
234
relations=form_object_relations,
235
project_config=project_config,
236
jinja_environment=jinja_environment,
237
)238
return form_object
239
240
@staticmethod241
def create_from_document(
242
*,
243
document: SDocDocument,
244
element_mid: str,
245
project_config: ProjectConfig,
246
jinja_environment: JinjaEnvironment,
247
) -> "GrammarElementFormObject":
248
assert isinstance(document, SDocDocument)
249
assert isinstance(document.grammar, DocumentGrammar)
250
251
grammar: DocumentGrammar = document.grammar
252
253
element: GrammarElement = grammar.get_element_by_mid(element_mid)
254
255
grammar_form_fields: List[GrammarFormField] = []
256
for grammar_field in element.fields:
257
grammar_form_field = GrammarFormField.create_from_grammar_field(
258
grammar_field=grammar_field
259
)260
grammar_form_fields.append(grammar_form_field)
261
262
grammar_form_relations: List[GrammarFormRelation] = []
263
for grammar_relation in element.relations:
264
grammar_form_relation = GrammarFormRelation(
265
relation_mid=grammar_relation.mid,
266
relation_type=grammar_relation.relation_type,
267
relation_role=grammar_relation.relation_role,
268
)269
grammar_form_relations.append(grammar_form_relation)
270
271
return GrammarElementFormObject(
272
document_mid=document.reserved_mid,
273
element_mid=element_mid,
274
element_name=element.tag,
275
is_composite=element.property_is_composite,
276
prefix=element.property_prefix,
277
view_style=element.property_view_style,
278
fields=grammar_form_fields,
279
relations=grammar_form_relations,
280
project_config=project_config,
281
jinja_environment=jinja_environment,
282
)283
284
def validate(self) -> bool:
285
fields_so_far: Set[str] = set()
286
for field in self.fields:
287
if len(field.field_name) == 0:
288
self.add_error(
289
field.get_input_field_name(),
290
f"Grammar field {field.field_name} must not be empty.",
291
)292
continue293
294
if not is_uppercase_underscore_string(field.field_name):
295
self.add_error(
296
field.get_input_field_name(),
297
(298
"Grammar field title shall consist of "299
"uppercase letters, digits and single underscores."300
),301
)302
continue303
304
if field.field_name in fields_so_far:
305
self.add_error(
306
field.get_input_field_name(),
307
f"Grammar field {field.field_name} is not unique.",
308
)309
else:
310
fields_so_far.add(field.field_name)
311
312
if len(self.relations) == 0 and self.element_name not in (
313
"TEXT",
314
"SECTION",
315
):316
self.add_error(
317
"Relations_Row",
318
(319
"Every grammar must include at least one relation. "320
"A grammar lacking any relations is not considered a "321
"realistic use case. To address this issue, you can create "322
"a default 'Parent' relation with no assigned role."323
),324
)325
else:
326
general_relations_so_far: Set[str] = set()
327
role_relations_so_far: Set[Tuple[str, str]] = set()
328
for relation in self.relations:
329
if (
330
relation.relation_role is None
331
or len(relation.relation_role) == 0
332
):333
if relation.relation_type in general_relations_so_far:
334
self.add_error(
335
relation.relation_type_input_name(),
336
(337
f"A duplicated general relation: {relation.relation_type}. "
338
"A relation is general when it does not have a role that specializes the relation. "339
"A grammar must have zero or one general relations of a given type (e.g., Parent or Child)."340
),341
)342
else:
343
general_relations_so_far.add(relation.relation_type)
344
else:
345
if (
346
relation.relation_type,
347
relation.relation_role,
348
) in role_relations_so_far:
349
self.add_error(
350
relation.relation_type_input_name(),
351
(352
f"A duplicated relation and role: "
353
f"{relation.relation_type} ({relation.relation_role}). "
354
"A grammar must have zero or one relations with a given type and a given role."355
),356
)357
else:
358
role_relations_so_far.add(
359
(relation.relation_type, relation.relation_role)
360
)361
362
return len(self.errors) == 0
363
364
def convert_to_grammar_element(
365
self, existing_grammar: DocumentGrammar
366
) -> GrammarElement:
367
grammar_fields: List[GrammarElementFieldType] = []
368
for field in self.fields:
369
grammar_field = GrammarElementFieldString(
370
parent=None,
371
title=field.field_name,
372
human_title=field.field_human_title,
373
required="True" if field.field_required else "False",
374
)375
grammar_fields.append(grammar_field)
376
relation_fields: List[GrammarElementRelationType] = []
377
378
for relation in self.relations:
379
if relation.relation_type == "Parent":
380
relation_fields.append(
381
GrammarElementRelationParent(
382
parent=None,
383
relation_type=relation.relation_type,
384
relation_role=relation.relation_role,
385
)386
)387
elif relation.relation_type == "Child":
388
relation_fields.append(
389
GrammarElementRelationChild(
390
parent=None,
391
relation_type=relation.relation_type,
392
relation_role=relation.relation_role,
393
)394
)395
elif relation.relation_type == "File":
396
relation_fields.append(
397
GrammarElementRelationFile(
398
parent=None,
399
relation_type=relation.relation_type,
400
relation_role=relation.relation_role,
401
)402
)403
else:
404
raise NotImplementedError(relation)
405
406
existing_element = existing_grammar.get_element_by_mid(self.element_mid)
407
408
requirement_element = GrammarElement(
409
parent=None,
410
tag=existing_element.tag,
411
property_is_composite=(
412
"True"413
if self.is_composite == True
414
else "False"
415
if self.is_composite == False
416
else ""
417
),418
property_prefix=self.prefix or "",
419
property_view_style=self.view_style or "",
420
fields=grammar_fields,
421
relations=relation_fields,
422
)423
return requirement_element
424
425
def render(self) -> Markup:
426
template: Template = self.jinja_environment.get_template(
427
"components/grammar_form_element/index.jinja"428
)429
rendered_template = template.render(form_object=self)
430
return render_turbo_stream(
431
content=rendered_template, action="update", target="modal"
432
)433
434
def render_after_validation(self) -> Markup:
435
rendered_template = self.jinja_environment.render_template_as_markup(
436
"components/grammar_form_element/index.jinja", form_object=self
437
)438
return render_turbo_stream(
439
content=rendered_template, action="update", target="modal"
440
)441
442
def render_row_with_reserved_field(self, field: GrammarFormField) -> str:
443
form_object = RowWithReservedFieldFormObject(
444
field=field,
445
errors=self.errors,
446
jinja_environment=self.jinja_environment,
447
)448
return form_object.render()
449
450
def render_row_with_custom_field(self, field: GrammarFormField) -> str:
451
assert isinstance(field, GrammarFormField)
452
form_object = RowWithCustomFieldFormObject(
453
field=field,
454
errors=self.errors,
455
jinja_environment=self.jinja_environment,
456
)457
return form_object.render()
458
459
def render_row_with_relation(self, relation: GrammarFormRelation) -> str:
460
assert isinstance(relation, GrammarFormRelation)
461
form_object = RowWithRelationFormObject(
462
relation=relation,
463
errors=self.errors,
464
jinja_environment=self.jinja_environment,
465
)466
return form_object.render()
467
468
def render_row_with_new_field(self) -> str:
469
field: GrammarFormField = GrammarFormField(
470
field_mid=MID.create(),
471
field_name="",
472
field_human_title=None,
473
field_required=False,
474
reserved=False,
475
)476
form_object = RowWithCustomFieldFormObject(
477
field=field,
478
errors=self.errors,
479
jinja_environment=self.jinja_environment,
480
)481
rendered_template: str = form_object.render()
482
return render_turbo_stream(
483
content=rendered_template,
484
action="append",
485
target="document__editable_grammar_fields",
486
)487
488
def render_row_with_new_relation(self) -> str:
489
relation = GrammarFormRelation(
490
relation_mid=MID.create(),
491
relation_type="Parent",
492
relation_role="",
493
)494
form_object = RowWithRelationFormObject(
495
relation=relation,
496
errors=self.errors,
497
jinja_environment=self.jinja_environment,
498
)499
rendered_template: str = form_object.render()
500
return render_turbo_stream(
501
content=rendered_template,
502
action="append",
503
target="document__editable_grammar_relations",
504
)505
506
@staticmethod507
def render_close_form() -> str:
508
return render_turbo_stream(
509
content="",
510
action="update",
511
target="modal",
512
)513
514
def get_input_field_is_composite(self) -> str:
515
return "is_composite"
516
517
def get_input_field_is_composite_value(self) -> str:
518
if self.is_composite is not None:
519
if self.is_composite == True:
520
return "true"
521
return "false"
522
return ""
523
524
def get_input_field_prefix(self) -> str:
525
return "prefix"
526
527
def get_input_field_prefix_value(self) -> str:
528
return self.prefix or ""
529
530
def get_input_field_view_style(self) -> str:
531
return "view_style"
532
533
def get_input_field_view_style_value(self) -> str:
534
return self.view_style or ""