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::icalfile()
00217 {
00218   kdDebug(5970) << "Entering KarmStorage::icalfile" << endl;
00219   return _icalfile;
00220 }
00221 
00222 QString KarmStorage::buildTaskView(KCal::ResourceCalendar *rc, TaskView *view)
00223 // makes *view contain the tasks out of *rc.
00224 {
00225   QString err;
00226   KCal::Todo::List todoList;
00227   KCal::Todo::List::ConstIterator todo;
00228   QDict< Task > map;
00229   vector<QString> runningTasks;
00230   vector<QDateTime> startTimes;
00231 
00232   // remember tasks that are running and their start times
00233   for ( int i=0; i<view->count(); i++)
00234   {
00235     if ( view->item_at_index(i)->isRunning() )
00236     {
00237       runningTasks.push_back( view->item_at_index(i)->uid() );
00238       startTimes.push_back( view->item_at_index(i)->lastStart() );
00239     }
00240   }
00241 
00242   //view->stopAllTimers();
00243   // delete old tasks
00244   while (view->item_at_index(0)) view->item_at_index(0)->cut();
00245 
00246   // 1. insert tasks form rc into taskview
00247   // 1.1. Build dictionary to look up Task object from Todo uid.  Each task is a
00248   // QListViewItem, and is initially added with the view as the parent.
00249   todoList = rc->rawTodos();
00250   for( todo = todoList.begin(); todo != todoList.end(); ++todo )
00251   {
00252     Task* task = new Task(*todo, view);
00253     map.insert( (*todo)->uid(), task );
00254     view->setRootIsDecorated(true);
00255     task->setPixmapProgress();
00256   }
00257 
00258   // 1.1. Load each task under it's parent task.
00259   for( todo = todoList.begin(); todo != todoList.end(); ++todo )
00260   {
00261     Task* task = map.find( (*todo)->uid() );
00262 
00263     // No relatedTo incident just means this is a top-level task.
00264     if ( (*todo)->relatedTo() )
00265     {
00266       Task* newParent = map.find( (*todo)->relatedToUid() );
00267 
00268       // Complete the loading but return a message
00269       if ( !newParent )
00270         err = i18n("Error loading \"%1\": could not find parent (uid=%2)")
00271           .arg(task->name())
00272           .arg((*todo)->relatedToUid());
00273 
00274       if (!err) task->move( newParent);
00275     }
00276   }
00277 
00278   view->clearActiveTasks();
00279   // restart tasks that have been running with their start times
00280   for ( int i=0; i<view->count(); i++)
00281   {
00282     for ( unsigned int n=0; n<runningTasks.size(); n++)
00283     {
00284       if ( runningTasks[n] == view->item_at_index(i)->uid() )
00285       {
00286         view->startTimerFor( view->item_at_index(i), startTimes[n] ); 
00287       }
00288     }
00289   }
00290   
00291   view->refresh();
00292 
00293   return err;
00294 }
00295 
00296 void KarmStorage::closeStorage(TaskView* view)
00297 {
00298   if ( _calendar )
00299   {
00300     _calendar->close();
00301     delete _calendar;
00302     _calendar = 0;
00303 
00304     view->clear();
00305   }
00306 }
00307 
00308 QString KarmStorage::save(TaskView* taskview)
00309 {
00310   kdDebug(5970) << "entering KarmStorage::save" << endl;
00311   QString err;
00312 
00313   QPtrStack< KCal::Todo > parents;
00314 
00315   for (Task* task=taskview->first_child(); task; task = task->nextSibling())
00316   {
00317     err=writeTaskAsTodo(task, 1, parents );
00318   }
00319 
00320   if ( !saveCalendar() )
00321   {
00322     err="Could not save";
00323   }
00324 
00325   if ( err.isEmpty() )
00326   {
00327     kdDebug(5970)
00328       << "KarmStorage::save : wrote "
00329       << taskview->count() << " tasks to " << _icalfile << endl;
00330   }
00331   else
00332   {
00333     kdWarning(5970) << "KarmStorage::save : " << err << endl;
00334   }
00335 
00336   return err;
00337 }
00338 
00339 QString KarmStorage::writeTaskAsTodo(Task* task, const int level,
00340     QPtrStack< KCal::Todo >& parents )
00341 {
00342   QString err;
00343   KCal::Todo* todo;
00344 
00345   todo = _calendar->todo(task->uid());
00346   if ( !todo )
00347   {
00348     kdDebug(5970) << "Could not get todo from calendar" << endl;
00349     return "Could not get todo from calendar";
00350   }
00351   task->asTodo(todo);
00352   if ( !parents.isEmpty() ) todo->setRelatedTo( parents.top() );
00353   parents.push( todo );
00354 
00355   for ( Task* nextTask = task->firstChild(); nextTask;
00356         nextTask = nextTask->nextSibling() )
00357   {
00358     err = writeTaskAsTodo(nextTask, level+1, parents );
00359   }
00360 
00361   parents.pop();
00362   return err;
00363 }
00364 
00365 bool KarmStorage::isEmpty()
00366 {
00367   KCal::Todo::List todoList;
00368 
00369   todoList = _calendar->rawTodos();
00370   return todoList.empty();
00371 }
00372 
00373 bool KarmStorage::isNewStorage(const Preferences* preferences) const
00374 {
00375   if ( !_icalfile.isNull() ) return preferences->iCalFile() != _icalfile;
00376   else return false;
00377 }
00378 
00379 //----------------------------------------------------------------------------
00380 // Routines that handle legacy flat file format.
00381 // These only stored total and session times.
00382 //
00383 
00384 QString KarmStorage::loadFromFlatFile(TaskView* taskview,
00385     const QString& filename)
00386 {
00387   QString err;
00388 
00389   kdDebug(5970)
00390     << "KarmStorage::loadFromFlatFile: " << filename << endl;
00391 
00392   QFile f(filename);
00393   if( !f.exists() )
00394     err = i18n("File \"%1\" not found.").arg(filename);
00395 
00396   if (!err)
00397   {
00398     if( !f.open( IO_ReadOnly ) )
00399       err = i18n("Could not open \"%1\".").arg(filename);
00400   }
00401 
00402   if (!err)
00403   {
00404 
00405     QString line;
00406 
00407     QPtrStack<Task> stack;
00408     Task *task;
00409 
00410     QTextStream stream(&f);
00411 
00412     while( !stream.atEnd() ) {
00413       // lukas: this breaks for non-latin1 chars!!!
00414       // if ( file.readLine( line, T_LINESIZE ) == 0 )
00415       //   break;
00416 
00417       line = stream.readLine();
00418       kdDebug(5970) << "DEBUG: line: " << line << "\n";
00419 
00420       if (line.isNull())
00421         break;
00422 
00423       long minutes;
00424       int level;
00425       QString name;
00426       DesktopList desktopList;
00427       if (!parseLine(line, &minutes, &name, &level, &desktopList))
00428         continue;
00429 
00430       unsigned int stackLevel = stack.count();
00431       for (unsigned int i = level; i<=stackLevel ; i++) {
00432         stack.pop();
00433       }
00434 
00435       if (level == 1) {
00436         kdDebug(5970) << "KarmStorage::loadFromFlatFile - toplevel task: "
00437           << name << " min: " << minutes << "\n";
00438         task = new Task(name, minutes, 0, desktopList, taskview);
00439         task->setUid(addTask(task, 0));
00440       }
00441       else {
00442         Task *parent = stack.top();
00443         kdDebug(5970) << "KarmStorage::loadFromFlatFile - task: " << name
00444             << " min: " << minutes << " parent" << parent->name() << "\n";
00445         task = new Task(name, minutes, 0, desktopList, parent);
00446 
00447         task->setUid(addTask(task, parent));
00448 
00449         // Legacy File Format (!):
00450         parent->changeTimes(0, -minutes);
00451         taskview->setRootIsDecorated(true);
00452         parent->setOpen(true);
00453       }
00454       if (!task->uid().isNull())
00455         stack.push(task);
00456       else
00457         delete task;
00458     }
00459 
00460     f.close();
00461 
00462   }
00463 
00464   return err;
00465 }
00466 
00467 QString KarmStorage::loadFromFlatFileCumulative(TaskView* taskview,
00468     const QString& filename)
00469 {
00470   QString err = loadFromFlatFile(taskview, filename);
00471   if (!err)
00472   {
00473     for (Task* task = taskview->first_child(); task;
00474         task = task->nextSibling())
00475     {
00476       adjustFromLegacyFileFormat(task);
00477     }
00478   }
00479   return err;
00480 }
00481 
00482 bool KarmStorage::parseLine(QString line, long *time, QString *name,
00483     int *level, DesktopList* desktopList)
00484 {
00485   if (line.find('#') == 0) {
00486     // A comment line
00487     return false;
00488   }
00489 
00490   int index = line.find('\t');
00491   if (index == -1) {
00492     // This doesn't seem like a valid record
00493     return false;
00494   }
00495 
00496   QString levelStr = line.left(index);
00497   QString rest = line.remove(0,index+1);
00498 
00499   index = rest.find('\t');
00500   if (index == -1) {
00501     // This doesn't seem like a valid record
00502     return false;
00503   }
00504 
00505   QString timeStr = rest.left(index);
00506   rest = rest.remove(0,index+1);
00507 
00508   bool ok;
00509 
00510   index = rest.find('\t'); // check for optional desktops string
00511   if (index >= 0) {
00512     *name = rest.left(index);
00513     QString deskLine = rest.remove(0,index+1);
00514 
00515     // now transform the ds string (e.g. "3", or "1,4,5") into
00516     // an DesktopList
00517     QString ds;
00518     int d;
00519     int commaIdx = deskLine.find(',');
00520     while (commaIdx >= 0) {
00521       ds = deskLine.left(commaIdx);
00522       d = ds.toInt(&ok);
00523       if (!ok)
00524         return false;
00525 
00526       desktopList->push_back(d);
00527       deskLine.remove(0,commaIdx+1);
00528       commaIdx = deskLine.find(',');
00529     }
00530 
00531     d = deskLine.toInt(&ok);
00532 
00533     if (!ok)
00534       return false;
00535 
00536     desktopList->push_back(d);
00537   }
00538   else {
00539     *name = rest.remove(0,index+1);
00540   }
00541 
00542   *time = timeStr.toLong(&ok);
00543 
00544   if (!ok) {
00545     // the time field was not a number
00546     return false;
00547   }
00548   *level = levelStr.toInt(&ok);
00549   if (!ok) {
00550     // the time field was not a number
00551     return false;
00552   }
00553 
00554   return true;
00555 }
00556 
00557 void KarmStorage::adjustFromLegacyFileFormat(Task* task)
00558 {
00559   // unless the parent is the listView
00560   if ( task->parent() )
00561     task->parent()->changeTimes(-task->sessionTime(), -task->time());
00562 
00563   // traverse depth first -
00564   // as soon as we're in a leaf, we'll substract it's time from the parent
00565   // then, while descending back we'll do the same for each node untill
00566   // we reach the root
00567   for ( Task* subtask = task->firstChild(); subtask;
00568       subtask = subtask->nextSibling() )
00569     adjustFromLegacyFileFormat(subtask);
00570 }
00571 
00572 //----------------------------------------------------------------------------
00573 // Routines that handle Comma-Separated Values export file format.
00574 //
00575 QString KarmStorage::exportcsvFile( TaskView *taskview,
00576                                     const ReportCriteria &rc )
00577 {
00578   QString delim = rc.delimiter;
00579   QString dquote = rc.quote;
00580   QString double_dquote = dquote + dquote;
00581   bool to_quote = true;
00582 
00583   QString err;
00584   Task* task;
00585   int maxdepth=0;
00586 
00587   kdDebug(5970)
00588     << "KarmStorage::exportcsvFile: " << rc.url << endl;
00589 
00590   QString title = i18n("Export Progress");
00591   KProgressDialog dialog( taskview, 0, title );
00592   dialog.setAutoClose( true );
00593   dialog.setAllowCancel( true );
00594   dialog.progressBar()->setTotalSteps( 2 * taskview->count() );
00595 
00596   // The default dialog was not displaying all the text in the title bar.
00597   int width = taskview->fontMetrics().width(title) * 3;
00598   QSize dialogsize;
00599   dialogsize.setWidth(width);
00600   dialog.setInitialSize( dialogsize, true );
00601 
00602   if ( taskview->count() > 1 ) dialog.show();
00603 
00604   QString retval;
00605 
00606   // Find max task depth
00607   int tasknr = 0;
00608   while ( tasknr < taskview->count() && !dialog.wasCancelled() )
00609   {
00610     dialog.progressBar()->advance( 1 );
00611     if ( tasknr % 15 == 0 ) kapp->processEvents(); // repainting is slow
00612     if ( taskview->item_at_index(tasknr)->depth() > maxdepth )
00613       maxdepth = taskview->item_at_index(tasknr)->depth();
00614     tasknr++;
00615   }
00616 
00617   // Export to file
00618   tasknr = 0;
00619   while ( tasknr < taskview->count() && !dialog.wasCancelled() )
00620   {
00621     task = taskview->item_at_index( tasknr );
00622     dialog.progressBar()->advance( 1 );
00623     if ( tasknr % 15 == 0 ) kapp->processEvents();
00624 
00625     // indent the task in the csv-file:
00626     for ( int i=0; i < task->depth(); ++i ) retval += delim;
00627 
00628     /*
00629     // CSV compliance
00630     // Surround the field with quotes if the field contains
00631     // a comma (delim) or a double quote
00632     if (task->name().contains(delim) || task->name().contains(dquote))
00633       to_quote = true;
00634     else
00635       to_quote = false;
00636     */
00637     to_quote = true;
00638 
00639     if (to_quote)
00640       retval += dquote;
00641 
00642     // Double quotes replaced by a pair of consecutive double quotes
00643     retval += task->name().replace( dquote, double_dquote );
00644 
00645     if (to_quote)
00646       retval += dquote;
00647 
00648     // maybe other tasks are more indented, so to align the columns:
00649     for ( int i = 0; i < maxdepth - task->depth(); ++i ) retval += delim;
00650 
00651     retval += delim + formatTime( task->sessionTime(),
00652                                    rc.decimalMinutes )
00653            + delim + formatTime( task->time(),
00654                                    rc.decimalMinutes )
00655            + delim + formatTime( task->totalSessionTime(),
00656                                    rc.decimalMinutes )
00657            + delim + formatTime( task->totalTime(),
00658                                    rc.decimalMinutes )
00659            + "\n";
00660     tasknr++;
00661   }
00662 
00663   // save, either locally or remote
00664   if ((rc.url.isLocalFile()) || (!rc.url.url().contains("/")))
00665   {
00666     QString filename=rc.url.path();
00667     if (filename.isEmpty()) filename=rc.url.url();
00668     QFile f( filename );
00669     if( !f.open( IO_WriteOnly ) ) {
00670         err = i18n( "Could not open \"%1\"." ).arg( filename );
00671     }
00672     if (!err)
00673     {
00674       QTextStream stream(&f);
00675       // Export to file
00676       stream << retval;
00677       f.close();
00678     }
00679   }
00680   else // use remote file
00681   {
00682     KTempFile tmpFile;
00683     if ( tmpFile.status() != 0 ) err = QString::fromLatin1( "Unable to get temporary file" );
00684     else
00685     {
00686       QTextStream *stream=tmpFile.textStream();
00687       *stream << retval;
00688       tmpFile.close();
00689       if (!KIO::NetAccess::upload( tmpFile.name(), rc.url, 0 )) err=QString::fromLatin1("Could not upload");
00690     }
00691   }
00692 
00693   return err;
00694 }
00695 
00696 //----------------------------------------------------------------------------
00697 // Routines that handle logging KArm history
00698 //
00699 
00700 //
00701 // public routines:
00702 //
00703 
00704 QString KarmStorage::addTask(const Task* task, const Task* parent)
00705 {
00706   KCal::Todo* todo;
00707   QString uid;
00708 
00709   todo = new KCal::Todo();
00710   if ( _calendar->addTodo( todo ) )
00711   {
00712     task->asTodo( todo  );
00713     if (parent)
00714       todo->setRelatedTo(_calendar->todo(parent->uid()));
00715     uid = todo->uid();
00716   }
00717   else
00718   {
00719     // Most likely a lock could not be pulled, although there are other
00720     // possiblities (like a really confused resource manager).
00721     uid = "";
00722   }
00723 
00724   return uid;
00725 }
00726 
00727 bool KarmStorage::removeTask(Task* task)
00728 {
00729 
00730   // delete history
00731   KCal::Event::List eventList = _calendar->rawEvents();
00732   for(KCal::Event::List::iterator i = eventList.begin();
00733       i != eventList.end();
00734       ++i)
00735   {
00736     //kdDebug(5970) << "KarmStorage::removeTask: "
00737     //  << (*i)->uid() << " - relatedToUid() "
00738     //  << (*i)->relatedToUid()
00739     //  << ", relatedTo() = " << (*i)->relatedTo() <<endl;
00740     if ( (*i)->relatedToUid() == task->uid()
00741         || ( (*i)->relatedTo()
00742             && (*i)->relatedTo()->uid() == task->uid()))
00743     {
00744       _calendar->deleteEvent(*i);
00745     }
00746   }
00747 
00748   // delete todo
00749   KCal::Todo *todo = _calendar->todo(task->uid());
00750   _calendar->deleteTodo(todo);
00751 
00752   // Save entire file
00753   saveCalendar();
00754 
00755   return true;
00756 }
00757 
00758 void KarmStorage::addComment(const Task* task, const QString& comment)
00759 {
00760 
00761   KCal::Todo* todo;
00762 
00763   todo = _calendar->todo(task->uid());
00764 
00765   // Do this to avoid compiler warnings about comment not being used.  once we
00766   // transition to using the addComment method, we need this second param.
00767   QString s = comment;
00768 
00769   // TODO: Use libkcal comments
00770   // todo->addComment(comment);
00771   // temporary
00772   todo->setDescription(task->comment());
00773 
00774   saveCalendar();
00775 }
00776 
00777 long KarmStorage::printTaskHistory (
00778         const Task               *task,
00779         const QMap<QString,long> &taskdaytotals,
00780         QMap<QString,long>       &daytotals,
00781         const QDate              &from,
00782         const QDate              &to,
00783         const int                level,
00784     vector <QString>         &matrix,
00785         const ReportCriteria     &rc)
00786 // to>=from is precondition
00787 {
00788   long ownline=linenr++; // the how many-th instance of this function is this
00789   long colrectot=0;      // colum where to write the task's total recursive time
00790   vector <QString> cell; // each line of the matrix is stored in an array of cells, one containing the recursive total
00791   long add;              // total recursive time of all subtasks
00792   QString delim = rc.delimiter;
00793   QString dquote = rc.quote;
00794   QString double_dquote = dquote + dquote;
00795   bool to_quote = true;
00796 
00797   const QString cr = QString::fromLatin1("\n");
00798   QString buf;
00799   QString daytaskkey, daykey;
00800   QDate day;
00801   long sum;
00802 
00803   if ( !task ) return 0;
00804 
00805   day = from;
00806   sum = 0;
00807   while (day <= to)
00808   {
00809     // write the time in seconds for the given task for the given day to s
00810     daykey = day.toString(QString::fromLatin1("yyyyMMdd"));
00811     daytaskkey = QString::fromLatin1("%1_%2")
00812       .arg(daykey)
00813       .arg(task->uid());
00814 
00815     if (taskdaytotals.contains(daytaskkey))
00816     {
00817       cell.push_back(QString::fromLatin1("%1")
00818         .arg(formatTime(taskdaytotals[daytaskkey]/60, rc.decimalMinutes)));
00819       sum += taskdaytotals[daytaskkey];  // in seconds
00820 
00821       if (daytotals.contains(daykey))
00822         daytotals.replace(daykey, daytotals[daykey]+taskdaytotals[daytaskkey]);
00823       else
00824         daytotals.insert(daykey, taskdaytotals[daytaskkey]);
00825     }
00826     cell.push_back(delim);
00827 
00828     day = day.addDays(1);
00829   }
00830 
00831   // Total for task
00832   cell.push_back(QString::fromLatin1("%1").arg(formatTime(sum/60, rc.decimalMinutes)));
00833 
00834   // room for the recursive total time (that cannot be calculated now)
00835   cell.push_back(delim);
00836   colrectot = cell.size();
00837   cell.push_back("???");
00838   cell.push_back(delim);
00839 
00840   // Task name
00841   for ( int i = level + 1; i > 0; i-- ) cell.push_back(delim);
00842 
00843   /*
00844   // CSV compliance
00845   // Surround the field with quotes if the field contains
00846   // a comma (delim) or a double quote
00847   to_quote = task->name().contains(delim) || task->name().contains(dquote);
00848   */
00849   to_quote = true;
00850   if ( to_quote) cell.push_back(dquote);
00851 
00852 
00853   // Double quotes replaced by a pair of consecutive double quotes
00854   cell.push_back(task->name().replace( dquote, double_dquote ));
00855 
00856   if ( to_quote) cell.push_back(dquote);
00857 
00858   cell.push_back(cr);
00859 
00860   add=0;
00861   for (Task* subTask = task->firstChild();
00862       subTask;
00863       subTask = subTask->nextSibling())
00864   {
00865     add += printTaskHistory( subTask, taskdaytotals, daytotals, from, to , level+1, matrix,
00866                       rc );
00867   }
00868   cell[colrectot]=(QString::fromLatin1("%1").arg(formatTime((add+sum)/60, rc.decimalMinutes )));
00869   for (unsigned int i=0; i < cell.size(); i++) matrix[ownline]+=cell[i];
00870   return add+sum;
00871 }
00872 
00873 QString KarmStorage::report( TaskView *taskview, const ReportCriteria &rc )
00874 {
00875   QString err;
00876   if ( rc.reportType == ReportCriteria::CSVHistoryExport )
00877       err = exportcsvHistory( taskview, rc.from, rc.to, rc );
00878   else if ( rc.reportType == ReportCriteria::CSVTotalsExport )
00879       err = exportcsvFile( taskview, rc );
00880   else {
00881       // hmmmm ... assert(0)?
00882   }
00883   return err;
00884 }
00885 
00886 // export history report as csv, all tasks X all dates in one block
00887 QString KarmStorage::exportcsvHistory ( TaskView      *taskview,
00888                                             const QDate   &from,
00889                                             const QDate   &to,
00890                                             const ReportCriteria &rc)
00891 {
00892   QString delim = rc.delimiter;
00893   const QString cr = QString::fromLatin1("\n");
00894   QString err;
00895 
00896   // below taken from timekard.cpp
00897   QString retval;
00898   QString taskhdr, totalhdr;
00899   QString line, buf;
00900   long sum;
00901 
00902   QValueList<HistoryEvent> events;
00903   QValueList<HistoryEvent>::iterator event;
00904   QMap<QString, long> taskdaytotals;
00905   QMap<QString, long> daytotals;
00906   QString daytaskkey, daykey;
00907   QDate day;
00908   QDate dayheading;
00909 
00910   // parameter-plausi
00911   if ( from > to )
00912   {
00913     err = QString::fromLatin1 (
00914             "'to' has to be a date later than or equal to 'from'.");
00915   }
00916 
00917   // header
00918   retval += i18n("Task History\n");
00919   retval += i18n("From %1 to %2")
00920     .arg(KGlobal::locale()->formatDate(from))
00921     .arg(KGlobal::locale()->formatDate(to));
00922   retval += cr;
00923   retval += i18n("Printed on: %1")
00924     .arg(KGlobal::locale()->formatDateTime(QDateTime::currentDateTime()));
00925   retval += cr;
00926 
00927   day=from;
00928   events = taskview->getHistory(from, to);
00929   taskdaytotals.clear();
00930   daytotals.clear();
00931 
00932   // Build lookup dictionary used to output data in table cells.  keys are
00933   // in this format: YYYYMMDD_NNNNNN, where Y = year, M = month, d = day and
00934   // NNNNN = the VTODO uid.  The value is the total seconds logged against
00935   // that task on that day.  Note the UID is the todo id, not the event id,
00936   // so times are accumulated for each task.
00937   for (event = events.begin(); event != events.end(); ++event)
00938   {
00939     daykey = (*event).start().date().toString(QString::fromLatin1("yyyyMMdd"));
00940     daytaskkey = QString(QString::fromLatin1("%1_%2"))
00941         .arg(daykey)
00942         .arg((*event).todoUid());
00943 
00944     if (taskdaytotals.contains(daytaskkey))
00945         taskdaytotals.replace(daytaskkey,
00946                 taskdaytotals[daytaskkey] + (*event).duration());
00947     else
00948         taskdaytotals.insert(daytaskkey, (*event).duration());
00949   }
00950 
00951   // day headings
00952   dayheading = from;
00953   while ( dayheading <= to )
00954   {
00955     // Use ISO 8601 format for date.
00956     retval += dayheading.toString(QString::fromLatin1("yyyy-MM-dd"));
00957     retval += delim;
00958     dayheading=dayheading.addDays(1);
00959   }
00960   retval += i18n("Sum") + delim + i18n("Total Sum") + delim + i18n("Task Hierarchy");
00961   retval += cr;
00962   retval += line;
00963 
00964   // the tasks
00965   vector <QString> matrix;
00966   linenr=0;
00967   for (int i=0; i<=taskview->count()+1; i++) matrix.push_back("");
00968   if (events.empty())
00969   {
00970     retval += i18n("  No hours logged.");
00971   }
00972   else
00973   {
00974     if ( rc.allTasks )
00975     {
00976       for ( Task* task= taskview->item_at_index(0);
00977             task; task= task->nextSibling() )
00978       {
00979         printTaskHistory( task, taskdaytotals, daytotals, from, to, 0,
00980                           matrix, rc );
00981       }
00982     }
00983     else
00984     {
00985       printTaskHistory( taskview->current_item(), taskdaytotals, daytotals,
00986                         from, to, 0, matrix, rc );
00987     }
00988     for (unsigned int i=0; i<matrix.size(); i++) retval+=matrix[i];
00989     retval += line;
00990 
00991     // totals
00992     sum = 0;
00993     day = from;
00994     while (day<=to)
00995     {
00996       daykey = day.toString(QString::fromLatin1("yyyyMMdd"));
00997 
00998       if (daytotals.contains(daykey))
00999       {
01000         retval += QString::fromLatin1("%1")
01001             .arg(formatTime(daytotals[daykey]/60, rc.decimalMinutes));
01002         sum += daytotals[daykey];  // in seconds
01003       }
01004       retval += delim;
01005       day = day.addDays(1);
01006     }
01007 
01008     retval += QString::fromLatin1("%1%2%3%4")
01009         .arg( formatTime( sum/60, rc.decimalMinutes ) )
01010         .arg( delim ).arg( delim )
01011         .arg( i18n( "Total" ) );
01012   }
01013 
01014   // above taken from timekard.cpp
01015 
01016   // save, either locally or remote
01017 
01018   if ((rc.url.isLocalFile()) || (!rc.url.url().contains("/")))
01019   {
01020     QString filename=rc.url.path();
01021     if (filename.isEmpty()) filename=rc.url.url();
01022     QFile f( filename );
01023     if( !f.open( IO_WriteOnly ) ) {
01024         err = i18n( "Could not open \"%1\"." ).arg( filename );
01025     }
01026     if (!err)
01027     {
01028       QTextStream stream(&f);
01029       // Export to file
01030       stream << retval;
01031       f.close();
01032     }
01033   }
01034   else // use remote file
01035   {
01036     KTempFile tmpFile;
01037     if ( tmpFile.status() != 0 )
01038     {
01039       err = QString::fromLatin1( "Unable to get temporary file" );
01040     }
01041     else
01042     {
01043       QTextStream *stream=tmpFile.textStream();
01044       *stream << retval;
01045       tmpFile.close();
01046       if (!KIO::NetAccess::upload( tmpFile.name(), rc.url, 0 )) err=QString::fromLatin1("Could not upload");
01047     }
01048   }
01049   return err;
01050 }
01051 
01052 void KarmStorage::stopTimer(const Task* task, QDateTime when)
01053 {
01054   kdDebug(5970) << "Entering KarmStorage::stopTimer" << endl;
01055   long delta = task->startTime().secsTo(when);
01056   changeTime(task, delta);
01057 }
01058 
01059 bool KarmStorage::bookTime(const Task* task,
01060                            const QDateTime& startDateTime,
01061                            const long durationInSeconds)
01062 {
01063   // Ignores preferences setting re: logging history.
01064   KCal::Event* e;
01065   QDateTime end;
01066 
01067   e = baseEvent( task );
01068   e->setDtStart( startDateTime );
01069   e->setDtEnd( startDateTime.addSecs( durationInSeconds ) );
01070 
01071   // Use a custom property to keep a record of negative durations
01072   e->setCustomProperty( kapp->instanceName(),
01073       QCString("duration"),
01074       QString::number(durationInSeconds));
01075 
01076   return _calendar->addEvent(e);
01077 }
01078 
01079 void KarmStorage::changeTime(const Task* task, const long deltaSeconds)
01080 {
01081   kdDebug(5970) << "Entering KarmStorage::changeTime ( " << task->name() << "," << deltaSeconds <<  " )" << endl;
01082   KCal::Event* e;
01083   QDateTime end;
01084 
01085   // Don't write events (with timer start/stop duration) if user has turned
01086   // this off in the settings dialog.
01087   if ( ! task->taskView()->preferences()->logging() ) return;
01088 
01089   e = baseEvent(task);
01090 
01091   // Don't use duration, as ICalFormatImpl::writeIncidence never writes a
01092   // duration, even though it looks like it's used in event.cpp.
01093   end = task->startTime();
01094   if ( deltaSeconds > 0 ) end = task->startTime().addSecs(deltaSeconds);
01095   e->setDtEnd(end);
01096 
01097   // Use a custom property to keep a record of negative durations
01098   e->setCustomProperty( kapp->instanceName(),
01099       QCString("duration"),
01100       QString::number(deltaSeconds));
01101 
01102   _calendar->addEvent(e);
01103 
01104   // This saves the entire iCal file each time, which isn't efficient but
01105   // ensures no data loss.  A faster implementation would be to append events
01106   // to a file, and then when KArm closes, append the data in this file to the
01107   // iCal file.
01108   //
01109   // Meanwhile, we simply use a timer to delay the full-saving until the GUI
01110   // has updated, for better user feedback. Feel free to get rid of this
01111   // if/when implementing the faster saving (DF).
01112   task->taskView()->scheduleSave();
01113 }
01114 
01115 
01116 KCal::Event* KarmStorage::baseEvent(const Task * task)
01117 {
01118   KCal::Event* e;
01119   QStringList categories;
01120 
01121   e = new KCal::Event;
01122   e->setSummary(task->name());
01123 
01124   // Can't use setRelatedToUid()--no error, but no RelatedTo written to disk
01125   e->setRelatedTo(_calendar->todo(task->uid()));
01126 
01127   // Debugging: some events where not getting a related-to field written.
01128   assert(e->relatedTo()->uid() == task->uid());
01129 
01130   // Have to turn this off to get datetimes in date fields.
01131   e->setFloats(false);
01132   e->setDtStart(task->startTime());
01133 
01134   // So someone can filter this mess out of their calendar display
01135   categories.append(i18n("KArm"));
01136   e->setCategories(categories);
01137 
01138   return e;
01139 }
01140 
01141 HistoryEvent::HistoryEvent(QString uid, QString name, long duration,
01142         QDateTime start, QDateTime stop, QString todoUid)
01143 {
01144   _uid = uid;
01145   _name = name;
01146   _duration = duration;
01147   _start = start;
01148   _stop = stop;
01149   _todoUid = todoUid;
01150 }
01151 
01152 
01153 QValueList<HistoryEvent> KarmStorage::getHistory(const QDate& from,
01154     const QDate& to)
01155 {
01156   QValueList<HistoryEvent> retval;
01157   QStringList processed;
01158   KCal::Event::List events;
01159   KCal::Event::List::iterator event;
01160   QString duration;
01161 
01162   for(QDate d = from; d <= to; d = d.addDays(1))
01163   {
01164     events = _calendar->rawEventsForDate( d );
01165     for (event = events.begin(); event != events.end(); ++event)
01166     {
01167 
01168       // KArm events have the custom property X-KDE-Karm-duration
01169       if (! processed.contains( (*event)->uid()))
01170       {
01171         // If an event spans multiple days, CalendarLocal::rawEventsForDate
01172         // will return the same event on both days.  To avoid double-counting
01173         // such events, we (arbitrarily) attribute the hours from both days on
01174         // the first day.  This mis-reports the actual time spent, but it is
01175         // an easy fix for a (hopefully) rare situation.
01176         processed.append( (*event)->uid());
01177 
01178         duration = (*event)->customProperty(kapp->instanceName(),
01179             QCString("duration"));
01180         if ( ! duration.isNull() )
01181         {
01182           if ( (*event)->relatedTo()
01183               &&  ! (*event)->relatedTo()->uid().isEmpty() )
01184           {
01185             retval.append(HistoryEvent(
01186                 (*event)->uid(),
01187                 (*event)->summary(),
01188                 duration.toLong(),
01189                 (*event)->dtStart(),
01190                 (*event)->dtEnd(),
01191                 (*event)->relatedTo()->uid()
01192                 ));
01193           }
01194           else
01195             // Something is screwy with the ics file, as this KArm history event
01196             // does not have a todo related to it.  Could have been deleted
01197             // manually?  We'll continue with report on with report ...
01198             kdDebug(5970) << "KarmStorage::getHistory(): "
01199               << "The event " << (*event)->uid()
01200               << " is not related to a todo.  Dropped." << endl;
01201         }
01202       }
01203     }
01204   }
01205 
01206   return retval;
01207 }
01208 
01209 bool KarmStorage::remoteResource( const QString& file ) const
01210 {
01211   QString f = file.lower();
01212   bool rval = f.startsWith( "http://" ) || f.startsWith( "ftp://" );
01213 
01214   kdDebug(5970) << "KarmStorage::remoteResource( " << file << " ) returns " << rval  << endl;
01215   return rval;
01216 }
01217 
01218 bool KarmStorage::saveCalendar()
01219 {
01220   kdDebug(5970) << "KarmStorage::saveCalendar" << endl;
01221 
01222   Event::List evl=_calendar->rawEvents();
01223   kdDebug(5970) << "summary - dtStart - dtEnd" << endl;
01224   for (unsigned int i=0; i<evl.count(); i++) 
01225   {
01226     kdDebug() << evl[i]->summary() << evl[i]->dtStart() << evl[i]->dtEnd() << endl;
01227   }
01228   KABC::Lock *lock = _calendar->lock();
01229   if ( !lock || !lock->lock() )
01230     return false;
01231 
01232   if ( _calendar && _calendar->save() ) {
01233     lock->unlock();
01234     return true;
01235   }
01236 
01237   lock->unlock();
01238   return false;
01239 }
KDE Home | KDE Accessibility Home | Description of Access Keys