File indexing completed on 2025-02-16 04:56:34

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