summaryrefslogtreecommitdiffstats
path: root/karm/karmstorage.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'karm/karmstorage.cpp')
-rw-r--r--karm/karmstorage.cpp1241
1 files changed, 1241 insertions, 0 deletions
diff --git a/karm/karmstorage.cpp b/karm/karmstorage.cpp
new file mode 100644
index 000000000..7e150d057
--- /dev/null
+++ b/karm/karmstorage.cpp
@@ -0,0 +1,1241 @@
+/*
+ * 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 <qfile.h>
+#include <qsize.h>
+#include <qdict.h>
+#include <qdatetime.h>
+#include <qstring.h>
+#include <qstringlist.h>
+
+#include "incidence.h"
+#include "kapplication.h" // kapp
+#include <kdebug.h>
+#include <kemailsettings.h>
+#include <klocale.h> // i18n
+#include <kmessagebox.h>
+#include <kprogress.h>
+#include <ktempfile.h>
+#include <resourcecalendar.h>
+#include <resourcelocal.h>
+#include <resourceremote.h>
+#include <kpimprefs.h>
+#include <taskview.h>
+#include <timekard.h>
+#include <karmutility.h>
+#include <kio/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;
+}
+
+QString KarmStorage::load (TaskView* view, const Preferences* preferences, QString 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)
+
+ QString 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 (
+ QFile::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;
+
+ QObject::connect (_calendar, SIGNAL(resourceChanged(ResourceCalendar *)),
+ view, SLOT(iCalFileModified(ResourceCalendar *)));
+ _calendar->setTimeZoneId( KPimPrefs::timezone() );
+ _calendar->setResourceName( QString::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;
+ QDict< Task > map;
+
+ // Build dictionary to look up Task object from Todo uid. Each task is a
+ // QListViewItem, 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;
+}
+
+QString KarmStorage::icalfile()
+{
+ kdDebug(5970) << "Entering KarmStorage::icalfile" << endl;
+ return _icalfile;
+}
+
+QString KarmStorage::buildTaskView(KCal::ResourceCalendar *rc, TaskView *view)
+// makes *view contain the tasks out of *rc.
+{
+ QString err;
+ KCal::Todo::List todoList;
+ KCal::Todo::List::ConstIterator todo;
+ QDict< Task > map;
+ vector<QString> runningTasks;
+ vector<QDateTime> 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
+ // QListViewItem, 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();
+ }
+}
+
+QString KarmStorage::save(TaskView* taskview)
+{
+ kdDebug(5970) << "entering KarmStorage::save" << endl;
+ QString err=QString();
+
+ QPtrStack< 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;
+}
+
+QString KarmStorage::writeTaskAsTodo(Task* task, const int level,
+ QPtrStack< KCal::Todo >& parents )
+{
+ QString 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.
+//
+
+QString KarmStorage::loadFromFlatFile(TaskView* taskview,
+ const QString& filename)
+{
+ QString err;
+
+ kdDebug(5970)
+ << "KarmStorage::loadFromFlatFile: " << filename << endl;
+
+ QFile 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)
+ {
+
+ QString line;
+
+ QPtrStack<Task> stack;
+ Task *task;
+
+ QTextStream 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;
+ QString 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;
+}
+
+QString KarmStorage::loadFromFlatFileCumulative(TaskView* taskview,
+ const QString& filename)
+{
+ QString err = loadFromFlatFile(taskview, filename);
+ if (!err)
+ {
+ for (Task* task = taskview->first_child(); task;
+ task = task->nextSibling())
+ {
+ adjustFromLegacyFileFormat(task);
+ }
+ }
+ return err;
+}
+
+bool KarmStorage::parseLine(QString line, long *time, QString *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;
+ }
+
+ QString levelStr = line.left(index);
+ QString rest = line.remove(0,index+1);
+
+ index = rest.find('\t');
+ if (index == -1) {
+ // This doesn't seem like a valid record
+ return false;
+ }
+
+ QString 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);
+ QString deskLine = rest.remove(0,index+1);
+
+ // now transform the ds string (e.g. "3", or "1,4,5") into
+ // an DesktopList
+ QString 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.
+//
+QString KarmStorage::exportcsvFile( TaskView *taskview,
+ const ReportCriteria &rc )
+{
+ QString delim = rc.delimiter;
+ QString dquote = rc.quote;
+ QString double_dquote = dquote + dquote;
+ bool to_quote = true;
+
+ QString err;
+ Task* task;
+ int maxdepth=0;
+
+ kdDebug(5970)
+ << "KarmStorage::exportcsvFile: " << rc.url << endl;
+
+ QString 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;
+ QSize dialogsize;
+ dialogsize.setWidth(width);
+ dialog.setInitialSize( dialogsize, true );
+
+ if ( taskview->count() > 1 ) dialog.show();
+
+ QString 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("/")))
+ {
+ QString filename=rc.url.path();
+ if (filename.isEmpty()) filename=rc.url.url();
+ QFile f( filename );
+ if( !f.open( IO_WriteOnly ) ) {
+ err = i18n( "Could not open \"%1\"." ).arg( filename );
+ }
+ if (!err)
+ {
+ QTextStream stream(&f);
+ // Export to file
+ stream << retval;
+ f.close();
+ }
+ }
+ else // use remote file
+ {
+ KTempFile tmpFile;
+ if ( tmpFile.status() != 0 ) err = QString::fromLatin1( "Unable to get temporary file" );
+ else
+ {
+ QTextStream *stream=tmpFile.textStream();
+ *stream << retval;
+ tmpFile.close();
+ if (!KIO::NetAccess::upload( tmpFile.name(), rc.url, 0 )) err=QString::fromLatin1("Could not upload");
+ }
+ }
+
+ return err;
+}
+
+//----------------------------------------------------------------------------
+// Routines that handle logging KArm history
+//
+
+//
+// public routines:
+//
+
+QString KarmStorage::addTask(const Task* task, const Task* parent)
+{
+ KCal::Todo* todo;
+ QString 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 QString& 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.
+ QString s = comment;
+
+ // TODO: Use libkcal comments
+ // todo->addComment(comment);
+ // temporary
+ todo->setDescription(task->comment());
+
+ saveCalendar();
+}
+
+long KarmStorage::printTaskHistory (
+ const Task *task,
+ const QMap<QString,long> &taskdaytotals,
+ QMap<QString,long> &daytotals,
+ const QDate &from,
+ const QDate &to,
+ const int level,
+ vector <QString> &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 <QString> 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
+ QString delim = rc.delimiter;
+ QString dquote = rc.quote;
+ QString double_dquote = dquote + dquote;
+ bool to_quote = true;
+
+ const QString cr = QString::fromLatin1("\n");
+ QString buf;
+ QString daytaskkey, daykey;
+ QDate 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(QString::fromLatin1("yyyyMMdd"));
+ daytaskkey = QString::fromLatin1("%1_%2")
+ .arg(daykey)
+ .arg(task->uid());
+
+ if (taskdaytotals.contains(daytaskkey))
+ {
+ cell.push_back(QString::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(QString::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]=(QString::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;
+}
+
+QString KarmStorage::report( TaskView *taskview, const ReportCriteria &rc )
+{
+ QString 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
+QString KarmStorage::exportcsvHistory ( TaskView *taskview,
+ const QDate &from,
+ const QDate &to,
+ const ReportCriteria &rc)
+{
+ QString delim = rc.delimiter;
+ const QString cr = QString::fromLatin1("\n");
+ QString err;
+
+ // below taken from timekard.cpp
+ QString retval;
+ QString taskhdr, totalhdr;
+ QString line, buf;
+ long sum;
+
+ QValueList<HistoryEvent> events;
+ QValueList<HistoryEvent>::iterator event;
+ QMap<QString, long> taskdaytotals;
+ QMap<QString, long> daytotals;
+ QString daytaskkey, daykey;
+ QDate day;
+ QDate dayheading;
+
+ // parameter-plausi
+ if ( from > to )
+ {
+ err = QString::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(KGlobal::locale()->formatDate(from))
+ .arg(KGlobal::locale()->formatDate(to));
+ retval += cr;
+ retval += i18n("Printed on: %1")
+ .arg(KGlobal::locale()->formatDateTime(QDateTime::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(QString::fromLatin1("yyyyMMdd"));
+ daytaskkey = QString(QString::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(QString::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 <QString> 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(QString::fromLatin1("yyyyMMdd"));
+
+ if (daytotals.contains(daykey))
+ {
+ retval += QString::fromLatin1("%1")
+ .arg(formatTime(daytotals[daykey]/60, rc.decimalMinutes));
+ sum += daytotals[daykey]; // in seconds
+ }
+ retval += delim;
+ day = day.addDays(1);
+ }
+
+ retval += QString::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("/")))
+ {
+ QString filename=rc.url.path();
+ if (filename.isEmpty()) filename=rc.url.url();
+ QFile f( filename );
+ if( !f.open( IO_WriteOnly ) ) {
+ err = i18n( "Could not open \"%1\"." ).arg( filename );
+ }
+ if (!err)
+ {
+ QTextStream stream(&f);
+ // Export to file
+ stream << retval;
+ f.close();
+ }
+ }
+ else // use remote file
+ {
+ KTempFile tmpFile;
+ if ( tmpFile.status() != 0 )
+ {
+ err = QString::fromLatin1( "Unable to get temporary file" );
+ }
+ else
+ {
+ QTextStream *stream=tmpFile.textStream();
+ *stream << retval;
+ tmpFile.close();
+ if (!KIO::NetAccess::upload( tmpFile.name(), rc.url, 0 )) err=QString::fromLatin1("Could not upload");
+ }
+ }
+ return err;
+}
+
+void KarmStorage::stopTimer(const Task* task, QDateTime when)
+{
+ kdDebug(5970) << "Entering KarmStorage::stopTimer" << endl;
+ long delta = task->startTime().secsTo(when);
+ changeTime(task, delta);
+}
+
+bool KarmStorage::bookTime(const Task* task,
+ const QDateTime& startDateTime,
+ const long durationInSeconds)
+{
+ // Ignores preferences setting re: logging history.
+ KCal::Event* e;
+ QDateTime 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(),
+ QCString("duration"),
+ QString::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;
+ QDateTime 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(),
+ QCString("duration"),
+ QString::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;
+ QStringList 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(QString uid, QString name, long duration,
+ QDateTime start, QDateTime stop, QString todoUid)
+{
+ _uid = uid;
+ _name = name;
+ _duration = duration;
+ _start = start;
+ _stop = stop;
+ _todoUid = todoUid;
+}
+
+
+QValueList<HistoryEvent> KarmStorage::getHistory(const QDate& from,
+ const QDate& to)
+{
+ QValueList<HistoryEvent> retval;
+ QStringList processed;
+ KCal::Event::List events;
+ KCal::Event::List::iterator event;
+ QString duration;
+
+ for(QDate 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-KDE-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(),
+ QCString("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 QString& file ) const
+{
+ QString 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
+ KABC::Lock *lock = _calendar->lock();
+ if ( !lock || !lock->lock() )
+ return false;
+
+ if ( _calendar && _calendar->save() ) {
+ lock->unlock();
+ return true;
+ }
+
+ lock->unlock();
+ return false;
+}