controller.cpp 16.8 KB
Newer Older
1
/*
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 * Copyright (C) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * As a special exception, permission is given to link this program
 * with any edition of Qt, and distribute the resulting executable,
 * without including the source code for Qt in the source distribution.
 */

23
24
#include "controller.h"

Laurent Montel's avatar
Laurent Montel committed
25
26
#include <Libkdepim/CollectionSearchJob>
#include <Libkdepim/PersonSearchJob>
27
28
#include "korganizer_debug.h"

29
30
#include <AkonadiCore/EntityTreeModel>
#include <AkonadiCore/EntityDisplayAttribute>
31
#include <AkonadiCore/CollectionIdentificationAttribute>
32
33
34
35
#include <AkonadiCore/CollectionModifyJob>
#include <AkonadiCore/CollectionFetchJob>
#include <AkonadiCore/CollectionFetchScope>
#include <AkonadiCore/AttributeFactory>
36
37
#include <AkonadiSearch/PIM/collectionquery.h>

38
39
40
#include <KCalCore/Event>
#include <KCalCore/Journal>
#include <KCalCore/Todo>
41
#include <KLocalizedString>
42

43
#include <QIcon>
44

45
CollectionNode::CollectionNode(ReparentingModel &personModel, const Akonadi::Collection &col)
46
47
48
49
    : Node(personModel),
      isSearchNode(false),
      mCollection(col),
      mCheckState(Qt::Unchecked)
50
51
52
53
54
55
56
57
58
59
{
}

CollectionNode::~CollectionNode()
{

}

bool CollectionNode::operator==(const ReparentingModel::Node &node) const
{
60
    const CollectionNode *collectionNode = dynamic_cast<const CollectionNode *>(&node);
61
62
63
64
65
66
67
68
    if (collectionNode) {
        return (collectionNode->mCollection == mCollection);
    }
    return false;
}

QVariant CollectionNode::data(int role) const
{
69
70
    switch (role) {
    case Qt::DisplayRole: {
71
72
73
74
75
76
        QStringList path;
        Akonadi::Collection c = mCollection;
        while (c.isValid()) {
            path.prepend(c.name());
            c = c.parentCollection();
        }
Laurent Montel's avatar
Laurent Montel committed
77
        return path.join(QLatin1Char('/'));
78
    }
79
    case Qt::DecorationRole:
80
81
82
83
        if (mCollection.hasAttribute<Akonadi::EntityDisplayAttribute>()) {
            return mCollection.attribute<Akonadi::EntityDisplayAttribute>()->icon();
        }
        return QVariant();
84
    case Qt::CheckStateRole:
85
86
87
        if (isSearchNode) {
            return QVariant();
        }
88
        return mCheckState;
89
90
91
    case Qt::ToolTipRole:
        return i18nc("Collection: name collectionId", "Collection: %1(%2)", mCollection.name(), QString::number(mCollection.id()));
    case IsSearchResultRole:
92
        return isSearchNode;
93
94
    case CollectionRole:
    case Akonadi::EntityTreeModel::CollectionRole:
95
        return QVariant::fromValue(mCollection);
96
    case NodeTypeRole:
97
        return CollectionNodeRole;
98
    default:
99
        qCDebug(KORGANIZER_LOG) << "unknown role" << role;
100
        return QVariant();
101
    }
102
103
}

104
bool CollectionNode::setData(const QVariant &value, int role)
105
106
107
108
109
110
111
112
113
{
    if (role == Qt::CheckStateRole) {
        mCheckState = static_cast<Qt::CheckState>(value.toInt());
        emitter.emitEnabled(mCheckState == Qt::Checked, mCollection);
        return true;
    }
    return false;
}

114
bool CollectionNode::isDuplicateOf(const QModelIndex &sourceIndex)
115
116
117
118
{
    return (sourceIndex.data(Akonadi::EntityTreeModel::CollectionIdRole).value<Akonadi::Collection::Id>() == mCollection.id());
}

119
PersonNode::PersonNode(ReparentingModel &personModel, const KPIM::Person &person)
120
    :   Node(personModel),
