mirror of
https://github.com/GothenburgBitFactory/timewarrior.git
synced 2025-07-07 20:06:39 +02:00
#131: Introduce 'modify' to adjust historical records to fixed date/time.
This change introduces a new command that, like lengthen, move, resize, and shorten, is intended to move and/or resize a record, but instead of taking an interval, will take an absolute date/time. This command is useful because it removes the need for the user to calculate the time intervals to shorten / lengthen a record by. For example, if the user accidentally forgot to stop tracking an interval before starting a new one, but new they stopped working at a specific time, it is easy to simply modify the end time of the interval that they had forgotten to stop.
This commit is contained in:
parent
0a766bb86f
commit
ceca4c817e
6 changed files with 300 additions and 3 deletions
|
@ -21,6 +21,7 @@ set (commands_SRCS CmdAnnotate.cpp
|
|||
CmdHelp.cpp
|
||||
CmdJoin.cpp
|
||||
CmdLengthen.cpp
|
||||
CmdModify.cpp
|
||||
CmdMove.cpp
|
||||
CmdReport.cpp
|
||||
CmdResize.cpp
|
||||
|
|
|
@ -52,6 +52,7 @@ int CmdHelpUsage (const Extensions& extensions)
|
|||
<< " timew help [<command> | interval | hints | date | duration]\n"
|
||||
<< " timew join @<id> @<id>\n"
|
||||
<< " timew lengthen @<id> [@<id> ...] <duration>\n"
|
||||
<< " timew modify (start|end) @<id> <date>\n"
|
||||
<< " timew month [<interval>] [<tag> ...]\n"
|
||||
<< " timew move @<id> <date>\n"
|
||||
<< " timew [report] <report> [<interval>] [<tag> ...]\n"
|
||||
|
@ -690,7 +691,27 @@ int CmdHelp (
|
|||
<< '\n'
|
||||
<< " $ timew lengthen @2 @10 @23 1hour\n"
|
||||
<< '\n'
|
||||
<< "See also 'summary', 'tag', 'untag', 'shorten'.\n"
|
||||
<< "See also 'summary', 'tag', 'untag', 'shorten', 'modify'.\n"
|
||||
<< '\n';
|
||||
|
||||
// Ruler 1 2 3 4 5 6 7 8
|
||||
// 12345678901234567890123456789012345678901234567890123456789012345678901234567890
|
||||
else if (words[0] == "modify")
|
||||
std::cout << '\n'
|
||||
<< "Syntax: timew modify (start|end) @<id> <date>\n"
|
||||
<< '\n'
|
||||
<< "The 'modify' command is used to change the start or end date of a closed\n"
|
||||
<< "interval. Using the 'summary' command, and specifying the ':ids' hint shows\n"
|
||||
<< "interval IDs. Using the right ID, you can identify an interval to modify. For\n"
|
||||
<< "example, show the IDs:\n"
|
||||
<< '\n'
|
||||
<< " $ timew summary :week :ids\n"
|
||||
<< '\n'
|
||||
<< "Then having selected '@2' as the interval you wish to modify:\n"
|
||||
<< '\n'
|
||||
<< " $ timew modify end @2 2018-10-10T17:10:00\n"
|
||||
<< '\n'
|
||||
<< "See also 'summary', 'lengthen', 'shorten', 'move'.\n"
|
||||
<< '\n';
|
||||
|
||||
// Ruler 1 2 3 4 5 6 7 8
|
||||
|
@ -736,7 +757,7 @@ int CmdHelp (
|
|||
<< '\n'
|
||||
<< " $ timew move @2 9am\n"
|
||||
<< '\n'
|
||||
<< "See also 'summary', 'tag', 'untag', 'lengthen', 'shorten'.\n"
|
||||
<< "See also 'summary', 'tag', 'untag', 'lengthen', 'shorten', 'modify'.\n"
|
||||
<< '\n';
|
||||
|
||||
// Ruler 1 2 3 4 5 6 7 8
|
||||
|
@ -776,7 +797,7 @@ int CmdHelp (
|
|||
<< '\n'
|
||||
<< " $ timew shorten @2 @10 @23 1hour\n"
|
||||
<< '\n'
|
||||
<< "See also 'summary', 'tag', 'untag', 'lengthen'.\n"
|
||||
<< "See also 'summary', 'tag', 'untag', 'lengthen', 'modify'.\n"
|
||||
<< '\n';
|
||||
|
||||
// Ruler 1 2 3 4 5 6 7 8
|
||||
|
|
98
src/commands/CmdModify.cpp
Normal file
98
src/commands/CmdModify.cpp
Normal file
|
@ -0,0 +1,98 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright 2015 - 2018, Thomas Lauf, Paul Beckingham, Federico Hernandez.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
// https://www.opensource.org/licenses/mit-license.php
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <format.h>
|
||||
#include <commands.h>
|
||||
#include <timew.h>
|
||||
#include <iostream>
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int CmdModify (
|
||||
const CLI& cli,
|
||||
Rules& rules,
|
||||
Database& database)
|
||||
{
|
||||
auto filter = getFilter (cli);
|
||||
std::set <int> ids = cli.getIds ();
|
||||
std::vector <std::string> words = cli.getWords ();
|
||||
enum { MODIFY_START, MODIFY_END } op = MODIFY_START;
|
||||
bool verbose = rules.getBoolean ("verbose");
|
||||
|
||||
if (words.empty())
|
||||
throw std::string ("Must specify start|end command to modify. See 'timew help modify'.");
|
||||
|
||||
if (words.at (0) == "start")
|
||||
op = MODIFY_START;
|
||||
else if (words.at (0) == "end")
|
||||
op = MODIFY_END;
|
||||
else
|
||||
throw format ("Must specify start|end command to modify. See 'timew help modify'.", words.at (0));
|
||||
|
||||
if (ids.empty ())
|
||||
throw std::string ("ID must be specified. See 'timew help modify'.");
|
||||
|
||||
if (ids.size () > 1)
|
||||
throw std::string ("Only one ID may be specified. See 'timew help modify'.");
|
||||
|
||||
Interval empty_filter = Interval();
|
||||
auto tracked = getTracked (database, rules, empty_filter);
|
||||
|
||||
int id = *ids.begin();
|
||||
if (id > static_cast <int> (tracked.size ()))
|
||||
throw format ("ID '@{1}' does not correspond to any tracking.", id);
|
||||
|
||||
Interval interval = tracked.at (tracked.size () - id);
|
||||
if (filter.start.toEpoch () == 0)
|
||||
throw std::string ("No updated time specified. See 'timew help modify'.");
|
||||
|
||||
switch (op)
|
||||
{
|
||||
case MODIFY_START:
|
||||
interval.start = filter.start;
|
||||
break;
|
||||
|
||||
case MODIFY_END:
|
||||
if (interval.is_open ())
|
||||
throw format ("Cannot modify end of open interval @{1}.", id);
|
||||
interval.end = filter.start;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!interval.is_open () && (interval.start > interval.end))
|
||||
throw format ("Cannot modify interval @{1} where start is after end.", id);
|
||||
|
||||
database.startTransaction ();
|
||||
|
||||
database.deleteInterval (tracked[tracked.size() - id]);
|
||||
validate(cli, rules, database, interval);
|
||||
database.addInterval(interval, verbose);
|
||||
|
||||
database.endTransaction();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
|
@ -48,6 +48,7 @@ int CmdHelpUsage ( const Extensions&);
|
|||
int CmdHelp (const CLI&, const Extensions&);
|
||||
int CmdJoin (const CLI&, Rules&, Database& );
|
||||
int CmdLengthen (const CLI&, Rules&, Database& );
|
||||
int CmdModify (const CLI&, Rules&, Database& );
|
||||
int CmdMove (const CLI&, Rules&, Database& );
|
||||
int CmdReport (const CLI&, Rules&, Database&, const Extensions&);
|
||||
int CmdResize (const CLI&, Rules&, Database& );
|
||||
|
|
|
@ -64,6 +64,7 @@ void initializeEntities (CLI& cli)
|
|||
cli.entity ("command", "--help");
|
||||
cli.entity ("command", "join");
|
||||
cli.entity ("command", "lengthen");
|
||||
cli.entity ("command", "modify");
|
||||
cli.entity ("command", "move");
|
||||
cli.entity ("command", "report");
|
||||
cli.entity ("command", "resize");
|
||||
|
@ -275,6 +276,7 @@ int dispatchCommand (
|
|||
command == "--help") status = CmdHelp (cli, extensions);
|
||||
else if (command == "join") status = CmdJoin (cli, rules, database );
|
||||
else if (command == "lengthen") status = CmdLengthen (cli, rules, database );
|
||||
else if (command == "modify") status = CmdModify (cli, rules, database );
|
||||
else if (command == "month") status = CmdChartMonth (cli, rules, database );
|
||||
else if (command == "move") status = CmdMove (cli, rules, database );
|
||||
else if (command == "report") status = CmdReport (cli, rules, database, extensions);
|
||||
|
|
174
test/modify.t
Executable file
174
test/modify.t
Executable file
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/env python2.7
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
#
|
||||
# Copyright 2006 - 2018, Thomas Lauf, Paul Beckingham, Federico Hernandez.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included
|
||||
# in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
# https://www.opensource.org/licenses/mit-license.php
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Ensure python finds the local simpletap module
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from basetest import Timew, TestCase
|
||||
|
||||
class TestModify(TestCase):
|
||||
def setUp(self):
|
||||
"""Executed before each test in the class"""
|
||||
self.t = Timew()
|
||||
|
||||
def test_modify_end_of_open_interval(self):
|
||||
"""Attempt to modify end of an open interval"""
|
||||
now_utc = datetime.now().utcnow()
|
||||
one_hour_before_utc = now_utc - timedelta(hours=1)
|
||||
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(one_hour_before_utc))
|
||||
code, out, err = self.t.runError("modify end @1 {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc))
|
||||
self.assertIn("Cannot modify end of open interval", err)
|
||||
|
||||
def test_modify_start_of_open_interval(self):
|
||||
"""Modify start of open interval"""
|
||||
now_utc = datetime.now().utcnow()
|
||||
one_hour_before_utc = now_utc - timedelta(hours=1)
|
||||
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc))
|
||||
code, out, err = self.t("modify start @1 {:%Y-%m-%dT%H:%M:%S}Z".format(one_hour_before_utc))
|
||||
|
||||
j = self.t.export()
|
||||
self.assertEquals(len(j), 1)
|
||||
self.assertOpenInterval(j[0],
|
||||
expectedStart="{:%Y%m%dT%H%M%S}Z".format(one_hour_before_utc))
|
||||
|
||||
def test_modify_invalid_subcommand(self):
|
||||
"""Modify without (start|stop) subcommand"""
|
||||
now_utc = datetime.now().utcnow()
|
||||
one_hour_before_utc = now_utc - timedelta(hours=1)
|
||||
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(one_hour_before_utc))
|
||||
self.t("stop")
|
||||
code, out, err = self.t.runError("modify @1 {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc))
|
||||
self.assertIn("Must specify start|end command to modify", err)
|
||||
|
||||
def test_modify_no_end_time(self):
|
||||
"""Modify without a time to stop at"""
|
||||
now_utc = datetime.now().utcnow()
|
||||
one_hour_before_utc = now_utc - timedelta(hours=1)
|
||||
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(one_hour_before_utc))
|
||||
self.t("stop")
|
||||
code, out, err = self.t.runError("modify end @1")
|
||||
self.assertIn("No updated time", err)
|
||||
|
||||
def test_modify_shorten_one_hour(self):
|
||||
"""Shorten the interval by one hour."""
|
||||
now_utc = datetime.now().utcnow()
|
||||
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=3)))
|
||||
self.t("stop {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=1)))
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(minutes=49)))
|
||||
self.t("stop")
|
||||
|
||||
code, out, err = self.t("modify end @2 {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=2)))
|
||||
|
||||
j = self.t.export()
|
||||
self.assertEquals(len(j), 2)
|
||||
self.assertClosedInterval(j[0],
|
||||
expectedStart="{:%Y%m%dT%H%M%S}Z".format(now_utc - timedelta(hours=3)),
|
||||
expectedEnd="{:%Y%m%dT%H%M%S}Z".format(now_utc - timedelta(hours=2)))
|
||||
|
||||
def test_modify_shorten_before_start(self):
|
||||
"""Modify should not move end before start."""
|
||||
now_utc = datetime.now().utcnow()
|
||||
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=3)))
|
||||
self.t("stop {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=1)))
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(minutes=49)))
|
||||
self.t("stop")
|
||||
|
||||
code, out, err = self.t.runError("modify end @2 {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=4)))
|
||||
self.assertIn("Cannot modify interval", err);
|
||||
|
||||
def test_modify_start_to_after_end(self):
|
||||
"""Modify should not move start beyond end."""
|
||||
now_utc = datetime.now().utcnow()
|
||||
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=3)))
|
||||
self.t("stop {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=1)))
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(minutes=49)))
|
||||
self.t("stop")
|
||||
|
||||
code, out, err = self.t.runError("modify start @2 {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(minutes=59)))
|
||||
self.assertIn("Cannot modify interval", err);
|
||||
|
||||
def test_modify_start_within_interval(self):
|
||||
"""Increase start time within interval."""
|
||||
now_utc = datetime.now().utcnow()
|
||||
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=3)))
|
||||
self.t("stop {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=1)))
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(minutes=49)))
|
||||
self.t("stop")
|
||||
|
||||
code, out, err = self.t("modify start @2 {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=2)))
|
||||
|
||||
j = self.t.export()
|
||||
self.assertEquals(len(j), 2)
|
||||
self.assertClosedInterval(j[0],
|
||||
expectedStart="{:%Y%m%dT%H%M%S}Z".format(now_utc - timedelta(hours=2)),
|
||||
expectedEnd="{:%Y%m%dT%H%M%S}Z".format(now_utc - timedelta(hours=1)))
|
||||
|
||||
def test_modify_move_stop_to_overlap_following_interval(self):
|
||||
"""Move end time to overlap with following interval."""
|
||||
now_utc = datetime.now().utcnow()
|
||||
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=3)))
|
||||
self.t("stop {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=1)))
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(minutes=49)))
|
||||
self.t("stop")
|
||||
|
||||
code, out, err = self.t.runError("modify end @2 {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(minutes=30)))
|
||||
self.assertIn("You cannot overlap intervals", err)
|
||||
|
||||
def test_modify_move_start_to_overlap_preceeding_interval(self):
|
||||
"""Move start time to overlap with preceeding interval."""
|
||||
now_utc = datetime.now().utcnow()
|
||||
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=3)))
|
||||
self.t("stop {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=1)))
|
||||
self.t("start {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(minutes=49)))
|
||||
self.t("stop")
|
||||
|
||||
code, out, err = self.t.runError("modify start @1 {:%Y-%m-%dT%H:%M:%S}Z".format(now_utc - timedelta(hours=2)))
|
||||
self.assertIn("You cannot overlap intervals", err)
|
||||
|
||||
if __name__ == "__main__":
|
||||
from simpletap import TAPTestRunner
|
||||
|
||||
unittest.main(testRunner=TAPTestRunner())
|
||||
|
||||
# vim: ai sts=4 et sw=4 ft=python
|
Loading…
Add table
Add a link
Reference in a new issue