diff --git a/docs/strictdoc_04_release_notes.sdoc b/docs/strictdoc_04_release_notes.sdoc index 16e0a5e46..0f15dcaa7 100644 --- a/docs/strictdoc_04_release_notes.sdoc +++ b/docs/strictdoc_04_release_notes.sdoc @@ -12,6 +12,8 @@ TITLE: Unreleased work The requirement-to-source traceability feature was extended to support linking requirements to the RST files. One more input scenario was handled for the Create Document workflow. When a project config has ``include_doc_paths`` or ``exclude_doc_paths`` search path filters specified, and an input document path contradicts to the provided filters, a validation message is shown. + +The Project Statistics screen was extended with the "Sections without any text" metric. Now it is possible to visualize which sections are still missing any introduction or description (free text). [/FREETEXT] [/SECTION] diff --git a/strictdoc/core/query_engine/grammar.py b/strictdoc/core/query_engine/grammar.py index 60f519fab..ff563058e 100644 --- a/strictdoc/core/query_engine/grammar.py +++ b/strictdoc/core/query_engine/grammar.py @@ -39,6 +39,8 @@ | NodeContainsExpression | + NodeContainsAnyFreeTextExpression + | NodeIsRequirementExpression | NodeIsSectionExpression @@ -66,6 +68,10 @@ 'node.contains("' string = /[A-Za-z0-9]+/ '")' ; +NodeContainsAnyFreeTextExpression: + _ = 'node.contains_any_text' +; + NodeHasParentRequirementsExpression: _ = 'node.has_parent_requirements' ; diff --git a/strictdoc/core/query_engine/query_object.py b/strictdoc/core/query_engine/query_object.py index fb0bad999..854e0df1f 100644 --- a/strictdoc/core/query_engine/query_object.py +++ b/strictdoc/core/query_engine/query_object.py @@ -42,6 +42,11 @@ def __init__(self, parent, _): self.parent = parent +class NodeContainsAnyFreeTextExpression: + def __init__(self, parent, _): + self.parent = parent + + class NodeHasChildRequirementsExpression: def __init__(self, parent, _): self.parent = parent @@ -133,6 +138,8 @@ def _evaluate(self, node, expression) -> bool: return self._evaluate_not_equal(node, expression) if isinstance(expression, NodeContainsExpression): return self._evaluate_node_contains(node, expression) + if isinstance(expression, NodeContainsAnyFreeTextExpression): + return self._evaluate_node_contains_any_text(node) if isinstance(expression, NodeHasParentRequirementsExpression): return self._evaluate_node_has_parent_requirements(node) if isinstance(expression, NodeHasChildRequirementsExpression): @@ -261,3 +268,12 @@ def _evaluate_node_contains( return True return False raise NotImplementedError + + def _evaluate_node_contains_any_text(self, node): + if not isinstance(node, Section): + raise TypeError( + f"node.contains_any_text can be only called on " + f"Section objects, got: {node.__class__.__name__}. To fix " + f"the error, prepend your query with node.is_section." + ) + return len(node.free_texts) > 0 diff --git a/strictdoc/core/query_engine/query_reader.py b/strictdoc/core/query_engine/query_reader.py index 1968f0790..5d63b1105 100644 --- a/strictdoc/core/query_engine/query_reader.py +++ b/strictdoc/core/query_engine/query_reader.py @@ -5,6 +5,7 @@ AndExpression, EqualExpression, InExpression, + NodeContainsAnyFreeTextExpression, NodeContainsExpression, NodeFieldExpression, NodeHasChildRequirementsExpression, @@ -26,6 +27,7 @@ EqualExpression, InExpression, NodeContainsExpression, + NodeContainsAnyFreeTextExpression, NodeFieldExpression, NodeHasChildRequirementsExpression, NodeHasParentRequirementsExpression, diff --git a/strictdoc/export/html/generators/project_statistics.py b/strictdoc/export/html/generators/project_statistics.py index 448e4663b..85e2294b4 100644 --- a/strictdoc/export/html/generators/project_statistics.py +++ b/strictdoc/export/html/generators/project_statistics.py @@ -4,6 +4,7 @@ from strictdoc import __version__ from strictdoc.backend.sdoc.models.requirement import Requirement +from strictdoc.backend.sdoc.models.section import Section from strictdoc.core.document_iterator import DocumentCachingIterator from strictdoc.core.document_tree_iterator import DocumentTreeIterator from strictdoc.core.project_config import ProjectConfig @@ -28,6 +29,9 @@ class DocumentTreeStats: # pylint: disable=too-many-instance-attributes total_tbc: int = 0 git_commit_hash: Optional[str] = None + # Section + sections_without_free_text: int = 0 + # UID requirements_no_uid: int = 0 requirements_no_links: int = 0 @@ -72,6 +76,10 @@ def export( for document in traceability_index.document_tree.document_list: document_iterator = DocumentCachingIterator(document) for node in document_iterator.all_content(): + if isinstance(node, Section): + if len(node.free_texts) == 0: + document_tree_stats.sections_without_free_text += 1 + if isinstance(node, Requirement): requirement: Requirement = assert_cast(node, Requirement) document_tree_stats.total_requirements += 1 diff --git a/strictdoc/export/html/templates/screens/project_statistics/main.jinja b/strictdoc/export/html/templates/screens/project_statistics/main.jinja index 39d7a614b..517a8074d 100644 --- a/strictdoc/export/html/templates/screens/project_statistics/main.jinja +++ b/strictdoc/export/html/templates/screens/project_statistics/main.jinja @@ -21,6 +21,12 @@ "Key":"Total documents", "Value": document_tree_stats.total_documents, }, + {"Section":"Sections"}, + { + "Key":"Sections without any text", + "Link": link_renderer.render_url('search?q=(node.is_section and not node.contains_any_text)'), + "Value": document_tree_stats.sections_without_free_text, + }, {"Section":"Requirements"}, { "Key":"Total requirements", diff --git a/tests/unit/strictdoc/core/query_engine/test_query_reader.py b/tests/unit/strictdoc/core/query_engine/test_query_reader.py index 888bd97c1..ca1386817 100644 --- a/tests/unit/strictdoc/core/query_engine/test_query_reader.py +++ b/tests/unit/strictdoc/core/query_engine/test_query_reader.py @@ -1,6 +1,7 @@ from strictdoc.core.query_engine.query_object import ( EqualExpression, InExpression, + NodeContainsAnyFreeTextExpression, NodeFieldExpression, NodeHasParentRequirementsExpression, NodeIsRequirementExpression, @@ -175,3 +176,14 @@ def test_90_not_expression(): assert isinstance(query_object, Query) assert isinstance(query_object.root_expression, NotExpression) assert isinstance(query_object.root_expression.expression, EqualExpression) + + +def test_95_contains_any_free_text(): + query = """\ +node.contains_any_text\ +""" + query_object = QueryReader.read(query) + assert isinstance(query_object, Query) + assert isinstance( + query_object.root_expression, NodeContainsAnyFreeTextExpression + )