Laurent Montel's avatar
Laurent Montel committed
121
        isSearchNode(false),
122
        mPerson(person),
Laurent Montel's avatar
Laurent Montel committed
123
        mCheckState(Qt::Unchecked)
124
125
126
127
128
129
130
131
132
133
134
{

}

PersonNode::~PersonNode()
{

}

bool PersonNode::operator==(const Node &node) const
{
135
    const PersonNode *personNode = dynamic_cast<const PersonNode *>(&node);
136
    if (personNode) {
137
        return (personNode->mPerson.uid == mPerson.uid);
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
    }
    return false;
}

void PersonNode::setChecked(bool enabled)
{
    if (enabled) {
        mCheckState = Qt::Checked;
    } else {
        mCheckState = Qt::Unchecked;
    }
}

QVariant PersonNode::data(int role) const
{
153
154
    switch (role) {
    case Qt::DisplayRole: {
155
156
        QString name = mPerson.name;
        if (!mPerson.ou.isEmpty()) {
157
            name += QStringLiteral(" (") + mPerson.ou + QStringLiteral(")");
158
159
        }
        return name;
160
    }
161
162
163
    case Qt::DecorationRole:
        return QIcon::fromTheme(QStringLiteral("meeting-participant"));
    case Qt::CheckStateRole:
164
165
166
        if (isSearchNode) {
            return QVariant();
        }
167
        return mCheckState;
168
169
    case Qt::ToolTipRole: {
        QString tooltip = i18n("Person: %1", mPerson.name);
170
        if (!mPerson.mail.isEmpty()) {
171
            tooltip += QStringLiteral("\n") + i18n("Mail: %1", mPerson.mail);
172
173
        }
        if (!mPerson.ou.isEmpty()) {
174
            tooltip += QStringLiteral("\n") + i18n("Organization Unit: %1", mPerson.ou);
175
176
        }
        return tooltip;
177
    }
178
    case PersonRole:
179
        return QVariant::fromValue(mPerson);
180
    case IsSearchResultRole:
181
        return isSearchNode;
182
    case NodeTypeRole:
183
        return PersonNodeRole;
184
185
    case CollectionRole:
    case Akonadi::EntityTreeModel::CollectionRole:
186
        return QVariant::fromValue(Akonadi::Collection(mPerson.rootCollection));
187
    default:
188
        qCDebug(KORGANIZER_LOG) << "unknown role" << role;
189
        return QVariant();
190
    }
191
192
}

193
bool PersonNode::setData(const QVariant &value, int role)
194
195
196
197
198
199
200
201
202
{
    if (role == Qt::CheckStateRole) {
        mCheckState = static_cast<Qt::CheckState>(value.toInt());
        emitter.emitEnabled(mCheckState == Qt::Checked, mPerson);
        return true;
    }
    return false;
}

203
bool PersonNode::adopts(const QModelIndex &sourceIndex)
204
205
206
207
208
209
210
{
    const Akonadi::Collection &parent = sourceIndex.data(Akonadi::EntityTreeModel::ParentCollectionRole).value<Akonadi::Collection>();
    if (parent.id() == mPerson.rootCollection) {
        return true;
    }

    const Akonadi::Collection &col = sourceIndex.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
211
    // qCDebug(KORGANIZER_LOG) << col.displayName();
212
213
214
    //FIXME: we need a way to compare the path we get from LDAP to the folder in akonadi.
    //TODO: get it from the folder attribute
    if ((col.isValid() && mPerson.folderPaths.contains(col.displayName())) || mPerson.collections.contains(col.id())) {
215
        // qCDebug(KORGANIZER_LOG) << "reparenting " << col.displayName() << " to " << mPerson.name;
216
217
218
219
220
        return true;
    }
    return false;
}

221
bool PersonNode::isDuplicateOf(const QModelIndex &sourceIndex)
222
{
223
    return (sourceIndex.data(PersonRole).value<KPIM::Person>().uid == mPerson.uid);
224
225
}

226
227
228
229
230
void PersonNode::update(const Node::Ptr &node)
{
    mPerson = node.staticCast<PersonNode>()->mPerson;
}

