From 229ac256b8a1901e147172cdbdfc6b2d0a418cd0 Mon Sep 17 00:00:00 2001 From: Rob Taylor Date: Fri, 25 Oct 2024 13:55:09 +0100 Subject: [PATCH] feat: wip: Export document settings & attributes to xlsx --- doorstop/core/document.py | 101 +++++++++++++++++++++----------------- doorstop/core/exporter.py | 23 +++++++++ 2 files changed, 79 insertions(+), 45 deletions(-) diff --git a/doorstop/core/document.py b/doorstop/core/document.py index f4a1f5c9..1f0c8874 100644 --- a/doorstop/core/document.py +++ b/doorstop/core/document.py @@ -10,7 +10,7 @@ import yaml -from doorstop import common, settings +from doorstop import common, settings as dsettings from doorstop.common import ( DoorstopError, DoorstopInfo, @@ -135,7 +135,7 @@ def new( """ # Check separator - if sep and sep not in settings.SEP_CHARS: + if sep and sep not in dsettings.SEP_CHARS: raise DoorstopError("invalid UID separator '{}'".format(sep)) config = os.path.join(path, Document.CONFIG) @@ -193,7 +193,23 @@ def include(self, node): return self._load(text, yamlfile, loader=IncludeLoader) @property - def set_properties(self, data): + def settings(self): + """ + Document settings as a dict + """ + sets = {} + for key, value in self._data.items(): + if key == "prefix": + sets[key] = str(value) + elif key == "parent": + if value: + sets[key] = value + else: + sets[key] = value + return sets + + @settings.setter + def settings(self, settings): def fill_key(key, value): match key: case "prefix": @@ -210,17 +226,29 @@ def fill_key(key, value): msg = f"unexpected document setting '{key}' in: {self.config}" raise DoorstopError(msg) - # Store parsed data - sets = data.get("settings", {}) - for key, value in sets.items(): + for key, value in settings.items(): try: fill_key(key, value) except (AttributeError, TypeError, ValueError): msg = f"invalid value for '{key}' in: {self.config}" raise DoorstopError(msg) + @property + def attributes(self): + """ + Document attributes as a dict + """ + # Save the attributes + attributes = {} + if self._attribute_defaults: + attributes["defaults"] = self._attribute_defaults + if self._extended_reviewed: + attributes["reviewed"] = self._extended_reviewed + return attributes + + @attributes.setter + def attributes(self, attributes): # Store parsed attributes - attributes = data.get("attributes", {}) for key, value in attributes.items(): if key == "defaults": self._attribute_defaults = value @@ -234,49 +262,32 @@ def fill_key(key, value): ) raise DoorstopError(msg) - self.extensions = data.get("extensions", {}) - def load(self, reload=False): """Load the document's properties from its file.""" if self._loaded and not reload: return log.debug("loading {}...".format(repr(self))) data = self._load_with_include(self.config) - self._load_from_dict(data, reload) + self.settings = data["settings"] if "settings" in data else {} + self.attributes = data["attributes"] if "attributes" in data else {} + self.extensions = data.get("extensions", {}) # Set meta attributes self._loaded = True if reload: list(self._iter(reload=reload)) - @property - def properties(self): - data = {} - sets = {} - for key, value in self._data.items(): - if key == "prefix": - sets[key] = str(value) - elif key == "parent": - if value: - sets[key] = value - else: - sets[key] = value - data["settings"] = sets - # Save the attributes - attributes = {} - if self._attribute_defaults: - attributes["defaults"] = self._attribute_defaults - if self._extended_reviewed: - attributes["reviewed"] = self._extended_reviewed - if attributes: - data["attributes"] = attributes - return data - @edit_document def save(self): """Save the document's properties to its file.""" log.debug("saving {}...".format(repr(self))) - # Dump the data to YAML - text = self._dump(self.properties) + + data = {} + if self.settings: + data["settings"] = self.settings, + if self.attributes: + data["attributes"] = self.attributes + + text = self._dump(data) # Save the YAML to file self._write(text, self.config) # Set meta attributes @@ -322,7 +333,7 @@ def _iter(self, reload=False): except Exception: log.error("Unable to load: %s", item) raise - if settings.CACHE_ITEMS and self.tree: + if dsettings.CACHE_ITEMS and self.tree: self.tree._item_cache[ # pylint: disable=protected-access item.uid ] = item @@ -401,7 +412,7 @@ def sep(self): def sep(self, value): """Set the prefix-number separator to use for new item UIDs.""" # TODO: raise a specific exception for invalid separator characters? - assert not value or value in settings.SEP_CHARS + assert not value or value in dsettings.SEP_CHARS self._data["sep"] = value.strip() # TODO: should the new separator be applied to all items? @@ -489,7 +500,7 @@ def index(self, value): path = os.path.join(self.path, Document.INDEX) log.info("creating {} index...".format(self)) common.write_lines( - self._lines_index(self.items), path, end=settings.WRITE_LINESEPERATOR + self._lines_index(self.items), path, end=dsettings.WRITE_LINESEPERATOR ) @index.deleter @@ -525,7 +536,7 @@ def add_item(self, number=None, level=None, reorder=True, defaults=None, name=No name, self.prefix ) raise DoorstopError(msg) - if self.sep not in settings.SEP_CHARS: + if self.sep not in dsettings.SEP_CHARS: msg = "cannot add item with name '{}' to document '{}' with an invalid separator '{}'".format( name, self.prefix, self.sep ) @@ -617,13 +628,13 @@ def reorder(self, manual=True, automatic=True, start=None, keep=None, _items=Non @staticmethod def _lines_index(items): """Generate (pseudo) YAML lines for the document index.""" - yield "#" * settings.MAX_LINE_LENGTH + yield "#" * dsettings.MAX_LINE_LENGTH yield "# THIS TEMPORARY FILE WILL BE DELETED AFTER DOCUMENT REORDERING" yield "# MANUALLY INDENT, DEDENT, & MOVE ITEMS TO THEIR DESIRED LEVEL" yield "# A NEW ITEM WILL BE ADDED FOR ANY UNKNOWN IDS, i.e. - new: " yield "# THE COMMENT WILL BE USED AS THE ITEM TEXT FOR NEW ITEMS" yield "# CHANGES WILL BE REFLECTED IN THE ITEM FILES AFTER CONFIRMATION" - yield "#" * settings.MAX_LINE_LENGTH + yield "#" * dsettings.MAX_LINE_LENGTH yield "" yield "initial: {}".format(items[0].level if items else 1.0) yield "outline:" @@ -632,8 +643,8 @@ def _lines_index(items): lines = item.text.strip().splitlines() comment = lines[0].replace("\\", "\\\\") if lines else "" line = space + "- {u}: # {c}".format(u=item.uid, c=comment) - if len(line) > settings.MAX_LINE_LENGTH: - line = line[: settings.MAX_LINE_LENGTH - 3] + "..." + if len(line) > dsettings.MAX_LINE_LENGTH: + line = line[: dsettings.MAX_LINE_LENGTH - 3] + "..." yield line @staticmethod @@ -863,9 +874,9 @@ def get_issues( return # Reorder or check item levels - if settings.REORDER: + if dsettings.REORDER: self.reorder(_items=items) - elif settings.CHECK_LEVELS: + elif dsettings.CHECK_LEVELS: yield from self._get_issues_level(items) item_validator = ItemValidator() diff --git a/doorstop/core/exporter.py b/doorstop/core/exporter.py index 67859af4..d6a3cc21 100644 --- a/doorstop/core/exporter.py +++ b/doorstop/core/exporter.py @@ -264,6 +264,22 @@ def _file_xlsx(obj, path, auto=False): return path +def _add_properties_sheet(wb, document_properties): + sheet = wb.create_sheet(title="Document Properties") + sheet.append([ + "prefix", + "settings key", + "settings value", + "attributes key", + "attributes value", + ]) + for prefix, data in document_properties.items(): + for set_k, set_v in data["settings"].items(): + sheet.append([prefix, repr(set_k), repr(set_v)]) + for attr_k, attr_v in data["attributes"].items(): + sheet.append([prefix, "", "", repr(attr_k), repr(attr_v)]) + + def _get_xlsx(obj, path, auto): # Create a new workbook workbook = openpyxl.Workbook() @@ -272,10 +288,17 @@ def _get_xlsx(obj, path, auto): first_sheet = None if is_tree(obj): + document_properties = {} log.debug("xlsx export: exporting tree") for obj2, path2 in iter_documents(obj, path, ".xlsx"): sheet = _add_xlsx_sheet(workbook, obj2, auto) first_sheet = sheet or first_sheet + document_properties[obj2.prefix] = { + "settings": obj2.settings, + "attributes": obj2.attributes + } + if document_properties: + _add_properties_sheet(workbook, document_properties) else: log.debug("xlsx export: exporting single Item/Document") first_sheet = _add_xlsx_sheet(workbook, obj, auto)