New Appointment -> New Task
[kdepim.git] / calendarsupport / incidencechanger.cpp
blob4e937eb629efa93c11030f60c77aa98d4a77b256
1 /*
2 Copyright (C) 2004 Reinhold Kainhofer <reinhold@kainhofer.com>
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License along
15 with this program; if not, write to the Free Software Foundation, Inc.,
16 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 As a special exception, permission is given to link this program
19 with any edition of Qt, and distribute the resulting executable,
20 without including the source code for Qt in the source distribution.
22 #include "incidencechanger.h"
23 #include "incidencechanger_p.h"
24 #include "calendar.h"
25 #include "calendaradaptor.h"
26 #include "dndfactory.h"
27 #include "groupware.h"
28 #include "kcalprefs.h"
29 #include "mailscheduler.h"
30 #include "utils.h"
32 #include <Akonadi/ItemCreateJob>
33 #include <Akonadi/ItemDeleteJob>
34 #include <Akonadi/ItemModifyJob>
36 #include <KMessageBox>
38 using namespace CalendarSupport;
40 bool IncidenceChanger::Private::myAttendeeStatusChanged( const KCalCore::Incidence::Ptr &newInc,
41 const KCalCore::Incidence::Ptr &oldInc )
43 KCalCore::Attendee::Ptr oldMe = oldInc->attendeeByMails( KCalPrefs::instance()->allEmails() );
44 KCalCore::Attendee::Ptr newMe = newInc->attendeeByMails( KCalPrefs::instance()->allEmails() );
45 if ( oldMe && newMe && ( oldMe->status() != newMe->status() ) ) {
46 return true;
49 return false;
52 void IncidenceChanger::Private::queueChange( Change *change )
54 // If there's already a change queued we just discard it
55 // and send the newer change, which already includes
56 // previous modifications
57 const Akonadi::Item::Id id = change->newItem.id();
58 if ( mQueuedChanges.contains( id ) ) {
59 delete mQueuedChanges.take( id );
62 mQueuedChanges[id] = change;
65 void IncidenceChanger::Private::cancelChanges( Akonadi::Item::Id id )
67 delete mQueuedChanges.take( id );
68 delete mCurrentChanges.take( id );
71 void IncidenceChanger::Private::performNextChange( Akonadi::Item::Id id )
73 delete mCurrentChanges.take( id );
75 if ( mQueuedChanges.contains( id ) ) {
76 performChange( mQueuedChanges.take( id ) );
80 bool IncidenceChanger::Private::performChange( Change *change )
82 Akonadi::Item newItem = change->newItem;
83 const KCalCore::Incidence::Ptr oldinc = change->oldInc;
84 const KCalCore::Incidence::Ptr newinc = CalendarSupport::incidence( newItem );
86 kDebug() << "id=" << newItem.id()
87 << "uid=" << newinc->uid()
88 << "version=" << newItem.revision()
89 << "summary=" << newinc->summary()
90 << "old summary" << oldinc->summary()
91 << "type=" << int( newinc->type() )
92 << "storageCollectionId=" << newItem.storageCollectionId();
94 // There's not any job modifying this item, so mCurrentChanges[item.id] can't exist
95 Q_ASSERT( !mCurrentChanges.contains( newItem.id() ) );
97 // Check if the item was deleted, we already check in changeIncidence() but
98 // this change could be already in the queue when the item was deleted
99 if ( !mCalendar->incidence( newItem.id() ).isValid() ||
100 mDeletedItemIds.contains( newItem.id() ) ) {
101 kDebug() << "Incidence deleted";
102 // return true, the user doesn't want to see errors because he was too fast
103 return true;
106 if ( newinc.data() == oldinc.data() ) {
107 // Don't do anything
108 kDebug() << "Incidence not changed";
109 return true;
110 } else {
112 if ( mLatestRevisionByItemId.contains( newItem.id() ) &&
113 mLatestRevisionByItemId[newItem.id()] > newItem.revision() ) {
114 /* When a ItemModifyJob ends, the application can still modify the old items if the user
115 * is quick because the ETM wasn't updated yet, and we'll get a STORE error, because
116 * we are not modifying the latest revision.
118 * When a job ends, we keep the new revision in m_latestVersionByItemId
119 * so we can update the item's revision
121 newItem.setRevision( mLatestRevisionByItemId[newItem.id()] );
124 kDebug() << "Changing incidence";
125 const bool attendeeStatusChanged = myAttendeeStatusChanged( oldinc, newinc );
126 const int revision = newinc->revision();
127 newinc->setRevision( revision + 1 );
128 // FIXME: Use a generic method for this! Ideally, have an interface class
129 // for group cheduling. Each implementation could then just do what
130 // it wants with the event. If no groupware is used,use the null
131 // pattern...
132 bool success = true;
133 if ( KCalPrefs::instance()->mUseGroupwareCommunication ) {
134 if ( !mGroupware ) {
135 kError() << "Groupware communication enabled but no groupware instance set";
136 } else {
137 success = mGroupware->sendICalMessage( change->parent,
138 KCalCore::iTIPRequest,
139 newinc,
140 INCIDENCEEDITED,
141 attendeeStatusChanged );
145 if ( !success ) {
146 kDebug() << "Changing incidence failed. Reverting changes.";
147 if ( newinc->type() == oldinc->type() ) {
148 KCalCore::IncidenceBase *i1 = newinc.data();
149 KCalCore::IncidenceBase *i2 = oldinc.data();
151 *i1 = *i2;
153 return false;
157 // FIXME: if that's a groupware incidence, and I'm not the organizer,
158 // send out a mail to the organizer with a counterproposal instead
159 // of actually changing the incidence. Then no locking is needed.
160 // FIXME: if that's a groupware incidence, and the incidence was
161 // never locked, we can't unlock it with endChange().
163 mCurrentChanges[newItem.id()] = change;
165 // Don't write back remote revision since we can't make sure it is the current one
166 // fixes problems with DAV resource
167 newItem.setRemoteRevision( QString() );
169 Akonadi::ItemModifyJob *job = new Akonadi::ItemModifyJob( newItem );
170 connect( job, SIGNAL(result( KJob *)), this, SLOT(changeIncidenceFinished(KJob *)) );
171 return true;
174 void IncidenceChanger::Private::changeIncidenceFinished( KJob *j )
176 // we should probably update the revision number here,or internally in the Event
177 // itself when certain things change. need to verify with ical documentation.
178 const Akonadi::ItemModifyJob* job = qobject_cast<const Akonadi::ItemModifyJob*>( j );
179 Q_ASSERT( job );
181 const Akonadi::Item newItem = job->item();
183 if ( !mCurrentChanges.contains( newItem.id() ) ) {
184 kDebug() << "Item was deleted? Great.";
185 cancelChanges( newItem.id() );
186 emit incidenceChangeFinished( Akonadi::Item(), newItem, UNKNOWN_MODIFIED, true );
187 return;
190 const Private::Change *change = mCurrentChanges[newItem.id()];
191 const KCalCore::Incidence::Ptr oldInc = change->oldInc;
193 Akonadi::Item oldItem;
194 oldItem.setPayload<KCalCore::Incidence::Ptr>( oldInc );
195 oldItem.setMimeType( oldInc->mimeType() );
196 oldItem.setId( newItem.id() );
198 if ( job->error() ) {
199 kWarning() << "Item modify failed:" << job->errorString();
201 const KCalCore::Incidence::Ptr newInc = CalendarSupport::incidence( newItem );
202 KMessageBox::sorry( change->parent,
203 i18n( "Unable to save changes for incidence %1 \"%2\": %3",
204 i18n( newInc->typeStr() ),
205 newInc->summary(),
206 job->errorString() ) );
207 emit incidenceChangeFinished( oldItem, newItem, change->action, false );
208 } else {
209 emit incidenceChangeFinished( oldItem, newItem, change->action, true );
212 mLatestRevisionByItemId[newItem.id()] = newItem.revision();
214 // execute any other modification if it exists
215 qRegisterMetaType<Akonadi::Item::Id>( "Akonadi::Item::Id" );
216 QMetaObject::invokeMethod( this, "performNextChange",
217 Qt::QueuedConnection,
218 Q_ARG( Akonadi::Item::Id, newItem.id() ) );
221 IncidenceChanger::IncidenceChanger( CalendarSupport::Calendar *cal,
222 QObject *parent,
223 Akonadi::Entity::Id defaultCollectionId )
224 : QObject( parent ), d( new Private( defaultCollectionId, cal ) )
226 connect( d, SIGNAL(incidenceChangeFinished(Akonadi::Item,Akonadi::Item,CalendarSupport::IncidenceChanger::WhatChanged,bool)),
227 SIGNAL(incidenceChangeFinished(Akonadi::Item,Akonadi::Item,CalendarSupport::IncidenceChanger::WhatChanged,bool)) );
230 IncidenceChanger::~IncidenceChanger()
232 delete d;
235 void IncidenceChanger::setGroupware( Groupware *groupware )
237 d->mGroupware = groupware;
240 bool IncidenceChanger::sendGroupwareMessage( const Akonadi::Item &aitem,
241 KCalCore::iTIPMethod method,
242 HowChanged action,
243 QWidget *parent )
245 const KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence( aitem );
246 if ( !incidence ) {
247 return false;
249 if ( KCalPrefs::instance()->thatIsMe( incidence->organizer()->email() ) &&
250 incidence->attendeeCount() > 0 &&
251 !KCalPrefs::instance()->mUseGroupwareCommunication ) {
252 emit schedule( method, aitem );
253 return true;
254 } else if ( KCalPrefs::instance()->mUseGroupwareCommunication ) {
255 if ( !d->mGroupware ) {
256 kError() << "Groupware communication enabled but no groupware instance set";
257 return false;
259 return d->mGroupware->sendICalMessage( parent, method, incidence, action, false );
261 return true;
264 void IncidenceChanger::cancelAttendees( const Akonadi::Item &aitem )
266 const KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence( aitem );
267 Q_ASSERT( incidence );
268 if ( KCalPrefs::instance()->mUseGroupwareCommunication ) {
269 if ( KMessageBox::questionYesNo(
271 i18n( "Some attendees were removed from the incidence. "
272 "Shall cancel messages be sent to these attendees?" ),
273 i18n( "Attendees Removed" ), KGuiItem( i18n( "Send Messages" ) ),
274 KGuiItem( i18n( "Do Not Send" ) ) ) == KMessageBox::Yes ) {
275 // don't use Akonadi::Groupware::sendICalMessage here, because that asks just
276 // a very general question "Other people are involved, send message to
277 // them?", which isn't helpful at all in this situation. Afterwards, it
278 // would only call the Akonadi::MailScheduler::performTransaction, so do this
279 // manually.
280 // FIXME: Groupware scheduling should be factored out to it's own class
281 // anyway
282 CalendarSupport::MailScheduler scheduler(
283 static_cast<CalendarSupport::Calendar*>(d->mCalendar) );
284 scheduler.performTransaction( incidence, KCalCore::iTIPCancel );
289 bool IncidenceChanger::deleteIncidence( const Akonadi::Item &aitem, QWidget *parent )
291 const KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence( aitem );
292 if ( !incidence ) {
293 return false;
296 if ( !isNotDeleted( aitem.id() ) ) {
297 kDebug() << "Item already deleted, skipping and returning true";
298 return true;
301 if ( !( d->mCalendar->hasDeleteRights( aitem ) ) ) {
302 kWarning() << "insufficient rights to delete incidence";
303 return false;
306 bool doDelete = sendGroupwareMessage( aitem, KCalCore::iTIPCancel,
307 INCIDENCEDELETED, parent );
308 if( !doDelete ) {
309 return false;
312 d->mDeletedItemIds.append( aitem.id() );
314 emit incidenceToBeDeleted( aitem );
315 d->cancelChanges( aitem.id() ); //abort changes to this incidence cause we will just delete it
316 Akonadi::ItemDeleteJob *job = new Akonadi::ItemDeleteJob( aitem );
317 connect( job, SIGNAL(result(KJob *)), this, SLOT(deleteIncidenceFinished(KJob *)) );
318 return true;
321 void IncidenceChanger::deleteIncidenceFinished( KJob *j )
323 // todo, cancel changes?
324 kDebug();
325 const Akonadi::ItemDeleteJob *job = qobject_cast<const Akonadi::ItemDeleteJob*>( j );
326 Q_ASSERT( job );
327 const Akonadi::Item::List items = job->deletedItems();
328 Q_ASSERT( items.count() == 1 );
329 KCalCore::Incidence::Ptr tmp = CalendarSupport::incidence( items.first() );
330 Q_ASSERT( tmp );
331 if ( job->error() ) {
332 KMessageBox::sorry( 0, //PENDING(AKONADI_PORT) set parent
333 i18n( "Unable to delete incidence %1 \"%2\": %3",
334 i18n( tmp->typeStr() ),
335 tmp->summary(),
336 job->errorString() ) );
337 d->mDeletedItemIds.removeOne( items.first().id() );
338 emit incidenceDeleteFinished( items.first(), false );
339 return;
341 if ( !KCalPrefs::instance()->thatIsMe( tmp->organizer()->email() ) ) {
342 const QStringList myEmails = KCalPrefs::instance()->allEmails();
343 bool notifyOrganizer = false;
344 for ( QStringList::ConstIterator it = myEmails.begin(); it != myEmails.end(); ++it ) {
345 QString email = *it;
346 KCalCore::Attendee::Ptr me( tmp->attendeeByMail( email ) );
347 if ( me ) {
348 if ( me->status() == KCalCore::Attendee::Accepted ||
349 me->status() == KCalCore::Attendee::Delegated ) {
350 notifyOrganizer = true;
352 KCalCore::Attendee::Ptr newMe( new KCalCore::Attendee( *me ) );
353 newMe->setStatus( KCalCore::Attendee::Declined );
354 tmp->clearAttendees();
355 tmp->addAttendee( newMe );
356 break;
360 if ( d->mGroupware ) {
361 if ( !d->mGroupware->doNotNotify() && notifyOrganizer ) {
362 CalendarSupport::MailScheduler scheduler(
363 static_cast<CalendarSupport::Calendar*>(d->mCalendar) );
364 scheduler.performTransaction( tmp, KCalCore::iTIPReply );
366 //reset the doNotNotify flag
367 d->mGroupware->setDoNotNotify( false );
370 d->mLatestRevisionByItemId.remove( items.first().id() );
371 emit incidenceDeleteFinished( items.first(), true );
374 bool IncidenceChanger::cutIncidences( const Akonadi::Item::List &list, QWidget *parent )
376 Akonadi::Item::List::ConstIterator it;
377 bool doDelete = true;
378 Akonadi::Item::List itemsToCut;
379 for ( it = list.constBegin(); it != list.constEnd(); ++it ) {
380 if ( CalendarSupport::hasIncidence( ( *it ) ) ) {
381 doDelete = sendGroupwareMessage( *it, KCalCore::iTIPCancel,
382 INCIDENCEDELETED, parent );
383 if ( doDelete ) {
384 emit incidenceToBeDeleted( *it );
385 itemsToCut.append( *it );
390 #ifndef QT_NO_DRAGANDDROP
391 CalendarAdaptor::Ptr cal( new CalendarAdaptor( d->mCalendar, parent ) );
392 CalendarSupport::DndFactory factory( cal, true/*delete calendarAdaptor*/ );
394 if ( factory.cutIncidences( itemsToCut ) ) {
395 #endif
396 for ( it = itemsToCut.constBegin(); it != itemsToCut.constEnd(); ++it ) {
397 emit incidenceDeleteFinished( *it, true );
399 return !itemsToCut.isEmpty();
400 #ifndef QT_NO_DRAGANDDROP
401 } else {
402 return false;
404 #endif
407 bool IncidenceChanger::cutIncidence( const Akonadi::Item &item, QWidget *parent )
409 Akonadi::Item::List items;
410 items.append( item );
411 return cutIncidences( items, parent );
414 void IncidenceChanger::setDefaultCollectionId( Akonadi::Entity::Id defaultCollectionId )
416 d->mDefaultCollectionId = defaultCollectionId;
419 bool IncidenceChanger::changeIncidence( const KCalCore::Incidence::Ptr &oldinc,
420 const Akonadi::Item &newItem,
421 WhatChanged action,
422 QWidget *parent )
424 if ( !CalendarSupport::hasIncidence( newItem ) || !newItem.isValid() ) {
425 kDebug() << "Skipping invalid item id=" << newItem.id();
426 return false;
429 if ( !d->mCalendar->hasChangeRights( newItem ) ) {
430 kWarning() << "insufficient rights to change incidence";
431 return false;
434 if ( !isNotDeleted( newItem.id() ) ) {
435 kDebug() << "Skipping change, the item got deleted";
436 return false;
439 Private::Change *change = new Private::Change();
440 change->action = action;
441 change->newItem = newItem;
442 change->oldInc = oldinc;
443 change->parent = parent;
445 if ( d->mCurrentChanges.contains( newItem.id() ) ) {
446 d->queueChange( change );
447 } else {
448 d->performChange( change );
450 return true;
453 bool IncidenceChanger::addIncidence( const KCalCore::Incidence::Ptr &incidence,
454 QWidget *parent, Akonadi::Collection &selectedCollection,
455 int &dialogCode )
457 const Akonadi::Collection defaultCollection = d->mCalendar->collection( d->mDefaultCollectionId );
459 const QString incidenceMimeType = incidence->mimeType();
460 const bool defaultIsOk = defaultCollection.contentMimeTypes().contains( incidenceMimeType ) &&
461 defaultCollection.rights() & Akonadi::Collection::CanCreateItem;
463 if ( d->mDestinationPolicy == ASK_DESTINATION ||
464 !defaultCollection.isValid() ||
465 !defaultIsOk ) {
466 QStringList mimeTypes( incidenceMimeType );
467 selectedCollection = CalendarSupport::selectCollection( parent,
468 dialogCode,
469 mimeTypes,
470 defaultCollection );
471 } else {
472 dialogCode = QDialog::Accepted;
473 selectedCollection = defaultCollection;
476 if ( selectedCollection.isValid() ) {
477 return addIncidence( incidence, selectedCollection, parent );
478 } else {
479 return false;
483 bool IncidenceChanger::addIncidence( const KCalCore::Incidence::Ptr &incidence,
484 const Akonadi::Collection &collection, QWidget *parent )
486 Q_UNUSED( parent );
488 if ( !incidence || !collection.isValid() ) {
489 return false;
492 if ( !( collection.rights() & Akonadi::Collection::CanCreateItem ) ) {
493 kWarning() << "insufficient rights to create incidence";
494 return false;
497 Akonadi::Item item;
498 item.setPayload<KCalCore::Incidence::Ptr>( incidence );
500 item.setMimeType( incidence->mimeType() );
501 Akonadi::ItemCreateJob *job = new Akonadi::ItemCreateJob( item, collection );
503 // The connection needs to be queued to be sure addIncidenceFinished
504 // is called after the kjob finished it's eventloop. That's needed
505 // because Akonadi::Groupware uses synchronous job->exec() calls.
506 connect( job, SIGNAL(result(KJob*)),
507 this, SLOT(addIncidenceFinished(KJob*)), Qt::QueuedConnection );
508 return true;
511 void IncidenceChanger::addIncidenceFinished( KJob *j )
513 kDebug();
514 const Akonadi::ItemCreateJob *job = qobject_cast<const Akonadi::ItemCreateJob*>( j );
515 Q_ASSERT( job );
516 KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence( job->item() );
518 if ( job->error() ) {
519 KMessageBox::sorry(
520 0, //PENDING(AKONADI_PORT) set parent, ideally the one passed in addIncidence...
521 i18n( "Unable to save %1 \"%2\": %3",
522 i18n( incidence->typeStr() ),
523 incidence->summary(),
524 job->errorString() ) );
525 emit incidenceAddFinished( job->item(), false );
526 return;
529 Q_ASSERT( incidence );
530 if ( KCalPrefs::instance()->mUseGroupwareCommunication ) {
531 if ( !d->mGroupware ) {
532 kError() << "Groupware communication enabled but no groupware instance set";
533 } else if ( !d->mGroupware->sendICalMessage(
534 0, //PENDING(AKONADI_PORT) set parent, ideally the one passed in addIncidence...
535 KCalCore::iTIPRequest,
536 incidence, INCIDENCEADDED, false ) ) {
537 kError() << "sendIcalMessage failed.";
541 emit incidenceAddFinished( job->item(), true );
544 void IncidenceChanger::setDestinationPolicy( DestinationPolicy destinationPolicy )
546 d->mDestinationPolicy = destinationPolicy;
549 IncidenceChanger::DestinationPolicy IncidenceChanger::destinationPolicy() const
551 return d->mDestinationPolicy;
554 bool IncidenceChanger::isNotDeleted( Akonadi::Item::Id id ) const
556 if ( d->mCalendar->incidence( id ).isValid() ) {
557 // it's inside the calendar, but maybe it's being deleted by a job or was
558 // deleted but the ETM doesn't know yet
559 return !d->mDeletedItemIds.contains( id );
560 } else {
561 // not inside the calendar, i don't know it
562 return false;
566 void IncidenceChanger::setCalendar( CalendarSupport::Calendar *calendar )
568 d->mCalendar = calendar;