231
KPIM::Person PersonNodeManager::person(const QModelIndex &sourceIndex)
232
{
233
    KPIM::Person person;
234
235
    const Akonadi::Collection col = sourceIndex.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
    if (col.isValid()) {
236
        Akonadi::CollectionIdentificationAttribute *attr = col.attribute<Akonadi::CollectionIdentificationAttribute>();
237
238
        if (attr && attr->collectionNamespace() == "usertoplevel") {
            person.name = col.displayName();
239
240
241
            person.mail = QString::fromUtf8(attr->mail());
            person.ou = QString::fromUtf8(attr->ou());
            person.uid = col.name();
242
243
244
            person.rootCollection = col.id();
        }
    }
245
    return person;
246
247
}

248
void PersonNodeManager::checkSourceIndex(const QModelIndex &sourceIndex)
249
{
250
    const KPIM::Person &p = person(sourceIndex);
251
    if (p.rootCollection > -1) {
252
        model.addNode(ReparentingModel::Node::Ptr(new PersonNode(model, p)));
253
254
255
    }
}

256
257
void PersonNodeManager::updateSourceIndex(const QModelIndex &sourceIndex)
{
258
    const KPIM::Person &p = person(sourceIndex);
259
    if (p.rootCollection > -1) {
260
        model.updateNode(ReparentingModel::Node::Ptr(new PersonNode(model, p)));
261
262
263
264
265
    }
}

void PersonNodeManager::checkSourceIndexRemoval(const QModelIndex &sourceIndex)
{
266
    const KPIM::Person &p = person(sourceIndex);
267
    if (p.rootCollection > -1) {
268
        model.removeNode(PersonNode(model, p));
269
270
    }
}
271

272
Controller::Controller(ReparentingModel *personModel, ReparentingModel *searchModel, QObject *parent)
273
    : QObject(parent),
274
275
276
277
      mPersonModel(personModel),
      mSearchModel(searchModel),
      mCollectionSearchJob(0),
      mPersonSearchJob(0)
278
{
279
    Akonadi::AttributeFactory::registerAttribute<Akonadi::CollectionIdentificationAttribute>();
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
}

void Controller::setSearchString(const QString &searchString)
{
    if (mCollectionSearchJob) {
        disconnect(mCollectionSearchJob, 0, this, 0);
        mCollectionSearchJob->kill(KJob::Quietly);
        mCollectionSearchJob = 0;
    }
    if (mPersonSearchJob) {
        disconnect(mPersonSearchJob, 0, this, 0);
        mPersonSearchJob->kill(KJob::Quietly);
        mPersonSearchJob = 0;
    }
    //TODO: Delay and abort when results are found
    mSearchModel->clear();
296
    Q_EMIT searchIsActive(!searchString.isEmpty());
Laurent Montel's avatar
Laurent Montel committed
297
    const bool showAllPersonalFolders = (searchString == QLatin1String("*"));
298
    if (searchString.size() < 2 && !showAllPersonalFolders) {
299
        Q_EMIT searching(false);
300
301
302
        return;
    }

303
    if (!showAllPersonalFolders) {
304
        Q_EMIT searching(true);
305

306
307
        mPersonSearchJob = new KPIM::PersonSearchJob(searchString, this);
        connect(mPersonSearchJob, &KPIM::PersonSearchJob::personsFound,
Laurent Montel's avatar
Laurent Montel committed
308
                this, static_cast<void (Controller::*)(const QVector<KPIM::Person> &)>(&Controller::onPersonsFound));
309
310
311
        connect(mPersonSearchJob, &KPIM::PersonSearchJob::personUpdate, this, &Controller::onPersonUpdate);
        connect(mPersonSearchJob, &KPIM::PersonSearchJob::result,
                this, static_cast<void (Controller::*)(KJob *)>(&Controller::onPersonsFound));
312
313
        mPersonSearchJob->start();
    }
314

315
316
    mCollectionSearchJob = new KPIM::CollectionSearchJob(searchString, QStringList() << QStringLiteral("text/calendar"), this);
    connect(mCollectionSearchJob, &KPIM::CollectionSearchJob::result, this, &Controller::onCollectionsFound);
317
318
319
    mCollectionSearchJob->start();
}

