예제 #1
0
static bool compareUids(const QStringList &_uids, const Incidence::List &incidences)
{
    QStringList uids = _uids;

    foreach(const KCalCore::Incidence::Ptr &incidence, incidences) {
        if (uids.contains(incidence->uid()))
            uids.removeAll(incidence->uid());
    }

    if (uids.isEmpty() && _uids.count() == incidences.count()) {
        return true;
    } else {
        qDebug() << uids.count() << incidences.count();
        return false;
    }
}
예제 #2
0
void EventArchiver::run( Calendar* calendar, const QDate& limitDate, QWidget* widget, bool withGUI, bool errorIfNone )
{
  // We need to use rawEvents, otherwise events hidden by filters will not be archived.
  Incidence::List incidences;
  Event::List events;
  Todo::List todos;
  Journal::List journals;
  
  if ( KOPrefs::instance()->mArchiveEvents ) {
    events = calendar->rawEvents(
      QDate( 1769, 12, 1 ),
      // #29555, also advertised by the "limitDate not included" in the class docu
      limitDate.addDays( -1 ),
      true );
  }
  if ( KOPrefs::instance()->mArchiveTodos ) {
    Todo::List t = calendar->rawTodos();
    Todo::List::ConstIterator it;
    for( it = t.begin(); it != t.end(); ++it ) {
      if ( (*it) && ( (*it)->isCompleted() ) &&  ( (*it)->completed().date() < limitDate ) ) {
        todos.append( *it );
      }
    }
  }
  
  incidences = Calendar::mergeIncidenceList( events, todos, journals );
  

  kdDebug(5850) << "EventArchiver: archiving incidences before " << limitDate << " -> " << incidences.count() << " incidences found." << endl;
  if ( incidences.isEmpty() ) {
    if ( withGUI && errorIfNone )
      KMessageBox::information( widget, i18n("There are no items before %1")
                          .arg(KGlobal::locale()->formatDate(limitDate)),
                          "ArchiverNoIncidences" );
    return;
  }


  switch ( KOPrefs::instance()->mArchiveAction ) {
  case KOPrefs::actionDelete:
    deleteIncidences( calendar, limitDate, widget, incidences, withGUI );
    break;
  case KOPrefs::actionArchive:
    archiveIncidences( calendar, limitDate, widget, incidences, withGUI );
    break;
  }
}
CallId Scheduler::acceptCancel( const IncidenceBase::Ptr &incidence,
                                ScheduleMessage::Status status,
                                const QString &attendee )
{
  Incidence::Ptr inc = incidence.staticCast<Incidence>();
  if ( !inc ) {
    return -1;
  }

  const CallId callId = ++d->mLatestCallId;
  ResultCode resultCode = ResultCodeSuccess;
  QString errorMessage;

  if ( inc->type() == IncidenceBase::TypeFreeBusy ) {
    // reply to this request is handled in korganizer's incomingdialog
    emitOperationFinished( callId, resultCode, errorMessage );
    return callId;
  }

  const Incidence::List existingIncidences = d->mCalendar->incidencesFromSchedulingID( inc->uid() );
  kDebug() << "Scheduler::acceptCancel="
           << Stringify::scheduleMessageStatus( status ) //krazy2:exclude=kdebug
           << ": found " << existingIncidences.count()
           << " incidences with schedulingID " << inc->schedulingID();

  Incidence::List::ConstIterator incit = existingIncidences.begin();
  for ( ; incit != existingIncidences.end() ; ++incit ) {
    Incidence::Ptr i = *incit;
    kDebug() << "Considering this found event ("
             << ( i->isReadOnly() ? "readonly" : "readwrite" )
             << ") :" << d->mFormat->toString( i );

    // If it's readonly, we can't possible remove it.
    if ( i->isReadOnly() ) {
      continue;
    }

    // Code for new invitations:
    // We cannot check the value of "status" to be RequestNew because
    // "status" comes from a similar check inside libical, where the event
    // is compared to other events in the calendar. But if we have another
    // version of the event around (e.g. shared folder for a group), the
    // status could be RequestNew, Obsolete or Updated.
    kDebug() << "looking in " << i->uid() << "'s attendees";

    // This is supposed to be a new request, not an update - however we want
    // to update the existing one to handle the "clicking more than once
    // on the invitation" case. So check the attendee status of the attendee.
    bool isMine = true;
    const Attendee::List attendees = i->attendees();
    Attendee::List::ConstIterator ait;
    for ( ait = attendees.begin(); ait != attendees.end(); ++ait ) {
      if ( (*ait)->email() == attendee &&
           (*ait)->status() == Attendee::NeedsAction ) {
        // This incidence wasn't created by me - it's probably in a shared
        // folder and meant for someone else, ignore it.
        kDebug() << "ignoring " << i->uid()
                 << " since I'm still NeedsAction there";
        isMine = false;
        break;
      }
    }

    if ( isMine ) {
      //TODO_SERGIO: use ItemDeleteJob and make this async.
      kDebug() << "removing existing incidence " << i->uid();

      Akonadi::Item item = d->mCalendar->itemForIncidenceUid( i->uid() );
      bool emitResult = true;
      if ( item.isValid() ) {
        const int changeId = d->mChanger->deleteIncidence( item );

        if ( changeId >= 0 ) {
          d->mCallIdByChangeId.insert( changeId, callId );
          d->mDeletedIncidenceByChangeId.insert( changeId, i );
          emitResult = false; // will be emitted in the job result's slot.
        } else {
          resultCode = ResultCodeErrorDeletingIncidence;
          errorMessage = QLatin1String( "Error while trying to delete the incidence" );
        }
      } else {
        resultCode = ResultCodeIncidenceNotFound;
        errorMessage = QLatin1String( "Couldn't find incidence in calendar" );
      }

      if ( emitResult ) {
        deleteTransaction( incidence->uid() );
        errorMessage = QLatin1String( "Error deleting incidence" );
        emitOperationFinished( callId, resultCode, errorMessage );
      }

      return callId;
    }
  }

  // in case we didn't find the to-be-removed incidence
  if ( existingIncidences.count() > 0 && inc->revision() > 0 ) {
    KMessageBox::error(
      0,
      i18nc( "@info",
             "The event or task could not be removed from your calendar. "
             "Maybe it has already been deleted or is not owned by you. "
             "Or it might belong to a read-only or disabled calendar." ) );
    resultCode = ResultCodeIncidenceNotFound;
    errorMessage = QLatin1String( "Incidence not found" );
  }
  deleteTransaction( incidence->uid() );
  emitOperationFinished( callId, resultCode, errorMessage );

  return callId;
}
CallId Scheduler::acceptRequest( const IncidenceBase::Ptr &incidence,
                                 ScheduleMessage::Status status,
                                 const QString &email )
{
  Incidence::Ptr inc = incidence.staticCast<Incidence>() ;
  if ( !inc ) {
    kWarning() << "Accept what?";
    return -1;
  }

  const CallId callId = ++d->mLatestCallId;
  ResultCode resultCode = ResultCodeSuccess;
  QString errorMessage;

  if ( inc->type() == IncidenceBase::TypeFreeBusy ) {
    emitOperationFinished( callId, ResultCodeSuccess, QString() );
    // reply to this request is handled in korganizer's incomingdialog
    return callId;
  }

  const Incidence::List existingIncidences = d->mCalendar->incidencesFromSchedulingID( inc->uid() );
  kDebug() << "status=" << Stringify::scheduleMessageStatus( status ) //krazy:exclude=kdebug
           << ": found " << existingIncidences.count()
           << " incidences with schedulingID " << inc->schedulingID()
           << "; uid was = " << inc->uid();

  if ( existingIncidences.isEmpty() ) {
    // Perfectly normal if the incidence doesn't exist. This is probably
    // a new invitation.
    kDebug() << "incidence not found; calendar = " << d->mCalendar.data()
             << "; incidence count = " << d->mCalendar->incidences().count();
  }
  Incidence::List::ConstIterator incit = existingIncidences.begin();
  for ( ; incit != existingIncidences.end() ; ++incit ) {
    Incidence::Ptr existingIncidence = *incit;
    kDebug() << "Considering this found event ("
             << ( existingIncidence->isReadOnly() ? "readonly" : "readwrite" )
             << ") :" << d->mFormat->toString( existingIncidence );
    // If it's readonly, we can't possible update it.
    if ( existingIncidence->isReadOnly() ) {
      continue;
    }
    if ( existingIncidence->revision() <= inc->revision() ) {
      // The new incidence might be an update for the found one
      bool isUpdate = true;
      // Code for new invitations:
      // If you think we could check the value of "status" to be RequestNew:  we can't.
      // It comes from a similar check inside libical, where the event is compared to
      // other events in the calendar. But if we have another version of the event around
      // (e.g. shared folder for a group), the status could be RequestNew, Obsolete or Updated.
      kDebug() << "looking in " << existingIncidence->uid() << "'s attendees";
      // This is supposed to be a new request, not an update - however we want to update
      // the existing one to handle the "clicking more than once on the invitation" case.
      // So check the attendee status of the attendee.
      const Attendee::List attendees = existingIncidence->attendees();
      Attendee::List::ConstIterator ait;
      for ( ait = attendees.begin(); ait != attendees.end(); ++ait ) {
        if( (*ait)->email() == email && (*ait)->status() == Attendee::NeedsAction ) {
          // This incidence wasn't created by me - it's probably in a shared folder
          // and meant for someone else, ignore it.
          kDebug() << "ignoring "
                   << existingIncidence->uid() << " since I'm still NeedsAction there";
          isUpdate = false;
          break;
        }
      }
      if ( isUpdate ) {
        if ( existingIncidence->revision() == inc->revision() &&
             existingIncidence->lastModified() > inc->lastModified() ) {
          // This isn't an update - the found incidence was modified more recently
          deleteTransaction( existingIncidence->uid() );
          errorMessage = QLatin1String( "This isn't an update - "
                                        "the found incidence was modified more recently" );
          kDebug() << errorMessage;
          emitOperationFinished( callId, ResultCodeNotUpdate, errorMessage );

          return callId;
        }
        kDebug() << "replacing existing incidence " << existingIncidence->uid();

        bool emitResult = true;
        const QString oldUid = existingIncidence->uid();
        if ( existingIncidence->type() != inc->type() ) {
          errorMessage = QLatin1String( "Cannot assign two different incidence types" );
          resultCode = ResultCodeDifferentIncidenceTypes;
        } else {
          IncidenceBase *existingIncidenceBase = existingIncidence.data();
          IncidenceBase *incBase = inc.data();
          *existingIncidenceBase = *incBase;
          existingIncidence->setSchedulingID( inc->uid(), oldUid );

          Akonadi::Item item = d->mCalendar->itemForIncidenceUid( oldUid );
          item.setPayload<Incidence::Ptr>( existingIncidence );
          if ( item.isValid() ) {
            const int changeId = d->mChanger->modifyIncidence( item );

            if ( changeId >= 0 ) {
              d->mCallIdByChangeId.insert( changeId, callId );
              emitResult = false; // will be emitted in the job result's slot.
            } else {
              resultCode = ResultCodeErrorUpdatingIncidence;
              errorMessage = QLatin1String( "Error while trying to update the incidence" );
            }
          } else {
            resultCode = ResultCodeIncidenceNotFound;
            errorMessage = QLatin1String( "Couldn't find incidence in calendar" );
          }
        }

        if ( emitResult ) {
          deleteTransaction( incidence->uid() );
          emitOperationFinished( callId, resultCode, errorMessage );
        }
        return callId;
      }
    } else {
      // This isn't an update - the found incidence has a bigger revision number

      deleteTransaction( incidence->uid() );

      errorMessage = QLatin1String( "This isn't an update - "
                                    "the found incidence has a bigger revision number" );
      kDebug() << errorMessage;
      emitOperationFinished( callId, ResultCodeNotUpdate, errorMessage );

      return callId;
    }
  }

  // Move the uid to be the schedulingID and make a unique UID
  inc->setSchedulingID( inc->uid(), CalFormat::createUniqueId() );
  // notify the user in case this is an update and we didn't find the to-be-updated incidence
  if ( existingIncidences.count() == 0 && inc->revision() > 0 ) {
    KMessageBox::information(
      0,
      i18nc( "@info",
             "<para>You accepted an invitation update, but an earlier version of the "
             "item could not be found in your calendar.</para>"
             "<para>This may have occurred because:<list>"
             "<item>the organizer did not include you in the original invitation</item>"
             "<item>you did not accept the original invitation yet</item>"
             "<item>you deleted the original invitation from your calendar</item>"
             "<item>you no longer have access to the calendar containing the invitation</item>"
             "</list></para>"
             "<para>This is not a problem, but we thought you should know.</para>" ),
      i18nc( "@title", "Cannot find invitation to be updated" ), "AcceptCantFindIncidence" );
  }
  kDebug() << "Storing new incidence with scheduling uid=" << inc->schedulingID()
           << " and uid=" << inc->uid();
  const int changeId = d->mChanger->createIncidence( inc );

  if ( changeId > 0 ) {
    d->mCallIdByChangeId[changeId] = callId;
  } else {
    emitOperationFinished( callId, ResultCodeErrorCreatingIncidence,
                           QLatin1String( "Error creating incidence" ) );
  }

  return callId;
}
예제 #5
0
void HtmlExport::createTodo (QTextStream *ts,Todo *todo)
{
    kdDebug(5850) << "HtmlExport::createTodo()" << endl;

    bool completed = todo->isCompleted();
    Incidence::List relations = todo->relations();

    *ts << "<tr>\n";

    *ts << "  <td class=\"sum";
    if (completed) *ts << "done";
    *ts << "\">\n";
    *ts << "    <a name=\"" << todo->uid() << "\"></a>\n";
    *ts << "    <b>" << cleanChars(todo->summary()) << "</b>\n";
    if (!todo->description().isEmpty()) {
        *ts << "    <p>" << breakString(cleanChars(todo->description())) << "</p>\n";
    }
    if (relations.count()) {
        *ts << "    <div align=\"right\"><a href=\"#sub" << todo->uid()
            << "\">" << i18n("Sub-Tasks") << "</a></div>\n";
    }
    *ts << "  </td>\n";

    *ts << "  <td";
    if (completed) *ts << " class=\"done\"";
    *ts << ">\n";
    *ts << "    " << todo->priority() << "\n";
    *ts << "  </td>\n";

    *ts << "  <td";
    if (completed) *ts << " class=\"done\"";
    *ts << ">\n";
    *ts << "    " << i18n("%1 %").arg(todo->percentComplete()) << "\n";
    *ts << "  </td>\n";

    if ( mSettings->taskDueDate() ) {
        *ts << "  <td";
        if (completed) *ts << " class=\"done\"";
        *ts << ">\n";
        if (todo->hasDueDate()) {
            *ts << "    " << todo->dtDueDateStr() << "\n";
        } else {
            *ts << "    &nbsp;\n";
        }
        *ts << "  </td>\n";
    }

    if ( mSettings->taskLocation() ) {
        *ts << "  <td";
        if (completed) *ts << " class=\"done\"";
        *ts << ">\n";
        formatLocation(ts,todo);
        *ts << "  </td>\n";
    }

    if ( mSettings->taskCategories() ) {
        *ts << "  <td";
        if (completed) *ts << " class=\"done\"";
        *ts << ">\n";
        formatCategories(ts,todo);
        *ts << "  </td>\n";
    }

    if ( mSettings->taskAttendees() ) {
        *ts << "  <td";
        if (completed) *ts << " class=\"done\"";
        *ts << ">\n";
        formatAttendees(ts,todo);
        *ts << "  </td>\n";
    }

    *ts << "</tr>\n";
}
예제 #6
0
void HtmlExport::createTodoList ( QTextStream *ts )
{
    Todo::List rawTodoList = mCalendar->todos();

    Todo::List::Iterator it = rawTodoList.begin();
    while ( it != rawTodoList.end() ) {
        Todo *ev = *it;
        Todo *subev = ev;
        if ( ev->relatedTo() ) {
            if ( ev->relatedTo()->type()=="Todo" ) {
                if ( rawTodoList.find( static_cast<Todo *>( ev->relatedTo() ) ) ==
                        rawTodoList.end() ) {
                    rawTodoList.append( static_cast<Todo *>( ev->relatedTo() ) );
                }
            }
        }
        it = rawTodoList.find( subev );
        ++it;
    }

    // FIXME: Sort list by priorities. This is brute force and should be
    // replaced by a real sorting algorithm.
    Todo::List todoList;
    for ( int i = 1; i <= 9; ++i ) {
        for( it = rawTodoList.begin(); it != rawTodoList.end(); ++it ) {
            if ( (*it)->priority() == i && checkSecrecy( *it ) ) {
                todoList.append( *it );
            }
        }
    }
    for( it = rawTodoList.begin(); it != rawTodoList.end(); ++it ) {
        if ( (*it)->priority() == 0 && checkSecrecy( *it ) ) {
            todoList.append( *it );
        }
    }

    int columns = 3;
    *ts << "<table border=\"0\" cellpadding=\"3\" cellspacing=\"3\">\n";
    *ts << "  <tr>\n";
    *ts << "    <th class=\"sum\">" << i18n("Task") << "</th>\n";
    *ts << "    <th>" << i18n("Priority") << "</th>\n";
    *ts << "    <th>" << i18n("Completed") << "</th>\n";
    if ( mSettings->taskDueDate() ) {
        *ts << "    <th>" << i18n("Due Date") << "</th>\n";
        ++columns;
    }
    if ( mSettings->taskLocation() ) {
        *ts << "    <th>" << i18n("Location") << "</th>\n";
        ++columns;
    }
    if ( mSettings->taskCategories() ) {
        *ts << "    <th>" << i18n("Categories") << "</th>\n";
        ++columns;
    }
    if ( mSettings->taskAttendees() ) {
        *ts << "    <th>" << i18n("Attendees") << "</th>\n";
        ++columns;
    }
    *ts << "  </tr>\n";

    // Create top-level list.
    for( it = todoList.begin(); it != todoList.end(); ++it ) {
        if ( !(*it)->relatedTo() ) createTodo( ts, *it );
    }

    // Create sub-level lists
    for( it = todoList.begin(); it != todoList.end(); ++it ) {
        Incidence::List relations = (*it)->relations();
        if (relations.count()) {
            // Generate sub-task list of event ev
            *ts << "  <tr>\n";
            *ts << "    <td class=\"subhead\" colspan=";
            *ts << "\"" << QString::number(columns) << "\"";
            *ts << "><a name=\"sub" << (*it)->uid() << "\"></a>"
                << i18n("Sub-Tasks of: ") << "<a href=\"#"
                << (*it)->uid() << "\"><b>" << cleanChars( (*it)->summary())
                << "</b></a></td>\n";
            *ts << "  </tr>\n";

            Todo::List sortedList;
            // FIXME: Sort list by priorities. This is brute force and should be
            // replaced by a real sorting algorithm.
            for ( int i = 1; i <= 9; ++i ) {
                Incidence::List::ConstIterator it2;
                for( it2 = relations.begin(); it2 != relations.end(); ++it2 ) {
                    Todo *ev3 = dynamic_cast<Todo *>( *it2 );
                    if ( ev3 && ev3->priority() == i ) sortedList.append( ev3 );
                }
            }
            Incidence::List::ConstIterator it2;
            for( it2 = relations.begin(); it2 != relations.end(); ++it2 ) {
                Todo *ev3 = dynamic_cast<Todo *>( *it2 );
                if ( ev3 && ev3->priority() == 0 ) sortedList.append( ev3 );
            }

            Todo::List::ConstIterator it3;
            for( it3 = sortedList.begin(); it3 != sortedList.end(); ++it3 ) {
                createTodo( ts, *it3 );
            }
        }
    }

    *ts << "</table>\n";
}
예제 #7
0
void HtmlExport::createTodo(QTextStream *ts, const Todo::Ptr &todo)
{
    qCDebug(KCALUTILS_LOG);

    const bool completed = todo->isCompleted();

    Incidence::List relations = d->mCalendar->relations(todo->uid());

    *ts << "<tr>" << endl;

    *ts << "  <td class=\"sum";
    if (completed) {
        *ts << "done";
    }
    *ts << "\">" << endl;
    *ts << "    <a name=\"" << todo->uid() << "\"></a>" << endl;
    *ts << "    <b>" << cleanChars(todo->summary()) << "</b>" << endl;
    if (!todo->description().isEmpty()) {
        *ts << "    <p>" << breakString(cleanChars(todo->description())) << "</p>" << endl;
    }
    if (relations.count()) {
        *ts << "    <div align=\"right\"><a href=\"#sub" << todo->uid()
            << "\">" << i18nc("@title:column sub-to-dos of the parent to-do",
                              "Sub-To-dos") << "</a></div>" << endl;
    }
    *ts << "  </td>" << endl;

    *ts << "  <td";
    if (completed) {
        *ts << " class=\"done\"";
    }
    *ts << ">" << endl;
    *ts << "    " << todo->priority() << endl;
    *ts << "  </td>" << endl;

    *ts << "  <td";
    if (completed) {
        *ts << " class=\"done\"";
    }
    *ts << ">" << endl;
    *ts << "    " << i18nc("@info to-do percent complete",
                           "%1 %", todo->percentComplete()) << endl;
    *ts << "  </td>" << endl;

    if (d->mSettings->taskDueDate()) {
        *ts << "  <td";
        if (completed) {
            *ts << " class=\"done\"";
        }
        *ts << ">" << endl;
        if (todo->hasDueDate()) {
            *ts << "    " << Stringify::formatDate(todo->dtDue(true)) << endl;
        } else {
            *ts << "    &nbsp;" << endl;
        }
        *ts << "  </td>" << endl;
    }

    if (d->mSettings->taskLocation()) {
        *ts << "  <td";
        if (completed) {
            *ts << " class=\"done\"";
        }
        *ts << ">" << endl;
        formatLocation(ts, todo);
        *ts << "  </td>" << endl;
    }

    if (d->mSettings->taskCategories()) {
        *ts << "  <td";
        if (completed) {
            *ts << " class=\"done\"";
        }
        *ts << ">" << endl;
        formatCategories(ts, todo);
        *ts << "  </td>" << endl;
    }

    if (d->mSettings->taskAttendees()) {
        *ts << "  <td";
        if (completed) {
            *ts << " class=\"done\"";
        }
        *ts << ">" << endl;
        formatAttendees(ts, todo);
        *ts << "  </td>" << endl;
    }

    *ts << "</tr>" << endl;
}
예제 #8
0
void HtmlExport::createTodoList(QTextStream *ts)
{
    Todo::List rawTodoList = d->mCalendar->todos();

    int index = 0;
    while (index < rawTodoList.count()) {
        Todo::Ptr ev = rawTodoList[ index ];
        Todo::Ptr subev = ev;
        const QString uid = ev->relatedTo();
        if (!uid.isEmpty()) {
            Incidence::Ptr inc = d->mCalendar->incidence(uid);
            if (inc && inc->type() == Incidence::TypeTodo) {
                Todo::Ptr todo = inc.staticCast<Todo>();
                if (!rawTodoList.contains(todo)) {
                    rawTodoList.append(todo);
                }
            }
        }
        index = rawTodoList.indexOf(subev);
        ++index;
    }

    // FIXME: Sort list by priorities. This is brute force and should be
    // replaced by a real sorting algorithm.
    Todo::List todoList;
    Todo::List::ConstIterator it;
    for (int i = 1; i <= 9; ++i) {
        for (it = rawTodoList.constBegin(); it != rawTodoList.constEnd(); ++it) {
            if ((*it)->priority() == i && checkSecrecy(*it)) {
                todoList.append(*it);
            }
        }
    }
    for (it = rawTodoList.constBegin(); it != rawTodoList.constEnd(); ++it) {
        if ((*it)->priority() == 0 && checkSecrecy(*it)) {
            todoList.append(*it);
        }
    }

    int columns = 3;
    *ts << "<table border=\"0\" cellpadding=\"3\" cellspacing=\"3\">" << endl;
    *ts << "  <tr>" << endl;
    *ts << "    <th class=\"sum\">" << i18nc("@title:column", "To-do") << "</th>" << endl;
    *ts << "    <th>" << i18nc("@title:column to-do priority", "Priority") << "</th>" << endl;
    *ts << "    <th>" << i18nc("@title:column to-do percent completed",
                               "Completed") << "</th>" << endl;
    if (d->mSettings->taskDueDate()) {
        *ts << "    <th>" << i18nc("@title:column to-do due date", "Due Date") << "</th>" << endl;
        ++columns;
    }
    if (d->mSettings->taskLocation()) {
        *ts << "    <th>" << i18nc("@title:column to-do location", "Location") << "</th>" << endl;
        ++columns;
    }
    if (d->mSettings->taskCategories()) {
        *ts << "    <th>" << i18nc("@title:column to-do categories", "Categories") << "</th>" << endl;
        ++columns;
    }
    if (d->mSettings->taskAttendees()) {
        *ts << "    <th>" << i18nc("@title:column to-do attendees", "Attendees") << "</th>" << endl;
        ++columns;
    }
    *ts << "  </tr>" << endl;

    // Create top-level list.
    for (it = todoList.constBegin(); it != todoList.constEnd(); ++it) {
        if ((*it)->relatedTo().isEmpty()) {
            createTodo(ts, *it);
        }
    }

    // Create sub-level lists
    for (it = todoList.constBegin(); it != todoList.constEnd(); ++it) {
        Incidence::List relations =  d->mCalendar->relations((*it)->uid());

        if (relations.count()) {
            // Generate sub-to-do list
            *ts << "  <tr>" << endl;
            *ts << "    <td class=\"subhead\" colspan=";
            *ts << "\"" << QString::number(columns) << "\"";
            *ts << "><a name=\"sub" << (*it)->uid() << "\"></a>"
                << i18nc("@title:column sub-to-dos of the parent to-do",
                         "Sub-To-dos of: ") << "<a href=\"#"
                << (*it)->uid() << "\"><b>" << cleanChars((*it)->summary())
                << "</b></a></td>" << endl;
            *ts << "  </tr>" << endl;

            Todo::List sortedList;
            // FIXME: Sort list by priorities. This is brute force and should be
            // replaced by a real sorting algorithm.
            for (int i = 1; i <= 9; ++i) {
                Incidence::List::ConstIterator it2;
                for (it2 = relations.constBegin(); it2 != relations.constEnd(); ++it2) {
                    Todo::Ptr ev3 = (*it2).staticCast<Todo>();
                    if (ev3 && ev3->priority() == i) {
                        sortedList.append(ev3);
                    }
                }
            }
            Incidence::List::ConstIterator it2;
            for (it2 = relations.constBegin(); it2 != relations.constEnd(); ++it2) {
                Todo::Ptr ev3 = (*it2).staticCast<Todo>();
                if (ev3 && ev3->priority() == 0) {
                    sortedList.append(ev3);
                }
            }

            Todo::List::ConstIterator it3;
            for (it3 = sortedList.constBegin(); it3 != sortedList.constEnd(); ++it3) {
                createTodo(ts, *it3);
            }
        }
    }

    *ts << "</table>" << endl;
}