Enhancement - #363 export.ical

- Added feature #363 supporting iCalendar export via the 'export.ical'
  command.
- Updated documentation.
- Removed unnecessary localization of canonical command names.
This commit is contained in:
Paul Beckingham 2010-05-31 16:05:51 -04:00
parent 3ef6aa9f8e
commit e368043fb8
15 changed files with 288 additions and 90 deletions

View file

@ -8,6 +8,8 @@
by using the 'log' command in place of 'add' (thanks to Cory Donnelly).
+ Added features #36 and #37, providing annual versions of the 'history'
and 'ghistory' command as 'history.annual' and 'ghistory.annual'.
+ Added feature #363 supporting iCalendar export via the 'export.ical'
command.
+ Fixed bug #406 so that task now includes command aliases in the _commands
helper command used by shell completion scripts.

1
NEWS
View file

@ -18,6 +18,7 @@ New Features in task 1.9
- New 'log' command to add tasks that are already completed.
- New annual history and ghistory command variations.
- Alias support in shell completion scripts.
- New iCalendar export format.
- Task is now part of Debian
Please refer to the ChangeLog file for full details. There are too many to

View file

@ -133,8 +133,14 @@ Shows task database statistics.
Imports tasks from a variety of formats.
.TP
.B export \fIfile
Exports all tasks as a CSV file.
.B export
Exports all tasks in CSV format. This command is an alias to the export.csv command.
Redirect the output to a file, if you wish to save it, or pipe it to another command.
.TP
.B export.ical
Exports all tasks in iCalendar format.
Redirect the output to a file, if you wish to save it, or pipe it to another command.
.TP
.B color [sample]

View file

@ -273,7 +273,8 @@ way.
.B alias.rm=delete
Task supports command aliases. This alias provides an alternate name (rm) for
the delete command. You can use aliases to provide alternate names for any of
task's commands.
task's commands. Several commands you may use are actually aliases - 'history',
for example, or 'export'.
.SS DATES

View file

@ -40,10 +40,9 @@
208 done
209 duplicate
210 edit
211 export
212 help
213 history.monthly
214 ghistory.monthly
215 import
216 info
217 prepend
@ -60,8 +59,6 @@
228 version
229 shell
230 config
231 history.annual
232 ghistory.annual
# 3xx Attributes - must be sequential
300 project

View file

