From 2c20d1fae516d4c3ecf0fc5780c3a6b2139d6370 Mon Sep 17 00:00:00 2001 From: Kristoffer Nordstroem Date: Mon, 4 Feb 2019 23:35:38 +0100 Subject: [PATCH 1/5] Merge branch 'xls' --- Makefile | 4 +- doc/Config1.json | 13 +- doc/requirements/Import.req | 12 + doc/requirements/Import/Xls.req | 15 + .../Import/XlsBackupImportFile.req | 12 + .../Import/XlsDefaultSolvedby.req | 14 + .../Import/XlsFutureInventedOn.req | 12 + doc/requirements/OutputTextDocument.req | 2 +- doc/requirements/OutputXLS.req | 11 + doc/requirements/Processing.req | 2 +- doc/topics/Import.tic | 2 + doc/topics/ReqsDocument.tic | 1 + rmtoo/imports/__init__.py | 12 + rmtoo/imports/abcimports.py | 22 ++ rmtoo/imports/xls.py | 162 +++++++++++ rmtoo/lib/Import.py | 87 ++++++ rmtoo/lib/RmtooMain.py | 3 + .../storagebackend/txtfile/TxtRecordEntry.py | 4 + rmtoo/outputs/xls.py | 263 ++++++++++++++++++ rmtoo/tests/RMTTest-Import/RMTTest-Import.py | 97 +++++++ .../tests/RMTTest-Import/RMTTest-XlsImport.py | 164 +++++++++++ .../RMTTest-Import/test-reqs-doubleid.xlsx | Bin 0 -> 10032 bytes .../RMTTest-Import/test-reqs-future.xlsx | Bin 0 -> 10099 bytes .../RMTTest-Import/test-reqs-solvedby.xlsx | Bin 0 -> 7929 bytes rmtoo/tests/RMTTest-Import/test-reqs.xlsx | Bin 0 -> 9951 bytes .../tests/RMTTest-Output/DefaultTemplate.xlsx | Bin 0 -> 7253 bytes rmtoo/tests/RMTTest-Output/RMTTest-Xls.py | 157 +++++++++++ 27 files changed, 1067 insertions(+), 4 deletions(-) create mode 100644 doc/requirements/Import.req create mode 100644 doc/requirements/Import/Xls.req create mode 100644 doc/requirements/Import/XlsBackupImportFile.req create mode 100644 doc/requirements/Import/XlsDefaultSolvedby.req create mode 100644 doc/requirements/Import/XlsFutureInventedOn.req create mode 100644 doc/requirements/OutputXLS.req create mode 100644 doc/topics/Import.tic create mode 100644 rmtoo/imports/__init__.py create mode 100644 rmtoo/imports/abcimports.py create mode 100644 rmtoo/imports/xls.py create mode 100644 rmtoo/lib/Import.py create mode 100644 rmtoo/outputs/xls.py create mode 100644 rmtoo/tests/RMTTest-Import/RMTTest-Import.py create mode 100644 rmtoo/tests/RMTTest-Import/RMTTest-XlsImport.py create mode 100644 rmtoo/tests/RMTTest-Import/test-reqs-doubleid.xlsx create mode 100644 rmtoo/tests/RMTTest-Import/test-reqs-future.xlsx create mode 100644 rmtoo/tests/RMTTest-Import/test-reqs-solvedby.xlsx create mode 100644 rmtoo/tests/RMTTest-Import/test-reqs.xlsx create mode 100644 rmtoo/tests/RMTTest-Output/DefaultTemplate.xlsx create mode 100644 rmtoo/tests/RMTTest-Output/RMTTest-Xls.py diff --git a/Makefile b/Makefile index 9da7d2bf..f5fac61b 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ .PHONY: all .PHONY: all_html all: artifacts/requirements.pdf artifacts/req-graph1.png \ - artifacts/req-graph2.png all_html + artifacts/requirements.xlsx artifacts/req-graph2.png \ + all_html # Adding new files (especially requirements) can not automatically # handled. The 'force' target tries to handle this. @@ -57,6 +58,7 @@ clean: artifacts/requirements.aux artifacts/requirements.dvi \ artifacts/requirements.log artifacts/requirements.out \ artifacts/requirements.pdf artifacts/requirements.toc \ + artifacts/requirements.xlsx \ add_data.py* rm -fr debian/rmtoo build diff --git a/doc/Config1.json b/doc/Config1.json index 3b5d5d9a..18446faa 100644 --- a/doc/Config1.json +++ b/doc/Config1.json @@ -20,6 +20,11 @@ "stop_on_errors": false } }, + "import": { + "xls": { + "import_filename": "doc/import/requirements.xlsx" + } + }, "requirements": { "input": { "default_language": "en_GB", @@ -28,7 +33,8 @@ ] }, "inventors": [ - "flonatel" + "flonatel", + "kno" ], "stakeholders": [ "development", @@ -90,6 +96,11 @@ "output_filename": "artifacts/reqtopics.tex" } ], + "xls": [ + { + "output_filename": "artifacts/requirements.xlsx" + } + ], "oopricing1": [ { "output_filename": "artifacts/reqspricing" diff --git a/doc/requirements/Import.req b/doc/requirements/Import.req new file mode 100644 index 00000000..19486c52 --- /dev/null +++ b/doc/requirements/Import.req @@ -0,0 +1,12 @@ +Name: Import Requirements +Type: requirement +Invented on: 2018-07-01 +Invented by: kno +Description: Import requirements from other sources. +Note: Provide a simpler interface to the text files. +Owner: development +Status: not done +Priority: development:10 +Effort estimation: 5 +Topic: Import +Solved by: Import/Xls diff --git a/doc/requirements/Import/Xls.req b/doc/requirements/Import/Xls.req new file mode 100644 index 00000000..eda3d6b8 --- /dev/null +++ b/doc/requirements/Import/Xls.req @@ -0,0 +1,15 @@ +Name: Xlsx Import +Type: design decision +Invented on: 2018-09-23 +Invented by: kno +Owner: development +Description: Import requirement files from the exported xlsx + file. +Rationale: Some people prefer to handle their requirements in + familiar tools. +Status: not done +Priority: development:10 +Effort estimation: 3 +Topic: Import +Solved by: Import/XlsDefaultSolvedby Import/XlsFutureInventedOn + Import/XlsBackupImportFile diff --git a/doc/requirements/Import/XlsBackupImportFile.req b/doc/requirements/Import/XlsBackupImportFile.req new file mode 100644 index 00000000..66cf45c0 --- /dev/null +++ b/doc/requirements/Import/XlsBackupImportFile.req @@ -0,0 +1,12 @@ +Name: Backup Imported File +Type: requirement +Invented on: 2018-10-12 +Invented by: kno +Owner: development +Description: Xlsx files that have been successfully imported are + renamed to its current name with the current timestamp appended. +Rationale: Avoid importing files multiple times. +Status: assigned:kno:2018-10-12 +Priority: development:10 +Effort estimation: 1 +Topic: Import diff --git a/doc/requirements/Import/XlsDefaultSolvedby.req b/doc/requirements/Import/XlsDefaultSolvedby.req new file mode 100644 index 00000000..c2ef5875 --- /dev/null +++ b/doc/requirements/Import/XlsDefaultSolvedby.req @@ -0,0 +1,14 @@ +Name: Default SolvedBy argument +Type: requirement +Invented on: 2018-09-23 +Invented by: kno +Owner: development +Description: New requirements without a parent-requirement, i.e. it's + not in any \emph{SolvedBy} list, are attached to the first + requirement in the list. +Rationale: To make handling easier, don't burden people with hierarchy + and deeper complexities of the tool if it isn't required. +Status: finished:kno:2018-10-12:2h +Priority: development:10 +Effort estimation: 1 +Topic: Import diff --git a/doc/requirements/Import/XlsFutureInventedOn.req b/doc/requirements/Import/XlsFutureInventedOn.req new file mode 100644 index 00000000..27b06684 --- /dev/null +++ b/doc/requirements/Import/XlsFutureInventedOn.req @@ -0,0 +1,12 @@ +Name: Constrain valid Invented On +Type: requirement +Invented on: 2018-10-02 +Invented by: kno +Owner: development +Description: Imported requirements may not have a \emph{InventedOn} + date in the future. +Rationale: Leads to cryptic errors messages +Status: finished:kno:2018-10-06:4h +Priority: development:1 +Effort estimation: 1 +Topic: Import diff --git a/doc/requirements/OutputTextDocument.req b/doc/requirements/OutputTextDocument.req index 9982493f..2f112fa0 100644 --- a/doc/requirements/OutputTextDocument.req +++ b/doc/requirements/OutputTextDocument.req @@ -12,5 +12,5 @@ Priority: customers:8 Effort estimation: 8 Topic: Output Solved by: OutputBaseReqRefs OutputBaseTags OutputConfgbl OutputHTML - OutputPDF OutputTextSameBase Output/DiffOfTwoVersions + OutputPDF OutputXLS OutputTextSameBase Output/DiffOfTwoVersions # Added by rmtoo-normalize-dependencies diff --git a/doc/requirements/OutputXLS.req b/doc/requirements/OutputXLS.req new file mode 100644 index 00000000..8e836348 --- /dev/null +++ b/doc/requirements/OutputXLS.req @@ -0,0 +1,11 @@ +Name: Output of XLS +Topic: Output +Description: \textsl{rmtoo} \textbf{must} support genration of XLS file output with all requirements. +Status: finished +Owner: development +Invented by: kno +Invented on: 2010-02-12 +Rationale: Suits love to handle their data in Excel. +Type: design decision +Priority: management:8 +Effort estimation: 8 diff --git a/doc/requirements/Processing.req b/doc/requirements/Processing.req index 403ea353..4728d17e 100644 --- a/doc/requirements/Processing.req +++ b/doc/requirements/Processing.req @@ -14,5 +14,5 @@ Status: not done Priority: development:10 Effort estimation: 21 Topic: Basics -Solved by: Analytics AutomaticGeneration Input Output +Solved by: Analytics AutomaticGeneration Input Output Import # Added by rmtoo-normalize-dependencies diff --git a/doc/topics/Import.tic b/doc/topics/Import.tic new file mode 100644 index 00000000..06e7eacd --- /dev/null +++ b/doc/topics/Import.tic @@ -0,0 +1,2 @@ +Name: Import +IncludeRequirements: full diff --git a/doc/topics/ReqsDocument.tic b/doc/topics/ReqsDocument.tic index d6eb5320..1b5db28b 100644 --- a/doc/topics/ReqsDocument.tic +++ b/doc/topics/ReqsDocument.tic @@ -10,6 +10,7 @@ SubTopic: ReqTags SubTopic: Topic SubTopic: Input SubTopic: Output +SubTopic: Import SubTopic: EmacsMode SubTopic: Documentation SubTopic: AutoCreationOfArtifacts diff --git a/rmtoo/imports/__init__.py b/rmtoo/imports/__init__.py new file mode 100644 index 00000000..c04bf726 --- /dev/null +++ b/rmtoo/imports/__init__.py @@ -0,0 +1,12 @@ +"""rmtoo + + Free and Open Source Requirements Management Tool + + Imports __init__.py + +(c) 2019, 2020 by Kristoffer +SPDX-License-Identifier: GPL-3.0-or-later +This file is part of rmtoo. +For licensing details see COPYING + +""" diff --git a/rmtoo/imports/abcimports.py b/rmtoo/imports/abcimports.py new file mode 100644 index 00000000..8f52d2e7 --- /dev/null +++ b/rmtoo/imports/abcimports.py @@ -0,0 +1,22 @@ +'''Import Abstract Base Class + +''' +import abc +import sys + +if sys.version_info >= (3, 4): + ABC = abc.ABC +else: + ABC = abc.ABCMeta('ABC', (object,), {}) # compatible with Python 2 *and* 3 + + +class AbcImports(ABC): + '''Define an ABC for all imports classes''' + + @abc.abstractmethod + def __init__(self, self_cfg, import_dest): + raise NotImplementedError + + @abc.abstractmethod + def run(self): + raise NotImplementedError diff --git a/rmtoo/imports/xls.py b/rmtoo/imports/xls.py new file mode 100644 index 00000000..05e0b31a --- /dev/null +++ b/rmtoo/imports/xls.py @@ -0,0 +1,162 @@ +"""Import xls spreadsheets again + +Suits love Excel. + +(c) 2020 by Kristoffer +SPDX-License-Identifier: GPL-3.0-or-later +This file is part of rmtoo. +For licensing details see COPYING + +""" +from __future__ import unicode_literals +import os +import codecs +import datetime +import distutils.file_util +from collections import OrderedDict +import openpyxl + +from rmtoo.lib.logging import tracer +from rmtoo.lib.RMTException import RMTException +from rmtoo.lib.configuration.Cfg import Cfg +from rmtoo.imports.abcimports import AbcImports + + +class XlsImport(AbcImports): + """Import an xls-sheet created in the output plugins""" + + default_config = { + "import_filename": None, + "requirement_ws": "Requirements", + "topics_sheet": "Topics", + } + + def __init__(self, self_cfg, import_dest): + tracer.info("called") + self.useable = False + self._cfg = dict(self.default_config) + self._cfg.update(self_cfg) + self._dest = dict() + self._entries = None + self._topics = None + + import_dest_cfg = Cfg(import_dest) + try: + req_dirs = import_dest_cfg.get_rvalue("requirements_dirs") + if req_dirs[0] and os.path.isdir(req_dirs[0]): + self.useable = True + self._dest["requirements_dirs"] = req_dirs[0] + except RMTException: + self.useable = False + try: + topics_dirs = import_dest_cfg.get_rvalue("topics_dirs") + if topics_dirs[0] and os.path.isdir(topics_dirs[0]): + self.useable = True + self._dest["topics_dirs"] = topics_dirs[0] + except RMTException: + self.useable = False + self._wb = None + tracer.debug("Finished.") + + def run(self): + if self.useable: + filename = self._cfg["import_filename"] + if filename and os.path.isfile(filename): + self.import_file(filename) + + def import_file(self, filename): + self._wb = openpyxl.load_workbook(filename) + headers, self._entries, self._topics = self._extract_dict() + self._entries = self._verify_entries(self._entries) + self._write_to_files(self._entries, self._topics) + self._post_process_file(filename) + + def _extract_dict(self): + headers = None + entries = [] + req_wb = self._wb[self._cfg["requirement_ws"]] + for row in req_wb: + if row[0].value == "ID": + headers = [cell.value for cell in row] + elif headers and row[0].value: + req = OrderedDict([(headers[i], cell.value) + for i, cell in enumerate(row)]) + entries.append(req) + + topics = OrderedDict() + topics_ws = self._wb[self._cfg["topics_sheet"]] + for row in topics_ws: + if row[0].value: + topic_name = row[0].value + topics[topic_name] = [] + if len(row) >= 3 and row[1].value: + topics[topic_name].append((row[1].value, row[2].value)) + + return headers, entries, topics + + @staticmethod + def _verify_entries(entries): + """Verify requirement id are not twice in the list. It could easily a + copy-paste error + + Future inventedOn dates causes errors. + + """ + ids = [] + for entry in entries: + assert entry["ID"] not in ids + ids.append(entry["ID"]) + + for entry in entries: + if (entry.get("Invented on", datetime.datetime.now()) > + datetime.datetime.now()): + raise RMTException(118, "Future inventedOn not accepted") + + # add unsolved entries to first + # Add first to list to avoid self-solving + solved_entries = [entries[0]["ID"]] + for entry in entries: + solved_str = entry.get("Solved by", "") + if solved_str: + solved = solved_str.split(" ") + solved_purged = [x for x in solved if x.strip()] + solved_entries.extend(solved_purged) + + added_entries = [] + for entry in entries: + if entry["ID"] not in solved_entries: + added_entries.append(entry["ID"]) + if added_entries: + entries[0]["Solved by"] = (entries[0]["Solved by"].strip() + " " + + " ".join(added_entries)) + return entries + + def _write_to_files(self, entries, topics): + for entry in entries: + filepath = os.path.join(self._dest["requirements_dirs"], + entry["ID"] + ".req") + with codecs.open(filepath, "w", "utf-8") as fhdl: + for key, value in entry.items(): + content = None + if key == "ID": + pass + elif isinstance(value, datetime.date): + content = str(value.date()) + elif value: + content = "\n ".join(str(value).splitlines()) + if content: + fhdl.write(": ".join([key, content]) + os.linesep) + + for name in topics.keys(): + filepath = os.path.join(self._dest["topics_dirs"], name + ".tic") + with codecs.open(filepath, "w", "utf-8") as fhdl: + for key, value in topics[name]: + fhdl.write(": ".join([key, value]) + os.linesep) + + @staticmethod + def _post_process_file(filename): + old_file = filename.split(".") + app = datetime.datetime.now().isoformat() + dest_file = old_file[:-1] + [app] + old_file[-1:] + dest = ".".join(dest_file) + distutils.file_util.move_file(filename, dest) diff --git a/rmtoo/lib/Import.py b/rmtoo/lib/Import.py new file mode 100644 index 00000000..93562cd9 --- /dev/null +++ b/rmtoo/lib/Import.py @@ -0,0 +1,87 @@ +"""Import data from other sources and write to the file system entries + +(c) 2018, 2020 by Kristoffer +SPDX-License-Identifier: GPL-3.0-or-later +This file is part of rmtoo. +For licensing details see COPYING + +""" +from __future__ import unicode_literals +from stevedore import extension +from six import iteritems + +from rmtoo.lib.logging import tracer +from rmtoo.lib.configuration.Cfg import Cfg +from rmtoo.imports.abcimports import AbcImports + + +class Import: + """Class parsing the configuration and importing from configured + import locations to configured input location. + + The idea is to not deal with, e.g., Excel sheets in this tool but + to enable seamless import and export. + + The default configuration provides no information about the + location of the *ts_topics.sources* location. + + :ivar _input_dir: latest item with requirements_dir and topics_dir + + :Example: an example configuration looks as follows. + "import": { + "xls": { + "import_filename": "import/requirements.xls" + } + } + + """ + + DEFAULT_CONFIG = {"xls": {"import_filename": None}} + + def __init__(self, config): + """Sets up Import for use.""" + tracer.info("called") + self.__plugin_manager = extension.ExtensionManager( + namespace="rmtoo.imports.plugin", invoke_on_load=False) + + assert config # we need a configuration + if "import" in config: + self._config = config["import"] + else: + self._config = self.DEFAULT_CONFIG + self._cfg = Cfg(self._config) + + self._input_dir = {"requirements_dirs": None, "topics_dirs": None} + self._extract_input_dir(config) + + self._import_obj = [] + self._set_run_modules() + tracer.debug("Finished.") + + def _extract_input_dir(self, config): + input_cfg = Cfg(config) + sources_dirs = "topics.ts_common.sources" + for cfg in input_cfg.get_value(sources_dirs): + for req_idx in [ + "requirements_dirs", "topics_dirs", "sources_dirs" + ]: + if req_idx in cfg[1]: + self._input_dir[req_idx] = cfg[1][req_idx] + + def _set_run_modules(self): + for module_name, cfg in iteritems(self._config): + import_obj = self.__plugin_manager[module_name].plugin( + cfg, self._input_dir) + if isinstance(import_obj, AbcImports): + self._import_obj.append(import_obj) + + @staticmethod + def execute(config): + """Process Import with configuration file""" + foreign_import = Import(config) + foreign_import.process_all() + + def process_all(self): + """Process all members""" + for module in self._import_obj: + module.run() diff --git a/rmtoo/lib/RmtooMain.py b/rmtoo/lib/RmtooMain.py index d24fc073..ab8c5422 100644 --- a/rmtoo/lib/RmtooMain.py +++ b/rmtoo/lib/RmtooMain.py @@ -19,11 +19,14 @@ from rmtoo.lib.TopicContinuumSet import TopicContinuumSet from rmtoo.lib.Analytics import Analytics from rmtoo.lib.Output import Output +from rmtoo.lib.Import import Import from rmtoo.lib.main.MainHelper import MainHelper from rmtoo.lib.logging import configure_logging def execute_cmds(config, input_mods, _mstdout, mstderr): + Import.execute(config) # Import foreign data + '''Checks are always done - to be sure that e.g. the dependencies are correct. Please note: there is no 'ONE' latest continuum any more diff --git a/rmtoo/lib/storagebackend/txtfile/TxtRecordEntry.py b/rmtoo/lib/storagebackend/txtfile/TxtRecordEntry.py index 525600d4..fb12b930 100644 --- a/rmtoo/lib/storagebackend/txtfile/TxtRecordEntry.py +++ b/rmtoo/lib/storagebackend/txtfile/TxtRecordEntry.py @@ -94,3 +94,7 @@ def set_comment(self, comment): def get_content_with_nl(self): """Return the raw content""" return self.content_raw + + def get_content_trimmed_with_nl(self): + """Return the raw content""" + return [x.strip() for x in self.content_raw] diff --git a/rmtoo/outputs/xls.py b/rmtoo/outputs/xls.py new file mode 100644 index 00000000..7ee5e55f --- /dev/null +++ b/rmtoo/outputs/xls.py @@ -0,0 +1,263 @@ +"""Output the content to a XLS sheet. Suits love Excel. + +(c) 2018, 2020 by Kristoffer +SPDX-License-Identifier: GPL-3.0-or-later +This file is part of rmtoo. +For licensing details see COPYING + +""" +from __future__ import unicode_literals + +import openpyxl +import datetime +import shutil + +from rmtoo.lib.StdOutputParams import StdOutputParams +from rmtoo.lib.ExecutorTopicContinuum import ExecutorTopicContinuum +from rmtoo.lib.logging import tracer +from rmtoo.lib.CreateMakeDependencies import CreateMakeDependencies + + +class XlsHandler: + """Act as an abstraction layer between rmtoo-objects and OpenPyxl + related things + + The default configuration is intended to sort the important + objects. Users can adapt it to their needs. + + The filename is handled in the Xls' parent class. + + """ + + default_config = { + "output_filename": + "artifacts/requirements.xlsx", + "req_attributes": [ + "ID", + "Name", + "Topic", + "Description", + "Status", + "Owner", + "Invented by", + "Invented on", + ], + "headers": [ + "ID", + "Name", + "Topic", + "Description", + "Status", + "Rationale", + "Owner", + "Effort estimation", + "Invented on", + ], + "req_sheet": + "Requirements", + "topic_sheet": + "Topics", + } + + def __init__(self, filename, config=None): + tracer.info("Creating XLS workbook: " + filename) + self.__filename = filename + self._cfg = self.default_config + for key, value in config.items(): + self._cfg[key] = value + + # We require those headers at least + self._req_headers = self._cfg["req_attributes"] + self._headers = list(self._cfg["headers"]) + self.req_row = 1 + self._reqlist = [] + self._topiclist = [] + + self._prepare_template() + + def write(self): + self._add_header() + self._write_reqs() + self._write_topics() + self._wb.save(filename=self.__filename) + + def _prepare_template(self): + usable = False + if "template_filename" in self._cfg: + try: + usable = self._template_useable() + except AssertionError: + tracer.warning("Using default template") + if usable: + shutil.copyfile(self._cfg["template_filename"], self.__filename) + self._wb = openpyxl.load_workbook(filename=self.__filename) + self._ws_req = self._wb[self._cfg["req_sheet"]] + self._ws_topics = self._wb[self._cfg["topic_sheet"]] + self._ws_cfg = self._wb["Configuration"] + else: + self._new_workbook() + + def _template_useable(self): + filename = self._cfg["template_filename"] + twb = openpyxl.load_workbook(filename=filename, read_only=True) + ws_req = twb[self._cfg["req_sheet"]] + + req_row = None + for j, row in enumerate(ws_req.iter_rows(min_row=1, max_col=1)): + cell = row[0] + if cell.value == "ID": + req_row = j + 1 + break + assert req_row + + header_row = ws_req[req_row] + headers = list() + for cell in header_row: + headers.append(cell.value) + # We want the required headers in our template and just copy this + # list + assert set(self._headers).issubset(headers) + + self.req_row = req_row + self._headers = headers + return True + + def _new_workbook(self): + self._wb = openpyxl.Workbook() + self._ws_req = self._wb.active + self._ws_req.title = self._cfg["req_sheet"] + self._ws_topics = self._wb.create_sheet(self._cfg["topic_sheet"]) + self._ws_cfg = self._wb.create_sheet("Configuration") + + def _add_header(self): + col = 1 + for val in self._headers: + cell = self._ws_req.cell(column=col, row=self.req_row) + if cell.value != val: + cell.value = val + col += 1 + self.req_row += 1 + + def _write_reqs(self): + for req in self._reqlist: + col = 1 + for key in self._headers: + if key in req: + val = req[key] + if isinstance(val, list): + val = str(val) + self._ws_req.cell(column=col, row=self.req_row, value=val) + col += 1 + self.req_row += 1 + self._reqlist = [] + + def _write_topics(self): + row = 1 + for topic in self._topiclist: + self._ws_topics.cell(column=1, row=row, value=topic.name) + for key, content in topic.members.items(): + self._ws_topics.cell(column=2, row=row, value=key) + self._ws_topics.cell(column=3, row=row, value=content) + row += 1 + self._topiclist = [] + + def add_req(self, req): + req_dict = self._req_extract_dict(req) + self._check_required_fields(req_dict) + self._add_new_headers(req_dict) + self._reqlist.append(req_dict) + + @staticmethod + def _req_extract_dict(req): + req_dict = {"ID": req.get_id()} + for rec in req.record: + if rec.get_tag() in req.values and isinstance( + req.values[rec.get_tag()], datetime.date): + req_dict[rec.get_tag()] = req.values[rec.get_tag()] + else: + req_dict[rec.get_tag()] = "\n".join( + rec.get_content_trimmed_with_nl()) + return req_dict + + def _check_required_fields(self, req_dict): + """Will raise KeyError if required fields aren't available""" + for val in self._req_headers: + if val not in req_dict.keys(): + tracer.warning("Key (" + val + ") error in " + req_dict["ID"]) + assert val in req_dict.keys() + + def _add_new_headers(self, req_dict): + for key in list(req_dict.keys()): + if key not in self._headers: + self._headers.append(key) + + def add_topic(self, topic): + class TopicList: + def __init__(self, name): + self.name = name + self.members = {} + + ctop = TopicList(topic.name) + for i in topic.get_tags(): + ctop.members[i.get_tag()] = i.get_content() + self._topiclist.append(ctop) + + +class Xls(StdOutputParams, ExecutorTopicContinuum, CreateMakeDependencies): + def __init__(self, oconfig): + """Create an openpyxl output object.""" + tracer.info("Called.") + StdOutputParams.__init__(self, oconfig) + CreateMakeDependencies.__init__(self) + self.__ce3set = None + self.__fd = None + self._opiface = XlsHandler(self._output_filename, self._config) + + @staticmethod + def strescape(string): + """Escapes a string: hexifies it.""" + result = "" + for fchar in string: + if ord(fchar) >= 32 and ord(fchar) < 127: + result += fchar + else: + result += "%02x" % ord(fchar) + return result + + def topic_set_pre(self, _topics_set): + pass + + def topic_set_post(self, topic_set): + """Clean up file.""" + tracer.debug("Clean up file.") + self._opiface.write() + tracer.debug("Finished.") + + def topic_pre(self, topic): + """Output one topic.""" + self._opiface.add_topic(topic) + + def topic_post(self, _topic): + """Cleanup things for topic.""" + pass + + def topic_name(self, name): + pass + + def topic_text(self, text): + pass + + def requirement_set_pre(self, rset): + """Prepare the requirements set output.""" + self.__ce3set = rset.get_ce3set() + self.__testcases = rset.get_testcases() + + def requirement_set_sort(self, list_to_sort): + """Sort by id.""" + return sorted(list_to_sort, key=lambda r: r.get_id()) + + def requirement(self, req): + self._opiface.add_req(req) + + def cmad_topic_continuum_pre(self, _): + pass diff --git a/rmtoo/tests/RMTTest-Import/RMTTest-Import.py b/rmtoo/tests/RMTTest-Import/RMTTest-Import.py new file mode 100644 index 00000000..b68f06cc --- /dev/null +++ b/rmtoo/tests/RMTTest-Import/RMTTest-Import.py @@ -0,0 +1,97 @@ +# (c) 2018 Kristoffer Nordstroem, see COPYING +import os +import pytest +import distutils.file_util + +from rmtoo.lib.Import import Import +from rmtoo.imports.xls import XlsImport +from rmtoo.tests.lib.Utils import create_tmp_dir, delete_tmp_dir +from rmtoo.lib.Encoding import Encoding + +LDIR = Encoding.to_unicode(os.path.dirname(os.path.abspath(__file__))) + + +@pytest.fixture(scope='function') +def tmpdir(): + tmpdir = create_tmp_dir() + yield tmpdir + delete_tmp_dir(tmpdir) + + +@pytest.fixture +def def_cfg(tmpdir): + def_cfg_imp_dest = {'topics': {'ts_common': { + 'sources': [['dummydriver', { + 'requirements_dirs': [tmpdir], + 'topics_dirs': [tmpdir] + }]]}}, 'tmpdir': tmpdir} + return def_cfg_imp_dest + + +class RMTTestImport: + '''Test-Class to import the modified artifacts again''' + + def rmttest_invalid_config_parser(self): + '''Just figure out where it blows up ''' + with pytest.raises(AssertionError): + Import(None) + + def rmttest_config_parser_wo_cfg(self, def_cfg): + '''Assert the default configuration is loaded, ignore the import + destination directory configuration''' + cfg = {'import': {}, "dummy": "the universe is mind-bogglingly big"} + cfg.update(def_cfg) + importer = Import(cfg) + assert not importer._config + assert not importer._import_obj + + def rmttest_config_parser_input_dir(self, def_cfg): + importer = Import(def_cfg) + assert (importer._input_dir['requirements_dirs'][0] + == def_cfg['tmpdir']) + assert (importer._input_dir['topics_dirs'][0] + == def_cfg['tmpdir']) + + def rmttest_config_parser(self, def_cfg): + '''Assert the default configuration is loaded, ignore the import + destination directory configuration''' + cfg = {"import": {"xls": { + 'import_filename': "asdf.xls"}}} + cfg.update(def_cfg) + importer = Import(cfg) + assert len(importer._import_obj) == 1 + assert isinstance(importer._import_obj[0], XlsImport) + + def rmttest_config_run_with_default_cfg(self, def_cfg): + tmpdir = def_cfg['tmpdir'] + src_fn = os.path.join(LDIR, "test-reqs.xlsx") + tmp_fn = os.path.join(tmpdir, "test-reqs.xlsx") + distutils.file_util.copy_file(src_fn, tmp_fn) + + cfg = {"import": {"xls": { + 'import_filename': tmp_fn}}} + cfg.update(def_cfg) + importer = Import(cfg) + importer.process_all() + + assert os.path.isfile(os.path.join(tmpdir, 'AutomaticGeneration.req')) + assert os.path.isfile(os.path.join(tmpdir, 'Completed.req')) + assert os.path.isfile(os.path.join(tmpdir, 'TestNewlines.req')) + + def rmttest_error_double_ids(self, def_cfg): + tmpdir = def_cfg['tmpdir'] + src_fn = os.path.join(LDIR, "test-reqs-doubleid.xlsx") + tmp_fn = os.path.join(tmpdir, "test-reqs-doubleid.xlsx") + distutils.file_util.copy_file(src_fn, tmp_fn) + + cfg = {"import": {"xls": { + 'import_filename': tmp_fn}}} + cfg.update(def_cfg) + importer = Import(cfg) + with pytest.raises(AssertionError): + importer.process_all() + + assert not os.path.isfile(os.path.join(tmpdir, + 'AutomaticGeneration.req')) + assert not os.path.isfile(os.path.join(tmpdir, 'Completed.req')) + assert not os.path.isfile(os.path.join(tmpdir, 'TestNewlines.req')) diff --git a/rmtoo/tests/RMTTest-Import/RMTTest-XlsImport.py b/rmtoo/tests/RMTTest-Import/RMTTest-XlsImport.py new file mode 100644 index 00000000..f9e641b4 --- /dev/null +++ b/rmtoo/tests/RMTTest-Import/RMTTest-XlsImport.py @@ -0,0 +1,164 @@ +# (c) 2018 Kristoffer Nordstroem, see COPYING +from __future__ import unicode_literals +import os +import re +import codecs +import pytest +import distutils.file_util + +from rmtoo.lib.Requirement import Requirement +from rmtoo.imports.xls import XlsImport +from rmtoo.tests.lib.Utils import create_tmp_dir, delete_tmp_dir +from rmtoo.lib.RMTException import RMTException +from rmtoo.lib.Encoding import Encoding + +LDIR = os.path.dirname(os.path.abspath(__file__)) +LIPSUM = ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " + "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim " + "veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea " + "commodo consequat. Duis aute irure dolor in reprehenderit in voluptate " + "velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint " + "occaecat cupidatat non proident, sunt in culpa qui officia deserunt " + "mollit anim id est laborum.") + + +@pytest.fixture(scope='function') +def tmpdir(): + tmpdir = create_tmp_dir() + yield tmpdir + delete_tmp_dir(tmpdir) + + +@pytest.fixture +def dest_dir(tmpdir): + dest_dirs = {u'requirements_dirs': [Encoding.to_unicode(tmpdir)], + u'topics_dirs': [Encoding.to_unicode(tmpdir)]} + return dest_dirs + + +class RMTTestXlsImport: + '''Test-Class to import the xls artifact''' + + imp_fn = os.path.join(LDIR, 'test-reqs.xlsx') + config = {u'import_filename': imp_fn, + u'requirement_ws': u'Requirements', + u'topics_sheet': u'Topics'} + + def rmttest_invalid_config_parser(self): + '''Just figure out where it blows up''' + dest_dirs = {u'requirements_dirs': [Encoding.to_unicode(tmpdir)], + u'topics_dirs': [Encoding.to_unicode(tmpdir)]} + importer = XlsImport({}, dest_dirs) + assert not importer.useable + + def rmttest_config_run_with_default_cfg(self, dest_dir): + tmpdir = dest_dir['requirements_dirs'][0] + tmp_fn = os.path.join(tmpdir, 'test-reqs.xlsx') + config = dict(self.config) + distutils.file_util.copy_file(config[u'import_filename'], tmp_fn) + config[u'import_filename'] = tmp_fn + + importer = XlsImport(config, dest_dir) + assert importer.useable + importer.run() + + assert os.path.isfile(os.path.join(tmpdir, 'AutomaticGeneration.req')) + completed_filename = os.path.join(tmpdir, 'Completed.req') + assert os.path.isfile(completed_filename) + + # Assert ID is filename and not written to file + id_occ = [re.findall(r'^ID:', line) + for line in open(completed_filename)] + for i in id_occ: + assert not i + + def rmttest_treat_newlines_correctly(self, dest_dir): + tmpdir = dest_dir['requirements_dirs'][0] + + config = dict(self.config) + tmp_fn = os.path.join(tmpdir, 'test-reqs.xlsx') + distutils.file_util.copy_file(config[u'import_filename'], tmp_fn) + config[u'import_filename'] = tmp_fn + + importer = XlsImport(config, dest_dir) + importer.run() + + newlines_filename = os.path.join(tmpdir, 'TestNewlines.req') + assert os.path.isfile(newlines_filename) + with codecs.open(newlines_filename, encoding='utf-8') as nl_fh: + req_content = nl_fh.read() + nl_req = Requirement(req_content, 'TestNewlines.req', + newlines_filename, None, None) + + # Test Description + parsed_desc = "\n".join(nl_req.record[2]. + get_content_trimmed_with_nl()) + assert parsed_desc == LIPSUM + "\n\nASDF" + + parsed_note = "\n".join(nl_req.record[10]. + get_content_trimmed_with_nl()) + assert parsed_note == "Lipsum\n\nHandle it well" + + parsed_invon = "\n".join(nl_req.record[7]. + get_content_trimmed_with_nl()) + assert parsed_invon == "2010-03-06" + + def rmttest_future_invented_on(self, dest_dir, record_property): + '''Ensure future InventedOn are not imported''' + record_property('req', 'Import/XlsFutureInventedOn-deadbeef') + tmpdir = dest_dir['requirements_dirs'][0] + + lcfg = dict(self.config) + imp_fn = os.path.join(LDIR, 'test-reqs-future.xlsx') + tmp_fn = os.path.join(tmpdir, 'test-reqs-future.xlsx') + distutils.file_util.copy_file(imp_fn, tmp_fn) + lcfg[u'import_filename'] = tmp_fn + + importer = XlsImport(lcfg, dest_dir) + assert importer.useable + with pytest.raises(RMTException): + importer.run() + + def rmttest_set_solvedby(self, dest_dir, record_property): + '''Esnure stuff''' + record_property('req', 'Import/XlsDefaultSolvedby-deadbeef') + tmpdir = dest_dir['requirements_dirs'][0] + + lcfg = dict(self.config) + imp_fn = os.path.join(LDIR, 'test-reqs-solvedby.xlsx') + tmp_fn = os.path.join(tmpdir, 'test-reqs-solvedby.xlsx') + distutils.file_util.copy_file(imp_fn, tmp_fn) + lcfg[u'import_filename'] = tmp_fn + + importer = XlsImport(lcfg, dest_dir) + assert importer.useable + importer.run() + + assert importer._entries[0]['ID'] == 'SW-101' + assert importer._entries[0]['Solved by'] == 'SW-102 SW-104 SW-105' + + def rmttest_defcfg_import_topics(self, dest_dir): + tmpdir = dest_dir['requirements_dirs'][0] + tmp_fn = os.path.join(tmpdir, 'test-reqs.xlsx') + config = dict(self.config) + distutils.file_util.copy_file(config[u'import_filename'], tmp_fn) + config[u'import_filename'] = tmp_fn + + importer = XlsImport(config, dest_dir) + assert importer.useable + importer.run() + + assert importer._topics['GUI'][0] == ('Name', 'GUI') + assert os.path.isfile(os.path.join(tmpdir, 'rmtoo.tic')) + completed_filename = os.path.join(tmpdir, 'GUI.tic') + assert os.path.isfile(completed_filename) + + # Assert ID is filename and not written to file + found = False + id_occ = [re.findall(r'^Name:.*', line) + for line in open(completed_filename)] + for i in id_occ: + if i and i[0] == 'Name: GUI': + found = True + assert found diff --git a/rmtoo/tests/RMTTest-Import/test-reqs-doubleid.xlsx b/rmtoo/tests/RMTTest-Import/test-reqs-doubleid.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..a6f5a9a45edd645dd06655947a5170d39b14458e GIT binary patch literal 10032 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 Nay&aHC90RJ{{bZqx-S3# literal 0 HcmV?d00001 diff --git a/rmtoo/tests/RMTTest-Import/test-reqs-solvedby.xlsx b/rmtoo/tests/RMTTest-Import/test-reqs-solvedby.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..c0be66ed2fbba5a0782fb9a2dd4496d07f6b34a3 GIT binary patch literal 7929 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 literal 0 HcmV?d00001 diff --git a/rmtoo/tests/RMTTest-Import/test-reqs.xlsx b/rmtoo/tests/RMTTest-Import/test-reqs.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3d963337695bea19330b6088adf8f1f6d78b3f8a GIT binary patch literal 9951 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 ZZ!?F6GV*Oe!@&V=kG|WWaU;FE`yabyX4U`z literal 0 HcmV?d00001 diff --git a/rmtoo/tests/RMTTest-Output/RMTTest-Xls.py b/rmtoo/tests/RMTTest-Output/RMTTest-Xls.py new file mode 100644 index 00000000..721cbc98 --- /dev/null +++ b/rmtoo/tests/RMTTest-Output/RMTTest-Xls.py @@ -0,0 +1,157 @@ +# (c) 2018 Kristoffer Nordstroem, see COPYING + +import os +from datetime import datetime +import openpyxl +import pytest +from unittest.mock import Mock + +from rmtoo.outputs.xls import XlsHandler as xh +from rmtoo.lib.Requirement import Requirement +from rmtoo.tests.lib.Utils import create_tmp_dir, delete_tmp_dir +from rmtoo.tests.lib.TestConfig import TestConfig + +LDIR = os.path.dirname(os.path.abspath(__file__)) + + +def create_req(req_id): + req_txt_items = {} + inv_date = datetime.strptime('1970-01-01', "%Y-%m-%d").date() + req_values = { + 'Name': 'Aston Martin DB5', + 'Topic': 'Escape Routes', + 'Description': "Flying out the roof", + 'Priority': 'development', + 'Owner': '007', + 'Invented on': inv_date.isoformat(), + 'Invented by': 'Q', + 'Status': 'not done', + 'Class': 'requirement' + } + req = Requirement(u"", req_id, None, None, TestConfig()) + for key, value in req_values.items(): + req.record.append(MockRecordEntry(key, value)) + req_txt_items[key] = MockRecordEntry(None, value) + req_txt_items['Invented on'] = inv_date + req.values.update(req_txt_items) + return req + + +class MockRecordEntry: + def __init__(self, tag, txt_str): + self.__txt_tag = tag + self.__txt_string = txt_str + + def get_content(self): + return self.__txt_string + + def get_content_trimmed_with_nl(self): + return [x.strip() for x in self.__txt_string.splitlines()] + + def get_output_string(self): + return self.__txt_string + + def get_tag(self): + return self.__txt_tag + + +class RMTTestOutputXls: + "Test-Class for the xlsx output class" + + def teardown_method(self): + if self.__tmpdir: + delete_tmp_dir(self.__tmpdir) + + def setup_method(self): + self.__tmpdir = create_tmp_dir() + self.oconfig = xh.default_config + self._filename = os.path.join(self.__tmpdir, "reqs.xlsx") + self.oconfig["output_filename"] = self._filename + + def rmttest_adding_req_header(self): + xlsh = xh(self._filename, self.oconfig) + xlsh.write() + + twb = openpyxl.load_workbook(filename=self._filename) + rws = twb['Requirements'] + + i = 0 + for tval in xh.default_config['headers']: + i += 1 + assert rws.cell(row=1, column=i).value == tval + assert i == 9 + assert rws['B1'].value == "Name" + + def rmttest_adding_req(self): + xlsh = xh(self._filename, self.oconfig) + xlsh.add_req(create_req(u'SW-101')) + xlsh.add_req(create_req(u'SW-102')) + xlsh.write() + + twb = openpyxl.load_workbook(filename=self._filename, + guess_types=True) + rws = twb['Requirements'] + assert rws['A2'].value == "SW-101" + assert rws['G2'].value == "007" + assert rws['I2'].value.date().isoformat() == "1970-01-01" + + assert rws['A3'].value == "SW-102" + + def rmttest_adding_req_with_missing_field(self): + xlsh = xh(self._filename, self.oconfig) + req = create_req(u'SW-101') + del req.values['Invented by'] + for i in range(len(req.record)): + if req.record[i].get_content() == 'Q': + req.record.pop(i) + break + with pytest.raises(AssertionError): + xlsh.add_req(req) + + def rmttest_adding_topic(self): + xlsh = xh(self._filename, self.oconfig) + topic_tags = [Mock(**{'get_tag.return_value': "asdf", + 'get_content.return_value': "qwer"})] + topic_cfg = {'get_tags.return_value': topic_tags} + topic = Mock(**topic_cfg) + topic.name = "SuperTopic" + xlsh.add_topic(topic) + xlsh.write() + + twb = openpyxl.load_workbook(filename=self._filename, + guess_types=True) + rws = twb['Topics'] + assert rws['A1'].value == "SuperTopic" + assert rws['B1'].value == "asdf" + assert rws['C1'].value == "qwer" + + +class RMTTestOutputXlsTemplate: + '''Test-Class for the xlsx output with a template workbook''' + + def teardown_method(self): + if self.__tmpdir: + delete_tmp_dir(self.__tmpdir) + + def setup_method(self): + self.__tmpdir = create_tmp_dir() + self.oconfig = xh.default_config + self._filename = os.path.join(self.__tmpdir, "reqs.xlsx") + self.oconfig["output_filename"] = self._filename + self.oconfig["template_filename"] = os.path.join( + LDIR, 'DefaultTemplate.xlsx') + + def rmttest_adding_req(self): + xlsh = xh(self._filename, self.oconfig) + xlsh.add_req(create_req(u'SW-101')) + xlsh.add_req(create_req(u'SW-102')) + xlsh.write() + + twb = openpyxl.load_workbook(filename=self._filename, + guess_types=True) + rws = twb['Requirements'] + assert rws['A3'].value == "SW-101" + assert rws['G3'].value == "007" + assert rws['I3'].value.date().isoformat() == "1970-01-01" + + assert rws['A4'].value == "SW-102" From 68a657c104f903b7e337931022b23741c51ff3b6 Mon Sep 17 00:00:00 2001 From: Kristoffer Nordstroem Date: Thu, 7 Feb 2019 00:46:35 +0100 Subject: [PATCH 2/5] fixing incompatibilities with newly released openpyxl 2.6.0 --- rmtoo/imports/xls.py | 2 +- rmtoo/tests/RMTTest-Output/RMTTest-Xls.py | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/rmtoo/imports/xls.py b/rmtoo/imports/xls.py index 05e0b31a..d4f250e3 100644 --- a/rmtoo/imports/xls.py +++ b/rmtoo/imports/xls.py @@ -85,7 +85,7 @@ def _extract_dict(self): topics = OrderedDict() topics_ws = self._wb[self._cfg["topics_sheet"]] - for row in topics_ws: + for row in topics_ws.rows: if row[0].value: topic_name = row[0].value topics[topic_name] = [] diff --git a/rmtoo/tests/RMTTest-Output/RMTTest-Xls.py b/rmtoo/tests/RMTTest-Output/RMTTest-Xls.py index 721cbc98..37c6ff59 100644 --- a/rmtoo/tests/RMTTest-Output/RMTTest-Xls.py +++ b/rmtoo/tests/RMTTest-Output/RMTTest-Xls.py @@ -88,8 +88,7 @@ def rmttest_adding_req(self): xlsh.add_req(create_req(u'SW-102')) xlsh.write() - twb = openpyxl.load_workbook(filename=self._filename, - guess_types=True) + twb = openpyxl.load_workbook(filename=self._filename) rws = twb['Requirements'] assert rws['A2'].value == "SW-101" assert rws['G2'].value == "007" @@ -118,8 +117,7 @@ def rmttest_adding_topic(self): xlsh.add_topic(topic) xlsh.write() - twb = openpyxl.load_workbook(filename=self._filename, - guess_types=True) + twb = openpyxl.load_workbook(filename=self._filename) rws = twb['Topics'] assert rws['A1'].value == "SuperTopic" assert rws['B1'].value == "asdf" @@ -147,8 +145,7 @@ def rmttest_adding_req(self): xlsh.add_req(create_req(u'SW-102')) xlsh.write() - twb = openpyxl.load_workbook(filename=self._filename, - guess_types=True) + twb = openpyxl.load_workbook(filename=self._filename) rws = twb['Requirements'] assert rws['A3'].value == "SW-101" assert rws['G3'].value == "007" From aeaae8dba3f859a0bc051431cff6b25e3d8dfae9 Mon Sep 17 00:00:00 2001 From: Kristoffer Nordstroem Date: Thu, 7 May 2020 22:40:08 +0200 Subject: [PATCH 3/5] Regression test on empty child import import is not working if there's no entry for the master requirement --- rmtoo/imports/xls.py | 7 ++++-- .../tests/RMTTest-Import/RMTTest-XlsImport.py | 21 ++++++++++++++++++ .../regression-import_empty.xlsx | Bin 0 -> 8667 bytes 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 rmtoo/tests/RMTTest-Import/regression-import_empty.xlsx diff --git a/rmtoo/imports/xls.py b/rmtoo/imports/xls.py index d4f250e3..888ab0d6 100644 --- a/rmtoo/imports/xls.py +++ b/rmtoo/imports/xls.py @@ -127,8 +127,11 @@ def _verify_entries(entries): if entry["ID"] not in solved_entries: added_entries.append(entry["ID"]) if added_entries: - entries[0]["Solved by"] = (entries[0]["Solved by"].strip() + " " + - " ".join(added_entries)) + if entries[0]["Solved by"]: + entries[0]["Solved by"] = (entries[0]["Solved by"].strip() + + " " + " ".join(added_entries)) + else: + entries[0]["Solved by"] = " ".join(added_entries) return entries def _write_to_files(self, entries, topics): diff --git a/rmtoo/tests/RMTTest-Import/RMTTest-XlsImport.py b/rmtoo/tests/RMTTest-Import/RMTTest-XlsImport.py index f9e641b4..565e40d2 100644 --- a/rmtoo/tests/RMTTest-Import/RMTTest-XlsImport.py +++ b/rmtoo/tests/RMTTest-Import/RMTTest-XlsImport.py @@ -162,3 +162,24 @@ def rmttest_defcfg_import_topics(self, dest_dir): if i and i[0] == 'Name: GUI': found = True assert found + + +class RMTTestXlsImportRegressionTests: + """Regression tests for the xls import functionality""" + imp_fn = os.path.join(LDIR, 'test-reqs.xlsx') + config = {u'import_filename': imp_fn, + u'requirement_ws': u'Specification', + u'topics_sheet': u'Topics'} + + def rmttest_regression_test_import_empty(self, dest_dir): + imp_fn = os.path.join(LDIR, 'regression-import_empty.xlsx') + tmpdir = dest_dir['requirements_dirs'][0] + tmp_fn = os.path.join(tmpdir, 'regression-import_empty.xlsx') + config = dict(self.config) + config[u'import_filename'] = tmp_fn + distutils.file_util.copy_file(imp_fn, tmp_fn) + + importer = XlsImport(config, dest_dir) + assert importer.useable + importer.run() + assert importer._entries[0]['Solved by'] == 'SW-AS-100 SW-AS-101' diff --git a/rmtoo/tests/RMTTest-Import/regression-import_empty.xlsx b/rmtoo/tests/RMTTest-Import/regression-import_empty.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4778df72197e142e6a7bf15924a405572de8c44c GIT binary patch literal 8667 zcmbVx1yCGo(=P51oZzm(bs+?ISlk1_7Wc(1XmFPhoB+X{;O=foaF^gtaJ!K6-7h)$ z|8s8D?Wx(_shaMY?YH~sexFv9g@(a~KtMo%2+GV;g!oO6AAjqCjjbG5n4dmNV%z0A zSy2KHJwh2SxE6#EKIS&cNi~vllC)9T5LSm|cln>3i(z4+L{xT0+Q0R_H~h3HXT!M2 zN+bIwL`^FMs+q>fE3a$t-rkLcdpPTD4+cO|*;bSR9Hk)hV`xQqAjiWoS_WzB6ev3m zxoNZqf_Nnq1lCn^VZR3jZv<%Vg=5;As%K4m!!!i|tu481q)Ca~1vpkUDB{SS`()&@ zJ@Wufn5f2qNN$V9UiE`Zhw*6mD6CRIj70K{gFf7^wUOj^@q^nRK43M}Z-DI#f4~hl zVFs|8I(h&py`v{m`x`-i}|4{Zf_88aYc?F>DXmZz+m<>WA!V2J7~@RPZa3Ijt(|D$5%)?};}O zGxv&lABf}pkf7;4tM;o6DNm#w<5LyZ9ZqZGN)ug|ZjZ9n$Zz3OCR|gvF{Gm=X{-9|jRvg@uBXdb89mA( z1U{d28krTpD6Jhb0#pc}l$FG^qonAu^ml{`G2z6^Fd*{{4uWij?j0FI2OOUKd3MEt z2V1pb-TmOSpDBU%ivNze;lqod2zzv>=mEdZCX=tLe1Y z$Hw$yYARAxN^?~G(*lz&MnLS)Wy;m9n-z+|Jy?VN5G#fY$J1A06lxtKJv98p(06g6 z)W)Q*2xkM|#4iqZHjbcK=bo)>B0Z*-P$WK&^2ptnTSkq=vsIoar|XPE38VB?eKfWD zwL0!wU1`6nSs$5=D1WjOSvg+qk*gY=gu-*>itiS_J!$<_dEV^9Ovz-UfwE$`1-C#z z%m?zN8CO25rm5bbl*(JXIAge#TKdFOogPiCx^Xtl5^*5p2EG#vYa=Jjt8oWrjA*kFxUJdV)9+r&A96< zG$(DE3lk`pR!*>r%MHK#-SUU#B3!!OVw!x8J8FSbe|uh2DhQE zRGB83PVw@rOmSLbJXJteXTicKkM6rxoaW6=M9%G6iRSo7iE$wx0SVT&WGu0ijalx; z=J)r^f=Ud49Zo)ty!V?0ie3(S1){)P0Z@+yJ48~|=48Uq|2=p?G%M6z%JLF;-6miq ztDeLzn)`0h?fvkEfEpzK&)95CA!Gn#*wPY;nC55BfKG6ip@^1F9W26bw zSs30vxy-)j#7NO+M6%gFrrD@_xnnAmO-jpi?2Qd9x~ksvq%km@)Q0xR+j^5I4z4_f zJ&t8x)zi5u=?v%K_1j2Uj+84MhU}*z@y&)*6B=cf+aJC$M&V( zf3B_6q&<){$s_TodxcJeFf_N%CP4SB3!>wB;~WR{4HgW?n33m!8M+CaVfeznG}Niz zNgZ2-!2GL-HP&znsc|ZMb}6!jEINctGjtw*pOX)AOL<)Uvv%+1_29=oENYM|Dy+x^ zxFcCw^V=*8Ve}r12cBqVynLqma&q(JLo(Q?V=SDZ3pKgDoHmj?szBT&LINXG9`xqm z*w;k*c~8Zh5vvVcujKdAnXR6-Q%)3A_&zLdx7)b0IlKyn4h7 zCW?Q9Y&bl9Nlzri{een4SaM zp*Sm;$`~{I%MKGj;V}s)Y0|Ml`g){G4o)N0tS*<^8tGcEhZsb{V)kFATdx=jt0)&s zoJZYkFNIjE5#i7enE6VcW0meHthENd93Uy;>Y4Y05$CF$N=-B^ea$opkW3Jj)oDwh zHs)Qm8&J1VV-Ht$Q%SG)-8`Eois?|tADO1--2zMFUdA$W^D@`Tbl3#~;`K_TwPw~n zf-XmjsrvyYdge&REK2GIb?R9-=|=b2@l#i??$CVij9vI0cT;*h<01SNyA54=D@X6D zv?$Oqk679p3!SXgEE`fp(o#^=ZXDTNho$%^VFT$*Q}`~QEdb4&p)Gul_K_b9BvK$K zi?~z)owAEL>stnq98Jpr>m`@k75wO-j1%}|E=1)fv6S#IOrOX~FHdBgs6COsWMLU? zq)^0>${4Thxk(XyTXJuf$`VtV(<>-rvM+6xX>a3PEQQH8P+pBkV8hP#`xj-YkkENy zlgCV8;r4SO*vnEi6qdHvpo`UNPOu}-ZtkcTUF*$4P7&0Kt)3q*ekj-@WNny)ML&V%Gj%A=W&N*#pec*}8F9`$WNh58u9)*jAPOYSAy%*51NxcIzp5BKS_li4lk`~%sMQUR_W19iFKgab!H_`Gj^oX0mOSDIpS zJKwrw?oGveRV7M;DU69&t-CQO)x@BWF*fG~@fj|<)W{To8EbNRV1vePaT8L^GqH0}rL~zOZrc5EN;#bAcXj7IHK~MZXJ)kY}GP zw^`7H7P~=8#V3QVx;U2fAZ=AoK50Qlv4dqh5zbt%J56XPCMrks~iRUQUgBe~euB-wO^|JP==SQ zYVvuA{gQ!AHQ!M2k3J&O#V_=fqo0@QQkMOUCa-lXy2IndFZ_z&?DQ~RV~~xkg9aV0 z?O*N9X4q*D(0F7LQAGz?@-kxiOkZHJ*xXxLO4IY`fm3{!azD$c8gkQ)YTgN`a4_+H z)W%e|XKTE!=_duzyherJ)suMZH9Hh+C7aW1RGAfj5PHMKe4sy}k4{-`#v4U6Lv5T& z-eEwEug#y1w>`?WGTbySgKsf>jtdy1;l%SFmI>cP3btn8pD3E+kA1cW2*{|5lG{{&zSpzRz0z4@>V-BoXQP!AD=S5mde96j01 zw*cjlO+7#%5~CeknR0uLCM~VbXcQ;9OoV9;NWRz@?t3dOaLbtFLZ+oX+egWd zga7>o_WHj3-Yb&-6Wb^&Rx0BdmeN+|N$on_dNu>XI0^Q$g)dS`(d#7*b77fci7W~t zc`&(b@rwF^mzuO6$BknDEgeGc+Yoxm(vA{OS=5+KPHGp564{RG7Ziw{1Y1RZ>PSpj zVMjoBUgP9G77FNDUX#!^t#=n7*&i1J>AUd9s8IVAt#Y>;E5@-(Lac?ZvFpe`*qU^Q zZNwj2|A<>r}B z)nBr#H0xKeIR_D15iHqcZ?QBx^al3zK4lEqE$?g%1bkKEsD}{a#_I5^ki-79qF_FQI@z2 zor{DIDD_LJ&6u6>ry7RrTQ)>G2R7~t)yXX_>R)-vCiWWUP0MU2tnxivdlnA8w$x+6 z%>~7Gr%0@-%dZvSti0LvHZfRz7c^p`e`wi7%pmbo$28kwu=XV9c0w=l_Ooan&_i%o zj{A*UeW|NChqH>C1P(rk} zA=157uif=!jHw_};mfr>W6TRV3nUwBYMHgMgo-30!)l6M2j~4tr|Ts(Y!h2P^LK*c z%1TOxweVeb6s09^-bkDh1$I`$0_-h1Qy6d7>}FStDJqBDY^Ogha_KaLGSBN?v@h4( zjGA5{KcR=@%2deZWBc~%aaZ@xe8NApZyY~~1vQ|E92MGUpQ& z&7~Tav+d9}0)k3j^rK&%>GqQX%+ZF)v%M_$d*^Bm-OrTs&ER`vz9AB#1~NjWWktKE zl=lX8_aftJ^;Rzlv9+fTDpOSjNh{ ztb&S)Gx!>~eHtfB#qF=&w;z%mrTa`0kLKDtxN+mevb(BNKx=iBN-h#NwNmX_9}>+F zT_7`5D$mzcKwS!{ z#A*s4rn}~g|I$n6yr6Zrfv-;LBgVzp|etqmp;1YHfMTYy2KVkJ4s!Dm%vSI zzb^SJoZq&S*l2Lvc57taVk7Ci6*aZG@Yl}vtDG1&%V;zaN|Q8i zscUIAm6Rq(^*#XUvCjKlzF>NbC^LQ{1!=}7cZn~*z+@jZ1I)gI$xGIDrS6(qkoK>l zM^X}0rz%)XigfBCI2gGDft*{hs#>2?DMo&>8TK)iEaCnwm9YQAo|qYcjX^4oU~?PO zpCrmB^>%qew#PJjA>pz74Ft`=P+P5F+jTo&Bk>v$x9(?*Lge_+91V3fXIe zKKUThYjX4>bv!sNX;soqw=rsU21l=m$cG_M75ddtlk}hlVYL9v7={$mAMP2aGWdzU z=i}pKH@DROTSF>cr?u}C;AhH7hsu}UfQc+VUD*gw>sPf|0Z7gq2+mJouHg5y)##uW zuV&3KzyZQMxis>L2;N1Qs#;n^SjyP`2<}ym)4|br9Fu+!&`B~O(4>v9d>v5E-3l6& z@nseU@=dyZQE9hZsX)FPioJ_d49`vz=Aktvfp;-e6^7|qsQWXD;;s@d;Hu(0O~Dm@Iz^z2W=#wv4g0V+e90oF(K$#C6>x?yv>A!C~wM$a4h zFm*yXyp5-)^5=8X`Q^0QbBrtk&Yy*;RFL{{p*0@i)T2f$7CV1oxSS*f@sJ#J>EsF< zzQqbD7(^>mN}1V7bIEFcq^ue%4VKcU-re{dNnOf`u@FD=crX8jq*uL>CogX5K;9G0 zc^x{t_QZnPN!f#Rw9Rcg0FkC`zWlXUK%ICXqBCBDD=dz_ZkGu2yogna4)Y6A;TIP- zn=CR;9A~MmaLC(i9NM|1f|ic`{ZrUiYe2&6*K2S|RqUkr%^_6ghVT)c0OksApr>hJ7!7-lJZzKF}US8k5{ELoCq zwZM8!=TnwySOMDCBUo8cYK^ZqWIezG>*>|lQz@QX=%)-IbZ8B3d*lz%k^U_M2>!}| zzi9m5De9k;eskP^Dg8c|!N}nu8x7XE&&MC#h8gmA+rdAIffzP}UAoo(MX6@kt z&Aaf-fT#-|^J0;nu966bZ!Klf32D)p#U-k|6mJ3-p7<+PjhB9m4bh8owm@3?8BHQC zQ-Ms`l*zC8MivEe!YR!MitpX-$;=BO&ysSdIg!l1Vv{-d`aF!ETh9@Co^N=G;jw5+ zMfO7S(p&7XGF%>1nYl@f3ongO0b*Rr%{PlKBcxjsnwbGOj$jLGWLRR*c6xK(%zcXnyx(uRCwXZu;u|`5Fywmv9rQN8 zNFRPSSRyw#oC4ct9Y_vJ%G&hAkXO=YjDsyAK&0pM6F%KzSP4Cm=!~<$Hha+qGa^MW zpccwK$q-txLYSX~&->1Cy0tz(~d}-rw(oq$prsA@FM}ycz@-x|EJlV zLM}Q$Ad*G&q&_>v0>(I%RM1?!%)Cqhv=#MC|8`>g7xy@84r;Z(a20WVUhK zz$=Ul8YqGrLq=lNK{j4ss(OD5S=VG(Ip5H2O4hll_5z>G`%5T(qhqfgGTlMs$Qsn7 zi+V=6w`5p0YXe&L9Dk%a=SL8jTibR_eX<V~j_jPj<;3Ni${~j}(|y ze7cp&RbT5I6CS8Ja71MaKiW!PCqMXEDr~Og+q~!1c5_KygIuY4?qx50xsNPeN@^|R z3(ejJIaN1uf^o?2)^n;36A0X!Z3hS~Xq1=Adq+RFsVPdR+%S(GOnW^4xzWV>-GfF> z4vx0ge>rj3#{bEQUPzXQJ%Pd8WTNpx-~;kuV*EI=719|(ZPl}dT{>%rLu<*ZYiHB0 z4N(P~itVuD z3E{AAN)5=;pMj2WyG}t$*Bn)ytTGeYZW9A!g8*-n6M`{|oqIdG%><5nyqVW-H_WM# zVPQ?$w{3XrW<)ng0&`8~bE%u0b9ksgw}$NQLf4O;!)`T$$)Xht-jMdS-%*0M9pze_ z4jd2ZDNGq@FG$9wiUN?&qy6sQgso-gToXP;8oa}(?&hNx%>SQ$dM#>e<7jN-sHf^? zXY8Q+Zgq{NJ7s`BGTp7D;h_p=elfD6GXYsI2yN6lWWuysV%0w5FsiO zQVyzy#G!as6MJn}P0bcRY=#J$KGZ|cO@RNdLdrWv*XZjvVwo||+7@Q_Is@sh)B-fOd~A9drB{qhmfdK!IKpR3tvHZ!b$l}(jM*$ zNYY)l`Shrz=l%MghSl~4D%yaSYlY@uc_10oZuh;?G#%Vd+#IU?h5d4`MOFQ&b~EB-!-dm85c z657W{{W048Q~7sF{%KhEO9CG2&7<B9SI;U7AgKkfWpL!L_eFWG Date: Sat, 23 May 2020 16:32:22 +0200 Subject: [PATCH 4/5] Add excel artifacts to template project --- contrib/template_project/Config.json | 55 ++++++++++-------- .../import/specification-template.xlsx | Bin 0 -> 8090 bytes .../template_project/requirements/project.req | 2 +- 3 files changed, 32 insertions(+), 25 deletions(-) create mode 100755 contrib/template_project/import/specification-template.xlsx diff --git a/contrib/template_project/Config.json b/contrib/template_project/Config.json index 21d7f307..8aca3697 100644 --- a/contrib/template_project/Config.json +++ b/contrib/template_project/Config.json @@ -3,29 +3,29 @@ "analytics": { "stop_on_errors": false } - }, + }, "requirements": { "input": { "commit_interval": { - "begin": "FILES", + "begin": "FILES", "end": "FILES" - }, - "default_language": "en_GB", + }, + "default_language": "en_GB", "dependency_notation": [ "Solved by" - ], + ], "directory": "requirements" - }, + }, "inventors": [ "flonatel" - ], + ], "stakeholders": [ - "development", - "management", - "users", + "development", + "management", + "users", "customers" ] - }, + }, "topics": { "ts_common": { "sources": [ @@ -36,54 +36,61 @@ "topic_root_node": "ReqsDocument", "constraints_dirs": [ "constraints" ], "testcases_dirs": [ "testcases" ] } - ] + ] ], "output": { "graph": [ { "output_filename": "artifacts/req-graph1.dot" } - ], + ], "graph2": [ { "output_filename": "artifacts/req-graph2.dot" } - ], + ], "html": [ { - "footer": "html/footer.html", - "header": "html/header.html", + "footer": "html/footer.html", + "header": "html/header.html", "output_directory": "artifacts/html" } - ], + ], "LatexJinja2": [ { "template_path": "latex/LatexJinja2", "output_filename": "artifacts/reqtopics.tex" } - ], + ], + "xls": [ + { + "output_filename": "artifacts/specification.xlsx", + "template_filename": "import/specification-template.xlsx", + "req_sheet": "Specification" + } + ], "prios": [ { "output_filename": "artifacts/reqsprios.tex" } - ], + ], "stats_burndown1": [ { - "output_filename": "artifacts/stats_burndown.csv", + "output_filename": "artifacts/stats_burndown.csv", "start_date": "2011-05-01" } - ], + ], "stats_reqs_cnt": [ { "output_filename": "artifacts/stats_reqs_cnt.csv" } - ], + ], "stats_sprint_burndown1": [ { - "output_filename": "artifacts/stats_sprint_burndown.csv", + "output_filename": "artifacts/stats_sprint_burndown.csv", "start_date": "2011-05-01" } - ], + ], "version1": [ { "output_filename": "artifacts/reqs-version.txt" diff --git a/contrib/template_project/import/specification-template.xlsx b/contrib/template_project/import/specification-template.xlsx new file mode 100755 index 0000000000000000000000000000000000000000..904d9b7c9f8c0614eb6316006be0257b7ba2cfb1 GIT binary patch literal 8090 zcmbVxby$>b^EQn%EV(pDqjU+<-QB$`0uoEgBHbX((ntw{gmiZ|2!cvTr_w3S4}3!( zzxR3m_-2pIV`uLBm^kN}In@*qkqF=(Ja_>21UWz%?l-}>`)ulLY3IVucK`Z5zC)#p z12bgL^A*bv-Z`-cIr&XWa!oWmlhez`q z42mY<+WO%LEesak1>Hk8P9E%hBX4|q@dRWwAd)Q3F{%p3!%O0Wd7iGZ3h0|h%8KLg z8y4GOIAO6+XJc)5t{ZUJdXWBhB*4i^=gpKaQge{9y)CbUJQev%5$Z$zDTTW>=i;#lbR3XA3^J-`vUriD9L2w#R+U}hbMiJoP)c)2@cz6E*LoC#s zA&xHW77%AkHmJQ_u!gZ}CkKJ|58X%JRK}<*JQj4%q4C{8z$i#flbG99TCLai!YsCq zy1ZA@kJl3}ZD}LeZiG8xWaItJw1G&{)E=7X4ViB?K$J1+;72g}Vp25C$pG=HE0&d4 zhcaC-2Lfx>cBxT0(!oyz_9fZ`7G3;m+}w`n-}<$4ZJ!wzebM8*8TjlsLD$g1tEa;? zRL&o%?I^`K6pyM{^GVxE%^8z$lKOoISZu%Ub3o}x4=ROAeo3%Bp;5vqSdA+3LFcuP z=oF4n;a1k-m!6eMGG3yOWv>tit27@b*qRXy_5;T(8ZteA^P&VU2?c1tzMyWfQJ!R) z%BJ;U(vs<3%Mz!9>n?H8jO3a&{BZyP^NWPO`%v>kUpTh16eGyovP-gO*Ll(sn_**Ht9I4%*~KO1j8{~{kft{@C&uYJm#B`O z>R)*8R4 z&MJ}9R}tE-;R8@MUsW#xOmHPQ$?*k{pA(IW9m^U~ee}&49Ll<0Hgrc%?bAo)i=>6O zI@5R|5?;YgBrvj5W%h$6Qsz~HXlK?iN5>HaDSxfGnU%QCa3$gP)wQRxXXB$%nG3IO z)czpbqM&^6SdbqPFX8i=60=WmHUtzQMe)@(LF6Q@*cNAp+BaDD{xi&|2}8K^U)tUC z@3S5McmG+qxwt~?|LsDDzZqucKF_Tt%#i6w|R@6wD=0AbWiRMTVzS|@W`O!h8& z_OewMFRTVfLNohIyhz>`QY#DKTITJ@x|A;NK(lSB;e{sLVZ22F(#TVjs%yn{Djww8 z)Ny7K$iz~*97LV$!x<(`F4+a=aN{<<0&ydA+na%KGez zy|V7`)L4LmnApSZ9MQBt+i9Ei%&scmo;y>$;saUQp{wkX_Kn1Ono9-#2+hK7n_vBG zB^*eQ0tU$tn)7_s4~!USK|QXKtBe;05BQYz9H?tt|MU~~leO!OJ1^M$qn}9b{sFaP zcZWFJx>#FUy1KCceB-=#R*RmoOD-RQ_g+vdXnmWk*aAM(?DI4GTXW`O~eLDiUkSPwTZI( zM%gLKY0-sMB7E4iQX8i?&rN>Sq?WkjPP?v&HyTTwen(pi2 zdsc;L^*2<5g=EnA?h!m@JI$rE+bq&Y%JPwWdd)*lG|G6<%<*ET;5>p;P(EI(6Dz$I z=q8w6Oh{VGOpkS(NYwniWpe8<)jLqEWt;7p!^u7<30F+5NRPb0<+8WUM4CmC@k)AM zyt?#pTtycEy2}J`dqQzFEMG~B9P)0piRFw@=quL++;hMEo!~Z3D`h{JJqo6-K%%mI zIG<|Dvfgsu_1Bk-%zjT>*2!ej5zth|qZOp#7fH%iJk6fgVbHS%$Cqf~)}?6FD9^sq z-hDWk6=}Z}RdK~ELm<_7Xi#T*GQlWLzmDEgnL&iyI}oVN%S_g5q9vz&q7`vWnNUx! zp{eau{upUon9pdJamVxM9sSMtj$ci_VUgoyqrKD7_IH$;(;jZ zC3Z|a1^Z|~*cDY@<@(qx!?ua}Jc-&O22Qak$|(k9eV1^EWu?w1WMQO!#|mjKQV7QR z+%mEe_~Sa#iQi&c7Oe@GbqRzy^h%0a%#1*{t)%867cPOibCIGb1VVUjys^$&pMw3Z z^JxV#LW_EcNyGZYxG_%hYCR%FQu;P2C|maWf;}P^mhgNyV)iL0VL0oY9-)25fgVbC zzj<@SY$k-x3QjAkpf|A9%vN&kvzy_^_*i)!3l`w*8;uNn5b&@FmO3VSM85uBm+(wY ze)%TAHgcA@yAH{(@P$++Hx?^QYr!vStSkcyM(y7^Ts@$Hma`{oGIcejGnhhd;!KuX z&)SibkRlq#Z)bAei(z901F+>CJ~eeem(i_^Dl9Rn^>Lf6$DY-&{9?qdFqH!xb*u<6 zmJAf|SGJ`Rs#ENxmqof73wd+4mM`Z6mf{onq#*Fzqm$(8_~W5O8lu6^gt)V4%}*_L zaa3||jpY}~wS)?TMP)!bfKPI!;pS3C&s$1@@|bxFVo4YK3$e>!t3JN3`+6ej!z&s& z6yMv{^AIZ6H#h`#*dN7&^t9bW_K6`WSQ-fquH>$({XJy=4HR+xgd$yK$gBWv%U&6- ztLau(?Q5jkh|l^EV!16)hJ%00IISgKj@)DUX=R^lYkZBuKCc<$zT;s;*1%T>U^Ax1B7bybjqzup7oQT*DyPQoqGQ5cYb|zU^nDxB&y^PM z8K;B1G(#8{M1ys#t)jSAmQ1u1am+Qy6PJaAG6Mv?YS#~s2CE)_`IxySdw`5E;#6*e za?-U$pYLa;bwD?ub91@rP{je{TcT1JEppi@GQ|~@yh^K2=^PGh>m(7W^T^|py)o5CkS=!~m~Pw-IAi7LquDU4A&Si?>LQktiAaWOd> zZVU-J3K7d>J%SLKSj|W@s^FRwWgkC0Ly}R^Wl&&n0`Wi%`|Cp@`=}v| z6Pr6U1-z@(!I1|2vpsbB&B9=U-Xl^szK@$DbhX+3WJMXr!XK?$5|+2+701Mz&g-m` zKYbr}oEDwp#Spc|z2I|Pf2+lZNXbX#RNzLy9tbnnEAS$b_sFPl0?PAS%;Y1D>Cc*Y z&7F#LOYT-tdL#qfG}l*F3v?MoDdJUMt`5lE_i?Z$RjjFnY zge_y6_nFCo9tbfnISuTDPaIR)f2FmO(wKqU%9>*|sS~d1KYQ$H9B6;YLBdJ}f!-chSAHn-BHkJ_$Fr;QjW zbvwRKwr=6?@q*aTI_f(j92_&&{|8=h-{VD;wzB3d2X4y`U8pPM^%x2heyF*GrXUQE z@6R=n4tj5aRrxt_HE4gUDPA;(e9%8`-k?bpvcOU`v>_qw$)WQ`WeUvm3~!Slc(N%- z_$#|;W>vpnDYEW68gtq5ZRZ>F7h*?X7dT>d1bh4>AzhR13S}3jdJ1#2G+JOlm1Zc zXU0#YwfXu$5k*7P)GLY3nTVexCzd_myyn=E)=t1zG_`^npd`{^W)!Hys%0vCWR`Df($xOVbDlfTSH`<)!hqnt~ zWFKByjYZaPwL8wecm_0*w0Cf&o`$qXD0Js{r*%90h)U2Kst*umx)3BF2jziAM18c) zL>>;xk=aNlbYt^A$VyK(X^!J0W(fD*_?>n^|&pBD*3l4 z%P;GF9IIejddMr$vQ`tlLpvd96s6VDvYkPel+QYRIWm++`zCf{q=9cdMG4%yj1pIK z0mCw_(5!fa8jHYNE}N30(dPHB?EAaFcR&odY-%(3^;RcJmCfk(v-e!aY#Vvq2IU%E zp|${>H%=jn&O15>$KMnp`ivTPABP7m1&%8ZbYVQY%C)@Sx9McT7^>S&``~$9rkGl; zdc*L3UpOAb?ESEdXOfmJ&?GKaUuI;SjbQUh&Avfc+2wrT5fELyO%7bxk(T7vOS=2H z3sk-1gSqbevwSv~D{#*5r%RN7DxbUBxZ6v-48olovyOOcW%>9$cN5 zg6C0bZCTC`qB4>DA3h}nuJs_aG#DF?#OksRa}OgiVpKEozHGg+F)0>)SjHd3q$<03 zf?%IeeQ z2=u$0O?(eU%<>;bDIIBX&7D_eMD$o5f&3@|KL zc&F{7VM>fNrgG0czu=I(271|3C^&>4%;g%8L^0;>lfC==dbnM^7lqyebI2n&kv6{U zIO>bZ!Y->Zf)Q~!)Apq2q&c=LrDBvV; z7hAAycE!fw2%d-=GNY;fpjdzDm-;mnxSEFL_=W8&E3Kw53I!dd*tX~Q+?1UrcW*w) zx`+B7=LbO<^|wO;CsSQJ{v6_Ftqw5<;vUo}xV=+znZT(m>J)#<6&Kk+4sKkFZ3k6A z3Qj^_RQSo#t_^vpIf3{qhH#iT!Z#vmaX9I~dLU9lANaz`peq+kxBuD=F6x^%A;Jkk zsnolI^3T01pGU}@wkhqIVon;4#A+NY2OgV=V3dEa3*SzEm~ z?>cNR)jvT5&adSF2m-PF*;qAaqh~m9z4vsbd_s-t32@D&5ywj>(}@}{w*~N*m#5gx+6Bdr|De%n|J=?eAi1GkcNTb2d{;40L$}>B9!nriEpnAzJAYuJjdeUr}ujw zM1|ZjRmPN@C!O-`wnI(216V>Jq(yCy@kU%NVq1B@R+8u_00U}`>``4RI)QG(sKv3& zZes2G$wX%QMy())0v3Rd$W*P>$3E?DMntGi6PB!rvBX32g4tJ$DIyh$MahSXLms{E z-aIYXZTR-QDn98I)`UV>ik^4<*AS`4R=2P0(q!yyo;7w&JWOs7>_w7wZ8e~pJB-_? z?S_v=g_*m*!0^(YNm1&wk$S3Fb(ycu2}IV@akMXK>dsNqIz)n+R!NHAB+~L%?QY3` z8B@lJT;VX`ph8MK`7A=2?UL|;VukTF^@C0n zVwKa}id|368nybHp{W2XQUg{YUHrXl4<(;wD%lkP{A%nn{%?_1NW9tkc&)m2B=bm6IZcsps)IL=iPaR5b?h@oZw!CP3pV; zb8(koe?RUK{JZ~Hn>$;AHC>%;9ISq}p~M7bMLO}j{r$}=wI3Ojs5(-Ovw_?J)NBG( ziyex}ZZmA}*jc}wn;o>JQ@S3|tyN5@zMXHDf7>boMHLzbTUbw?+m*rvhG$^0M_n{E zT)P$e>4mB^k(8^;(z(rLev-m;=26QxWw74;rAsq$J+ex)HZH}2Oz zuN`rI_2zH}1(_k*pR6WUvDeI0w)yC11(hycT~h~R}_3Y1=K zpFv2BWfGNU4&(UF;DpFB>*HKtOBl1#!jXwpQ2Hh;5^6RTrd_ULZ3h_{;AE<9udk4d z1iOiK{>4eg~XWv4lMDOSTxKa^Gy1&FRq+5hv0(z6)SbpGa zh4&V;70Do!E1{Q5DLC<)!{l>jk&?H39ogFM?aH=`ri0VAu@Nz(@ATf$mZB4f<)$+* z4!NiDC_8ykDQ2{^ZnVBldkpx%L=iSifU$DE!l$R2JB0{B@z_0Z-i*q1nE$0kj zWS-U|=d}{aR!sjq@y_CTAxu2IWmhf9v8Be~Jg)dV+xZMW5g8_^JIYnb+yM zw-hnEo?LVv{1fKq-#J*$cB}Fb4=cSrlrL3PJ!``bViw&K7F!iKQLCCcm7bthHF6F+ zW3S&7F;}FOdqgGBiee49ULjV!Tb-7Dr+V)wXJ)-eOLuM%{l}uXUt1eWLL6Kz9b8Sd zJRB`ujPFr*Bys5Z-R58V$2oegeMMrPnnDFknret|%+*-S#;&-6QcfZ5%3JmYb55QI zu4EJ7bhu?>lw{SbQAfEFVdL#MGSE^o-fvw-&X{o3@+Q^<_6;2SfuG1Lpk;C>NxRIr<-twrSD2tVZ=>vUKO(+2*h;cvQk?U z{`rsSQIGXl62Q2`kHuaOt}9YS3X&~MlcSmPA9vLUm%#@V>~IPS`PX5mSdVDIyf3p- zEUr|Yjc(MjsrazYGM6&G;gLbPpN;biWu}5OsM4CBVRySYLc4yn`q}RxK4~O_c#;V( z_yoSL^?g6Ucpt07fK`y~T~e?9JV3)E5WxLn9)Bm>?wQBG%5OZRn&Mv-f9HMfxwv1# zcz3D)hnf3R{dd~qo*eompYEXczv};{ivHC8oea3ov0p-Xx0m~`_AjF3PXoWN@a`9N zzeM=1^8b!O_|wYogZ*Ev^rQVxD?gWie_Hu{zWJ+_v%8_@f85K@mB61?em?@;&qBY1 z|1La#nT-Ba{k_56pOJov0pWjd;6D)er}FQJdEWznNgeUOuKQ<4{L{kkP;!r~za$s< p)5722?4NdiPxHSf#qgck{{cg4ipX~r0SAY9|G2|3DEa-j{{w@k$;to# literal 0 HcmV?d00001 diff --git a/contrib/template_project/requirements/project.req b/contrib/template_project/requirements/project.req index 98af3f51..0f06b6bb 100644 --- a/contrib/template_project/requirements/project.req +++ b/contrib/template_project/requirements/project.req @@ -1,5 +1,5 @@ Name: project -Type: master requirement +Type: requirement Invented on: 2010-10-09 Invented by: flonatel Owner: development From f44ba6cdd325034f1a3bf9e65f39f31fce0d86c6 Mon Sep 17 00:00:00 2001 From: Kristoffer Nordstroem Date: Sun, 10 Aug 2025 10:08:23 +0200 Subject: [PATCH 5/5] Add dependencies for XLS to setup.py --- requirements.txt | 2 ++ setup.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index e1ded690..756ee280 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,8 @@ gitpython>=3.1.41 jinja2>=2.10 numpy odfpy==1.3.4 +six>=1.17 +openpyxl>=2.5.6 pylint pytest pytest-cov diff --git a/setup.py b/setup.py index 71b8f108..82188947 100644 --- a/setup.py +++ b/setup.py @@ -131,7 +131,9 @@ def adjust(input_filename, output): "stevedore>=1.21", "pylint>=1.7.1", "odfpy==1.3.4", - "jinja2>=2.10"], + "jinja2>=2.10", + "six>=1.17", + "openpyxl>=2.5.6"], license="GPL-3.0-or-later", platforms="all", @@ -185,7 +187,11 @@ def adjust(input_filename, output): "tlp1 = rmtoo.outputs.tlp1:Tlp1", "version1 = rmtoo.outputs.version1:version1", "xml1 = rmtoo.outputs.xml1:Xml1", - "xml_ganttproject_2 = rmtoo.outputs.xml_ganttproject_2:xml_ganttproject_2" + "xml_ganttproject_2 = rmtoo.outputs.xml_ganttproject_2:xml_ganttproject_2", + "xls = rmtoo.outputs.xls:Xls" + ], + "rmtoo.imports.plugin" : [ + "xls = rmtoo.imports.xls:XlsImport" ], "rmtoo.output.markup" : [ "latex = rmtoo.lib.Markup:LaTeX",