320
void Controller::onCollectionsFound(KJob *job)
321
{
322
    mCollectionSearchJob = 0;
323
    if (!mPersonSearchJob) {
324
        Q_EMIT searching(false);
325
    }
326
    if (job->error()) {
327
        qCWarning(KORGANIZER_LOG) << job->errorString();
328
329
        return;
    }
330
    Q_FOREACH (const Akonadi::Collection &col, static_cast<KPIM::CollectionSearchJob *>(job)->matchingCollections()) {
331
        CollectionNode *collectionNode = new CollectionNode(*mSearchModel, col);
332
        collectionNode->isSearchNode = true;
333
        //toggled by the checkbox, results in collection getting monitored
334
        // connect(&collectionNode->emitter, SIGNAL(enabled(bool,Akonadi::Collection)), this, SLOT(onCollectionEnabled(bool,Akonadi::Collection)));
335
336
337
338
        mSearchModel->addNode(ReparentingModel::Node::Ptr(collectionNode));
    }
}

Laurent Montel's avatar
Laurent Montel committed
339
void Controller::onPersonsFound(const QVector<KPIM::Person> &persons)
340
{
341
    Q_FOREACH (const KPIM::Person &p, persons) {
342
        PersonNode *personNode = new PersonNode(*mSearchModel, p);
343
        personNode->isSearchNode = true;
344
        //toggled by the checkbox, results in person getting added to main model
345
        // connect(&personNode->emitter, SIGNAL(enabled(bool,Person)), this, SLOT(onPersonEnabled(bool,Person)));
346
347
        mSearchModel->addNode(ReparentingModel::Node::Ptr(personNode));
    }
348
349
}

350
void Controller::onPersonUpdate(const KPIM::Person &person)
351
{
352
    PersonNode *personNode = new PersonNode(*mSearchModel, person);
353
354
355
356
    personNode->isSearchNode = true;
    mSearchModel->updateNode(ReparentingModel::Node::Ptr(personNode));
}

357
void Controller::onPersonsFound(KJob *job)
358
{
359
    mPersonSearchJob = 0;
360
    if (!mCollectionSearchJob) {
361
        Q_EMIT searching(false);
362
363
    }
    if (job->error()) {
364
        qCWarning(KORGANIZER_LOG) << job->errorString();
365
366
        return;
    }
367
368
369
370
371
372
}

static Akonadi::EntityTreeModel *findEtm(QAbstractItemModel *model)
{
    QAbstractProxyModel *proxyModel;
    while (model) {
373
        proxyModel = qobject_cast<QAbstractProxyModel *>(model);
374
375
376
377
378
379
        if (proxyModel && proxyModel->sourceModel()) {
            model = proxyModel->sourceModel();
        } else {
            break;
        }
    }
380
    return qobject_cast<Akonadi::EntityTreeModel *>(model);
381
382
}

383
void Controller::setCollectionState(const Akonadi::Collection &collection, CollectionState collectionState, bool recursive)
384
{
385
386
387
388
389
390
    //We removed the children first, so the children in the tree are removed before the parents
    if (recursive) {
        //We have to include all mimetypes since mimetypes are not available yet (they will be synced once the collectoins are referenced)
        Akonadi::CollectionFetchJob *fetchJob = new Akonadi::CollectionFetchJob(collection, Akonadi::CollectionFetchJob::Recursive, this);
        fetchJob->setProperty("collectionState", static_cast<int>(collectionState));
        fetchJob->fetchScope().setListFilter(Akonadi::CollectionFetchScope::NoFilter);
391
        connect(fetchJob, &Akonadi::CollectionFetchJob::result, this, &Controller::onPersonCollectionsFetched);
392
393
394
395
396
    }
    {
        Akonadi::CollectionFetchJob *fetchJob = new Akonadi::CollectionFetchJob(collection, Akonadi::CollectionFetchJob::Base, this);
        fetchJob->setProperty("collectionState", static_cast<int>(collectionState));
        fetchJob->fetchScope().setListFilter(Akonadi::CollectionFetchScope::NoFilter);
397
        connect(fetchJob, &Akonadi::CollectionFetchJob::result, this, &Controller::onPersonCollectionsFetched);
398
    }
399
400
}