@ -105,12 +105,21 @@ void Cmd::load ()
{
if (commands.size () == 0)
{
// Commands whose names are not localized.
commands.push_back ("_projects");
commands.push_back ("_tags");
commands.push_back ("_commands");
commands.push_back ("_ids");
commands.push_back ("_config");
commands.push_back ("_version");
commands.push_back ("export.csv");
commands.push_back ("export.ical");
commands.push_back ("history.monthly");
commands.push_back ("history.annual");
commands.push_back ("ghistory.monthly");
commands.push_back ("ghistory.annual");
// Commands whose names are localized.
commands.push_back (context.stringtable.get (CMD_ADD, "add"));
commands.push_back (context.stringtable.get (CMD_APPEND, "append"));
commands.push_back (context.stringtable.get (CMD_ANNOTATE, "annotate"));
@ -121,12 +130,7 @@ void Cmd::load ()
commands.push_back (context.stringtable.get (CMD_DONE, "done"));
commands.push_back (context.stringtable.get (CMD_DUPLICATE, "duplicate"));
commands.push_back (context.stringtable.get (CMD_EDIT, "edit"));
commands.push_back (context.stringtable.get (CMD_EXPORT, "export"));
commands.push_back (context.stringtable.get (CMD_HELP, "help"));
commands.push_back (context.stringtable.get (CMD_HISTORY_MONTHLY, "history.monthly"));
commands.push_back (context.stringtable.get (CMD_HISTORY_ANNUAL, "history.annual"));
commands.push_back (context.stringtable.get (CMD_GHISTORY_MONTHLY, "ghistory.monthly"));
commands.push_back (context.stringtable.get (CMD_GHISTORY_ANNUAL, "ghistory.annual"));
commands.push_back (context.stringtable.get (CMD_IMPORT, "import"));
commands.push_back (context.stringtable.get (CMD_INFO, "info"));
commands.push_back (context.stringtable.get (CMD_LOG, "log"));
@ -198,15 +202,16 @@ bool Cmd::isReadOnlyCommand ()
command == "_ids" ||
command == "_config" ||
command == "_version" ||
command == "export.csv" ||
command == "export.ical" ||
command == "history.monthly" ||
command == "history.annual" ||
command == "ghistory.monthly" ||
command == "ghistory.annual" ||
command == context.stringtable.get (CMD_CALENDAR, "calendar") ||
command == context.stringtable.get (CMD_COLORS, "colors") ||
command == context.stringtable.get (CMD_CONFIG, "config") ||
command == context.stringtable.get (CMD_EXPORT, "export") ||
command == context.stringtable.get (CMD_HELP, "help") ||
command == context.stringtable.get (CMD_HISTORY_MONTHLY, "history.monthly") ||
command == context.stringtable.get (CMD_HISTORY_ANNUAL, "history.annual") ||
command == context.stringtable.get (CMD_GHISTORY_MONTHLY, "ghistory.monthly") ||
command == context.stringtable.get (CMD_GHISTORY_ANNUAL, "ghistory.annual") ||
command == context.stringtable.get (CMD_INFO, "info") ||
command == context.stringtable.get (CMD_PROJECTS, "projects") ||
command == context.stringtable.get (CMD_SHELL, "shell") ||

View file

@ -159,10 +159,14 @@ std::string Config::defaults =
"#import.synonym.tags=?\n"
"#import.synonym.uuid=?\n"
"\n"
"# Export Controls\n"
"export.ical.class=PRIVATE # Could be PUBLIC, PRIVATE or CONFIDENTIAL\n"
"\n"
"# Aliases - alternate names for commands\n"
"alias.rm=delete # Alias for the delete command\n"
"alias.history=history.monthly # Prefer monthly history reports\n"
"alias.ghistory=ghistory.monthly # Prefer monthly graphical history reports\n"
"alias.history=history.monthly # Prefer monthly over annual history reports\n"
"alias.ghistory=ghistory.monthly # Prefer monthly graphical over annual history reports\n"
"alias.export=export.csv # Prefer CSV over iCal export\n"
"\n"
"# Fields: id,uuid,project,priority,priority_long,entry,entry_time,\n" // TODO
"# start,entry_time,due,recur,recurrence_indicator,age,\n" // TODO

View file

@ -229,7 +229,8 @@ int Context::dispatch (std::string &out)
else if (cmd.command == "delete") { rc = handleDelete (out); }
else if (cmd.command == "start") { rc = handleStart (out); }
else if (cmd.command == "stop") { rc = handleStop (out); }
else if (cmd.command == "export") { rc = handleExport (out); }
else if (cmd.command == "export.csv") { rc = handleExportCSV (out); }
else if (cmd.command == "export.ical") { rc = handleExportiCal (out); }
else if (cmd.command == "import") { rc = handleImport (out); }
else if (cmd.command == "duplicate") { rc = handleDuplicate (out); }
else if (cmd.command == "edit") { rc = handleEdit (out); }

View file

@ -5,12 +5,12 @@ task_SOURCES = API.cpp Att.cpp Cmd.cpp Color.cpp Config.cpp Context.cpp \
Grid.cpp Hooks.cpp Keymap.cpp Location.cpp Nibbler.cpp \
Path.cpp Permission.cpp Record.cpp Sequence.cpp \
StringTable.cpp Subst.cpp TDB.cpp Table.cpp Task.cpp Timer.cpp \
command.cpp custom.cpp edit.cpp import.cpp interactive.cpp \
main.cpp recur.cpp report.cpp rules.cpp text.cpp util.cpp \
API.h Att.h Cmd.h Color.h Config.h Context.h Date.h \
Directory.h Duration.h File.h Filter.h Grid.h Hooks.h Keymap.h \
Location.h Nibbler.h Path.h Permission.h Record.h Sequence.h \
StringTable.h Subst.h TDB.h Table.h Task.h Timer.h i18n.h \
main.h text.h util.h
command.cpp custom.cpp edit.cpp export.cpp import.cpp \
interactive.cpp main.cpp recur.cpp report.cpp rules.cpp \
text.cpp util.cpp API.h Att.h Cmd.h Color.h Config.h Context.h \
Date.h Directory.h Duration.h File.h Filter.h Grid.h Hooks.h \
Keymap.h Location.h Nibbler.h Path.h Permission.h Record.h \
Sequence.h StringTable.h Subst.h TDB.h Table.h Task.h Timer.h \
i18n.h main.h text.h util.h
task_CPPFLAGS=$(LUA_CFLAGS)
task_LDFLAGS=$(LUA_LFLAGS)

View file

@ -1306,59 +1306,6 @@ int handleDone (std::string &outs)
return rc;
}
////////////////////////////////////////////////////////////////////////////////
int handleExport (std::string &outs)
{
int rc = 0;
if (context.hooks.trigger ("pre-export-command"))
{
std::stringstream out;
// Deliberately no 'id'.
out << "'uuid',"
<< "'status',"
<< "'tags',"
<< "'entry',"
<< "'start',"
<< "'due',"
<< "'recur',"
<< "'end',"
<< "'project',"
<< "'priority',"
<< "'fg',"
<< "'bg',"
<< "'description'"
<< "\n";
int count = 0;
// Get all the tasks.
std::vector <Task> tasks;
context.tdb.lock (context.config.getBoolean ("locking"));
handleRecurrence ();
context.tdb.load (tasks, context.filter);
context.tdb.commit ();
context.tdb.unlock ();
foreach (task, tasks)
{
context.hooks.trigger ("pre-display", *task);
if (task->getStatus () != Task::recurring)
{
out << task->composeCSV ().c_str ();
++count;
}
}
outs = out.str ();
context.hooks.trigger ("post-export-command");
}
return rc;
}
////////////////////////////////////////////////////////////////////////////////
int handleModify (std::string &outs)
{

229
src/export.cpp Normal file
View file

@ -0,0 +1,229 @@
////////////////////////////////////////////////////////////////////////////////
// task - a command line task list manager.
//
// Copyright 2006 - 2010, Paul Beckingham, Federico Hernandez.
// All rights reserved.
//
// This program is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software
// Foundation; either version 2 of the License, or (at your option) any later
// version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public License along with
// this program; if not, write to the
//
// Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor,
// Boston, MA
// 02110-1301
// USA
//
////////////////////////////////////////////////////////////////////////////////
#include <iostream>
#include <sstream>
#include "Att.h"
#include "text.h"
#include "util.h"
#include "main.h"
#include "../auto.h"
extern Context context;
////////////////////////////////////////////////////////////////////////////////
int handleExportCSV (std::string &outs)
{
int rc = 0;
if (context.hooks.trigger ("pre-export-command"))
{
std::stringstream out;
// Deliberately no 'id'.
out << "'uuid',"
<< "'status',"
<< "'tags',"
<< "'entry',"
<< "'start',"
<< "'due',"
<< "'recur',"
<< "'end',"
<< "'project',"
<< "'priority',"
<< "'fg',"
<< "'bg',"
<< "'description'"
<< "\n";
int count = 0;
// Get all the tasks.
std::vector <Task> tasks;
context.tdb.lock (context.config.getBoolean ("locking"));
handleRecurrence ();
context.tdb.load (tasks, context.filter);
context.tdb.commit ();
context.tdb.unlock ();
foreach (task, tasks)
{
context.hooks.trigger ("pre-display", *task);
if (task->getStatus () != Task::recurring)
{
out << task->composeCSV ().c_str ();
++count;
}
}
outs = out.str ();
context.hooks.trigger ("post-export-command");
}
return rc;
}
////////////////////////////////////////////////////////////////////////////////
// http://tools.ietf.org/html/rfc5545
//
// Note: Recurring tasks could be included in more detail.
int handleExportiCal (std::string &outs)
{
int rc = 0;
if (context.hooks.trigger ("pre-export-command"))
{
std::stringstream out;
out << "BEGIN:VCALENDAR\n"
<< "VERSION:2.0\n"
<< "PRODID:-//GBF//" << PACKAGE_STRING << "//EN\n";
int count = 0;
// Get all the tasks.
std::vector <Task> tasks;
context.tdb.lock (context.config.getBoolean ("locking"));
handleRecurrence ();
context.tdb.load (tasks, context.filter);
context.tdb.commit ();
context.tdb.unlock ();
foreach (task, tasks)
{
context.hooks.trigger ("pre-display", *task);
if (task->getStatus () != Task::recurring)
{
out << "BEGIN:VTODO\n";
// Required UID:20070313T123432Z-456553@example.com
out << "UID:" << task->get ("uuid") << "\n";
// Required DTSTAMP:20070313T123432Z
Date entry (atoi (task->get ("entry").c_str ()));
out << "DTSTAMP:" << entry.toISO () << "\n";
// Optional DTSTART:20070514T110000Z
if (task->has ("start"))
{
Date start (atoi (task->get ("start").c_str ()));
out << "DTSTART:" << start.toISO () << "\n";
}
// Optional DUE:20070709T130000Z
if (task->has ("due"))
{
Date due (atoi (task->get ("due").c_str ()));
out << "DUE:" << due.toISO () << "\n";
}
// Optional COMPLETED:20070707T100000Z
if (task->has ("end") && task->getStatus () == Task::completed)
{
Date end (atoi (task->get ("end").c_str ()));
out << "COMPLETED:" << end.toISO () << "\n";
}
out << "SUMMARY:" << task->get ("description") << "\n";
// Optional CLASS:PUBLIC/PRIVATE/CONFIDENTIAL
std::string classification = context.config.get ("export.ical.class");
if (classification == "")
classification = "PRIVATE";
out << "CLASS:" << classification << "\n";
// Optional multiple CATEGORIES:FAMILY,FINANCE
if (task->getTagCount () > 0)
{
std::vector <std::string> tags;
task->getTags (tags);
std::string all;
join (all, ",", tags);
out << "CATEGORIES:" << all << "\n";
}
// Optional PRIORITY:
// 1-4 H
// 5 M
// 6-9 L
if (task->has ("priority"))
{
out << "PRIORITY:";
std::string priority = task->get ("priority");
if (priority == "H") out << "1";
else if (priority == "M") out << "5";
else out << "9";
out << "\n";
}
// Optional STATUS:NEEDS-ACTION/IN-PROCESS/COMPLETED/CANCELLED
out << "STATUS:";
Task::status stat = task->getStatus ();
if (stat == Task::pending || stat == Task::waiting)
{
if (task->has ("start"))
out << "IN-PROCESS";
else
out << "NEEDS-ACTION";
}
else if (stat == Task::completed)
{
out << "COMPLETED";
}
else if (stat == Task::deleted)
{
out << "CANCELLED";
}
out << "\n";
// Optional COMMENT:annotation1
// Optional COMMENT:annotation2
std::vector <Att> annotations;
task->getAnnotations (annotations);
foreach (anno, annotations)
out << "COMMENT:" << anno->value () << "\n";
out << "END:VTODO\n";
++count;
}
}
out << "END:VCALENDAR\n";
outs = out.str ();
context.hooks.trigger ("post-export-command");
}
return rc;
}
////////////////////////////////////////////////////////////////////////////////

View file

@ -77,10 +77,9 @@
#define CMD_DONE 208
#define CMD_DUPLICATE 209
#define CMD_EDIT 210
#define CMD_EXPORT 211
#define CMD_HELP 212
#define CMD_HISTORY_MONTHLY 213
#define CMD_GHISTORY_MONTHLY 214
#define CMD_IMPORT 215
#define CMD_INFO 216
#define CMD_PREPEND 217
@ -97,8 +96,6 @@
#define CMD_VERSION 228
#define CMD_SHELL 229
#define CMD_CONFIG 230
#define CMD_HISTORY_ANNUAL 231
#define CMD_GHISTORY_ANNUAL 232
// 3xx Attributes
#define ATT_PROJECT 300

View file

@ -59,7 +59,6 @@ int handleAdd (std::string &);
int handleLog (std::string &);
int handleAppend (std::string &);
int handlePrepend (std::string &);
int handleExport (std::string &);
int handleDone (std::string &);
int handleModify (std::string &);
int handleProjects (std::string &);
@ -127,6 +126,10 @@ std::string colorizeDebug (const std::string&);
// import.cpp
int handleImport (std::string&);
// export.cpp
int handleExportCSV (std::string &);
int handleExportiCal (std::string &);
// list template
///////////////////////////////////////////////////////////////////////////////
template <class T> bool listDiff (const T& left, const T& right)

View file

@ -192,7 +192,11 @@ int shortUsage (std::string &outs)
row = table.addRow ();
table.addCell (row, 1, "task export");
table.addCell (row, 2, "Lists all tasks in CSV format.");
table.addCell (row, 2, "Lists all tasks in CSV format. Alias to export.csv");
row = table.addRow ();
table.addCell (row, 1, "task export.ical");
table.addCell (row, 2, "Lists all tasks in iCalendar format.");
row = table.addRow ();
table.addCell (row, 1, "task color [sample]");

View file

@ -9,8 +9,9 @@ OBJECTS = ../t-TDB.o ../t-Task.o ../t-text.o ../t-Date.o ../t-Table.o \
../t-Nibbler.o ../t-Location.o ../t-Filter.o ../t-Context.o \
../t-Keymap.o ../t-command.o ../t-interactive.o ../t-report.o \
../t-Grid.o ../t-Color.o ../t-rules.o ../t-recur.o ../t-custom.o \
../t-import.o ../t-edit.o ../t-Timer.o ../t-Permission.o ../t-Path.o \
../t-File.o ../t-Directory.o ../t-Hooks.o ../t-API.o
../t-export.o ../t-import.o ../t-edit.o ../t-Timer.o \
../t-Permission.o ../t-Path.o ../t-File.o ../t-Directory.o \
../t-Hooks.o ../t-API.o
all: $(PROJECT)