diff --git a/autotests/reservationmanagertest.cpp b/autotests/reservationmanagertest.cpp index 6986326..b48127f 100644 --- a/autotests/reservationmanagertest.cpp +++ b/autotests/reservationmanagertest.cpp @@ -1,134 +1,141 @@ /* Copyright (C) 2018 Volker Krause This program 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 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 Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include class ReservationManagerTest : public QObject { Q_OBJECT private: void clearReservations(ReservationManager *mgr) { for (const auto id : mgr->reservations()) { mgr->removeReservation(id); } } void clearPasses(PkPassManager *mgr) { for (const auto id : mgr->passes()) mgr->removePass(id); } + QByteArray readFile(const QString &fn) + { + QFile f(fn); + f.open(QFile::ReadOnly); + return f.readAll(); + } + private slots: void initTestCase() { QStandardPaths::setTestModeEnabled(true); } void testOperations() { ReservationManager mgr; clearReservations(&mgr); QSignalSpy addSpy(&mgr, &ReservationManager::reservationAdded); QVERIFY(addSpy.isValid()); QSignalSpy updateSpy(&mgr, &ReservationManager::reservationUpdated); QVERIFY(updateSpy.isValid()); QSignalSpy rmSpy(&mgr, &ReservationManager::reservationRemoved); QVERIFY(rmSpy.isValid()); QVERIFY(mgr.reservations().isEmpty()); - mgr.importReservation(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/4U8465-v1.json"))); + mgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/data/4U8465-v1.json"))); auto res = mgr.reservations(); QCOMPARE(res.size(), 1); const auto resId = res.at(0); QVERIFY(!resId.isEmpty()); QCOMPARE(addSpy.size(), 1); QCOMPARE(addSpy.at(0).at(0).toString(), resId); QVERIFY(updateSpy.isEmpty()); QVERIFY(!mgr.reservation(resId).isNull()); - mgr.importReservation(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/4U8465-v2.json"))); + mgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/data/4U8465-v2.json"))); QCOMPARE(addSpy.size(), 1); QCOMPARE(updateSpy.size(), 1); QCOMPARE(mgr.reservations().size(), 1); QCOMPARE(updateSpy.at(0).at(0).toString(), resId); QVERIFY(mgr.reservation(resId).isValid()); mgr.removeReservation(resId); QCOMPARE(addSpy.size(), 1); QCOMPARE(updateSpy.size(), 1); QCOMPARE(rmSpy.size(), 1); QCOMPARE(rmSpy.at(0).at(0).toString(), resId); QVERIFY(mgr.reservations().isEmpty()); QVERIFY(mgr.reservation(resId).isNull()); clearReservations(&mgr); auto attraction = KItinerary::TouristAttraction(); attraction.setName(QStringLiteral("Sky Tree")); auto visit = KItinerary::TouristAttractionVisit(); visit.setTouristAttraction(attraction); mgr.addReservation(QVariant::fromValue(visit)); auto addedResId = mgr.reservations().at(0); QCOMPARE(addSpy.size(), 2); QVERIFY(!mgr.reservations().isEmpty()); QCOMPARE(mgr.reservations().size(), 1); QCOMPARE(addSpy.at(1).at(0).toString(), addedResId); QVERIFY(mgr.reservation(addedResId).isValid()); } void testPkPassChanges() { PkPassManager passMgr; clearPasses(&passMgr); ReservationManager mgr; mgr.setPkPassManager(&passMgr); clearReservations(&mgr); QSignalSpy addSpy(&mgr, &ReservationManager::reservationAdded); QVERIFY(addSpy.isValid()); QSignalSpy updateSpy(&mgr, &ReservationManager::reservationUpdated); QVERIFY(updateSpy.isValid()); QVERIFY(mgr.reservations().isEmpty()); const auto passId = QStringLiteral("pass.booking.kde.org/MTIzNA=="); passMgr.importPass(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/boardingpass-v1.pkpass"))); QCOMPARE(addSpy.size(), 1); QVERIFY(updateSpy.isEmpty()); passMgr.importPass(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/boardingpass-v2.pkpass"))); QCOMPARE(addSpy.size(), 1); QCOMPARE(updateSpy.size(), 1); } }; QTEST_GUILESS_MAIN(ReservationManagerTest) #include "reservationmanagertest.moc" diff --git a/autotests/timelinemodeltest.cpp b/autotests/timelinemodeltest.cpp index 0ac9c23..335cd0c 100644 --- a/autotests/timelinemodeltest.cpp +++ b/autotests/timelinemodeltest.cpp @@ -1,419 +1,426 @@ /* Copyright (C) 2018 Volker Krause This program 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 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 Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include class TimelineModelTest : public QObject { Q_OBJECT private: void clearPasses(PkPassManager *mgr) { for (const auto id : mgr->passes()) mgr->removePass(id); } void clearReservations(ReservationManager *mgr) { for (const auto id : mgr->reservations()) { mgr->removeReservation(id); } } + QByteArray readFile(const QString &fn) + { + QFile f(fn); + f.open(QFile::ReadOnly); + return f.readAll(); + } + private slots: void initTestCase() { qputenv("TZ", "UTC"); QStandardPaths::setTestModeEnabled(true); } void testModel() { PkPassManager mgr; clearPasses(&mgr); ReservationManager resMgr; clearReservations(&resMgr); resMgr.setPkPassManager(&mgr); TimelineModel model; model.setReservationManager(&resMgr); QSignalSpy insertSpy(&model, &TimelineModel::rowsInserted); QVERIFY(insertSpy.isValid()); QSignalSpy updateSpy(&model, &TimelineModel::dataChanged); QVERIFY(updateSpy.isValid()); QSignalSpy rmSpy(&model, &TimelineModel::rowsRemoved); QVERIFY(rmSpy.isValid()); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); mgr.importPass(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/boardingpass-v1.pkpass"))); QCOMPARE(insertSpy.size(), 1); QCOMPARE(insertSpy.at(0).at(1).toInt(), 0); QCOMPARE(insertSpy.at(0).at(2).toInt(), 0); QVERIFY(updateSpy.isEmpty()); QCOMPARE(model.rowCount(), 2); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); mgr.importPass(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/boardingpass-v2.pkpass"))); QCOMPARE(insertSpy.size(), 1); QCOMPARE(updateSpy.size(), 1); QCOMPARE(updateSpy.at(0).at(0).toModelIndex().row(), 0); QCOMPARE(model.rowCount(), 2); clearReservations(&resMgr); QCOMPARE(insertSpy.size(), 1); QCOMPARE(updateSpy.size(), 1); QCOMPARE(rmSpy.size(), 1); QCOMPARE(model.rowCount(), 1); } void testNestedElements() { ReservationManager resMgr; clearReservations(&resMgr); TimelineModel model; model.setHomeCountryIsoCode(QStringLiteral("DE")); model.setReservationManager(&resMgr); QSignalSpy insertSpy(&model, &TimelineModel::rowsInserted); QVERIFY(insertSpy.isValid()); QSignalSpy updateSpy(&model, &TimelineModel::dataChanged); QVERIFY(updateSpy.isValid()); QSignalSpy rmSpy(&model, &TimelineModel::rowsRemoved); QVERIFY(rmSpy.isValid()); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); - resMgr.importReservation(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/haus-randa-v1.json"))); + resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/data/haus-randa-v1.json"))); QCOMPARE(insertSpy.size(), 3); QCOMPARE(insertSpy.at(0).at(1).toInt(), 0); QCOMPARE(insertSpy.at(0).at(2).toInt(), 0); QCOMPARE(insertSpy.at(1).at(1).toInt(), 1); QCOMPARE(insertSpy.at(1).at(2).toInt(), 1); QVERIFY(updateSpy.isEmpty()); QCOMPARE(model.rowCount(), 4); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::CountryInfo); QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Hotel); QCOMPARE(model.index(1, 0).data(TimelineModel::ElementRangeRole), TimelineModel::RangeBegin); QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Hotel); QCOMPARE(model.index(2, 0).data(TimelineModel::ElementRangeRole), TimelineModel::RangeEnd); // move end date of a hotel booking: dataChanged on RangeBegin, move (or del/ins) on RangeEnd - resMgr.importReservation(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/haus-randa-v2.json"))); + resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/data/haus-randa-v2.json"))); QCOMPARE(insertSpy.size(), 4); QCOMPARE(updateSpy.size(), 1); QCOMPARE(rmSpy.size(), 1); QCOMPARE(updateSpy.at(0).at(0).toModelIndex().row(), 1); QCOMPARE(insertSpy.at(2).at(1).toInt(), 0); QCOMPARE(insertSpy.at(2).at(2).toInt(), 0); QCOMPARE(rmSpy.at(0).at(1), 2); QCOMPARE(model.rowCount(), 4); // delete a split element const auto resId = model.data(model.index(1, 0), TimelineModel::ReservationIdsRole).toStringList().value(0); QVERIFY(!resId.isEmpty()); resMgr.removeReservation(resId); QCOMPARE(rmSpy.size(), 4); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); } void testCountryInfos() { ReservationManager resMgr; clearReservations(&resMgr); TimelineModel model; model.setHomeCountryIsoCode(QStringLiteral("DE")); model.setReservationManager(&resMgr); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); - resMgr.importReservation(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/flight-txl-lhr-sfo.json"))); + resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/data/flight-txl-lhr-sfo.json"))); QCOMPARE(model.rowCount(), 5); // 2x country info, 2x flights, today marker QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::CountryInfo); auto countryInfo = model.index(0, 0).data(TimelineModel::CountryInformationRole).value(); QCOMPARE(countryInfo.drivingSide(), KItinerary::KnowledgeDb::DrivingSide::Left); QCOMPARE(countryInfo.drivingSideDiffers(), true); QCOMPARE(countryInfo.powerPlugCompatibility(), CountryInformation::Incompatible); QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::CountryInfo); countryInfo = model.index(2, 0).data(TimelineModel::CountryInformationRole).value(); QCOMPARE(countryInfo.drivingSide(), KItinerary::KnowledgeDb::DrivingSide::Right); QCOMPARE(countryInfo.drivingSideDiffers(), false); QCOMPARE(countryInfo.powerPlugCompatibility(), CountryInformation::Incompatible); QCOMPARE(model.index(3, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(4, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); // remove the GB flight should also remove the GB country info auto resId = model.index(1, 0).data(TimelineModel::ReservationIdsRole).toStringList().value(0); resMgr.removeReservation(resId); QCOMPARE(model.rowCount(), 3); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::CountryInfo); QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); // remove the US flight should also remove the US country info resId = model.index(1, 0).data(TimelineModel::ReservationIdsRole).toStringList().value(0); resMgr.removeReservation(resId); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); } void testWeatherElements() { using namespace KItinerary; ReservationManager resMgr; clearReservations(&resMgr); WeatherForecastManager weatherMgr; weatherMgr.setTestModeEnabled(true); TimelineModel model; model.setReservationManager(&resMgr); model.setWeatherForecastManager(&weatherMgr); QCOMPARE(model.rowCount(), 1); // no weather data, as we don't know where we are // Add an element that will result in a defined location GeoCoordinates geo; geo.setLatitude(52.0f); geo.setLongitude(13.0f); Airport a; a.setGeo(geo); Flight f; f.setArrivalAirport(a); f.setDepartureTime(QDateTime(QDate(2018, 1, 1), QTime(0, 0))); FlightReservation res; res.setReservationFor(f); resMgr.addReservation(res); QCOMPARE(model.rowCount(), 11); // 1x flight, 1x today, 9x weather QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast); auto fc = model.index(2, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.dateTime().date(), QDate::currentDate()); QCOMPARE(fc.minimumTemperature(), 13.0f); QCOMPARE(fc.maximumTemperature(), 52.0f); QCOMPARE(model.index(10, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast); fc = model.index(10, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.dateTime().date(), QDate::currentDate().addDays(8)); // Add a flight one day from now changing location mid-day geo.setLatitude(46.0f); geo.setLongitude(8.0f); a.setGeo(geo); f.setArrivalAirport(a); f.setDepartureTime(QDateTime(QDate::currentDate().addDays(1), QTime(12, 0))); f.setArrivalTime(QDateTime(QDate::currentDate().addDays(1), QTime(14, 0))); res.setReservationFor(f); resMgr.addReservation(res); QCOMPARE(model.rowCount(), 13); // 2x flight, 1x today, 10x weather QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast); fc = model.index(2, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.dateTime().date(), QDate::currentDate()); QCOMPARE(model.index(3, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast); fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 13.0f); QCOMPARE(fc.maximumTemperature(), 52.0f); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(0, 0))); QCOMPARE(model.index(4, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(5, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast); fc = model.index(5, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 8.0f); QCOMPARE(fc.maximumTemperature(), 46.0f); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(14, 0))); QCOMPARE(model.index(6, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast); fc = model.index(6, 0).data(TimelineModel::WeatherForecastRole).value(); QCOMPARE(fc.minimumTemperature(), 8.0f); QCOMPARE(fc.maximumTemperature(), 46.0f); QVERIFY(fc.isValid()); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(2), QTime(0, 0))); // check we get update signals for all weather elements QSignalSpy spy(&model, &TimelineModel::dataChanged); QVERIFY(spy.isValid()); emit weatherMgr.forecastUpdated(); QCOMPARE(spy.size(), 10); fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 13.0f); QCOMPARE(fc.maximumTemperature(), 52.0f); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(0, 0))); fc = model.index(9, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 8.0f); QCOMPARE(fc.maximumTemperature(), 46.0f); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(5), QTime(0, 0))); // add a location change far in the future, this must not change anything geo.setLatitude(60.0f); geo.setLongitude(11.0f); a.setGeo(geo); f.setArrivalAirport(a); f.setDepartureTime(QDateTime(QDate::currentDate().addYears(1), QTime(6, 0))); res.setReservationFor(f); resMgr.addReservation(res); QCOMPARE(model.rowCount(), 14); fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 13.0f); QCOMPARE(fc.maximumTemperature(), 52.0f); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(0, 0))); fc = model.index(9, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 8.0f); QCOMPARE(fc.maximumTemperature(), 46.0f); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(5), QTime(0, 0))); // result is the same when data hasn't been added incrementally model.setReservationManager(nullptr); model.setReservationManager(&resMgr); QCOMPARE(model.rowCount(), 14); fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 13.0f); QCOMPARE(fc.maximumTemperature(), 52.0f); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(0, 0))); fc = model.index(9, 0).data(TimelineModel::WeatherForecastRole).value(); QVERIFY(fc.isValid()); QCOMPARE(fc.minimumTemperature(), 8.0f); QCOMPARE(fc.maximumTemperature(), 46.0f); QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(5), QTime(0, 0))); // clean up auto resId = model.index(13, 0).data(TimelineModel::ReservationIdsRole).toStringList().value(0); resMgr.removeReservation(resId); resId = model.index(4, 0).data(TimelineModel::ReservationIdsRole).toStringList().value(0); resMgr.removeReservation(resId); QCOMPARE(model.rowCount(), 11); // test case: two conesequtive location changes, the first one to an unknown location // result: the weather element before the first location change ends with the start of that // result 2: we get a second weather element the same day after the second location change // TODO } void testMultiTraveller() { using namespace KItinerary; ReservationManager resMgr; clearReservations(&resMgr); TimelineModel model; model.setReservationManager(&resMgr); QCOMPARE(model.rowCount(), 1); // 1x TodayMarker QSignalSpy insertSpy(&model, &TimelineModel::rowsInserted); QVERIFY(insertSpy.isValid()); QSignalSpy updateSpy(&model, &TimelineModel::dataChanged); QVERIFY(updateSpy.isValid()); QSignalSpy rmSpy(&model, &TimelineModel::rowsRemoved); QVERIFY(rmSpy.isValid()); // full import at runtime - resMgr.importReservation(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/google-multi-passenger-flight.json"))); + resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/data/google-multi-passenger-flight.json"))); QCOMPARE(model.rowCount(), 3); // 2x Flight, 1x TodayMarger QCOMPARE(insertSpy.count(), 2); QCOMPARE(updateSpy.count(), 2); QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); QCOMPARE(model.index(0, 0).data(TimelineModel::ReservationIdsRole).toStringList().size(), 2); QCOMPARE(model.index(1, 0).data(TimelineModel::ReservationIdsRole).toStringList().size(), 2); // already existing data model.setReservationManager(nullptr); model.setReservationManager(&resMgr); QCOMPARE(model.rowCount(), 3); // 2x Flight, 1x TodayMarger QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight); QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker); QCOMPARE(model.index(0, 0).data(TimelineModel::ReservationIdsRole).toStringList().size(), 2); QCOMPARE(model.index(1, 0).data(TimelineModel::ReservationIdsRole).toStringList().size(), 2); // update splits element updateSpy.clear(); insertSpy.clear(); auto resId = model.index(1, 0).data(TimelineModel::ReservationIdsRole).toStringList().value(0); QVERIFY(!resId.isEmpty()); auto res = resMgr.reservation(resId).value(); auto flight = res.reservationFor().value(); flight.setDepartureTime(flight.departureTime().addDays(1)); res.setReservationFor(flight); resMgr.updateReservation(resId, res); QCOMPARE(model.rowCount(), 4); QCOMPARE(updateSpy.count(), 1); QCOMPARE(insertSpy.count(), 1); QCOMPARE(rmSpy.count(), 0); // update merges two elements updateSpy.clear(); insertSpy.clear(); rmSpy.clear(); flight.setDepartureTime(flight.departureTime().addDays(-1)); res.setReservationFor(flight); resMgr.updateReservation(resId, res); QCOMPARE(model.rowCount(), 3); QCOMPARE(updateSpy.count(), 1); QCOMPARE(rmSpy.count(), 1); QCOMPARE(insertSpy.count(), 0); // removal of merged items updateSpy.clear(); rmSpy.clear(); clearReservations(&resMgr); QCOMPARE(model.rowCount(), 1); QCOMPARE(rmSpy.count(), 2); QCOMPARE(updateSpy.count(), 2); } }; QTEST_GUILESS_MAIN(TimelineModelTest) #include "timelinemodeltest.moc" diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 8b8cc79..7f7febd 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,111 +1,112 @@ set(itinerary_srcs countryinformation.cpp pkpassmanager.cpp pkpassimageprovider.cpp reservationmanager.cpp timelinemodel.cpp ) ecm_qt_declare_logging_category(itinerary_srcs HEADER logging.h IDENTIFIER Log CATEGORY_NAME org.kde.itinerary ) add_library(itinerary STATIC ${itinerary_srcs}) target_link_libraries(itinerary PUBLIC itinerary-weather KPim::Itinerary KPim::PkPass KF5::I18n Qt5::Network Qt5::Quick ) if (Qt5QuickCompiler_FOUND) qtquick_compiler_add_resources(qml_srcs qml.qrc) else () set(qml_srcs qml.qrc) endif() add_executable(itinerary-app main.cpp applicationcontroller.cpp + contenttypeprober.cpp countrymodel.cpp localizer.cpp settings.cpp tickettokenmodel.cpp util.cpp weatherforecastmodel.cpp ${qml_srcs} ) target_include_directories(itinerary-app PRIVATE ${CMAKE_BINARY_DIR}) target_link_libraries(itinerary-app PRIVATE itinerary KF5::Contacts ) if (ANDROID) # explicitly add runtime dependencies and transitive link dependencies, # so androiddeployqt picks them up target_link_libraries(itinerary-app PRIVATE KF5::Archive KF5::Kirigami2 Qt5::AndroidExtras Qt5::Svg KF5::Prison OpenSSL::SSL ) kirigami_package_breeze_icons(ICONS checkmark dialog-cancel document-edit document-open document-save edit-delete edit-download edit-paste go-home go-next-symbolic help-about map-symbolic meeting-attending settings-configure view-calendar-day view-refresh weather-clear weather-clear-night weather-few-clouds weather-few-clouds-night weather-clouds weather-clouds-night weather-showers-day weather-showers-night weather-showers-scattered-day weather-showers-scattered-night weather-snow-scattered-day weather-snow-scattered-night weather-storm-day weather-storm-night weather-many-clouds weather-fog weather-showers weather-showers-scattered weather-hail weather-snow weather-snow-scattered weather-storm ) else () target_link_libraries(itinerary-app PRIVATE KF5::DBusAddons Qt5::Positioning Qt5::Widgets ) set_target_properties(itinerary-app PROPERTIES OUTPUT_NAME "itinerary") endif() install(TARGETS itinerary-app ${INSTALL_TARGETS_DEFAULT_ARGS}) if (NOT ANDROID) install(PROGRAMS org.kde.itinerary.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES org.kde.itinerary.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) endif() diff --git a/src/app/applicationcontroller.cpp b/src/app/applicationcontroller.cpp index 4e2aab8..4e89fd9 100644 --- a/src/app/applicationcontroller.cpp +++ b/src/app/applicationcontroller.cpp @@ -1,414 +1,434 @@ /* Copyright (C) 2018 Volker Krause This program 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 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 Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "applicationcontroller.h" +#include "contenttypeprober.h" #include "logging.h" #include "pkpassmanager.h" #include "reservationmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_ANDROID #include #include #else #include #include #endif using namespace KItinerary; #ifdef Q_OS_ANDROID static void importReservation(JNIEnv *env, jobject that, jstring data) { Q_UNUSED(that); ApplicationController::instance()->importData(env->GetStringUTFChars(data, 0)); } static const JNINativeMethod methods[] = { {"importReservation", "(Ljava/lang/String;)V", (void*)importReservation} }; Q_DECL_EXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void*) { static bool initialized = false; if (initialized) return JNI_VERSION_1_6; initialized = true; JNIEnv *env = nullptr; if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) { qCWarning(Log) << "Failed to get JNI environment."; return -1; } jclass cls = env->FindClass("org/kde/itinerary/Activity"); if (env->RegisterNatives(cls, methods, sizeof(methods) / sizeof(JNINativeMethod)) < 0) { qCWarning(Log) << "Failed to register native functions."; return -1; } return JNI_VERSION_1_4; } #endif ApplicationController* ApplicationController::s_instance = nullptr; ApplicationController::ApplicationController(QObject* parent) : QObject(parent) #ifdef Q_OS_ANDROID , m_activityResultReceiver(this) #endif { s_instance = this; connect(QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &ApplicationController::clipboardContentChanged); } ApplicationController::~ApplicationController() { s_instance = nullptr; } ApplicationController* ApplicationController::instance() { return s_instance; } void ApplicationController::setReservationManager(ReservationManager* resMgr) { m_resMgr = resMgr; } void ApplicationController::setPkPassManager(PkPassManager* pkPassMgr) { m_pkPassMgr = pkPassMgr; } void ApplicationController::showOnMap(const QVariant &place) { if (place.isNull()) { return; } const auto geo = JsonLdDocument::readProperty(place, "geo").value(); const auto addr = JsonLdDocument::readProperty(place, "address").value(); #ifdef Q_OS_ANDROID QString intentUri; if (geo.isValid()) { intentUri = QLatin1String("geo:") + QString::number(geo.latitude()) + QLatin1Char(',') + QString::number(geo.longitude()); } else if (!addr.isEmpty()) { intentUri = QLatin1String("geo:0,0?q=") + addr.streetAddress() + QLatin1String(", ") + addr.postalCode() + QLatin1Char(' ') + addr.addressLocality() + QLatin1String(", ") + addr.addressCountry(); } else { return; } const auto activity = QtAndroid::androidActivity(); if (activity.isValid()) { activity.callMethod("launchViewIntentFromUri", "(Ljava/lang/String;)V", QAndroidJniObject::fromString(intentUri).object()); } #else if (geo.isValid()) { // zoom out further from airports, they are larger and you usually want to go further away from them const auto zoom = place.userType() == qMetaTypeId() ? 12 : 17; 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); return; } if (!addr.isEmpty()) { 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); } #endif } bool ApplicationController::canNavigateTo(const QVariant& place) { if (place.isNull()) { return false; } if (JsonLdDocument::readProperty(place, "geo").value().isValid()) { return true; } #ifdef Q_OS_ANDROID return !JsonLdDocument::readProperty(place, "address").value().isEmpty(); #else return false; #endif } void ApplicationController::navigateTo(const QVariant& place) { if (place.isNull()) { return; } #ifdef Q_OS_ANDROID const auto geo = JsonLdDocument::readProperty(place, "geo").value(); const auto addr = JsonLdDocument::readProperty(place, "address").value(); QString intentUri; if (geo.isValid()) { intentUri = QLatin1String("google.navigation:q=") + QString::number(geo.latitude()) + QLatin1Char(',') + QString::number(geo.longitude()); } else if (!addr.isEmpty()) { intentUri = QLatin1String("google.navigation:q=") + addr.streetAddress() + QLatin1String(", ") + addr.postalCode() + QLatin1Char(' ') + addr.addressLocality() + QLatin1String(", ") + addr.addressCountry(); } else { return; } const auto activity = QtAndroid::androidActivity(); if (activity.isValid()) { activity.callMethod("launchViewIntentFromUri", "(Ljava/lang/String;)V", QAndroidJniObject::fromString(intentUri).object()); } #else if (m_pendingNavigation) { return; } if (!m_positionSource) { m_positionSource = QGeoPositionInfoSource::createDefaultSource(this); if (!m_positionSource) { qWarning() << "no geo position info source available"; return; } } if (m_positionSource->lastKnownPosition().isValid()) { navigateTo(m_positionSource->lastKnownPosition(), place); } else { m_pendingNavigation = connect(m_positionSource, &QGeoPositionInfoSource::positionUpdated, this, [this, place](const QGeoPositionInfo &pos) { navigateTo(pos, place); }); m_positionSource->requestUpdate(); } #endif } #ifndef Q_OS_ANDROID void ApplicationController::navigateTo(const QGeoPositionInfo &from, const QVariant &to) { qDebug() << from.coordinate() << from.isValid(); disconnect(m_pendingNavigation); if (!from.isValid()) { return; } const auto geo = JsonLdDocument::readProperty(to, "geo").value(); if (geo.isValid()) { QUrl url; url.setScheme(QStringLiteral("https")); url.setHost(QStringLiteral("www.openstreetmap.org")); url.setPath(QStringLiteral("/directions")); QUrlQuery query; query.addQueryItem(QLatin1String("route"), QString::number(from.coordinate().latitude()) + QLatin1Char(',') + QString::number(from.coordinate().longitude()) + QLatin1Char(';') + QString::number(geo.latitude()) + QLatin1Char(',') + QString::number(geo.longitude())); url.setQuery(query); QDesktopServices::openUrl(url); return; } } #endif -static bool isPkPassFile(const QUrl &url) -{ - if (url.isLocalFile()) { - QFile f(url.toLocalFile()); - if (f.open(QFile::ReadOnly)) { - char buffer[4]; - if (f.read(buffer, sizeof(buffer)) != sizeof(buffer)) { - return false; - } - return buffer[0] == 'P' && buffer[1] == 'K' && buffer[2] == 0x03 && buffer[3] == 0x04; - } - } - return url.fileName().endsWith(QLatin1String(".pkpass")); -} - #ifdef Q_OS_ANDROID void ApplicationController::importFromIntent(const QAndroidJniObject &intent) { if (!intent.isValid()) { return; } const auto uri = intent.callObjectMethod("getData", "()Landroid/net/Uri;"); if (!uri.isValid()) { return; } const auto scheme = uri.callObjectMethod("getScheme", "()Ljava/lang/String;"); qCDebug(Log) << uri.callObjectMethod("toString", "()Ljava/lang/String;").toString(); if (scheme.toString() == QLatin1String("content")) { const auto tmpFile = QtAndroid::androidActivity().callObjectMethod("receiveContent", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object()); const auto tmpUrl = QUrl::fromLocalFile(tmpFile.toString()); importLocalFile(tmpUrl, true); return; } const auto uriStr = uri.callObjectMethod("toString", "()Ljava/lang/String;"); importFromUrl(QUrl(uriStr.toString())); } void ApplicationController::ActivityResultReceiver::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &intent) { qCDebug(Log) << receiverRequestCode << resultCode; m_controller->importFromIntent(intent); } #endif void ApplicationController::showImportFileDialog() { #ifdef Q_OS_ANDROID const auto ACTION_OPEN_DOCUMENT = QAndroidJniObject::getStaticObjectField("android/content/Intent", "ACTION_OPEN_DOCUMENT"); QAndroidJniObject intent("android/content/Intent", "(Ljava/lang/String;)V", ACTION_OPEN_DOCUMENT.object()); const auto CATEGORY_OPENABLE = QAndroidJniObject::getStaticObjectField("android/content/Intent", "CATEGORY_OPENABLE"); intent.callObjectMethod("addCategory", "(Ljava/lang/String;)Landroid/content/Intent;", CATEGORY_OPENABLE.object()); intent.callObjectMethod("setType", "(Ljava/lang/String;)Landroid/content/Intent;", QAndroidJniObject::fromString(QStringLiteral("*/*")).object()); QtAndroid::startActivity(intent, 0, &m_activityResultReceiver); #endif } void ApplicationController::importFromClipboard() { if (QGuiApplication::clipboard()->mimeData()->hasUrls()) { const auto urls = QGuiApplication::clipboard()->mimeData()->urls(); for (const auto url : urls) importFromUrl(url); } if (QGuiApplication::clipboard()->mimeData()->hasText()) { const auto content = QGuiApplication::clipboard()->text(); const auto data = IataBcbpParser::parse(content, QDate::currentDate()); ExtractorPostprocessor postproc; postproc.process(data); for (const auto &res : postproc.result()) m_resMgr->addReservation(res); return; } } void ApplicationController::importFromUrl(const QUrl &url) { qCDebug(Log) << url; if (url.isLocalFile()) { importLocalFile(url, false); return; } if (url.scheme().startsWith(QLatin1String("http"))) { if (!m_nam ) { m_nam = new QNetworkAccessManager(this); } auto reqUrl(url); reqUrl.setScheme(QLatin1String("https")); QNetworkRequest req(reqUrl); req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); auto reply = m_nam->get(req); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if (reply->error() != QNetworkReply::NoError) { qCDebug(Log) << reply->url() << reply->errorString(); return; } importData(reply->readAll()); }); return; } qCDebug(Log) << "Unhandled URL type:" << url; } void ApplicationController::importLocalFile(const QUrl &url, bool isTempFile) { qCDebug(Log) << url; - if (isPkPassFile(url)) { - if (isTempFile) - m_pkPassMgr->importPassFromTempFile(url); - else - m_pkPassMgr->importPass(url); - } else { - m_resMgr->importReservation(url); + if (url.isEmpty() || !url.isLocalFile()) + return; + + QFile f(url.toLocalFile()); + if (!f.open(QFile::ReadOnly)) { + qCWarning(Log) << "Failed to open" << url << f.errorString(); + return; + } + + const auto head = f.peek(4); + const auto type = ContentTypeProber::probe(head, url); + switch (type) { + case ContentTypeProber::Unknown: + qCWarning(Log) << "Unknown content type:" << url; + break; + case ContentTypeProber::PkPass: + if (isTempFile) + m_pkPassMgr->importPassFromTempFile(url); + else + m_pkPassMgr->importPass(url); + break; + case ContentTypeProber::JsonLd: + m_resMgr->importReservation(f.readAll()); + break; + case ContentTypeProber::PDF: + // TODO + break; } } void ApplicationController::importData(const QByteArray &data) { qCDebug(Log); - m_resMgr->importReservation(data); + const auto type = ContentTypeProber::probe(data); + switch (type) { + case ContentTypeProber::Unknown: + qCWarning(Log) << "Unknown content type for received data."; + break; + case ContentTypeProber::PkPass: + // TODO + break; + case ContentTypeProber::JsonLd: + m_resMgr->importReservation(data); + break; + case ContentTypeProber::PDF: + // TODO + break; + } } void ApplicationController::checkCalendar() { #ifdef Q_OS_ANDROID const auto activity = QtAndroid::androidActivity(); if (activity.isValid()) { activity.callMethod("checkCalendar"); } #endif } bool ApplicationController::hasClipboardContent() const { return QGuiApplication::clipboard()->mimeData()->hasText() || QGuiApplication::clipboard()->mimeData()->hasUrls(); } diff --git a/src/app/contenttypeprober.cpp b/src/app/contenttypeprober.cpp new file mode 100644 index 0000000..25d6cfa --- /dev/null +++ b/src/app/contenttypeprober.cpp @@ -0,0 +1,45 @@ +/* + Copyright (C) 2018 Volker Krause + + This program 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 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 Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "contenttypeprober.h" + +#include +#include + +ContentTypeProber::Type ContentTypeProber::probe(const QByteArray &head, const QUrl &url) +{ + // check content + if (head.size() >= 4) { + if (head[0] == 'P' && head[1] == 'K' && head[2] == 0x03 && head[3] == 0x04) + return PkPass; + if (head[0] == '%' && head[1] == 'P' && head[2] == 'D' && head[3] == 'F') + return PDF; + if (head[0] == '[' || head[0] == '{') + return JsonLd; + } + + // guess from file extension + const auto fn = url.fileName(); + if (fn.endsWith(QLatin1String(".pkpass"), Qt::CaseInsensitive)) + return PkPass; + if (fn.endsWith(QLatin1String(".pdf"), Qt::CaseInsensitive)) + return PDF; + if (fn.endsWith(QLatin1String(".json"), Qt::CaseInsensitive) || fn.endsWith(QLatin1String(".jsonld"), Qt::CaseInsensitive)) + return JsonLd; + + return Unknown; +} diff --git a/src/app/contenttypeprober.h b/src/app/contenttypeprober.h new file mode 100644 index 0000000..44f5972 --- /dev/null +++ b/src/app/contenttypeprober.h @@ -0,0 +1,38 @@ +/* + Copyright (C) 2018 Volker Krause + + This program 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 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 Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef CONTENTTYPEPROBER_H +#define CONTENTTYPEPROBER_H + +#include + +class QByteArray; + +/** Detect file type by content and/or URL. */ +namespace ContentTypeProber +{ + enum Type { + Unknown, + PkPass, + PDF, + JsonLd + }; + + Type probe(const QByteArray &head, const QUrl &url = {}); +} + +#endif // CONTENTTYPEPROBER_H diff --git a/src/app/reservationmanager.cpp b/src/app/reservationmanager.cpp index 3489c7a..ed3a582 100644 --- a/src/app/reservationmanager.cpp +++ b/src/app/reservationmanager.cpp @@ -1,250 +1,236 @@ /* Copyright (C) 2018 Volker Krause This program 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 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 Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "reservationmanager.h" #include "pkpassmanager.h" #include "logging.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; ReservationManager::ReservationManager(QObject* parent) : QObject(parent) { } ReservationManager::~ReservationManager() = default; void ReservationManager::setPkPassManager(PkPassManager* mgr) { m_passMgr = mgr; connect(mgr, &PkPassManager::passAdded, this, &ReservationManager::passAdded); connect(mgr, &PkPassManager::passUpdated, this, &ReservationManager::passUpdated); connect(mgr, &PkPassManager::passRemoved, this, &ReservationManager::passRemoved); } QVector ReservationManager::reservations() const { const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations"); QDir::root().mkpath(basePath); QVector resIds; for (QDirIterator it(basePath, QDir::NoDotAndDotDot | QDir::Files); it.hasNext();) { it.next(); resIds.push_back(it.fileInfo().baseName()); } return resIds; } QVariant ReservationManager::reservation(const QString& id) const { if (id.isEmpty()) { return {}; } const auto it = m_reservations.constFind(id); if (it != m_reservations.constEnd()) { return it.value(); } const QString resPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations/") + id + QLatin1String(".jsonld"); QFile f(resPath); if (!f.open(QFile::ReadOnly)) { qCWarning(Log) << "Failed to open JSON-LD reservation data file:" << resPath << f.errorString(); return {}; } const auto doc = QJsonDocument::fromJson(f.readAll()); if (!doc.isArray() && doc.array().size() != 1) { qCWarning(Log) << "Invalid JSON-LD reservation data file:" << resPath; return {}; } const auto resData = JsonLdDocument::fromJson(doc.array()); if (resData.size() != 1) { qCWarning(Log) << "Unable to parse JSON-LD reservation data file:" << resPath; return {}; } // re-run post-processing to benefit from newer augmentations ExtractorPostprocessor postproc; postproc.process(resData); if (postproc.result().size() != 1) { qCWarning(Log) << "Post-processing discarded the reservation:" << resPath; return {}; } const auto res = postproc.result().at(0); m_reservations.insert(id, res); return res; } -void ReservationManager::importReservation(const QUrl& filename) -{ - if (!filename.isLocalFile()) - return; - - QFile f(filename.toLocalFile()); - if (!f.open(QFile::ReadOnly)) { - qCWarning(Log) << "Unable to open file:" << f.errorString(); - return; - } - - importReservation(f.readAll()); -} - void ReservationManager::importReservation(const QByteArray& data) { QJsonParseError error; const auto doc = QJsonDocument::fromJson(data, &error); if (!doc.isArray()) { qCWarning(Log) << "Invalid JSON format." << error.errorString() << error.offset; return; } const auto resData = JsonLdDocument::fromJson(doc.array()); importReservations(resData); } void ReservationManager::importReservations(const QVector &resData) { ExtractorPostprocessor postproc; postproc.setContextDate(QDateTime(QDate::currentDate(), QTime(0, 0))); postproc.process(resData); const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations/"); QDir::root().mkpath(basePath); for (auto res : postproc.result()) { QString resId; bool oldResFound = false; // check if we know this one already, and update if that's the case for (const auto &oldResId : reservations()) { const auto oldRes = reservation(oldResId); if (MergeUtil::isSame(oldRes, res)) { res = JsonLdDocument::apply(oldRes, res); resId = oldResId; oldResFound = true; break; } } if (resId.isEmpty()) { resId = QUuid::createUuid().toString(); } const QString path = basePath + resId + QLatin1String(".jsonld"); QFile f(path); if (!f.open(QFile::WriteOnly)) { qCWarning(Log) << "Unable to create file:" << f.errorString(); continue; } f.write(QJsonDocument(JsonLdDocument::toJson({res})).toJson()); m_reservations.insert(resId, res); if (oldResFound) { emit reservationUpdated(resId); } else { emit reservationAdded(resId); } } } void ReservationManager::addReservation(const QVariant &res) { QString resId = QUuid::createUuid().toString(); const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations/"); QDir::root().mkpath(basePath); const QString path = basePath + resId + QLatin1String(".jsonld"); QFile f(path); if (!f.open(QFile::WriteOnly)) { qCWarning(Log) << "Unable to create file:" << f.errorString(); return; } f.write(QJsonDocument(JsonLdDocument::toJson({res})).toJson()); m_reservations.insert(resId, res); emit reservationAdded(resId); } void ReservationManager::updateReservation(const QString &resId, const QVariant &res) { const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations/"); QDir::root().mkpath(basePath); const QString path = basePath + resId + QLatin1String(".jsonld"); QFile f(path); if (!f.open(QFile::WriteOnly)) { qCWarning(Log) << "Unable to open file:" << f.errorString(); return; } f.write(QJsonDocument(JsonLdDocument::toJson({res})).toJson()); m_reservations.insert(resId, res); emit reservationUpdated(resId); } void ReservationManager::removeReservation(const QString& id) { const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations/"); QFile::remove(basePath + QLatin1Char('/') + id + QLatin1String(".jsonld")); emit reservationRemoved(id); m_reservations.remove(id); } void ReservationManager::removeReservations(const QStringList& ids) { for (const auto &id : ids) removeReservation(id); } void ReservationManager::passAdded(const QString& passId) { const auto pass = m_passMgr->pass(passId); ExtractorEngine engine; engine.setPass(pass); const auto data = engine.extract(); const auto res = JsonLdDocument::fromJson(data); importReservations(res); } void ReservationManager::passUpdated(const QString& passId) { passAdded(passId); } void ReservationManager::passRemoved(const QString& passId) { Q_UNUSED(passId); // TODO } diff --git a/src/app/reservationmanager.h b/src/app/reservationmanager.h index bb52ffe..c80904d 100644 --- a/src/app/reservationmanager.h +++ b/src/app/reservationmanager.h @@ -1,66 +1,65 @@ /* Copyright (C) 2018 Volker Krause This program 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 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 Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef RESERVATIONMANAGER_H #define RESERVATIONMANAGER_H #include #include #include class PkPassManager; class QUrl; /** Manages JSON-LD reservation data. */ class ReservationManager : public QObject { Q_OBJECT public: ReservationManager(QObject *parent = nullptr); ~ReservationManager(); void setPkPassManager(PkPassManager *mgr); QVector reservations() const; Q_INVOKABLE QVariant reservation(const QString &id) const; Q_INVOKABLE void addReservation(const QVariant &res); Q_INVOKABLE void updateReservation(const QString &resId, const QVariant &res); Q_INVOKABLE void removeReservation(const QString &id); Q_INVOKABLE void removeReservations(const QStringList &ids); - void importReservation(const QUrl &filename); void importReservation(const QByteArray &data); signals: void reservationAdded(const QString &id); void reservationUpdated(const QString &id); void reservationRemoved(const QString &id); private: void importReservations(const QVector &resData); void passAdded(const QString &passId); void passUpdated(const QString &passId); void passRemoved(const QString &passId); mutable QHash m_reservations; PkPassManager *m_passMgr = nullptr; }; #endif // RESERVATIONMANAGER_H