diff --git a/plugins/messageviewer/bodypartformatter/autotests/data/boarding-pass-pkpass.mbox.html b/plugins/messageviewer/bodypartformatter/autotests/data/boarding-pass-pkpass.mbox.html index 7d45437d..bd05930d 100644 --- a/plugins/messageviewer/bodypartformatter/autotests/data/boarding-pass-pkpass.mbox.html +++ b/plugins/messageviewer/bodypartformatter/autotests/data/boarding-pass-pkpass.mbox.html @@ -1,133 +1,137 @@
Add reservation to calendar.
- + - +
ZRH + ZRH
Show location on map
+
TXL + TXL
Show location on map
+
9/15/17 LX 962
Boarding: 8:25 PM CEST Seat: 10E
Have a nice flight!
diff --git a/plugins/messageviewer/bodypartformatter/itinerary/itineraryurlhandler.cpp b/plugins/messageviewer/bodypartformatter/itinerary/itineraryurlhandler.cpp index bef0e283..d99cd9e9 100644 --- a/plugins/messageviewer/bodypartformatter/itinerary/itineraryurlhandler.cpp +++ b/plugins/messageviewer/bodypartformatter/itinerary/itineraryurlhandler.cpp @@ -1,418 +1,343 @@ /* Copyright (c) 2017 Volker Krause 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 "itineraryurlhandler.h" #include "itinerarymemento.h" #include "itinerary_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; ItineraryUrlHandler::ItineraryUrlHandler() { m_appPath = QStandardPaths::findExecutable(QStringLiteral("itinerary")); } QString ItineraryUrlHandler::name() const { return QString::fromUtf8(staticMetaObject.className()); } static bool canAddToCalendar(ItineraryMemento *m) { for (const auto &d : m->data()) { if (JsonLd::isA(d.reservations.at(0))) { const auto f = d.reservations.at(0).value().reservationFor().value(); if (f.departureTime().isValid() && f.arrivalTime().isValid()) { return true; } continue; } else if (SortUtil::startDateTime(d.reservations.at(0)).isValid()) { return true; } } return false; } bool ItineraryUrlHandler::handleClick(MessageViewer::Viewer *viewerInstance, MimeTreeParser::Interface::BodyPart *part, const QString &path) const { Q_UNUSED(viewerInstance); if (path == QLatin1String("semanticAction")) { const auto m = memento(part); if (!m || !m->hasData()) { qCWarning(ITINERARY_LOG) << "sementic action: data not found"; return true; } handleContextMenuRequest(part, path, QCursor::pos()); return true; } if (path.startsWith(QLatin1String("semanticExpand?"))) { auto idx = path.midRef(15).toInt(); auto m = memento(part); m->toggleExpanded(idx); const auto nodeHelper = part->nodeHelper(); emit nodeHelper->update(MimeTreeParser::Delayed); return true; } return false; } -static QString escapePlaceName(const QString &name) -{ - return QString(name).replace(QLatin1Char('&'), QLatin1String("&&")); // avoid & being turned into an action accelerator; -} - -static void addGoToMapAction(QMenu *menu, const GeoCoordinates &geo, const QString &placeName, int zoom = 17) -{ - if (geo.isValid()) { - auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("map-symbolic")), i18n("Show \'%1\' On Map", escapePlaceName(placeName))); - QObject::connect(action, &QAction::triggered, menu, [geo, zoom]() { - QUrl url; - url.setScheme(QStringLiteral("https")); - url.setHost(QStringLiteral("www.openstreetmap.org")); - url.setPath(QStringLiteral("/")); - const QString fragment = QLatin1String("map=") + QString::number(zoom) - + QLatin1Char('/') + QString::number(geo.latitude()) - + QLatin1Char('/') + QString::number(geo.longitude()); - url.setFragment(fragment); - QDesktopServices::openUrl(url); - }); - } -} - -static void addGoToMapAction(QMenu *menu, const PostalAddress &addr, const QString &placeName) -{ - if (!addr.addressLocality().isEmpty()) { - auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("map-symbolic")), i18n("Show \'%1\' On Map", escapePlaceName(placeName))); - QObject::connect(action, &QAction::triggered, menu, [addr]() { - QUrl url; - url.setScheme(QStringLiteral("https")); - url.setHost(QStringLiteral("www.openstreetmap.org")); - url.setPath(QStringLiteral("/search")); - const QString queryString = addr.streetAddress() + QLatin1String(", ") - + addr.postalCode() + QLatin1Char(' ') - + addr.addressLocality() + QLatin1String(", ") - + addr.addressCountry(); - QUrlQuery query; - query.addQueryItem(QStringLiteral("query"), queryString); - url.setQuery(query); - QDesktopServices::openUrl(url); - }); - } -} - -static void addGoToMapAction(QMenu *menu, const QVariant &place, QSet &places) -{ - const auto name = LocationUtil::name(place); - if (places.contains(name)) { - return; - } - places.insert(name); - - const auto geo = LocationUtil::geo(place); - const auto zoom = JsonLd::isA(place) ? 12 : 17; - if (geo.isValid()) { - addGoToMapAction(menu, geo, name, zoom); - } else { - addGoToMapAction(menu, LocationUtil::address(place), name); - } -} - bool ItineraryUrlHandler::handleContextMenuRequest(MimeTreeParser::Interface::BodyPart *part, const QString &path, const QPoint &p) const { Q_UNUSED(part); if (path != QLatin1String("semanticAction")) { return false; } const auto m = memento(part); if (!m || !m->hasData()) { return false; } const auto date = dateForReservation(m); QMenu menu; QAction *action = nullptr; if (date.isValid()) { action = menu.addAction(QIcon::fromTheme(QStringLiteral("view-calendar")), i18n("Show Calendar")); QObject::connect(action, &QAction::triggered, this, [this, date](){ showCalendar(date); }); } action = menu.addAction(QIcon::fromTheme(QStringLiteral("appointment-new")), i18n("Add To Calendar")); action->setEnabled(canAddToCalendar(m)); QObject::connect(action, &QAction::triggered, this, [this, m](){ addToCalendar(m); }); - QSet places; - for (const auto &d : m->data()) { - const auto res = d.reservations.at(0); // for multi-traveler reservations all subsequent ones are equal regarding what we are interested here - if (LocationUtil::isLocationChange(res)) { - const auto dep = LocationUtil::departureLocation(res); - addGoToMapAction(&menu, dep, places); - const auto arr = LocationUtil::arrivalLocation(res); - addGoToMapAction(&menu, arr, places); - } else { - const auto loc = LocationUtil::location(res); - addGoToMapAction(&menu, loc, places); - } - } - if (!m_appPath.isEmpty()) { menu.addSeparator(); action = menu.addAction(QIcon::fromTheme(QStringLiteral("map-globe")), i18n("Import into KDE Itinerary")); QObject::connect(action, &QAction::triggered, this, [this, part]() { openInApp(part); }); } QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect"), QStringLiteral("org.kde.kdeconnect.daemon"), QStringLiteral( "devices")); msg.setArguments({true, true}); QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); reply.waitForFinished(); if (reply.isValid()) { for (const QString &deviceId : reply.value()) { QDBusInterface deviceIface(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + deviceId, QStringLiteral("org.kde.kdeconnect.device")); QDBusReply pluginReply = deviceIface.call(QStringLiteral("hasPlugin"), QLatin1String("kdeconnect_share")); if (pluginReply.value()) { action = menu.addAction(QIcon::fromTheme(QStringLiteral("kdeconnect")), i18n("Send to %1", deviceIface.property("name").toString())); QObject::connect(action, &QAction::triggered, this, [this, part, deviceId]() { openWithKDEConnect(part, deviceId); }); } } } menu.exec(p); return true; } QString ItineraryUrlHandler::statusBarMessage(MimeTreeParser::Interface::BodyPart *part, const QString &path) const { Q_UNUSED(part); if (path == QLatin1String("semanticAction")) { return i18n("Add reservation to your calendar."); } return {}; } ItineraryMemento *ItineraryUrlHandler::memento(MimeTreeParser::Interface::BodyPart *part) const { const auto node = part->content()->topLevel(); const auto nodeHelper = part->nodeHelper(); if (!nodeHelper || !node) { return nullptr; } return dynamic_cast(nodeHelper->bodyPartMemento(node->topLevel(), ItineraryMemento::identifier())); } QDate ItineraryUrlHandler::dateForReservation(ItineraryMemento *memento) const { for (const auto &d : memento->data()) { const auto dt = SortUtil::startDateTime(d.reservations.at(0)); if (dt.isValid()) { return dt.date(); } } return {}; } void ItineraryUrlHandler::showCalendar(const QDate &date) const { // ensure KOrganizer or Kontact are running if (KontactInterface::PimUniqueApplication::activateApplication(QLatin1String("korganizer"))) { // select the date of the reservation QDBusInterface korgIface(QStringLiteral("org.kde.korganizer"), QStringLiteral("/Calendar"), QStringLiteral("org.kde.Korganizer.Calendar"), QDBusConnection::sessionBus()); if (!korgIface.isValid()) { qCWarning(ITINERARY_LOG) << "Calendar interface is not valid! " << korgIface.lastError().message(); return; } korgIface.call(QStringLiteral("showEventView")); korgIface.call(QStringLiteral("showDate"), date); } } static void attachPass(const KCalendarCore::Event::Ptr &event, const QVector &reservations, ItineraryMemento *memento) { for (const auto &reservation : reservations) { if (!JsonLd::canConvert(reservation)) { return; } const auto res = JsonLd::convert(reservation); const auto data = memento->rawPassData(res.pkpassPassTypeIdentifier(), res.pkpassSerialNumber()); if (data.isEmpty()) { return; } event->deleteAttachments(QStringLiteral("application/vnd.apple.pkpass")); using namespace KCalendarCore; Attachment att(data.toBase64(), QStringLiteral("application/vnd.apple.pkpass")); att.setLabel(i18n("Boarding Pass")); // TODO add passenger name after string freeze is lifted event->addAttachment(att); } } void ItineraryUrlHandler::addToCalendar(ItineraryMemento *memento) const { using namespace KCalendarCore; const auto calendar = CalendarSupport::calendarSingleton(true); for (const auto &d : memento->data()) { auto event = d.event; if (!event) { event.reset(new KCalendarCore::Event); CalendarHandler::fillEvent(d.reservations, event); if (!event->dtStart().isValid() || !event->dtEnd().isValid() || event->summary().isEmpty()) { continue; } attachPass(event, d.reservations, memento); calendar->addEvent(event); } else { event->startUpdates(); CalendarHandler::fillEvent(d.reservations, event); event->endUpdates(); attachPass(event, d.reservations, memento); calendar->modifyIncidence(event); } } } void ItineraryUrlHandler::openInApp(MimeTreeParser::Interface::BodyPart *part) const { const auto fileName = createItineraryFile(part); QProcess::startDetached(m_appPath, {fileName}); } void ItineraryUrlHandler::openWithKDEConnect(MimeTreeParser::Interface::BodyPart *part, const QString &deviceId) const { const auto fileName = createItineraryFile(part); QDBusInterface remoteApp(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/MainApplication"), QStringLiteral("org.qtproject.Qt.QCoreApplication")); QVersionNumber kdeconnectVersion = QVersionNumber::fromString(remoteApp.property("applicationVersion").toString()); QString method; if (kdeconnectVersion >= QVersionNumber(1, 4, 0)) { method = QStringLiteral("openFile"); } else { method = QStringLiteral("shareUrl"); } QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + deviceId + QStringLiteral("/share"), QStringLiteral("org.kde.kdeconnect.device.share"), method); msg.setArguments({QUrl::fromLocalFile(fileName).toString()}); QDBusConnection::sessionBus().send(msg); } QString ItineraryUrlHandler::createItineraryFile(MimeTreeParser::Interface::BodyPart *part) const { QTemporaryFile f(QStringLiteral("XXXXXX.itinerary")); if (!f.open()) { qCWarning(ITINERARY_LOG) << "Failed to open temporary file:" << f.errorString(); return {}; } f.close(); part->nodeHelper()->addTempFile(f.fileName()); f.setAutoRemove(false); KItinerary::File file(f.fileName()); if (!file.open(KItinerary::File::Write)) { qCWarning(ITINERARY_LOG) << "Failed to open itinerary bundle file:" << file.errorString(); return {}; } const auto m = memento(part); // add reservations const auto extractedData = m->data(); for (const auto &d : extractedData) { for (const auto &res : d.reservations) { file.addReservation(res); } } // add pkpass attachments for (const auto &passData : m->passData()) { file.addPass(KItinerary::File::passId(passData.passTypeIdentifier, passData.serialNumber), passData.rawData); } // add documents for (const auto &docData : m->documentData()) { file.addDocument(docData.docId, docData.docInfo, docData.rawData); } return f.fileName(); } diff --git a/plugins/messageviewer/bodypartformatter/itinerary/templates/busreservation.html b/plugins/messageviewer/bodypartformatter/itinerary/templates/busreservation.html index 136d5411..83b93a2f 100644 --- a/plugins/messageviewer/bodypartformatter/itinerary/templates/busreservation.html +++ b/plugins/messageviewer/bodypartformatter/itinerary/templates/busreservation.html @@ -1,54 +1,56 @@
{{ trip.departureBusStop.name }} {% if trip.departurePlatform %} [{{ trip.departurePlatform }}] {% endif %} + {% with trip.departureBusStop as location %}{% include "org.kde.messageviewer/itinerary/location.html" %}{% endwith %} {{ trip.arrivalBusStop.name }} {% if trip.arrivalPlatform %} [{{ trip.arrivalPlatform }}] {% endif %} + {% with trip.arrivalBusStop as location %}{% include "org.kde.messageviewer/itinerary/location.html" %}{% endwith %}
{{ trip.departureTime|formatDateTime }} {% if trip.busName and trip.busNumber %} {{ trip.busName }}
{{ trip.busNumber }} {% else %} {{ trip.busName }} {{ trip.busNumber }} {% endif %}
{{ trip.arrivalTime|formatDateTime }}
{% if elem.reservations.0.ticketToken %} {% if elem.state.expanded %} {% else %} {% endif %} {% endif %} {% if res.reservedTicket.ticketedSeat.seatNumber %} {% i18n "Seat: %1" res.reservedTicket.ticketedSeat.seatNumber %} {% endif %}
{% include "org.kde.messageviewer/itinerary/actions.html" %}
{% if elem.state.expanded %}
{% endif %} diff --git a/plugins/messageviewer/bodypartformatter/itinerary/templates/event.html b/plugins/messageviewer/bodypartformatter/itinerary/templates/event.html index 4fe3ec57..9222d5fc 100644 --- a/plugins/messageviewer/bodypartformatter/itinerary/templates/event.html +++ b/plugins/messageviewer/bodypartformatter/itinerary/templates/event.html @@ -1,11 +1,14 @@
{{ trip.name }}
{{ trip.startDate|formatDateTime }}
- {{ trip.location.name }}
- {{ trip.location.address|formatAddress|safe }} + {{ trip.location.name }} + {% with trip.location as location %}{% include "org.kde.messageviewer/itinerary/location.html" %}{% endwith %} + {% if not trip.location.address.isEmpty %} +
{{ trip.location.address|formatAddress|safe }} + {% endif %} {% if res.underName.name %}
{% i18n "Under Name: %1" res.underName.name %}
{% endif %}
{% include "org.kde.messageviewer/itinerary/actions.html" %}
diff --git a/plugins/messageviewer/bodypartformatter/itinerary/templates/flightreservation.html b/plugins/messageviewer/bodypartformatter/itinerary/templates/flightreservation.html index 1800e7d2..59a0780a 100644 --- a/plugins/messageviewer/bodypartformatter/itinerary/templates/flightreservation.html +++ b/plugins/messageviewer/bodypartformatter/itinerary/templates/flightreservation.html @@ -1,85 +1,87 @@ - {% if trip.departureAirport.iataCode %} - - {% else %} - - {% endif %} + - {% if trip.arrivalAirport.iataCode %} - - {% else %} - - {% endif %} +
- {{ trip.departureAirport.iataCode }} - {{ trip.departureAirport.name }} + {% if trip.departureAirport.iataCode %} + {{ trip.departureAirport.iataCode }} + {% else %} + {{ trip.departureAirport.name }} + {% endif %} + {% with trip.departureAirport as location %}{% include "org.kde.messageviewer/itinerary/location.html" %}{% endwith %} + - {{ trip.arrivalAirport.iataCode }} - {{ trip.arrivalAirport.name }} + {% if trip.arrivalAirport.iataCode %} + {{ trip.arrivalAirport.iataCode }} + {% else %} + {{ trip.arrivalAirport.name }} + {% endif %} + {% with trip.arrivalAirport as location %}{% include "org.kde.messageviewer/itinerary/location.html" %}{% endwith %} +
{% if trip.departureTime %} {{ trip.departureTime|formatDateTime }} {% else %} {{ trip.departureDay|formatDate }} {% endif %} {{ trip.airline.iataCode }} {{ trip.flightNumber }} {{ trip.arrivalTime|formatDateTime }}
{% if elem.reservations.0.ticketToken or elem.reservations.size > 1 %} {% if elem.state.expanded %} {% else %} {% endif %} {% endif %} {% if trip.departureGate %} {% i18n "Gate: %1" trip.departureGate %} {% endif %} {% if trip.boardingTime %} {% i18n "Boarding: %1" trip.boardingTime|formatTime %} {% endif %} {% if res.boardingGroup and elem.reservations.size == 1 and not elem.state.expanded %} {% i18n "Group: %1" res.boardingGroup %} {% endif %} {% if res.airplaneSeat and elem.reservations.size == 1 and not elem.state.expanded %} {% i18n "Seat: %1" res.airplaneSeat %} {% endif %}
{% if elem.reservations.size == 1 %} {% include "org.kde.messageviewer/itinerary/actions.html" %} {% endif %}
{% if elem.state.expanded %} {% for d in elem.reservations %} {% with d.reservation as res %} {% if res.underName.name %}
{{ res.underName.name }}
{% endif %}
{% if res.boardingGroup %} {% i18n "Group: %1" res.boardingGroup %} {% endif %} {% if res.airplaneSeat %} {% i18n "Seat: %1" res.airplaneSeat %} {% endif %}
{% if d.ticketToken %}
{% endif %} {% if elem.reservations.size > 1 %} {% include "org.kde.messageviewer/itinerary/actions.html" %} {% endif %} {% endwith %} {% endfor %} {% endif %} diff --git a/plugins/messageviewer/bodypartformatter/itinerary/templates/foodestablishmentreservation.html b/plugins/messageviewer/bodypartformatter/itinerary/templates/foodestablishmentreservation.html index beb885ba..185c3c98 100644 --- a/plugins/messageviewer/bodypartformatter/itinerary/templates/foodestablishmentreservation.html +++ b/plugins/messageviewer/bodypartformatter/itinerary/templates/foodestablishmentreservation.html @@ -1,13 +1,15 @@ -
{{ trip.name }} - {{ res.startTime|formatDateTime }}
+
{{ trip.name }} + {% with trip as location %}{% include "org.kde.messageviewer/itinerary/location.html" %}{% endwith %} + - {{ res.startTime|formatDateTime }}
{% if res.partySize > 0 %}
{% i18n "Number Of People: %1" res.partySize %}
{% endif %} {% if res.reservationNumber %}
{% i18n "Reservation Number: %1" res.reservationNumber %}
{% endif %} {% if res.underName.name %}
{% i18n "Under Name: %1" res.underName.name %}
{% endif %}
{{ trip.address|formatAddress|safe }}
{% include "org.kde.messageviewer/itinerary/actions.html" %}
diff --git a/plugins/messageviewer/bodypartformatter/itinerary/templates/location.html b/plugins/messageviewer/bodypartformatter/itinerary/templates/location.html new file mode 100644 index 00000000..6f458154 --- /dev/null +++ b/plugins/messageviewer/bodypartformatter/itinerary/templates/location.html @@ -0,0 +1,13 @@ +{% if location.geo.isValid %} + +{% elif location.address.addressLocality or location.address.streetAddress %} + +{% endif %} diff --git a/plugins/messageviewer/bodypartformatter/itinerary/templates/lodgingreservation.html b/plugins/messageviewer/bodypartformatter/itinerary/templates/lodgingreservation.html index 066f5bda..a134577f 100644 --- a/plugins/messageviewer/bodypartformatter/itinerary/templates/lodgingreservation.html +++ b/plugins/messageviewer/bodypartformatter/itinerary/templates/lodgingreservation.html @@ -1,4 +1,6 @@ -
{{ trip.name }} {{ res.checkinTime|formatDate }} - {{ res.checkoutTime|formatDate }}
+
{{ trip.name }} + {% with trip as location %}{% include "org.kde.messageviewer/itinerary/location.html" %}{% endwith %} + {{ res.checkinTime|formatDate }} - {{ res.checkoutTime|formatDate }}
{{ trip.address|formatAddress|safe }}
{% include "org.kde.messageviewer/itinerary/actions.html" %}
diff --git a/plugins/messageviewer/bodypartformatter/itinerary/templates/rentalcarreservation.html b/plugins/messageviewer/bodypartformatter/itinerary/templates/rentalcarreservation.html index 1afa01dc..e2b73b1e 100644 --- a/plugins/messageviewer/bodypartformatter/itinerary/templates/rentalcarreservation.html +++ b/plugins/messageviewer/bodypartformatter/itinerary/templates/rentalcarreservation.html @@ -1,16 +1,17 @@
{% if trip.rentalCompany.name %} {{ trip.rentalCompany.name }}: {% endif %} {{ trip.name }} {% if trip.name and trip.model %}({% endif %} {{ trip.model }} {% if trip.name and trip.model %}){% endif %}
{{ res.pickupTime|formatDateTime }} - {{ res.dropoffTime|formatDateTime }}
{{ res.pickupLocation.name }} + {% with trip.pickupLocation as location %}{% include "org.kde.messageviewer/itinerary/location.html" %}{% endwith %} {{ res.pickupLocation|formatAddress|safe }}
{% include "org.kde.messageviewer/itinerary/actions.html" %}
diff --git a/plugins/messageviewer/bodypartformatter/itinerary/templates/templates.qrc b/plugins/messageviewer/bodypartformatter/itinerary/templates/templates.qrc index ffc3d784..f0d1c7bd 100644 --- a/plugins/messageviewer/bodypartformatter/itinerary/templates/templates.qrc +++ b/plugins/messageviewer/bodypartformatter/itinerary/templates/templates.qrc @@ -1,14 +1,15 @@ itinerary.html flightreservation.html + location.html lodgingreservation.html trainreservation.html busreservation.html event.html actions.html foodestablishmentreservation.html rentalcarreservation.html taxireservation.html diff --git a/plugins/messageviewer/bodypartformatter/itinerary/templates/trainreservation.html b/plugins/messageviewer/bodypartformatter/itinerary/templates/trainreservation.html index 2f4a012c..40d0631f 100644 --- a/plugins/messageviewer/bodypartformatter/itinerary/templates/trainreservation.html +++ b/plugins/messageviewer/bodypartformatter/itinerary/templates/trainreservation.html @@ -1,84 +1,86 @@
{{ trip.departureStation.name }} {% if trip.departurePlatform %} [{{ trip.departurePlatform }}] {% endif %} + {% with trip.departureStation as location %}{% include "org.kde.messageviewer/itinerary/location.html" %}{% endwith %} {{ trip.arrivalStation.name}} {% if trip.arrivalPlatform %} [{{ trip.arrivalPlatform }}] {% endif %} + {% with trip.arrivalStation as location %}{% include "org.kde.messageviewer/itinerary/location.html" %}{% endwith %}
{% if trip.departureTime %} {{ trip.departureTime|formatDateTime }} {% else %} {{ trip.departureDay|formatDate }} {% endif %} {% if trip.trainName and trip.trainNumber %} {{ trip.trainName }}
{{ trip.trainNumber }} {% else %} {{ trip.trainName }} {{ trip.trainNumber }} {% endif %}
{{ trip.arrivalTime|formatDateTime }}
{% if elem.reservations.0.ticketToken or elem.reservations.size > 1 %} {% if elem.state.expanded %} {% else %} {% endif %} {% endif %} {% if res.reservedTicket.ticketedSeat.seatSection and not elem.state.expanded %} {% i18n "Coach: %1" res.reservedTicket.ticketedSeat.seatSection %} {% endif %} {% if res.reservedTicket.ticketedSeat.seatNumber and not elem.state.expanded %} {% i18n "Seat: %1" res.reservedTicket.ticketedSeat.seatNumber %} {% endif %}
{% include "org.kde.messageviewer/itinerary/actions.html" %}
{% if elem.state.expanded %} {% for d in elem.reservations %} {% with d.reservation as res %} {% if res.underName.name %}
{{ res.underName.name }}
{% endif %}
{% if res.reservedTicket.ticketedSeat.seatSection %} {% i18n "Coach: %1" res.reservedTicket.ticketedSeat.seatSection %} {% endif %} {% if res.reservedTicket.ticketedSeat.seatNumber %} {% i18n "Seat: %1" res.reservedTicket.ticketedSeat.seatNumber %} {% endif %}
{% if d.ticketToken %}
{% endif %} {% if elem.reservations.size > 1 %} {% include "org.kde.messageviewer/itinerary/actions.html" %} {% endif %} {% endwith %} {% endfor %} {% endif %}