Skip to content

Commit

Permalink
backend/sdoc: allow using "DESCRIPTION" or "CONTENT" field instead of…
Browse files Browse the repository at this point in the history
… "STATEMENT"

Closes #1823
  • Loading branch information
stanislaw committed May 19, 2024
1 parent bd0968d commit 2a30c99
Show file tree
Hide file tree
Showing 20 changed files with 267 additions and 44 deletions.
30 changes: 27 additions & 3 deletions strictdoc/backend/sdoc/error_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,36 @@ def grammar_missing_reserved_statement(
return StrictDocSemanticError(
title=(
f"Grammar element '{grammar_element.tag}' is missing a reserved"
" STATEMENT field declaration."
" content field declaration, one of {STATEMENT, DESCRIPTION, CONTENT}."
),
hint=(
"STATEMENT plays a key role in StrictDoc's HTML user interface "
"A content field plays a key role in StrictDoc's HTML user interface "
"as well as in the other export formats. It is a reserved field"
" that any grammar must provide."
" that any grammar element must have."
),
example=None,
line=line,
col=column,
filename=path_to_sdoc_file,
)

@staticmethod
def grammar_reserved_statement_must_be_required(
grammar_element: GrammarElement,
field_title: str,
path_to_sdoc_file: str,
line: int,
column: int,
):
return StrictDocSemanticError(
title=(
f"Grammar element '{grammar_element.tag}'s {field_title} field "
f"must be declared as 'REQUIRED: True'."
),
hint=(
"A content field plays a key role in StrictDoc's HTML user interface "
"as well as in the other export formats. It is a reserved field"
" that any grammar element must have with 'REQUIRED: True'."
),
example=None,
line=line,
Expand Down
24 changes: 21 additions & 3 deletions strictdoc/backend/sdoc/models/document_grammar.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# mypy: disable-error-code="no-redef,no-untyped-call,no-untyped-def,union-attr,type-arg"
from collections import OrderedDict
from typing import Dict, List, Optional, Set, Union
from typing import Dict, List, Optional, Set, Tuple, Union

from strictdoc.backend.sdoc.models.type_system import (
RESERVED_NON_META_FIELDS,
Expand Down Expand Up @@ -75,9 +75,27 @@ def __init__(
else create_default_relations(self)
)
fields_map: OrderedDict = OrderedDict()
for field in fields:
fields_map[field.title] = field

statement_field: Optional[Tuple[str, int]] = None
description_field: Optional[Tuple[str, int]] = None
content_field: Optional[Tuple[str, int]] = None
for field_idx_, field_ in enumerate(fields):
fields_map[field_.title] = field_
if field_.title == RequirementFieldName.STATEMENT:
statement_field = (RequirementFieldName.STATEMENT, field_idx_)
elif field_.title == "DESCRIPTION":
description_field = (
RequirementFieldName.DESCRIPTION,
field_idx_,
)
elif field_.title == "CONTENT":
content_field = (RequirementFieldName.CONTENT, field_idx_)
else:
pass
self.fields_map: Dict[str, GrammarElementField] = fields_map
self.content_field: Tuple[str, int] = (
statement_field or description_field or content_field or ("", -1)
)
self.mid: MID = MID.create()

@staticmethod
Expand Down
2 changes: 1 addition & 1 deletion strictdoc/backend/sdoc/models/free_text.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import html
from typing import List, Any, Optional
from typing import Any, List, Optional

from strictdoc.backend.sdoc.models.anchor import Anchor
from strictdoc.backend.sdoc.models.inline_link import InlineLink
Expand Down
38 changes: 19 additions & 19 deletions strictdoc/backend/sdoc/models/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,11 @@ def reserved_title(self) -> Optional[str]:

@property
def reserved_statement(self) -> Optional[str]:
element: GrammarElement = self.document.grammar.elements_by_type[
self.requirement_type
]
return self._get_cached_field(
RequirementFieldName.STATEMENT, singleline_only=False
element.content_field[0], singleline_only=False
)

@property
Expand Down Expand Up @@ -276,6 +279,12 @@ def get_requirement_style_mode(self) -> str:
assert self.ng_document_reference.get_document() is not None
return self.ng_document_reference.get_document().config.get_requirement_style_mode()

def get_content_field_name(self) -> str:
element: GrammarElement = self.document.grammar.elements_by_type[
self.requirement_type
]
return element.content_field[0]

def has_requirement_references(self, ref_type: str) -> bool:
if len(self.relations) == 0:
return False
Expand Down Expand Up @@ -374,9 +383,7 @@ def enumerate_meta_fields(
self.requirement_type
]
grammar_field_titles = list(map(lambda f: f.title, element.fields))
statement_field_index = grammar_field_titles.index(
RequirementFieldName.STATEMENT
)
statement_field_index: int = element.content_field[1]
for field in self.enumerate_fields():
if field.field_name in RESERVED_NON_META_FIELDS:
continue
Expand Down Expand Up @@ -412,6 +419,13 @@ def get_field_human_title(self, field_name: str) -> str:
field_human_title = element.fields_map[field_name]
return field_human_title.get_field_human_name()

def get_field_human_title_for_statement(self) -> str:
element: GrammarElement = self.document.grammar.elements_by_type[
self.requirement_type
]
field_human_title = element.fields_map[element.content_field[0]]
return field_human_title.get_field_human_name()

def get_requirement_prefix(self) -> str:
parent: Union[SDocSection, SDocDocument] = assert_cast(
self.parent, (SDocSection, SDocDocument, SDocCompositeNode)
Expand Down Expand Up @@ -479,25 +493,11 @@ def set_field_value(
self.requirement_type
]
grammar_field_titles = list(map(lambda f: f.title, element.fields))
# FIXME: This will go away very soon when the RELATIONS become a
# separate field in SDoc REQUIREMENT's grammar.
grammar_field_titles.append("REFS")
field_index = grammar_field_titles.index(field_name)

try:
title_field_index = grammar_field_titles.index(
RequirementFieldName.TITLE
)
except ValueError:
# It is a rare edge case when a grammar is without a TITLE but if it
# happens, use STATEMENT as a fallback.
title_field_index = grammar_field_titles.index(
RequirementFieldName.STATEMENT
)

field_value = None
field_value_multiline = None
if field_index <= title_field_index:
if field_index < element.content_field[1]:
field_value = value
else:
field_value_multiline = value
Expand Down
8 changes: 8 additions & 0 deletions strictdoc/backend/sdoc/models/type_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,22 @@ class RequirementFieldName:
STATUS = "STATUS"
TAGS = "TAGS"
TITLE = "TITLE"

# {STATEMENT, DESCRIPTION, CONTENT} are aliases.
# It is assumed that either field is provided for each node.
STATEMENT = "STATEMENT"
DESCRIPTION = "DESCRIPTION"
CONTENT = "CONTENT"

RATIONALE = "RATIONALE"
COMMENT = "COMMENT"


RESERVED_NON_META_FIELDS = [
RequirementFieldName.TITLE,
RequirementFieldName.STATEMENT,
RequirementFieldName.DESCRIPTION,
RequirementFieldName.CONTENT,
RequirementFieldName.COMMENT,
RequirementFieldName.RATIONALE,
RequirementFieldName.LEVEL,
Expand Down
34 changes: 26 additions & 8 deletions strictdoc/backend/sdoc/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
SDocNode,
)
from strictdoc.backend.sdoc.models.section import SDocSection
from strictdoc.backend.sdoc.models.type_system import (
GrammarElementField,
RequirementFieldName,
)
from strictdoc.helpers.exception import StrictDocException


Expand All @@ -29,13 +33,10 @@ def __init__(self, path_to_sdoc_file: Optional[str]):
if path_to_sdoc_file is not None:
assert os.path.isfile(path_to_sdoc_file), path_to_sdoc_file
self.path_to_sdoc_dir = os.path.dirname(path_to_sdoc_file)

self.document_grammar: Optional[DocumentGrammar] = None
self.document_reference: DocumentReference = DocumentReference()
self.context_document_reference: DocumentReference = DocumentReference()
self.document_config: Optional[DocumentConfig] = None
self.document_grammar: DocumentGrammar = DocumentGrammar.create_default(
None
)
self.document_view: Optional[DocumentView] = None
self.document_has_requirements = False

Expand All @@ -48,7 +49,10 @@ def __init__(self, parse_context: ParseContext):
self.parse_context: ParseContext = parse_context

def process_document(self, document: SDocDocument):
document.grammar = self.parse_context.document_grammar
document.grammar = (
self.parse_context.document_grammar
or DocumentGrammar.create_default(document)
)
self.parse_context.document = document
document.ng_including_document_reference = (
self.parse_context.context_document_reference
Expand Down Expand Up @@ -86,12 +90,26 @@ def process_document_grammar_element(self, grammar_element: GrammarElement):
grammar_element._tx_position
)

if grammar_element.content_field[0] not in grammar_element.fields_map:
raise StrictDocSemanticError.grammar_missing_reserved_statement(
grammar_element,
self.parse_context.path_to_sdoc_file,
line_start,
col_start,
)
content_field: GrammarElementField = grammar_element.fields_map[
grammar_element.content_field[0]
]
# FIXME: Enable for STATEMENT as well. For now, don't want to break
# backward compatibility.
if (
grammar_element.tag == "REQUIREMENT"
and "STATEMENT" not in grammar_element.fields_map
content_field.title
in (RequirementFieldName.DESCRIPTION, RequirementFieldName.CONTENT)
and not content_field.required
):
raise StrictDocSemanticError.grammar_missing_reserved_statement(
raise StrictDocSemanticError.grammar_reserved_statement_must_be_required(
grammar_element,
content_field.title,
self.parse_context.path_to_sdoc_file,
line_start,
col_start,
Expand Down
10 changes: 6 additions & 4 deletions strictdoc/export/html/form_objects/requirement_form_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,13 +644,15 @@ def validate(
),
)

requirement_statement = self.fields["STATEMENT"][
requirement_element = self.grammar.elements_by_type[self.element_type]
statement_field_name = requirement_element.content_field[0]
requirement_statement = self.fields[statement_field_name][
0
].field_unescaped_value
if requirement_statement is None or len(requirement_statement) == 0:
self.add_error(
"STATEMENT",
"Node statement must not be empty.",
statement_field_name,
f"Node {statement_field_name.lower()} must not be empty.",
)
else:
(
Expand All @@ -661,7 +663,7 @@ def validate(
context_document=context_document,
).write_with_validation(requirement_statement)
if parsed_html is None:
self.add_error("STATEMENT", rst_error)
self.add_error(statement_field_name, rst_error)

requirement_uid: Optional[str] = (
self.fields["UID"][0].field_unescaped_value
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{%- if requirement.reserved_statement and view_object.current_view.includes_field(requirement.requirement_type, "STATEMENT") -%}
{%- if requirement.reserved_statement and view_object.current_view.includes_field(requirement.requirement_type, requirement.get_content_field_name()) -%}
{%- if truncated_statement is true -%}
{# truncated in DTR tiny card #}
<sdoc-requirement-field data-field-label="statement">
<sdoc-autogen>{{ view_object.markup_renderer.render_truncated_requirement_statement(requirement) }}</sdoc-autogen>
</sdoc-requirement-field>
{%- else -%}
{# default with label #}
<sdoc-requirement-field-label>{{ requirement.get_field_human_title("STATEMENT") }}:</sdoc-requirement-field-label>
<sdoc-requirement-field-label>{{ requirement.get_field_human_title_for_statement() }}:</sdoc-requirement-field-label>
<sdoc-requirement-field
data-field-label="statement"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[DOCUMENT]
TITLE: Document 1

[GRAMMAR]
ELEMENTS:
- TAG: REQUIREMENT
FIELDS:
- TITLE: TITLE
TYPE: String
REQUIRED: True
- TITLE: DESCRIPTION
TYPE: String
REQUIRED: True

[FREETEXT]
Hello world!
[/FREETEXT]

[REQUIREMENT]
TITLE: Requirement title
DESCRIPTION: >>>
Requirement statement.
<<<
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[DOCUMENT]
TITLE: Document 1

[GRAMMAR]
ELEMENTS:
- TAG: REQUIREMENT
FIELDS:
- TITLE: TITLE
TYPE: String
REQUIRED: True
- TITLE: DESCRIPTION
TYPE: String
REQUIRED: True

[FREETEXT]
Hello world!
[/FREETEXT]

[REQUIREMENT]
TITLE: Requirement title
DESCRIPTION: >>>
Requirement statement.
<<<
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from tests.end2end.e2e_case import E2ECase
from tests.end2end.end2end_test_setup import End2EndTestSetup
from tests.end2end.helpers.screens.document.form_edit_requirement import (
Form_EditRequirement,
)
from tests.end2end.helpers.screens.project_index.screen_project_index import (
Screen_ProjectIndex,
)
from tests.end2end.server import SDocTestServer


class Test(E2ECase):
def test(self):
test_setup = End2EndTestSetup(path_to_test_file=__file__)

with SDocTestServer(
input_path=test_setup.path_to_sandbox
) as test_server:
self.open(test_server.get_host_and_port())

screen_project_index = Screen_ProjectIndex(self)

screen_project_index.assert_on_screen()
screen_project_index.assert_contains_document("Document 1")

screen_document = screen_project_index.do_click_on_first_document()

screen_document.assert_on_screen_document()
screen_document.assert_header_document_title("Document 1")

screen_document.assert_text("Hello world!")

requirement = screen_document.get_requirement()
form_edit_requirement: Form_EditRequirement = (
requirement.do_open_form_edit_requirement()
)
form_edit_requirement.do_clear_field("DESCRIPTION")
form_edit_requirement.do_form_submit_and_catch_error(
"Node description must not be empty."
)

assert test_setup.compare_sandbox_and_expected_output()
2 changes: 1 addition & 1 deletion tests/integration/expect_exit.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
if unexpected_exit_code:
print( # noqa: T201
"error: expect_exit: expected exit code: "
"f{expected_exit_code}, actual: {process.returncode}."
f"{expected_exit_code}, actual: {process.returncode}."
)

unexpected_content = expect_no_content and len(stdout) > 0
Expand Down
Loading

0 comments on commit 2a30c99

Please sign in to comment.