diff --git a/akonadi/calendar/incidencechanger.cpp b/akonadi/calendar/incidencechanger.cpp index cd81ce7b5..34f47c5e1 100644 --- a/akonadi/calendar/incidencechanger.cpp +++ b/akonadi/calendar/incidencechanger.cpp @@ -1,1602 +1,1602 @@ /* Copyright (C) 2004 Reinhold Kainhofer Copyright (C) 2010-2012 Sérgio Martins This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "incidencechanger.h" #include "incidencechanger_p.h" #include "mailscheduler_p.h" #include "utils_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; using namespace KCalCore; #ifdef PLEASE_TEST_INVITATIONS # define RUNNING_UNIT_TESTS true #else # define RUNNING_UNIT_TESTS false #endif ITIPHandlerDialogDelegate::Action actionFromStatus(ITIPHandlerHelper::SendResult result) { //enum SendResult { // Canceled, /**< Sending was canceled by the user, meaning there are // local changes of which other attendees are not aware. */ // FailKeepUpdate, /**< Sending failed, the changes to the incidence must be kept. */ // FailAbortUpdate, /**< Sending failed, the changes to the incidence must be undone. */ // NoSendingNeeded, /**< In some cases it is not needed to send an invitation // (e.g. when we are the only attendee) */ // Success switch (result) { case ITIPHandlerHelper::ResultCanceled: return ITIPHandlerDialogDelegate::ActionDontSendMessage; case ITIPHandlerHelper::ResultSuccess: return ITIPHandlerDialogDelegate::ActionSendMessage; default: return ITIPHandlerDialogDelegate::ActionAsk; } } class ActAsDialog: public QDialog { public: explicit ActAsDialog(const QStringList &actAsList, QWidget *parent) :QDialog(parent) { mActAsComboBox = new QComboBox(); QVBoxLayout *layout = new QVBoxLayout(this); foreach(const QString & actor, actAsList) { mActAsComboBox->addItem(actor, actor); } QLabel *label= new QLabel(); label->setText(i18n("You can change the incidence with different identities,\nplease select the identity, as whom you want to act.")); QPushButton *ok = new QPushButton(); ok->setText(QLatin1String("Ok")); ok->setDefault(true); connect(ok, SIGNAL(clicked(bool)), this, SLOT(accept())); layout->addWidget(label); layout->addWidget(mActAsComboBox); layout->addWidget(ok); } QString actAs() { int index = mActAsComboBox->currentIndex(); return mActAsComboBox->itemData(index, Qt::UserRole).toString(); } private: QComboBox *mActAsComboBox; }; QString actAs(Incidence::Ptr incidence, Change::Ptr change) { if (!change->actAs.isEmpty()) { return change->actAs; } QStringList mes; const QString email = incidence->organizer()->email(); const Attendee::List &attendees = incidence->attendees(); if (Akonadi::CalendarUtils::thatIsMe(email)) { mes.append(email); } for (int i = 0; i < attendees.size(); i++) { if (!mes.contains(attendees[i]->email()) && Akonadi::CalendarUtils::thatIsMe(attendees[i])) { mes.append(attendees[i]->email()); } } //TODO: ask user as what person he wanna act. if (mes.count() > 1) { ActAsDialog dlg(mes, change->parentWidget); dlg.exec(); change->actAs = dlg.actAs(); - } else { + } else if (mes.count() == 1) { change->actAs = mes.first(); } return change->actAs; } bool weAreOrganizer(Incidence::Ptr incidence, Change::Ptr change) { const QString email = incidence->organizer()->email(); const QString me = actAs(incidence, change); return me == email; } bool allowedModificationsWithoutRevisionUpdate(Incidence::Ptr incidence) { // Modifications that are per user allowd without getting outofsync with organisator // * if only alarm settings are modified. // * if we add/modify the SchedulingId const QSet dirtyFields = incidence->dirtyFields(); QSet allowedModifications; allowedModifications << IncidenceBase::FieldAlarms << IncidenceBase::FieldLastModified << IncidenceBase::FieldSchedulingId; if ((dirtyFields - allowedModifications).isEmpty()) { return true; } return false; } bool allowedAttendeeModifications(Incidence::Ptr incidence, const Attendee::List &oldAttendees) { //The user can change the partstat for all identities of his if (incidence->dirtyFields().contains(IncidenceBase::FieldAttendees)) { const Attendee::List &attendees = incidence->attendees(); if (attendees.size() != oldAttendees.size()) { return false; } for (int i = 0; i < attendees.size(); i++) { if (attendees[i]->email() != oldAttendees[i]->email()) { return false; } if (attendees[i]->status() != oldAttendees[i]->status()) { if (Akonadi::CalendarUtils::thatIsMe(attendees[i])) { kDebug() << "Allowing modification of partstat for: " << attendees[i]->email(); continue; } else { return false; } } } } return true; } Attendee::List getChangedAttendees(Incidence::Ptr incidence, const Attendee::List &oldAttendees) { Attendee::List list; if (!allowedAttendeeModifications(incidence, oldAttendees)) { kWarning() << "Attendee modifications not allowed"; return Attendee::List(); } //The user can change the partstat for all identities of his if (incidence->dirtyFields().contains(IncidenceBase::FieldAttendees)) { const Attendee::List &attendees = incidence->attendees(); Q_ASSERT(attendees.size() == oldAttendees.size()); for (int i = 0; i < attendees.size(); i++) { Q_ASSERT(attendees[i]->email() == oldAttendees[i]->email()); if (attendees[i]->status() != oldAttendees[i]->status()) { if (Akonadi::CalendarUtils::thatIsMe(attendees[i])) { kDebug() << "Allowing modification of partstat for: " << attendees[i]->email(); list << attendees[i]; } else { Q_ASSERT(false); } } } } return list; } void resetAttendeeStatus(Incidence::Ptr incidence, Change::Ptr change) { //Reset attendee status, when resceduling QSet resetPartStatus; resetPartStatus << IncidenceBase::FieldDtStart << IncidenceBase::FieldDtEnd << IncidenceBase::FieldDtStart << IncidenceBase::FieldLocation << IncidenceBase::FieldDtDue << IncidenceBase::FieldDuration << IncidenceBase::FieldRecurrence; if (!(incidence->dirtyFields() & resetPartStatus).isEmpty() && weAreOrganizer(incidence, change)) { foreach (const Attendee::Ptr &attendee, incidence->attendees()) { if ( attendee->role() != Attendee::NonParticipant && attendee->status() != Attendee::Delegated && !Akonadi::CalendarUtils::thatIsMe(attendee)) { attendee->setStatus(Attendee::NeedsAction); attendee->setRSVP(true); } } } } namespace Akonadi { // Does a queued emit, with QMetaObject::invokeMethod static void emitCreateFinished(IncidenceChanger *changer, int changeId, const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString) { QMetaObject::invokeMethod(changer, "createFinished", Qt::QueuedConnection, Q_ARG(int, changeId), Q_ARG(Akonadi::Item, item), Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode), Q_ARG(QString, errorString)); } // Does a queued emit, with QMetaObject::invokeMethod static void emitModifyFinished(IncidenceChanger *changer, int changeId, const Akonadi::Item &item, IncidenceChanger::ResultCode resultCode, const QString &errorString) { QMetaObject::invokeMethod(changer, "modifyFinished", Qt::QueuedConnection, Q_ARG(int, changeId), Q_ARG(Akonadi::Item, item), Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode), Q_ARG(QString, errorString)); } // Does a queued emit, with QMetaObject::invokeMethod static void emitDeleteFinished(IncidenceChanger *changer, int changeId, const QVector &itemIdList, IncidenceChanger::ResultCode resultCode, const QString &errorString) { QMetaObject::invokeMethod(changer, "deleteFinished", Qt::QueuedConnection, Q_ARG(int, changeId), Q_ARG(QVector, itemIdList), Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode), Q_ARG(QString, errorString)); } } class ConflictPreventerPrivate; class ConflictPreventer { friend class ConflictPreventerPrivate; public: static ConflictPreventer* self(); // To avoid conflicts when the two modifications came from within the same application QHash mLatestRevisionByItemId; private: ConflictPreventer() {} ~ConflictPreventer() {} }; class ConflictPreventerPrivate { public: ConflictPreventer instance; }; K_GLOBAL_STATIC(ConflictPreventerPrivate, sConflictPreventerPrivate) ConflictPreventer* ConflictPreventer::self() { return &sConflictPreventerPrivate->instance; } IncidenceChanger::Private::Private(bool enableHistory, ITIPHandlerComponentFactory *factory, IncidenceChanger *qq) : q(qq) { mLatestChangeId = 0; mShowDialogsOnError = true; mFactory = factory ? factory : new ITIPHandlerComponentFactory(this); mHistory = enableHistory ? new History(this) : 0; mUseHistory = enableHistory; mDestinationPolicy = DestinationPolicyDefault; mRespectsCollectionRights = false; mGroupwareCommunication = false; mLatestAtomicOperationId = 0; mBatchOperationInProgress = false; mAutoAdjustRecurrence = true; m_collectionFetchJob = 0; m_invitationPolicy = InvitationPolicyAsk; qRegisterMetaType >("QVector"); qRegisterMetaType("Akonadi::Item::Id"); qRegisterMetaType("Akonadi::Item"); qRegisterMetaType( "Akonadi::IncidenceChanger::ResultCode"); qRegisterMetaType("ITIPHandlerHelper::SendResult"); } IncidenceChanger::Private::~Private() { if (!mAtomicOperations.isEmpty() || !mQueuedModifications.isEmpty() || !mModificationsInProgress.isEmpty()) { kDebug() << "Normal if the application was being used. " "But might indicate a memory leak if it wasn't"; } } bool IncidenceChanger::Private::atomicOperationIsValid(uint atomicOperationId) const { // Changes must be done between startAtomicOperation() and endAtomicOperation() return mAtomicOperations.contains(atomicOperationId) && !mAtomicOperations[atomicOperationId]->m_endCalled; } bool IncidenceChanger::Private::hasRights(const Collection &collection, IncidenceChanger::ChangeType changeType) const { bool result = false; switch (changeType) { case ChangeTypeCreate: result = collection.rights() & Akonadi::Collection::CanCreateItem; break; case ChangeTypeModify: result = collection.rights() & Akonadi::Collection::CanChangeItem; break; case ChangeTypeDelete: result = collection.rights() & Akonadi::Collection::CanDeleteItem; break; default: Q_ASSERT_X(false, "hasRights", "invalid type"); } return !collection.isValid() || !mRespectsCollectionRights || result; } Akonadi::Job* IncidenceChanger::Private::parentJob(const Change::Ptr &change) const { return (mBatchOperationInProgress && !change->queuedModification) ? mAtomicOperations[mLatestAtomicOperationId]->transaction() : 0; } void IncidenceChanger::Private::queueModification(Change::Ptr change) { // If there's already a change queued we just discard it // and send the newer change, which already includes // previous modifications const Akonadi::Item::Id id = change->newItem.id(); if (mQueuedModifications.contains(id)) { Change::Ptr toBeDiscarded = mQueuedModifications.take(id); toBeDiscarded->resultCode = ResultCodeModificationDiscarded; toBeDiscarded->completed = true; mChangeById.remove(toBeDiscarded->id); } change->queuedModification = true; mQueuedModifications[id] = change; } void IncidenceChanger::Private::performNextModification(Akonadi::Item::Id id) { mModificationsInProgress.remove(id); if (mQueuedModifications.contains(id)) { const Change::Ptr change = mQueuedModifications.take(id); performModification(change); } } void IncidenceChanger::Private::handleTransactionJobResult(KJob *job) { TransactionSequence *transaction = qobject_cast(job); Q_ASSERT(transaction); Q_ASSERT(mAtomicOperationByTransaction.contains(transaction)); const uint atomicOperationId = mAtomicOperationByTransaction.take(transaction); Q_ASSERT(mAtomicOperations.contains(atomicOperationId)); AtomicOperation *operation = mAtomicOperations[atomicOperationId]; Q_ASSERT(operation); Q_ASSERT(operation->m_id == atomicOperationId); if (job->error()) { if (!operation->rolledback()) operation->setRolledback(); kError() << "Transaction failed, everything was rolledback. " << job->errorString(); } else { Q_ASSERT(operation->m_endCalled); Q_ASSERT(!operation->pendingJobs()); } if (!operation->pendingJobs() && operation->m_endCalled) { delete mAtomicOperations.take(atomicOperationId); mBatchOperationInProgress = false; } else { operation->m_transactionCompleted = true; } } void IncidenceChanger::Private::handleCreateJobResult(KJob *job) { Change::Ptr change = mChangeForJob.take(job); const ItemCreateJob *j = qobject_cast(job); Q_ASSERT(j); Akonadi::Item item = j->item(); QString description; if (change->atomicOperationId != 0) { AtomicOperation *a = mAtomicOperations[change->atomicOperationId]; ++a->m_numCompletedChanges; change->completed = true; description = a->m_description; } // for user undo/redo if (change->recordToHistory) { mHistory->recordCreation(item, description, change->atomicOperationId); } if (j->error()) { const QString errorString = j->errorString(); ResultCode resultCode = ResultCodeJobError; item = change->newItem; kError() << errorString; if (mShowDialogsOnError) { KMessageBox::sorry(change->parentWidget, i18n("Error while trying to create calendar item. Error was: %1", errorString)); } mChangeById.remove(change->id); change->errorString = errorString; change->resultCode = resultCode; // puff, change finally goes out of scope, and emits the incidenceCreated signal. } else { Q_ASSERT(item.isValid()); Q_ASSERT(item.hasPayload()); change->newItem = item; if (change->useGroupwareCommunication) { connect(change.data(),SIGNAL(dialogClosedAfterChange(int,ITIPHandlerHelper::SendResult)), SLOT(handleCreateJobResult2(int,ITIPHandlerHelper::SendResult))); handleInvitationsAfterChange(change); } else { handleCreateJobResult2(change->id, ITIPHandlerHelper::ResultSuccess); } } } void IncidenceChanger::Private::handleCreateJobResult2(int changeId, ITIPHandlerHelper::SendResult status) { Change::Ptr change = mChangeById[changeId]; Akonadi::Item item = change->newItem; mChangeById.remove(changeId); if (status == ITIPHandlerHelper::ResultFailAbortUpdate) { kError() << "Sending invitations failed, but did not delete the incidence"; } const uint atomicOperationId = change->atomicOperationId; if (atomicOperationId != 0) { mInvitationStatusByAtomicOperation.insert(atomicOperationId, status); } change->errorString = QString(); change->resultCode = ResultCodeSuccess; // puff, change finally goes out of scope, and emits the incidenceCreated signal. } void IncidenceChanger::Private::handleDeleteJobResult(KJob *job) { Change::Ptr change = mChangeForJob.take(job); const ItemDeleteJob *j = qobject_cast(job); const Item::List items = j->deletedItems(); QSharedPointer deletionChange = change.staticCast(); foreach(const Akonadi::Item &item, items) { deletionChange->mItemIds.append(item.id()); } QString description; if (change->atomicOperationId != 0) { AtomicOperation *a = mAtomicOperations[change->atomicOperationId]; a->m_numCompletedChanges++; change->completed = true; description = a->m_description; } if (j->error()) { const QString errorString = j->errorString(); kError() << errorString; if (mShowDialogsOnError) { KMessageBox::sorry(change->parentWidget, i18n("Error while trying to delete calendar item. Error was: %1", errorString)); } foreach(const Item &item, items) { // Werent deleted due to error mDeletedItemIds.remove(mDeletedItemIds.indexOf(item.id())); } mChangeById.remove(change->id); change->resultCode = ResultCodeJobError; change->errorString = errorString; change->emitCompletionSignal(); } else { // success if (change->recordToHistory) { Q_ASSERT(mHistory); mHistory->recordDeletions(items, description, change->atomicOperationId); } if (change->useGroupwareCommunication) { connect(change.data(),SIGNAL(dialogClosedAfterChange(int,ITIPHandlerHelper::SendResult)), SLOT(handleDeleteJobResult2(int,ITIPHandlerHelper::SendResult))); handleInvitationsAfterChange(change); } else { handleDeleteJobResult2(change->id, ITIPHandlerHelper::ResultSuccess); } } } void IncidenceChanger::Private::handleDeleteJobResult2(int changeId, ITIPHandlerHelper::SendResult status) { Change::Ptr change = mChangeById[changeId]; mChangeById.remove(change->id); if (status == ITIPHandlerHelper::ResultSuccess) { change->errorString = QString(); change->resultCode = ResultCodeSuccess; } else { change->errorString = i18nc("errormessage for a job ended with an unexpected result", "An unknown error occurred"); change->resultCode = ResultCodeJobError; } // puff, change finally goes out of scope, and emits the incidenceDeleted signal. } void IncidenceChanger::Private::handleModifyJobResult(KJob *job) { Change::Ptr change = mChangeForJob.take(job); const ItemModifyJob *j = qobject_cast(job); const Item item = j->item(); Q_ASSERT(mDirtyFieldsByJob.contains(job)); Q_ASSERT(item.hasPayload()); const QSet dirtyFields = mDirtyFieldsByJob.value(job); item.payload()->setDirtyFields(dirtyFields); QString description; if (change->atomicOperationId != 0) { AtomicOperation *a = mAtomicOperations[change->atomicOperationId]; a->m_numCompletedChanges++; change->completed = true; description = a->m_description; } if (j->error()) { const QString errorString = j->errorString(); ResultCode resultCode = ResultCodeJobError; if (deleteAlreadyCalled(item.id())) { // User deleted the item almost at the same time he changed it. We could just return success // but the delete is probably already recorded to History, and that would make undo not work // in the proper order. resultCode = ResultCodeAlreadyDeleted; kWarning() << "Trying to change item " << item.id() << " while deletion is in progress."; } else { kError() << errorString; } if (mShowDialogsOnError) { KMessageBox::sorry(change->parentWidget, i18n("Error while trying to modify calendar item. Error was: %1", errorString)); } mChangeById.remove(change->id); change->errorString = errorString; change->resultCode = resultCode; // puff, change finally goes out of scope, and emits the incidenceModified signal. QMetaObject::invokeMethod(this, "performNextModification", Qt::QueuedConnection, Q_ARG(Akonadi::Item::Id, item.id())); } else { // success ConflictPreventer::self()->mLatestRevisionByItemId[item.id()] = item.revision(); change->newItem = item; if (change->recordToHistory && !change->originalItems.isEmpty()) { Q_ASSERT(change->originalItems.count() == 1); mHistory->recordModification(change->originalItems.first(), item, description, change->atomicOperationId); } if (change->useGroupwareCommunication) { connect(change.data(),SIGNAL(dialogClosedAfterChange(int,ITIPHandlerHelper::SendResult)), SLOT(handleModifyJobResult2(int,ITIPHandlerHelper::SendResult))); handleInvitationsAfterChange(change); } else { handleModifyJobResult2(change->id, ITIPHandlerHelper::ResultSuccess); } } } void IncidenceChanger::Private::handleModifyJobResult2(int changeId, ITIPHandlerHelper::SendResult status) { Change::Ptr change = mChangeById[changeId]; mChangeById.remove(changeId); if (change->atomicOperationId != 0) { mInvitationStatusByAtomicOperation.insert(change->atomicOperationId, status); } change->errorString = QString(); change->resultCode = ResultCodeSuccess; // puff, change finally goes out of scope, and emits the incidenceModified signal. QMetaObject::invokeMethod(this, "performNextModification", Qt::QueuedConnection, Q_ARG(Akonadi::Item::Id, change->newItem.id())); } bool IncidenceChanger::Private::deleteAlreadyCalled(Akonadi::Item::Id id) const { return mDeletedItemIds.contains(id); } void IncidenceChanger::Private::handleInvitationsBeforeChange(const Change::Ptr &change) { if (mGroupwareCommunication) { ITIPHandlerHelper::SendResult result = ITIPHandlerHelper::ResultSuccess; switch (change->type) { case IncidenceChanger::ChangeTypeCreate: // nothing needs to be done break; case IncidenceChanger::ChangeTypeDelete: { ITIPHandlerHelper::SendResult status; bool sendOk = true; Q_ASSERT(!change->originalItems.isEmpty()); ITIPHandlerHelper *handler = new ITIPHandlerHelper(mFactory, change->parentWidget); handler->setParent(this); if (m_invitationPolicy == InvitationPolicySend) { handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionSendMessage); } else if (m_invitationPolicy == InvitationPolicyDontSend) { handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionDontSendMessage); } else if (mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) { handler->setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId))); } connect(handler, SIGNAL(finished(Akonadi::ITIPHandlerHelper::SendResult,QString)), change.data(), SLOT(emitUserDialogClosedBeforeChange(Akonadi::ITIPHandlerHelper::SendResult))); foreach(const Akonadi::Item &item, change->originalItems) { Q_ASSERT(item.hasPayload()); Incidence::Ptr incidence = CalendarUtils::incidence(item); if (!incidence->supportsGroupwareCommunication()) { continue; } // We only send CANCEL if we're the organizer. // If we're not, then we send REPLY with PartStat=Declined in handleInvitationsAfterChange() if (weAreOrganizer(incidence, change)) { //TODO: not to popup all delete message dialogs at once :( sendOk = false; handler->setActAs(actAs(incidence, change)); handler->sendIncidenceDeletedMessage(KCalCore::iTIPCancel, incidence, Akonadi::Attendees); if (change->atomicOperationId) { mInvitationStatusByAtomicOperation.insert(change->atomicOperationId, status); } //TODO: with some status we want to break immediately } } if (sendOk) { change->emitUserDialogClosedBeforeChange(result); } } return; case IncidenceChanger::ChangeTypeModify: { if (change->originalItems.isEmpty()) { break; } Q_ASSERT(change->originalItems.count() == 1); Incidence::Ptr oldIncidence = CalendarUtils::incidence(change->originalItems.first()); Incidence::Ptr newIncidence = CalendarUtils::incidence(change->newItem); if (!oldIncidence->supportsGroupwareCommunication()) { break; } if (allowedModificationsWithoutRevisionUpdate(newIncidence)) { change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess); return; } if (allowedAttendeeModifications(newIncidence, oldIncidence->attendees())) { change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess); return; } if (RUNNING_UNIT_TESTS && !weAreOrganizer(newIncidence, change)) { // This is a bit of a workaround when running tests. I don't want to show the // "You're not organizer, do you want to modify event?" dialog in unit-tests, but want // to emulate a "yes" and a "no" press. if (m_invitationPolicy == InvitationPolicySend) { change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess); return; } else if (m_invitationPolicy == InvitationPolicyDontSend) { change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultCanceled); return; } } ITIPHandlerHelper handler(mFactory, change->parentWidget); const bool modify = handler.handleIncidenceAboutToBeModified(newIncidence, weAreOrganizer(newIncidence, change) ? Akonadi::Attendees : Akonadi::Organizer); if (modify) { break; } else { result = ITIPHandlerHelper::ResultCanceled; } if (newIncidence->type() == oldIncidence->type()) { IncidenceBase *i1 = newIncidence.data(); IncidenceBase *i2 = oldIncidence.data(); *i1 = *i2; } } break; default: Q_ASSERT(false); result = ITIPHandlerHelper::ResultCanceled; } change->emitUserDialogClosedBeforeChange(result); } else { change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess); } } void IncidenceChanger::Private::handleInvitationsAfterChange(const Change::Ptr &change) { if (change->useGroupwareCommunication) { ITIPHandlerHelper *handler = new ITIPHandlerHelper(mFactory, change->parentWidget); connect(handler, SIGNAL(finished(Akonadi::ITIPHandlerHelper::SendResult,QString)), change.data(), SLOT(emitUserDialogClosedAfterChange(Akonadi::ITIPHandlerHelper::SendResult))); handler->setParent(this); const bool alwaysSend = m_invitationPolicy == InvitationPolicySend; const bool neverSend = m_invitationPolicy == InvitationPolicyDontSend; if (alwaysSend) { handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionSendMessage); } if (neverSend) { handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionDontSendMessage); } switch (change->type) { case IncidenceChanger::ChangeTypeCreate: { Incidence::Ptr incidence = CalendarUtils::incidence(change->newItem); if (incidence->supportsGroupwareCommunication()) { handler->setActAs(incidence->organizer()->email()); handler->sendIncidenceCreatedMessage(KCalCore::iTIPRequest, incidence, Akonadi::Attendees); return; } } break; case IncidenceChanger::ChangeTypeDelete: { handler->deleteLater(); handler = 0; Q_ASSERT(!change->originalItems.isEmpty()); foreach(const Akonadi::Item &item, change->originalItems) { Q_ASSERT(item.hasPayload()); Incidence::Ptr incidence = CalendarUtils::incidence(item); Q_ASSERT(incidence); if (!incidence->supportsGroupwareCommunication()) continue; if (!weAreOrganizer(incidence, change)) { const QStringList myEmails = Akonadi::CalendarUtils::allEmails(); bool notifyOrganizer = false; KCalCore::Attendee::Ptr me(incidence->attendeeByMails(myEmails)); if (me) { if (me->status() == KCalCore::Attendee::Accepted || me->status() == KCalCore::Attendee::Delegated) { notifyOrganizer = true; } KCalCore::Attendee::Ptr newMe(new KCalCore::Attendee(*me)); newMe->setStatus(KCalCore::Attendee::Declined); incidence->clearAttendees(); incidence->addAttendee(newMe); } if (notifyOrganizer) { MailScheduler scheduler(mFactory, change->parentWidget); // TODO make async scheduler.performTransaction2(incidence, KCalCore::iTIPReply, actAs(incidence, change)); } } } } break; case IncidenceChanger::ChangeTypeModify: { if (change->originalItems.isEmpty()) { break; } Q_ASSERT(change->originalItems.count() == 1); Incidence::Ptr oldIncidence = CalendarUtils::incidence(change->originalItems.first()); Incidence::Ptr newIncidence = CalendarUtils::incidence(change->newItem); if (!newIncidence->supportsGroupwareCommunication()) { break; } if (allowedModificationsWithoutRevisionUpdate(newIncidence)) { break; } if (!neverSend && !alwaysSend && mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) { handler->setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId))); } const bool attendeeStatusChanged = myAttendeeStatusChanged(newIncidence, oldIncidence, Akonadi::CalendarUtils::allEmails()); if (attendeeStatusChanged) { if (!weAreOrganizer(newIncidence, change)) { KCalCore::Incidence::Ptr clone(newIncidence->clone()); clone->clearAttendees(); Attendee::List attendees = getChangedAttendees(newIncidence, oldIncidence->attendees()); if (!attendees.isEmpty()) { Attendee::Ptr a = attendees.first(); clone->addAttendee(a); handler->setActAs(actAs(newIncidence, change)); handler->sendIncidenceModifiedMessage(KCalCore::iTIPReply, clone, attendeeStatusChanged, Akonadi::Organizer); } else { kWarning() << "No attendee found"; change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultCanceled); } } else { handler->setActAs(actAs(newIncidence, change)); handler->sendIncidenceModifiedMessage(KCalCore::iTIPRequest, newIncidence, attendeeStatusChanged, Akonadi::Attendees); } } else { handler->setActAs(actAs(newIncidence, change)); handler->sendIncidenceModifiedMessage(KCalCore::iTIPRequest, newIncidence, false, weAreOrganizer(newIncidence, change)? Akonadi::Attendees: Akonadi::Organizer); } return; } break; default: handler->deleteLater(); handler = 0; Q_ASSERT(false); change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultCanceled); return; } handler->deleteLater(); handler = 0; change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultSuccess); } else { change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultSuccess); } } /** static */ bool IncidenceChanger::Private::myAttendeeStatusChanged(const Incidence::Ptr &newInc, const Incidence::Ptr &oldInc, const QStringList &myEmails) { Q_ASSERT(newInc); Q_ASSERT(oldInc); foreach (const QString &email, myEmails) { const Attendee::Ptr oldMe = oldInc->attendeeByMail(email); const Attendee::Ptr newMe = newInc->attendeeByMail(email); if (oldMe && newMe && oldMe->status() != newMe->status()) { return true; } } return false; } IncidenceChanger::IncidenceChanger(QObject *parent) : QObject(parent) , d(new Private(/**history=*/true, /*factory=*/0, this)) { } IncidenceChanger::IncidenceChanger(ITIPHandlerComponentFactory *factory, QObject *parent) : QObject(parent) , d(new Private(/**history=*/true, factory, this)) { } IncidenceChanger::IncidenceChanger(bool enableHistory, QObject *parent) : QObject(parent) , d(new Private(enableHistory, /*factory=*/0, this)) { } IncidenceChanger::~IncidenceChanger() { delete d; } int IncidenceChanger::createIncidence(const Incidence::Ptr &incidence, const Collection &collection, QWidget *parent) { if (!incidence) { kWarning() << "An invalid payload is not allowed."; d->cancelTransaction(); return -1; } const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0; const Change::Ptr change(new CreationChange(this, ++d->mLatestChangeId, atomicOperationId, parent)); const int changeId = change->id; Q_ASSERT(!(d->mBatchOperationInProgress && !d->mAtomicOperations.contains(atomicOperationId))); if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) { const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent); kWarning() << errorMessage; change->resultCode = ResultCodeRolledback; change->errorString = errorMessage; d->cleanupTransaction(); return changeId; } if (incidence->recurrenceId().isValid()) { resetAttendeeStatus(incidence, change); } Item item; item.setPayload(incidence); item.setMimeType(incidence->mimeType()); change->newItem = item; d->step1DetermineDestinationCollection(change, collection); return change->id; } int IncidenceChanger::deleteIncidence(const Item &item, QWidget *parent) { Item::List list; list.append(item); return deleteIncidences(list, parent); } int IncidenceChanger::deleteIncidences(const Item::List &items, QWidget *parent) { if (items.isEmpty()) { kError() << "Delete what?"; d->cancelTransaction(); return -1; } foreach(const Item &item, items) { if (!item.isValid()) { kError() << "Items must be valid!"; d->cancelTransaction(); return -1; } } const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0; const int changeId = ++d->mLatestChangeId; const Change::Ptr change(new DeletionChange(this, changeId, atomicOperationId, parent)); foreach(const Item &item, items) { if (!d->hasRights(item.parentCollection(), ChangeTypeDelete)) { kWarning() << "Item " << item.id() << " can't be deleted due to ACL restrictions"; const QString errorString = d->showErrorDialog(ResultCodePermissions, parent); change->resultCode = ResultCodePermissions; change->errorString = errorString; d->cancelTransaction(); return changeId; } } if (!d->allowAtomicOperation(atomicOperationId, change)) { const QString errorString = d->showErrorDialog(ResultCodeDuplicateId, parent); change->resultCode = ResultCodeDuplicateId; change->errorString = errorString; kWarning() << errorString; d->cancelTransaction(); return changeId; } Item::List itemsToDelete; foreach(const Item &item, items) { if (d->deleteAlreadyCalled(item.id())) { // IncidenceChanger::deleteIncidence() called twice, ignore this one. kDebug() << "Item " << item.id() << " already deleted or being deleted, skipping"; } else { itemsToDelete.append(item); } } if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) { const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent); change->resultCode = ResultCodeRolledback; change->errorString = errorMessage; kError() << errorMessage; d->cleanupTransaction(); return changeId; } if (itemsToDelete.isEmpty()) { QVector itemIdList; itemIdList.append(Item().id()); kDebug() << "Items already deleted or being deleted, skipping"; const QString errorMessage = i18n("That calendar item was already deleted, or currently being deleted."); // Queued emit because return must be executed first, otherwise caller won't know this workId change->resultCode = ResultCodeAlreadyDeleted; change->errorString = errorMessage; d->cancelTransaction(); kWarning() << errorMessage; return changeId; } change->originalItems = itemsToDelete; d->mChangeById.insert(changeId, change); if (d->mGroupwareCommunication) { connect(change.data(), SIGNAL(dialogClosedBeforeChange(int,ITIPHandlerHelper::SendResult)), d, SLOT(deleteIncidences2(int,ITIPHandlerHelper::SendResult))); d->handleInvitationsBeforeChange(change); } else { d->deleteIncidences2(changeId, ITIPHandlerHelper::ResultSuccess); } return changeId; } void IncidenceChanger::Private::deleteIncidences2(int changeId, ITIPHandlerHelper::SendResult status) { Q_UNUSED(status); Change::Ptr change = mChangeById[changeId]; const uint atomicOperationId = change->atomicOperationId; ItemDeleteJob *deleteJob = new ItemDeleteJob(change->originalItems, parentJob(change)); mChangeForJob.insert(deleteJob, change); if (mBatchOperationInProgress) { AtomicOperation *atomic = mAtomicOperations[atomicOperationId]; Q_ASSERT(atomic); atomic->addChange(change); } foreach(const Item &item, change->originalItems) { mDeletedItemIds << item.id(); } // Do some cleanup if (mDeletedItemIds.count() > 100) mDeletedItemIds.remove(0, 50); // QueuedConnection because of possible sync exec calls. connect(deleteJob, SIGNAL(result(KJob*)), SLOT(handleDeleteJobResult(KJob*)), Qt::QueuedConnection); } int IncidenceChanger::modifyIncidence(const Item &changedItem, const KCalCore::Incidence::Ptr &originalPayload, QWidget *parent) { if (!changedItem.isValid() || !changedItem.hasPayload()) { kWarning() << "An invalid item or payload is not allowed."; d->cancelTransaction(); return -1; } if (!d->hasRights(changedItem.parentCollection(), ChangeTypeModify)) { kWarning() << "Item " << changedItem.id() << " can't be deleted due to ACL restrictions"; const int changeId = ++d->mLatestChangeId; const QString errorString = d->showErrorDialog(ResultCodePermissions, parent); emitModifyFinished(this, changeId, changedItem, ResultCodePermissions, errorString); d->cancelTransaction(); return changeId; } //TODO also update revision here instead of in the editor changedItem.payload()->setLastModified(KDateTime::currentUtcDateTime()); const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0; const int changeId = ++d->mLatestChangeId; ModificationChange *modificationChange = new ModificationChange(this, changeId, atomicOperationId, parent); Change::Ptr change(modificationChange); if (originalPayload) { Item originalItem(changedItem); originalItem.setPayload(originalPayload); modificationChange->originalItems << originalItem; } modificationChange->newItem = changedItem; d->mChangeById.insert(changeId, change); if (!d->allowAtomicOperation(atomicOperationId, change)) { const QString errorString = d->showErrorDialog(ResultCodeDuplicateId, parent); change->resultCode = ResultCodeDuplicateId; change->errorString = errorString; d->cancelTransaction(); kWarning() << "Atomic operation now allowed"; return changeId; } if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) { const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent); kError() << errorMessage; d->cleanupTransaction(); emitModifyFinished(this, changeId, changedItem, ResultCodeRolledback, errorMessage); } else { d->adjustRecurrence(originalPayload, CalendarUtils::incidence(modificationChange->newItem)); d->performModification(change); } return changeId; } void IncidenceChanger::Private::performModification(Change::Ptr change) { const Item::Id id = change->newItem.id(); Akonadi::Item &newItem = change->newItem; Q_ASSERT(newItem.isValid()); Q_ASSERT(newItem.hasPayload()); const int changeId = change->id; if (deleteAlreadyCalled(id)) { // IncidenceChanger::deleteIncidence() called twice, ignore this one. kDebug() << "Item " << id << " already deleted or being deleted, skipping"; // Queued emit because return must be executed first, otherwise caller won't know this workId emitModifyFinished(q, change->id, newItem, ResultCodeAlreadyDeleted, i18n("That calendar item was already deleted, or currently being deleted.")); return; } const uint atomicOperationId = change->atomicOperationId; const bool hasAtomicOperationId = atomicOperationId != 0; if (hasAtomicOperationId && mAtomicOperations[atomicOperationId]->rolledback()) { const QString errorMessage = showErrorDialog(ResultCodeRolledback, 0); kError() << errorMessage; emitModifyFinished(q, changeId, newItem, ResultCodeRolledback, errorMessage); return; } if (mGroupwareCommunication) { connect(change.data(), SIGNAL(dialogClosedBeforeChange(int,ITIPHandlerHelper::SendResult)), SLOT(performModification2(int,ITIPHandlerHelper::SendResult))); handleInvitationsBeforeChange(change); } else { performModification2(change->id, ITIPHandlerHelper::ResultSuccess); } } void IncidenceChanger::Private::performModification2(int changeId, ITIPHandlerHelper::SendResult status) { Change::Ptr change = mChangeById[changeId]; const Item::Id id = change->newItem.id(); Akonadi::Item &newItem = change->newItem; Q_ASSERT(newItem.isValid()); Q_ASSERT(newItem.hasPayload()); if (status == ITIPHandlerHelper::ResultCanceled) { //TODO:fireout what is right here:) // User got a "You're not the organizer, do you really want to send" dialog, and said "no" kDebug() << "User cancelled, giving up"; emitModifyFinished(q, change->id, newItem, ResultCodeUserCanceled, QString()); return; } const uint atomicOperationId = change->atomicOperationId; const bool hasAtomicOperationId = atomicOperationId != 0; QHash &latestRevisionByItemId = ConflictPreventer::self()->mLatestRevisionByItemId; if (latestRevisionByItemId.contains(id) && latestRevisionByItemId[id] > newItem.revision()) { /* When a ItemModifyJob ends, the application can still modify the old items if the user * is quick because the ETM wasn't updated yet, and we'll get a STORE error, because * we are not modifying the latest revision. * * When a job ends, we keep the new revision in mLatestRevisionByItemId * so we can update the item's revision */ newItem.setRevision(latestRevisionByItemId[id]); } Incidence::Ptr incidence = CalendarUtils::incidence(newItem); { if (!allowedModificationsWithoutRevisionUpdate(incidence)) { // increment revision ( KCalCore revision, not akonadi ) const int revision = incidence->revision(); incidence->setRevision(revision + 1); } resetAttendeeStatus(incidence, change); } // Dav Fix // Don't write back remote revision since we can't make sure it is the current one newItem.setRemoteRevision(QString()); if (mModificationsInProgress.contains(newItem.id())) { // There's already a ItemModifyJob running for this item ID // Let's wait for it to end. queueModification(change); } else { ItemModifyJob *modifyJob = new ItemModifyJob(newItem, parentJob(change)); mChangeForJob.insert(modifyJob, change); mDirtyFieldsByJob.insert(modifyJob, incidence->dirtyFields()); if (hasAtomicOperationId) { AtomicOperation *atomic = mAtomicOperations[atomicOperationId]; Q_ASSERT(atomic); atomic->addChange(change); } mModificationsInProgress[newItem.id()] = change; // QueuedConnection because of possible sync exec calls. connect(modifyJob, SIGNAL(result(KJob*)), SLOT(handleModifyJobResult(KJob*)), Qt::QueuedConnection); } } void IncidenceChanger::startAtomicOperation(const QString &operationDescription) { if (d->mBatchOperationInProgress) { kDebug() << "An atomic operation is already in progress."; return; } ++d->mLatestAtomicOperationId; d->mBatchOperationInProgress = true; AtomicOperation *atomicOperation = new AtomicOperation(d, d->mLatestAtomicOperationId); atomicOperation->m_description = operationDescription; d->mAtomicOperations.insert(d->mLatestAtomicOperationId, atomicOperation); } void IncidenceChanger::endAtomicOperation() { if (!d->mBatchOperationInProgress) { kDebug() << "No atomic operation is in progress."; return; } Q_ASSERT_X(d->mLatestAtomicOperationId != 0, "IncidenceChanger::endAtomicOperation()", "Call startAtomicOperation() first."); Q_ASSERT(d->mAtomicOperations.contains(d->mLatestAtomicOperationId)); AtomicOperation *atomicOperation = d->mAtomicOperations[d->mLatestAtomicOperationId]; Q_ASSERT(atomicOperation); atomicOperation->m_endCalled = true; const bool allJobsCompleted = !atomicOperation->pendingJobs(); if (allJobsCompleted && atomicOperation->rolledback() && atomicOperation->m_transactionCompleted) { // The transaction job already completed, we can cleanup: delete d->mAtomicOperations.take(d->mLatestAtomicOperationId); d->mBatchOperationInProgress = false; }/* else if ( allJobsCompleted ) { Q_ASSERT( atomicOperation->transaction ); atomicOperation->transaction->commit(); we using autocommit now }*/ } void IncidenceChanger::setShowDialogsOnError(bool enable) { d->mShowDialogsOnError = enable; } bool IncidenceChanger::showDialogsOnError() const { return d->mShowDialogsOnError; } void IncidenceChanger::setRespectsCollectionRights(bool respects) { d->mRespectsCollectionRights = respects; } bool IncidenceChanger::respectsCollectionRights() const { return d->mRespectsCollectionRights; } void IncidenceChanger::setDestinationPolicy(IncidenceChanger::DestinationPolicy destinationPolicy) { d->mDestinationPolicy = destinationPolicy; } IncidenceChanger::DestinationPolicy IncidenceChanger::destinationPolicy() const { return d->mDestinationPolicy; } void IncidenceChanger::setDefaultCollection(const Akonadi::Collection &collection) { d->mDefaultCollection = collection; } Collection IncidenceChanger::defaultCollection() const { return d->mDefaultCollection; } bool IncidenceChanger::historyEnabled() const { return d->mUseHistory; } void IncidenceChanger::setHistoryEnabled(bool enable) { if (d->mUseHistory != enable) { d->mUseHistory = enable; if (enable && !d->mHistory) d->mHistory = new History(d); } } History* IncidenceChanger::history() const { return d->mHistory; } bool IncidenceChanger::deletedRecently(Akonadi::Item::Id id) const { return d->deleteAlreadyCalled(id); } void IncidenceChanger::setGroupwareCommunication(bool enabled) { d->mGroupwareCommunication = enabled; } bool IncidenceChanger::groupwareCommunication() const { return d->mGroupwareCommunication; } void IncidenceChanger::setAutoAdjustRecurrence(bool enable) { d->mAutoAdjustRecurrence = enable; } bool IncidenceChanger::autoAdjustRecurrence() const { return d->mAutoAdjustRecurrence; } void IncidenceChanger::setInvitationPolicy(IncidenceChanger::InvitationPolicy policy) { d->m_invitationPolicy = policy; } IncidenceChanger::InvitationPolicy IncidenceChanger::invitationPolicy() const { return d->m_invitationPolicy; } Akonadi::Collection IncidenceChanger::lastCollectionUsed() const { return d->mLastCollectionUsed; } QString IncidenceChanger::Private::showErrorDialog(IncidenceChanger::ResultCode resultCode, QWidget *parent) { QString errorString; switch (resultCode) { case IncidenceChanger::ResultCodePermissions: errorString = i18n("Operation can not be performed due to ACL restrictions"); break; case IncidenceChanger::ResultCodeInvalidUserCollection: errorString = i18n("The chosen collection is invalid"); break; case IncidenceChanger::ResultCodeInvalidDefaultCollection: errorString = i18n("Default collection is invalid or doesn't have proper ACLs" " and DestinationPolicyNeverAsk was used"); break; case IncidenceChanger::ResultCodeDuplicateId: errorString = i18n("Duplicate item id in a group operation"); break; case IncidenceChanger::ResultCodeRolledback: errorString = i18n("One change belonging to a group of changes failed. " "All changes are being rolled back."); break; default: Q_ASSERT(false); return QString(i18n("Unknown error")); } if (mShowDialogsOnError) { KMessageBox::sorry(parent, errorString); } return errorString; } void IncidenceChanger::Private::adjustRecurrence(const KCalCore::Incidence::Ptr &originalIncidence, const KCalCore::Incidence::Ptr &incidence) { if (!originalIncidence || !incidence->recurs() || incidence->hasRecurrenceId() || !mAutoAdjustRecurrence || !incidence->dirtyFields().contains(KCalCore::Incidence::FieldDtStart)) { return; } const QDate originalDate = originalIncidence->dtStart().date(); const QDate newStartDate = incidence->dtStart().date(); if (!originalDate.isValid() || !newStartDate.isValid() || originalDate == newStartDate) return; KCalCore::Recurrence *recurrence = incidence->recurrence(); switch (recurrence->recurrenceType()) { case KCalCore::Recurrence::rWeekly: { QBitArray days = recurrence->days(); const int oldIndex = originalDate.dayOfWeek()-1; // QDate returns [1-7]; const int newIndex = newStartDate.dayOfWeek()-1; if (oldIndex != newIndex) { days.clearBit(oldIndex); days.setBit(newIndex); recurrence->setWeekly(recurrence->frequency(), days); } } default: break; // Other types not implemented } // Now fix cases where dtstart would be bigger than the recurrence end rendering it impossible for a view to show it: // To retrieve the recurrence end don't trust Recurrence::endDt() since it returns dtStart if the rrule's end is < than dtstart, // it seems someone made Recurrence::endDt() more robust, but getNextOccurrences() still craps out. So lets fix it here // there's no reason to write bogus ical to disk. const QDate recurrenceEndDate = recurrence->defaultRRule() ? recurrence->defaultRRule()->endDt().date() : QDate(); if (recurrenceEndDate.isValid() && recurrenceEndDate < newStartDate) { recurrence->setEndDate(newStartDate); } } void IncidenceChanger::Private::cancelTransaction() { if (mBatchOperationInProgress) { mAtomicOperations[mLatestAtomicOperationId]->setRolledback(); } } void IncidenceChanger::Private::cleanupTransaction() { Q_ASSERT(mAtomicOperations.contains(mLatestAtomicOperationId)); AtomicOperation *operation = mAtomicOperations[mLatestAtomicOperationId]; Q_ASSERT(operation); Q_ASSERT(operation->rolledback()); if (!operation->pendingJobs() && operation->m_endCalled && operation->m_transactionCompleted) { delete mAtomicOperations.take(mLatestAtomicOperationId); mBatchOperationInProgress = false; } } bool IncidenceChanger::Private::allowAtomicOperation(int atomicOperationId, const Change::Ptr &change) const { bool allow = true; if (atomicOperationId > 0) { Q_ASSERT(mAtomicOperations.contains(atomicOperationId)); AtomicOperation *operation = mAtomicOperations.value(atomicOperationId); if (change->type == ChangeTypeCreate) { allow = true; } else if (change->type == ChangeTypeModify) { allow = !operation->m_itemIdsInOperation.contains(change->newItem.id()); } else if (change->type == ChangeTypeDelete) { DeletionChange::Ptr deletion = change.staticCast(); foreach(Akonadi::Item::Id id, deletion->mItemIds) { if (operation->m_itemIdsInOperation.contains(id)) { allow = false; break; } } } } if (!allow) { kWarning() << "Each change belonging to a group operation" << "must have a different Akonadi::Item::Id"; } return allow; } /**reimp*/ void ModificationChange::emitCompletionSignal() { emitModifyFinished(changer, id, newItem, resultCode, errorString); } /**reimp*/ void CreationChange::emitCompletionSignal() { // Does a queued emit, with QMetaObject::invokeMethod emitCreateFinished(changer, id, newItem, resultCode, errorString); } /**reimp*/ void DeletionChange::emitCompletionSignal() { emitDeleteFinished(changer, id, mItemIds, resultCode, errorString); } /** Lost code from KDE 4.4 that was never called/used with incidenceeditors-ng. Attendees were removed from this incidence. Only the removed attendees are present in the incidence, so we just need to send a cancel messages to all attendees groupware messages are enabled at all. void IncidenceChanger::cancelAttendees( const Akonadi::Item &aitem ) { const KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence( aitem ); Q_ASSERT( incidence ); if ( KCalPrefs::instance()->mUseGroupwareCommunication ) { if ( KMessageBox::questionYesNo( 0, i18n( "Some attendees were removed from the incidence. " "Shall cancel messages be sent to these attendees?" ), i18n( "Attendees Removed" ), KGuiItem( i18n( "Send Messages" ) ), KGuiItem( i18n( "Do Not Send" ) ) ) == KMessageBox::Yes ) { // don't use Akonadi::Groupware::sendICalMessage here, because that asks just // a very general question "Other people are involved, send message to // them?", which isn't helpful at all in this situation. Afterwards, it // would only call the Akonadi::MailScheduler::performTransaction, so do this // manually. CalendarSupport::MailScheduler scheduler( static_cast(d->mCalendar) ); scheduler.performTransaction( incidence, KCalCore::iTIPCancel ); } } } */ AtomicOperation::AtomicOperation(IncidenceChanger::Private *icp, uint ident) : m_id(ident) , m_endCalled(false) , m_numCompletedChanges(0) , m_transactionCompleted(false) , m_wasRolledback(false) , m_transaction(0) , m_incidenceChangerPrivate(icp) { Q_ASSERT(m_id != 0); } Akonadi::TransactionSequence *AtomicOperation::transaction() { if (!m_transaction) { m_transaction = new Akonadi::TransactionSequence; m_transaction->setAutomaticCommittingEnabled(true); m_incidenceChangerPrivate->mAtomicOperationByTransaction.insert(m_transaction, m_id); QObject::connect(m_transaction, SIGNAL(result(KJob*)), m_incidenceChangerPrivate, SLOT(handleTransactionJobResult(KJob*))); } return m_transaction; }