Members of the KDE Community are recommended to subscribe to the kde-community mailing list at https://mail.kde.org/mailman/listinfo/kde-community to allow them to participate in important discussions and receive other important announcements

Commit a18064e9 authored by Volker Krause's avatar Volker Krause

Refactor ical generation code

It's now in its own class independent of the kmail plugin code, making
it much easier to unit test.
parent bc0f9c8e
......@@ -68,3 +68,8 @@ ecm_add_test(
NAME_PREFIX "messageviewerplugins-"
LINK_LIBRARIES Qt5::Test semantic_extractor
)
ecm_add_test(
calendarhandlertest.cpp
NAME_PREFIX "messageviewerplugins-"
LINK_LIBRARIES Qt5::Test semantic_extractor
)
BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT
DTSTAMP:20171227T111649Z
CREATED:20171227T111649Z
UID:1b22236a-21ff-4885-8c99-b3b2bbca062c
LAST-MODIFIED:20171227T111649Z
DESCRIPTION:Booking reference: XXX007
SUMMARY:Flight AB 8075 from HEL to TXL
LOCATION:Helsinki
DTSTART;TZID=Europe/Helsinki:20170920T150500
DTEND;TZID=Europe/Berlin:20170920T160000
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
[
{
"@context": "http://schema.org",
"@type": "FlightReservation",
"airplaneSeat": "16E",
"airplaneSeatClass": {
"@type": "AirplaneSeatClass",
"name": "P"
},
"boardingGroup": "C",
"boardingPolicy": "GroupBoardingPolicy",
"modifiedTime": "2017-09-20T08:14:39+02:00",
"modifyReservationUrl": "https://www.airberlin.com/de-DE/cockpit/index/index/bookingNo/XXX007/lastname/DOE/submit/1",
"reservationFor": {
"@type": "Flight",
"airline": {
"@type": "Airline",
"iataCode": "AB",
"image": "https://www.airberlin.com/site/affiliate/bannerintegration/boardingpass/ab_ow_pos_RGB.png",
"name": "airberlin"
},
"arrivalAirport": {
"@type": "Airport",
"iataCode": "TXL",
"name": "Berlin (Tegel)"
},
"arrivalTime": "2017-09-20T16:00:00+02:00",
"boardingTime": "2017-09-20T14:25:00+03:00",
"departureAirport": {
"@type": "Airport",
"iataCode": "HEL",
"name": "Helsinki"
},
"departureGate": "",
"departureTime": "2017-09-20T15:05:00+03:00",
"flightNumber": "8075"
},
"reservationNumber": "XXX007",
"reservationStatus": "http://schema.org/Confirmed",
"ticketDownloadUrl": "https://m.airberlin.com/ckbc/pnr/XXX007/ln/DOE/cpid/94482631/fn/JOHN",
"ticketNumber": "1234567890123",
"ticketToken": "https://checkin.airberlin.com/app/barcode.fly?pnr=XXX007&ln=DOE&cpid=12345678&fn=JOHN",
"underName": {
"@type": "Person",
"name": "JOHN DOE"
}
}
]
BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT
DTSTAMP:20171227T111649Z
CREATED:20171227T111649Z
UID:1b22236a-21ff-4885-8c99-b3b2bbca062c
LAST-MODIFIED:20171227T111649Z
DESCRIPTION:Booking reference: 1234567890
SUMMARY:Hotel reservation: Glo Hotel Sello
LOCATION:Leppävaarankatu 1\, 02600 Espoo\, Finland
DTSTART;VALUE=DATE:20170919
DTEND;VALUE=DATE:20170921
TRANSP:TRANSPARENT
END:VEVENT
END:VCALENDAR
[
{
"@context": "http://schema.org",
"@type": "LodgingReservation",
"bookingAgent": {
"@type": "Organization",
"name": "Booking.com",
"url": "https://www.booking.com/"
},
"bookingTime": "2017-09-08T14:38:31+03:00",
"cancelReservationUrl": "https://secure.booking.com/mybooking.en-gb.html?auth_key=magic&source=conf_metadata&pbsource=email_cancel",
"checkinDate": "2017-09-19T15:00:00+03:00",
"checkoutDate": "2017-09-20T12:00:00+03:00",
"confirmReservationUrl": "/confirmation.en-gb.html?aid=123456;auth_key=magic&&source=conf_metadata",
"modifiedTime": "2017-09-08T14:38:31+03:00",
"modifyReservationUrl": "https://secure.booking.com/mybooking.en-gb.html?aid=123456;auth_key=magic&&source=conf_metadata&pbsource=conf_email_modify",
"numAdults": 2,
"priceCurrency": "EUR",
"reservationFor": {
"@type": "LodgingBusiness",
"address": {
"@type": "PostalAddress",
"addressCountry": "Finland",
"addressLocality": "Espoo",
"addressRegion": "",
"postalCode": "02600",
"streetAddress": "Leppävaarankatu 1"
},
"contactPoint": "Reception",
"image": "https://q.bstatic.com/images/hotel/max1280x900/285/28526193.jpg",
"name": "Glo Hotel Sello",
"telephone": "+358101234567",
"url": "https://www.booking.com/hotel/fi/palace-sello.html?aid=123456&label=postbooking_confemail"
},
"reservationNumber": "1234567890",
"reservationStatus": "http://schema.org/Confirmed",
"underName": {
"@type": "Person",
"email": "john.doe@email.com",
"name": "John Doe"
},
"url": "https://secure.booking.com/mybooking.en-gb.html?aid=123456;auth_key=magic&&source=conf_metadata&pbsource=conf_email_modify"
}
]
BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT
DTSTAMP:20171227T111649Z
CREATED:20171227T111649Z
UID:1b22236a-21ff-4885-8c99-b3b2bbca062c
LAST-MODIFIED:20171227T111649Z
DESCRIPTION:Coach: 17\nSeat: 62\nBooking reference: XXX007
SUMMARY:Train 5186 from Nîmes Gare to Lyon Part-Dieu
LOCATION:Nîmes Gare
DTSTART;TZID="UTC+02:00":20170929T182600
DTEND;TZID="UTC+02:00":20170929T195200
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
[
{
"@context": "http://schema.org",
"@type": "TrainReservation",
"bookingAgent": {
"@type": "Organization",
"email": "someone@trainline.fr",
"name": "Trainline"
},
"bookingTime": "2017-09-19T13:42:00+02:00",
"cancelReservationUrl": "https://www.trainline.fr/tickets",
"modifiedTime": "2017-09-21T09:19:38+02:00",
"modifyReservationUrl": "https://www.trainline.fr/tickets",
"reservationFor": {
"@type": "TrainTrip",
"arrivalStation": {
"@type": "TrainStation",
"geo": {
"@type": "GeoCoordinates",
"latitude": 45.760559,
"longitude": 4.859355
},
"name": "Lyon Part-Dieu"
},
"arrivalTime": "2017-09-29T19:52:00+02:00",
"departureStation": {
"@type": "TrainStation",
"geo": {
"@type": "GeoCoordinates",
"latitude": 43.832291,
"longitude": 4.365845
},
"name": "Nîmes Gare"
},
"departureTime": "2017-09-29T18:26:00+02:00",
"trainCompany": {
"@type": "Organization",
"name": "SNCF"
},
"trainName": "TGV",
"trainNumber": "5186"
},
"reservationNumber": "XXX007",
"reservationStatus": "http://schema.org/ReservationConfirmed",
"reservedTicket": {
"@type": "Ticket",
"ticketToken": "aztecCode:somerandomdata DOE JOHN111110 00000",
"ticketedSeat": {
"@type": "Seat",
"seatNumber": "62",
"seatSection": "17",
"seatingType": "Economy"
},
"underName": {
"@type": "Person",
"name": "John Doe"
}
},
"underName": {
"@type": "Person",
"name": "John Doe"
},
"url": "https://www.trainline.fr/tickets"
}
]
/*
Copyright (c) 2017 Volker Krause <vkrause@kde.org>
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 "calendarhandler.h"
#include "extractorpostprocessor.h"
#include "jsonlddocument.h"
#include <KCalCore/ICalFormat>
#include <KCalCore/MemoryCalendar>
#include <QDebug>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QObject>
#include <QTest>
using namespace KCalCore;
class CalendarHandlerTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testCreateEvent_data()
{
QTest::addColumn<QString>("jsonFile");
QTest::addColumn<QString>("icalFile");
QDir dir(QStringLiteral(SOURCE_DIR "/calendarhandlerdata"));
const auto lst = dir.entryList(QStringList(QStringLiteral("*.json")), QDir::Files | QDir::Readable | QDir::NoSymLinks);
for (const auto &file : lst) {
const auto refFile = dir.path() + QLatin1Char('/') + file.left(file.size() - 4) + QStringLiteral("ics");
if (!QFile::exists(refFile)) {
qDebug() << "reference file" << refFile << "does not exist, skipping test file" << file;
continue;
}
QTest::newRow(file.toLatin1()) << QString(dir.path() + QLatin1Char('/') + file) << refFile;
}
}
void testCreateEvent()
{
QFETCH(QString, jsonFile);
QFETCH(QString, icalFile);
QFile f(jsonFile);
QVERIFY(f.open(QFile::ReadOnly));
const auto inArray = QJsonDocument::fromJson(f.readAll()).array();
QVERIFY(!inArray.isEmpty());
const auto preData = JsonLdDocument::fromJson(inArray);
QCOMPARE(inArray.size(), preData.size());
ExtractorPostprocessor postproc;
postproc.process(preData);
QCOMPARE(inArray.size(), postproc.result().size());
MemoryCalendar::Ptr refCal(new MemoryCalendar(QTimeZone{}));
ICalFormat format;
format.load(refCal, icalFile);
const auto refEvents = refCal->rawEvents(KCalCore::EventSortStartDate, KCalCore::SortDirectionAscending);
QCOMPARE(refEvents.size(), inArray.size());
for (int i = 0; i < inArray.size(); ++i) {
Event::Ptr newEvent(new Event);
CalendarHandler::fillEvent(postproc.result().at(i), newEvent);
// sync volatile fields, we only care for differences elsewhere
const auto refEvent = refEvents.at(i);
newEvent->setUid(refEvent->uid());
newEvent->setLastModified(refEvent->lastModified());
newEvent->setCreated(refEvent->created());
if (*newEvent != *refEvent) {
qDebug().noquote() << "Actual: " << format.toICalString(newEvent);
qDebug().noquote() << "Expected: " << format.toICalString(refEvent);
}
QCOMPARE(newEvent->dtStart(), refEvent->dtStart());
QCOMPARE(newEvent->dtEnd(), refEvent->dtEnd());
QVERIFY(*newEvent == *refEvent);
}
}
};
QTEST_APPLESS_MAIN(CalendarHandlerTest)
#include "calendarhandlertest.moc"
......@@ -8,6 +8,7 @@ configure_file(config-semantic.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-semant
# static lib for use by unit test
set(semantic_lib_srcs
airportdb/airportdb.cpp
calendarhandler.cpp
datatypes.cpp
extractor.cpp
extractorengine.cpp
......@@ -22,7 +23,7 @@ qt5_add_resources(semantic_lib_srcs extractors/extractors.qrc)
ecm_qt_declare_logging_category(semantic_lib_srcs HEADER semantic_debug.h IDENTIFIER SEMANTIC_LOG CATEGORY_NAME org.kde.pim.messageviewer.semantic)
add_library(semantic_extractor STATIC ${semantic_lib_srcs})
set_target_properties(semantic_extractor PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_link_libraries(semantic_extractor PUBLIC Qt5::Core KF5::Mime PRIVATE Qt5::Qml)
target_link_libraries(semantic_extractor PUBLIC Qt5::Core KF5::Mime KF5::CalendarCore PRIVATE Qt5::Qml KF5::I18n)
if (HAVE_POPPLER)
target_link_libraries(semantic_extractor PRIVATE Poppler::Qt5)
endif()
......
/*
Copyright (c) 2017 Volker Krause <vkrause@kde.org>
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 "calendarhandler.h"
#include "datatypes.h"
#include "jsonlddocument.h"
#include "semantic_debug.h"
#include <KLocalizedString>
using namespace KCalCore;
void CalendarHandler::fillEvent(const QVariant &reservation, const KCalCore::Event::Ptr &event)
{
const int typeId = reservation.userType();
if (typeId == qMetaTypeId<FlightReservation>()) {
return fillFlightReservation(reservation, event);
} else if (typeId == qMetaTypeId<LodgingReservation>()) {
return fillLodgingReservation(reservation, event);
} else if (typeId == qMetaTypeId<TrainReservation>()) {
return fillTrainReservation(reservation, event);
}
}
void CalendarHandler::fillFlightReservation(const QVariant &reservation, const KCalCore::Event::Ptr &event)
{
const auto flight = JsonLdDocument::readProperty(reservation, "reservationFor");
const auto airline = JsonLdDocument::readProperty(flight, "airline");
const auto depPort = JsonLdDocument::readProperty(flight, "departureAirport");
const auto arrPort = JsonLdDocument::readProperty(flight, "arrivalAirport");
if (flight.isNull() || airline.isNull() || depPort.isNull() || arrPort.isNull()) {
qCDebug(SEMANTIC_LOG) << "got invalid flight reservation";
return;
}
event->setSummary(i18n("Flight %1 %2 from %3 to %4",
JsonLdDocument::readProperty(airline, "iataCode").toString(),
JsonLdDocument::readProperty(flight, "flightNumber").toString(),
JsonLdDocument::readProperty(depPort, "iataCode").toString(),
JsonLdDocument::readProperty(arrPort, "iataCode").toString()
));
event->setLocation(JsonLdDocument::readProperty(depPort, "name").toString());
event->setDtStart(JsonLdDocument::readProperty(flight, "departureTime").toDateTime());
event->setDtEnd(JsonLdDocument::readProperty(flight, "arrivalTime").toDateTime());
event->setAllDay(false);
event->setDescription(i18n("Booking reference: %1",
JsonLdDocument::readProperty(reservation, "reservationNumber").toString()
));
}
void CalendarHandler::fillTrainReservation(const QVariant &reservation, const KCalCore::Event::Ptr &event)
{
const auto trip = JsonLdDocument::readProperty(reservation, "reservationFor");
const auto depStation = JsonLdDocument::readProperty(trip, "departureStation");
const auto arrStation = JsonLdDocument::readProperty(trip, "arrivalStation");
if (trip.isNull() || depStation.isNull() || arrStation.isNull()) {
return;
}
event->setSummary(i18n("Train %1 from %2 to %3",
JsonLdDocument::readProperty(trip, "trainNumber").toString(),
JsonLdDocument::readProperty(depStation, "name").toString(),
JsonLdDocument::readProperty(arrStation, "name").toString()
));
event->setLocation(JsonLdDocument::readProperty(depStation, "name").toString());
event->setDtStart(JsonLdDocument::readProperty(trip, "departureTime").toDateTime());
event->setDtEnd(JsonLdDocument::readProperty(trip, "arrivalTime").toDateTime());
event->setAllDay(false);
QStringList desc;
auto s = JsonLdDocument::readProperty(trip, "departurePlatform").toString();
if (!s.isEmpty()) {
desc.push_back(i18n("Departure platform: %1", s));
}
const auto ticket = JsonLdDocument::readProperty(reservation, "reservedTicket");
const auto seat = JsonLdDocument::readProperty(ticket, "ticketedSeat");
s = JsonLdDocument::readProperty(seat, "seatSection").toString();
if (!s.isEmpty()) {
desc.push_back(i18n("Coach: %1", s));
}
s = JsonLdDocument::readProperty(seat, "seatNumber").toString();
if (!s.isEmpty()) {
desc.push_back(i18n("Seat: %1", s));
}
s = JsonLdDocument::readProperty(trip, "arrivalPlatform").toString();
if (!s.isEmpty()) {
desc.push_back(i18n("Arrival platform: %1", s));
}
s = JsonLdDocument::readProperty(reservation, "reservationNumber").toString();
if (!s.isEmpty()) {
desc.push_back(i18n("Booking reference: %1", s));
}
event->setDescription(desc.join(QLatin1Char('\n')));
}
void CalendarHandler::fillLodgingReservation(const QVariant &reservation, const KCalCore::Event::Ptr &event)
{
const auto lodgingBusiness = JsonLdDocument::readProperty(reservation, "reservationFor");
const auto address = JsonLdDocument::readProperty(lodgingBusiness, "address");
if (lodgingBusiness.isNull() || address.isNull()) {
return;
}
event->setSummary(i18n("Hotel reservation: %1",
JsonLdDocument::readProperty(lodgingBusiness, "name").toString()
));
event->setLocation(i18n("%1, %2 %3, %4",
JsonLdDocument::readProperty(address, "streetAddress").toString(),
JsonLdDocument::readProperty(address, "postalCode").toString(),
JsonLdDocument::readProperty(address, "addressLocality").toString(),
JsonLdDocument::readProperty(address, "addressCountry").toString()
));
event->setDtStart(QDateTime(JsonLdDocument::readProperty(reservation, "checkinDate").toDate(), QTime()));
event->setDtEnd(QDateTime(JsonLdDocument::readProperty(reservation, "checkoutDate").toDate(), QTime()));
event->setAllDay(true);
event->setDescription(i18n("Booking reference: %1",
JsonLdDocument::readProperty(reservation, "reservationNumber").toString()
));
event->setTransparency(Event::Transparent);
}
/*
Copyright (c) 2017 Volker Krause <vkrause@kde.org>
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.
*/
#ifndef CALENDARHANDLER_H
#define CALENDARHANDLER_H
#include <KCalCore/Event>
class QVariant;
/** Methods for converting between ical events and JSON-LD booking data. */
class CalendarHandler
{
public:
static void fillEvent(const QVariant &reservation, const KCalCore::Event::Ptr &event);
private:
static void fillFlightReservation(const QVariant &reservation, const KCalCore::Event::Ptr &event);
static void fillTrainReservation(const QVariant &reservation, const KCalCore::Event::Ptr &event);
static void fillLodgingReservation(const QVariant &reservation, const KCalCore::Event::Ptr &event);
};
#endif // CALENDARHANDLER_H
......@@ -18,6 +18,7 @@
*/
#include "semanticurlhandler.h"
#include "calendarhandler.h"
#include "datatypes.h"
#include "jsonlddocument.h"
#include "semanticmemento.h"
......@@ -237,133 +238,15 @@ void SemanticUrlHandler::showCalendar(const QDate &date) const
korgIface->call(QStringLiteral("showDate"), date);
}
KCalCore::Event::Ptr SemanticUrlHandler::eventForReservation(const QVariant &reservation) const
{
const int reservationId = reservation.userType();
if (reservationId == qMetaTypeId<FlightReservation>()) {
return eventForFlightReservation(reservation);
} else if (reservationId == qMetaTypeId<LodgingReservation>()) {
return eventForLodgingReservation(reservation);
} else if (reservationId == qMetaTypeId<TrainReservation>()) {
return eventForTrainReservation(reservation);
}
return {};
}
KCalCore::Event::Ptr SemanticUrlHandler::eventForFlightReservation(const QVariant &reservation) const
{
using namespace KCalCore;
const auto flight = JsonLdDocument::readProperty(reservation, "reservationFor");
const auto airline = JsonLdDocument::readProperty(flight, "airline");
const auto depPort = JsonLdDocument::readProperty(flight, "departureAirport");
const auto arrPort = JsonLdDocument::readProperty(flight, "arrivalAirport");
if (flight.isNull() || airline.isNull() || depPort.isNull() || arrPort.isNull()) {
return {};
}
Event::Ptr event(new Event);
event->setSummary(i18n("Flight %1 %2 from %3 to %4",
JsonLdDocument::readProperty(airline, "iataCode").toString(),
JsonLdDocument::readProperty(flight, "flightNumber").toString(),
JsonLdDocument::readProperty(depPort, "iataCode").toString(),
JsonLdDocument::readProperty(arrPort, "iataCode").toString()
));
event->setLocation(JsonLdDocument::readProperty(depPort, "name").toString());
event->setDtStart(JsonLdDocument::readProperty(flight, "departureTime").toDateTime());
event->setDtEnd(JsonLdDocument::readProperty(flight, "arrivalTime").toDateTime());
event->setAllDay(false);
event->setDescription(i18n("Booking reference: %1",
JsonLdDocument::readProperty(reservation, "reservationNumber").toString()
));
return event;
}
KCalCore::Event::Ptr SemanticUrlHandler::eventForLodgingReservation(const QVariant &reservation) const
{
using namespace KCalCore;
const auto lodgingBusiness = JsonLdDocument::readProperty(reservation, "reservationFor");
const auto address = JsonLdDocument::readProperty(lodgingBusiness, "address");
if (lodgingBusiness.isNull() || address.isNull()) {
return {};
}
Event::Ptr event(new Event);
event->setSummary(i18n("Hotel reservation: %1",
JsonLdDocument::readProperty(lodgingBusiness, "name").toString()
));
event->setLocation(i18n("%1, %2 %3, %4",
JsonLdDocument::readProperty(address, "streetAddress").toString(),
JsonLdDocument::readProperty(address, "postalCode").toString(),
JsonLdDocument::readProperty(address, "addressLocality").toString(),
JsonLdDocument::readProperty(address, "addressCountry").toString()
));
event->setDtStart(QDateTime(JsonLdDocument::readProperty(reservation, "checkinDate").toDate(), QTime()));
event->setDtEnd(QDateTime(JsonLdDocument::readProperty(reservation, "checkoutDate").toDate(), QTime(23, 59, 59)));
event->setAllDay(true);
event->setDescription(i18n("Booking reference: %1",
JsonLdDocument::readProperty(reservation, "reservationNumber").toString()
));
event->setTransparency(Event::Transparent);
return event;
}
KCalCore::Event::Ptr SemanticUrlHandler::eventForTrainReservation(const QVariant &reservation) const
void SemanticUrlHandler::addToCalendar(SemanticMemento *memento) const
{
using namespace KCalCore;
const auto trip = JsonLdDocument::readProperty(reservation, "reservationFor");
const auto depStation = JsonLdDocument::readProperty(trip, "departureStation");
const auto arrStation = JsonLdDocument::readProperty(trip, "arrivalStation");
if (trip.isNull() || depStation.isNull() || arrStation.isNull()) {
return {};
}
Event::Ptr event(new Event);
event->setSummary(i18n("Train %1 from %2 to %3",
JsonLdDocument::readProperty(trip, "trainNumber").toString(),
JsonLdDocument::readProperty(depStation, "name").toString(),
JsonLdDocument::readProperty(arrStation, "name").toString()
));
event->setLocation(JsonLdDocument::readProperty(depStation, "name").toString());
event->setDtStart(JsonLdDocument::readProperty(trip, "departureTime").toDateTime());
event->setDtEnd(JsonLdDocument::readProperty(trip, "arrivalTime").toDateTime());
event->setAllDay(false);