diff --git a/framework/src/domain/eventcontroller.cpp b/framework/src/domain/eventcontroller.cpp index 22e267fc..3ea840fa 100644 --- a/framework/src/domain/eventcontroller.cpp +++ b/framework/src/domain/eventcontroller.cpp @@ -1,208 +1,210 @@ /* * Copyright (C) 2017 Michael Bohlender, * Copyright (C) 2018 Christian Mollekopf, * * 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. */ #include "eventcontroller.h" #include #include #include #include #include #include #include "eventoccurrencemodel.h" using namespace Sink::ApplicationDomain; class AttendeeController : public Kube::ListPropertyController { Q_OBJECT public: AttendeeController() : Kube::ListPropertyController{{"name", "email", "status"}} { } }; EventController::EventController() : Kube::Controller(), controller_attendees{new AttendeeController}, action_save{new Kube::ControllerAction{this, &EventController::save}} { updateSaveAction(); } void EventController::save() { using namespace Sink; using namespace Sink::ApplicationDomain; const auto calendar = getCalendar(); if (!calendar) { qWarning() << "No calendar selected"; return; } const auto occurrenceVariant = getEventOccurrence(); if (occurrenceVariant.isValid()) { const auto occurrence = occurrenceVariant.value(); Sink::ApplicationDomain::Event event = *occurrence.domainObject; //Apply the changed properties on top of what's existing auto calcoreEvent = KCalCore::ICalFormat().readIncidence(event.getIcal()).dynamicCast(); if(!calcoreEvent) { SinkWarning() << "Invalid ICal to process, ignoring..."; return; } saveToEvent(*calcoreEvent); event.setIcal(KCalCore::ICalFormat().toICalString(calcoreEvent).toUtf8()); event.setCalendar(*calendar); auto job = Store::modify(event) .then([&] (const KAsync::Error &error) { if (error) { SinkWarning() << "Failed to save the event: " << error; } emit done(); }); run(job); } else { Sink::ApplicationDomain::Event event(calendar->resourceInstanceIdentifier()); auto calcoreEvent = QSharedPointer::create(); calcoreEvent->setUid(QUuid::createUuid().toString()); saveToEvent(*calcoreEvent); event.setIcal(KCalCore::ICalFormat().toICalString(calcoreEvent).toUtf8()); event.setCalendar(*calendar); auto job = Store::create(event) .then([&] (const KAsync::Error &error) { if (error) { SinkWarning() << "Failed to save the event: " << error; } emit done(); }); run(job); } } void EventController::updateSaveAction() { saveAction()->setEnabled(!getSummary().isEmpty()); } static EventController::ParticipantStatus toStatus(KCalCore::Attendee::PartStat status) { switch(status) { case KCalCore::Attendee::Accepted: return EventController::Accepted; case KCalCore::Attendee::Declined: return EventController::Declined; case KCalCore::Attendee::NeedsAction: default: break; } return EventController::Unknown; } static KCalCore::Attendee::PartStat fromStatus(EventController::ParticipantStatus status) { switch(status) { case EventController::Accepted: return KCalCore::Attendee::Accepted; case EventController::Declined: return KCalCore::Attendee::Declined; case EventController::Unknown: break; } return KCalCore::Attendee::NeedsAction; } void EventController::populateFromEvent(const KCalCore::Event &event) { setSummary(event.summary()); setDescription(event.description()); setLocation(event.location()); setRecurring(event.recurs()); setAllDay(event.allDay()); + setOrganizer(event.organizer()->fullName()); for (const auto &attendee : event.attendees()) { attendeesController()->add({{"name", attendee->name()}, {"email", attendee->email()}, {"status", toStatus(attendee->status())}}); } } void EventController::saveToEvent(KCalCore::Event &event) { event.setSummary(getSummary()); event.setDescription(getDescription()); event.setLocation(getLocation()); event.setDtStart(getStart()); event.setDtEnd(getEnd()); event.setAllDay(getAllDay()); + event.setOrganizer(getOrganizer()); event.clearAttendees(); KCalCore::Attendee::List attendees; attendeesController()->traverse([&] (const QVariantMap &map) { bool rsvp = true; KCalCore::Attendee::PartStat status = fromStatus(map["status"].value()); KCalCore::Attendee::Role role = KCalCore::Attendee::ReqParticipant; event.addAttendee(KCalCore::Attendee::Ptr::create(map["name"].toString(), map["email"].toString(), rsvp, status, role, QString{})); }); } void EventController::init() { using namespace Sink; const auto occurrenceVariant = getEventOccurrence(); if (occurrenceVariant.isValid()) { const auto occurrence = occurrenceVariant.value(); Sink::ApplicationDomain::Event event = *occurrence.domainObject; setCalendar(ApplicationDomainType::Ptr::create(ApplicationDomainType::createEntity(event.resourceInstanceIdentifier(), event.getCalendar()))); auto icalEvent = KCalCore::ICalFormat().readIncidence(event.getIcal()).dynamicCast(); if(!icalEvent) { SinkWarning() << "Invalid ICal to process, ignoring..."; return; } populateFromEvent(*icalEvent); setStart(occurrence.start); setEnd(occurrence.end); } } void EventController::remove() { const auto occurrenceVariant = getEventOccurrence(); if (occurrenceVariant.isValid()) { const auto occurrence = occurrenceVariant.value(); Sink::ApplicationDomain::Event event = *occurrence.domainObject; run(Sink::Store::remove(event)); } } #include "eventcontroller.moc" diff --git a/framework/src/domain/eventcontroller.h b/framework/src/domain/eventcontroller.h index 680b5f43..4110c74b 100644 --- a/framework/src/domain/eventcontroller.h +++ b/framework/src/domain/eventcontroller.h @@ -1,77 +1,78 @@ /* * Copyright (C) 2017 Michael Bohlender, * Copyright (C) 2018 Christian Mollekopf, * * 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. */ #pragma once #include "kube_export.h" #include #include #include #include "controller.h" namespace KCalCore { class Event; }; class KUBE_EXPORT EventController : public Kube::Controller { Q_OBJECT // Input properties KUBE_CONTROLLER_PROPERTY(QVariant, EventOccurrence, eventOccurrence) //Interface properties KUBE_CONTROLLER_PROPERTY(QByteArray, AccountId, accountId) KUBE_CONTROLLER_PROPERTY(QString, Summary, summary) KUBE_CONTROLLER_PROPERTY(QString, Description, description) KUBE_CONTROLLER_PROPERTY(QString, Location, location) KUBE_CONTROLLER_PROPERTY(QDateTime, Start, start) KUBE_CONTROLLER_PROPERTY(QDateTime, End, end) KUBE_CONTROLLER_PROPERTY(QString, RecurrenceString, recurrenceString) KUBE_CONTROLLER_PROPERTY(bool, AllDay, allDay) KUBE_CONTROLLER_PROPERTY(bool, Recurring, recurring) KUBE_CONTROLLER_PROPERTY(Sink::ApplicationDomain::ApplicationDomainType::Ptr, Calendar, calendar) + KUBE_CONTROLLER_PROPERTY(QString, Organizer, organizer) KUBE_CONTROLLER_LISTCONTROLLER(attendees) KUBE_CONTROLLER_ACTION(save) public: enum ParticipantStatus { Unknown, Accepted, Declined, }; Q_ENUM(ParticipantStatus); explicit EventController(); void init() override; Q_INVOKABLE void remove(); protected: void populateFromEvent(const KCalCore::Event &event); void saveToEvent(KCalCore::Event &event); private slots: void updateSaveAction(); }; diff --git a/framework/src/domain/invitationcontroller.cpp b/framework/src/domain/invitationcontroller.cpp index d3627d51..9d1e27ef 100644 --- a/framework/src/domain/invitationcontroller.cpp +++ b/framework/src/domain/invitationcontroller.cpp @@ -1,199 +1,196 @@ /* * Copyright (C) 2017 Michael Bohlender, * Copyright (C) 2018 Christian Mollekopf, * * 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. */ #include "invitationcontroller.h" #include #include #include #include #include #include #include #include "mailtemplates.h" #include "sinkutils.h" using namespace Sink::ApplicationDomain; InvitationController::InvitationController() : EventController(), action_accept{new Kube::ControllerAction{this, &InvitationController::accept}}, action_decline{new Kube::ControllerAction{this, &InvitationController::decline}} { } void InvitationController::loadICal(const QString &ical) { using namespace Sink; using namespace Sink::ApplicationDomain; KCalCore::Calendar::Ptr calendar(new KCalCore::MemoryCalendar{QTimeZone::systemTimeZone()}); auto msg = KCalCore::ICalFormat{}.parseScheduleMessage(calendar, ical.toUtf8()); if (!msg) { SinkWarning() << "Invalid scheudle message to process, ignoring..."; return; } auto icalEvent = msg->event().dynamicCast(); if (msg->method() != KCalCore::iTIPRequest) { SinkWarning() << "Invalid method " << msg->method(); return; } if(!icalEvent) { SinkWarning() << "Invalid ICal to process, ignoring..."; return; } - setOrganizer(icalEvent->organizer()->fullName()); - Query query; query.request(); query.request(); query.filter(icalEvent->uid().toUtf8()); Store::fetchAll(query).then([this, icalEvent](const QList &events) { if (!events.isEmpty()) { setState(InvitationState::Accepted); auto icalEvent = KCalCore::ICalFormat().readIncidence(events.first()->getIcal()).dynamicCast(); if(!icalEvent) { SinkWarning() << "Invalid ICal to process, ignoring..."; return; } populateFromEvent(*icalEvent); setStart(icalEvent->dtStart()); setEnd(icalEvent->dtEnd()); setUid(icalEvent->uid().toUtf8()); } else { setState(InvitationState::Unknown); populateFromEvent(*icalEvent); setStart(icalEvent->dtStart()); setEnd(icalEvent->dtEnd()); setUid(icalEvent->uid().toUtf8()); } }).exec(); } void sendIMipMessage(const QByteArray &accountId, const QString &from, KCalCore::Event::Ptr event) { const auto organizerEmail = event->organizer()->fullName(); if (organizerEmail.isEmpty()) { qWarning() << "Failed to find the organizer to send the reply to " << organizerEmail; return; } QString body = "The invitation has been accepted"; auto msg = MailTemplates::createIMipMessage( from, {{organizerEmail}, {}, {}}, QString("\"%1\" has been accepted by %2").arg(event->summary()).arg("Meeeee"), body, KCalCore::ICalFormat{}.createScheduleMessage(event, KCalCore::iTIPReply)); qWarning() << "Msg " << msg->encodedContent(); SinkUtils::sendMail(msg->encodedContent(true), accountId) .then([&] (const KAsync::Error &error) { if (error) { SinkWarning() << "Failed to send message " << error; } }).exec(); } void InvitationController::storeEvent(InvitationState status) { using namespace Sink; using namespace Sink::ApplicationDomain; const auto calendar = getCalendar(); if (!calendar) { qWarning() << "No calendar selected"; return; } Query query; query.request() .request() .request(); auto job = Store::fetchAll(query) .then([=] (const QList &list) { if (list.isEmpty()) { qWarning() << "Failed to find an identity"; } QString fromAddress; QByteArray accountId; bool foundMatch = false; for (const auto &identity : list) { const auto id = attendeesController()->findByProperty("email", identity->getAddress()); if (!id.isEmpty()) { if (status == InvitationController::Accepted) { attendeesController()->setValue(id, "status", EventController::Accepted); } else { attendeesController()->setValue(id, "status", EventController::Declined); } fromAddress = identity->getAddress(); accountId = identity->getAccount(); foundMatch = true; } } if (!foundMatch) { qWarning() << "Failed to find a matching identity"; return KAsync::error("Failed to find a matching identity"); } auto calcoreEvent = QSharedPointer::create(); calcoreEvent->setUid(getUid()); saveToEvent(*calcoreEvent); - calcoreEvent->setOrganizer(getOrganizer()); sendIMipMessage(accountId, fromAddress, calcoreEvent); Event event(calendar->resourceInstanceIdentifier()); event.setIcal(KCalCore::ICalFormat().toICalString(calcoreEvent).toUtf8()); event.setCalendar(*calendar); return Store::create(event) .then([=] (const KAsync::Error &error) { if (error) { SinkWarning() << "Failed to save the event: " << error; } setState(status); emit done(); }); }); run(job); } void InvitationController::accept() { storeEvent(InvitationState::Accepted); } void InvitationController::decline() { storeEvent(InvitationState::Declined); } diff --git a/framework/src/domain/invitationcontroller.h b/framework/src/domain/invitationcontroller.h index 79fc9af3..f1173c3f 100644 --- a/framework/src/domain/invitationcontroller.h +++ b/framework/src/domain/invitationcontroller.h @@ -1,57 +1,56 @@ /* * Copyright (C) 2017 Michael Bohlender, * Copyright (C) 2018 Christian Mollekopf, * * 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. */ #pragma once #include "kube_export.h" #include #include #include #include "eventcontroller.h" class KUBE_EXPORT InvitationController : public EventController { Q_OBJECT public: enum InvitationState { Unknown, Accepted, Declined, }; Q_ENUM(InvitationState); KUBE_CONTROLLER_PROPERTY(QByteArray, Uid, uid) KUBE_CONTROLLER_PROPERTY(InvitationState, State, state) - KUBE_CONTROLLER_PROPERTY(QString, Organizer, organizer) KUBE_CONTROLLER_ACTION(accept) KUBE_CONTROLLER_ACTION(decline) public: explicit InvitationController(); Q_INVOKABLE void loadICal(const QString &message); private: void storeEvent(InvitationState); }; diff --git a/tests/teststore.cpp b/tests/teststore.cpp index a8d4ddc4..a811b28c 100644 --- a/tests/teststore.cpp +++ b/tests/teststore.cpp @@ -1,435 +1,444 @@ /* Copyright (c) 2018 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "teststore.h" #include #include #include #include #include #include #include #include #include #include #include #include "framework/src/domain/mime/mailtemplates.h" #include "framework/src/keyring.h" using namespace Kube; static void iterateOverObjects(const QVariantList &list, std::function callback) { for (const auto &entry : list) { auto object = entry.toMap(); callback(object); } } static QStringList toStringList(const QVariantList &list) { QStringList s; for (const auto &e : list) { s << e.toString(); } return s; } static QByteArrayList toByteArrayList(const QVariantList &list) { QByteArrayList s; for (const auto &e : list) { s << e.toByteArray(); } return s; } static void createMail(const QVariantMap &object, const QByteArray &folder = {}, const QByteArray &resourceId = {}) { using namespace Sink::ApplicationDomain; auto toAddresses = toStringList(object["to"].toList()); auto ccAddresses = toStringList(object["cc"].toList()); auto bccAddresses = toStringList(object["bcc"].toList()); QList attachments = {}; if (object.contains("attachments")) { auto attachmentSpecs = object["attachments"].toList(); for (int i = 0; i < attachmentSpecs.size(); ++i) { auto const &spec = attachmentSpecs.at(i).toMap(); attachments << Attachment{spec["name"].toString(), spec["name"].toString(), spec["mimeType"].toByteArray(), false, spec["data"].toByteArray()}; } } KMime::Types::Mailbox from; if (object.contains("from")) { from.fromUnicodeString(object["from"].toString()); } else { from.fromUnicodeString("identity@example.org"); } auto msg = MailTemplates::createMessage({}, toAddresses, ccAddresses, bccAddresses, from, object["subject"].toString(), object["body"].toString(), object["bodyIsHtml"].toBool(), attachments, {}, {}); Q_ASSERT(msg); if (object.contains("messageId")) { msg->messageID(true)->from7BitString(object["messageId"].toByteArray()); } if (object.contains("inReplyTo")) { msg->inReplyTo(true)->from7BitString(object["inReplyTo"].toByteArray()); } if (object.contains("date")) { msg->date(true)->setDateTime(QDateTime::fromString(object["date"].toString(), Qt::ISODate)); } msg->assemble(); auto res = resourceId; if (res.isEmpty()) { res = object["resource"].toByteArray(); } auto mail = ApplicationDomainType::createEntity(res); mail.setMimeMessage(msg->encodedContent(true)); mail.setUnread(object["unread"].toBool()); mail.setDraft(object["draft"].toBool()); if (!folder.isEmpty()) { mail.setFolder(folder); } Sink::Store::create(mail).exec().waitForFinished(); } static void createFolder(const QVariantMap &object, const QByteArray &folderId = {}) { using namespace Sink::ApplicationDomain; auto resourceId = object["resource"].toByteArray(); auto folder = ApplicationDomainType::createEntity(resourceId); folder.setName(object["name"].toString()); folder.setSpecialPurpose(toByteArrayList(object["specialpurpose"].toList())); if (!folderId.isEmpty()) { folder.setParent(folderId); } Sink::Store::create(folder).exec().waitForFinished(); iterateOverObjects(object.value("mails").toList(), [=](const QVariantMap &object) { createMail(object, folder.identifier(), resourceId); }); iterateOverObjects(object.value("folders").toList(), [=](const QVariantMap &object) { createFolder(object, folder.identifier()); }); } static void createEvent(const QVariantMap &object, const QByteArray &calendarId, const QByteArray &resourceId) { using Sink::ApplicationDomain::ApplicationDomainType; using Sink::ApplicationDomain::Event; auto sinkEvent = ApplicationDomainType::createEntity(resourceId); auto calcoreEvent = QSharedPointer::create(); QString uid; if (object.contains("uid")) { uid = object["uid"].toString(); } else { uid = QUuid::createUuid().toString(); } calcoreEvent->setUid(uid); auto summary = object["summary"].toString(); calcoreEvent->setSummary(summary); if (object.contains("description")) { auto description = object["description"].toString(); calcoreEvent->setDescription(description); } auto startTime = object["starts"].toDateTime(); auto endTime = object["ends"].toDateTime(); calcoreEvent->setDtStart(startTime); calcoreEvent->setDtEnd(endTime); if (object.contains("allDay")) { calcoreEvent->setAllDay(object["allDay"].toBool()); } - auto ical = KCalCore::ICalFormat().toICalString(calcoreEvent); - sinkEvent.setIcal(ical.toUtf8()); + if (object.contains("organizer")) { + calcoreEvent->setOrganizer(object["organizer"].toString()); + } + if (object.contains("attendees")) { + for (const auto &attendee : object["attendees"].toList()) { + auto map = attendee.toMap(); + calcoreEvent->addAttendee(KCalCore::Attendee::Ptr::create(map["name"].toString(), map["email"].toString(), true, KCalCore::Attendee::NeedsAction, KCalCore::Attendee::ReqParticipant, QString{})); + } + } + + sinkEvent.setIcal(KCalCore::ICalFormat().toICalString(calcoreEvent).toUtf8()); sinkEvent.setCalendar(calendarId); Sink::Store::create(sinkEvent).exec().waitForFinished(); } static void createTodo(const QVariantMap &object, const QByteArray &calendarId, const QByteArray &resourceId) { using Sink::ApplicationDomain::ApplicationDomainType; using Sink::ApplicationDomain::Todo; auto sinkEvent = ApplicationDomainType::createEntity(resourceId); auto calcoreEvent = QSharedPointer::create(); QString uid; if (object.contains("uid")) { uid = object["uid"].toString(); } else { uid = QUuid::createUuid().toString(); } calcoreEvent->setUid(uid); auto summary = object["summary"].toString(); calcoreEvent->setSummary(summary); if (object.contains("description")) { calcoreEvent->setDescription(object["description"].toString()); } calcoreEvent->setDtStart(object["starts"].toDateTime()); calcoreEvent->setDtDue(object["due"].toDateTime()); sinkEvent.setIcal(KCalCore::ICalFormat().toICalString(calcoreEvent).toUtf8()); sinkEvent.setCalendar(calendarId); Sink::Store::create(sinkEvent).exec().waitForFinished(); } static void createCalendar(const QVariantMap &object) { using Sink::ApplicationDomain::Calendar; using Sink::ApplicationDomain::ApplicationDomainType; auto resourceId = object["resource"].toByteArray(); auto calendar = ApplicationDomainType::createEntity(resourceId); calendar.setName(object["name"].toString()); calendar.setColor(object["color"].toByteArray()); if (object.contains("contentTypes")) { calendar.setContentTypes(toByteArrayList(object["contentTypes"].toList())); } else { calendar.setContentTypes({"event", "todo"}); } Sink::Store::create(calendar).exec().waitForFinished(); auto calendarId = calendar.identifier(); iterateOverObjects(object.value("events").toList(), [calendarId, resourceId](const QVariantMap &object) { createEvent(object, calendarId, resourceId); }); iterateOverObjects(object.value("todos").toList(), [calendarId, resourceId](const QVariantMap &object) { createTodo(object, calendarId, resourceId); }); } static void createContact(const QVariantMap &object, const QByteArray &addressbookId = {}, const QByteArray &resourceId = {}) { using Sink::ApplicationDomain::ApplicationDomainType; using Sink::ApplicationDomain::Contact; QString uid; if (object.contains("uid")) { uid = object["uid"].toString(); } else { uid = QUuid::createUuid().toString(); } KContacts::Addressee addressee; addressee.setUid(uid); addressee.setGivenName(object["givenname"].toString()); addressee.setFamilyName(object["familyname"].toString()); addressee.setFormattedName(object["givenname"].toString() + " " + object["familyname"].toString()); addressee.setEmails(object["email"].toStringList()); KContacts::VCardConverter converter; auto res = resourceId; if (res.isEmpty()) { res = object["resource"].toByteArray(); } auto contact = ApplicationDomainType::createEntity(res); contact.setVcard(converter.createVCard(addressee, KContacts::VCardConverter::v3_0)); contact.setAddressbook(addressbookId); Sink::Store::create(contact).exec().waitForFinished(); } static void createAddressbook(const QVariantMap &object) { using namespace Sink::ApplicationDomain; auto resourceId = object["resource"].toByteArray(); auto addressbook = ApplicationDomainType::createEntity(resourceId); addressbook.setName(object["name"].toString()); Sink::Store::create(addressbook).exec().waitForFinished(); iterateOverObjects(object.value("contacts").toList(), [=](const QVariantMap &object) { createContact(object, addressbook.identifier(), resourceId); }); } void TestStore::setup(const QVariantMap &map) { using namespace Sink::ApplicationDomain; //Cleanup any old data const auto accounts = Sink::Store::read({}); for (const auto &account : accounts) { Sink::Store::remove(account).exec().waitForFinished(); } iterateOverObjects(map.value("accounts").toList(), [&] (const QVariantMap &object) { auto account = ApplicationDomainType::createEntity("", object["id"].toByteArray()); account.setName(object["name"].toString()); Kube::Keyring::instance()->unlock(account.identifier()); Sink::Store::create(account).exec().waitForFinished(); }); QByteArrayList resources; iterateOverObjects(map.value("resources").toList(), [&] (const QVariantMap &object) { resources << object["id"].toByteArray(); auto resource = [&] { using namespace Sink::ApplicationDomain; auto resource = ApplicationDomainType::createEntity("", object["id"].toByteArray()); if (object["type"] == "dummy") { resource.setResourceType("sink.dummy"); } else if (object["type"] == "mailtransport") { resource.setResourceType("sink.mailtransport"); resource.setProperty("testmode", true); } else if (object["type"] == "caldav") { resource.setResourceType("sink.caldav"); resource.setProperty("testmode", true); } else if (object["type"] == "carddav") { resource.setResourceType("sink.carddav"); resource.setProperty("testmode", true); } else { Q_ASSERT(false); } return resource; }(); resource.setAccount(object["account"].toByteArray()); Sink::Store::create(resource).exec().waitForFinished(); Sink::SecretStore::instance().insert(resource.identifier(), "secret"); }); iterateOverObjects(map.value("identities").toList(), [] (const QVariantMap &object) { auto identity = Sink::ApplicationDomain::Identity{}; identity.setAccount(object["account"].toByteArray()); identity.setAddress(object["address"].toString()); identity.setName(object["name"].toString()); Sink::Store::create(identity).exec().waitForFinished(); }); iterateOverObjects(map.value("folders").toList(), [] (const QVariantMap &map) { createFolder(map); }); iterateOverObjects(map.value("mails").toList(), [] (const QVariantMap &map) { createMail(map); }); iterateOverObjects(map.value("calendars").toList(), createCalendar); iterateOverObjects(map.value("addressbooks").toList(), createAddressbook); Sink::ResourceControl::flushMessageQueue(resources).exec().waitForFinished(); } void TestStore::shutdownResources() { const auto resources = Sink::Store::read({}); for (const auto &resource : resources) { Sink::ResourceControl::shutdown(resource.identifier()).exec().waitForFinished(); } } QVariant TestStore::load(const QByteArray &type, const QVariantMap &filter) { using namespace Sink::ApplicationDomain; const auto list = loadList(type, filter); if (!list.isEmpty()) { if (list.size() > 1) { qWarning() << "While loading" << type << "with filter" << filter << "; got multiple elements, but returning the first one."; } return list.first(); } return {}; } template QVariantList toVariantList(const QList &list) { QVariantList result; std::transform(list.constBegin(), list.constEnd(), std::back_inserter(result), [] (const T &m) { return QVariant::fromValue(T::Ptr::create(m)); }); Q_ASSERT(list.size() == result.size()); return result; } QVariantList TestStore::loadList(const QByteArray &type, const QVariantMap &_filter) { auto filter = _filter; using namespace Sink::ApplicationDomain; Sink::Query query; if (filter.contains("resource")) { query.resourceFilter(filter.take("resource").toByteArray()); } for (auto it = filter.begin(); it != filter.end(); ++it) { query.filter(it.key().toUtf8(), {it.value()}); } if (type == "mail") { return toVariantList(Sink::Store::read(query)); } if (type == "folder") { return toVariantList(Sink::Store::read(query)); } if (type == "resource") { return toVariantList(Sink::Store::read(query)); } if (type == "account") { return toVariantList(Sink::Store::read(query)); } Q_ASSERT(false); return {}; } QVariantMap TestStore::read(const QVariant &object) { using namespace Sink::ApplicationDomain; QVariantMap map; if (auto mail = object.value()) { map.insert("uid", mail->identifier()); map.insert("subject", mail->getSubject()); map.insert("draft", mail->getDraft()); return map; } Q_ASSERT(false); return {}; } diff --git a/views/calendar/main.qml b/views/calendar/main.qml index 2ee0344b..045c5dee 100644 --- a/views/calendar/main.qml +++ b/views/calendar/main.qml @@ -1,280 +1,284 @@ /* * Copyright (C) 2018 Christian Mollekopf, * * 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. */ import QtQuick 2.7 import QtQuick.Controls 2.0 import QtQuick.Window 2.0 import org.kube.framework 1.0 as Kube import org.kube.test 1.0 import "qml" ApplicationWindow { id: app height: Screen.desktopAvailableHeight * 0.8 width: Screen.desktopAvailableWidth * 0.8 Component.onCompleted: { var initialState = { accounts: [ { id: "account1", name: "Test Account" }, { id: "account2", name: "Test Account2 With A Very Long Name" }, ], identities: [{ account: "account1", name: "Test Identity", address: "identity@example.org" }], resources: [ { id: "caldavresource", account: "account1", type: "caldav", }, { id: "caldavresource2", account: "account2", type: "caldav", } ], calendars: [{ id: "calendar1", resource: "caldavresource", name: "Test Calendar", color: "#af1a6a", events: [ { resource: "caldavresource", summary: "Short event1!", starts: "2018-04-09T14:03:00", + organizer: "organizer@example.org", + attendees: [{email: "attendee1@example.org"}, {email: "identity@example.org"}] }, { resource: "caldavresource", summary: "Short event2!", starts: "2018-04-09T14:03:00", + organizer: "organizer@example.org", + attendees: [{email: "attendee1@example.org"}, {email: "identity@example.org"}] }, { resource: "caldavresource", summary: "Short event3!", starts: "2018-04-09T14:03:00", }, { resource: "caldavresource", summary: "Test Event1 with a waaaaaaaay to long summary. Why don't you just use the description you fool!", description: "This is test event #1", starts: "2018-04-10T14:03:00", ends: "2018-04-10T16:03:00", }, { resource: "caldavresource", summary: "Test Event2", description: "This is test event #2", starts: "2018-04-11T09:03:00", ends: "2018-04-11T14:03:00", }, { resource: "caldavresource", summary: "Test Event3", description: "This is test event #3", starts: "2018-04-11T10:00:00", ends: "2018-04-11T15:00:00", }, { resource: "caldavresource", summary: "Test Event4", description: "This is test event #4", starts: "2018-04-12T03:00:00", ends: "2018-04-14T22:00:00", }, { resource: "caldavresource", summary: "!!! Test Event5", description: "!!! This is test event #5", starts: "2018-04-22T22:00:00", ends: "2018-04-25T03:00:00", }, // Day-long events { resource: "caldavresource", summary: "Test day-long event1", description: "This is test day-long event #1", starts: "2018-04-10T00:00:00", ends: "2018-04-14T00:00:00", allDay: true, }, { resource: "caldavresource", summary: "Test day-long event2", description: "This is test day-long event #2", starts: "2018-04-11", allDay: true, }, { resource: "caldavresource", summary: "Test day-long event3", description: "This is test day-long event #3", starts: "2018-04-01T00:00:00", ends: "2018-04-13T00:00:00", allDay: true, }, { resource: "caldavresource", summary: "Test day-long event4", description: "This is test day-long event #4", starts: "2018-04-01T00:00:00", ends: "2018-04-25T00:00:00", allDay: true, }, { resource: "caldavresource", summary: "!!! Test day-long event5", description: "!!! This is test day-long event #5", starts: "2018-04-01T00:00:00", ends: "2018-04-05T00:00:00", allDay: true, }, { resource: "caldavresource", summary: "!!! Test day-long event6", description: "!!! This is test day-long event #6", starts: "2018-04-23T00:00:00", ends: "2018-04-25T00:00:00", allDay: true, }, { resource: "caldavresource", summary: "Test day-long event7", description: "This is test day-long event #7", starts: "2018-04-12", allDay: true, }, ], }, { id: "calendar2", resource: "caldavresource", name: "Test Calendar2", color: "#00cc4b" }, { id: "calendar3", resource: "caldavresource", name: "Test Calendar3 Loooooooooooooooooooooooooooooong Name", color: "#00cc4b" }, { id: "calendar4", resource: "caldavresource", name: "Test Calendar4", color: "#cc0000" }, { id: "calendar5", resource: "caldavresource", name: "Test Calendar5", color: "#cc0000" }, { id: "calendar6", resource: "caldavresource", name: "Test Calendar6", color: "#f67400" }, { id: "calendar7", resource: "caldavresource", name: "Test Calendar7", color: "#f67400" }, { id: "calendar8", resource: "caldavresource", name: "Test Calendar8", color: "#f67400" }, { id: "calendar9", resource: "caldavresource", name: "Test Calendar9", color: "#f67400" }, { id: "calendar10", resource: "caldavresource", name: "Test Calendar 10", color: "#f67400" }, { id: "calendar11", resource: "caldavresource", name: "Test Calendar11", color: "#f67400" }, { id: "calendar12", resource: "caldavresource", name: "Test Calendar12", color: "#f67400" }, { id: "calendar13", resource: "caldavresource", name: "Test Calendar13", color: "#f67400" }, { id: "calendar14", resource: "caldavresource", name: "Test Calendar14", color: "#f67400" }, { id: "calendar15", resource: "caldavresource", name: "Test Calendar15", color: "#f67400" }, { id: "calendar16", resource: "caldavresource", name: "Test Calendar16", color: "#f67400" }, { id: "account2calendar", resource: "caldavresource2", name: "Account2Calendar", color: "#f67400" }], } TestStore.setup(initialState) } View { anchors.fill: parent currentDate: "2018-04-11T13:04:03" autoUpdateDate: false } } diff --git a/views/calendar/qml/EventView.qml b/views/calendar/qml/EventView.qml index 285802d6..4abd1999 100644 --- a/views/calendar/qml/EventView.qml +++ b/views/calendar/qml/EventView.qml @@ -1,125 +1,147 @@ /* * Copyright (C) 2018 Michael Bohlender, * * 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. */ import QtQuick 2.4 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 import org.kube.framework 1.0 as Kube import "dateutils.js" as DateUtils FocusScope { id: root property var controller width: stackView.width height: stackView.height signal done() StackView { id: stackView anchors.centerIn: parent width: stackView.currentItem.implicitWidth height: stackView.currentItem.implicitHeight initialItem: eventDetails clip: true } Component { id: eventDetails Rectangle { implicitWidth: contentLayout.implicitWidth + 2 * Kube.Units.largeSpacing implicitHeight: contentLayout.implicitHeight + 2 * Kube.Units.largeSpacing color: Kube.Colors.viewBackgroundColor ColumnLayout { id: contentLayout anchors { centerIn: parent } spacing: Kube.Units.smallSpacing Kube.Heading { Layout.fillWidth: true text: controller.summary } Kube.SelectableLabel { visible: controller.allDay text: controller.start.toLocaleString(Qt.locale(), "dd. MMMM") + (DateUtils.sameDay(controller.start, controller.end) ? "" : " - " + controller.end.toLocaleString(Qt.locale(), "dd. MMMM")) } Kube.SelectableLabel { visible: !controller.allDay text: controller.start.toLocaleString(Qt.locale(), "dd. MMMM hh:mm") + " - " + (DateUtils.sameDay(controller.start, controller.end) ? controller.end.toLocaleString(Qt.locale(), "hh:mm") : controller.end.toLocaleString(Qt.locale(), "dd. MMMM hh:mm")) } Kube.SelectableLabel { visible: controller.recurring text: qsTr("repeats %1").arg(controller.recurrenceString) } Kube.SelectableLabel { text: "@" + controller.location visible: controller.location } + Kube.SelectableLabel { + text: qsTr("Organizer: %1").arg(controller.organizer) + visible: controller.organizer + } + + Flow { + visible: attendeeRepeater.count + height: contentHeight + Kube.SelectableLabel { + text: qsTr("Attending:") + visible: controller.organizer + } + Repeater { + id: attendeeRepeater + model: controller.attendees.model + delegate: Kube.Label { + text: qsTr("%1 %2").arg(model.name).arg(model.email) + (index == (attendeeRepeater.count - 1) ? "" : ",") + elide: Text.ElideRight + } + } + } + Kube.TextArea { Layout.fillWidth: true text: controller.description } Item { width: 1 height: Kube.Units.largeSpacing } RowLayout { Kube.Button { text: qsTr("Remove") onClicked: { root.controller.remove() root.done() } } Item { Layout.fillWidth: true } Kube.Button { text: qsTr("Edit") onClicked: { stackView.push(editor, StackView.Immediate) } } } } } } Component { id: editor EventEditor { controller: root.controller editMode: true onDone: root.done() } } }