karm

karmstorage.cpp
1 /*
2  * This file only:
3  * Copyright (C) 2003, 2004 Mark Bucciarelli <mark@hubcapconsulting.com>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the
17  * Free Software Foundation, Inc.
18  * 51 Franklin Street, Fifth Floor
19  * Boston, MA 02110-1301 USA.
20  *
21  */
22 
23 #include <sys/types.h>
24 #include <sys/stat.h>
25 #include <fcntl.h>
26 #include <unistd.h>
27 
28 #include <cassert>
29 
30 #include <tqfile.h>
31 #include <tqsize.h>
32 #include <tqdict.h>
33 #include <tqdatetime.h>
34 #include <tqstring.h>
35 #include <tqstringlist.h>
36 
37 #include "incidence.h"
38 #include "kapplication.h" // kapp
39 #include <kdebug.h>
40 #include <kemailsettings.h>
41 #include <klocale.h> // i18n
42 #include <kmessagebox.h>
43 #include <kprogress.h>
44 #include <ktempfile.h>
45 #include <resourcecalendar.h>
46 #include <resourcelocal.h>
47 #include <resourceremote.h>
48 #include <kpimprefs.h>
49 #include <taskview.h>
50 #include <timekard.h>
51 #include <karmutility.h>
52 #include <kio/netaccess.h>
53 #include <kurl.h>
54 #include <vector>
55 
56 //#include <calendarlocal.h>
57 //#include <journal.h>
58 //#include <event.h>
59 //#include <todo.h>
60 
61 #include "karmstorage.h"
62 #include "preferences.h"
63 #include "task.h"
64 #include "reportcriteria.h"
65 
66 using namespace std;
67 
68 KarmStorage *KarmStorage::_instance = 0;
69 static long linenr; // how many lines written by printTaskHistory so far
70 
71 
72 KarmStorage *KarmStorage::instance()
73 {
74  if (_instance == 0) _instance = new KarmStorage();
75  return _instance;
76 }
77 
78 KarmStorage::KarmStorage()
79 {
80  _calendar = 0;
81 }
82 
83 TQString KarmStorage::load (TaskView* view, const Preferences* preferences, TQString fileName )
84 // loads data from filename into view. If no filename is given, filename from preferences is used.
85 // filename might be of use if this program is run as embedded konqueror plugin.
86 {
87  // When I tried raising an exception from this method, the compiler
88  // complained that exceptions are not allowed. Not sure how apps
89  // typically handle error conditions in KDE, but I'll return the error
90  // as a string (empty is no error). -- Mark, Aug 8, 2003
91 
92  // Use KDE_CXXFLAGS=$(USE_EXCEPTIONS) in Makefile.am if you want to use
93  // exceptions (David Faure)
94 
95  TQString err;
96  KEMailSettings settings;
97  if ( fileName.isEmpty() ) fileName = preferences->iCalFile();
98 
99  // If same file, don't reload
100  if ( fileName == _icalfile ) return err;
101 
102 
103  // If file doesn't exist, create a blank one to avoid ResourceLocal load
104  // error. We make it user and group read/write, others read. This is
105  // masked by the users umask. (See man creat)
106  if ( ! remoteResource( _icalfile ) )
107  {
108  int handle;
109  handle = open (
110  TQFile::encodeName( fileName ),
111  O_CREAT|O_EXCL|O_WRONLY,
112  S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH
113  );
114  if (handle != -1) close(handle);
115  }
116 
117  if ( _calendar)
118  closeStorage(view);
119 
120  // Create local file resource and add to resources
121  _icalfile = fileName;
122 
123  KCal::ResourceCached *resource;
124  if ( remoteResource( _icalfile ) )
125  {
126  KURL url( _icalfile );
127  resource = new KCal::ResourceRemote( url, url ); // same url for upload and download
128  }
129  else
130  {
131  resource = new KCal::ResourceLocal( _icalfile );
132  }
133  _calendar = resource;
134 
135  TQObject::connect (_calendar, TQT_SIGNAL(resourceChanged(ResourceCalendar *)),
136  view, TQT_SLOT(iCalFileModified(ResourceCalendar *)));
137  _calendar->setTimeZoneId( KPimPrefs::timezone() );
138  _calendar->setResourceName( TQString::fromLatin1("KArm") );
139  _calendar->open();
140  _calendar->load();
141 
142  // Claim ownership of iCalendar file if no one else has.
143  KCal::Person owner = resource->getOwner();
144  if ( owner.isEmpty() )
145  {
146  resource->setOwner( KCal::Person(
147  settings.getSetting( KEMailSettings::RealName ),
148  settings.getSetting( KEMailSettings::EmailAddress ) ) );
149  }
150 
151  // Build task view from iCal data
152  if (!err)
153  {
154  KCal::Todo::List todoList;
155  KCal::Todo::List::ConstIterator todo;
156  TQDict< Task > map;
157 
158  // Build dictionary to look up Task object from Todo uid. Each task is a
159  // TQListViewItem, and is initially added with the view as the parent.
160  todoList = _calendar->rawTodos();
161  kdDebug(5970) << "KarmStorage::load "
162  << "rawTodo count (includes completed todos) ="
163  << todoList.count() << endl;
164  for( todo = todoList.begin(); todo != todoList.end(); ++todo )
165  {
166  // Initially, if a task was complete, it was removed from the view.
167  // However, this increased the complexity of reporting on task history.
168  //
169  // For example, if a task is complete yet has time logged to it during
170  // the date range specified on the history report, we have to figure out
171  // how that task fits into the task hierarchy. Currently, this
172  // structure is held in memory by the structure in the list view.
173  //
174  // I considered creating a second tree that held the full structure of
175  // all complete and incomplete tasks. But this seemed to much of a
176  // change with an impending beta release and a full todo list.
177  //
178  // Hence this "solution". Include completed tasks, but mark them as
179  // inactive in the view.
180  //
181  //if ((*todo)->isCompleted()) continue;
182 
183  Task* task = new Task(*todo, view);
184  map.insert( (*todo)->uid(), task );
185  view->setRootIsDecorated(true);
186  task->setPixmapProgress();
187  }
188 
189  // Load each task under it's parent task.
190  for( todo = todoList.begin(); todo != todoList.end(); ++todo )
191  {
192  Task* task = map.find( (*todo)->uid() );
193 
194  // No relatedTo incident just means this is a top-level task.
195  if ( (*todo)->relatedTo() )
196  {
197  Task* newParent = map.find( (*todo)->relatedToUid() );
198 
199  // Complete the loading but return a message
200  if ( !newParent )
201  err = i18n("Error loading \"%1\": could not find parent (uid=%2)")
202  .arg(task->name())
203  .arg((*todo)->relatedToUid());
204 
205  if (!err) task->move( newParent);
206  }
207  }
208 
209  kdDebug(5970) << "KarmStorage::load - loaded " << view->count()
210  << " tasks from " << _icalfile << endl;
211  }
212 
213  return err;
214 }
215 
216 TQString KarmStorage::icalfile()
217 {
218  kdDebug(5970) << "Entering KarmStorage::icalfile" << endl;
219  return _icalfile;
220 }
221 
222 TQString KarmStorage::buildTaskView(KCal::ResourceCalendar *rc, TaskView *view)
223 // makes *view contain the tasks out of *rc.
224 {
225  TQString err;
226  KCal::Todo::List todoList;
227  KCal::Todo::List::ConstIterator todo;
228  TQDict< Task > map;
229  vector<TQString> runningTasks;
230  vector<TQDateTime> startTimes;
231 
232  // remember tasks that are running and their start times
233  for ( int i=0; i<view->count(); i++)
234  {
235  if ( view->item_at_index(i)->isRunning() )
236  {
237  runningTasks.push_back( view->item_at_index(i)->uid() );
238  startTimes.push_back( view->item_at_index(i)->lastStart() );
239  }
240  }
241 
242  //view->stopAllTimers();
243  // delete old tasks
244  while (view->item_at_index(0)) view->item_at_index(0)->cut();
245 
246  // 1. insert tasks form rc into taskview
247  // 1.1. Build dictionary to look up Task object from Todo uid. Each task is a
248  // TQListViewItem, and is initially added with the view as the parent.
249  todoList = rc->rawTodos();
250  for( todo = todoList.begin(); todo != todoList.end(); ++todo )
251  {
252  Task* task = new Task(*todo, view);
253  map.insert( (*todo)->uid(), task );
254  view->setRootIsDecorated(true);
255  task->setPixmapProgress();
256  }
257 
258  // 1.1. Load each task under it's parent task.
259  for( todo = todoList.begin(); todo != todoList.end(); ++todo )
260  {
261  Task* task = map.find( (*todo)->uid() );
262 
263  // No relatedTo incident just means this is a top-level task.
264  if ( (*todo)->relatedTo() )
265  {
266  Task* newParent = map.find( (*todo)->relatedToUid() );
267 
268  // Complete the loading but return a message
269  if ( !newParent )
270  err = i18n("Error loading \"%1\": could not find parent (uid=%2)")
271  .arg(task->name())
272  .arg((*todo)->relatedToUid());
273 
274  if (!err) task->move( newParent);
275  }
276  }
277 
278  view->clearActiveTasks();
279  // restart tasks that have been running with their start times
280  for ( int i=0; i<view->count(); i++)
281  {
282  for ( unsigned int n=0; n<runningTasks.size(); n++)
283  {
284  if ( runningTasks[n] == view->item_at_index(i)->uid() )
285  {
286  view->startTimerFor( view->item_at_index(i), startTimes[n] );
287  }
288  }
289  }
290 
291  view->refresh();
292 
293  return err;
294 }
295 
296 void KarmStorage::closeStorage(TaskView* view)
297 {
298  if ( _calendar )
299  {
300  _calendar->close();
301  delete _calendar;
302  _calendar = 0;
303 
304  view->clear();
305  }
306 }
307 
308 TQString KarmStorage::save(TaskView* taskview)
309 {
310  kdDebug(5970) << "entering KarmStorage::save" << endl;
311  TQString err=TQString();
312 
313  TQPtrStack< KCal::Todo > parents;
314 
315  for (Task* task=taskview->first_child(); task; task = task->nextSibling())
316  {
317  err=writeTaskAsTodo(task, 1, parents );
318  }
319 
320  if ( !saveCalendar() )
321  {
322  err="Could not save";
323  }
324 
325  if ( err.isEmpty() )
326  {
327  kdDebug(5970)
328  << "KarmStorage::save : wrote "
329  << taskview->count() << " tasks to " << _icalfile << endl;
330  }
331  else
332  {
333  kdWarning(5970) << "KarmStorage::save : " << err << endl;
334  }
335 
336  return err;
337 }
338 
339 TQString KarmStorage::writeTaskAsTodo(Task* task, const int level,
340  TQPtrStack< KCal::Todo >& parents )
341 {
342  TQString err;
343  KCal::Todo* todo;
344 
345  todo = _calendar->todo(task->uid());
346  if ( !todo )
347  {
348  kdDebug(5970) << "Could not get todo from calendar" << endl;
349  return "Could not get todo from calendar";
350  }
351  task->asTodo(todo);
352  if ( !parents.isEmpty() ) todo->setRelatedTo( parents.top() );
353  parents.push( todo );
354 
355  for ( Task* nextTask = task->firstChild(); nextTask;
356  nextTask = nextTask->nextSibling() )
357  {
358  err = writeTaskAsTodo(nextTask, level+1, parents );
359  }
360 
361  parents.pop();
362  return err;
363 }
364 
366 {
367  KCal::Todo::List todoList;
368 
369  todoList = _calendar->rawTodos();
370  return todoList.empty();
371 }
372 
373 bool KarmStorage::isNewStorage(const Preferences* preferences) const
374 {
375  if ( !_icalfile.isNull() ) return preferences->iCalFile() != _icalfile;
376  else return false;
377 }
378 
379 //----------------------------------------------------------------------------
380 // Routines that handle legacy flat file format.
381 // These only stored total and session times.
382 //
383 
385  const TQString& filename)
386 {
387  TQString err;
388 
389  kdDebug(5970)
390  << "KarmStorage::loadFromFlatFile: " << filename << endl;
391 
392  TQFile f(filename);
393  if( !f.exists() )
394  err = i18n("File \"%1\" not found.").arg(filename);
395 
396  if (!err)
397  {
398  if( !f.open( IO_ReadOnly ) )
399  err = i18n("Could not open \"%1\".").arg(filename);
400  }
401 
402  if (!err)
403  {
404 
405  TQString line;
406 
407  TQPtrStack<Task> stack;
408  Task *task;
409 
410  TQTextStream stream(&f);
411 
412  while( !stream.atEnd() ) {
413  // lukas: this breaks for non-latin1 chars!!!
414  // if ( file.readLine( line, T_LINESIZE ) == 0 )
415  // break;
416 
417  line = stream.readLine();
418  kdDebug(5970) << "DEBUG: line: " << line << "\n";
419 
420  if (line.isNull())
421  break;
422 
423  long minutes;
424  int level;
425  TQString name;
426  DesktopList desktopList;
427  if (!parseLine(line, &minutes, &name, &level, &desktopList))
428  continue;
429 
430  unsigned int stackLevel = stack.count();
431  for (unsigned int i = level; i<=stackLevel ; i++) {
432  stack.pop();
433  }
434 
435  if (level == 1) {
436  kdDebug(5970) << "KarmStorage::loadFromFlatFile - toplevel task: "
437  << name << " min: " << minutes << "\n";
438  task = new Task(name, minutes, 0, desktopList, taskview);
439  task->setUid(addTask(task, 0));
440  }
441  else {
442  Task *parent = stack.top();
443  kdDebug(5970) << "KarmStorage::loadFromFlatFile - task: " << name
444  << " min: " << minutes << " parent" << parent->name() << "\n";
445  task = new Task(name, minutes, 0, desktopList, parent);
446 
447  task->setUid(addTask(task, parent));
448 
449  // Legacy File Format (!):
450  parent->changeTimes(0, -minutes);
451  taskview->setRootIsDecorated(true);
452  parent->setOpen(true);
453  }
454  if (!task->uid().isNull())
455  stack.push(task);
456  else
457  delete task;
458  }
459 
460  f.close();
461 
462  }
463 
464  return err;
465 }
466 
468  const TQString& filename)
469 {
470  TQString err = loadFromFlatFile(taskview, filename);
471  if (!err)
472  {
473  for (Task* task = taskview->first_child(); task;
474  task = task->nextSibling())
475  {
476  adjustFromLegacyFileFormat(task);
477  }
478  }
479  return err;
480 }
481 
482 bool KarmStorage::parseLine(TQString line, long *time, TQString *name,
483  int *level, DesktopList* desktopList)
484 {
485  if (line.find('#') == 0) {
486  // A comment line
487  return false;
488  }
489 
490  int index = line.find('\t');
491  if (index == -1) {
492  // This doesn't seem like a valid record
493  return false;
494  }
495 
496  TQString levelStr = line.left(index);
497  TQString rest = line.remove(0,index+1);
498 
499  index = rest.find('\t');
500  if (index == -1) {
501  // This doesn't seem like a valid record
502  return false;
503  }
504 
505  TQString timeStr = rest.left(index);
506  rest = rest.remove(0,index+1);
507 
508  bool ok;
509 
510  index = rest.find('\t'); // check for optional desktops string
511  if (index >= 0) {
512  *name = rest.left(index);
513  TQString deskLine = rest.remove(0,index+1);
514 
515  // now transform the ds string (e.g. "3", or "1,4,5") into
516  // an DesktopList
517  TQString ds;
518  int d;
519  int commaIdx = deskLine.find(',');
520  while (commaIdx >= 0) {
521  ds = deskLine.left(commaIdx);
522  d = ds.toInt(&ok);
523  if (!ok)
524  return false;
525 
526  desktopList->push_back(d);
527  deskLine.remove(0,commaIdx+1);
528  commaIdx = deskLine.find(',');
529  }
530 
531  d = deskLine.toInt(&ok);
532 
533  if (!ok)
534  return false;
535 
536  desktopList->push_back(d);
537  }
538  else {
539  *name = rest.remove(0,index+1);
540  }
541 
542  *time = timeStr.toLong(&ok);
543 
544  if (!ok) {
545  // the time field was not a number
546  return false;
547  }
548  *level = levelStr.toInt(&ok);
549  if (!ok) {
550  // the time field was not a number
551  return false;
552  }
553 
554  return true;
555 }
556 
557 void KarmStorage::adjustFromLegacyFileFormat(Task* task)
558 {
559  // unless the parent is the listView
560  if ( task->parent() )
561  task->parent()->changeTimes(-task->sessionTime(), -task->time());
562 
563  // traverse depth first -
564  // as soon as we're in a leaf, we'll substract it's time from the parent
565  // then, while descending back we'll do the same for each node untill
566  // we reach the root
567  for ( Task* subtask = task->firstChild(); subtask;
568  subtask = subtask->nextSibling() )
569  adjustFromLegacyFileFormat(subtask);
570 }
571 
572 //----------------------------------------------------------------------------
573 // Routines that handle Comma-Separated Values export file format.
574 //
575 TQString KarmStorage::exportcsvFile( TaskView *taskview,
576  const ReportCriteria &rc )
577 {
578  TQString delim = rc.delimiter;
579  TQString dquote = rc.quote;
580  TQString double_dquote = dquote + dquote;
581  bool to_quote = true;
582 
583  TQString err;
584  Task* task;
585  int maxdepth=0;
586 
587  kdDebug(5970)
588  << "KarmStorage::exportcsvFile: " << rc.url << endl;
589 
590  TQString title = i18n("Export Progress");
591  KProgressDialog dialog( taskview, 0, title );
592  dialog.setAutoClose( true );
593  dialog.setAllowCancel( true );
594  dialog.progressBar()->setTotalSteps( 2 * taskview->count() );
595 
596  // The default dialog was not displaying all the text in the title bar.
597  int width = taskview->fontMetrics().width(title) * 3;
598  TQSize dialogsize;
599  dialogsize.setWidth(width);
600  dialog.setInitialSize( dialogsize, true );
601 
602  if ( taskview->count() > 1 ) dialog.show();
603 
604  TQString retval;
605 
606  // Find max task depth
607  int tasknr = 0;
608  while ( tasknr < taskview->count() && !dialog.wasCancelled() )
609  {
610  dialog.progressBar()->advance( 1 );
611  if ( tasknr % 15 == 0 ) kapp->processEvents(); // repainting is slow
612  if ( taskview->item_at_index(tasknr)->depth() > maxdepth )
613  maxdepth = taskview->item_at_index(tasknr)->depth();
614  tasknr++;
615  }
616 
617  // Export to file
618  tasknr = 0;
619  while ( tasknr < taskview->count() && !dialog.wasCancelled() )
620  {
621  task = taskview->item_at_index( tasknr );
622  dialog.progressBar()->advance( 1 );
623  if ( tasknr % 15 == 0 ) kapp->processEvents();
624 
625  // indent the task in the csv-file:
626  for ( int i=0; i < task->depth(); ++i ) retval += delim;
627 
628  /*
629  // CSV compliance
630  // Surround the field with quotes if the field contains
631  // a comma (delim) or a double quote
632  if (task->name().contains(delim) || task->name().contains(dquote))
633  to_quote = true;
634  else
635  to_quote = false;
636  */
637  to_quote = true;
638 
639  if (to_quote)
640  retval += dquote;
641 
642  // Double quotes replaced by a pair of consecutive double quotes
643  retval += task->name().replace( dquote, double_dquote );
644 
645  if (to_quote)
646  retval += dquote;
647 
648  // maybe other tasks are more indented, so to align the columns:
649  for ( int i = 0; i < maxdepth - task->depth(); ++i ) retval += delim;
650 
651  retval += delim + formatTime( task->sessionTime(),
652  rc.decimalMinutes )
653  + delim + formatTime( task->time(),
654  rc.decimalMinutes )
655  + delim + formatTime( task->totalSessionTime(),
656  rc.decimalMinutes )
657  + delim + formatTime( task->totalTime(),
658  rc.decimalMinutes )
659  + "\n";
660  tasknr++;
661  }
662 
663  // save, either locally or remote
664  if ((rc.url.isLocalFile()) || (!rc.url.url().contains("/")))
665  {
666  TQString filename=rc.url.path();
667  if (filename.isEmpty()) filename=rc.url.url();
668  TQFile f( filename );
669  if( !f.open( IO_WriteOnly ) ) {
670  err = i18n( "Could not open \"%1\"." ).arg( filename );
671  }
672  if (!err)
673  {
674  TQTextStream stream(&f);
675  // Export to file
676  stream << retval;
677  f.close();
678  }
679  }
680  else // use remote file
681  {
682  KTempFile tmpFile;
683  if ( tmpFile.status() != 0 ) err = TQString::fromLatin1( "Unable to get temporary file" );
684  else
685  {
686  TQTextStream *stream=tmpFile.textStream();
687  *stream << retval;
688  tmpFile.close();
689  if (!KIO::NetAccess::upload( tmpFile.name(), rc.url, 0 )) err=TQString::fromLatin1("Could not upload");
690  }
691  }
692 
693  return err;
694 }
695 
696 //----------------------------------------------------------------------------
697 // Routines that handle logging KArm history
698 //
699 
700 //
701 // public routines:
702 //
703 
704 TQString KarmStorage::addTask(const Task* task, const Task* parent)
705 {
706  KCal::Todo* todo;
707  TQString uid;
708 
709  todo = new KCal::Todo();
710  if ( _calendar->addTodo( todo ) )
711  {
712  task->asTodo( todo );
713  if (parent)
714  todo->setRelatedTo(_calendar->todo(parent->uid()));
715  uid = todo->uid();
716  }
717  else
718  {
719  // Most likely a lock could not be pulled, although there are other
720  // possiblities (like a really confused resource manager).
721  uid = "";
722  }
723 
724  return uid;
725 }
726 
728 {
729 
730  // delete history
731  KCal::Event::List eventList = _calendar->rawEvents();
732  for(KCal::Event::List::iterator i = eventList.begin();
733  i != eventList.end();
734  ++i)
735  {
736  //kdDebug(5970) << "KarmStorage::removeTask: "
737  // << (*i)->uid() << " - relatedToUid() "
738  // << (*i)->relatedToUid()
739  // << ", relatedTo() = " << (*i)->relatedTo() <<endl;
740  if ( (*i)->relatedToUid() == task->uid()
741  || ( (*i)->relatedTo()
742  && (*i)->relatedTo()->uid() == task->uid()))
743  {
744  _calendar->deleteEvent(*i);
745  }
746  }
747 
748  // delete todo
749  KCal::Todo *todo = _calendar->todo(task->uid());
750  _calendar->deleteTodo(todo);
751 
752  // Save entire file
753  saveCalendar();
754 
755  return true;
756 }
757 
758 void KarmStorage::addComment(const Task* task, const TQString& comment)
759 {
760 
761  KCal::Todo* todo;
762 
763  todo = _calendar->todo(task->uid());
764 
765  // Do this to avoid compiler warnings about comment not being used. once we
766  // transition to using the addComment method, we need this second param.
767  TQString s = comment;
768 
769  // TODO: Use libkcal comments
770  // todo->addComment(comment);
771  // temporary
772  todo->setDescription(task->comment());
773 
774  saveCalendar();
775 }
776 
777 long KarmStorage::printTaskHistory (
778  const Task *task,
779  const TQMap<TQString,long> &taskdaytotals,
780  TQMap<TQString,long> &daytotals,
781  const TQDate &from,
782  const TQDate &to,
783  const int level,
784  vector <TQString> &matrix,
785  const ReportCriteria &rc)
786 // to>=from is precondition
787 {
788  long ownline=linenr++; // the how many-th instance of this function is this
789  long colrectot=0; // colum where to write the task's total recursive time
790  vector <TQString> cell; // each line of the matrix is stored in an array of cells, one containing the recursive total
791  long add; // total recursive time of all subtasks
792  TQString delim = rc.delimiter;
793  TQString dquote = rc.quote;
794  TQString double_dquote = dquote + dquote;
795  bool to_quote = true;
796 
797  const TQString cr = TQString::fromLatin1("\n");
798  TQString buf;
799  TQString daytaskkey, daykey;
800  TQDate day;
801  long sum;
802 
803  if ( !task ) return 0;
804 
805  day = from;
806  sum = 0;
807  while (day <= to)
808  {
809  // write the time in seconds for the given task for the given day to s
810  daykey = day.toString(TQString::fromLatin1("yyyyMMdd"));
811  daytaskkey = TQString::fromLatin1("%1_%2")
812  .arg(daykey)
813  .arg(task->uid());
814 
815  if (taskdaytotals.contains(daytaskkey))
816  {
817  cell.push_back(TQString::fromLatin1("%1")
818  .arg(formatTime(taskdaytotals[daytaskkey]/60, rc.decimalMinutes)));
819  sum += taskdaytotals[daytaskkey]; // in seconds
820 
821  if (daytotals.contains(daykey))
822  daytotals.replace(daykey, daytotals[daykey]+taskdaytotals[daytaskkey]);
823  else
824  daytotals.insert(daykey, taskdaytotals[daytaskkey]);
825  }
826  cell.push_back(delim);
827 
828  day = day.addDays(1);
829  }
830 
831  // Total for task
832  cell.push_back(TQString::fromLatin1("%1").arg(formatTime(sum/60, rc.decimalMinutes)));
833 
834  // room for the recursive total time (that cannot be calculated now)
835  cell.push_back(delim);
836  colrectot = cell.size();
837  cell.push_back("???");
838  cell.push_back(delim);
839 
840  // Task name
841  for ( int i = level + 1; i > 0; i-- ) cell.push_back(delim);
842 
843  /*
844  // CSV compliance
845  // Surround the field with quotes if the field contains
846  // a comma (delim) or a double quote
847  to_quote = task->name().contains(delim) || task->name().contains(dquote);
848  */
849  to_quote = true;
850  if ( to_quote) cell.push_back(dquote);
851 
852 
853  // Double quotes replaced by a pair of consecutive double quotes
854  cell.push_back(task->name().replace( dquote, double_dquote ));
855 
856  if ( to_quote) cell.push_back(dquote);
857 
858  cell.push_back(cr);
859 
860  add=0;
861  for (Task* subTask = task->firstChild();
862  subTask;
863  subTask = subTask->nextSibling())
864  {
865  add += printTaskHistory( subTask, taskdaytotals, daytotals, from, to , level+1, matrix,
866  rc );
867  }
868  cell[colrectot]=(TQString::fromLatin1("%1").arg(formatTime((add+sum)/60, rc.decimalMinutes )));
869  for (unsigned int i=0; i < cell.size(); i++) matrix[ownline]+=cell[i];
870  return add+sum;
871 }
872 
873 TQString KarmStorage::report( TaskView *taskview, const ReportCriteria &rc )
874 {
875  TQString err;
876  if ( rc.reportType == ReportCriteria::CSVHistoryExport )
877  err = exportcsvHistory( taskview, rc.from, rc.to, rc );
878  else if ( rc.reportType == ReportCriteria::CSVTotalsExport )
879  err = exportcsvFile( taskview, rc );
880  else {
881  // hmmmm ... assert(0)?
882  }
883  return err;
884 }
885 
886 // export history report as csv, all tasks X all dates in one block
887 TQString KarmStorage::exportcsvHistory ( TaskView *taskview,
888  const TQDate &from,
889  const TQDate &to,
890  const ReportCriteria &rc)
891 {
892  TQString delim = rc.delimiter;
893  const TQString cr = TQString::fromLatin1("\n");
894  TQString err;
895 
896  // below taken from timekard.cpp
897  TQString retval;
898  TQString taskhdr, totalhdr;
899  TQString line, buf;
900  long sum;
901 
902  TQValueList<HistoryEvent> events;
903  TQValueList<HistoryEvent>::iterator event;
904  TQMap<TQString, long> taskdaytotals;
905  TQMap<TQString, long> daytotals;
906  TQString daytaskkey, daykey;
907  TQDate day;
908  TQDate dayheading;
909 
910  // parameter-plausi
911  if ( from > to )
912  {
913  err = TQString::fromLatin1 (
914  "'to' has to be a date later than or equal to 'from'.");
915  }
916 
917  // header
918  retval += i18n("Task History\n");
919  retval += i18n("From %1 to %2")
920  .arg(KGlobal::locale()->formatDate(from))
921  .arg(KGlobal::locale()->formatDate(to));
922  retval += cr;
923  retval += i18n("Printed on: %1")
924  .arg(KGlobal::locale()->formatDateTime(TQDateTime::currentDateTime()));
925  retval += cr;
926 
927  day=from;
928  events = taskview->getHistory(from, to);
929  taskdaytotals.clear();
930  daytotals.clear();
931 
932  // Build lookup dictionary used to output data in table cells. keys are
933  // in this format: YYYYMMDD_NNNNNN, where Y = year, M = month, d = day and
934  // NNNNN = the VTODO uid. The value is the total seconds logged against
935  // that task on that day. Note the UID is the todo id, not the event id,
936  // so times are accumulated for each task.
937  for (event = events.begin(); event != events.end(); ++event)
938  {
939  daykey = (*event).start().date().toString(TQString::fromLatin1("yyyyMMdd"));
940  daytaskkey = TQString(TQString::fromLatin1("%1_%2"))
941  .arg(daykey)
942  .arg((*event).todoUid());
943 
944  if (taskdaytotals.contains(daytaskkey))
945  taskdaytotals.replace(daytaskkey,
946  taskdaytotals[daytaskkey] + (*event).duration());
947  else
948  taskdaytotals.insert(daytaskkey, (*event).duration());
949  }
950 
951  // day headings
952  dayheading = from;
953  while ( dayheading <= to )
954  {
955  // Use ISO 8601 format for date.
956  retval += dayheading.toString(TQString::fromLatin1("yyyy-MM-dd"));
957  retval += delim;
958  dayheading=dayheading.addDays(1);
959  }
960  retval += i18n("Sum") + delim + i18n("Total Sum") + delim + i18n("Task Hierarchy");
961  retval += cr;
962  retval += line;
963 
964  // the tasks
965  vector <TQString> matrix;
966  linenr=0;
967  for (int i=0; i<=taskview->count()+1; i++) matrix.push_back("");
968  if (events.empty())
969  {
970  retval += i18n(" No hours logged.");
971  }
972  else
973  {
974  if ( rc.allTasks )
975  {
976  for ( Task* task= taskview->item_at_index(0);
977  task; task= task->nextSibling() )
978  {
979  printTaskHistory( task, taskdaytotals, daytotals, from, to, 0,
980  matrix, rc );
981  }
982  }
983  else
984  {
985  printTaskHistory( taskview->current_item(), taskdaytotals, daytotals,
986  from, to, 0, matrix, rc );
987  }
988  for (unsigned int i=0; i<matrix.size(); i++) retval+=matrix[i];
989  retval += line;
990 
991  // totals
992  sum = 0;
993  day = from;
994  while (day<=to)
995  {
996  daykey = day.toString(TQString::fromLatin1("yyyyMMdd"));
997 
998  if (daytotals.contains(daykey))
999  {
1000  retval += TQString::fromLatin1("%1")
1001  .arg(formatTime(daytotals[daykey]/60, rc.decimalMinutes));
1002  sum += daytotals[daykey]; // in seconds
1003  }
1004  retval += delim;
1005  day = day.addDays(1);
1006  }
1007 
1008  retval += TQString::fromLatin1("%1%2%3%4")
1009  .arg( formatTime( sum/60, rc.decimalMinutes ) )
1010  .arg( delim ).arg( delim )
1011  .arg( i18n( "Total" ) );
1012  }
1013 
1014  // above taken from timekard.cpp
1015 
1016  // save, either locally or remote
1017 
1018  if ((rc.url.isLocalFile()) || (!rc.url.url().contains("/")))
1019  {
1020  TQString filename=rc.url.path();
1021  if (filename.isEmpty()) filename=rc.url.url();
1022  TQFile f( filename );
1023  if( !f.open( IO_WriteOnly ) ) {
1024  err = i18n( "Could not open \"%1\"." ).arg( filename );
1025  }
1026  if (!err)
1027  {
1028  TQTextStream stream(&f);
1029  // Export to file
1030  stream << retval;
1031  f.close();
1032  }
1033  }
1034  else // use remote file
1035  {
1036  KTempFile tmpFile;
1037  if ( tmpFile.status() != 0 )
1038  {
1039  err = TQString::fromLatin1( "Unable to get temporary file" );
1040  }
1041  else
1042  {
1043  TQTextStream *stream=tmpFile.textStream();
1044  *stream << retval;
1045  tmpFile.close();
1046  if (!KIO::NetAccess::upload( tmpFile.name(), rc.url, 0 )) err=TQString::fromLatin1("Could not upload");
1047  }
1048  }
1049  return err;
1050 }
1051 
1052 void KarmStorage::stopTimer(const Task* task, TQDateTime when)
1053 {
1054  kdDebug(5970) << "Entering KarmStorage::stopTimer" << endl;
1055  long delta = task->startTime().secsTo(when);
1056  changeTime(task, delta);
1057 }
1058 
1059 bool KarmStorage::bookTime(const Task* task,
1060  const TQDateTime& startDateTime,
1061  const long durationInSeconds)
1062 {
1063  // Ignores preferences setting re: logging history.
1064  KCal::Event* e;
1065  TQDateTime end;
1066 
1067  e = baseEvent( task );
1068  e->setDtStart( startDateTime );
1069  e->setDtEnd( startDateTime.addSecs( durationInSeconds ) );
1070 
1071  // Use a custom property to keep a record of negative durations
1072  e->setCustomProperty( kapp->instanceName(),
1073  TQCString("duration"),
1074  TQString::number(durationInSeconds));
1075 
1076  return _calendar->addEvent(e);
1077 }
1078 
1079 void KarmStorage::changeTime(const Task* task, const long deltaSeconds)
1080 {
1081  kdDebug(5970) << "Entering KarmStorage::changeTime ( " << task->name() << "," << deltaSeconds << " )" << endl;
1082  KCal::Event* e;
1083  TQDateTime end;
1084 
1085  // Don't write events (with timer start/stop duration) if user has turned
1086  // this off in the settings dialog.
1087  if ( ! task->taskView()->preferences()->logging() ) return;
1088 
1089  e = baseEvent(task);
1090 
1091  // Don't use duration, as ICalFormatImpl::writeIncidence never writes a
1092  // duration, even though it looks like it's used in event.cpp.
1093  end = task->startTime();
1094  if ( deltaSeconds > 0 ) end = task->startTime().addSecs(deltaSeconds);
1095  e->setDtEnd(end);
1096 
1097  // Use a custom property to keep a record of negative durations
1098  e->setCustomProperty( kapp->instanceName(),
1099  TQCString("duration"),
1100  TQString::number(deltaSeconds));
1101 
1102  _calendar->addEvent(e);
1103 
1104  // This saves the entire iCal file each time, which isn't efficient but
1105  // ensures no data loss. A faster implementation would be to append events
1106  // to a file, and then when KArm closes, append the data in this file to the
1107  // iCal file.
1108  //
1109  // Meanwhile, we simply use a timer to delay the full-saving until the GUI
1110  // has updated, for better user feedback. Feel free to get rid of this
1111  // if/when implementing the faster saving (DF).
1112  task->taskView()->scheduleSave();
1113 }
1114 
1115 
1116 KCal::Event* KarmStorage::baseEvent(const Task * task)
1117 {
1118  KCal::Event* e;
1119  TQStringList categories;
1120 
1121  e = new KCal::Event;
1122  e->setSummary(task->name());
1123 
1124  // Can't use setRelatedToUid()--no error, but no RelatedTo written to disk
1125  e->setRelatedTo(_calendar->todo(task->uid()));
1126 
1127  // Debugging: some events where not getting a related-to field written.
1128  assert(e->relatedTo()->uid() == task->uid());
1129 
1130  // Have to turn this off to get datetimes in date fields.
1131  e->setFloats(false);
1132  e->setDtStart(task->startTime());
1133 
1134  // So someone can filter this mess out of their calendar display
1135  categories.append(i18n("KArm"));
1136  e->setCategories(categories);
1137 
1138  return e;
1139 }
1140 
1141 HistoryEvent::HistoryEvent(TQString uid, TQString name, long duration,
1142  TQDateTime start, TQDateTime stop, TQString todoUid)
1143 {
1144  _uid = uid;
1145  _name = name;
1146  _duration = duration;
1147  _start = start;
1148  _stop = stop;
1149  _todoUid = todoUid;
1150 }
1151 
1152 
1153 TQValueList<HistoryEvent> KarmStorage::getHistory(const TQDate& from,
1154  const TQDate& to)
1155 {
1156  TQValueList<HistoryEvent> retval;
1157  TQStringList processed;
1158  KCal::Event::List events;
1159  KCal::Event::List::iterator event;
1160  TQString duration;
1161 
1162  for(TQDate d = from; d <= to; d = d.addDays(1))
1163  {
1164  events = _calendar->rawEventsForDate( d );
1165  for (event = events.begin(); event != events.end(); ++event)
1166  {
1167 
1168  // KArm events have the custom property X-KDE-Karm-duration
1169  if (! processed.contains( (*event)->uid()))
1170  {
1171  // If an event spans multiple days, CalendarLocal::rawEventsForDate
1172  // will return the same event on both days. To avoid double-counting
1173  // such events, we (arbitrarily) attribute the hours from both days on
1174  // the first day. This mis-reports the actual time spent, but it is
1175  // an easy fix for a (hopefully) rare situation.
1176  processed.append( (*event)->uid());
1177 
1178  duration = (*event)->customProperty(kapp->instanceName(),
1179  TQCString("duration"));
1180  if ( ! duration.isNull() )
1181  {
1182  if ( (*event)->relatedTo()
1183  && ! (*event)->relatedTo()->uid().isEmpty() )
1184  {
1185  retval.append(HistoryEvent(
1186  (*event)->uid(),
1187  (*event)->summary(),
1188  duration.toLong(),
1189  (*event)->dtStart(),
1190  (*event)->dtEnd(),
1191  (*event)->relatedTo()->uid()
1192  ));
1193  }
1194  else
1195  // Something is screwy with the ics file, as this KArm history event
1196  // does not have a todo related to it. Could have been deleted
1197  // manually? We'll continue with report on with report ...
1198  kdDebug(5970) << "KarmStorage::getHistory(): "
1199  << "The event " << (*event)->uid()
1200  << " is not related to a todo. Dropped." << endl;
1201  }
1202  }
1203  }
1204  }
1205 
1206  return retval;
1207 }
1208 
1209 bool KarmStorage::remoteResource( const TQString& file ) const
1210 {
1211  TQString f = file.lower();
1212  bool rval = f.startsWith( "http://" ) || f.startsWith( "ftp://" );
1213 
1214  kdDebug(5970) << "KarmStorage::remoteResource( " << file << " ) returns " << rval << endl;
1215  return rval;
1216 }
1217 
1218 bool KarmStorage::saveCalendar()
1219 {
1220  kdDebug(5970) << "KarmStorage::saveCalendar" << endl;
1221 
1222 #if 0
1223  Event::List evl=_calendar->rawEvents();
1224  kdDebug(5970) << "summary - dtStart - dtEnd" << endl;
1225  for (unsigned int i=0; i<evl.count(); i++)
1226  {
1227  kdDebug() << evl[i]->summary() << evl[i]->dtStart() << evl[i]->dtEnd() << endl;
1228  }
1229 #endif
1230  KABC::Lock *lock = _calendar->lock();
1231  if ( !lock || !lock->lock() )
1232  return false;
1233 
1234  if ( _calendar && _calendar->save() ) {
1235  lock->unlock();
1236  return true;
1237  }
1238 
1239  lock->unlock();
1240  return false;
1241 }