From 9ce366ddab2f26c7ffc119529ca534438b57c6cf Mon Sep 17 00:00:00 2001 From: Dheepak Krishnamurthy Date: Tue, 17 Aug 2021 23:47:00 -0600 Subject: [PATCH] task export [] --- src/commands/CmdCustom.cpp | 4 +- src/commands/CmdExport.cpp | 76 +++++++++++++++++++++++++++++-- src/commands/CmdExport.h | 2 + test/tw-2575.t | 91 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 test/tw-2575.t diff --git a/src/commands/CmdCustom.cpp b/src/commands/CmdCustom.cpp index 6fb338110..49a2418e7 100644 --- a/src/commands/CmdCustom.cpp +++ b/src/commands/CmdCustom.cpp @@ -102,9 +102,11 @@ int CmdCustom::execute (std::string& output) if (reportFilter != "") Context::getContext ().cli2.addFilter (reportFilter); - // Apply filter. + // Make sure reccurent tasks are generated. handleUntil (); handleRecurrence (); + + // Apply filter. Filter filter; std::vector filtered; filter.subset (filtered); diff --git a/src/commands/CmdExport.cpp b/src/commands/CmdExport.cpp index 99c512030..4ec50dbee 100644 --- a/src/commands/CmdExport.cpp +++ b/src/commands/CmdExport.cpp @@ -28,13 +28,16 @@ #include #include #include +#include +#include +#include #include //////////////////////////////////////////////////////////////////////////////// CmdExport::CmdExport () { _keyword = "export"; - _usage = "task export"; + _usage = "task export []"; _description = "Exports tasks in JSON format"; _read_only = true; _displays_id = true; @@ -42,7 +45,7 @@ CmdExport::CmdExport () _uses_context = false; _accepts_filter = true; _accepts_modifications = false; - _accepts_miscellaneous = false; + _accepts_miscellaneous = true; _category = Command::Category::migration; } @@ -51,6 +54,40 @@ int CmdExport::execute (std::string& output) { int rc = 0; + auto words = Context::getContext ().cli2.getWords (); + std::string selectedReport = ""; + + if (words.size () == 1) + { + // Find the report matching the prompt + for (auto& command : Context::getContext ().commands) + { + if (command.second->category () == Command::Category::report && + closeEnough(command.second->keyword (), words[0])) + { + selectedReport = command.second->keyword (); + break; + } + } + + if (selectedReport.empty ()) { + Context::getContext ().error("Unable to find report that matches '" + words[0] + "'."); + } + } + + auto reportSort = Context::getContext ().config.get ("report." + selectedReport + ".sort"); + auto reportFilter = Context::getContext ().config.get ("report." + selectedReport + ".filter"); + + auto sortOrder = split (reportSort, ','); + if (sortOrder.size () != 0 && + sortOrder[0] != "none") { + validateSortColumns (sortOrder); + } + + // Add the report filter to any existing filter. + if (reportFilter != "") + Context::getContext ().cli2.addFilter (reportFilter); + // Make sure reccurent tasks are generated. handleUntil (); handleRecurrence (); @@ -60,6 +97,32 @@ int CmdExport::execute (std::string& output) std::vector filtered; filter.subset (filtered); + std::vector sequence; + if (sortOrder.size () && + sortOrder[0] == "none") + { + // Assemble a sequence vector that represents the tasks listed in + // Context::getContext ().cli2._uuid_ranges, in the order in which they appear. This + // equates to no sorting, just a specified order. + sortOrder.clear (); + for (auto& i : Context::getContext ().cli2._uuid_list) + for (unsigned int t = 0; t < filtered.size (); ++t) + if (filtered[t].get ("uuid") == i) + sequence.push_back (t); + } + else + { + // There is a sortOrder, so sorting will take place, which means the initial + // order of sequence is ascending. + for (unsigned int i = 0; i < filtered.size (); ++i) + sequence.push_back (i); + + // Sort the tasks. + if (sortOrder.size ()) { + sort_tasks (filtered, sequence, reportSort); + } + } + // Export == render. Timer timer; @@ -77,8 +140,9 @@ int CmdExport::execute (std::string& output) output += "[\n"; int counter = 0; - for (auto& task : filtered) + for (auto& t : sequence) { + auto task = filtered[t]; if (counter) { if (json_array) @@ -104,3 +168,9 @@ int CmdExport::execute (std::string& output) } //////////////////////////////////////////////////////////////////////////////// + +void CmdExport::validateSortColumns (std::vector & columns) +{ + for (auto& col : columns) + legacySortColumnMap (col); +} diff --git a/src/commands/CmdExport.h b/src/commands/CmdExport.h index dbbcea9b0..a1277a5f2 100644 --- a/src/commands/CmdExport.h +++ b/src/commands/CmdExport.h @@ -35,6 +35,8 @@ class CmdExport : public Command public: CmdExport (); int execute (std::string&); +private: + void validateSortColumns (std::vector &); }; #endif diff --git a/test/tw-2575.t b/test/tw-2575.t new file mode 100644 index 000000000..246cf45d7 --- /dev/null +++ b/test/tw-2575.t @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import os +import unittest +import re +import json + +# Ensure python finds the local simpletap module +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from basetest import Task, TestCase + + +class TestExport(TestCase): + def setUp(self): + self.t = Task() + + self.t("add one") + self.t("add two project:strange") + self.t("add task1 +home project:A") + self.t("add task2 +work project:A") + + self.t.config("report.foo.description", "DESC") + self.t.config("report.foo.labels", "ID,DESCRIPTION") + self.t.config("report.foo.columns", "id,description") + self.t.config("report.foo.sort", "urgency+") + self.t.config("report.foo.filter", "project:A") + self.t.config("urgency.user.tag.home.coefficient", "15") + + def assertTaskEqual(self, t1, t2): + keys = [k for k in sorted(t1.keys()) if k not in ["entry", "modified", "uuid"]] + for k in keys: + self.assertEqual(t1[k], t2[k]) + + def test_exports(self): + """Verify exports work""" + code, out, err = self.t("export") + out = json.loads(out) + + self.assertEqual(len(out), 4) + + self.assertTaskEqual(out[0], {"id": 1, "description": "one", "status": "pending", "urgency": 0}) + self.assertTaskEqual(out[1], {"id": 2, "description": "two", "project": "strange", "status": "pending", "urgency": 1}) + self.assertTaskEqual(out[2], {"id": 3, "description": "task1", "status": "pending", "project": "A", "tags": ["home"], "urgency": 16.8}) + self.assertTaskEqual(out[3], {"id": 4, "description": "task2", "status": "pending", "project": "A", "tags": ["work"], "urgency": 1.8}) + + def test_exports_filter(self): + """Verify exports with filter work""" + code, out, err = self.t("one export") + out = json.loads(out) + + self.assertEqual(len(out), 1) + + self.assertTaskEqual(out[0], {"id": 1, "description": "one", "status": "pending", "urgency": 0}) + + def test_exports_with_limits_and_filter(self): + """Verify exports with limits and filter work""" + code, out, err = self.t("task limit:4 export") + out = json.loads(out) + + self.assertEqual(len(out), 2) + + self.assertTaskEqual(out[0], {"id": 3, "description": "task1", "status": "pending", "project": "A", "tags": ["home"], "urgency": 16.8}) + self.assertTaskEqual(out[1], {"id": 4, "description": "task2", "status": "pending", "project": "A", "tags": ["work"], "urgency": 1.8}) + + code, out, err = self.t("task limit:1 export") + out = json.loads(out) + + self.assertEqual(len(out), 1) + + self.assertTaskEqual(out[0], {"id": 3, "description": "task1", "status": "pending", "project": "A", "tags": ["home"], "urgency": 16.8}) + + def test_exports_report(self): + """Verify exports with report work""" + code, out, err = self.t("export foo") + out = json.loads(out) + + self.assertEqual(len(out), 2) + + self.assertTaskEqual(out[0], {"id": 4, "description": "task2", "status": "pending", "project": "A", "tags": ["work"], "urgency": 1.8}) + self.assertTaskEqual(out[1], {"id": 3, "description": "task1", "status": "pending", "project": "A", "tags": ["home"], "urgency": 16.8}) + + +if __name__ == "__main__": + from simpletap import TAPTestRunner + + unittest.main(testRunner=TAPTestRunner()) + +# vim: ai sts=4 et sw=4 ft=python syntax=python