7#include "incidencechanger.h"
8#include "akonadicalendar_debug.h"
9#include "calendarutils.h"
10#include "incidencechanger_p.h"
11#include "mailscheduler_p.h"
13#include <Akonadi/ItemCreateJob>
14#include <Akonadi/ItemDeleteJob>
15#include <Akonadi/ItemModifyJob>
16#include <Akonadi/TransactionSequence>
20#include <KLocalizedString>
28AKONADI_CALENDAR_TESTS_EXPORT
bool akonadi_calendar_running_unittests =
false;
41 case ITIPHandlerHelper::ResultCanceled:
43 case ITIPHandlerHelper::ResultSuccess:
53 return Akonadi::CalendarUtils::thatIsMe(email);
56static bool allowedModificationsWithoutRevisionUpdate(
const Incidence::Ptr &incidence)
63 return dirtyFields == alarmOnlyModify;
68 ITIPHandlerHelper::MessagePrivacyFlags helperFlags;
69 helperFlags.setFlag(ITIPHandlerHelper::MessagePrivacySign, (flags & IncidenceChanger::InvitationPrivacySign) == IncidenceChanger::InvitationPrivacySign);
70 helperFlags.setFlag(ITIPHandlerHelper::MessagePrivacyEncrypt,
71 (flags & IncidenceChanger::InvitationPrivacyEncrypt) == IncidenceChanger::InvitationPrivacyEncrypt);
72 helper->setMessagePrivacy(helperFlags);
78static void emitCreateFinished(IncidenceChanger *changer,
81 Akonadi::IncidenceChanger::ResultCode resultCode,
89 Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode),
95emitModifyFinished(IncidenceChanger *changer,
int changeId,
const Akonadi::Item &item, IncidenceChanger::ResultCode resultCode,
const QString &errorString)
100 Q_ARG(
int, changeId),
102 Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode),
107static void emitDeleteFinished(IncidenceChanger *changer,
110 IncidenceChanger::ResultCode resultCode,
116 Q_ARG(
int, changeId),
118 Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode),
130 mShowDialogsOnError =
true;
132 mHistory = enableHistory ?
new History(
this) : nullptr;
133 mUseHistory = enableHistory;
134 mDestinationPolicy = IncidenceChanger::DestinationPolicyDefault;
135 mRespectsCollectionRights =
false;
136 mGroupwareCommunication =
false;
137 mLatestAtomicOperationId = 0;
138 mBatchOperationInProgress =
false;
139 mAutoAdjustRecurrence =
true;
140 m_collectionFetchJob =
nullptr;
141 m_invitationPolicy = IncidenceChanger::InvitationPolicyAsk;
143 qRegisterMetaType<QList<Akonadi::Item::Id>>(
"QList<Akonadi::Item::Id>");
144 qRegisterMetaType<Akonadi::Item::Id>(
"Akonadi::Item::Id");
145 qRegisterMetaType<Akonadi::Item>(
"Akonadi::Item");
146 qRegisterMetaType<Akonadi::IncidenceChanger::ResultCode>(
"Akonadi::IncidenceChanger::ResultCode");
147 qRegisterMetaType<ITIPHandlerHelper::SendResult>(
"ITIPHandlerHelper::SendResult");
150IncidenceChangerPrivate::~IncidenceChangerPrivate()
152 if (!mAtomicOperations.isEmpty() || !mQueuedModifications.isEmpty() || !mModificationsInProgress.isEmpty()) {
153 qCDebug(AKONADICALENDAR_LOG) <<
"Normal if the application was being used. "
154 "But might indicate a memory leak if it wasn't";
158bool IncidenceChangerPrivate::atomicOperationIsValid(uint atomicOperationId)
const
161 return mAtomicOperations.contains(atomicOperationId) && !mAtomicOperations[atomicOperationId]->m_endCalled;
164bool IncidenceChangerPrivate::hasRights(
const Collection &collection, IncidenceChanger::ChangeType changeType)
const
167 switch (changeType) {
168 case IncidenceChanger::ChangeTypeCreate:
171 case IncidenceChanger::ChangeTypeModify:
174 case IncidenceChanger::ChangeTypeDelete:
178 Q_ASSERT_X(
false,
"hasRights",
"invalid type");
181 return !collection.
isValid() || !mRespectsCollectionRights || result;
184Akonadi::Job *IncidenceChangerPrivate::parentJob(
const Change::Ptr &change)
const
186 return (mBatchOperationInProgress && !change->queuedModification) ? mAtomicOperations[mLatestAtomicOperationId]->transaction() :
nullptr;
189void IncidenceChangerPrivate::queueModification(
const Change::Ptr &change)
195 if (mQueuedModifications.contains(
id)) {
196 Change::Ptr toBeDiscarded = mQueuedModifications.take(
id);
197 toBeDiscarded->resultCode = IncidenceChanger::ResultCodeModificationDiscarded;
198 toBeDiscarded->completed =
true;
199 mChangeById.remove(toBeDiscarded->id);
202 change->queuedModification =
true;
203 mQueuedModifications[id] = change;
208 mModificationsInProgress.remove(
id);
210 if (mQueuedModifications.contains(
id)) {
211 const Change::Ptr change = mQueuedModifications.take(
id);
212 performModification(change);
216void IncidenceChangerPrivate::handleTransactionJobResult(
KJob *job)
218 auto transaction = qobject_cast<TransactionSequence *>(job);
219 Q_ASSERT(transaction);
220 Q_ASSERT(mAtomicOperationByTransaction.contains(transaction));
222 const uint atomicOperationId = mAtomicOperationByTransaction.take(transaction);
224 Q_ASSERT(mAtomicOperations.contains(atomicOperationId));
225 AtomicOperation *operation = mAtomicOperations[atomicOperationId];
227 Q_ASSERT(operation->m_id == atomicOperationId);
229 if (!operation->rolledback()) {
230 operation->setRolledback();
232 qCritical() <<
"Transaction failed, everything was rolledback. " << job->
errorString();
234 Q_ASSERT(operation->m_endCalled);
235 Q_ASSERT(!operation->pendingJobs());
238 if (!operation->pendingJobs() && operation->m_endCalled) {
239 delete mAtomicOperations.take(atomicOperationId);
240 mBatchOperationInProgress =
false;
242 operation->m_transactionCompleted =
true;
246void IncidenceChangerPrivate::handleCreateJobResult(
KJob *job)
248 Change::Ptr change = mChangeForJob.take(job);
250 const auto j = qobject_cast<const ItemCreateJob *>(job);
255 const QString errorString = j->errorString();
256 IncidenceChanger::ResultCode resultCode = IncidenceChanger::ResultCodeJobError;
257 item = change->newItem;
258 qCritical() << errorString;
259 if (mShowDialogsOnError) {
260 KMessageBox::error(change->parentWidget,
i18n(
"Error while trying to create calendar item. Error was: %1", errorString));
262 mChangeById.remove(change->id);
263 change->errorString = errorString;
264 change->resultCode = resultCode;
269 change->newItem = item;
271 if (change->useGroupwareCommunication) {
272 connect(change.data(), &Change::dialogClosedAfterChange,
this, &IncidenceChangerPrivate::handleCreateJobResult2);
273 handleInvitationsAfterChange(change);
275 handleCreateJobResult2(change->id, ITIPHandlerHelper::ResultSuccess);
280void IncidenceChangerPrivate::handleCreateJobResult2(
int changeId, ITIPHandlerHelper::SendResult
status)
282 Change::Ptr change = mChangeById[changeId];
285 mChangeById.remove(changeId);
287 if (
status == ITIPHandlerHelper::ResultFailAbortUpdate) {
288 qCritical() <<
"Sending invitations failed, but did not delete the incidence";
291 const uint atomicOperationId = change->atomicOperationId;
292 if (atomicOperationId != 0) {
293 mInvitationStatusByAtomicOperation.insert(atomicOperationId,
status);
297 if (change->atomicOperationId != 0) {
298 AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
299 ++a->m_numCompletedChanges;
300 change->completed =
true;
301 description = a->m_description;
305 if (change->recordToHistory) {
306 mHistory->recordCreation(item, description, change->atomicOperationId);
309 change->errorString =
QString();
310 change->resultCode = IncidenceChanger::ResultCodeSuccess;
314void IncidenceChangerPrivate::handleDeleteJobResult(
KJob *job)
316 Change::Ptr change = mChangeForJob.take(job);
318 const auto j = qobject_cast<const ItemDeleteJob *>(job);
323 deletionChange->mItemIds.reserve(deletionChange->mItemIds.count() + items.
count());
325 deletionChange->mItemIds.append(item.
id());
328 if (change->atomicOperationId != 0) {
329 AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
330 a->m_numCompletedChanges++;
331 change->completed =
true;
332 description = a->m_description;
335 const QString errorString = j->errorString();
336 qCritical() << errorString;
338 if (mShowDialogsOnError) {
339 KMessageBox::error(change->parentWidget,
i18n(
"Error while trying to delete calendar item. Error was: %1", errorString));
342 for (
const Item &item : items) {
344 mDeletedItemIds.remove(mDeletedItemIds.indexOf(item.
id()));
346 mChangeById.remove(change->id);
347 change->resultCode = IncidenceChanger::ResultCodeJobError;
348 change->errorString = errorString;
349 change->emitCompletionSignal();
351 if (change->recordToHistory) {
353 mHistory->recordDeletions(items, description, change->atomicOperationId);
356 if (change->useGroupwareCommunication) {
357 connect(change.data(), &Change::dialogClosedAfterChange,
this, &IncidenceChangerPrivate::handleDeleteJobResult2);
358 handleInvitationsAfterChange(change);
360 handleDeleteJobResult2(change->id, ITIPHandlerHelper::ResultSuccess);
365void IncidenceChangerPrivate::handleDeleteJobResult2(
int changeId, ITIPHandlerHelper::SendResult
status)
367 Change::Ptr change = mChangeById[changeId];
368 mChangeById.remove(change->id);
370 if (
status == ITIPHandlerHelper::ResultSuccess) {
371 change->errorString =
QString();
372 change->resultCode = IncidenceChanger::ResultCodeSuccess;
374 change->errorString =
i18nc(
"errormessage for a job ended with an unexpected result",
"An unknown error occurred");
375 change->resultCode = IncidenceChanger::ResultCodeJobError;
381void IncidenceChangerPrivate::handleModifyJobResult(
KJob *job)
383 Change::Ptr change = mChangeForJob.take(job);
385 const auto j = qobject_cast<const ItemModifyJob *>(job);
386 const Item item = j->item();
387 Q_ASSERT(mDirtyFieldsByJob.contains(job));
392 if (change->atomicOperationId != 0) {
393 AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
394 a->m_numCompletedChanges++;
395 change->completed =
true;
396 description = a->m_description;
399 const QString errorString = j->errorString();
400 IncidenceChanger::ResultCode resultCode = IncidenceChanger::ResultCodeJobError;
401 if (deleteAlreadyCalled(item.id())) {
405 resultCode = IncidenceChanger::ResultCodeAlreadyDeleted;
406 qCWarning(AKONADICALENDAR_LOG) <<
"Trying to change item " << item.id() <<
" while deletion is in progress.";
408 qCritical() << errorString;
410 if (mShowDialogsOnError) {
411 KMessageBox::error(change->parentWidget,
i18n(
"Error while trying to modify calendar item. Error was: %1", errorString));
413 mChangeById.remove(change->id);
414 change->errorString = errorString;
415 change->resultCode = resultCode;
420 (*(s_latestRevisionByItemId()))[item.id()] = item.revision();
421 change->newItem = item;
422 if (change->recordToHistory && !change->originalItems.isEmpty()) {
423 Q_ASSERT(change->originalItems.count() == 1);
424 mHistory->recordModification(change->originalItems.constFirst(), item, description, change->atomicOperationId);
427 if (change->useGroupwareCommunication) {
428 connect(change.data(), &Change::dialogClosedAfterChange,
this, &IncidenceChangerPrivate::handleModifyJobResult2);
429 handleInvitationsAfterChange(change);
431 handleModifyJobResult2(change->id, ITIPHandlerHelper::ResultSuccess);
436void IncidenceChangerPrivate::handleModifyJobResult2(
int changeId, ITIPHandlerHelper::SendResult
status)
438 Change::Ptr change = mChangeById[changeId];
440 mChangeById.remove(changeId);
441 if (change->atomicOperationId != 0) {
442 mInvitationStatusByAtomicOperation.insert(change->atomicOperationId,
status);
444 change->errorString =
QString();
445 change->resultCode = IncidenceChanger::ResultCodeSuccess;
453 return mDeletedItemIds.contains(
id);
456void IncidenceChangerPrivate::handleInvitationsBeforeChange(
const Change::Ptr &change)
458 if (mGroupwareCommunication) {
459 ITIPHandlerHelper::SendResult result = ITIPHandlerHelper::ResultSuccess;
460 switch (change->type) {
461 case IncidenceChanger::ChangeTypeCreate:
464 case IncidenceChanger::ChangeTypeDelete: {
465 ITIPHandlerHelper::SendResult
status;
467 Q_ASSERT(!change->originalItems.isEmpty());
469 auto handler =
new ITIPHandlerHelper(mFactory, change->parentWidget);
470 handler->setParent(
this);
471 updateHandlerPrivacyPolicy(handler, m_invitationPrivacy);
473 if (m_invitationPolicy == IncidenceChanger::InvitationPolicySend) {
475 }
else if (m_invitationPolicy == IncidenceChanger::InvitationPolicyDontSend) {
477 }
else if (mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) {
478 handler->setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId)));
481 connect(handler, &ITIPHandlerHelper::finished, change.data(), &Change::emitUserDialogClosedBeforeChange);
486 if (!
incidence->supportsGroupwareCommunication()) {
491 if (Akonadi::CalendarUtils::thatIsMe(
incidence->organizer().email())) {
495 if (change->atomicOperationId) {
496 mInvitationStatusByAtomicOperation.insert(change->atomicOperationId,
status);
503 change->emitUserDialogClosedBeforeChange(result);
507 case IncidenceChanger::ChangeTypeModify: {
508 if (change->originalItems.isEmpty()) {
512 Q_ASSERT(change->originalItems.count() == 1);
516 if (!oldIncidence->supportsGroupwareCommunication()) {
520 if (allowedModificationsWithoutRevisionUpdate(newIncidence)) {
521 change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess);
525 if (akonadi_calendar_running_unittests && !weAreOrganizer(newIncidence)) {
529 if (m_invitationPolicy == IncidenceChanger::InvitationPolicySend) {
530 change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess);
532 }
else if (m_invitationPolicy == IncidenceChanger::InvitationPolicyDontSend) {
533 change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultCanceled);
538 ITIPHandlerHelper handler(mFactory, change->parentWidget);
539 const bool modify = handler.handleIncidenceAboutToBeModified(newIncidence);
543 result = ITIPHandlerHelper::ResultCanceled;
546 if (newIncidence->type() == oldIncidence->type()) {
555 result = ITIPHandlerHelper::ResultCanceled;
557 change->emitUserDialogClosedBeforeChange(result);
559 change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess);
563void IncidenceChangerPrivate::handleInvitationsAfterChange(
const Change::Ptr &change)
565 if (change->useGroupwareCommunication) {
566 auto handler =
new ITIPHandlerHelper(mFactory, change->parentWidget);
567 connect(handler, &ITIPHandlerHelper::finished, change.data(), &Change::emitUserDialogClosedAfterChange);
568 handler->setParent(
this);
569 updateHandlerPrivacyPolicy(handler, m_invitationPrivacy);
571 const bool alwaysSend = (m_invitationPolicy == IncidenceChanger::InvitationPolicySend);
572 const bool neverSend = (m_invitationPolicy == IncidenceChanger::InvitationPolicyDontSend);
581 switch (change->type) {
582 case IncidenceChanger::ChangeTypeCreate: {
584 if (
incidence->supportsGroupwareCommunication()) {
590 case IncidenceChanger::ChangeTypeDelete:
591 handler->deleteLater();
593 Q_ASSERT(!change->originalItems.isEmpty());
595 Q_ASSERT(item.hasPayload());
598 if (!
incidence->supportsGroupwareCommunication()) {
602 if (!Akonadi::CalendarUtils::thatIsMe(
incidence->organizer().email())) {
603 const QStringList myEmails = Akonadi::CalendarUtils::allEmails();
604 bool notifyOrganizer =
false;
608 notifyOrganizer =
true;
617 if (notifyOrganizer) {
618 MailScheduler scheduler(mFactory, change->parentWidget);
624 case IncidenceChanger::ChangeTypeModify: {
625 if (change->originalItems.isEmpty()) {
629 Q_ASSERT(change->originalItems.count() == 1);
633 if (!newIncidence->supportsGroupwareCommunication() || !Akonadi::CalendarUtils::thatIsMe(newIncidence->organizer().email())) {
638 if (allowedModificationsWithoutRevisionUpdate(newIncidence)) {
642 if (!neverSend && !alwaysSend && mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) {
643 handler->setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId)));
646 const bool attendeeStatusChanged = myAttendeeStatusChanged(newIncidence, oldIncidence, Akonadi::CalendarUtils::allEmails());
652 handler->deleteLater();
655 change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultCanceled);
658 handler->deleteLater();
660 change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultSuccess);
662 change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultSuccess);
671 const Attendee oldMe = oldInc->attendeeByMails(myEmails);
672 const Attendee newMe = newInc->attendeeByMails(myEmails);
677IncidenceChanger::IncidenceChanger(
QObject *parent)
679 , d(new IncidenceChangerPrivate(true, nullptr, this))
685 , d(new IncidenceChangerPrivate(true, factory, this))
689IncidenceChanger::IncidenceChanger(
bool enableHistory,
QObject *parent)
691 , d(new IncidenceChangerPrivate(enableHistory, nullptr, this))
695IncidenceChanger::~IncidenceChanger() =
default;
699 const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
701 const Change::Ptr change(
new CreationChange(
this, ++d->mLatestChangeId, atomicOperationId,
parent));
702 const int changeId = change->id;
703 Q_ASSERT(!(d->mBatchOperationInProgress && !d->mAtomicOperations.contains(atomicOperationId)));
704 if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
708 change->resultCode = ResultCodeRolledback;
710 d->cleanupTransaction();
714 change->newItem = item;
716 d->step1DetermineDestinationCollection(change, collection);
724 qCWarning(AKONADICALENDAR_LOG) <<
"An invalid payload is not allowed.";
725 d->cancelTransaction();
733 return createFromItem(item, collection,
parent);
736int IncidenceChanger::deleteIncidence(
const Item &item,
QWidget *parent)
741 return deleteIncidences(list,
parent);
747 qCritical() <<
"Delete what?";
748 d->cancelTransaction();
752 for (
const Item &item : items) {
753 if (!item.isValid()) {
754 qCritical() <<
"Items must be valid!";
755 d->cancelTransaction();
760 const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
761 const int changeId = ++d->mLatestChangeId;
762 const Change::Ptr change(
new DeletionChange(
this, changeId, atomicOperationId,
parent));
764 for (
const Item &item : items) {
765 if (!d->hasRights(item.parentCollection(), ChangeTypeDelete)) {
766 qCWarning(AKONADICALENDAR_LOG) <<
"Item " << item.id() <<
" can't be deleted due to ACL restrictions";
767 const QString errorString = d->showErrorDialog(ResultCodePermissions,
parent);
768 change->resultCode = ResultCodePermissions;
769 change->errorString = errorString;
770 d->cancelTransaction();
775 if (!d->allowAtomicOperation(atomicOperationId, change)) {
776 const QString errorString = d->showErrorDialog(ResultCodeDuplicateId,
parent);
777 change->resultCode = ResultCodeDuplicateId;
778 change->errorString = errorString;
779 qCWarning(AKONADICALENDAR_LOG) << errorString;
780 d->cancelTransaction();
785 for (
const Item &item : items) {
786 if (d->deleteAlreadyCalled(item.id())) {
788 qCDebug(AKONADICALENDAR_LOG) <<
"Item " << item.id() <<
" already deleted or being deleted, skipping";
790 itemsToDelete.
append(item);
794 if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
796 change->resultCode = ResultCodeRolledback;
799 d->cleanupTransaction();
806 qCDebug(AKONADICALENDAR_LOG) <<
"Items already deleted or being deleted, skipping";
809 change->resultCode = ResultCodeAlreadyDeleted;
811 d->cancelTransaction();
815 change->originalItems = itemsToDelete;
817 d->mChangeById.
insert(changeId, change);
819 if (d->mGroupwareCommunication) {
820 connect(change.data(), &Change::dialogClosedBeforeChange, d.get(), &IncidenceChangerPrivate::deleteIncidences2);
821 d->handleInvitationsBeforeChange(change);
823 d->deleteIncidences2(changeId, ITIPHandlerHelper::ResultSuccess);
828void IncidenceChangerPrivate::deleteIncidences2(
int changeId, ITIPHandlerHelper::SendResult
status)
831 Change::Ptr change = mChangeById[changeId];
832 const uint atomicOperationId = change->atomicOperationId;
833 auto deleteJob =
new ItemDeleteJob(change->originalItems, parentJob(change));
834 mChangeForJob.insert(deleteJob, change);
836 if (mBatchOperationInProgress) {
837 AtomicOperation *atomic = mAtomicOperations[atomicOperationId];
839 atomic->addChange(change);
842 mDeletedItemIds.reserve(mDeletedItemIds.count() + change->originalItems.count());
843 for (
const Item &item :
std::as_const(change->originalItems)) {
844 mDeletedItemIds << item.id();
848 if (mDeletedItemIds.count() > 100) {
849 mDeletedItemIds.remove(0, 50);
858 if (!changedItem.isValid() || !changedItem.hasPayload<
Incidence::Ptr>()) {
859 qCWarning(AKONADICALENDAR_LOG) <<
"An invalid item or payload is not allowed.";
860 d->cancelTransaction();
864 if (!d->hasRights(changedItem.parentCollection(), ChangeTypeModify)) {
865 qCWarning(AKONADICALENDAR_LOG) <<
"Item " << changedItem.id() <<
" can't be deleted due to ACL restrictions";
866 const int changeId = ++d->mLatestChangeId;
867 const QString errorString = d->showErrorDialog(ResultCodePermissions,
parent);
868 emitModifyFinished(
this, changeId, changedItem, ResultCodePermissions, errorString);
869 d->cancelTransaction();
876 const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
877 const int changeId = ++d->mLatestChangeId;
878 auto modificationChange =
new ModificationChange(
this, changeId, atomicOperationId,
parent);
879 Change::Ptr change(modificationChange);
881 if (originalPayload) {
882 Item originalItem(changedItem);
884 modificationChange->originalItems << originalItem;
887 modificationChange->newItem = changedItem;
888 d->mChangeById.insert(changeId, change);
890 if (!d->allowAtomicOperation(atomicOperationId, change)) {
891 const QString errorString = d->showErrorDialog(ResultCodeDuplicateId,
parent);
893 change->resultCode = ResultCodeDuplicateId;
894 change->errorString = errorString;
895 d->cancelTransaction();
896 qCWarning(AKONADICALENDAR_LOG) <<
"Atomic operation now allowed";
900 if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
903 d->cleanupTransaction();
904 emitModifyFinished(
this, changeId, changedItem, ResultCodeRolledback, errorMessage);
907 d->performModification(change);
913void IncidenceChangerPrivate::performModification(
const Change::Ptr &change)
915 const Item::Id id = change->newItem.id();
920 const int changeId = change->id;
922 if (deleteAlreadyCalled(
id)) {
924 qCDebug(AKONADICALENDAR_LOG) <<
"Item " <<
id <<
" already deleted or being deleted, skipping";
927 emitModifyFinished(q,
930 IncidenceChanger::ResultCodeAlreadyDeleted,
931 i18n(
"That calendar item was already deleted, or currently being deleted."));
935 const uint atomicOperationId = change->atomicOperationId;
936 const bool hasAtomicOperationId = atomicOperationId != 0;
937 if (hasAtomicOperationId && mAtomicOperations[atomicOperationId]->rolledback()) {
938 const QString errorMessage = showErrorDialog(IncidenceChanger::ResultCodeRolledback,
nullptr);
940 emitModifyFinished(q, changeId, newItem, IncidenceChanger::ResultCodeRolledback, errorMessage);
943 if (mGroupwareCommunication) {
944 connect(change.data(), &Change::dialogClosedBeforeChange,
this, &IncidenceChangerPrivate::performModification2);
945 handleInvitationsBeforeChange(change);
947 performModification2(change->id, ITIPHandlerHelper::ResultSuccess);
951void IncidenceChangerPrivate::performModification2(
int changeId, ITIPHandlerHelper::SendResult
status)
953 Change::Ptr change = mChangeById[changeId];
954 const Item::Id id = change->newItem.id();
958 if (
status == ITIPHandlerHelper::ResultCanceled) {
960 qCDebug(AKONADICALENDAR_LOG) <<
"User cancelled, giving up";
961 emitModifyFinished(q, change->id, newItem, IncidenceChanger::ResultCodeUserCanceled,
QString());
965 const uint atomicOperationId = change->atomicOperationId;
966 const bool hasAtomicOperationId = atomicOperationId != 0;
969 if (latestRevisionByItemId.
contains(
id) && latestRevisionByItemId[id] > newItem.
revision()) {
982 if (!allowedModificationsWithoutRevisionUpdate(incidence)) {
983 const int revision =
incidence->revision();
991 if (!(
incidence->dirtyFields() & resetPartStatus).isEmpty() && weAreOrganizer(incidence)) {
993 for (
auto &attendee : attendees) {
996 attendee.setRSVP(
true);
1007 if (mModificationsInProgress.contains(newItem.
id())) {
1010 queueModification(change);
1012 auto modifyJob =
new ItemModifyJob(newItem, parentJob(change));
1013 mChangeForJob.insert(modifyJob, change);
1014 mDirtyFieldsByJob.insert(modifyJob,
incidence->dirtyFields());
1016 if (hasAtomicOperationId) {
1017 AtomicOperation *atomic = mAtomicOperations[atomicOperationId];
1019 atomic->addChange(change);
1022 mModificationsInProgress[newItem.
id()] = change;
1028void IncidenceChanger::startAtomicOperation(
const QString &operationDescription)
1030 if (d->mBatchOperationInProgress) {
1031 qCDebug(AKONADICALENDAR_LOG) <<
"An atomic operation is already in progress.";
1035 ++d->mLatestAtomicOperationId;
1036 d->mBatchOperationInProgress =
true;
1038 auto atomicOperation =
new AtomicOperation(d.get(), d->mLatestAtomicOperationId);
1039 atomicOperation->m_description = operationDescription;
1040 d->mAtomicOperations.
insert(d->mLatestAtomicOperationId, atomicOperation);
1043void IncidenceChanger::endAtomicOperation()
1045 if (!d->mBatchOperationInProgress) {
1046 qCDebug(AKONADICALENDAR_LOG) <<
"No atomic operation is in progress.";
1050 Q_ASSERT_X(d->mLatestAtomicOperationId != 0,
"IncidenceChanger::endAtomicOperation()",
"Call startAtomicOperation() first.");
1052 Q_ASSERT(d->mAtomicOperations.contains(d->mLatestAtomicOperationId));
1053 AtomicOperation *atomicOperation = d->mAtomicOperations[d->mLatestAtomicOperationId];
1054 Q_ASSERT(atomicOperation);
1055 atomicOperation->m_endCalled =
true;
1057 const bool allJobsCompleted = !atomicOperation->pendingJobs();
1059 if (allJobsCompleted && atomicOperation->rolledback() && atomicOperation->m_transactionCompleted) {
1061 delete d->mAtomicOperations.take(d->mLatestAtomicOperationId);
1062 d->mBatchOperationInProgress =
false;
1069void IncidenceChanger::setShowDialogsOnError(
bool enable)
1071 d->mShowDialogsOnError = enable;
1073 d->mHistory->incidenceChanger()->setShowDialogsOnError(enable);
1077bool IncidenceChanger::showDialogsOnError()
const
1079 return d->mShowDialogsOnError;
1082void IncidenceChanger::setRespectsCollectionRights(
bool respects)
1084 d->mRespectsCollectionRights = respects;
1087bool IncidenceChanger::respectsCollectionRights()
const
1089 return d->mRespectsCollectionRights;
1092void IncidenceChanger::setDestinationPolicy(IncidenceChanger::DestinationPolicy destinationPolicy)
1094 d->mDestinationPolicy = destinationPolicy;
1097IncidenceChanger::DestinationPolicy IncidenceChanger::destinationPolicy()
const
1099 return d->mDestinationPolicy;
1104 d->mEntityTreeModel = entityTreeModel;
1109 return d->mEntityTreeModel;
1114 d->mDefaultCollection = collection;
1117Collection IncidenceChanger::defaultCollection()
const
1119 return d->mDefaultCollection;
1122bool IncidenceChanger::historyEnabled()
const
1124 return d->mUseHistory;
1127void IncidenceChanger::setHistoryEnabled(
bool enable)
1129 if (d->mUseHistory != enable) {
1130 d->mUseHistory = enable;
1131 if (enable && !d->mHistory) {
1132 d->mHistory =
new History(d.get());
1137History *IncidenceChanger::history()
const
1144 return d->deleteAlreadyCalled(
id);
1147void IncidenceChanger::setGroupwareCommunication(
bool enabled)
1149 d->mGroupwareCommunication = enabled;
1152bool IncidenceChanger::groupwareCommunication()
const
1154 return d->mGroupwareCommunication;
1157void IncidenceChanger::setAutoAdjustRecurrence(
bool enable)
1159 d->mAutoAdjustRecurrence = enable;
1162bool IncidenceChanger::autoAdjustRecurrence()
const
1164 return d->mAutoAdjustRecurrence;
1167void IncidenceChanger::setInvitationPolicy(IncidenceChanger::InvitationPolicy policy)
1169 d->m_invitationPolicy = policy;
1172IncidenceChanger::InvitationPolicy IncidenceChanger::invitationPolicy()
const
1174 return d->m_invitationPolicy;
1179 return d->mLastCollectionUsed;
1184 d->m_invitationPrivacy = invitationPrivacy;
1189 return d->m_invitationPrivacy;
1192QString IncidenceChangerPrivate::showErrorDialog(IncidenceChanger::ResultCode resultCode,
QWidget *parent)
1195 switch (resultCode) {
1196 case IncidenceChanger::ResultCodePermissions:
1197 errorString =
i18n(
"Operation can not be performed due to ACL restrictions");
1199 case IncidenceChanger::ResultCodeInvalidUserCollection:
1200 errorString =
i18n(
"The chosen collection is invalid");
1202 case IncidenceChanger::ResultCodeInvalidDefaultCollection:
1204 "Default collection is invalid or doesn't have proper ACLs"
1205 " and DestinationPolicyNeverAsk was used");
1207 case IncidenceChanger::ResultCodeDuplicateId:
1208 errorString =
i18n(
"Duplicate item id in a group operation");
1210 case IncidenceChanger::ResultCodeRolledback:
1212 "One change belonging to a group of changes failed. "
1213 "All changes are being rolled back.");
1220 if (mShowDialogsOnError) {
1229 if (!originalIncidence || !
incidence->recurs() ||
incidence->hasRecurrenceId() || !mAutoAdjustRecurrence
1234 const QDate originalDate = originalIncidence->dtStart().date();
1237 if (!originalDate.
isValid() || !newStartDate.
isValid() || originalDate == newStartDate) {
1243 case KCalendarCore::Recurrence::rWeekly: {
1245 const int oldIndex = originalDate.
dayOfWeek() - 1;
1246 const int newIndex = newStartDate.
dayOfWeek() - 1;
1247 if (oldIndex != newIndex) {
1261 const QDate recurrenceEndDate = recurrence->defaultRRule() ? recurrence->defaultRRule()->endDt().date() :
QDate();
1262 if (recurrenceEndDate.
isValid() && recurrenceEndDate < newStartDate) {
1267void IncidenceChangerPrivate::cancelTransaction()
1269 if (mBatchOperationInProgress) {
1270 mAtomicOperations[mLatestAtomicOperationId]->setRolledback();
1274void IncidenceChangerPrivate::cleanupTransaction()
1276 Q_ASSERT(mAtomicOperations.contains(mLatestAtomicOperationId));
1277 AtomicOperation *operation = mAtomicOperations[mLatestAtomicOperationId];
1278 Q_ASSERT(operation);
1279 Q_ASSERT(operation->rolledback());
1280 if (!operation->pendingJobs() && operation->m_endCalled && operation->m_transactionCompleted) {
1281 delete mAtomicOperations.take(mLatestAtomicOperationId);
1282 mBatchOperationInProgress =
false;
1286bool IncidenceChangerPrivate::allowAtomicOperation(
int atomicOperationId,
const Change::Ptr &change)
const
1289 if (atomicOperationId > 0) {
1290 Q_ASSERT(mAtomicOperations.contains(atomicOperationId));
1291 AtomicOperation *operation = mAtomicOperations.value(atomicOperationId);
1293 if (change->type == IncidenceChanger::ChangeTypeCreate) {
1295 }
else if (change->type == IncidenceChanger::ChangeTypeModify) {
1296 allow = !operation->m_itemIdsInOperation.contains(change->newItem.id());
1297 }
else if (change->type == IncidenceChanger::ChangeTypeDelete) {
1298 DeletionChange::Ptr deletion = change.staticCast<DeletionChange>();
1300 if (operation->m_itemIdsInOperation.contains(
id)) {
1309 qCWarning(AKONADICALENDAR_LOG) <<
"Each change belonging to a group operation"
1310 <<
"must have a different Akonadi::Item::Id";
1317void ModificationChange::emitCompletionSignal()
1319 emitModifyFinished(changer,
id, newItem, resultCode, errorString);
1323void CreationChange::emitCompletionSignal()
1326 emitCreateFinished(changer,
id, newItem, resultCode, errorString);
1330void DeletionChange::emitCompletionSignal()
1332 emitDeleteFinished(changer,
id, mItemIds, resultCode, errorString);
1367AtomicOperation::AtomicOperation(IncidenceChangerPrivate *icp, uint ident)
1369 , m_endCalled(false)
1370 , m_numCompletedChanges(0)
1371 , m_transactionCompleted(false)
1372 , m_wasRolledback(false)
1373 , m_transaction(nullptr)
1374 , m_incidenceChangerPrivate(icp)
1376 Q_ASSERT(m_id != 0);
1381 if (!m_transaction) {
1385 m_incidenceChangerPrivate->mAtomicOperationByTransaction.insert(m_transaction, m_id);
1390 return m_transaction;
1393#include "moc_incidencechanger.cpp"
History class for implementing undo/redo of calendar operations.
Factory to create Akonadi::MessageQueueJob jobs or ITIPHandlerDialogDelegate objects.
@ ActionAsk
Ask the user for a decision.
@ ActionDontSendMessage
Answer with No.
@ ActionSendMessage
Answer with Yes.
void setRevision(int revision)
void setRemoteRevision(const QString &revision)
void setAutomaticCommittingEnabled(bool enable)
ushort recurrenceType() const
void setWeekly(int freq, const QBitArray &days, int weekStart=1)
void setEndDate(const QDate &endDate)
virtual QString errorString() const
Q_SCRIPTABLE CaptureState status()
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Incidence::Ptr incidence(const Akonadi::Item &item)
Returns the incidence from an Akonadi item, or a null pointer if the item has no such payload.
FreeBusyManager::Singleton.
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
void clearBit(qsizetype i)
int dayOfWeek() const const
bool isValid(int year, int month, int day)
QDateTime currentDateTimeUtc()
bool contains(const Key &key) const const
void append(QList< T > &&value)
qsizetype count() const const
iterator insert(const_iterator before, parameter_type value)
bool isEmpty() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
QSharedPointer< X > staticCast() const const
QString & insert(qsizetype position, QChar ch)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)