401
void Controller::onPersonCollectionsFetched(KJob *job)
402
{
403
    if (job->error()) {
404
        qCWarning(KORGANIZER_LOG) << "Failed to fetch collections " << job->errorString();
405
406
        return;
    }
407
408
    Akonadi::EntityTreeModel *etm = findEtm(mPersonModel);
    if (!etm) {
409
        qCWarning(KORGANIZER_LOG) << "Couldn't find etm";
410
411
        return;
    }
412
413

    const CollectionState collectionState = static_cast<CollectionState>(job->property("collectionState").toInt());
Laurent Montel's avatar
Laurent Montel committed
414
    Q_FOREACH (const Akonadi::Collection &col, static_cast<Akonadi::CollectionFetchJob *>(job)->collections()) {
415
        // qCDebug(KORGANIZER_LOG) << col.displayName() << "do enable " << enabled;
416
417
418
419
420
421
422
423
424
425
426
        Akonadi::Collection modifiedCollection = col;
        if (collectionState == Enabled) {
            modifiedCollection.setShouldList(Akonadi::Collection::ListDisplay, true);
        }
        if (collectionState == Disabled) {
            modifiedCollection.setShouldList(Akonadi::Collection::ListDisplay, false);
        }
        //HACK: We have no way of getting to the correct session as used by the etm,
        //and two concurrent jobs end up overwriting the enabled state of each other.
        etm->setCollectionReferenced(modifiedCollection, collectionState == Referenced);
    }
427
428
}

429
void Controller::addPerson(const KPIM::Person &person)
430
{
431
    qCDebug(KORGANIZER_LOG) << person.uid << person.name << person.rootCollection;
432
    KPIM::Person p = person;
433
434

    if (person.rootCollection == -1) {
435
        Akonadi::Search::PIM::CollectionQuery query;
436
437
        query.setNamespace(QStringList() << QStringLiteral("usertoplevel"));
        query.pathMatches(QStringLiteral("/Other Users/") + p.uid);
438
        query.setLimit(1);
439
        Akonadi::Search::PIM::ResultIterator it = query.exec();
440
441
442
443
        Akonadi::Collection::List collections;
        while (it.next()) {
            collections << Akonadi::Collection(it.id());
        }
444
        qCDebug(KORGANIZER_LOG) << "Found collections " << collections.size() << "for" << p.name;
445
        if (collections.size() == 1) {
446
            qCDebug(KORGANIZER_LOG) << "Set rootCollection=" << collections.at(0).id();
447
448
            p.rootCollection = collections.at(0).id();
        }
449
450
    }

451
    PersonNode *personNode = new PersonNode(*mPersonModel, p);
452
453
    personNode->setChecked(true);
    mPersonModel->addNode(ReparentingModel::Node::Ptr(personNode));
454

455
456
    if (p.rootCollection > -1) {
        setCollectionState(Akonadi::Collection(p.rootCollection), Referenced, true);
457
    } else {
458
        qCDebug(KORGANIZER_LOG) << "well this only a ldap search object without a collection";
459
460
        //TODO: use freebusy data into calendar
    }
461
462
}

463
void Controller::removePerson(const KPIM::Person &person)
464
{
465
    qCDebug(KORGANIZER_LOG) << person.uid << person.name << person.rootCollection;
466
    mPersonModel->removeNode(PersonNode(*mPersonModel, person));
467

468
469
470
    if (person.rootCollection > -1) {
        setCollectionState(Akonadi::Collection(person.rootCollection), Disabled, true);
    } else {
471
        qCDebug(KORGANIZER_LOG) << "well this only a ldap search object without a collection";
472
473
        //TODO: delete freebusy data from calendar
    }
474
475
}