karm

karmstorage.cpp

00001 /*
00002  *   This file only:
00003  *     Copyright (C) 2003, 2004  Mark Bucciarelli <mark@hubcapconsulting.com>
00004  *
00005  *   This program is free software; you can redistribute it and/or modify
00006  *   it under the terms of the GNU General Public License as published by
00007  *   the Free Software Foundation; either version 2 of the License, or
00008  *   (at your option) any later version.
00009  *
00010  *   This program is distributed in the hope that it will be useful,
00011  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
00012  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00013  *   GNU General Public License for more details.
00014  *
00015  *   You should have received a copy of the GNU General Public License along
00016  *   with this program; if not, write to the
00017  *      Free Software Foundation, Inc.
00018  *      51 Franklin Street, Fifth Floor
00019  *      Boston, MA  02110-1301  USA.
00020  *
00021  */
00022 
00023 #include <sys/types.h>
00024 #include <sys/stat.h>
00025 #include <fcntl.h>
00026 #include <unistd.h>
00027 
00028 #include <cassert>
00029 
00030 #include <qfile.h>
00031 #include <qsize.h>
00032 #include <qdict.h>
00033 #include <qdatetime.h>
00034 #include <qstring.h>
00035 #include <qstringlist.h>
00036 
00037 #include "incidence.h"
00038 #include "kapplication.h"       // kapp
00039 #include <kdebug.h>
00040 #include <kemailsettings.h>
00041 #include <klocale.h>            // i18n
00042 #include <kmessagebox.h>
00043 #include <kprogress.h>
00044 #include <ktempfile.h>
00045 #include <resourcecalendar.h>
00046 #include <resourcelocal.h>
00047 #include <resourceremote.h>
00048 #include <kpimprefs.h>
00049 #include <taskview.h>
00050 #include <timekard.h>
00051 #include <karmutility.h>
00052 #include <kio/netaccess.h>
00053 #include <kurl.h>
00054 #include <vector>
00055 
00056 //#include <calendarlocal.h>
00057 //#include <journal.h>
00058 //#include <event.h>
00059 //#include <todo.h>
00060 
00061 #include "karmstorage.h"
00062 #include "preferences.h"
00063 #include "task.h"
00064 #include "reportcriteria.h"
00065 
00066 using namespace std;
00067 
00068 KarmStorage *KarmStorage::_instance = 0;
00069 static long linenr;  // how many lines written by printTaskHistory so far
00070 
00071 
00072 KarmStorage *KarmStorage::instance()
00073 {
00074   if (_instance == 0) _instance = new KarmStorage();
00075   return _instance;
00076 }
00077 
00078 KarmStorage::KarmStorage()
00079 {
00080   _calendar = 0;
00081 }
00082 
00083 QString KarmStorage::load (TaskView* view, const Preferences* preferences, QString fileName )
00084 // loads data from filename into view. If no filename is given, filename from preferences is used.
00085 // filename might be of use if this program is run as embedded konqueror plugin.
00086 {
00087   // When I tried raising an exception from this method, the compiler
00088   // complained that exceptions are not allowed.  Not sure how apps
00089   // typically handle error conditions in KDE, but I'll return the error
00090   // as a string (empty is no error).  -- Mark, Aug 8, 2003
00091 
00092   // Use KDE_CXXFLAGS=$(USE_EXCEPTIONS) in Makefile.am if you want to use
00093   // exceptions (David Faure)
00094 
00095   QString err;
00096   KEMailSettings settings;
00097   if ( fileName.isEmpty() ) fileName = preferences->iCalFile();
00098 
00099   // If same file, don't reload
00100   if ( fileName == _icalfile ) return err;
00101 
00102 
00103   // If file doesn't exist, create a blank one to avoid ResourceLocal load
00104   // error.  We make it user and group read/write, others read.  This is
00105   // masked by the users umask.  (See man creat)
00106   if ( ! remoteResource( _icalfile ) )
00107   {
00108     int handle;
00109     handle = open (
00110         QFile::encodeName( fileName ),
00111         O_CREAT|O_EXCL|O_WRONLY,
00112         S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH
00113         );
00114     if (handle != -1) close(handle);
00115   }
00116 
00117   if ( _calendar)
00118     closeStorage(view);
00119 
00120   // Create local file resource and add to resources
00121   _icalfile = fileName;
00122 
00123   KCal::ResourceCached *resource;
00124   if ( remoteResource( _icalfile ) )
00125   {
00126     KURL url( _icalfile );
00127     resource = new KCal::ResourceRemote( url, url ); // same url for upload and download
00128   }
00129   else
00130   {
00131     resource = new KCal::ResourceLocal( _icalfile );
00132   }
00133   _calendar = resource;
00134 
00135   QObject::connect (_calendar, SIGNAL(resourceChanged(ResourceCalendar *)),
00136                 view, SLOT(iCalFileModified(ResourceCalendar *)));
00137   _calendar->setTimeZoneId( KPimPrefs::timezone() );
00138   _calendar->setResourceName( QString::fromLatin1("KArm") );
00139   _calendar->open();
00140   _calendar->load();
00141 
00142   // Claim ownership of iCalendar file if no one else has.
00143   KCal::Person owner = resource->getOwner();
00144   if ( owner.isEmpty() )
00145   {
00146     resource->setOwner( KCal::Person(
00147           settings.getSetting( KEMailSettings::RealName ),
00148           settings.getSetting( KEMailSettings::EmailAddress ) ) );
00149   }
00150 
00151   // Build task view from iCal data
00152   if (!err)
00153   {
00154     KCal::Todo::List todoList;
00155     KCal::Todo::List::ConstIterator todo;
00156     QDict< Task > map;
00157 
00158     // Build dictionary to look up Task object from Todo uid.  Each task is a
00159     // QListViewItem, and is initially added with the view as the parent.
00160     todoList = _calendar->rawTodos();
00161     kdDebug(5970) << "KarmStorage::load "
00162       << "rawTodo count (includes completed todos) ="
00163       << todoList.count() << endl;
00164     for( todo = todoList.begin(); todo != todoList.end(); ++todo )
00165     {
00166       // Initially, if a task was complete, it was removed from the view.
00167       // However, this increased the complexity of reporting on task history.
00168       //
00169       // For example, if a task is complete yet has time logged to it during
00170       // the date range specified on the history report, we have to figure out
00171       // how that task fits into the task hierarchy.  Currently, this
00172       // structure is held in memory by the structure in the list view.
00173       //
00174       // I considered creating a second tree that held the full structure of
00175       // all complete and incomplete tasks.  But this seemed to much of a
00176       // change with an impending beta release and a full todo list.
00177       //
00178       // Hence this "solution".  Include completed tasks, but mark them as
00179       // inactive in the view.
00180       //
00181       //if ((*todo)->isCompleted()) continue;
00182 
00183       Task* task = new Task(*todo, view);
00184       map.insert( (*todo)->uid(), task );
00185       view->setRootIsDecorated(true);
00186       task->setPixmapProgress();
00187     }
00188 
00189     // Load each task under it's parent task.
00190     for( todo = todoList.begin(); todo != todoList.end(); ++todo )
00191     {
00192       Task* task = map.find( (*todo)->uid() );
00193 
00194       // No relatedTo incident just means this is a top-level task.
00195       if ( (*todo)->relatedTo() )
00196       {
00197         Task* newParent = map.find( (*todo)->relatedToUid() );
00198 
00199         // Complete the loading but return a message
00200         if ( !newParent )
00201           err = i18n("Error loading \"%1\": could not find parent (uid=%2)")
00202             .arg(task->name())
00203             .arg((*todo)->relatedToUid());
00204 
00205         if (!err) task->move( newParent);
00206       }
00207     }
00208 
00209     kdDebug(5970) << "KarmStorage::load - loaded " << view->count()
00210       << " tasks from " << _icalfile << endl;
00211   }
00212 
00213   return err;
00214 }
00215 
00216 QString KarmStorage::buildTaskView(KCal::ResourceCalendar *rc, TaskView *view)
00217 // makes *view contain the tasks out of *rc.
00218 {
00219   QString err;
00220   KCal::Todo::List todoList;
00221   KCal::Todo::List::ConstIterator todo;
00222   QDict< Task > map;
00223   vector<QString> runningTasks;
00224   vector<QDateTime> startTimes;
00225 
00226   // remember tasks that are running and their start times
00227   for ( int i=0; i<view->count(); i++)
00228   {
00229     if ( view->item_at_index(i)->isRunning() )
00230     {
00231       runningTasks.push_back( view->item_at_index(i)->uid() );
00232       startTimes.push_back( view->item_at_index(i)->lastStart() );
00233     }
00234   }
00235 
00236   //view->stopAllTimers();
00237   // delete old tasks
00238   while (view->item_at_index(0)) view->item_at_index(0)->cut();
00239 
00240   // 1. insert tasks form rc into taskview
00241   // 1.1. Build dictionary to look up Task object from Todo uid.  Each task is a
00242   // QListViewItem, and is initially added with the view as the parent.
00243   todoList = rc->rawTodos();
00244   for( todo = todoList.begin(); todo != todoList.end(); ++todo )
00245   {
00246     Task* task = new Task(*todo, view);
00247     map.insert( (*todo)->uid(), task );
00248     view->setRootIsDecorated(true);
00249     task->setPixmapProgress();
00250   }
00251 
00252   // 1.1. Load each task under it's parent task.
00253   for( todo = todoList.begin(); todo != todoList.end(); ++todo )
00254   {
00255     Task* task = map.find( (*todo)->uid() );
00256 
00257     // No relatedTo incident just means this is a top-level task.
00258     if ( (*todo)->relatedTo() )
00259     {
00260       Task* newParent = map.find( (*todo)->relatedToUid() );
00261 
00262       // Complete the loading but return a message
00263       if ( !newParent )
00264         err = i18n("Error loading \"%1\": could not find parent (uid=%2)")
00265           .arg(task->name())
00266           .arg((*todo)->relatedToUid());
00267 
00268       if (!err) task->move( newParent);
00269     }
00270   }
00271 
00272   view->clearActiveTasks();
00273   // restart tasks that have been running with their start times
00274   for ( int i=0; i<view->count(); i++)
00275   {
00276     for ( int n=0; n<runningTasks.size(); n++)
00277     {
00278       if ( runningTasks[n] == view->item_at_index(i)->uid() )
00279       {
00280         view->startTimerFor( view->item_at_index(i), startTimes[n] ); 
00281       }
00282     }
00283   }
00284   
00285   view->refresh();
00286 
00287   return err;
00288 }
00289 
00290 void KarmStorage::closeStorage(TaskView* view)
00291 {
00292   if ( _calendar )
00293   {
00294     _calendar->close();
00295     delete _calendar;
00296     _calendar = 0;
00297 
00298     view->clear();
00299   }
00300 }
00301 
00302 QString KarmStorage::save(TaskView* taskview)
00303 {
00304   kdDebug(5970) << "entering KarmStorage::save" << endl;
00305   QString err;
00306 
00307   QPtrStack< KCal::Todo > parents;
00308 
00309   for (Task* task=taskview->first_child(); task; task = task->nextSibling())
00310   {
00311     err=writeTaskAsTodo(task, 1, parents );
00312   }
00313 
00314   if ( !saveCalendar() )
00315   {
00316     err="Could not save";
00317   }
00318 
00319   if ( err.isEmpty() )
00320   {
00321     kdDebug(5970)
00322       << "KarmStorage::save : wrote "
00323       << taskview->count() << " tasks to " << _icalfile << endl;
00324   }
00325   else
00326   {
00327     kdWarning(5970) << "KarmStorage::save : " << err << endl;
00328   }
00329 
00330   return err;
00331 }
00332 
00333 QString KarmStorage::writeTaskAsTodo(Task* task, const int level,
00334     QPtrStack< KCal::Todo >& parents )
00335 {
00336   QString err;
00337   KCal::Todo* todo;
00338 
00339   todo = _calendar->todo(task->uid());
00340   if ( !todo )
00341   {
00342     kdDebug(5970) << "Could not get todo from calendar" << endl;
00343     return "Could not get todo from calendar";
00344   }
00345   task->asTodo(todo);
00346   if ( !parents.isEmpty() ) todo->setRelatedTo( parents.top() );
00347   parents.push( todo );
00348 
00349   for ( Task* nextTask = task->firstChild(); nextTask;
00350         nextTask = nextTask->nextSibling() )
00351   {
00352     err = writeTaskAsTodo(nextTask, level+1, parents );
00353   }
00354 
00355   parents.pop();
00356   return err;
00357 }
00358 
00359 bool KarmStorage::isEmpty()
00360 {
00361   KCal::Todo::List todoList;
00362 
00363   todoList = _calendar->rawTodos();
00364   return todoList.empty();
00365 }
00366 
00367 bool KarmStorage::isNewStorage(const Preferences* preferences) const
00368 {
00369   if ( !_icalfile.isNull() ) return preferences->iCalFile() != _icalfile;
00370   else return false;
00371 }
00372 
00373 //----------------------------------------------------------------------------
00374 // Routines that handle legacy flat file format.
00375 // These only stored total and session times.
00376 //
00377 
00378 QString KarmStorage::loadFromFlatFile(TaskView* taskview,
00379     const QString& filename)
00380 {
00381   QString err;
00382 
00383   kdDebug(5970)
00384     << "KarmStorage::loadFromFlatFile: " << filename << endl;
00385 
00386   QFile f(filename);
00387   if( !f.exists() )
00388     err = i18n("File \"%1\" not found.").arg(filename);
00389 
00390   if (!err)
00391   {
00392     if( !f.open( IO_ReadOnly ) )
00393       err = i18n("Could not open \"%1\".").arg(filename);
00394   }
00395 
00396   if (!err)
00397   {
00398 
00399     QString line;
00400 
00401     QPtrStack<Task> stack;
00402     Task *task;
00403 
00404     QTextStream stream(&f);
00405 
00406     while( !stream.atEnd() ) {
00407       // lukas: this breaks for non-latin1 chars!!!
00408       // if ( file.readLine( line, T_LINESIZE ) == 0 )
00409       //   break;
00410 
00411       line = stream.readLine();
00412       kdDebug(5970) << "DEBUG: line: " << line << "\n";
00413 
00414       if (line.isNull())
00415         break;
00416 
00417       long minutes;
00418       int level;
00419       QString name;
00420       DesktopList desktopList;
00421       if (!parseLine(line, &minutes, &name, &level, &desktopList))
00422         continue;
00423 
00424       unsigned int stackLevel = stack.count();
00425       for (unsigned int i = level; i<=stackLevel ; i++) {
00426         stack.pop();
00427       }
00428 
00429       if (level == 1) {
00430         kdDebug(5970) << "KarmStorage::loadFromFlatFile - toplevel task: "
00431           << name << " min: " << minutes << "\n";
00432         task = new Task(name, minutes, 0, desktopList, taskview);
00433         task->setUid(addTask(task, 0));
00434       }
00435       else {
00436         Task *parent = stack.top();
00437         kdDebug(5970) << "KarmStorage::loadFromFlatFile - task: " << name
00438             << " min: " << minutes << " parent" << parent->name() << "\n";
00439         task = new Task(name, minutes, 0, desktopList, parent);
00440 
00441         task->setUid(addTask(task, parent));
00442 
00443         // Legacy File Format (!):
00444         parent->changeTimes(0, -minutes);
00445         taskview->setRootIsDecorated(true);
00446         parent->setOpen(true);
00447       }
00448       if (!task->uid().isNull())
00449         stack.push(task);
00450       else
00451         delete task;
00452     }
00453 
00454     f.close();
00455 
00456   }
00457 
00458   return err;
00459 }
00460 
00461 QString KarmStorage::loadFromFlatFileCumulative(TaskView* taskview,
00462     const QString& filename)
00463 {
00464   QString err = loadFromFlatFile(taskview, filename);
00465   if (!err)
00466   {
00467     for (Task* task = taskview->first_child(); task;
00468         task = task->nextSibling())
00469     {
00470       adjustFromLegacyFileFormat(task);
00471     }
00472   }
00473   return err;
00474 }
00475 
00476 bool KarmStorage::parseLine(QString line, long *time, QString *name,
00477     int *level, DesktopList* desktopList)
00478 {
00479   if (line.find('#') == 0) {
00480     // A comment line
00481     return false;
00482   }
00483 
00484   int index = line.find('\t');
00485   if (index == -1) {
00486     // This doesn't seem like a valid record
00487     return false;
00488   }
00489 
00490   QString levelStr = line.left(index);
00491   QString rest = line.remove(0,index+1);
00492 
00493   index = rest.find('\t');
00494   if (index == -1) {
00495     // This doesn't seem like a valid record
00496     return false;
00497   }
00498 
00499   QString timeStr = rest.left(index);
00500   rest = rest.remove(0,index+1);
00501 
00502   bool ok;
00503 
00504   index = rest.find('\t'); // check for optional desktops string
00505   if (index >= 0) {
00506     *name = rest.left(index);
00507     QString deskLine = rest.remove(0,index+1);
00508 
00509     // now transform the ds string (e.g. "3", or "1,4,5") into
00510     // an DesktopList
00511     QString ds;
00512     int d;
00513     int commaIdx = deskLine.find(',');
00514     while (commaIdx >= 0) {
00515       ds = deskLine.left(commaIdx);
00516       d = ds.toInt(&ok);
00517       if (!ok)
00518         return false;
00519 
00520       desktopList->push_back(d);
00521       deskLine.remove(0,commaIdx+1);
00522       commaIdx = deskLine.find(',');
00523     }
00524 
00525     d = deskLine.toInt(&ok);
00526 
00527     if (!ok)
00528       return false;
00529 
00530     desktopList->push_back(d);
00531   }
00532   else {
00533     *name = rest.remove(0,index+1);
00534   }
00535 
00536   *time = timeStr.toLong(&ok);
00537 
00538   if (!ok) {
00539     // the time field was not a number
00540     return false;
00541   }
00542   *level = levelStr.toInt(&ok);
00543   if (!ok) {
00544     // the time field was not a number
00545     return false;
00546   }
00547 
00548   return true;
00549 }
00550 
00551 void KarmStorage::adjustFromLegacyFileFormat(Task* task)
00552 {
00553   // unless the parent is the listView
00554   if ( task->parent() )
00555     task->parent()->changeTimes(-task->sessionTime(), -task->time());
00556 
00557   // traverse depth first -
00558   // as soon as we're in a leaf, we'll substract it's time from the parent
00559   // then, while descending back we'll do the same for each node untill
00560   // we reach the root
00561   for ( Task* subtask = task->firstChild(); subtask;
00562       subtask = subtask->nextSibling() )
00563     adjustFromLegacyFileFormat(subtask);
00564 }
00565 
00566 //----------------------------------------------------------------------------
00567 // Routines that handle Comma-Separated Values export file format.
00568 //
00569 QString KarmStorage::exportcsvFile( TaskView *taskview,
00570                                     const ReportCriteria &rc )
00571 {
00572   QString delim = rc.delimiter;
00573   QString dquote = rc.quote;
00574   QString double_dquote = dquote + dquote;
00575   bool to_quote = true;
00576 
00577   QString err;
00578   Task* task;
00579   int maxdepth=0;
00580 
00581   kdDebug(5970)
00582     << "KarmStorage::exportcsvFile: " << rc.url << endl;
00583 
00584   QString title = i18n("Export Progress");
00585   KProgressDialog dialog( taskview, 0, title );
00586   dialog.setAutoClose( true );
00587   dialog.setAllowCancel( true );
00588   dialog.progressBar()->setTotalSteps( 2 * taskview->count() );
00589 
00590   // The default dialog was not displaying all the text in the title bar.
00591   int width = taskview->fontMetrics().width(title) * 3;
00592   QSize dialogsize;
00593   dialogsize.setWidth(width);
00594   dialog.setInitialSize( dialogsize, true );
00595 
00596   if ( taskview->count() > 1 ) dialog.show();
00597 
00598   QString retval;
00599 
00600   // Find max task depth
00601   int tasknr = 0;
00602   while ( tasknr < taskview->count() && !dialog.wasCancelled() )
00603   {
00604     dialog.progressBar()->advance( 1 );
00605     if ( tasknr % 15 == 0 ) kapp->processEvents(); // repainting is slow
00606     if ( taskview->item_at_index(tasknr)->depth() > maxdepth )
00607       maxdepth = taskview->item_at_index(tasknr)->depth();
00608     tasknr++;
00609   }
00610 
00611   // Export to file
00612   tasknr = 0;
00613   while ( tasknr < taskview->count() && !dialog.wasCancelled() )
00614   {
00615     task = taskview->item_at_index( tasknr );
00616     dialog.progressBar()->advance( 1 );
00617     if ( tasknr % 15 == 0 ) kapp->processEvents();
00618 
00619     // indent the task in the csv-file:
00620     for ( int i=0; i < task->depth(); ++i ) retval += delim;
00621 
00622     /*
00623     // CSV compliance
00624     // Surround the field with quotes if the field contains
00625     // a comma (delim) or a double quote
00626     if (task->name().contains(delim) || task->name().contains(dquote))
00627       to_quote = true;
00628     else
00629       to_quote = false;
00630     */
00631     to_quote = true;
00632 
00633     if (to_quote)
00634       retval += dquote;
00635 
00636     // Double quotes replaced by a pair of consecutive double quotes
00637     retval += task->name().replace( dquote, double_dquote );
00638 
00639     if (to_quote)
00640       retval += dquote;
00641 
00642     // maybe other tasks are more indented, so to align the columns:
00643     for ( int i = 0; i < maxdepth - task->depth(); ++i ) retval += delim;
00644 
00645     retval += delim + formatTime( task->sessionTime(),
00646                                    rc.decimalMinutes )
00647            + delim + formatTime( task->time(),
00648                                    rc.decimalMinutes )
00649            + delim + formatTime( task->totalSessionTime(),
00650                                    rc.decimalMinutes )
00651            + delim + formatTime( task->totalTime(),
00652                                    rc.decimalMinutes )
00653            + "\n";
00654     tasknr++;
00655   }
00656 
00657   // save, either locally or remote
00658   if ((rc.url.isLocalFile()) || (!rc.url.url().contains("/")))
00659   {
00660     QString filename=rc.url.path();
00661     if (filename.isEmpty()) filename=rc.url.url();
00662     QFile f( filename );
00663     if( !f.open( IO_WriteOnly ) ) {
00664         err = i18n( "Could not open \"%1\"." ).arg( filename );
00665     }
00666     if (!err)
00667     {
00668       QTextStream stream(&f);
00669       // Export to file
00670       stream << retval;
00671       f.close();
00672     }
00673   }
00674   else // use remote file
00675   {
00676     KTempFile tmpFile;
00677     if ( tmpFile.status() != 0 ) err = QString::fromLatin1( "Unable to get temporary file" );
00678     else
00679     {
00680       QTextStream *stream=tmpFile.textStream();
00681       *stream << retval;
00682       tmpFile.close();
00683       if (!KIO::NetAccess::upload( tmpFile.name(), rc.url, 0 )) err=QString::fromLatin1("Could not upload");
00684     }
00685   }
00686 
00687   return err;
00688 }
00689 
00690 //----------------------------------------------------------------------------
00691 // Routines that handle logging KArm history
00692 //
00693 
00694 //
00695 // public routines:
00696 //
00697 
00698 QString KarmStorage::addTask(const Task* task, const Task* parent)
00699 {
00700   KCal::Todo* todo;
00701   QString uid;
00702 
00703   todo = new KCal::Todo();
00704   if ( _calendar->addTodo( todo ) )
00705   {
00706     task->asTodo( todo  );
00707     if (parent)
00708       todo->setRelatedTo(_calendar->todo(parent->uid()));
00709     uid = todo->uid();
00710   }
00711   else
00712   {
00713     // Most likely a lock could not be pulled, although there are other
00714     // possiblities (like a really confused resource manager).
00715     uid = "";
00716   }
00717 
00718   return uid;
00719 }
00720 
00721 bool KarmStorage::removeTask(Task* task)
00722 {
00723 
00724   // delete history
00725   KCal::Event::List eventList = _calendar->rawEvents();
00726   for(KCal::Event::List::iterator i = eventList.begin();
00727       i != eventList.end();
00728       ++i)
00729   {
00730     //kdDebug(5970) << "KarmStorage::removeTask: "
00731     //  << (*i)->uid() << " - relatedToUid() "
00732     //  << (*i)->relatedToUid()
00733     //  << ", relatedTo() = " << (*i)->relatedTo() <<endl;
00734     if ( (*i)->relatedToUid() == task->uid()
00735         || ( (*i)->relatedTo()
00736             && (*i)->relatedTo()->uid() == task->uid()))
00737     {
00738       _calendar->deleteEvent(*i);
00739     }
00740   }
00741 
00742   // delete todo
00743   KCal::Todo *todo = _calendar->todo(task->uid());
00744   _calendar->deleteTodo(todo);
00745 
00746   // Save entire file
00747   saveCalendar();
00748 
00749   return true;
00750 }
00751 
00752 void KarmStorage::addComment(const Task* task, const QString& comment)
00753 {
00754 
00755   KCal::Todo* todo;
00756 
00757   todo = _calendar->todo(task->uid());
00758 
00759   // Do this to avoid compiler warnings about comment not being used.  once we
00760   // transition to using the addComment method, we need this second param.
00761   QString s = comment;
00762 
00763   // TODO: Use libkcal comments
00764   // todo->addComment(comment);
00765   // temporary
00766   todo->setDescription(task->comment());
00767 
00768   saveCalendar();
00769 }
00770 
00771 long KarmStorage::printTaskHistory (
00772         const Task               *task,
00773         const QMap<QString,long> &taskdaytotals,
00774         QMap<QString,long>       &daytotals,
00775         const QDate              &from,
00776         const QDate              &to,
00777         const int                level,
00778     vector <QString>         &matrix,
00779         const ReportCriteria     &rc)
00780 // to>=from is precondition
00781 {
00782   long ownline=linenr++; // the how many-th instance of this function is this
00783   long colrectot=0;      // colum where to write the task's total recursive time
00784   vector <QString> cell; // each line of the matrix is stored in an array of cells, one containing the recursive total
00785   long add;              // total recursive time of all subtasks
00786   QString delim = rc.delimiter;
00787   QString dquote = rc.quote;
00788   QString double_dquote = dquote + dquote;
00789   bool to_quote = true;
00790 
00791   const QString cr = QString::fromLatin1("\n");
00792   QString buf;
00793   QString daytaskkey, daykey;
00794   QDate day;
00795   long sum;
00796 
00797   if ( !task ) return 0;
00798 
00799   day = from;
00800   sum = 0;
00801   while (day <= to)
00802   {
00803     // write the time in seconds for the given task for the given day to s
00804     daykey = day.toString(QString::fromLatin1("yyyyMMdd"));
00805     daytaskkey = QString::fromLatin1("%1_%2")
00806       .arg(daykey)
00807       .arg(task->uid());
00808 
00809     if (taskdaytotals.contains(daytaskkey))
00810     {
00811       cell.push_back(QString::fromLatin1("%1")
00812         .arg(formatTime(taskdaytotals[daytaskkey]/60, rc.decimalMinutes)));
00813       sum += taskdaytotals[daytaskkey];  // in seconds
00814 
00815       if (daytotals.contains(daykey))
00816         daytotals.replace(daykey, daytotals[daykey]+taskdaytotals[daytaskkey]);
00817       else
00818         daytotals.insert(daykey, taskdaytotals[daytaskkey]);
00819     }
00820     cell.push_back(delim);
00821 
00822     day = day.addDays(1);
00823   }
00824 
00825   // Total for task
00826   cell.push_back(QString::fromLatin1("%1").arg(formatTime(sum/60, rc.decimalMinutes)));
00827 
00828   // room for the recursive total time (that cannot be calculated now)
00829   cell.push_back(delim);
00830   colrectot = cell.size();
00831   cell.push_back("???");
00832   cell.push_back(delim);
00833 
00834   // Task name
00835   for ( int i = level + 1; i > 0; i-- ) cell.push_back(delim);
00836 
00837   /*
00838   // CSV compliance
00839   // Surround the field with quotes if the field contains
00840   // a comma (delim) or a double quote
00841   to_quote = task->name().contains(delim) || task->name().contains(dquote);
00842   */
00843   to_quote = true;
00844   if ( to_quote) cell.push_back(dquote);
00845 
00846 
00847   // Double quotes replaced by a pair of consecutive double quotes
00848   cell.push_back(task->name().replace( dquote, double_dquote ));
00849 
00850   if ( to_quote) cell.push_back(dquote);
00851 
00852   cell.push_back(cr);
00853 
00854   add=0;
00855   for (Task* subTask = task->firstChild();
00856       subTask;
00857       subTask = subTask->nextSibling())
00858   {
00859     add += printTaskHistory( subTask, taskdaytotals, daytotals, from, to , level+1, matrix,
00860                       rc );
00861   }
00862   cell[colrectot]=(QString::fromLatin1("%1").arg(formatTime((add+sum)/60, rc.decimalMinutes )));
00863   for (unsigned int i=0; i < cell.size(); i++) matrix[ownline]+=cell[i];
00864   return add+sum;
00865 }
00866 
00867 QString KarmStorage::report( TaskView *taskview, const ReportCriteria &rc )
00868 {
00869   QString err;
00870   if ( rc.reportType == ReportCriteria::CSVHistoryExport )
00871       err = exportcsvHistory( taskview, rc.from, rc.to, rc );
00872   else if ( rc.reportType == ReportCriteria::CSVTotalsExport )
00873       err = exportcsvFile( taskview, rc );
00874   else {
00875       // hmmmm ... assert(0)?
00876   }
00877   return err;
00878 }
00879 
00880 // export history report as csv, all tasks X all dates in one block
00881 QString KarmStorage::exportcsvHistory ( TaskView      *taskview,
00882                                             const QDate   &from,
00883                                             const QDate   &to,
00884                                             const ReportCriteria &rc)
00885 {
00886   QString delim = rc.delimiter;
00887   const QString cr = QString::fromLatin1("\n");
00888   QString err;
00889 
00890   // below taken from timekard.cpp
00891   QString retval;
00892   QString taskhdr, totalhdr;
00893   QString line, buf;
00894   long sum;
00895 
00896   QValueList<HistoryEvent> events;
00897   QValueList<HistoryEvent>::iterator event;
00898   QMap<QString, long> taskdaytotals;
00899   QMap<QString, long> daytotals;
00900   QString daytaskkey, daykey;
00901   QDate day;
00902   QDate dayheading;
00903 
00904   // parameter-plausi
00905   if ( from > to )
00906   {
00907     err = QString::fromLatin1 (
00908             "'to' has to be a date later than or equal to 'from'.");
00909   }
00910 
00911   // header
00912   retval += i18n("Task History\n");
00913   retval += i18n("From %1 to %2")
00914     .arg(KGlobal::locale()->formatDate(from))
00915     .arg(KGlobal::locale()->formatDate(to));
00916   retval += cr;
00917   retval += i18n("Printed on: %1")
00918     .arg(KGlobal::locale()->formatDateTime(QDateTime::currentDateTime()));
00919   retval += cr;
00920 
00921   day=from;
00922   events = taskview->getHistory(from, to);
00923   taskdaytotals.clear();
00924   daytotals.clear();
00925 
00926   // Build lookup dictionary used to output data in table cells.  keys are
00927   // in this format: YYYYMMDD_NNNNNN, where Y = year, M = month, d = day and
00928   // NNNNN = the VTODO uid.  The value is the total seconds logged against
00929   // that task on that day.  Note the UID is the todo id, not the event id,
00930   // so times are accumulated for each task.
00931   for (event = events.begin(); event != events.end(); ++event)
00932   {
00933     daykey = (*event).start().date().toString(QString::fromLatin1("yyyyMMdd"));
00934     daytaskkey = QString(QString::fromLatin1("%1_%2"))
00935         .arg(daykey)
00936         .arg((*event).todoUid());
00937 
00938     if (taskdaytotals.contains(daytaskkey))
00939         taskdaytotals.replace(daytaskkey,
00940                 taskdaytotals[daytaskkey] + (*event).duration());
00941     else
00942         taskdaytotals.insert(daytaskkey, (*event).duration());
00943   }
00944 
00945   // day headings
00946   dayheading = from;
00947   while ( dayheading <= to )
00948   {
00949     // Use ISO 8601 format for date.
00950     retval += dayheading.toString(QString::fromLatin1("yyyy-MM-dd"));
00951     retval += delim;
00952     dayheading=dayheading.addDays(1);
00953   }
00954   retval += i18n("Sum") + delim + i18n("Total Sum") + delim + i18n("Task Hierarchy");
00955   retval += cr;
00956   retval += line;
00957 
00958   // the tasks
00959   vector <QString> matrix;
00960   linenr=0;
00961   for (int i=0; i<=taskview->count()+1; i++) matrix.push_back("");
00962   if (events.empty())
00963   {
00964     retval += i18n("  No hours logged.");
00965   }
00966   else
00967   {
00968     if ( rc.allTasks )
00969     {
00970       for ( Task* task= taskview->item_at_index(0);
00971             task; task= task->nextSibling() )
00972       {
00973         printTaskHistory( task, taskdaytotals, daytotals, from, to, 0,
00974                           matrix, rc );
00975       }
00976     }
00977     else
00978     {
00979       printTaskHistory( taskview->current_item(), taskdaytotals, daytotals,
00980                         from, to, 0, matrix, rc );
00981     }
00982     for (unsigned int i=0; i<matrix.size(); i++) retval+=matrix[i];
00983     retval += line;
00984 
00985     // totals
00986     sum = 0;
00987     day = from;
00988     while (day<=to)
00989     {
00990       daykey = day.toString(QString::fromLatin1("yyyyMMdd"));
00991 
00992       if (daytotals.contains(daykey))
00993       {
00994         retval += QString::fromLatin1("%1")
00995             .arg(formatTime(daytotals[daykey]/60, rc.decimalMinutes));
00996         sum += daytotals[daykey];  // in seconds
00997       }
00998       retval += delim;
00999       day = day.addDays(1);
01000     }
01001 
01002     retval += QString::fromLatin1("%1%2%3%4")
01003         .arg( formatTime( sum/60, rc.decimalMinutes ) )
01004         .arg( delim ).arg( delim )
01005         .arg( i18n( "Total" ) );
01006   }
01007 
01008   // above taken from timekard.cpp
01009 
01010   // save, either locally or remote
01011 
01012   if ((rc.url.isLocalFile()) || (!rc.url.url().contains("/")))
01013   {
01014     QString filename=rc.url.path();
01015     if (filename.isEmpty()) filename=rc.url.url();
01016     QFile f( filename );
01017     if( !f.open( IO_WriteOnly ) ) {
01018         err = i18n( "Could not open \"%1\"." ).arg( filename );
01019     }
01020     if (!err)
01021     {
01022       QTextStream stream(&f);
01023       // Export to file
01024       stream << retval;
01025       f.close();
01026     }
01027   }
01028   else // use remote file
01029   {
01030     KTempFile tmpFile;
01031     if ( tmpFile.status() != 0 )
01032     {
01033       err = QString::fromLatin1( "Unable to get temporary file" );
01034     }
01035     else
01036     {
01037       QTextStream *stream=tmpFile.textStream();
01038       *stream << retval;
01039       tmpFile.close();
01040       if (!KIO::NetAccess::upload( tmpFile.name(), rc.url, 0 )) err=QString::fromLatin1("Could not upload");
01041     }
01042   }
01043   return err;
01044 }
01045 
01046 void KarmStorage::stopTimer(const Task* task)
01047 {
01048   long delta = task->startTime().secsTo(QDateTime::currentDateTime());
01049   changeTime(task, delta);
01050 }
01051 
01052 bool KarmStorage::bookTime(const Task* task,
01053                            const QDateTime& startDateTime,
01054                            const long durationInSeconds)
01055 {
01056   // Ignores preferences setting re: logging history.
01057   KCal::Event* e;
01058   QDateTime end;
01059 
01060   e = baseEvent( task );
01061   e->setDtStart( startDateTime );
01062   e->setDtEnd( startDateTime.addSecs( durationInSeconds ) );
01063 
01064   // Use a custom property to keep a record of negative durations
01065   e->setCustomProperty( kapp->instanceName(),
01066       QCString("duration"),
01067       QString::number(durationInSeconds));
01068 
01069   return _calendar->addEvent(e);
01070 }
01071 
01072 void KarmStorage::changeTime(const Task* task, const long deltaSeconds)
01073 {
01074   KCal::Event* e;
01075   QDateTime end;
01076 
01077   // Don't write events (with timer start/stop duration) if user has turned
01078   // this off in the settings dialog.
01079   if ( ! task->taskView()->preferences()->logging() ) return;
01080 
01081   e = baseEvent(task);
01082 
01083   // Don't use duration, as ICalFormatImpl::writeIncidence never writes a
01084   // duration, even though it looks like it's used in event.cpp.
01085   end = task->startTime();
01086   if ( deltaSeconds > 0 ) end = task->startTime().addSecs(deltaSeconds);
01087   e->setDtEnd(end);
01088 
01089   // Use a custom property to keep a record of negative durations
01090   e->setCustomProperty( kapp->instanceName(),
01091       QCString("duration"),
01092       QString::number(deltaSeconds));
01093 
01094   _calendar->addEvent(e);
01095 
01096   // This saves the entire iCal file each time, which isn't efficient but
01097   // ensures no data loss.  A faster implementation would be to append events
01098   // to a file, and then when KArm closes, append the data in this file to the
01099   // iCal file.
01100   //
01101   // Meanwhile, we simply use a timer to delay the full-saving until the GUI
01102   // has updated, for better user feedback. Feel free to get rid of this
01103   // if/when implementing the faster saving (DF).
01104   task->taskView()->scheduleSave();
01105 }
01106 
01107 
01108 KCal::Event* KarmStorage::baseEvent(const Task * task)
01109 {
01110   KCal::Event* e;
01111   QStringList categories;
01112 
01113   e = new KCal::Event;
01114   e->setSummary(task->name());
01115 
01116   // Can't use setRelatedToUid()--no error, but no RelatedTo written to disk
01117   e->setRelatedTo(_calendar->todo(task->uid()));
01118 
01119   // Debugging: some events where not getting a related-to field written.
01120   assert(e->relatedTo()->uid() == task->uid());
01121 
01122   // Have to turn this off to get datetimes in date fields.
01123   e->setFloats(false);
01124   e->setDtStart(task->startTime());
01125 
01126   // So someone can filter this mess out of their calendar display
01127   categories.append(i18n("KArm"));
01128   e->setCategories(categories);
01129 
01130   return e;
01131 }
01132 
01133 HistoryEvent::HistoryEvent(QString uid, QString name, long duration,
01134         QDateTime start, QDateTime stop, QString todoUid)
01135 {
01136   _uid = uid;
01137   _name = name;
01138   _duration = duration;
01139   _start = start;
01140   _stop = stop;
01141   _todoUid = todoUid;
01142 }
01143 
01144 
01145 QValueList<HistoryEvent> KarmStorage::getHistory(const QDate& from,
01146     const QDate& to)
01147 {
01148   QValueList<HistoryEvent> retval;
01149   QStringList processed;
01150   KCal::Event::List events;
01151   KCal::Event::List::iterator event;
01152   QString duration;
01153 
01154   for(QDate d = from; d <= to; d = d.addDays(1))
01155   {
01156     events = _calendar->rawEventsForDate( d );
01157     for (event = events.begin(); event != events.end(); ++event)
01158     {
01159 
01160       // KArm events have the custom property X-KDE-Karm-duration
01161       if (! processed.contains( (*event)->uid()))
01162       {
01163         // If an event spans multiple days, CalendarLocal::rawEventsForDate
01164         // will return the same event on both days.  To avoid double-counting
01165         // such events, we (arbitrarily) attribute the hours from both days on
01166         // the first day.  This mis-reports the actual time spent, but it is
01167         // an easy fix for a (hopefully) rare situation.
01168         processed.append( (*event)->uid());
01169 
01170         duration = (*event)->customProperty(kapp->instanceName(),
01171             QCString("duration"));
01172         if ( ! duration.isNull() )
01173         {
01174           if ( (*event)->relatedTo()
01175               &&  ! (*event)->relatedTo()->uid().isEmpty() )
01176           {
01177             retval.append(HistoryEvent(
01178                 (*event)->uid(),
01179                 (*event)->summary(),
01180                 duration.toLong(),
01181                 (*event)->dtStart(),
01182                 (*event)->dtEnd(),
01183                 (*event)->relatedTo()->uid()
01184                 ));
01185           }
01186           else
01187             // Something is screwy with the ics file, as this KArm history event
01188             // does not have a todo related to it.  Could have been deleted
01189             // manually?  We'll continue with report on with report ...
01190             kdDebug(5970) << "KarmStorage::getHistory(): "
01191               << "The event " << (*event)->uid()
01192               << " is not related to a todo.  Dropped." << endl;
01193         }
01194       }
01195     }
01196   }
01197 
01198   return retval;
01199 }
01200 
01201 bool KarmStorage::remoteResource( const QString& file ) const
01202 {
01203   QString f = file.lower();
01204   bool rval = f.startsWith( "http://" ) || f.startsWith( "ftp://" );
01205 
01206   kdDebug(5970) << "KarmStorage::remoteResource( " << file << " ) returns " << rval  << endl;
01207   return rval;
01208 }
01209 
01210 bool KarmStorage::saveCalendar()
01211 {
01212   kdDebug(5970) << "KarmStorage::saveCalendar" << endl;
01213 
01214   KABC::Lock *lock = _calendar->lock();
01215   if ( !lock || !lock->lock() )
01216     return false;
01217 
01218   if ( _calendar && _calendar->save() ) {
01219     lock->unlock();
01220     return true;
01221   }
01222 
01223   lock->unlock();
01224   return false;
01225 }
KDE Home | KDE Accessibility Home | Description of Access Keys