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