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 "tdeapplication.h" // kapp
39 #include <kdebug.h>
40 #include <tdeemailsettings.h>
41 #include <tdelocale.h> // i18n
42 #include <tdemessagebox.h>
43 #include <kprogress.h>
44 #include <tdetempfile.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 <tdeio/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, TQ_SIGNAL(resourceChanged(ResourceCalendar *)),
136  view, TQ_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 (!TDEIO::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(TDEGlobal::locale()->formatDate(from))
921  .arg(TDEGlobal::locale()->formatDate(to));
922  retval += cr;
923  retval += i18n("Printed on: %1")
924  .arg(TDEGlobal::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 (!TDEIO::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-TDE-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  TDEABC::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 }
One start/stop event that has been logged.
Definition: karmstorage.h:356
HistoryEvent()
Needed to be used in a value list.
Definition: karmstorage.h:359
Singleton to store/retrieve KArm data to/from persistent storage.
Definition: karmstorage.h:68
bool isNewStorage(const Preferences *preferences) const
Check if iCalendar file name in the preferences has changed since the last call to load.
TQString loadFromFlatFile(TaskView *taskview, const TQString &filename)
Read tasks and their total times from a text file (legacy storage).
TQString loadFromFlatFileCumulative(TaskView *taskview, const TQString &filename)
Reads tasks and their total times from text file (legacy).
bool isEmpty()
Check if the iCalendar file currently loaded has any Todos in it.
bool removeTask(Task *task)
Remove this task from iCalendar file.
bool bookTime(const Task *task, const TQDateTime &startDateTime, long durationInSeconds)
Book time to a task.
TQString addTask(const Task *task, const Task *parent)
Add this task from iCalendar file.
void stopTimer(const Task *task, TQDateTime when=TQDateTime::currentDateTime())
Log the event that the timer has stopped for this task.
TQString report(TaskView *taskview, const ReportCriteria &rc)
Output a report based on contents of ReportCriteria.
TQValueList< HistoryEvent > getHistory(const TQDate &from, const TQDate &to)
Return a list of start/stop events for the given date range.
void changeTime(const Task *task, const long deltaSeconds)
Log the change in a task's time.
void addComment(const Task *task, const TQString &comment)
Log a new comment for this task.
Provide an interface to the configuration options for the program.
Definition: preferences.h:17
Stores entries from export dialog.
bool decimalMinutes
True if the durations should be output in decimal hours.
TQString quote
The quote to use for text fields when outputting comma-seperated reports.
TQDate to
For history reports, the upper bound of the date range to report on.
bool allTasks
True if the report should contain all tasks in Karm.
KURL url
For reports that write to a file, the filename to write to.
TQString delimiter
The delimiter to use when outputting comma-seperated value reports.
REPORTTYPE reportType
The type of report we are running.
TQDate from
For history reports, the lower bound of the date range to report on.
Container and interface for the tasks.
Definition: taskview.h:43
Preferences * preferences()
Return preferences user selected on settings dialog.
Definition: taskview.cpp:363
Task * first_child() const
Return the first item in the view, cast to a Task pointer.
Definition: taskview.cpp:172
long count()
Return the total number if items in the view.
Definition: taskview.cpp:379
void refresh()
Used to refresh (e.g.
Definition: taskview.cpp:248
Task * current_item() const
Return the current item in the view, cast to a Task pointer.
Definition: taskview.cpp:177
Task * item_at_index(int i)
Return the i'th item (zero-based), cast to a Task pointer.
Definition: taskview.cpp:182
void clearActiveTasks()
clears all active tasks.
Definition: taskview.cpp:408
void startTimerFor(Task *task, TQDateTime startTime=TQDateTime::currentDateTime())
starts timer for task.
Definition: taskview.cpp:386
void scheduleSave()
Schedule that we should save very soon.
Definition: taskview.cpp:356
TQValueList< HistoryEvent > getHistory(const TQDate &from, const TQDate &to) const
Return list of start/stop events for given date range.
Definition: taskview.cpp:782
A class representing a task.
Definition: task.h:42
TQDateTime lastStart()
delivers when the task was started last
Definition: task.h:238
void changeTimes(long minutesSession, long minutes, KarmStorage *storage=0)
Add minutes to time and session time, and write to storage.
Definition: task.cpp:213
TQString name() const
returns the name of this task.
Definition: task.h:162
Task * firstChild() const
return parent Task or null in case of TaskView.
Definition: task.h:60
TQDateTime startTime() const
Return time the task was started.
Definition: task.h:137
KCal::Todo * asTodo(KCal::Todo *calendar) const
Load the todo passed in with this tasks info.
Definition: task.cpp:296
void setPixmapProgress()
Sets an appropriate icon for this task based on its level of completion.
Definition: task.cpp:184
TQString uid() const
Return unique iCalendar Todo ID for this task.
Definition: task.h:70
TaskView * taskView() const
Return task view for this task.
Definition: task.h:65
void move(Task *destination)
cut Task out of parent Task or the TaskView and into the destination Task
Definition: task.cpp:399
bool isRunning() const
return the state of a task - if it's running or not
Definition: task.cpp:132
void cut()
cut Task out of parent Task or the TaskView
Definition: task.cpp:389
TQString comment() const
Retrieve the entire comment for the task.
Definition: task.cpp:426
void setUid(const TQString uid)
Set unique id for the task.
Definition: task.cpp:128