You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdepim/karm/karmstorage.cpp

1242 lines
34 KiB

/*
* This file only:
* Copyright (C) 2003, 2004 Mark Bucciarelli <mark@hubcapconsulting.com>
*
* 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 <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cassert>
#include <tqfile.h>
#include <tqsize.h>
#include <tqdict.h>
#include <tqdatetime.h>
#include <tqstring.h>
#include <tqstringlist.h>
#include "incidence.h"
#include "tdeapplication.h" // kapp
#include <kdebug.h>
#include <tdeemailsettings.h>
#include <tdelocale.h> // i18n
#include <tdemessagebox.h>
#include <kprogress.h>
#include <tdetempfile.h>
#include <resourcecalendar.h>
#include <resourcelocal.h>
#include <resourceremote.h>
#include <kpimprefs.h>
#include <taskview.h>
#include <timekard.h>
#include <karmutility.h>
#include <tdeio/netaccess.h>
#include <kurl.h>
#include <vector>
//#include <calendarlocal.h>
//#include <journal.h>
//#include <event.h>
//#include <todo.h>
#include "karmstorage.h"
#include "preferences.h"
#include "task.h"
#include "reportcriteria.h"
using namespace std;
KarmStorage *KarmStorage::_instance = 0;
static long linenr; // how many lines written by printTaskHistory so far
KarmStorage *KarmStorage::instance()
{
if (_instance == 0) _instance = new KarmStorage();
return _instance;
}
KarmStorage::KarmStorage()
{
_calendar = 0;
}
TQString KarmStorage::load (TaskView* view, const Preferences* preferences, TQString fileName )
// loads data from filename into view. If no filename is given, filename from preferences is used.
// filename might be of use if this program is run as embedded konqueror plugin.
{
// When I tried raising an exception from this method, the compiler
// complained that exceptions are not allowed. Not sure how apps
// typically handle error conditions in KDE, but I'll return the error
// as a string (empty is no error). -- Mark, Aug 8, 2003
// Use KDE_CXXFLAGS=$(USE_EXCEPTIONS) in Makefile.am if you want to use
// exceptions (David Faure)
TQString err;
KEMailSettings settings;
if ( fileName.isEmpty() ) fileName = preferences->iCalFile();
// If same file, don't reload
if ( fileName == _icalfile ) return err;
// If file doesn't exist, create a blank one to avoid ResourceLocal load
// error. We make it user and group read/write, others read. This is
// masked by the users umask. (See man creat)
if ( ! remoteResource( _icalfile ) )
{
int handle;
handle = open (
TQFile::encodeName( fileName ),
O_CREAT|O_EXCL|O_WRONLY,
S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH
);
if (handle != -1) close(handle);
}
if ( _calendar)
closeStorage(view);
// Create local file resource and add to resources
_icalfile = fileName;
KCal::ResourceCached *resource;
if ( remoteResource( _icalfile ) )
{
KURL url( _icalfile );
resource = new KCal::ResourceRemote( url, url ); // same url for upload and download
}
else
{
resource = new KCal::ResourceLocal( _icalfile );
}
_calendar = resource;
TQObject::connect (_calendar, TQ_SIGNAL(resourceChanged(ResourceCalendar *)),
view, TQ_SLOT(iCalFileModified(ResourceCalendar *)));
_calendar->setTimeZoneId( KPimPrefs::timezone() );
_calendar->setResourceName( TQString::fromLatin1("KArm") );
_calendar->open();
_calendar->load();
// Claim ownership of iCalendar file if no one else has.
KCal::Person owner = resource->getOwner();
if ( owner.isEmpty() )
{
resource->setOwner( KCal::Person(
settings.getSetting( KEMailSettings::RealName ),
settings.getSetting( KEMailSettings::EmailAddress ) ) );
}
// Build task view from iCal data
if (!err)
{
KCal::Todo::List todoList;
KCal::Todo::List::ConstIterator todo;
TQDict< Task > map;
// Build dictionary to look up Task object from Todo uid. Each task is a
// TQListViewItem, and is initially added with the view as the parent.
todoList = _calendar->rawTodos();
kdDebug(5970) << "KarmStorage::load "
<< "rawTodo count (includes completed todos) ="
<< todoList.count() << endl;
for( todo = todoList.begin(); todo != todoList.end(); ++todo )
{
// Initially, if a task was complete, it was removed from the view.
// However, this increased the complexity of reporting on task history.
//
// For example, if a task is complete yet has time logged to it during
// the date range specified on the history report, we have to figure out
// how that task fits into the task hierarchy. Currently, this
// structure is held in memory by the structure in the list view.
//
// I considered creating a second tree that held the full structure of
// all complete and incomplete tasks. But this seemed to much of a
// change with an impending beta release and a full todo list.
//
// Hence this "solution". Include completed tasks, but mark them as
// inactive in the view.
//
//if ((*todo)->isCompleted()) continue;
Task* task = new Task(*todo, view);
map.insert( (*todo)->uid(), task );
view->setRootIsDecorated(true);
task->setPixmapProgress();
}
// Load each task under it's parent task.
for( todo = todoList.begin(); todo != todoList.end(); ++todo )
{
Task* task = map.find( (*todo)->uid() );
// No relatedTo incident just means this is a top-level task.
if ( (*todo)->relatedTo() )
{
Task* newParent = map.find( (*todo)->relatedToUid() );
// Complete the loading but return a message
if ( !newParent )
err = i18n("Error loading \"%1\": could not find parent (uid=%2)")
.arg(task->name())
.arg((*todo)->relatedToUid());
if (!err) task->move( newParent);
}
}
kdDebug(5970) << "KarmStorage::load - loaded " << view->count()
<< " tasks from " << _icalfile << endl;
}
return err;
}
TQString KarmStorage::icalfile()
{
kdDebug(5970) << "Entering KarmStorage::icalfile" << endl;
return _icalfile;
}
TQString KarmStorage::buildTaskView(KCal::ResourceCalendar *rc, TaskView *view)
// makes *view contain the tasks out of *rc.
{
TQString err;
KCal::Todo::List todoList;
KCal::Todo::List::ConstIterator todo;
TQDict< Task > map;
vector<TQString> runningTasks;
vector<TQDateTime> startTimes;
// remember tasks that are running and their start times
for ( int i=0; i<view->count(); i++)
{
if ( view->item_at_index(i)->isRunning() )
{
runningTasks.push_back( view->item_at_index(i)->uid() );
startTimes.push_back( view->item_at_index(i)->lastStart() );
}
}
//view->stopAllTimers();
// delete old tasks
while (view->item_at_index(0)) view->item_at_index(0)->cut();
// 1. insert tasks form rc into taskview
// 1.1. Build dictionary to look up Task object from Todo uid. Each task is a
// TQListViewItem, and is initially added with the view as the parent.
todoList = rc->rawTodos();
for( todo = todoList.begin(); todo != todoList.end(); ++todo )
{
Task* task = new Task(*todo, view);
map.insert( (*todo)->uid(), task );
view->setRootIsDecorated(true);
task->setPixmapProgress();
}
// 1.1. Load each task under it's parent task.
for( todo = todoList.begin(); todo != todoList.end(); ++todo )
{
Task* task = map.find( (*todo)->uid() );
// No relatedTo incident just means this is a top-level task.
if ( (*todo)->relatedTo() )
{
Task* newParent = map.find( (*todo)->relatedToUid() );
// Complete the loading but return a message
if ( !newParent )
err = i18n("Error loading \"%1\": could not find parent (uid=%2)")
.arg(task->name())
.arg((*todo)->relatedToUid());
if (!err) task->move( newParent);
}
}
view->clearActiveTasks();
// restart tasks that have been running with their start times
for ( int i=0; i<view->count(); i++)
{
for ( unsigned int n=0; n<runningTasks.size(); n++)
{
if ( runningTasks[n] == view->item_at_index(i)->uid() )
{
view->startTimerFor( view->item_at_index(i), startTimes[n] );
}
}
}
view->refresh();
return err;
}
void KarmStorage::closeStorage(TaskView* view)
{
if ( _calendar )
{
_calendar->close();
delete _calendar;
_calendar = 0;
view->clear();
}
}
TQString KarmStorage::save(TaskView* taskview)
{
kdDebug(5970) << "entering KarmStorage::save" << endl;
TQString err=TQString();
TQPtrStack< KCal::Todo > parents;
for (Task* task=taskview->first_child(); task; task = task->nextSibling())
{
err=writeTaskAsTodo(task, 1, parents );
}
if ( !saveCalendar() )
{
err="Could not save";
}
if ( err.isEmpty() )
{
kdDebug(5970)
<< "KarmStorage::save : wrote "
<< taskview->count() << " tasks to " << _icalfile << endl;
}
else
{
kdWarning(5970) << "KarmStorage::save : " << err << endl;
}
return err;
}
TQString KarmStorage::writeTaskAsTodo(Task* task, const int level,
TQPtrStack< KCal::Todo >& parents )
{
TQString err;
KCal::Todo* todo;
todo = _calendar->todo(task->uid());
if ( !todo )
{
kdDebug(5970) << "Could not get todo from calendar" << endl;
return "Could not get todo from calendar";
}
task->asTodo(todo);
if ( !parents.isEmpty() ) todo->setRelatedTo( parents.top() );
parents.push( todo );
for ( Task* nextTask = task->firstChild(); nextTask;
nextTask = nextTask->nextSibling() )
{
err = writeTaskAsTodo(nextTask, level+1, parents );
}
parents.pop();
return err;
}
bool KarmStorage::isEmpty()
{
KCal::Todo::List todoList;
todoList = _calendar->rawTodos();
return todoList.empty();
}
bool KarmStorage::isNewStorage(const Preferences* preferences) const
{
if ( !_icalfile.isNull() ) return preferences->iCalFile() != _icalfile;
else return false;
}
//----------------------------------------------------------------------------
// Routines that handle legacy flat file format.
// These only stored total and session times.
//
TQString KarmStorage::loadFromFlatFile(TaskView* taskview,
const TQString& filename)
{
TQString err;
kdDebug(5970)
<< "KarmStorage::loadFromFlatFile: " << filename << endl;
TQFile f(filename);
if( !f.exists() )
err = i18n("File \"%1\" not found.").arg(filename);
if (!err)
{
if( !f.open( IO_ReadOnly ) )
err = i18n("Could not open \"%1\".").arg(filename);
}
if (!err)
{
TQString line;
TQPtrStack<Task> stack;
Task *task;
TQTextStream stream(&f);
while( !stream.atEnd() ) {
// lukas: this breaks for non-latin1 chars!!!
// if ( file.readLine( line, T_LINESIZE ) == 0 )
// break;
line = stream.readLine();
kdDebug(5970) << "DEBUG: line: " << line << "\n";
if (line.isNull())
break;
long minutes;
int level;
TQString name;
DesktopList desktopList;
if (!parseLine(line, &minutes, &name, &level, &desktopList))
continue;
unsigned int stackLevel = stack.count();
for (unsigned int i = level; i<=stackLevel ; i++) {
stack.pop();
}
if (level == 1) {
kdDebug(5970) << "KarmStorage::loadFromFlatFile - toplevel task: "
<< name << " min: " << minutes << "\n";
task = new Task(name, minutes, 0, desktopList, taskview);
task->setUid(addTask(task, 0));
}
else {
Task *parent = stack.top();
kdDebug(5970) << "KarmStorage::loadFromFlatFile - task: " << name
<< " min: " << minutes << " parent" << parent->name() << "\n";
task = new Task(name, minutes, 0, desktopList, parent);
task->setUid(addTask(task, parent));
// Legacy File Format (!):
parent->changeTimes(0, -minutes);
taskview->setRootIsDecorated(true);
parent->setOpen(true);
}
if (!task->uid().isNull())
stack.push(task);
else
delete task;
}
f.close();
}
return err;
}
TQString KarmStorage::loadFromFlatFileCumulative(TaskView* taskview,
const TQString& filename)
{
TQString err = loadFromFlatFile(taskview, filename);
if (!err)
{
for (Task* task = taskview->first_child(); task;
task = task->nextSibling())
{
adjustFromLegacyFileFormat(task);
}
}
return err;
}
bool KarmStorage::parseLine(TQString line, long *time, TQString *name,
int *level, DesktopList* desktopList)
{
if (line.find('#') == 0) {
// A comment line
return false;
}
int index = line.find('\t');
if (index == -1) {
// This doesn't seem like a valid record
return false;
}
TQString levelStr = line.left(index);
TQString rest = line.remove(0,index+1);
index = rest.find('\t');
if (index == -1) {
// This doesn't seem like a valid record
return false;
}
TQString timeStr = rest.left(index);
rest = rest.remove(0,index+1);
bool ok;
index = rest.find('\t'); // check for optional desktops string
if (index >= 0) {
*name = rest.left(index);
TQString deskLine = rest.remove(0,index+1);
// now transform the ds string (e.g. "3", or "1,4,5") into
// an DesktopList
TQString ds;
int d;
int commaIdx = deskLine.find(',');
while (commaIdx >= 0) {
ds = deskLine.left(commaIdx);
d = ds.toInt(&ok);
if (!ok)
return false;
desktopList->push_back(d);
deskLine.remove(0,commaIdx+1);
commaIdx = deskLine.find(',');
}
d = deskLine.toInt(&ok);
if (!ok)
return false;
desktopList->push_back(d);
}
else {
*name = rest.remove(0,index+1);
}
*time = timeStr.toLong(&ok);
if (!ok) {
// the time field was not a number
return false;
}
*level = levelStr.toInt(&ok);
if (!ok) {
// the time field was not a number
return false;
}
return true;
}
void KarmStorage::adjustFromLegacyFileFormat(Task* task)
{
// unless the parent is the listView
if ( task->parent() )
task->parent()->changeTimes(-task->sessionTime(), -task->time());
// traverse depth first -
// as soon as we're in a leaf, we'll substract it's time from the parent
// then, while descending back we'll do the same for each node untill
// we reach the root
for ( Task* subtask = task->firstChild(); subtask;
subtask = subtask->nextSibling() )
adjustFromLegacyFileFormat(subtask);
}
//----------------------------------------------------------------------------
// Routines that handle Comma-Separated Values export file format.
//
TQString KarmStorage::exportcsvFile( TaskView *taskview,
const ReportCriteria &rc )
{
TQString delim = rc.delimiter;
TQString dquote = rc.quote;
TQString double_dquote = dquote + dquote;
bool to_quote = true;
TQString err;
Task* task;
int maxdepth=0;
kdDebug(5970)
<< "KarmStorage::exportcsvFile: " << rc.url << endl;
TQString title = i18n("Export Progress");
KProgressDialog dialog( taskview, 0, title );
dialog.setAutoClose( true );
dialog.setAllowCancel( true );
dialog.progressBar()->setTotalSteps( 2 * taskview->count() );
// The default dialog was not displaying all the text in the title bar.
int width = taskview->fontMetrics().width(title) * 3;
TQSize dialogsize;
dialogsize.setWidth(width);
dialog.setInitialSize( dialogsize, true );
if ( taskview->count() > 1 ) dialog.show();
TQString retval;
// Find max task depth
int tasknr = 0;
while ( tasknr < taskview->count() && !dialog.wasCancelled() )
{
dialog.progressBar()->advance( 1 );
if ( tasknr % 15 == 0 ) kapp->processEvents(); // repainting is slow
if ( taskview->item_at_index(tasknr)->depth() > maxdepth )
maxdepth = taskview->item_at_index(tasknr)->depth();
tasknr++;
}
// Export to file
tasknr = 0;
while ( tasknr < taskview->count() && !dialog.wasCancelled() )
{
task = taskview->item_at_index( tasknr );
dialog.progressBar()->advance( 1 );
if ( tasknr % 15 == 0 ) kapp->processEvents();
// indent the task in the csv-file:
for ( int i=0; i < task->depth(); ++i ) retval += delim;
/*
// CSV compliance
// Surround the field with quotes if the field contains
// a comma (delim) or a double quote
if (task->name().contains(delim) || task->name().contains(dquote))
to_quote = true;
else
to_quote = false;
*/
to_quote = true;
if (to_quote)
retval += dquote;
// Double quotes replaced by a pair of consecutive double quotes
retval += task->name().replace( dquote, double_dquote );
if (to_quote)
retval += dquote;
// maybe other tasks are more indented, so to align the columns:
for ( int i = 0; i < maxdepth - task->depth(); ++i ) retval += delim;
retval += delim + formatTime( task->sessionTime(),
rc.decimalMinutes )
+ delim + formatTime( task->time(),
rc.decimalMinutes )
+ delim + formatTime( task->totalSessionTime(),
rc.decimalMinutes )
+ delim + formatTime( task->totalTime(),
rc.decimalMinutes )
+ "\n";
tasknr++;
}
// save, either locally or remote
if ((rc.url.isLocalFile()) || (!rc.url.url().contains("/")))
{
TQString filename=rc.url.path();
if (filename.isEmpty()) filename=rc.url.url();
TQFile f( filename );
if( !f.open( IO_WriteOnly ) ) {
err = i18n( "Could not open \"%1\"." ).arg( filename );
}
if (!err)
{
TQTextStream stream(&f);
// Export to file
stream << retval;
f.close();
}
}
else // use remote file
{
KTempFile tmpFile;
if ( tmpFile.status() != 0 ) err = TQString::fromLatin1( "Unable to get temporary file" );
else
{
TQTextStream *stream=tmpFile.textStream();
*stream << retval;
tmpFile.close();
if (!TDEIO::NetAccess::upload( tmpFile.name(), rc.url, 0 )) err=TQString::fromLatin1("Could not upload");
}
}
return err;
}
//----------------------------------------------------------------------------
// Routines that handle logging KArm history
//
//
// public routines:
//
TQString KarmStorage::addTask(const Task* task, const Task* parent)
{
KCal::Todo* todo;
TQString uid;
todo = new KCal::Todo();
if ( _calendar->addTodo( todo ) )
{
task->asTodo( todo );
if (parent)
todo->setRelatedTo(_calendar->todo(parent->uid()));
uid = todo->uid();
}
else
{
// Most likely a lock could not be pulled, although there are other
// possiblities (like a really confused resource manager).
uid = "";
}
return uid;
}
bool KarmStorage::removeTask(Task* task)
{
// delete history
KCal::Event::List eventList = _calendar->rawEvents();
for(KCal::Event::List::iterator i = eventList.begin();
i != eventList.end();
++i)
{
//kdDebug(5970) << "KarmStorage::removeTask: "
// << (*i)->uid() << " - relatedToUid() "
// << (*i)->relatedToUid()
// << ", relatedTo() = " << (*i)->relatedTo() <<endl;
if ( (*i)->relatedToUid() == task->uid()
|| ( (*i)->relatedTo()
&& (*i)->relatedTo()->uid() == task->uid()))
{
_calendar->deleteEvent(*i);
}
}
// delete todo
KCal::Todo *todo = _calendar->todo(task->uid());
_calendar->deleteTodo(todo);
// Save entire file
saveCalendar();
return true;
}
void KarmStorage::addComment(const Task* task, const TQString& comment)
{
KCal::Todo* todo;
todo = _calendar->todo(task->uid());
// Do this to avoid compiler warnings about comment not being used. once we
// transition to using the addComment method, we need this second param.
TQString s = comment;
// TODO: Use libkcal comments
// todo->addComment(comment);
// temporary
todo->setDescription(task->comment());
saveCalendar();
}
long KarmStorage::printTaskHistory (
const Task *task,
const TQMap<TQString,long> &taskdaytotals,
TQMap<TQString,long> &daytotals,
const TQDate &from,
const TQDate &to,
const int level,
vector <TQString> &matrix,
const ReportCriteria &rc)
// to>=from is precondition
{
long ownline=linenr++; // the how many-th instance of this function is this
long colrectot=0; // colum where to write the task's total recursive time
vector <TQString> cell; // each line of the matrix is stored in an array of cells, one containing the recursive total
long add; // total recursive time of all subtasks
TQString delim = rc.delimiter;
TQString dquote = rc.quote;
TQString double_dquote = dquote + dquote;
bool to_quote = true;
const TQString cr = TQString::fromLatin1("\n");
TQString buf;
TQString daytaskkey, daykey;
TQDate day;
long sum;
if ( !task ) return 0;
day = from;
sum = 0;
while (day <= to)
{
// write the time in seconds for the given task for the given day to s
daykey = day.toString(TQString::fromLatin1("yyyyMMdd"));
daytaskkey = TQString::fromLatin1("%1_%2")
.arg(daykey)
.arg(task->uid());
if (taskdaytotals.contains(daytaskkey))
{
cell.push_back(TQString::fromLatin1("%1")
.arg(formatTime(taskdaytotals[daytaskkey]/60, rc.decimalMinutes)));
sum += taskdaytotals[daytaskkey]; // in seconds
if (daytotals.contains(daykey))
daytotals.replace(daykey, daytotals[daykey]+taskdaytotals[daytaskkey]);
else
daytotals.insert(daykey, taskdaytotals[daytaskkey]);
}
cell.push_back(delim);
day = day.addDays(1);
}
// Total for task
cell.push_back(TQString::fromLatin1("%1").arg(formatTime(sum/60, rc.decimalMinutes)));
// room for the recursive total time (that cannot be calculated now)
cell.push_back(delim);
colrectot = cell.size();
cell.push_back("???");
cell.push_back(delim);
// Task name
for ( int i = level + 1; i > 0; i-- ) cell.push_back(delim);
/*
// CSV compliance
// Surround the field with quotes if the field contains
// a comma (delim) or a double quote
to_quote = task->name().contains(delim) || task->name().contains(dquote);
*/
to_quote = true;
if ( to_quote) cell.push_back(dquote);
// Double quotes replaced by a pair of consecutive double quotes
cell.push_back(task->name().replace( dquote, double_dquote ));
if ( to_quote) cell.push_back(dquote);
cell.push_back(cr);
add=0;
for (Task* subTask = task->firstChild();
subTask;
subTask = subTask->nextSibling())
{
add += printTaskHistory( subTask, taskdaytotals, daytotals, from, to , level+1, matrix,
rc );
}
cell[colrectot]=(TQString::fromLatin1("%1").arg(formatTime((add+sum)/60, rc.decimalMinutes )));
for (unsigned int i=0; i < cell.size(); i++) matrix[ownline]+=cell[i];
return add+sum;
}
TQString KarmStorage::report( TaskView *taskview, const ReportCriteria &rc )
{
TQString err;
if ( rc.reportType == ReportCriteria::CSVHistoryExport )
err = exportcsvHistory( taskview, rc.from, rc.to, rc );
else if ( rc.reportType == ReportCriteria::CSVTotalsExport )
err = exportcsvFile( taskview, rc );
else {
// hmmmm ... assert(0)?
}
return err;
}
// export history report as csv, all tasks X all dates in one block
TQString KarmStorage::exportcsvHistory ( TaskView *taskview,
const TQDate &from,
const TQDate &to,
const ReportCriteria &rc)
{
TQString delim = rc.delimiter;
const TQString cr = TQString::fromLatin1("\n");
TQString err;
// below taken from timekard.cpp
TQString retval;
TQString taskhdr, totalhdr;
TQString line, buf;
long sum;
TQValueList<HistoryEvent> events;
TQValueList<HistoryEvent>::iterator event;
TQMap<TQString, long> taskdaytotals;
TQMap<TQString, long> daytotals;
TQString daytaskkey, daykey;
TQDate day;
TQDate dayheading;
// parameter-plausi
if ( from > to )
{
err = TQString::fromLatin1 (
"'to' has to be a date later than or equal to 'from'.");
}
// header
retval += i18n("Task History\n");
retval += i18n("From %1 to %2")
.arg(TDEGlobal::locale()->formatDate(from))
.arg(TDEGlobal::locale()->formatDate(to));
retval += cr;
retval += i18n("Printed on: %1")
.arg(TDEGlobal::locale()->formatDateTime(TQDateTime::currentDateTime()));
retval += cr;
day=from;
events = taskview->getHistory(from, to);
taskdaytotals.clear();
daytotals.clear();
// Build lookup dictionary used to output data in table cells. keys are
// in this format: YYYYMMDD_NNNNNN, where Y = year, M = month, d = day and
// NNNNN = the VTODO uid. The value is the total seconds logged against
// that task on that day. Note the UID is the todo id, not the event id,
// so times are accumulated for each task.
for (event = events.begin(); event != events.end(); ++event)
{
daykey = (*event).start().date().toString(TQString::fromLatin1("yyyyMMdd"));
daytaskkey = TQString(TQString::fromLatin1("%1_%2"))
.arg(daykey)
.arg((*event).todoUid());
if (taskdaytotals.contains(daytaskkey))
taskdaytotals.replace(daytaskkey,
taskdaytotals[daytaskkey] + (*event).duration());
else
taskdaytotals.insert(daytaskkey, (*event).duration());
}
// day headings
dayheading = from;
while ( dayheading <= to )
{
// Use ISO 8601 format for date.
retval += dayheading.toString(TQString::fromLatin1("yyyy-MM-dd"));
retval += delim;
dayheading=dayheading.addDays(1);
}
retval += i18n("Sum") + delim + i18n("Total Sum") + delim + i18n("Task Hierarchy");
retval += cr;
retval += line;
// the tasks
vector <TQString> matrix;
linenr=0;
for (int i=0; i<=taskview->count()+1; i++) matrix.push_back("");
if (events.empty())
{
retval += i18n(" No hours logged.");
}
else
{
if ( rc.allTasks )
{
for ( Task* task= taskview->item_at_index(0);
task; task= task->nextSibling() )
{
printTaskHistory( task, taskdaytotals, daytotals, from, to, 0,
matrix, rc );
}
}
else
{
printTaskHistory( taskview->current_item(), taskdaytotals, daytotals,
from, to, 0, matrix, rc );
}
for (unsigned int i=0; i<matrix.size(); i++) retval+=matrix[i];
retval += line;
// totals
sum = 0;
day = from;
while (day<=to)
{
daykey = day.toString(TQString::fromLatin1("yyyyMMdd"));
if (daytotals.contains(daykey))
{
retval += TQString::fromLatin1("%1")
.arg(formatTime(daytotals[daykey]/60, rc.decimalMinutes));
sum += daytotals[daykey]; // in seconds
}
retval += delim;
day = day.addDays(1);
}
retval += TQString::fromLatin1("%1%2%3%4")
.arg( formatTime( sum/60, rc.decimalMinutes ) )
.arg( delim ).arg( delim )
.arg( i18n( "Total" ) );
}
// above taken from timekard.cpp
// save, either locally or remote
if ((rc.url.isLocalFile()) || (!rc.url.url().contains("/")))
{
TQString filename=rc.url.path();
if (filename.isEmpty()) filename=rc.url.url();
TQFile f( filename );
if( !f.open( IO_WriteOnly ) ) {
err = i18n( "Could not open \"%1\"." ).arg( filename );
}
if (!err)
{
TQTextStream stream(&f);
// Export to file
stream << retval;
f.close();
}
}
else // use remote file
{
KTempFile tmpFile;
if ( tmpFile.status() != 0 )
{
err = TQString::fromLatin1( "Unable to get temporary file" );
}
else
{
TQTextStream *stream=tmpFile.textStream();
*stream << retval;
tmpFile.close();
if (!TDEIO::NetAccess::upload( tmpFile.name(), rc.url, 0 )) err=TQString::fromLatin1("Could not upload");
}
}
return err;
}
void KarmStorage::stopTimer(const Task* task, TQDateTime when)
{
kdDebug(5970) << "Entering KarmStorage::stopTimer" << endl;
long delta = task->startTime().secsTo(when);
changeTime(task, delta);
}
bool KarmStorage::bookTime(const Task* task,
const TQDateTime& startDateTime,
const long durationInSeconds)
{
// Ignores preferences setting re: logging history.
KCal::Event* e;
TQDateTime end;
e = baseEvent( task );
e->setDtStart( startDateTime );
e->setDtEnd( startDateTime.addSecs( durationInSeconds ) );
// Use a custom property to keep a record of negative durations
e->setCustomProperty( kapp->instanceName(),
TQCString("duration"),
TQString::number(durationInSeconds));
return _calendar->addEvent(e);
}
void KarmStorage::changeTime(const Task* task, const long deltaSeconds)
{
kdDebug(5970) << "Entering KarmStorage::changeTime ( " << task->name() << "," << deltaSeconds << " )" << endl;
KCal::Event* e;
TQDateTime end;
// Don't write events (with timer start/stop duration) if user has turned
// this off in the settings dialog.
if ( ! task->taskView()->preferences()->logging() ) return;
e = baseEvent(task);
// Don't use duration, as ICalFormatImpl::writeIncidence never writes a
// duration, even though it looks like it's used in event.cpp.
end = task->startTime();
if ( deltaSeconds > 0 ) end = task->startTime().addSecs(deltaSeconds);
e->setDtEnd(end);
// Use a custom property to keep a record of negative durations
e->setCustomProperty( kapp->instanceName(),
TQCString("duration"),
TQString::number(deltaSeconds));
_calendar->addEvent(e);
// This saves the entire iCal file each time, which isn't efficient but
// ensures no data loss. A faster implementation would be to append events
// to a file, and then when KArm closes, append the data in this file to the
// iCal file.
//
// Meanwhile, we simply use a timer to delay the full-saving until the GUI
// has updated, for better user feedback. Feel free to get rid of this
// if/when implementing the faster saving (DF).
task->taskView()->scheduleSave();
}
KCal::Event* KarmStorage::baseEvent(const Task * task)
{
KCal::Event* e;
TQStringList categories;
e = new KCal::Event;
e->setSummary(task->name());
// Can't use setRelatedToUid()--no error, but no RelatedTo written to disk
e->setRelatedTo(_calendar->todo(task->uid()));
// Debugging: some events where not getting a related-to field written.
assert(e->relatedTo()->uid() == task->uid());
// Have to turn this off to get datetimes in date fields.
e->setFloats(false);
e->setDtStart(task->startTime());
// So someone can filter this mess out of their calendar display
categories.append(i18n("KArm"));
e->setCategories(categories);
return e;
}
HistoryEvent::HistoryEvent(TQString uid, TQString name, long duration,
TQDateTime start, TQDateTime stop, TQString todoUid)
{
_uid = uid;
_name = name;
_duration = duration;
_start = start;
_stop = stop;
_todoUid = todoUid;
}
TQValueList<HistoryEvent> KarmStorage::getHistory(const TQDate& from,
const TQDate& to)
{
TQValueList<HistoryEvent> retval;
TQStringList processed;
KCal::Event::List events;
KCal::Event::List::iterator event;
TQString duration;
for(TQDate d = from; d <= to; d = d.addDays(1))
{
events = _calendar->rawEventsForDate( d );
for (event = events.begin(); event != events.end(); ++event)
{
// KArm events have the custom property X-TDE-Karm-duration
if (! processed.contains( (*event)->uid()))
{
// If an event spans multiple days, CalendarLocal::rawEventsForDate
// will return the same event on both days. To avoid double-counting
// such events, we (arbitrarily) attribute the hours from both days on
// the first day. This mis-reports the actual time spent, but it is
// an easy fix for a (hopefully) rare situation.
processed.append( (*event)->uid());
duration = (*event)->customProperty(kapp->instanceName(),
TQCString("duration"));
if ( ! duration.isNull() )
{
if ( (*event)->relatedTo()
&& ! (*event)->relatedTo()->uid().isEmpty() )
{
retval.append(HistoryEvent(
(*event)->uid(),
(*event)->summary(),
duration.toLong(),
(*event)->dtStart(),
(*event)->dtEnd(),
(*event)->relatedTo()->uid()
));
}
else
// Something is screwy with the ics file, as this KArm history event
// does not have a todo related to it. Could have been deleted
// manually? We'll continue with report on with report ...
kdDebug(5970) << "KarmStorage::getHistory(): "
<< "The event " << (*event)->uid()
<< " is not related to a todo. Dropped." << endl;
}
}
}
}
return retval;
}
bool KarmStorage::remoteResource( const TQString& file ) const
{
TQString f = file.lower();
bool rval = f.startsWith( "http://" ) || f.startsWith( "ftp://" );
kdDebug(5970) << "KarmStorage::remoteResource( " << file << " ) returns " << rval << endl;
return rval;
}
bool KarmStorage::saveCalendar()
{
kdDebug(5970) << "KarmStorage::saveCalendar" << endl;
#if 0
Event::List evl=_calendar->rawEvents();
kdDebug(5970) << "summary - dtStart - dtEnd" << endl;
for (unsigned int i=0; i<evl.count(); i++)
{
kdDebug() << evl[i]->summary() << evl[i]->dtStart() << evl[i]->dtEnd() << endl;
}
#endif
TDEABC::Lock *lock = _calendar->lock();
if ( !lock || !lock->lock() )
return false;
if ( _calendar && _calendar->save() ) {
lock->unlock();
return true;
}
lock->unlock();
return false;
}