kde-playground/kdepimlibs/akonadi/calendar/incidencechanger.cpp
2015-04-14 21:49:29 +00:00

1283 lines
47 KiB
C++

/*
Copyright (C) 2004 Reinhold Kainhofer <reinhold@kainhofer.com>
Copyright (C) 2010-2012 Sérgio Martins <iamsergio@gmail.com>
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 <akonadi/itemcreatejob.h>
#include <akonadi/itemmodifyjob.h>
#include <akonadi/itemdeletejob.h>
#include <akonadi/transactionsequence.h>
#include <KJob>
#include <KLocalizedString>
#include <KGuiItem>
#include <KMessageBox>
#include <QBitArray>
using namespace Akonadi;
using namespace KCalCore;
#ifdef PLEASE_TEST_INVITATIONS
# define RUNNING_UNIT_TESTS true
#else
# define RUNNING_UNIT_TESTS false
#endif
ITIPHandlerHelper::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 ITIPHandlerHelper::ActionDontSendMessage;
case ITIPHandlerHelper::ResultSuccess:
return ITIPHandlerHelper::ActionSendMessage;
default:
return ITIPHandlerHelper::ActionAsk;
}
}
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<Akonadi::Item::Id> &itemIdList,
IncidenceChanger::ResultCode resultCode,
const QString &errorString)
{
QMetaObject::invokeMethod(changer, "deleteFinished", Qt::QueuedConnection,
Q_ARG(int, changeId),
Q_ARG(QVector<Akonadi::Item::Id>, 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<Akonadi::Item::Id, int> 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, IncidenceChanger *qq) : q(qq)
{
mLatestChangeId = 0;
mShowDialogsOnError = true;
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<Akonadi::Item::Id> >("QVector<Akonadi::Item::Id>");
qRegisterMetaType<Akonadi::Item::Id>("Akonadi::Item::Id");
qRegisterMetaType<Akonadi::Item>("Akonadi::Item");
qRegisterMetaType<Akonadi::IncidenceChanger::ResultCode>(
"Akonadi::IncidenceChanger::ResultCode");
}
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)
{
//kDebug();
TransactionSequence *transaction = qobject_cast<TransactionSequence*>(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)
{
//kDebug();
QString errorString;
ResultCode resultCode = ResultCodeSuccess;
Change::Ptr change = mChangeForJob.take(job);
mChangeById.remove(change->id);
const ItemCreateJob *j = qobject_cast<const ItemCreateJob*>(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;
}
if (j->error()) {
item = change->newItem;
resultCode = ResultCodeJobError;
errorString = j->errorString();
kError() << errorString;
if (mShowDialogsOnError) {
KMessageBox::sorry(change->parentWidget,
i18n("Error while trying to create calendar item. Error was: %1",
errorString));
}
} else {
Q_ASSERT(item.isValid());
Q_ASSERT(item.hasPayload<KCalCore::Incidence::Ptr>());
change->newItem = item;
handleInvitationsAfterChange(change);
// for user undo/redo
if (change->recordToHistory) {
mHistory->recordCreation(item, description, change->atomicOperationId);
}
}
change->errorString = errorString;
change->resultCode = resultCode;
// puff, change finally goes out of scope, and emits the incidenceCreated signal.
}
void IncidenceChanger::Private::handleDeleteJobResult(KJob *job)
{
//kDebug();
QString errorString;
ResultCode resultCode = ResultCodeSuccess;
Change::Ptr change = mChangeForJob.take(job);
mChangeById.remove(change->id);
const ItemDeleteJob *j = qobject_cast<const ItemDeleteJob*>(job);
const Item::List items = j->deletedItems();
QSharedPointer<DeletionChange> deletionChange = change.staticCast<DeletionChange>();
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()) {
resultCode = ResultCodeJobError;
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()));
}
} else { // success
if (change->recordToHistory) {
Q_ASSERT(mHistory);
mHistory->recordDeletions(items, description, change->atomicOperationId);
}
handleInvitationsAfterChange(change);
}
change->errorString = errorString;
change->resultCode = resultCode;
// puff, change finally goes out of scope, and emits the incidenceDeleted signal.
}
void IncidenceChanger::Private::handleModifyJobResult(KJob *job)
{
QString errorString;
ResultCode resultCode = ResultCodeSuccess;
Change::Ptr change = mChangeForJob.take(job);
mChangeById.remove(change->id);
const ItemModifyJob *j = qobject_cast<const ItemModifyJob*>(job);
const Item item = j->item();
Q_ASSERT(mDirtyFieldsByJob.contains(job));
Q_ASSERT(item.hasPayload<KCalCore::Incidence::Ptr>());
item.payload<KCalCore::Incidence::Ptr>()->setDirtyFields(mDirtyFieldsByJob.value(job));
const QSet<KCalCore::IncidenceBase::Field> dirtyFields = mDirtyFieldsByJob.value(job);
QString description;
if (change->atomicOperationId != 0) {
AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
a->m_numCompletedChanges++;
change->completed = true;
description = a->m_description;
}
if (j->error()) {
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;
errorString = j->errorString();
kWarning() << "Trying to change item " << item.id() << " while deletion is in progress.";
} else {
resultCode = ResultCodeJobError;
errorString = j->errorString();
kError() << errorString;
}
if (mShowDialogsOnError) {
KMessageBox::sorry(change->parentWidget,
i18n("Error while trying to modify calendar item. Error was: %1",
errorString));
}
} 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);
}
handleInvitationsAfterChange(change);
}
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()));
}
bool IncidenceChanger::Private::deleteAlreadyCalled(Akonadi::Item::Id id) const
{
return mDeletedItemIds.contains(id);
}
bool IncidenceChanger::Private::handleInvitationsBeforeChange(const Change::Ptr &change)
{
bool result = true;
if (mGroupwareCommunication) {
ITIPHandlerHelper handler(change->parentWidget); // TODO make async
if (m_invitationPolicy == InvitationPolicySend) {
handler.setDefaultAction(ITIPHandlerHelper::ActionSendMessage);
} else if (m_invitationPolicy == InvitationPolicyDontSend) {
handler.setDefaultAction(ITIPHandlerHelper::ActionDontSendMessage);
} else if (mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) {
handler.setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId)));
}
switch (change->type) {
case IncidenceChanger::ChangeTypeCreate:
// nothing needs to be done
break;
case IncidenceChanger::ChangeTypeDelete:
{
ITIPHandlerHelper::SendResult status;
Q_ASSERT(!change->originalItems.isEmpty());
foreach(const Akonadi::Item &item, change->originalItems) {
Q_ASSERT(item.hasPayload<KCalCore::Incidence::Ptr>());
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 (Akonadi::CalendarUtils::thatIsMe(incidence->organizer()->email())) {
status = handler.sendIncidenceDeletedMessage(KCalCore::iTIPCancel, incidence);
if (change->atomicOperationId) {
mInvitationStatusByAtomicOperation.insert(change->atomicOperationId, status);
}
result = status != ITIPHandlerHelper::ResultFailAbortUpdate;
//TODO: with some status we want to break immediately
}
}
}
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 (!oldIncidence->supportsGroupwareCommunication()) {
break;
}
const bool weAreOrganizer = Akonadi::CalendarUtils::thatIsMe(newIncidence->organizer()->email());
if (RUNNING_UNIT_TESTS && !weAreOrganizer) {
// 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) {
return true;
} else if (m_invitationPolicy == InvitationPolicyDontSend) {
return false;
}
}
const bool modify = handler.handleIncidenceAboutToBeModified(newIncidence);
if (modify) {
break;
}
if (newIncidence->type() == oldIncidence->type()) {
IncidenceBase *i1 = newIncidence.data();
IncidenceBase *i2 = oldIncidence.data();
*i1 = *i2;
}
result = false;
}
break;
default:
Q_ASSERT(false);
result = false;
}
}
return result;
}
bool IncidenceChanger::Private::handleInvitationsAfterChange(const Change::Ptr &change)
{
if (change->useGroupwareCommunication) {
ITIPHandlerHelper handler(change->parentWidget); // TODO make async
const bool alwaysSend = m_invitationPolicy == InvitationPolicySend;
const bool neverSend = m_invitationPolicy == InvitationPolicyDontSend;
if (alwaysSend) {
handler.setDefaultAction(ITIPHandlerHelper::ActionSendMessage);
}
if (neverSend) {
handler.setDefaultAction(ITIPHandlerHelper::ActionDontSendMessage);
}
switch (change->type) {
case IncidenceChanger::ChangeTypeCreate:
{
Incidence::Ptr incidence = CalendarUtils::incidence(change->newItem);
if (incidence->supportsGroupwareCommunication()) {
const ITIPHandlerHelper::SendResult status =
handler.sendIncidenceCreatedMessage(KCalCore::iTIPRequest, incidence);
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);
}
}
}
break;
case IncidenceChanger::ChangeTypeDelete:
{
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 (!Akonadi::CalendarUtils::thatIsMe(incidence->organizer()->email())) {
const QStringList myEmails = Akonadi::CalendarUtils::allEmails();
bool notifyOrganizer = false;
for (QStringList::ConstIterator it = myEmails.begin(); it != myEmails.end(); ++it) {
const QString email = *it;
KCalCore::Attendee::Ptr me(incidence->attendeeByMail(email));
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);
break;
}
}
if (notifyOrganizer) {
MailScheduler scheduler; // TODO make async
scheduler.performTransaction(incidence, KCalCore::iTIPReply);
}
}
}
}
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() ||
!Akonadi::CalendarUtils::thatIsMe(newIncidence->organizer()->email())) {
// If we're not the organizer, the user already saw the "Do you really want to do this, incidence will become out of sync"
break;
}
if (!neverSend && !alwaysSend && mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) {
handler.setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId)));
}
const bool attendeeStatusChanged = myAttendeeStatusChanged(newIncidence,
oldIncidence,
Akonadi::CalendarUtils::allEmails());
ITIPHandlerHelper::SendResult status = handler.sendIncidenceModifiedMessage(KCalCore::iTIPRequest,
newIncidence,
attendeeStatusChanged);
if (change->atomicOperationId != 0) {
mInvitationStatusByAtomicOperation.insert(change->atomicOperationId, status);
}
}
break;
default:
Q_ASSERT(false);
return false;
}
}
return true;
}
/** static */
bool IncidenceChanger::Private::myAttendeeStatusChanged(const Incidence::Ptr &newInc,
const Incidence::Ptr &oldInc,
const QStringList &myEmails)
{
Q_ASSERT(newInc);
Q_ASSERT(oldInc);
const Attendee::Ptr oldMe = oldInc->attendeeByMails(myEmails);
const Attendee::Ptr newMe = newInc->attendeeByMails(myEmails);
return oldMe && newMe && oldMe->status() != newMe->status();
}
IncidenceChanger::IncidenceChanger(QObject *parent) : QObject(parent)
, d(new Private(/**history=*/true, this))
{
}
IncidenceChanger::IncidenceChanger(bool enableHistory, QObject *parent) : QObject(parent)
, d(new Private(enableHistory, this))
{
}
IncidenceChanger::~IncidenceChanger()
{
delete d;
}
int IncidenceChanger::createIncidence(const Incidence::Ptr &incidence,
const Collection &collection,
QWidget *parent)
{
//kDebug();
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;
}
Item item;
item.setPayload<KCalCore::Incidence::Ptr>(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)
{
//kDebug();
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<Akonadi::Item::Id> 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->handleInvitationsBeforeChange(change);
ItemDeleteJob *deleteJob = new ItemDeleteJob(itemsToDelete, d->parentJob(change));
d->mChangeForJob.insert(deleteJob, change);
d->mChangeById.insert(changeId, change);
if (d->mBatchOperationInProgress) {
AtomicOperation *atomic = d->mAtomicOperations[atomicOperationId];
Q_ASSERT(atomic);
atomic->addChange(change);
}
foreach(const Item &item, itemsToDelete) {
d->mDeletedItemIds << item.id();
}
// Do some cleanup
if (d->mDeletedItemIds.count() > 100)
d->mDeletedItemIds.remove(0, 50);
// QueuedConnection because of possible sync exec calls.
connect(deleteJob, SIGNAL(result(KJob*)),
d, SLOT(handleDeleteJobResult(KJob*)), Qt::QueuedConnection);
return changeId;
}
int IncidenceChanger::modifyIncidence(const Item &changedItem,
const KCalCore::Incidence::Ptr &originalPayload,
QWidget *parent)
{
if (!changedItem.isValid() || !changedItem.hasPayload<Incidence::Ptr>()) {
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<Incidence::Ptr>()->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<KCalCore::Incidence::Ptr>(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<Incidence::Ptr>());
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;
}
const bool userCancelled = !handleInvitationsBeforeChange(change);
if (userCancelled) {
// 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, changeId, newItem, ResultCodeUserCanceled, QString());
return;
}
QHash<Akonadi::Item::Id, int> &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);
{ // increment revision ( KCalCore revision, not akonadi )
const int revision = incidence->revision();
incidence->setRevision(revision + 1);
}
// 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(this);
}
}
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<DeletionChange>();
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<CalendarSupport::Calendar*>(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;
}