60
67
namespace Akonadi {
61
static Akonadi::Collection selectCollection( QWidget *parent,
63
const QStringList &mimeTypes,
64
const Akonadi::Collection &defCollection )
66
QPointer<Akonadi::CollectionDialog> dlg( new Akonadi::CollectionDialog( parent ) );
68
kDebug() << "selecting collections with mimeType in " << mimeTypes;
70
dlg->changeCollectionDialogOptions( Akonadi::CollectionDialog::KeepTreeExpanded );
71
dlg->setMimeTypeFilter( mimeTypes );
72
dlg->setAccessRightsFilter( Akonadi::Collection::CanCreateItem );
73
if ( defCollection.isValid() ) {
74
dlg->setDefaultCollection( defCollection );
76
Akonadi::Collection collection;
78
// FIXME: don't use exec.
79
dialogCode = dlg->exec();
80
if ( dialogCode == QDialog::Accepted ) {
81
collection = dlg->selectedCollection();
83
if ( !collection.isValid() ) {
84
kWarning() <<"An invalid collection was selected!";
92
68
// Does a queued emit, with QMetaObject::invokeMethod
93
69
static void emitCreateFinished( IncidenceChanger *changer,
259
238
Q_ASSERT( mAtomicOperations.contains(atomicOperationId) );
260
239
AtomicOperation *operation = mAtomicOperations[atomicOperationId];
261
240
Q_ASSERT( operation );
262
Q_ASSERT( operation->id == atomicOperationId );
241
Q_ASSERT( operation->m_id == atomicOperationId );
263
242
if ( job->error() ) {
264
243
if ( !operation->rolledback() )
265
244
operation->setRolledback();
266
245
kError() << "Transaction failed, everything was rolledback. "
267
246
<< job->errorString();
269
Q_ASSERT( operation->endCalled );
248
Q_ASSERT( operation->m_endCalled );
270
249
Q_ASSERT( !operation->pendingJobs() );
273
if ( !operation->pendingJobs() && operation->endCalled ) {
252
if ( !operation->pendingJobs() && operation->m_endCalled ) {
274
253
delete mAtomicOperations.take( atomicOperationId );
275
254
mBatchOperationInProgress = false;
277
operation->transactionCompleted = true;
256
operation->m_transactionCompleted = true;
446
425
bool result = true;
447
426
if ( mGroupwareCommunication ) {
448
427
ITIPHandlerHelper handler( change->parentWidget ); // TODO make async
449
if ( mInvitationStatusByAtomicOperation.contains( change->atomicOperationId ) ) {
429
if ( m_invitationPolicy == InvitationPolicySend ) {
430
handler.setDefaultAction( ITIPHandlerHelper::ActionSendMessage );
431
} else if ( m_invitationPolicy == InvitationPolicyDontSend ) {
432
handler.setDefaultAction( ITIPHandlerHelper::ActionDontSendMessage );
433
} else if ( mInvitationStatusByAtomicOperation.contains( change->atomicOperationId ) ) {
450
434
handler.setDefaultAction( actionFromStatus( mInvitationStatusByAtomicOperation.value( change->atomicOperationId ) ) );
460
444
Q_ASSERT( !change->originalItems.isEmpty() );
461
445
foreach( const Akonadi::Item &item, change->originalItems ) {
462
446
Q_ASSERT( item.hasPayload<KCalCore::Incidence::Ptr>() );
463
Incidence::Ptr incidence = item.payload<KCalCore::Incidence::Ptr>();
464
if ( !incidence->supportsGroupwareCommunication() )
447
Incidence::Ptr incidence = CalendarUtils::incidence( item );
448
if ( !incidence->supportsGroupwareCommunication() ) {
466
status = handler.sendIncidenceDeletedMessage( KCalCore::iTIPCancel, incidence );
467
if ( change->atomicOperationId ) {
468
mInvitationStatusByAtomicOperation.insert( change->atomicOperationId, status );
470
result = status != ITIPHandlerHelper::ResultFailAbortUpdate;
471
//TODO: with some status we want to break immediately
451
// We only send CANCEL if we're the organizer.
452
// If we're not, then we send REPLY with PartStat=Declined in handleInvitationsAfterChange()
453
if ( Akonadi::CalendarUtils::thatIsMe( incidence->organizer()->email() ) ) {
454
status = handler.sendIncidenceDeletedMessage( KCalCore::iTIPCancel, incidence );
455
if ( change->atomicOperationId ) {
456
mInvitationStatusByAtomicOperation.insert( change->atomicOperationId, status );
458
result = status != ITIPHandlerHelper::ResultFailAbortUpdate;
459
//TODO: with some status we want to break immediately
475
464
case IncidenceChanger::ChangeTypeModify:
477
if ( !change->originalItems.isEmpty() ) {
478
Q_ASSERT( change->originalItems.count() == 1 );
479
Incidence::Ptr oldIncidence = change->originalItems.first().payload<KCalCore::Incidence::Ptr>();
480
Incidence::Ptr newIncidence = change->newItem.payload<KCalCore::Incidence::Ptr>();
482
if ( oldIncidence->supportsGroupwareCommunication() ) {
483
const bool modify = handler.handleIncidenceAboutToBeModified( newIncidence );
485
if ( newIncidence->type() == oldIncidence->type() ) {
486
IncidenceBase *i1 = newIncidence.data();
487
IncidenceBase *i2 = oldIncidence.data();
466
if ( change->originalItems.isEmpty() ) {
470
Q_ASSERT( change->originalItems.count() == 1 );
471
Incidence::Ptr oldIncidence = CalendarUtils::incidence( change->originalItems.first() );
472
Incidence::Ptr newIncidence = CalendarUtils::incidence( change->newItem );
474
if ( !oldIncidence->supportsGroupwareCommunication() ) {
478
const bool weAreOrganizer = Akonadi::CalendarUtils::thatIsMe( newIncidence->organizer()->email() );
479
if (RUNNING_UNIT_TESTS && !weAreOrganizer ) {
480
// This is a bit of a workaround when running tests. I don't want to show the
481
// "You're not organizer, do you want to modify event?" dialog in unit-tests, but want
482
// to emulate a "yes" and a "no" press.
483
if ( m_invitationPolicy == InvitationPolicySend ) {
485
} else if (m_invitationPolicy == InvitationPolicyDontSend) {
490
const bool modify = handler.handleIncidenceAboutToBeModified( newIncidence );
495
if ( newIncidence->type() == oldIncidence->type() ) {
496
IncidenceBase *i1 = newIncidence.data();
497
IncidenceBase *i2 = oldIncidence.data();
506
513
if ( change->useGroupwareCommunication ) {
507
514
ITIPHandlerHelper handler( change->parentWidget ); // TODO make async
516
const bool alwaysSend = m_invitationPolicy == InvitationPolicySend;
517
const bool neverSend = m_invitationPolicy == InvitationPolicyDontSend;
519
handler.setDefaultAction( ITIPHandlerHelper::ActionSendMessage );
523
handler.setDefaultAction( ITIPHandlerHelper::ActionDontSendMessage );
508
526
switch( change->type ) {
509
527
case IncidenceChanger::ChangeTypeCreate:
511
Incidence::Ptr incidence = change->newItem.payload<KCalCore::Incidence::Ptr>();
529
Incidence::Ptr incidence = CalendarUtils::incidence( change->newItem );
512
530
if ( incidence->supportsGroupwareCommunication() ) {
513
531
const ITIPHandlerHelper::SendResult status =
514
532
handler.sendIncidenceCreatedMessage( KCalCore::iTIPRequest, incidence );
564
582
case IncidenceChanger::ChangeTypeModify:
566
if ( !change->originalItems.isEmpty() ) {
567
Q_ASSERT( change->originalItems.count() == 1 );
568
Incidence::Ptr oldIncidence = change->originalItems.first().payload<KCalCore::Incidence::Ptr>();
569
Incidence::Ptr newIncidence = change->newItem.payload<KCalCore::Incidence::Ptr>();
570
if ( newIncidence->supportsGroupwareCommunication() ) {
571
if ( mInvitationStatusByAtomicOperation.contains( change->atomicOperationId ) ) {
572
handler.setDefaultAction( actionFromStatus( mInvitationStatusByAtomicOperation.value( change->atomicOperationId ) ) );
574
const bool attendeeStatusChanged = myAttendeeStatusChanged( newIncidence,
576
Akonadi::CalendarUtils::allEmails() );
577
ITIPHandlerHelper::SendResult status = handler.sendIncidenceModifiedMessage( KCalCore::iTIPRequest,
579
attendeeStatusChanged );
581
if ( change->atomicOperationId != 0 ) {
582
mInvitationStatusByAtomicOperation.insert( change->atomicOperationId, status );
584
if ( change->originalItems.isEmpty() ) {
588
Q_ASSERT( change->originalItems.count() == 1 );
589
Incidence::Ptr oldIncidence = CalendarUtils::incidence( change->originalItems.first() );
590
Incidence::Ptr newIncidence = CalendarUtils::incidence( change->newItem );
592
if ( !newIncidence->supportsGroupwareCommunication() ||
593
!Akonadi::CalendarUtils::thatIsMe( newIncidence->organizer()->email() ) ) {
594
// If we're not the organizer, the user already saw the "Do you really want to do this, incidence will become out of sync"
598
if ( !neverSend && !alwaysSend && mInvitationStatusByAtomicOperation.contains( change->atomicOperationId ) ) {
599
handler.setDefaultAction( actionFromStatus( mInvitationStatusByAtomicOperation.value( change->atomicOperationId ) ) );
602
const bool attendeeStatusChanged = myAttendeeStatusChanged( newIncidence,
604
Akonadi::CalendarUtils::allEmails() );
606
ITIPHandlerHelper::SendResult status = handler.sendIncidenceModifiedMessage( KCalCore::iTIPRequest,
608
attendeeStatusChanged );
610
if ( change->atomicOperationId != 0 ) {
611
mInvitationStatusByAtomicOperation.insert( change->atomicOperationId, status );
653
d->handleInvitationsBeforeChange( change );
655
if ( collection.isValid() && d->hasRights( collection, ChangeTypeCreate ) ) {
656
// The collection passed always has priority
657
collectionToUse = collection;
659
switch( d->mDestinationPolicy ) {
660
case DestinationPolicyDefault:
661
if ( d->mDefaultCollection.isValid() &&
662
d->hasRights( d->mDefaultCollection, ChangeTypeCreate ) ) {
663
collectionToUse = d->mDefaultCollection;
666
kWarning() << "Destination policy is to use the default collection."
667
<< "But it's invalid or doesn't have proper ACLs."
668
<< "isValid = " << d->mDefaultCollection.isValid()
669
<< "has ACLs = " << d->hasRights( d->mDefaultCollection,
671
// else fallthrough, and ask the user.
672
case DestinationPolicyAsk:
675
const QStringList mimeTypes( incidence->mimeType() );
676
collectionToUse = selectCollection( parent, dialogCode /*by-ref*/, mimeTypes,
677
d->mDefaultCollection );
678
if ( dialogCode != QDialog::Accepted ) {
679
kDebug() << "User canceled collection choosing";
680
change->resultCode = ResultCodeUserCanceled;
681
d->cancelTransaction();
685
if ( collectionToUse.isValid() && !d->hasRights( collectionToUse, ChangeTypeCreate ) ) {
686
kWarning() << "No ACLs for incidence creation";
687
const QString errorMessage = d->showErrorDialog( ResultCodePermissions, parent );
688
change->resultCode = ResultCodePermissions;
689
change->errorString = errorMessage;
690
d->cancelTransaction();
694
// TODO: add unit test for these two situations after reviewing API
695
if ( !collectionToUse.isValid() ) {
696
kError() << "Invalid collection selected. Can't create incidence.";
697
change->resultCode = ResultCodeInvalidUserCollection;
698
const QString errorString = d->showErrorDialog( ResultCodeInvalidUserCollection, parent );
699
change->errorString = errorString;
700
d->cancelTransaction();
705
case DestinationPolicyNeverAsk:
707
const bool hasRights = d->hasRights( d->mDefaultCollection, ChangeTypeCreate );
708
if ( d->mDefaultCollection.isValid() && hasRights ) {
709
collectionToUse = d->mDefaultCollection;
711
const QString errorString = d->showErrorDialog( ResultCodeInvalidDefaultCollection, parent );
712
kError() << errorString << "; rights are " << hasRights;
713
change->resultCode = hasRights ? ResultCodeInvalidDefaultCollection :
714
ResultCodePermissions;
715
change->errorString = errorString;
716
d->cancelTransaction();
723
Q_ASSERT_X( false, "createIncidence()", "unknown destination policy" );
724
d->cancelTransaction();
729
d->mLastCollectionUsed = collectionToUse;
732
item.setPayload<Incidence::Ptr>( incidence );
679
item.setPayload<KCalCore::Incidence::Ptr>( incidence );
733
680
item.setMimeType( incidence->mimeType() );
735
ItemCreateJob *createJob = new ItemCreateJob( item, collectionToUse, d->parentJob( change ) );
736
d->mChangeForJob.insert( createJob, change );
738
if ( d->mBatchOperationInProgress ) {
739
AtomicOperation *atomic = d->mAtomicOperations[d->mLatestAtomicOperationId];
741
atomic->addChange( change );
744
// QueuedConnection because of possible sync exec calls.
745
connect( createJob, SIGNAL(result(KJob*)),
746
d, SLOT(handleCreateJobResult(KJob*)), Qt::QueuedConnection );
748
d->mChangeById.insert( changeId, change );
682
change->newItem = item;
684
d->step1DetermineDestinationCollection( change, collection );
749
686
return change->id;
948
handleInvitationsBeforeChange( change );
886
const bool userCancelled = !handleInvitationsBeforeChange( change );
887
if ( userCancelled ) {
888
// User got a "You're not the organizer, do you really want to send" dialog, and said "no"
889
kDebug() << "User cancelled, giving up";
890
emitModifyFinished( q, changeId, newItem, ResultCodeUserCanceled, QString() );
950
894
QHash<Akonadi::Item::Id, int> &latestRevisionByItemId =
951
895
ConflictPreventer::self()->mLatestRevisionByItemId;
1003
947
++d->mLatestAtomicOperationId;
1004
948
d->mBatchOperationInProgress = true;
1006
AtomicOperation *atomicOperation = new AtomicOperation( d->mLatestAtomicOperationId );
1007
atomicOperation->description = operationDescription;
950
AtomicOperation *atomicOperation = new AtomicOperation( d, d->mLatestAtomicOperationId );
951
atomicOperation->m_description = operationDescription;
1008
952
d->mAtomicOperations.insert( d->mLatestAtomicOperationId, atomicOperation );
1009
d->mAtomicOperationByTransaction.insert( atomicOperation->transaction, d->mLatestAtomicOperationId );
1011
d->connect( atomicOperation->transaction, SIGNAL(result(KJob*)),
1012
d, SLOT(handleTransactionJobResult(KJob*)) );
1015
955
void IncidenceChanger::endAtomicOperation()
1026
966
Q_ASSERT( d->mAtomicOperations.contains(d->mLatestAtomicOperationId) );
1027
967
AtomicOperation *atomicOperation = d->mAtomicOperations[d->mLatestAtomicOperationId];
1028
968
Q_ASSERT( atomicOperation );
1029
atomicOperation->endCalled = true;
969
atomicOperation->m_endCalled = true;
1031
971
const bool allJobsCompleted = !atomicOperation->pendingJobs();
1033
973
if ( allJobsCompleted && atomicOperation->rolledback() &&
1034
atomicOperation->transactionCompleted ) {
974
atomicOperation->m_transactionCompleted ) {
1035
975
// The transaction job already completed, we can cleanup:
1036
976
delete d->mAtomicOperations.take( d->mLatestAtomicOperationId );
1037
977
d->mBatchOperationInProgress = false;
1115
1055
return d->mGroupwareCommunication;
1058
void IncidenceChanger::setAutoAdjustRecurrence( bool enable )
1060
d->mAutoAdjustRecurrence = enable;
1063
bool IncidenceChanger::autoAdjustRecurrence() const
1065
return d->mAutoAdjustRecurrence;
1068
void IncidenceChanger::setInvitationPolicy( IncidenceChanger::InvitationPolicy policy )
1070
d->m_invitationPolicy = policy;
1073
IncidenceChanger::InvitationPolicy IncidenceChanger::invitationPolicy() const
1075
return d->m_invitationPolicy;
1118
1078
Akonadi::Collection IncidenceChanger::lastCollectionUsed() const
1120
1080
return d->mLastCollectionUsed;
1154
1114
return errorString;
1117
void IncidenceChanger::Private::adjustRecurrence( const KCalCore::Incidence::Ptr &originalIncidence,
1118
const KCalCore::Incidence::Ptr &incidence )
1120
if ( !originalIncidence || !incidence->recurs() || incidence->hasRecurrenceId() || !mAutoAdjustRecurrence
1121
|| !incidence->dirtyFields().contains( KCalCore::Incidence::FieldDtStart ) ) {
1125
const QDate originalDate = originalIncidence->dtStart().date();
1126
const QDate newStartDate = incidence->dtStart().date();
1128
if ( !originalDate.isValid() || !newStartDate.isValid() || originalDate == newStartDate )
1131
KCalCore::Recurrence *recurrence = incidence->recurrence();
1132
switch( recurrence->recurrenceType() ) {
1133
case KCalCore::Recurrence::rWeekly: {
1134
QBitArray days = recurrence->days();
1135
const int oldIndex = originalDate.dayOfWeek()-1; // QDate returns [1-7];
1136
const int newIndex = newStartDate.dayOfWeek()-1;
1137
if ( oldIndex != newIndex ) {
1138
days.clearBit( oldIndex );
1139
days.setBit( newIndex );
1140
recurrence->setWeekly( recurrence->frequency(), days );
1144
break; // Other types not implemented
1147
// Now fix cases where dtstart would be bigger than the recurrence end rendering it impossible for a view to show it:
1148
// To retrieve the recurrence end don't trust Recurrence::endDt() since it returns dtStart if the rrule's end is < than dtstart,
1149
// it seems someone made Recurrence::endDt() more robust, but getNextOccurrences() still craps out. So lets fix it here
1150
// there's no reason to write bogus ical to disk.
1151
const QDate recurrenceEndDate = recurrence->defaultRRule() ? recurrence->defaultRRule()->endDt().date() : QDate();
1152
if ( recurrenceEndDate.isValid() && recurrenceEndDate < newStartDate ) {
1153
recurrence->setEndDate( newStartDate );
1157
1157
void IncidenceChanger::Private::cancelTransaction()
1159
1159
if ( mBatchOperationInProgress ) {
1184
1184
if ( change->type == ChangeTypeCreate ) {
1186
1186
} else if ( change->type == ChangeTypeModify ) {
1187
allow = !operation->mItemIdsInOperation.contains( change->newItem.id() );
1187
allow = !operation->m_itemIdsInOperation.contains( change->newItem.id() );
1188
1188
} else if ( change->type == ChangeTypeDelete ) {
1189
1189
DeletionChange::Ptr deletion = change.staticCast<DeletionChange>();
1190
1190
foreach( Akonadi::Item::Id id, deletion->mItemIds ) {
1191
if ( operation->mItemIdsInOperation.contains( id ) ) {
1191
if ( operation->m_itemIdsInOperation.contains( id ) ) {
1258
AtomicOperation::AtomicOperation( IncidenceChanger::Private *icp,
1259
uint ident ) : m_id ( ident )
1260
, m_endCalled( false )
1261
, m_numCompletedChanges( 0 )
1262
, m_transactionCompleted( false )
1263
, m_wasRolledback( false )
1264
, m_transaction( 0 )
1265
, m_incidenceChangerPrivate( icp )
1268
Q_ASSERT( m_id != 0 );
1271
Akonadi::TransactionSequence *AtomicOperation::transaction()
1273
if ( !m_transaction ) {
1274
m_transaction = new Akonadi::TransactionSequence;
1275
m_transaction->setAutomaticCommittingEnabled( true );
1277
m_incidenceChangerPrivate->mAtomicOperationByTransaction.insert( m_transaction, m_id );
1279
QObject::connect( m_transaction, SIGNAL(result(KJob*)),
1280
m_incidenceChangerPrivate, SLOT(handleTransactionJobResult(KJob*)) );
1283
return m_transaction;