StrictDoc Documentation
strictdoc/export/html/form_objects/grammar_element_form_object.py
Source file coverage
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_described
53
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
    @staticmethod
69
    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_described
95
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_described
116
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
    @staticmethod
145
    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_mid
218
        )
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
                True
226
                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
    @staticmethod
241
    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
                continue
293
 
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
                continue
303
 
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
    @staticmethod
507
    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 ""