Permalink
Browse files

#131: Introduce 'modify' to adjust historical records to fixed date/t…

…ime.

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.
  • Loading branch information...
sruffell authored and lauft committed Nov 13, 2018
1 parent 0a766bb commit ceca4c817e88b69318d5535ce119071cfc996910
Showing with 300 additions and 3 deletions.
  1. +1 −0 src/commands/CMakeLists.txt
  2. +24 −3 src/commands/CmdHelp.cpp
  3. +98 −0 src/commands/CmdModify.cpp
  4. +1 −0 src/commands/commands.h
  5. +2 −0 src/init.cpp
  6. +174 −0 test/modify.t
@@ -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
@@ -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);
@@ -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

0 comments on commit ceca4c8

Please sign in to comment.