Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions doorstop/core/publishers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,61 @@ def get_line_generator(self):
"""Return the lines generator for the class."""
return self.lines

@staticmethod
def parse_publish_entry(entry):
"""Return the attribute name and optional field selectors for a publish entry."""
if isinstance(entry, str):
return entry, None
if isinstance(entry, dict) and len(entry) == 1:
attr = next(iter(entry))
config = entry[attr]
if isinstance(config, dict):
return attr, config.get("fields")
return attr, None
return None, None

@staticmethod
def render_publish_value(value, fields=None, line_separator=", "):
"""Render a custom publish attribute value for non-HTML tabular output."""
if (
fields
and isinstance(fields, list)
and isinstance(value, list)
and value
and isinstance(value[0], dict)
):
rendered_values = []
for ref in value:
parts = []
for field_entry in fields:
if isinstance(field_entry, dict):
for url_key, label_spec in field_entry.items():
if isinstance(label_spec, str):
label = str(ref.get(label_spec, "")).strip()
elif isinstance(label_spec, dict):
label_fields = label_spec.get("label", [])
separator = label_spec.get("separator", ": ")
if isinstance(label_fields, str):
label_fields = [label_fields]
label = separator.join(
str(ref.get(field, "")).strip()
for field in label_fields
if str(ref.get(field, "")).strip()
)
else:
label = str(ref.get(url_key, "")).strip()
if label:
parts.append(label)
elif isinstance(field_entry, str):
part = str(ref.get(field_entry, "")).strip()
if part:
parts.append(part)
rendered_values.append(" ".join(parts))
return line_separator.join(value for value in rendered_values if value)
if isinstance(value, list):
return line_separator.join(str(item) for item in value)
return value

def setPath(self, path):
"""Set the export path of the tree and/or document."""
self.path = path
Expand Down
11 changes: 8 additions & 3 deletions doorstop/core/publishers/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,20 @@ def lines(self, obj, **kwargs):
# Add custom publish attributes
if item.document and item.document.publish:
header_printed = False
for attr in item.document.publish:
if not item.attribute(attr):
for entry in item.document.publish:
attr, fields = self.parse_publish_entry(entry)
if not attr:
continue
value = item.attribute(attr)
if not value:
continue
if not header_printed:
header_printed = True
yield "\\begin{longtable}{|l|l|}"
yield "Attribute & Value\\\\"
yield self.HLINE
yield "{} & {}".format(attr, item.attribute(attr))
rendered = self.render_publish_value(value, fields)
yield "{} & {}".format(attr, rendered)
if header_printed:
yield self.END_LONGTABLE
else:
Expand Down
23 changes: 22 additions & 1 deletion doorstop/core/publishers/tests/test_publisher_latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from unittest.mock import Mock, patch

from doorstop.core import publisher
from doorstop.core.publishers.tests.helpers import getLines
from doorstop.core.publishers.tests.helpers import YAML_STRUCTURED_ATTRIBUTES, getLines
from doorstop.core.publishers.tests.helpers_latex import YAML_LATEX_DOC
from doorstop.core.tests import MockDataMixIn, MockDocument, MockItem, MockItemAndVCS

Expand Down Expand Up @@ -200,6 +200,27 @@ def test_setting_publish_child_links_false(self):
# Assert
self.assertEqual(expected, result)

@patch("doorstop.settings.PUBLISH_CHILD_LINKS", False)
def test_lines_latex_item_with_structured_publish_attribute(self):
"""Verify structured publish entries can be rendered in LaTeX output."""
item = MockItemAndVCS(
"path/to/REQ001.yml",
_file=(
"level: 1\n"
"text: Test\n"
"spec-refs-from:\n"
" - url: https://example.com/spec\n"
" section: Sec\n"
),
)
item.document = MockDocument(
"path/to/.doorstop.yml", _file=YAML_STRUCTURED_ATTRIBUTES
)

text = getLines(publisher.publish_lines(item, ".tex"))

self.assertIn("spec-refs-from & Sec", text)

@patch("doorstop.settings.CHECK_REF", False)
def test_external_reference_check_ref_false(self):
"""Verify that external references are published correctly with settings.CHECK_REF set to False."""
Expand Down
25 changes: 23 additions & 2 deletions doorstop/core/publishers/tests/test_publisher_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from unittest.mock import Mock, patch

from doorstop.core import publisher
from doorstop.core.publishers.tests.helpers import getLines
from doorstop.core.tests import MockDataMixIn, MockItemAndVCS
from doorstop.core.publishers.tests.helpers import YAML_STRUCTURED_ATTRIBUTES, getLines
from doorstop.core.tests import MockDataMixIn, MockDocument, MockItemAndVCS
from doorstop.core.tests.helpers import on_error_with_retry


Expand Down Expand Up @@ -108,6 +108,27 @@ def test_lines_text_item_with_child_links(self):
# Assert
self.assertIn("Child links: tst1", text)

@patch("doorstop.settings.PUBLISH_CHILD_LINKS", False)
def test_lines_text_item_with_structured_publish_attribute(self):
"""Verify structured publish entries can be rendered in text output."""
item = MockItemAndVCS(
"path/to/REQ001.yml",
_file=(
"level: 1\n"
"text: Test\n"
"spec-refs-from:\n"
" - url: https://example.com/spec\n"
" section: Sec\n"
),
)
item.document = MockDocument(
"path/to/.doorstop.yml", _file=YAML_STRUCTURED_ATTRIBUTES
)

text = getLines(publisher.publish_lines(item, ".txt"))

self.assertIn("spec-refs-from: Sec", text)

def test_lines_text_item(self):
"""Verify text can be published from an item."""
with patch.object(
Expand Down
11 changes: 8 additions & 3 deletions doorstop/core/publishers/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,15 @@ def lines(self, obj, **_):
# Attributes
if item.document and item.document.publish:
yield ""
for attr in item.document.publish:
if not item.attribute(attr):
for entry in item.document.publish:
attr, fields = self.parse_publish_entry(entry)
if not attr:
continue
attr_line = "{}: {}".format(attr, item.attribute(attr))
value = item.attribute(attr)
if not value:
continue
rendered = self.render_publish_value(value, fields)
attr_line = "{}: {}".format(attr, rendered)
yield from self._chunks(attr_line)

yield "" # break between items
Expand Down
Loading