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"
25 #include "calendaradaptor.h"
26 #include "dndfactory.h"
27 #include "groupware.h"
28 #include "kcalprefs.h"
29 #include "mailscheduler.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() ) ) {
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
106 if ( newinc
.data() == oldinc
.data() ) {
108 kDebug() << "Incidence not changed";
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
133 if ( KCalPrefs::instance()->mUseGroupwareCommunication
) {
135 kError() << "Groupware communication enabled but no groupware instance set";
137 success
= mGroupware
->sendICalMessage( change
->parent
,
138 KCalCore::iTIPRequest
,
141 attendeeStatusChanged
);
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();
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
*)) );
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
);
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 );
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() ),
206 job
->errorString() ) );
207 emit
incidenceChangeFinished( oldItem
, newItem
, change
->action
, false );
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
,
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()
235 void IncidenceChanger::setGroupware( Groupware
*groupware
)
237 d
->mGroupware
= groupware
;
240 bool IncidenceChanger::sendGroupwareMessage( const Akonadi::Item
&aitem
,
241 KCalCore::iTIPMethod method
,
245 const KCalCore::Incidence::Ptr incidence
= CalendarSupport::incidence( aitem
);
249 if ( KCalPrefs::instance()->thatIsMe( incidence
->organizer()->email() ) &&
250 incidence
->attendeeCount() > 0 &&
251 !KCalPrefs::instance()->mUseGroupwareCommunication
) {
252 emit
schedule( method
, aitem
);
254 } else if ( KCalPrefs::instance()->mUseGroupwareCommunication
) {
255 if ( !d
->mGroupware
) {
256 kError() << "Groupware communication enabled but no groupware instance set";
259 return d
->mGroupware
->sendICalMessage( parent
, method
, incidence
, action
, false );
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
280 // FIXME: Groupware scheduling should be factored out to it's own class
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
);
296 if ( !isNotDeleted( aitem
.id() ) ) {
297 kDebug() << "Item already deleted, skipping and returning true";
301 if ( !( d
->mCalendar
->hasDeleteRights( aitem
) ) ) {
302 kWarning() << "insufficient rights to delete incidence";
306 bool doDelete
= sendGroupwareMessage( aitem
, KCalCore::iTIPCancel
,
307 INCIDENCEDELETED
, parent
);
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
*)) );
321 void IncidenceChanger::deleteIncidenceFinished( KJob
*j
)
323 // todo, cancel changes?
325 const Akonadi::ItemDeleteJob
*job
= qobject_cast
<const Akonadi::ItemDeleteJob
*>( j
);
327 const Akonadi::Item::List items
= job
->deletedItems();
328 Q_ASSERT( items
.count() == 1 );
329 KCalCore::Incidence::Ptr tmp
= CalendarSupport::incidence( items
.first() );
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() ),
336 job
->errorString() ) );
337 d
->mDeletedItemIds
.removeOne( items
.first().id() );
338 emit
incidenceDeleteFinished( items
.first(), false );
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
) {
346 KCalCore::Attendee::Ptr
me( tmp
->attendeeByMail( email
) );
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
);
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
);
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
) ) {
396 for ( it
= itemsToCut
.constBegin(); it
!= itemsToCut
.constEnd(); ++it
) {
397 emit
incidenceDeleteFinished( *it
, true );
399 return !itemsToCut
.isEmpty();
400 #ifndef QT_NO_DRAGANDDROP
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
,
424 if ( !CalendarSupport::hasIncidence( newItem
) || !newItem
.isValid() ) {
425 kDebug() << "Skipping invalid item id=" << newItem
.id();
429 if ( !d
->mCalendar
->hasChangeRights( newItem
) ) {
430 kWarning() << "insufficient rights to change incidence";
434 if ( !isNotDeleted( newItem
.id() ) ) {
435 kDebug() << "Skipping change, the item got deleted";
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
);
448 d
->performChange( change
);
453 bool IncidenceChanger::addIncidence( const KCalCore::Incidence::Ptr
&incidence
,
454 QWidget
*parent
, Akonadi::Collection
&selectedCollection
,
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() ||
466 QStringList
mimeTypes( incidenceMimeType
);
467 selectedCollection
= CalendarSupport::selectCollection( parent
,
472 dialogCode
= QDialog::Accepted
;
473 selectedCollection
= defaultCollection
;
476 if ( selectedCollection
.isValid() ) {
477 return addIncidence( incidence
, selectedCollection
, parent
);
483 bool IncidenceChanger::addIncidence( const KCalCore::Incidence::Ptr
&incidence
,
484 const Akonadi::Collection
&collection
, QWidget
*parent
)
488 if ( !incidence
|| !collection
.isValid() ) {
492 if ( !( collection
.rights() & Akonadi::Collection::CanCreateItem
) ) {
493 kWarning() << "insufficient rights to create incidence";
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
);
511 void IncidenceChanger::addIncidenceFinished( KJob
*j
)
514 const Akonadi::ItemCreateJob
*job
= qobject_cast
<const Akonadi::ItemCreateJob
*>( j
);
516 KCalCore::Incidence::Ptr incidence
= CalendarSupport::incidence( job
->item() );
518 if ( job
->error() ) {
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 );
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
);
561 // not inside the calendar, i don't know it
566 void IncidenceChanger::setCalendar( CalendarSupport::Calendar
*calendar
)
568 d
->mCalendar
= calendar
;