20 #include "incidencechanger.h"
21 #include "incidencechanger_p.h"
22 #include "mailscheduler_p.h"
25 #include <akonadi/itemcreatejob.h>
26 #include <akonadi/itemmodifyjob.h>
27 #include <akonadi/itemdeletejob.h>
28 #include <akonadi/transactionsequence.h>
29 #include <akonadi/collectiondialog.h>
32 #include <KLocalizedString>
34 #include <KMessageBox>
35 #include <KStandardGuiItem>
37 using namespace Akonadi;
38 using namespace KCalCore;
52 return ITIPHandlerHelper::ActionDontSendMessage;
54 return ITIPHandlerHelper::ActionSendMessage;
56 return ITIPHandlerHelper::ActionAsk;
63 const QStringList &mimeTypes,
68 kDebug() <<
"selecting collections with mimeType in " << mimeTypes;
70 dlg->changeCollectionDialogOptions( Akonadi::CollectionDialog::KeepTreeExpanded );
71 dlg->setMimeTypeFilter( mimeTypes );
73 if ( defCollection.
isValid() ) {
74 dlg->setDefaultCollection( defCollection );
79 dialogCode = dlg->exec();
80 if ( dialogCode == QDialog::Accepted ) {
81 collection = dlg->selectedCollection();
84 kWarning() <<
"An invalid collection was selected!";
93 static void emitCreateFinished( IncidenceChanger *changer,
95 const Akonadi::Item &item,
96 Akonadi::IncidenceChanger::ResultCode resultCode,
97 const QString &errorString )
99 QMetaObject::invokeMethod( changer,
"createFinished", Qt::QueuedConnection,
100 Q_ARG(
int, changeId ),
101 Q_ARG( Akonadi::Item, item ),
102 Q_ARG( Akonadi::IncidenceChanger::ResultCode, resultCode ),
103 Q_ARG( QString, errorString ) );
107 static void emitModifyFinished( IncidenceChanger *changer,
109 const Akonadi::Item &item,
110 IncidenceChanger::ResultCode resultCode,
111 const QString &errorString )
113 QMetaObject::invokeMethod( changer,
"modifyFinished", Qt::QueuedConnection,
114 Q_ARG(
int, changeId ),
115 Q_ARG( Akonadi::Item, item ),
116 Q_ARG( Akonadi::IncidenceChanger::ResultCode, resultCode ),
117 Q_ARG( QString, errorString ) );
121 static void emitDeleteFinished( IncidenceChanger *changer,
123 const QVector<Akonadi::Item::Id> &itemIdList,
124 IncidenceChanger::ResultCode resultCode,
125 const QString &errorString )
127 QMetaObject::invokeMethod( changer,
"deleteFinished", Qt::QueuedConnection,
128 Q_ARG(
int, changeId ),
129 Q_ARG( QVector<Akonadi::Item::Id>, itemIdList ),
130 Q_ARG( Akonadi::IncidenceChanger::ResultCode, resultCode ),
131 Q_ARG( QString, errorString ) );
135 class ConflictPreventerPrivate;
136 class ConflictPreventer {
137 friend class ConflictPreventerPrivate;
139 static ConflictPreventer*
self();
142 QHash<Akonadi::Item::Id, int> mLatestRevisionByItemId;
144 ConflictPreventer() {}
145 ~ConflictPreventer() {}
148 class ConflictPreventerPrivate {
150 ConflictPreventer instance;
153 K_GLOBAL_STATIC( ConflictPreventerPrivate, sConflictPreventerPrivate )
155 ConflictPreventer* ConflictPreventer::self()
157 return &sConflictPreventerPrivate->instance;
160 IncidenceChanger::Private::Private(
bool enableHistory, IncidenceChanger *qq ) : q( qq )
163 mShowDialogsOnError =
true;
164 mHistory = enableHistory ?
new History(
this ) : 0;
165 mUseHistory = enableHistory;
166 mDestinationPolicy = DestinationPolicyDefault;
167 mRespectsCollectionRights =
false;
168 mGroupwareCommunication =
false;
169 mLatestAtomicOperationId = 0;
170 mBatchOperationInProgress =
false;
172 qRegisterMetaType<QVector<Akonadi::Item::Id> >(
"QVector<Akonadi::Item::Id>" );
173 qRegisterMetaType<Akonadi::Item::Id>(
"Akonadi::Item::Id" );
174 qRegisterMetaType<Akonadi::Item>(
"Akonadi::Item");
175 qRegisterMetaType<Akonadi::IncidenceChanger::ResultCode>(
176 "Akonadi::IncidenceChanger::ResultCode" );
179 IncidenceChanger::Private::~Private()
181 if ( !mAtomicOperations.isEmpty() ||
182 !mQueuedModifications.isEmpty() ||
183 !mModificationsInProgress.isEmpty() ) {
184 kDebug() <<
"Normal if the application was being used. "
185 "But might indicate a memory leak if it wasn't";
189 bool IncidenceChanger::Private::atomicOperationIsValid( uint atomicOperationId )
const
192 return mAtomicOperations.contains( atomicOperationId ) &&
193 !mAtomicOperations[atomicOperationId]->m_endCalled;
196 bool IncidenceChanger::Private::hasRights(
const Collection &collection,
197 IncidenceChanger::ChangeType changeType )
const
200 switch( changeType ) {
201 case ChangeTypeCreate:
204 case ChangeTypeModify:
207 case ChangeTypeDelete:
211 Q_ASSERT_X(
false,
"hasRights",
"invalid type" );
214 return !collection.
isValid() || !mRespectsCollectionRights || result;
217 Akonadi::Job* IncidenceChanger::Private::parentJob(
const Change::Ptr &change )
const
219 return (mBatchOperationInProgress && !change->queuedModification) ?
220 mAtomicOperations[mLatestAtomicOperationId]->transaction() : 0;
223 void IncidenceChanger::Private::queueModification( Change::Ptr change )
228 const Akonadi::Item::Id
id = change->newItem.id();
229 if ( mQueuedModifications.contains(
id ) ) {
230 Change::Ptr toBeDiscarded = mQueuedModifications.take(
id );
231 toBeDiscarded->resultCode = ResultCodeModificationDiscarded;
232 toBeDiscarded->completed =
true;
233 mChangeById.remove( toBeDiscarded->id );
236 change->queuedModification =
true;
237 mQueuedModifications[id] = change;
240 void IncidenceChanger::Private::performNextModification( Akonadi::Item::Id
id )
242 mModificationsInProgress.remove(
id );
244 if ( mQueuedModifications.contains(
id ) ) {
245 const Change::Ptr change = mQueuedModifications.take(
id );
246 performModification( change );
250 void IncidenceChanger::Private::handleTransactionJobResult( KJob *job )
254 Q_ASSERT( transaction );
255 Q_ASSERT( mAtomicOperationByTransaction.contains( transaction ) );
257 const uint atomicOperationId = mAtomicOperationByTransaction.take( transaction );
259 Q_ASSERT( mAtomicOperations.contains(atomicOperationId) );
260 AtomicOperation *operation = mAtomicOperations[atomicOperationId];
261 Q_ASSERT( operation );
262 Q_ASSERT( operation->m_id == atomicOperationId );
263 if ( job->error() ) {
264 if ( !operation->rolledback() )
265 operation->setRolledback();
266 kError() <<
"Transaction failed, everything was rolledback. "
267 << job->errorString();
269 Q_ASSERT( operation->m_endCalled );
270 Q_ASSERT( !operation->pendingJobs() );
273 if ( !operation->pendingJobs() && operation->m_endCalled ) {
274 delete mAtomicOperations.take( atomicOperationId );
275 mBatchOperationInProgress =
false;
277 operation->m_transactionCompleted =
true;
281 void IncidenceChanger::Private::handleCreateJobResult( KJob *job )
285 ResultCode resultCode = ResultCodeSuccess;
287 Change::Ptr change = mChangeForJob.take( job );
288 mChangeById.remove( change->id );
292 Akonadi::Item item = j->
item();
295 if ( change->atomicOperationId != 0 ) {
296 AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
297 a->m_numCompletedChanges++;
298 change->completed =
true;
299 description = a->m_description;
303 item = change->newItem;
304 resultCode = ResultCodeJobError;
306 kError() << errorString;
307 if ( mShowDialogsOnError ) {
308 KMessageBox::sorry( change->parentWidget,
309 i18n(
"Error while trying to create calendar item. Error was: %1",
313 Q_ASSERT( item.isValid() );
314 Q_ASSERT( item.hasPayload<KCalCore::Incidence::Ptr>() );
315 change->newItem = item;
316 handleInvitationsAfterChange( change );
318 if ( change->recordToHistory ) {
319 mHistory->recordCreation( item, description, change->atomicOperationId );
323 change->errorString = errorString;
324 change->resultCode = resultCode;
328 void IncidenceChanger::Private::handleDeleteJobResult( KJob *job )
332 ResultCode resultCode = ResultCodeSuccess;
334 Change::Ptr change = mChangeForJob.take( job );
335 mChangeById.remove( change->id );
340 QSharedPointer<DeletionChange> deletionChange = change.staticCast<DeletionChange>();
342 foreach(
const Akonadi::Item &item, items ) {
343 deletionChange->mItemIds.append( item.id() );
346 if ( change->atomicOperationId != 0 ) {
347 AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
348 a->m_numCompletedChanges++;
349 change->completed =
true;
350 description = a->m_description;
353 resultCode = ResultCodeJobError;
355 kError() << errorString;
356 if ( mShowDialogsOnError ) {
357 KMessageBox::sorry( change->parentWidget,
358 i18n(
"Error while trying to delete calendar item. Error was: %1",
362 foreach(
const Item &item, items ) {
364 mDeletedItemIds.remove( mDeletedItemIds.indexOf( item.id() ) );
367 if ( change->recordToHistory ) {
368 Q_ASSERT( mHistory );
369 mHistory->recordDeletions( items, description, change->atomicOperationId );
372 handleInvitationsAfterChange( change );
375 change->errorString = errorString;
376 change->resultCode = resultCode;
380 void IncidenceChanger::Private::handleModifyJobResult( KJob *job )
383 ResultCode resultCode = ResultCodeSuccess;
384 Change::Ptr change = mChangeForJob.take( job );
385 mChangeById.remove( change->id );
388 const Item item = j->
item();
389 Q_ASSERT( mDirtyFieldsByJob.contains( job ) );
390 Q_ASSERT( item.hasPayload<KCalCore::Incidence::Ptr>() );
391 item.payload<KCalCore::Incidence::Ptr>()->setDirtyFields( mDirtyFieldsByJob.value( job ) );
392 const QSet<KCalCore::IncidenceBase::Field> dirtyFields = mDirtyFieldsByJob.value( job );
394 if ( change->atomicOperationId != 0 ) {
395 AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
396 a->m_numCompletedChanges++;
397 change->completed =
true;
398 description = a->m_description;
401 if ( deleteAlreadyCalled( item.id() ) ) {
405 resultCode = ResultCodeAlreadyDeleted;
407 kWarning() <<
"Trying to change item " << item.id() <<
" while deletion is in progress.";
409 resultCode = ResultCodeJobError;
411 kError() << errorString;
413 if ( mShowDialogsOnError ) {
414 KMessageBox::sorry( change->parentWidget,
415 i18n(
"Error while trying to modify calendar item. Error was: %1",
419 ConflictPreventer::self()->mLatestRevisionByItemId[item.id()] = item.revision();
420 change->newItem = item;
421 if ( change->recordToHistory && !change->originalItems.isEmpty() ) {
422 Q_ASSERT( change->originalItems.count() == 1 );
423 mHistory->recordModification( change->originalItems.first(), item,
424 description, change->atomicOperationId );
427 handleInvitationsAfterChange( change );
430 change->errorString = errorString;
431 change->resultCode = resultCode;
434 QMetaObject::invokeMethod(
this,
"performNextModification",
435 Qt::QueuedConnection,
436 Q_ARG( Akonadi::Item::Id, item.id() ) );
439 bool IncidenceChanger::Private::deleteAlreadyCalled( Akonadi::Item::Id
id )
const
441 return mDeletedItemIds.contains(
id );
444 bool IncidenceChanger::Private::handleInvitationsBeforeChange(
const Change::Ptr &change )
447 if ( mGroupwareCommunication ) {
449 if ( mInvitationStatusByAtomicOperation.contains( change->atomicOperationId ) ) {
450 handler.setDefaultAction( actionFromStatus( mInvitationStatusByAtomicOperation.value( change->atomicOperationId ) ) );
453 switch( change->type ) {
454 case IncidenceChanger::ChangeTypeCreate:
457 case IncidenceChanger::ChangeTypeDelete:
460 Q_ASSERT( !change->originalItems.isEmpty() );
461 foreach(
const Akonadi::Item &item, change->originalItems ) {
462 Q_ASSERT( item.hasPayload<KCalCore::Incidence::Ptr>() );
463 Incidence::Ptr incidence = CalendarUtils::incidence( item );
464 if ( !incidence->supportsGroupwareCommunication() ) {
469 if ( Akonadi::CalendarUtils::thatIsMe( incidence->organizer()->email() ) ) {
470 status = handler.sendIncidenceDeletedMessage( KCalCore::iTIPCancel, incidence );
471 if ( change->atomicOperationId ) {
472 mInvitationStatusByAtomicOperation.insert( change->atomicOperationId, status );
474 result = status != ITIPHandlerHelper::ResultFailAbortUpdate;
480 case IncidenceChanger::ChangeTypeModify:
482 if ( change->originalItems.isEmpty() ) {
486 Q_ASSERT( change->originalItems.count() == 1 );
487 Incidence::Ptr oldIncidence = change->originalItems.first().payload<KCalCore::Incidence::Ptr>();
488 Incidence::Ptr newIncidence = change->newItem.payload<KCalCore::Incidence::Ptr>();
490 if ( !oldIncidence->supportsGroupwareCommunication() ) {
494 const bool modify = handler.handleIncidenceAboutToBeModified( newIncidence );
499 if ( newIncidence->type() == oldIncidence->type() ) {
500 IncidenceBase *i1 = newIncidence.data();
501 IncidenceBase *i2 = oldIncidence.data();
515 bool IncidenceChanger::Private::handleInvitationsAfterChange(
const Change::Ptr &change )
517 if ( change->useGroupwareCommunication ) {
519 switch( change->type ) {
520 case IncidenceChanger::ChangeTypeCreate:
522 Incidence::Ptr incidence = change->newItem.payload<KCalCore::Incidence::Ptr>();
523 if ( incidence->supportsGroupwareCommunication() ) {
525 handler.sendIncidenceCreatedMessage( KCalCore::iTIPRequest, incidence );
527 if ( status == ITIPHandlerHelper::ResultFailAbortUpdate ) {
528 kError() <<
"Sending invitations failed, but did not delete the incidence";
531 const uint atomicOperationId = change->atomicOperationId;
532 if ( atomicOperationId != 0 ) {
533 mInvitationStatusByAtomicOperation.insert( atomicOperationId, status );
538 case IncidenceChanger::ChangeTypeDelete:
540 Q_ASSERT( !change->originalItems.isEmpty() );
541 foreach(
const Akonadi::Item &item, change->originalItems ) {
542 Q_ASSERT( item.hasPayload() );
543 Incidence::Ptr incidence = item.payload<KCalCore::Incidence::Ptr>();
544 Q_ASSERT( incidence );
545 if ( !incidence->supportsGroupwareCommunication() )
548 if ( !Akonadi::CalendarUtils::thatIsMe( incidence->organizer()->email() ) ) {
549 const QStringList myEmails = Akonadi::CalendarUtils::allEmails();
550 bool notifyOrganizer =
false;
551 for ( QStringList::ConstIterator it = myEmails.begin(); it != myEmails.end(); ++it ) {
552 const QString email = *it;
553 KCalCore::Attendee::Ptr me( incidence->attendeeByMail( email ) );
555 if ( me->status() == KCalCore::Attendee::Accepted ||
556 me->status() == KCalCore::Attendee::Delegated ) {
557 notifyOrganizer =
true;
559 KCalCore::Attendee::Ptr newMe(
new KCalCore::Attendee( *me ) );
560 newMe->setStatus( KCalCore::Attendee::Declined );
561 incidence->clearAttendees();
562 incidence->addAttendee( newMe );
567 if ( notifyOrganizer ) {
568 MailScheduler scheduler;
569 scheduler.performTransaction( incidence, KCalCore::iTIPReply );
575 case IncidenceChanger::ChangeTypeModify:
577 if ( change->originalItems.isEmpty() ) {
581 Q_ASSERT( change->originalItems.count() == 1 );
582 Incidence::Ptr oldIncidence = CalendarUtils::incidence( change->originalItems.first() );
583 Incidence::Ptr newIncidence = CalendarUtils::incidence( change->newItem );
585 if ( !newIncidence->supportsGroupwareCommunication() ||
586 !Akonadi::CalendarUtils::thatIsMe( newIncidence->organizer()->email() ) ) {
591 if ( mInvitationStatusByAtomicOperation.contains( change->atomicOperationId ) ) {
592 handler.setDefaultAction( actionFromStatus( mInvitationStatusByAtomicOperation.value( change->atomicOperationId ) ) );
595 const bool attendeeStatusChanged = myAttendeeStatusChanged( newIncidence,
597 Akonadi::CalendarUtils::allEmails() );
601 attendeeStatusChanged );
603 if ( change->atomicOperationId != 0 ) {
604 mInvitationStatusByAtomicOperation.insert( change->atomicOperationId, status );
617 bool IncidenceChanger::Private::myAttendeeStatusChanged(
const Incidence::Ptr &newInc,
618 const Incidence::Ptr &oldInc,
619 const QStringList &myEmails )
623 const Attendee::Ptr oldMe = oldInc->attendeeByMails( myEmails );
624 const Attendee::Ptr newMe = newInc->attendeeByMails( myEmails );
626 return oldMe && newMe && oldMe->status() != newMe->status();
629 IncidenceChanger::IncidenceChanger( QObject *parent ) : QObject( parent )
630 , d( new Private( true, this ) )
634 IncidenceChanger::IncidenceChanger(
bool enableHistory, QObject *parent ) : QObject( parent )
635 , d( new Private( enableHistory, this ) )
639 IncidenceChanger::~IncidenceChanger()
644 int IncidenceChanger::createIncidence(
const Incidence::Ptr &incidence,
650 kWarning() <<
"An invalid payload is not allowed.";
651 d->cancelTransaction();
655 const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
657 const Change::Ptr change(
new CreationChange(
this, ++d->mLatestChangeId,
658 atomicOperationId, parent ) );
661 const int changeId = change->
id;
662 Q_ASSERT( !( d->mBatchOperationInProgress && !d->mAtomicOperations.contains( atomicOperationId ) ) );
663 if ( d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback() ) {
664 const QString errorMessage = d->showErrorDialog( ResultCodeRolledback, parent );
665 kWarning() << errorMessage;
667 change->resultCode = ResultCodeRolledback;
668 change->errorString = errorMessage;
669 d->cleanupTransaction();
673 d->handleInvitationsBeforeChange( change );
675 if ( collection.
isValid() && d->hasRights( collection, ChangeTypeCreate ) ) {
677 collectionToUse = collection;
679 switch( d->mDestinationPolicy ) {
680 case DestinationPolicyDefault:
681 if ( d->mDefaultCollection.isValid() &&
682 d->hasRights( d->mDefaultCollection, ChangeTypeCreate ) ) {
683 collectionToUse = d->mDefaultCollection;
686 kWarning() <<
"Destination policy is to use the default collection."
687 <<
"But it's invalid or doesn't have proper ACLs."
688 <<
"isValid = " << d->mDefaultCollection.
isValid()
689 <<
"has ACLs = " << d->hasRights( d->mDefaultCollection,
692 case DestinationPolicyAsk:
695 const QStringList mimeTypes( incidence->mimeType() );
696 collectionToUse = selectCollection( parent, dialogCode , mimeTypes,
697 d->mDefaultCollection );
698 if ( dialogCode != QDialog::Accepted ) {
699 kDebug() <<
"User canceled collection choosing";
700 change->resultCode = ResultCodeUserCanceled;
701 d->cancelTransaction();
705 if ( collectionToUse.isValid() && !d->hasRights( collectionToUse, ChangeTypeCreate ) ) {
706 kWarning() <<
"No ACLs for incidence creation";
707 const QString errorMessage = d->showErrorDialog( ResultCodePermissions, parent );
708 change->resultCode = ResultCodePermissions;
709 change->errorString = errorMessage;
710 d->cancelTransaction();
715 if ( !collectionToUse.isValid() ) {
716 kError() <<
"Invalid collection selected. Can't create incidence.";
717 change->resultCode = ResultCodeInvalidUserCollection;
718 const QString errorString = d->showErrorDialog( ResultCodeInvalidUserCollection, parent );
719 change->errorString = errorString;
720 d->cancelTransaction();
725 case DestinationPolicyNeverAsk:
727 const bool hasRights = d->hasRights( d->mDefaultCollection, ChangeTypeCreate );
728 if ( d->mDefaultCollection.isValid() && hasRights ) {
729 collectionToUse = d->mDefaultCollection;
731 const QString errorString = d->showErrorDialog( ResultCodeInvalidDefaultCollection, parent );
732 kError() << errorString <<
"; rights are " << hasRights;
733 change->resultCode = hasRights ? ResultCodeInvalidDefaultCollection :
734 ResultCodePermissions;
735 change->errorString = errorString;
736 d->cancelTransaction();
743 Q_ASSERT_X(
false,
"createIncidence()",
"unknown destination policy" );
744 d->cancelTransaction();
749 d->mLastCollectionUsed = collectionToUse;
752 item.setPayload<Incidence::Ptr>( incidence );
753 item.setMimeType( incidence->mimeType() );
756 d->mChangeForJob.insert( createJob, change );
758 if ( d->mBatchOperationInProgress ) {
759 AtomicOperation *atomic = d->mAtomicOperations[d->mLatestAtomicOperationId];
761 atomic->addChange( change );
765 connect( createJob, SIGNAL(result(KJob*)),
766 d, SLOT(handleCreateJobResult(KJob*)), Qt::QueuedConnection );
768 d->mChangeById.insert( changeId, change );
772 int IncidenceChanger::deleteIncidence(
const Item &item, QWidget *parent )
777 return deleteIncidences( list, parent );
780 int IncidenceChanger::deleteIncidences(
const Item::List &items, QWidget *parent )
783 if ( items.isEmpty() ) {
784 kError() <<
"Delete what?";
785 d->cancelTransaction();
789 foreach(
const Item &item, items ) {
790 if ( !item.isValid() ) {
791 kError() <<
"Items must be valid!";
792 d->cancelTransaction();
797 const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
798 const int changeId = ++d->mLatestChangeId;
799 const Change::Ptr change(
new DeletionChange(
this, changeId, atomicOperationId, parent ) );
801 foreach(
const Item &item, items ) {
802 if ( !d->hasRights( item.parentCollection(), ChangeTypeDelete ) ) {
803 kWarning() <<
"Item " << item.id() <<
" can't be deleted due to ACL restrictions";
804 const QString errorString = d->showErrorDialog( ResultCodePermissions, parent );
805 change->resultCode = ResultCodePermissions;
806 change->errorString = errorString;
807 d->cancelTransaction();
812 if ( !d->allowAtomicOperation( atomicOperationId, change ) ) {
813 const QString errorString = d->showErrorDialog( ResultCodeDuplicateId, parent );
814 change->resultCode = ResultCodeDuplicateId;
815 change->errorString = errorString;
816 kWarning() << errorString;
817 d->cancelTransaction();
821 Item::List itemsToDelete;
822 foreach(
const Item &item, items ) {
823 if ( d->deleteAlreadyCalled( item.id() ) ) {
825 kDebug() <<
"Item " << item.id() <<
" already deleted or being deleted, skipping";
827 itemsToDelete.append( item );
831 if ( d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback() ) {
832 const QString errorMessage = d->showErrorDialog( ResultCodeRolledback, parent );
833 change->resultCode = ResultCodeRolledback;
834 change->errorString = errorMessage;
835 kError() << errorMessage;
836 d->cleanupTransaction();
840 if ( itemsToDelete.isEmpty() ) {
841 QVector<Akonadi::Item::Id> itemIdList;
842 itemIdList.append( Item().
id() );
843 kDebug() <<
"Items already deleted or being deleted, skipping";
844 const QString errorMessage =
845 i18n(
"That calendar item was already deleted, or currently being deleted." );
847 change->resultCode = ResultCodeAlreadyDeleted;
848 change->errorString = errorMessage;
849 d->cancelTransaction();
850 kWarning() << errorMessage;
853 change->originalItems = itemsToDelete;
854 d->handleInvitationsBeforeChange( change );
857 d->mChangeForJob.insert( deleteJob, change );
858 d->mChangeById.insert( changeId, change );
860 if ( d->mBatchOperationInProgress ) {
861 AtomicOperation *atomic = d->mAtomicOperations[atomicOperationId];
863 atomic->addChange( change );
866 foreach(
const Item &item, itemsToDelete ) {
867 d->mDeletedItemIds << item.id();
871 if ( d->mDeletedItemIds.count() > 100 )
872 d->mDeletedItemIds.remove( 0, 50 );
875 connect( deleteJob, SIGNAL(result(KJob*)),
876 d, SLOT(handleDeleteJobResult(KJob*)), Qt::QueuedConnection );
881 int IncidenceChanger::modifyIncidence(
const Item &changedItem,
882 const KCalCore::Incidence::Ptr &originalPayload,
885 if ( !changedItem.isValid() || !changedItem.hasPayload<Incidence::Ptr>() ) {
886 kWarning() <<
"An invalid item or payload is not allowed.";
887 d->cancelTransaction();
891 if ( !d->hasRights( changedItem.parentCollection(), ChangeTypeModify ) ) {
892 kWarning() <<
"Item " << changedItem.id() <<
" can't be deleted due to ACL restrictions";
893 const int changeId = ++d->mLatestChangeId;
894 const QString errorString = d->showErrorDialog( ResultCodePermissions, parent );
895 emitModifyFinished(
this, changeId, changedItem, ResultCodePermissions, errorString );
896 d->cancelTransaction();
901 changedItem.payload<Incidence::Ptr>()->setLastModified( KDateTime::currentUtcDateTime() );
903 const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
904 const int changeId = ++d->mLatestChangeId;
905 ModificationChange *modificationChange =
new ModificationChange(
this, changeId,
906 atomicOperationId, parent );
907 Change::Ptr change( modificationChange );
909 if ( originalPayload ) {
910 Item originalItem( changedItem );
911 originalItem.setPayload<KCalCore::Incidence::Ptr>( originalPayload );
912 modificationChange->originalItems << originalItem;
915 modificationChange->newItem = changedItem;
916 d->mChangeById.insert( changeId, change );
918 if ( !d->allowAtomicOperation( atomicOperationId, change ) ) {
919 const QString errorString = d->showErrorDialog( ResultCodeDuplicateId, parent );
920 change->resultCode = ResultCodeDuplicateId;
921 change->errorString = errorString;
922 d->cancelTransaction();
923 kWarning() <<
"Atomic operation now allowed";
927 if ( d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback() ) {
928 const QString errorMessage = d->showErrorDialog( ResultCodeRolledback, parent );
929 kError() << errorMessage;
930 d->cleanupTransaction();
931 emitModifyFinished(
this, changeId, changedItem, ResultCodeRolledback, errorMessage );
933 d->performModification( change );
939 void IncidenceChanger::Private::performModification( Change::Ptr change )
941 const Item::Id
id = change->newItem.id();
942 Akonadi::Item &newItem = change->newItem;
943 Q_ASSERT( newItem.isValid() );
944 Q_ASSERT( newItem.hasPayload<Incidence::Ptr>() );
946 const int changeId = change->id;
948 if ( deleteAlreadyCalled(
id ) ) {
950 kDebug() <<
"Item " <<
id <<
" already deleted or being deleted, skipping";
953 emitModifyFinished( q, change->id, newItem, ResultCodeAlreadyDeleted,
954 i18n(
"That calendar item was already deleted, or currently being deleted." ) );
958 const uint atomicOperationId = change->atomicOperationId;
959 const bool hasAtomicOperationId = atomicOperationId != 0;
960 if ( hasAtomicOperationId &&
961 mAtomicOperations[atomicOperationId]->rolledback() ) {
962 const QString errorMessage = showErrorDialog( ResultCodeRolledback, 0 );
963 kError() << errorMessage;
964 emitModifyFinished( q, changeId, newItem, ResultCodeRolledback, errorMessage );
968 const bool userCancelled = !handleInvitationsBeforeChange( change );
969 if ( userCancelled ) {
971 kDebug() <<
"User cancelled, giving up";
972 emitModifyFinished( q, changeId, newItem, ResultCodeUserCanceled, QString() );
976 QHash<Akonadi::Item::Id, int> &latestRevisionByItemId =
977 ConflictPreventer::self()->mLatestRevisionByItemId;
978 if ( latestRevisionByItemId.contains(
id ) &&
979 latestRevisionByItemId[id] > newItem.revision() ) {
987 newItem.setRevision( latestRevisionByItemId[
id] );
990 Incidence::Ptr incidence = newItem.payload<Incidence::Ptr>();
992 const int revision = incidence->revision();
993 incidence->setRevision( revision + 1 );
998 newItem.setRemoteRevision( QString() );
1000 if ( mModificationsInProgress.contains( newItem.id() ) ) {
1003 queueModification( change );
1006 mChangeForJob.insert( modifyJob, change );
1007 mDirtyFieldsByJob.insert( modifyJob, incidence->dirtyFields() );
1009 if ( hasAtomicOperationId ) {
1010 AtomicOperation *atomic = mAtomicOperations[atomicOperationId];
1012 atomic->addChange( change );
1015 mModificationsInProgress[newItem.id()] = change;
1017 connect( modifyJob, SIGNAL(result(KJob*)),
1018 SLOT(handleModifyJobResult(KJob*)), Qt::QueuedConnection );
1022 void IncidenceChanger::startAtomicOperation(
const QString &operationDescription )
1024 if ( d->mBatchOperationInProgress ) {
1025 kDebug() <<
"An atomic operation is already in progress.";
1029 ++d->mLatestAtomicOperationId;
1030 d->mBatchOperationInProgress =
true;
1032 AtomicOperation *atomicOperation =
new AtomicOperation( d, d->mLatestAtomicOperationId );
1033 atomicOperation->m_description = operationDescription;
1034 d->mAtomicOperations.insert( d->mLatestAtomicOperationId, atomicOperation );
1037 void IncidenceChanger::endAtomicOperation()
1039 if ( !d->mBatchOperationInProgress ) {
1040 kDebug() <<
"No atomic operation is in progress.";
1044 Q_ASSERT_X( d->mLatestAtomicOperationId != 0,
1045 "IncidenceChanger::endAtomicOperation()",
1046 "Call startAtomicOperation() first." );
1048 Q_ASSERT( d->mAtomicOperations.contains(d->mLatestAtomicOperationId) );
1049 AtomicOperation *atomicOperation = d->mAtomicOperations[d->mLatestAtomicOperationId];
1050 Q_ASSERT( atomicOperation );
1051 atomicOperation->m_endCalled =
true;
1053 const bool allJobsCompleted = !atomicOperation->pendingJobs();
1055 if ( allJobsCompleted && atomicOperation->rolledback() &&
1056 atomicOperation->m_transactionCompleted ) {
1058 delete d->mAtomicOperations.take( d->mLatestAtomicOperationId );
1059 d->mBatchOperationInProgress =
false;
1066 void IncidenceChanger::setShowDialogsOnError(
bool enable )
1068 d->mShowDialogsOnError = enable;
1071 bool IncidenceChanger::showDialogsOnError()
const
1073 return d->mShowDialogsOnError;
1076 void IncidenceChanger::setRespectsCollectionRights(
bool respects )
1078 d->mRespectsCollectionRights = respects;
1081 bool IncidenceChanger::respectsCollectionRights()
const
1083 return d->mRespectsCollectionRights;
1086 void IncidenceChanger::setDestinationPolicy( IncidenceChanger::DestinationPolicy destinationPolicy )
1088 d->mDestinationPolicy = destinationPolicy;
1091 IncidenceChanger::DestinationPolicy IncidenceChanger::destinationPolicy()
const
1093 return d->mDestinationPolicy;
1098 d->mDefaultCollection = collection;
1101 Collection IncidenceChanger::defaultCollection()
const
1103 return d->mDefaultCollection;
1106 bool IncidenceChanger::historyEnabled()
const
1108 return d->mUseHistory;
1111 void IncidenceChanger::setHistoryEnabled(
bool enable )
1113 if ( d->mUseHistory != enable ) {
1114 d->mUseHistory = enable;
1115 if ( enable && !d->mHistory )
1116 d->mHistory =
new History(
this );
1120 History* IncidenceChanger::history()
const
1125 bool IncidenceChanger::deletedRecently( Akonadi::Item::Id
id )
const
1127 return d->deleteAlreadyCalled(
id );
1130 void IncidenceChanger::setGroupwareCommunication(
bool enabled )
1132 d->mGroupwareCommunication = enabled;
1135 bool IncidenceChanger::groupwareCommunication()
const
1137 return d->mGroupwareCommunication;
1142 return d->mLastCollectionUsed;
1145 QString IncidenceChanger::Private::showErrorDialog( IncidenceChanger::ResultCode resultCode,
1148 QString errorString;
1149 switch( resultCode ) {
1150 case IncidenceChanger::ResultCodePermissions:
1151 errorString = i18n(
"Operation can not be performed due to ACL restrictions" );
1153 case IncidenceChanger::ResultCodeInvalidUserCollection:
1154 errorString = i18n(
"The chosen collection is invalid" );
1156 case IncidenceChanger::ResultCodeInvalidDefaultCollection:
1157 errorString = i18n(
"Default collection is invalid or doesn't have proper ACLs"
1158 " and DestinationPolicyNeverAsk was used" );
1160 case IncidenceChanger::ResultCodeDuplicateId:
1161 errorString = i18n(
"Duplicate item id in a group operation");
1163 case IncidenceChanger::ResultCodeRolledback:
1164 errorString = i18n(
"One change belonging to a group of changes failed. "
1165 "All changes are being rolled back." );
1169 return QString( i18n(
"Unknown error" ) );
1172 if ( mShowDialogsOnError ) {
1173 KMessageBox::sorry( parent, errorString );
1179 void IncidenceChanger::Private::cancelTransaction()
1181 if ( mBatchOperationInProgress ) {
1182 mAtomicOperations[mLatestAtomicOperationId]->setRolledback();
1186 void IncidenceChanger::Private::cleanupTransaction()
1188 Q_ASSERT( mAtomicOperations.contains(mLatestAtomicOperationId) );
1189 AtomicOperation *operation = mAtomicOperations[mLatestAtomicOperationId];
1190 Q_ASSERT( operation );
1191 Q_ASSERT( operation->rolledback() );
1192 if ( !operation->pendingJobs() && operation->m_endCalled && operation->m_transactionCompleted ) {
1193 delete mAtomicOperations.take(mLatestAtomicOperationId);
1194 mBatchOperationInProgress =
false;
1198 bool IncidenceChanger::Private::allowAtomicOperation(
int atomicOperationId,
1199 const Change::Ptr &change )
const
1202 if ( atomicOperationId > 0 ) {
1203 Q_ASSERT( mAtomicOperations.contains( atomicOperationId ) );
1204 AtomicOperation *operation = mAtomicOperations.value( atomicOperationId );
1206 if ( change->type == ChangeTypeCreate ) {
1208 }
else if ( change->type == ChangeTypeModify ) {
1209 allow = !operation->m_itemIdsInOperation.contains( change->newItem.id() );
1210 }
else if ( change->type == ChangeTypeDelete ) {
1211 DeletionChange::Ptr deletion = change.staticCast<DeletionChange>();
1212 foreach( Akonadi::Item::Id
id, deletion->mItemIds ) {
1213 if ( operation->m_itemIdsInOperation.contains(
id ) ) {
1222 kWarning() <<
"Each change belonging to a group operation"
1223 <<
"must have a different Akonadi::Item::Id";
1230 void ModificationChange::emitCompletionSignal()
1232 emitModifyFinished( changer,
id, newItem, resultCode, errorString );
1236 void CreationChange::emitCompletionSignal()
1239 emitCreateFinished( changer,
id, newItem, resultCode, errorString );
1243 void DeletionChange::emitCompletionSignal()
1245 emitDeleteFinished( changer,
id, mItemIds, resultCode, errorString );
1280 AtomicOperation::AtomicOperation( IncidenceChanger::Private *icp,
1281 uint ident ) : m_id ( ident )
1282 , m_endCalled( false )
1283 , m_numCompletedChanges( 0 )
1284 , m_transactionCompleted( false )
1285 , m_wasRolledback( false )
1286 , m_transaction( 0 )
1287 , m_incidenceChangerPrivate( icp )
1290 Q_ASSERT( m_id != 0 );
1295 if ( !m_transaction ) {
1299 m_incidenceChangerPrivate->mAtomicOperationByTransaction.insert( m_transaction, m_id );
1301 QObject::connect( m_transaction, SIGNAL(result(KJob*)),
1302 m_incidenceChangerPrivate, SLOT(handleTransactionJobResult(KJob*)) );
1305 return m_transaction;
Item item() const
Returns the created item with the new unique id, or an invalid item if the job failed.
A collection selection dialog.
Represents a collection of PIM items.
Item item() const
Returns the modified and stored item including the changed revision number.
Base class for all actions in the Akonadi storage.
Can change items in this collection.
void setAutomaticCommittingEnabled(bool enable)
Disable automatic committing.
Job that deletes items from the Akonadi storage.
Can delete items in this collection.
History class for implementing undo/redo of calendar operations.
Can create new items in this collection.
The invitation was sent to all attendees.
Job that creates a new item in the Akonadi storage.
Id id() const
Returns the unique identifier of the entity.
Rights rights() const
Returns the rights the user has on the collection.
This class handles sending of invitations to attendees when Incidences (e.g.
Sending was canceled by the user, meaning there are local changes of which other attendees are not aw...
Base class for jobs that need to run a sequence of sub-jobs in a transaction.
Job that modifies an existing item in the Akonadi storage.
virtual QString errorString() const
Returns the error string, if there has been an error, an empty string otherwise.
bool isValid() const
Returns whether the entity is valid.
Item::List deletedItems() const
Returns the items passed on in the constructor.