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 <LibkdepimAkonadi/CollectionSearchJob>
#include <LibkdepimAkonadi/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
{
Laurent Montel's avatar
Laurent Montel committed
341
    for (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
}