Commit bab3630f authored by Volker Krause's avatar Volker Krause
Browse files

Initial infrastructure for supporting generic passes

That is things that are not tied to a timeline element, such as bonus/
discount program membership cards, flat rate tickets, etc.
parent f74dd1a6
......@@ -28,6 +28,7 @@ ecm_add_test(favoritelocationtest.cpp LINK_LIBRARIES Qt::Test itinerary)
ecm_add_test(transfertest.cpp LINK_LIBRARIES Qt::Test itinerary)
ecm_add_test(livedatamanagertest.cpp LINK_LIBRARIES Qt::Test itinerary)
ecm_add_test(healthcertificatemanagertest.cpp LINK_LIBRARIES Qt::Test itinerary)
ecm_add_test(passmanagertest.cpp LINK_LIBRARIES Qt::Test itinerary)
ecm_add_test(weathertest.cpp LINK_LIBRARIES Qt::Test itinerary-weather)
target_include_directories(weathertest PRIVATE ${CMAKE_BINARY_DIR})
{
"@context": "http://schema.org",
"@type": "ProgramMembership",
"member": {
"@type": "Person",
"familyName": "Dragon",
"givenName": "Konqi"
},
"membershipNumber": "7081123456789012",
"programName": "BahnCard 25 (2. Kl.) (BC25)",
"token": "aztec:dummy_barcode"
}
/*
SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "testhelper.h"
#include <passmanager.h>
#include <KItinerary/JsonLdDocument>
#include <KItinerary/ProgramMembership>
#include <qtest.h>
#include <QAbstractItemModelTester>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSignalSpy>
#include <QStandardPaths>
#include <QTemporaryFile>
using namespace KItinerary;
class PassManagerTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase()
{
qputenv("TZ", "UTC");
QStandardPaths::setTestModeEnabled(true);
}
void testPassManager()
{
PassManager mgr;
QAbstractItemModelTester modelTest(&mgr);
// clear previous leftovers
while (mgr.rowCount()) {
QVERIFY(mgr.removeRow(0));
}
QCOMPARE(mgr.rowCount(), 0);
// test import
QVERIFY(mgr.import(JsonLdDocument::fromJsonSingular(QJsonDocument::fromJson(Test::readFile(QStringLiteral(SOURCE_DIR "/data/bahncard.json"))).object())));
QCOMPARE(mgr.rowCount(), 1);
// retrieval
auto idx = mgr.index(0, 0);
const auto passId = idx.data(PassManager::PassIdRole).toString();
QVERIFY(!passId.isEmpty());
const auto pass = idx.data(PassManager::PassRole);
QVERIFY(!pass.isNull());
QVERIFY(JsonLd::isA<ProgramMembership>(pass));
QCOMPARE(idx.data(PassManager::PassTypeRole).toInt(), PassManager::ProgramMembership);
{
// test persistence
PassManager mgr2;
QCOMPARE(mgr2.rowCount(), 1);
auto idx = mgr2.index(0, 0);
QCOMPARE(idx.data(PassManager::PassIdRole).toString(), passId);
const auto pass = idx.data(PassManager::PassRole);
QVERIFY(!pass.isNull());
QVERIFY(JsonLd::isA<ProgramMembership>(pass));
}
// test removal
QVERIFY(mgr.remove(passId));
QCOMPARE(mgr.rowCount(), 0);
}
};
QTEST_GUILESS_MAIN(PassManagerTest)
#include "passmanagertest.moc"
......@@ -11,9 +11,19 @@
#include <pkpassmanager.h>
#include <reservationmanager.h>
#include <QFile>
namespace Test
{
/** Read the entire file content. */
inline QByteArray readFile(const QString &fn)
{
QFile f(fn);
f.open(QFile::ReadOnly);
return f.readAll();
}
/** Delete all reservations. */
inline void clearAll(ReservationManager *mgr)
{
......
......@@ -21,6 +21,7 @@ target_sources(itinerary PRIVATE
locationinformation.cpp
navigationcontroller.cpp
notificationhelper.cpp
passmanager.cpp
pkpassmanager.cpp
pkpassimageprovider.cpp
publictransport.cpp
......@@ -158,6 +159,7 @@ if (ANDROID)
view-list-details
view-refresh
view-statistics
wallet-open
zoom-in-symbolic
zoom-out-symbolic
......
/*
SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as QQC2
import Qt.labs.qmlmodels 1.0 as Models
import org.kde.kirigami 2.19 as Kirigami
import org.kde.itinerary 1.0
import "." as App
Kirigami.ScrollablePage {
id: root
title: i18n("Passes and Programs")
Component {
id: programMembershipPage
App.ProgramMembershipPage {}
}
Models.DelegateChooser {
id: chooser
role: "type"
Models.DelegateChoice {
roleValue: PassManager.ProgramMembership
Kirigami.BasicListItem {
highlighted: false
text: model.pass.programName
subtitle: {
if (!model.pass.member.name)
return model.pass.membershipNumber;
if (!model.pass.membershipNumber)
return model.pass.member.name;
return i18nc("name - number", "%1 - %2", model.pass.member.name, model.pass.membershipNumber)
}
onClicked: applicationWindow().pageStack.push(programMembershipPage, { programMembership: model.pass, passId: model.passId })
}
}
}
ListView {
id: passListView
model: PassManager
delegate: chooser
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
visible: passListView.count === 0
icon.name: "wallet-open"
text: i18n("Import bonus or discount program cards or flat rate passes.")
helpfulAction: Kirigami.Action {
text: i18n("Import...")
icon.name: i18n("document-open")
onTriggered: importDialog.open()
}
}
}
}
/*
SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.19 as Kirigami
import org.kde.kitinerary 1.0
import org.kde.itinerary 1.0
import "." as App
Kirigami.ScrollablePage {
id: root
title: i18n("Program Membership")
property string passId
property var programMembership
BarcodeScanModeController {
id: scanModeController
page: root
}
Kirigami.OverlaySheet {
id: deleteWarningSheet
header: Kirigami.Heading {
text: i18n("Delete Pass")
}
QQC2.Label {
text: i18n("Do you really want to delete this pass?")
wrapMode: Text.WordWrap
}
footer: RowLayout {
QQC2.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Delete")
icon.name: "edit-delete"
onClicked: {
PassManager.remove(passId)
applicationWindow().pageStack.pop();
}
}
}
}
actions.main: Kirigami.Action {
icon.name: "view-barcode-qr"
text: i18n("Barcode Scan Mode")
onTriggered: scanModeController.toggle()
visible: barcodeContainer.visible
checkable: true
checked: scanModeController.enabled
}
actions.contextualActions: [
Kirigami.Action {
icon.name: "edit-delete"
text: i18n("Delete")
onTriggered: deleteWarningSheet.open()
}
]
ColumnLayout {
width: parent.width
QQC2.Label {
Layout.fillWidth: true
text: programMembership.programName
horizontalAlignment: Qt.AlignHCenter
font.bold: true
}
App.BarcodeContainer {
id: barcodeContainer
Layout.alignment: Qt.AlignCenter
Layout.fillWidth: true
barcodeType: programMembership.tokenType
barcodeContent: programMembership.tokenData
onDoubleClicked: scanModeController.toggle()
}
Kirigami.FormLayout {
Layout.fillWidth: true
Kirigami.Separator {
Kirigami.FormData.label: i18n("Member")
Kirigami.FormData.isSection: true
visible: nameLabel.visible || numberLabel.visible
}
QQC2.Label {
id: nameLabel
Kirigami.FormData.label: i18n("Name:")
text: programMembership.member.name
visible: programMembership.member.name !== ""
}
QQC2.Label {
id: numberLabel
Kirigami.FormData.label: i18n("Number:")
text: programMembership.membershipNumber
visible: programMembership.membershipNumber !== ""
}
}
}
}
......@@ -13,6 +13,7 @@
#include "importexport.h"
#include "livedatamanager.h"
#include "logging.h"
#include "passmanager.h"
#include "pkpassmanager.h"
#include "reservationmanager.h"
#include "transfermanager.h"
......@@ -202,6 +203,11 @@ void ApplicationController::setTripGroupManager(TripGroupManager *tripGroupMgr)
m_tripGroupMgr = tripGroupMgr;
}
void ApplicationController::setPassManager(PassManager *passMgr)
{
m_passMgr = passMgr;
}
void ApplicationController::setHealthCertificateManager(HealthCertificateManager *healthCertMgr)
{
m_healthCertMgr = healthCertMgr;
......@@ -395,7 +401,8 @@ void ApplicationController::importData(const QByteArray &data, const QString &fi
#endif
engine.setContextDate(QDateTime(QDate::currentDate(), QTime(0, 0)));
engine.setData(data, fileName);
const auto resIds = m_resMgr->importReservations(JsonLdDocument::fromJson(engine.extract()));
const auto extractorResult = JsonLdDocument::fromJson(engine.extract());
const auto resIds = m_resMgr->importReservations(extractorResult);
if (!resIds.isEmpty()) {
// check if there is a document we want to attach here
QMimeDatabase db;
......@@ -423,6 +430,12 @@ void ApplicationController::importData(const QByteArray &data, const QString &fi
return;
}
// look for time-less passes/program memberships/etc
if (m_passMgr->import(extractorResult)) {
Q_EMIT infoMessage(i18n("Pass imported."));
return;
}
// look for health certificate barcodes instead
// if we don't find anything, try to import as health certificate directly
if (importHealthCertificateRecursive(engine.rootDocumentNode()) || m_healthCertMgr->importCertificate(data)) {
......
......@@ -13,6 +13,7 @@ class DocumentManager;
class FavoriteLocationModel;
class HealthCertificateManager;
class LiveDataManager;
class PassManager;
class PkPassManager;
class ReservationManager;
class TransferManager;
......@@ -51,6 +52,7 @@ public:
void setFavoriteLocationModel(FavoriteLocationModel *favLocModel);
void setLiveDataManager(LiveDataManager *liveDataMgr);
void setTripGroupManager(TripGroupManager *tripGroupMgr);
void setPassManager(PassManager *passMgr);
void setHealthCertificateManager(HealthCertificateManager *healthCertMgr);
// data import
......@@ -106,6 +108,7 @@ private:
FavoriteLocationModel *m_favLocModel = nullptr;
LiveDataManager *m_liveDataMgr = nullptr;
TripGroupManager *m_tripGroupMgr = nullptr;
PassManager *m_passMgr = nullptr;
HealthCertificateManager *m_healthCertMgr = nullptr;
QNetworkAccessManager *m_nam = nullptr;
......
......@@ -22,6 +22,7 @@
#include "mapdownloadmanager.h"
#include "navigationcontroller.h"
#include "notificationconfigcontroller.h"
#include "passmanager.h"
#include "pkpassmanager.h"
#include "timelinemodel.h"
#include "pkpassimageprovider.h"
......@@ -153,6 +154,7 @@ static TripGroupInfoProvider s_tripGroupInfoProvider;
static TripGroupProxyModel *s_tripGroupProxyModel = nullptr;
static MapDownloadManager *s_mapDownloadManager = nullptr;
static HealthCertificateManager *s_healthCertificateManager = nullptr;
static PassManager *s_passManager = nullptr;
#define REGISTER_SINGLETON_INSTANCE(Class, Instance) \
qmlRegisterSingletonInstance<Class>("org.kde.itinerary", 1, 0, #Class, Instance);
......@@ -183,6 +185,7 @@ void registerApplicationSingletons()
REGISTER_SINGLETON_INSTANCE(TripGroupProxyModel, s_tripGroupProxyModel)
REGISTER_SINGLETON_INSTANCE(MapDownloadManager, s_mapDownloadManager)
REGISTER_SINGLETON_INSTANCE(HealthCertificateManager, s_healthCertificateManager)
REGISTER_SINGLETON_INSTANCE(PassManager, s_passManager)
REGISTER_SINGLETON_GADGET_INSTANCE(TripGroupInfoProvider, s_tripGroupInfoProvider)
......@@ -273,8 +276,8 @@ int main(int argc, char **argv)
Settings settings;
s_settings = &settings;
PkPassManager passMgr;
s_pkPassManager = &passMgr;
PkPassManager pkPassMgr;
s_pkPassManager = &pkPassMgr;
ReservationManager resMgr;
s_reservationManager = &resMgr;
......@@ -290,7 +293,7 @@ int main(int argc, char **argv)
s_tripGroupManager = &tripGroupMgr;
LiveDataManager liveDataMgr;
liveDataMgr.setPkPassManager(&passMgr);
liveDataMgr.setPkPassManager(&pkPassMgr);
liveDataMgr.setReservationManager(&resMgr);
liveDataMgr.setPollingEnabled(settings.queryLiveData());
liveDataMgr.setShowNotificationsOnLockScreen(settings.showNotificationOnLockScreen());
......@@ -339,14 +342,18 @@ int main(int argc, char **argv)
HealthCertificateManager healthCertificateMgr;
s_healthCertificateManager = &healthCertificateMgr;
PassManager passMgr;
s_passManager = &passMgr;
ApplicationController appController;
appController.setReservationManager(&resMgr);
appController.setPkPassManager(&passMgr);
appController.setPkPassManager(&pkPassMgr);
appController.setDocumentManager(&docMgr);
appController.setFavoriteLocationModel(&favLocModel);
appController.setTransferManager(&transferManager);
appController.setLiveDataManager(&liveDataMgr);
appController.setTripGroupManager(&tripGroupMgr);
appController.setPassManager(&passMgr);
appController.setHealthCertificateManager(&healthCertificateMgr);
#ifndef Q_OS_ANDROID
QObject::connect(&service, &KDBusService::activateRequested, [&](const QStringList &args, const QString &workingDir) {
......@@ -369,7 +376,7 @@ int main(int argc, char **argv)
registerApplicationSingletons();
QQmlApplicationEngine engine;
engine.addImageProvider(QStringLiteral("org.kde.pkpass"), new PkPassImageProvider(&passMgr));
engine.addImageProvider(QStringLiteral("org.kde.pkpass"), new PkPassImageProvider(&pkPassMgr));
auto l10nContext = new KLocalizedContext(&engine);
l10nContext->setTranslationDomain(QStringLiteral(TRANSLATION_DOMAIN));
engine.rootContext()->setContextObject(l10nContext);
......
......@@ -82,6 +82,13 @@ Kirigami.ApplicationWindow {
enabled: pageStack.layers.depth < 2
onTriggered: pageStack.layers.push(statisticsComponent)
},
Kirigami.Action {
id: passAction
text: i18n("Passes & Programs")
iconName: "wallet-open"
onTriggered: pageStack.push(passComponent)
visible: Settings.developmentMode // TODO remove once this is sufficiently complete
},
Kirigami.Action {
id: healthCertAction
text: i18n("Health Certificates")
......@@ -200,6 +207,10 @@ Kirigami.ApplicationWindow {
reservationManager: ReservationManager
tripGroupManager: TripGroupManager
}
}
Component {
id: passComponent
App.PassPage {}
}
// replace loader with component once we depend on KHealthCertificate unconditionally
Loader {
......
/*
SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "passmanager.h"
#include "logging.h"
#include <KItinerary/ExtractorPostprocessor>
#include <KItinerary/JsonLdDocument>
#include <KItinerary/ProgramMembership>
#include <QDirIterator>
#include <QJsonDocument>
#include <QJsonObject>
#include <QStandardPaths>
#include <QUuid>
using namespace KItinerary;
PassManager::PassManager(QObject *parent)
: QAbstractListModel(parent)
{
load();
}
PassManager::~PassManager() = default;
int PassManager::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_entries.size();
}
bool PassManager::import(const QVariant &pass)
{
if (JsonLd::isA<KItinerary::ProgramMembership>(pass)) {
Entry entry;
entry.id = QUuid::createUuid().toString();
entry.data = pass;
auto path = basePath();
QDir().mkpath(path);
path += entry.id;
QFile f(path);
if (!f.open(QFile::WriteOnly)) {
qCWarning(Log) << "Failed to open file:" << f.fileName() << f.errorString();
return false;
}
f.write(QJsonDocument(JsonLdDocument::toJson(entry.data)).toJson());
f.close();
beginInsertRows({}, rowCount(), rowCount());
m_entries.push_back(std::move(entry));
endInsertRows();
return true;
}
return false;
}
bool PassManager::import(const QVector<QVariant> &passes)
{
ExtractorPostprocessor postproc;
postproc.setValidationEnabled(false);
postproc.process(passes);
const auto processed = postproc.result();
bool result = false;
for (const auto &pass : processed) {
result |= import(pass);
}
return result;
}
QVariant PassManager::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index)) {
return {};
}
auto &entry = m_entries[index.row()];
switch (role) {
case PassRole:
ensureLoaded(entry);
return entry.data;
case PassIdRole:
return entry.id;
case PassTypeRole:
ensureLoaded(entry);
if (JsonLd::isA<KItinerary::ProgramMembership>(entry.data)) {
return ProgramMembership;
}
return {};
}
return {};
}
QHash<int, QByteArray> PassManager::roleNames() const
{
auto r = QAbstractListModel::roleNames();
r.insert(PassRole, "pass");
r.insert(PassIdRole, "passId");
r.insert(PassTypeRole, "type");
return r;
}
void PassManager::load()
{
QDirIterator it(basePath(), QDir::Files);
while (it.hasNext()) {
it.next();
m_entries.push_back({it.fileName(), QVariant()});
}
}
void PassManager::ensureLoaded(Entry &entry) const
{
if (!entry.data.isNull()) {
return;
}
QFile f(basePath() + entry.id);
if (!f.open(QFile::ReadOnly)) {
qCWarning(Log) << "Failed to open file:" << f.fileName() << f.errorString();
return;
}
entry.data = JsonLdDocument::fromJsonSingular(QJsonDocument::fromJson(f.readAll()).object());
}
bool PassManager::remove(const QString &passId)
{
auto it = std::find_if(m_entries.begin(), m_entries.end(), [passId](const auto &entry) {
return entry.id == passId;
});
if (it != m_entries.end()) {
return removeRow(std::distance(m_entries.begin(), it));
}
return false;
}
bool PassManager::removeRow(int row, const QModelIndex& parent)
{
return QAbstractListModel::removeRow(row, parent);
}
bool PassManager::removeRows(int row, int count, const QModelIndex& parent)