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" // tdeApp
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
66using namespace std;
67
68KarmStorage *KarmStorage::_instance = 0;
69static long linenr; // how many lines written by printTaskHistory so far
70
71
72KarmStorage *KarmStorage::instance()
73{
74 if (_instance == 0) _instance = new KarmStorage();
75 return _instance;
76}
77
78KarmStorage::KarmStorage()
79{
80 _calendar = 0;
81}
82
83TQString 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
216TQString KarmStorage::icalfile()
217{
218 kdDebug(5970) << "Entering KarmStorage::icalfile" << endl;
219 return _icalfile;
220}
221
222TQString 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
296void 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
308TQString 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
339TQString 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
373bool 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
482bool 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
557void 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//
575TQString 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 ) tdeApp->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 ) tdeApp->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
704TQString 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
758void 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
777long 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
873TQString 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
887TQString 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
1052void 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
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( tdeApp->instanceName(),
1073 TQCString("duration"),
1074 TQString::number(durationInSeconds));
1075
1076 return _calendar->addEvent(e);
1077}
1078
1079void 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( tdeApp->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
1116KCal::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
1141HistoryEvent::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
1153TQValueList<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(tdeApp->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
1209bool 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
1218bool 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
long duration()
In seconds.
Definition: karmstorage.h:365
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
TaskView * taskView() const
Return task view for this task.
Definition: task.h:65
TQString name() const
returns the name of this task.
Definition: task.h:162
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
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
Task * firstChild() const
return parent Task or null in case of TaskView.
Definition: task.h:60
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