File indexing completed on 2024-06-23 04:42:35

0001 // SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
0002 // SPDX-License-Identifier: LGPL-2.1-or-later
0003 
0004 #include "attendeesmodel.h"
0005 #include "kalendar_debug.h"
0006 #include <KContacts/Addressee>
0007 #include <KLocalizedString>
0008 #include <QMetaEnum>
0009 #include <QModelIndex>
0010 #include <QRegularExpression>
0011 
0012 #include <Akonadi/Item>
0013 #include <Akonadi/ItemFetchJob>
0014 #include <Akonadi/ItemFetchScope>
0015 #include <Akonadi/SearchQuery>
0016 
0017 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0018 #include <akonadi_version.h>
0019 #if AKONADI_VERSION >= QT_VERSION_CHECK(5, 19, 40)
0020 #include <Akonadi/ContactSearchJob>
0021 #else
0022 #include <Akonadi/Contact/ContactSearchJob>
0023 #endif
0024 #else
0025 #include <Akonadi/ContactSearchJob>
0026 #endif
0027 
0028 AttendeeStatusModel::AttendeeStatusModel(QObject *parent)
0029     : QAbstractListModel(parent)
0030 {
0031     QRegularExpression lowerToCapitalSep(QStringLiteral("([a-z])([A-Z])"));
0032     QRegularExpression capitalToCapitalSep(QStringLiteral("([A-Z])([A-Z])"));
0033 
0034     for (int i = 0; i < QMetaEnum::fromType<KCalendarCore::Attendee::PartStat>().keyCount(); i++) {
0035         int value = QMetaEnum::fromType<KCalendarCore::Attendee::PartStat>().value(i);
0036 
0037         // QLatin1String is a workaround for QT_NO_CAST_FROM_ASCII.
0038         // Regular expression adds space between every lowercase and Capitalised character then does the same
0039         // for capitalised letters together, e.g. ThisIsATest. Not a problem right now, but best to be safe.
0040         const QLatin1String enumName = QLatin1String(QMetaEnum::fromType<KCalendarCore::Attendee::PartStat>().key(i));
0041         QString displayName = enumName;
0042         displayName.replace(lowerToCapitalSep, QStringLiteral("\\1 \\2"));
0043         displayName.replace(capitalToCapitalSep, QStringLiteral("\\1 \\2"));
0044         displayName.replace(lowerToCapitalSep, QStringLiteral("\\1 \\2"));
0045 
0046         m_status[value] = i18n(displayName.toStdString().c_str());
0047     }
0048 }
0049 
0050 QVariant AttendeeStatusModel::data(const QModelIndex &idx, int role) const
0051 {
0052     if (!idx.isValid()) {
0053         return {};
0054     }
0055 
0056     const int value = QMetaEnum::fromType<KCalendarCore::Attendee::PartStat>().value(idx.row());
0057 
0058     switch (role) {
0059     case DisplayNameRole:
0060         return m_status[value];
0061     case ValueRole:
0062         return value;
0063     default:
0064         qCWarning(KALENDAR_LOG) << "Unknown role for attendee:" << QMetaEnum::fromType<Roles>().valueToKey(role);
0065         return {};
0066     }
0067 }
0068 
0069 QHash<int, QByteArray> AttendeeStatusModel::roleNames() const
0070 {
0071     return {
0072         {DisplayNameRole, QByteArrayLiteral("display")},
0073         {ValueRole, QByteArrayLiteral("value")},
0074     };
0075 }
0076 
0077 int AttendeeStatusModel::rowCount(const QModelIndex &) const
0078 {
0079     return m_status.size();
0080 }
0081 
0082 AttendeesModel::AttendeesModel(QObject *parent, KCalendarCore::Incidence::Ptr incidencePtr)
0083     : QAbstractListModel(parent)
0084     , m_incidence(incidencePtr)
0085     , m_attendeeStatusModel(parent)
0086 {
0087     connect(this, &AttendeesModel::attendeesChanged, this, &AttendeesModel::updateAkonadiContactIds);
0088 }
0089 
0090 KCalendarCore::Incidence::Ptr AttendeesModel::incidencePtr() const
0091 {
0092     return m_incidence;
0093 }
0094 
0095 void AttendeesModel::setIncidencePtr(KCalendarCore::Incidence::Ptr incidence)
0096 {
0097     if (m_incidence == incidence) {
0098         return;
0099     }
0100     m_incidence = incidence;
0101 
0102     Q_EMIT incidencePtrChanged();
0103     Q_EMIT attendeesChanged();
0104     Q_EMIT attendeeStatusModelChanged();
0105     Q_EMIT layoutChanged();
0106 }
0107 
0108 KCalendarCore::Attendee::List AttendeesModel::attendees() const
0109 {
0110     return m_incidence->attendees();
0111 }
0112 
0113 void AttendeesModel::updateAkonadiContactIds()
0114 {
0115     m_attendeesAkonadiIds.clear();
0116 
0117     const auto attendees = m_incidence->attendees();
0118     for (const auto &attendee : attendees) {
0119         auto job = new Akonadi::ContactSearchJob();
0120         job->setQuery(Akonadi::ContactSearchJob::Email, attendee.email());
0121 
0122         connect(job, &Akonadi::ContactSearchJob::result, this, [this](KJob *job) {
0123             auto searchJob = qobject_cast<Akonadi::ContactSearchJob *>(job);
0124 
0125             const auto items = searchJob->items();
0126             for (const auto &item : items) {
0127                 m_attendeesAkonadiIds.append(item.id());
0128             }
0129 
0130             Q_EMIT attendeesAkonadiIdsChanged();
0131         });
0132     }
0133 
0134     Q_EMIT attendeesAkonadiIdsChanged();
0135 }
0136 
0137 AttendeeStatusModel *AttendeesModel::attendeeStatusModel()
0138 {
0139     return &m_attendeeStatusModel;
0140 }
0141 
0142 QList<qint64> AttendeesModel::attendeesAkonadiIds() const
0143 {
0144     return m_attendeesAkonadiIds;
0145 }
0146 
0147 QVariant AttendeesModel::data(const QModelIndex &idx, int role) const
0148 {
0149     if (!hasIndex(idx.row(), idx.column())) {
0150         return {};
0151     }
0152     const auto attendee = m_incidence->attendees().at(idx.row());
0153     switch (role) {
0154     case CuTypeRole:
0155         return attendee.cuType();
0156     case DelegateRole:
0157         return attendee.delegate();
0158     case DelegatorRole:
0159         return attendee.delegator();
0160     case EmailRole:
0161         return attendee.email();
0162     case FullNameRole:
0163         return attendee.fullName();
0164     case IsNullRole:
0165         return attendee.isNull();
0166     case NameRole:
0167         return attendee.name();
0168     case RoleRole:
0169         return attendee.role();
0170     case RSVPRole:
0171         return attendee.RSVP();
0172     case StatusRole:
0173         return attendee.status();
0174     case UidRole:
0175         return attendee.uid();
0176     default:
0177         qCWarning(KALENDAR_LOG) << "Unknown role for attendee:" << QMetaEnum::fromType<Roles>().valueToKey(role);
0178         return {};
0179     }
0180 }
0181 
0182 bool AttendeesModel::setData(const QModelIndex &idx, const QVariant &value, int role)
0183 {
0184     if (!idx.isValid()) {
0185         return false;
0186     }
0187 
0188     // When modifying attendees, remember you cannot change them directly from m_incidence->attendees (is a const).
0189     KCalendarCore::Attendee::List currentAttendees(m_incidence->attendees());
0190 
0191     switch (role) {
0192     case CuTypeRole: {
0193         const auto cuType = static_cast<KCalendarCore::Attendee::CuType>(value.toInt());
0194         currentAttendees[idx.row()].setCuType(cuType);
0195         break;
0196     }
0197     case DelegateRole: {
0198         const QString delegate = value.toString();
0199         currentAttendees[idx.row()].setDelegate(delegate);
0200         break;
0201     }
0202     case DelegatorRole: {
0203         const QString delegator = value.toString();
0204         currentAttendees[idx.row()].setDelegator(delegator);
0205         break;
0206     }
0207     case EmailRole: {
0208         const QString email = value.toString();
0209         currentAttendees[idx.row()].setEmail(email);
0210         break;
0211     }
0212     case FullNameRole: {
0213         // Not a writable property
0214         return false;
0215     }
0216     case IsNullRole: {
0217         // Not an editable value
0218         return false;
0219     }
0220     case NameRole: {
0221         const QString name = value.toString();
0222         currentAttendees[idx.row()].setName(name);
0223         break;
0224     }
0225     case RoleRole: {
0226         const auto role = static_cast<KCalendarCore::Attendee::Role>(value.toInt());
0227         currentAttendees[idx.row()].setRole(role);
0228         break;
0229     }
0230     case RSVPRole: {
0231         const bool rsvp = value.toBool();
0232         currentAttendees[idx.row()].setRSVP(rsvp);
0233         break;
0234     }
0235     case StatusRole: {
0236         const auto status = static_cast<KCalendarCore::Attendee::PartStat>(value.toInt());
0237         currentAttendees[idx.row()].setStatus(status);
0238         break;
0239     }
0240     case UidRole: {
0241         const QString uid = value.toString();
0242         currentAttendees[idx.row()].setUid(uid);
0243         break;
0244     }
0245     default:
0246         qCWarning(KALENDAR_LOG) << "Unknown role for incidence:" << QMetaEnum::fromType<Roles>().valueToKey(role);
0247         return false;
0248     }
0249     m_incidence->setAttendees(currentAttendees);
0250     Q_EMIT dataChanged(idx, idx);
0251     return true;
0252 }
0253 
0254 QHash<int, QByteArray> AttendeesModel::roleNames() const
0255 {
0256     return {
0257         {CuTypeRole, QByteArrayLiteral("cuType")},
0258         {DelegateRole, QByteArrayLiteral("delegate")},
0259         {DelegatorRole, QByteArrayLiteral("delegator")},
0260         {EmailRole, QByteArrayLiteral("email")},
0261         {FullNameRole, QByteArrayLiteral("fullName")},
0262         {IsNullRole, QByteArrayLiteral("isNull")},
0263         {NameRole, QByteArrayLiteral("name")},
0264         {RoleRole, QByteArrayLiteral("role")},
0265         {RSVPRole, QByteArrayLiteral("rsvp")},
0266         {StatusRole, QByteArrayLiteral("status")},
0267         {UidRole, QByteArrayLiteral("uid")},
0268     };
0269 }
0270 
0271 int AttendeesModel::rowCount(const QModelIndex &) const
0272 {
0273     return m_incidence->attendeeCount();
0274 }
0275 
0276 void AttendeesModel::addAttendee(qint64 itemId, const QString &email)
0277 {
0278     if (itemId) {
0279         Akonadi::Item item(itemId);
0280 
0281         auto job = new Akonadi::ItemFetchJob(item);
0282         job->fetchScope().fetchFullPayload();
0283 
0284         connect(job, &Akonadi::ItemFetchJob::result, this, [this, email](KJob *job) {
0285             const Akonadi::ItemFetchJob *fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
0286             const auto item = fetchJob->items().at(0);
0287             const auto payload = item.payload<KContacts::Addressee>();
0288 
0289             KCalendarCore::Attendee attendee(payload.name(),
0290                                              payload.preferredEmail(),
0291                                              true,
0292                                              KCalendarCore::Attendee::NeedsAction,
0293                                              KCalendarCore::Attendee::ReqParticipant);
0294 
0295             if (!email.isNull()) {
0296                 attendee.setEmail(email);
0297             }
0298 
0299             m_incidence->addAttendee(attendee);
0300             // Otherwise won't update
0301             Q_EMIT attendeesChanged();
0302             Q_EMIT layoutChanged();
0303         });
0304     } else {
0305         // QLatin1String is a workaround for QT_NO_CAST_FROM_ASCII
0306         // addAttendee method does not work with null strings, so we use empty strings
0307         KCalendarCore::Attendee attendee(QLatin1String(""),
0308                                          QLatin1String(""),
0309                                          true,
0310                                          KCalendarCore::Attendee::NeedsAction,
0311                                          KCalendarCore::Attendee::ReqParticipant);
0312 
0313         // addAttendee won't actually add any attendees without a set name
0314         m_incidence->addAttendee(attendee);
0315     }
0316 
0317     Q_EMIT attendeesChanged();
0318     Q_EMIT layoutChanged();
0319 }
0320 
0321 void AttendeesModel::deleteAttendee(int row)
0322 {
0323     if (!hasIndex(row, 0)) {
0324         return;
0325     }
0326 
0327     KCalendarCore::Attendee::List currentAttendees(m_incidence->attendees());
0328     currentAttendees.removeAt(row);
0329     m_incidence->setAttendees(currentAttendees);
0330 
0331     Q_EMIT attendeesChanged();
0332     Q_EMIT layoutChanged();
0333 }
0334 
0335 void AttendeesModel::deleteAttendeeFromAkonadiId(qint64 itemId)
0336 {
0337     Akonadi::Item item(itemId);
0338 
0339     auto job = new Akonadi::ItemFetchJob(item);
0340     job->fetchScope().fetchFullPayload();
0341 
0342     connect(job, &Akonadi::ItemFetchJob::result, this, [this](KJob *job) {
0343         auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
0344 
0345         auto item = fetchJob->items().at(0);
0346         auto payload = item.payload<KContacts::Addressee>();
0347 
0348         for (int i = 0; i < m_incidence->attendeeCount(); i++) {
0349             const auto emails = payload.emails();
0350             for (const auto &email : emails) {
0351                 if (m_incidence->attendees()[i].email() == email) {
0352                     deleteAttendee(i);
0353                     break;
0354                 }
0355             }
0356         }
0357     });
0358 }