diff --git a/autotests/integrationtests/iresourcerepository_integration/test_iresourcerepository.cpp b/autotests/integrationtests/iresourcerepository_integration/test_iresourcerepository.cpp index c67dfff..d13d786 100644 --- a/autotests/integrationtests/iresourcerepository_integration/test_iresourcerepository.cpp +++ b/autotests/integrationtests/iresourcerepository_integration/test_iresourcerepository.cpp @@ -1,92 +1,96 @@ /* * Copyright 2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_iresourcerepository.h" #include #include #include #include "core/resourcerepository.h" #include "core/contributorrepository.h" #include "core/language.h" #include "core/icourse.h" #include "core/unit.h" #include "../src/settings.h" +TestIResourceRepository::TestIResourceRepository() + : m_repositoryLocation(QUrl::fromLocalFile(qApp->applicationDirPath() + "/../autotests/data/")) +{ +} + void TestIResourceRepository::init() { // check that test data is deployed at the expected location - QVERIFY(QFile::exists("data/courses/de/de.xml")); - QVERIFY(QFile::exists("data/courses/fr/fr.xml")); + QVERIFY(QFile::exists(m_repositoryLocation.toLocalFile() + "/courses/de/de.xml")); + QVERIFY(QFile::exists(m_repositoryLocation.toLocalFile() + "/courses/fr/fr.xml")); } void TestIResourceRepository::resourceRepository() { - ResourceRepository repository(QUrl::fromLocalFile("data/courses/")); - QCOMPARE(repository.storageLocation().toLocalFile(), "data/courses/"); + ResourceRepository repository(QUrl::fromLocalFile(m_repositoryLocation.toLocalFile() + "/courses/")); + QCOMPARE(repository.storageLocation(), QUrl::fromLocalFile(m_repositoryLocation.toLocalFile() + "/courses/")); performInterfaceTests(&repository); } void TestIResourceRepository::contributorRepository() { - ContributorRepository repository; - repository.setStorageLocation(QUrl::fromLocalFile("data/contributorrepository/")); // contributor repository requires subdirectory "courses" - QCOMPARE(repository.storageLocation().toLocalFile(), "data/contributorrepository/"); + ContributorRepository repository(QUrl::fromLocalFile(m_repositoryLocation.toLocalFile() + "/contributorrepository/")); // contributor repository requires subdirectory "courses" + QCOMPARE(repository.storageLocation(), QUrl::fromLocalFile(m_repositoryLocation.toLocalFile() + "/contributorrepository/")); performInterfaceTests(&repository); } void TestIResourceRepository::performInterfaceTests(IResourceRepository *interface) { QVERIFY(interface->languages().count() > 0); // automatically load languages QCOMPARE(interface->courses().count(), 0); // load courses only on demand // test adding QSignalSpy spyAboutToBeAdded(dynamic_cast(interface), SIGNAL(courseAboutToBeAdded(std::shared_ptr, int))); QSignalSpy spyAdded(dynamic_cast(interface), SIGNAL(courseAdded())); QCOMPARE(spyAboutToBeAdded.count(), 0); QCOMPARE(spyAdded.count(), 0); interface->reloadCourses(); // initial loading of courses QCOMPARE(interface->courses().count(), 2); QCOMPARE(spyAboutToBeAdded.count(), 2); QCOMPARE(spyAdded.count(), 2); // test reloading of courses interface->reloadCourses(); // initial loading of courses QCOMPARE(interface->courses().count(), 2); // test removal // note: repository does not provide removal of courses, yet // test access of courses grouped by language auto languages = interface->languages(); std::shared_ptr german; for (auto language : interface->languages()) { if (language->id() == "de") { german = language; break; } } QVERIFY(german != nullptr); // ensure that German language was found QCOMPARE(interface->courses(german->id()).count(), 1); // there is exactly one German course QCOMPARE(interface->courses(nullptr).count(), 2); // all courses in total are 2 QVERIFY(interface->courses().first()->units().size() > 0); } QTEST_GUILESS_MAIN(TestIResourceRepository) diff --git a/autotests/integrationtests/iresourcerepository_integration/test_iresourcerepository.h b/autotests/integrationtests/iresourcerepository_integration/test_iresourcerepository.h index 955ed06..d9912d6 100644 --- a/autotests/integrationtests/iresourcerepository_integration/test_iresourcerepository.h +++ b/autotests/integrationtests/iresourcerepository_integration/test_iresourcerepository.h @@ -1,59 +1,61 @@ /* * Copyright 2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef TEST_IRESOURCEREPOSITORY_H #define TEST_IRESOURCEREPOSITORY_H #include +#include class IResourceRepository; class TestIResourceRepository : public QObject { Q_OBJECT public: - TestIResourceRepository() = default; + TestIResourceRepository(); private Q_SLOTS: /** * Called before every test case. */ void init(); /** * @brief integration test of ResourceRepository for IResourceRepository interface contract * Test that expectations of the IResourceRepository interface are implemented by * ResourceRepository class. */ void resourceRepository(); /** * @brief integration test of ContributorRepository for IResourceRepository interface contract * Test that expectations of the IResourceRepository interface are implemented by * ContributorRepository class. */ void contributorRepository(); private: void performInterfaceTests(IResourceRepository *repository); + QUrl m_repositoryLocation; }; #endif diff --git a/autotests/unittests/courseresource/test_courseresource.cpp b/autotests/unittests/courseresource/test_courseresource.cpp index bd5ed69..64040db 100644 --- a/autotests/unittests/courseresource/test_courseresource.cpp +++ b/autotests/unittests/courseresource/test_courseresource.cpp @@ -1,192 +1,192 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_courseresource.h" #include "resourcerepositorystub.h" #include "core/language.h" #include "core/unit.h" #include "core/phrase.h" #include "core/phonemegroup.h" #include "core/resources/courseresource.h" #include "../mocks/languagestub.h" #include #include #include #include #include #include #include #include #include TestCourseResource::TestCourseResource() { } void TestCourseResource::init() { } void TestCourseResource::cleanup() { } void TestCourseResource::loadCourseResource() { std::shared_ptr language(new LanguageStub("de")); auto group = std::static_pointer_cast(language)->addPhonemeGroup("id", "title"); group->addPhoneme("g", "G"); group->addPhoneme("u", "U"); std::vector> languages; languages.push_back(language); ResourceRepositoryStub repository(languages); - const QString courseDirectory = "data/courses/de/"; + const QString courseDirectory = qApp->applicationDirPath() + "/../autotests/data/courses/de/"; const QString courseFile = courseDirectory + "de.xml"; auto course = CourseResource::create(QUrl::fromLocalFile(courseFile), &repository); QCOMPARE(course->file().toLocalFile(), courseFile); QCOMPARE(course->id(), "de"); QCOMPARE(course->foreignId(), "artikulate-basic"); QCOMPARE(course->title(), "Artikulate Deutsch"); QCOMPARE(course->description(), "Ein Kurs in (hoch-)deutscher Aussprache."); QVERIFY(course->language() != nullptr); QCOMPARE(course->language()->id(), "de"); QCOMPARE(course->units().count(), 1); QCOMPARE(course->units().first()->course(), course); const auto unit = course->units().first(); QVERIFY(unit != nullptr); QCOMPARE(unit->id(), "1"); QCOMPARE(unit->title(), QStringLiteral("Auf der Straße")); QCOMPARE(unit->foreignId(), "{dd60f04a-eb37-44b7-9787-67aaf7d3578d}"); QCOMPARE(unit->course(), course); QCOMPARE(unit->phrases().count(), 3); // note: this test takes the silent assumption that phrases are added to the list in same // order as they are defined in the file. This assumption should be made explicit or dropped const auto firstPhrase = unit->phrases().first(); QVERIFY(firstPhrase != nullptr); QCOMPARE(firstPhrase->id(), "1"); QCOMPARE(firstPhrase->foreignId(), "{3a4c1926-60d7-44c6-80d1-03165a641c75}"); QCOMPARE(firstPhrase->text(), "Guten Tag."); QCOMPARE(firstPhrase->soundFileUrl(), courseDirectory + "de_01.ogg"); QCOMPARE(firstPhrase->type(), Phrase::Type::Sentence); QCOMPARE(firstPhrase->phonemes().count(), 2); } void TestCourseResource::unitAddAndRemoveHandling() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); - const QString courseDirectory = "data/courses/de/"; + const QString courseDirectory = qApp->applicationDirPath() + "/../autotests/data/courses/de/"; const QString courseFile = courseDirectory + "de.xml"; auto course = CourseResource::create(QUrl::fromLocalFile(courseFile), &repository); // begin of test auto unit = Unit::create(); unit->setId("testunit"); const int initialUnitNumber = course->units().count(); QCOMPARE(initialUnitNumber, 1); QSignalSpy spyAboutToBeAdded(course.get(), SIGNAL(unitAboutToBeAdded(std::shared_ptr, int))); QSignalSpy spyAdded(course.get(), SIGNAL(unitAdded())); QCOMPARE(spyAboutToBeAdded.count(), 0); QCOMPARE(spyAdded.count(), 0); auto sharedUnit = course->addUnit(std::move(unit)); QCOMPARE(course->units().count(), initialUnitNumber + 1); QCOMPARE(spyAboutToBeAdded.count(), 1); QCOMPARE(spyAdded.count(), 1); QCOMPARE(sharedUnit->course(), course); } void TestCourseResource::coursePropertyChanges() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); - const QString courseDirectory = "data/courses/de/"; + const QString courseDirectory = qApp->applicationDirPath() + "/../autotests/data/courses/de/"; const QString courseFile = courseDirectory + "de.xml"; auto course = CourseResource::create(QUrl::fromLocalFile(courseFile), &repository); // id { const QString value = "newId"; QSignalSpy spy(course.get(), SIGNAL(idChanged())); QCOMPARE(spy.count(), 0); course->setId(value); QCOMPARE(course->id(), value); QCOMPARE(spy.count(), 1); } // foreign id { const QString value = "newForeignId"; QSignalSpy spy(course.get(), SIGNAL(foreignIdChanged())); QCOMPARE(spy.count(), 0); course->setForeignId(value); QCOMPARE(course->foreignId(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newTitle"; QSignalSpy spy(course.get(), SIGNAL(titleChanged())); QCOMPARE(spy.count(), 0); course->setTitle(value); QCOMPARE(course->title(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newI18nTitle"; QSignalSpy spy(course.get(), SIGNAL(i18nTitleChanged())); QCOMPARE(spy.count(), 0); course->setI18nTitle(value); QCOMPARE(course->i18nTitle(), value); QCOMPARE(spy.count(), 1); } // description { const QString value = "newDescription"; QSignalSpy spy(course.get(), SIGNAL(descriptionChanged())); QCOMPARE(spy.count(), 0); course->setDescription(value); QCOMPARE(course->description(), value); QCOMPARE(spy.count(), 1); } // language { std::shared_ptr testLanguage; QSignalSpy spy(course.get(), SIGNAL(languageChanged())); QCOMPARE(spy.count(), 0); course->setLanguage(testLanguage); QCOMPARE(course->language(), testLanguage); QCOMPARE(spy.count(), 1); } } QTEST_GUILESS_MAIN(TestCourseResource) diff --git a/autotests/unittests/resourcerepository/test_resourcerepository.cpp b/autotests/unittests/resourcerepository/test_resourcerepository.cpp index 783cf19..b4381ea 100644 --- a/autotests/unittests/resourcerepository/test_resourcerepository.cpp +++ b/autotests/unittests/resourcerepository/test_resourcerepository.cpp @@ -1,84 +1,89 @@ /* * Copyright 2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_resourcerepository.h" #include #include #include #include "src/core/resourcerepository.h" #include "src/core/language.h" +TestResourceRepository::TestResourceRepository() + : m_repositoryLocation(QUrl::fromLocalFile(qApp->applicationDirPath() + "/../autotests/data/")) +{ +} + void TestResourceRepository::init() { // check that test data is deployed at the expected location - QVERIFY(QFile::exists("data/courses/de/de.xml")); - QVERIFY(QFile::exists("data/courses/fr/fr.xml")); + QVERIFY(QFile::exists(m_repositoryLocation.toLocalFile() + "/courses/de/de.xml")); + QVERIFY(QFile::exists(m_repositoryLocation.toLocalFile() + "/courses/fr/fr.xml")); } void TestResourceRepository::cleanup() { // TODO cleanup after test run } void TestResourceRepository::createRepository() { - ResourceRepository repository(QUrl::fromLocalFile("data/courses/")); - QCOMPARE(repository.storageLocation().toLocalFile(), "data/courses/"); + ResourceRepository repository(QUrl::fromLocalFile(m_repositoryLocation.toLocalFile() + "/courses/")); + QCOMPARE(repository.storageLocation(), QUrl::fromLocalFile(m_repositoryLocation.toLocalFile() + "/courses/")); repository.reloadCourses(); QCOMPARE(repository.courses().count(), 2); } void TestResourceRepository::iResourceRepositoryCompatability() { - ResourceRepository repository(QUrl::fromLocalFile("data/courses/")); + ResourceRepository repository(QUrl::fromLocalFile(m_repositoryLocation.toLocalFile() + "/courses/")); IResourceRepository *interface = &repository; - QCOMPARE(interface->storageLocation().toLocalFile(), "data/courses/"); + QCOMPARE(interface->storageLocation(), QUrl::fromLocalFile(m_repositoryLocation.toLocalFile() + "/courses/")); QVERIFY(interface->languages().count() > 0); QCOMPARE(interface->courses().count(), 0); // test adding QSignalSpy spyAboutToBeAdded(dynamic_cast(interface), SIGNAL(courseAboutToBeAdded(std::shared_ptr, int))); QSignalSpy spyAdded(dynamic_cast(interface), SIGNAL(courseAdded())); QCOMPARE(spyAboutToBeAdded.count(), 0); QCOMPARE(spyAdded.count(), 0); repository.reloadCourses(); QCOMPARE(interface->courses().count(), 2); QCOMPARE(spyAboutToBeAdded.count(), 2); QCOMPARE(spyAdded.count(), 2); // test removal // note: repository does not provide removal of courses, yet // test access of courses grouped by language auto languages = interface->languages(); std::shared_ptr german; for (auto language : interface->languages()) { if (language->id() == "de") { german = language; break; } } QVERIFY(german != nullptr); // ensure that German language was found QCOMPARE(interface->courses(german->id()).count(), 1); // there is exactly one German course QCOMPARE(interface->courses(nullptr).count(), 2); // all courses in total are 2 } QTEST_GUILESS_MAIN(TestResourceRepository) diff --git a/autotests/unittests/resourcerepository/test_resourcerepository.h b/autotests/unittests/resourcerepository/test_resourcerepository.h index ca3ec07..7f8bcfe 100644 --- a/autotests/unittests/resourcerepository/test_resourcerepository.h +++ b/autotests/unittests/resourcerepository/test_resourcerepository.h @@ -1,56 +1,60 @@ /* * Copyright 2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef TEST_RESOURCEREPOSITORY_H #define TEST_RESOURCEREPOSITORY_H #include +#include class TestResourceRepository : public QObject { Q_OBJECT public: - TestResourceRepository() = default; + TestResourceRepository(); private Q_SLOTS: /** * Called before every test case. */ void init(); /** * Called after every test case. */ void cleanup(); /** * Create and load repository with simple testdata */ void createRepository(); /** * @brief integration test for IResourceRepository interface * Test expectations of the IResourceRepository interface. */ void iResourceRepositoryCompatability(); + +private: + QUrl m_repositoryLocation; }; #endif diff --git a/src/core/contributorrepository.cpp b/src/core/contributorrepository.cpp index 90064d5..7ae3c2f 100644 --- a/src/core/contributorrepository.cpp +++ b/src/core/contributorrepository.cpp @@ -1,418 +1,425 @@ /* * Copyright 2013-2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "contributorrepository.h" #include "artikulate_debug.h" #include "language.h" #include "unit.h" #include "phrase.h" #include "phoneme.h" #include "phonemegroup.h" #include "resources/editablecourseresource.h" #include "resources/skeletonresource.h" #include "liblearnerprofile/src/profilemanager.h" #include "liblearnerprofile/src/learninggoal.h" #include #include #include #include #include #include ContributorRepository::ContributorRepository() : IEditableRepository() { loadLanguageResources(); } +ContributorRepository::ContributorRepository(QUrl storageLocation) + : IEditableRepository() + , m_storageLocation(std::move(storageLocation)) +{ + loadLanguageResources(); +} + ContributorRepository::~ContributorRepository() = default; void ContributorRepository::loadLanguageResources() { // load language resources // all other resources are only loaded on demand QDir dir(":/artikulate/languages/"); dir.setFilter(QDir::Files | QDir::NoSymLinks); QFileInfoList list = dir.entryInfoList(); for (int i = 0; i < list.size(); ++i) { QFileInfo fileInfo = list.at(i); if (fileInfo.completeSuffix() != QLatin1String("xml")) { continue; } addLanguage(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); } } void ContributorRepository::sync() { for (auto iter = m_courses.begin(); iter != m_courses.end(); ++iter) { for (auto course : iter.value()) { course->sync(); } } for (auto skeleton : m_skeletonResources) { skeleton->sync(); } } bool ContributorRepository::modified() const { for (auto iter = m_courses.constBegin(); iter != m_courses.constEnd(); ++iter) { for (auto course : iter.value()) { if (course->isModified()) { return true; } } } for (auto const &courseRes : m_skeletonResources) { if (courseRes->isModified()) { return true; } } return false; } void ContributorRepository::addLanguage(const QUrl &languageFile) { if (m_loadedResources.contains(languageFile.toLocalFile())) { return; } auto language = Language::create(languageFile); emit languageResourceAboutToBeAdded(language, m_languages.count()); m_languages.append(language); m_loadedResources.append(languageFile.toLocalFile()); m_courses.insert(language->id(), QVector>()); emit languageResourceAdded(); } QUrl ContributorRepository::storageLocation() const { return m_storageLocation; } void ContributorRepository::setStorageLocation(const QUrl &path) { m_storageLocation = path; emit repositoryChanged(); reloadCourses(); } QVector> ContributorRepository::languages() const { return m_languages; } std::shared_ptr ContributorRepository::language(int index) const { Q_ASSERT(index >= 0 && index < m_languages.count()); return m_languages.at(index); } ILanguage * ContributorRepository::language(LearnerProfile::LearningGoal *learningGoal) const { if (!learningGoal) { return nullptr; } if (learningGoal->category() != LearnerProfile::LearningGoal::Language) { qCritical() << "Cannot translate non-language learning goal to language"; return nullptr; } for (auto language : m_languages) { if (language->id() == learningGoal->identifier()) { return language.get(); } } qCritical() << "No language registered with identifier " << learningGoal->identifier() << ": aborting"; return nullptr; } QVector> ContributorRepository::courseResources(std::shared_ptr language) { if (!language) { QVector> courses; for (auto iter = m_courses.constBegin(); iter != m_courses.constEnd(); ++iter) { courses.append(iter.value()); } return courses; } // return empty list if no course available for language if (!m_courses.contains(language->id())) { return QVector>(); } return m_courses[language->id()]; } QVector> ContributorRepository::courses() const { QVector> courses; for (const auto &courseList : m_courses) { for (const auto &course : courseList) { courses.append(course); } } return courses; } QVector> ContributorRepository::editableCourses() const { QVector> courses; for (const auto &courseList : m_courses) { for (const auto &course : courseList) { courses.append(course); } } return courses; } QVector> ContributorRepository::courses(const QString &languageId) const { if (languageId.isEmpty()) { return courses(); } QVector> courses; if (m_courses.contains(languageId)) { for (const auto &course : m_courses[languageId]) { courses.append(course); } } return courses; } std::shared_ptr ContributorRepository::editableCourse(std::shared_ptr language, int index) const { Q_ASSERT(m_courses.contains(language->id())); Q_ASSERT(index >= 0 && index < m_courses[language->id()].count()); return m_courses[language->id()].at(index); } void ContributorRepository::reloadCourseOrSkeleton(std::shared_ptr courseOrSkeleton) { if (!courseOrSkeleton) { qCritical() << "Cannot reload non-existing course"; return; } if (!courseOrSkeleton->file().isValid()) { qCritical() << "Cannot reload temporary file, aborting."; return; } // figure out if this is a course or a skeleton if (courseOrSkeleton->language()) { // only course files have a language //TODO better add a check if this is contained in the course list // to catch possible errors QUrl file = courseOrSkeleton->file(); m_loadedResources.removeOne(courseOrSkeleton->file().toLocalFile()); removeCourse(courseOrSkeleton); addCourse(file); } else { for (auto resource : m_skeletonResources) { if (resource->id() == courseOrSkeleton->id()) { // TODO no reload available return; } } } } void ContributorRepository::reloadCourses() { // register skeleton resources QDir skeletonDirectory = QDir(storageLocation().toLocalFile()); skeletonDirectory.setFilter(QDir::Files | QDir::Hidden); if (!skeletonDirectory.cd(QStringLiteral("skeletons"))) { qCritical() << "There is no subdirectory \"skeletons\" in directory " << skeletonDirectory.path() << " cannot load skeletons."; } else { // read skeletons QFileInfoList list = skeletonDirectory.entryInfoList(); for (int i = 0; i < list.size(); ++i) { QFileInfo fileInfo = list.at(i); addSkeleton(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); } } // register contributor course files QDir courseDirectory(storageLocation().toLocalFile()); if (!courseDirectory.cd(QStringLiteral("courses"))) { qCritical() << "There is no subdirectory \"courses\" in directory " << courseDirectory.path() << " cannot load courses."; } else { // find courses courseDirectory.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); QFileInfoList courseDirList = courseDirectory.entryInfoList(); // traverse all course directories for (const QFileInfo &info : courseDirList) { QDir courseDir = QDir(info.absoluteFilePath()); courseDir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); QFileInfoList courseLangDirList = courseDir.entryInfoList(); // traverse all language directories for each course for (const QFileInfo &langInfo : courseLangDirList) { QDir courseLangDir = QDir(langInfo.absoluteFilePath()); courseLangDir.setFilter(QDir::Files); QStringList nameFilters; nameFilters.append(QStringLiteral("*.xml")); QFileInfoList courses = courseLangDir.entryInfoList(nameFilters); // find and add course files for (const QFileInfo &courseInfo : courses) { addCourse(QUrl::fromLocalFile(courseInfo.filePath())); } } } } //TODO this signal should only be emitted when repository was added/removed // yet the call to this method is very seldom and emitting it too often is not that harmful emit repositoryChanged(); } void ContributorRepository::updateCourseFromSkeleton(std::shared_ptr course) { //TODO implement status information that are shown at mainwindow if (course->foreignId().isEmpty()) { qCritical() << "No skeleton ID specified, aborting update."; return; } std::shared_ptr skeleton; for (const auto &iter : m_skeletonResources) { if (iter->id() == course->foreignId()) { skeleton = iter; break; } } if (!skeleton) { qCritical() << "Could not find skeleton with id " << course->foreignId() << ", aborting update."; } else { course->updateFrom(skeleton); } } std::shared_ptr ContributorRepository::addCourse(const QUrl &courseFile) { std::shared_ptr course; // skip already loaded resources if (m_loadedResources.contains(courseFile.toLocalFile())) { // TODO return existing resource } else { course = EditableCourseResource::create(courseFile, this); if (course->language() == nullptr) { qCritical() << "Could not load course, language unknown:" << courseFile.toLocalFile(); course.reset(); } else { // this is the regular case m_loadedResources.append(courseFile.toLocalFile()); const QString languageId = course->language()->id(); Q_ASSERT(!languageId.isEmpty()); if (!m_courses.contains(languageId)) { m_courses.insert(languageId, QVector>()); } emit courseAboutToBeAdded(course, m_courses[course->language()->id()].count()); m_courses[languageId].append(course); emit courseAdded(); emit languageCoursesChanged(); } } return course; } void ContributorRepository::removeCourse(std::shared_ptr course) { for (int index = 0; index < m_courses[course->language()->id()].length(); ++index) { if (m_courses[course->language()->id()].at(index) == course) { emit courseAboutToBeRemoved(index); m_courses[course->language()->id()].removeAt(index); emit courseRemoved(); return; } } } IEditableCourse * ContributorRepository::createCourse(std::shared_ptr language, std::shared_ptr skeleton) { // set path QString path = QStringLiteral("%1/%2/%3/%4/%4.xml") .arg(storageLocation().toLocalFile(), QStringLiteral("courses"), skeleton->id(), language->id()); auto course = EditableCourseResource::create(QUrl::fromLocalFile(path), this); Q_ASSERT(course); course->setId(QUuid::createUuid().toString()); course->setTitle(skeleton->title()); course->setDescription(skeleton->description()); course->setLanguage(language); course->setForeignId(skeleton->id()); return course.get(); } std::shared_ptr ContributorRepository::addSkeleton(const QUrl &file) { std::shared_ptr resource; // skip already loaded resources if (m_loadedResources.contains(file.toLocalFile())) { qCInfo(ARTIKULATE_LOG()) << "Skeleton already loaded, using known resource:" << file; for (auto skeleton : m_skeletonResources) { if (skeleton->file() == file) { resource = skeleton; break; } } } else { resource = SkeletonResource::create(file, this); m_loadedResources.append(resource->file().toLocalFile()); emit skeletonAboutToBeAdded(resource.get(), m_skeletonResources.count()); m_skeletonResources.append(resource); emit skeletonAdded(); } return resource; } void ContributorRepository::removeSkeleton(SkeletonResource *skeleton) { for (int index = 0; index < m_skeletonResources.length(); ++index) { if (m_skeletonResources.at(index)->id() == skeleton->id()) { emit skeletonAboutToBeRemoved(index, index); m_skeletonResources.removeAt(index); emit skeletonRemoved(); return; } } } QVector> ContributorRepository::skeletons() const { QVector> skeletonList; for (const auto &skeleton : m_skeletonResources) { skeletonList.append(skeleton); } return skeletonList; } diff --git a/src/core/contributorrepository.h b/src/core/contributorrepository.h index ce17888..b573a7c 100644 --- a/src/core/contributorrepository.h +++ b/src/core/contributorrepository.h @@ -1,196 +1,197 @@ /* * Copyright 2013-2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef CONTRIBUTORREPOSITORY_H #define CONTRIBUTORREPOSITORY_H #include "artikulatecore_export.h" #include "ieditablerepository.h" #include #include #include #include #include #include #include #include "liblearnerprofile/src/learninggoal.h" class SkeletonResource; class EditableCourseResource; class LanguageResource; class Language; class ICourse; /** * @class ContributorRepository * This class handles the resources of a contributor. */ class ARTIKULATECORE_EXPORT ContributorRepository : public IEditableRepository { Q_OBJECT Q_INTERFACES(IResourceRepository) Q_INTERFACES(IEditableRepository) Q_PROPERTY(QUrl repositoryUrl READ storageLocation WRITE setStorageLocation NOTIFY repositoryChanged) public: explicit ContributorRepository(); + explicit ContributorRepository(QUrl storageLocation); ~ContributorRepository() override; /** * save all changes to course resources */ void sync(); /** * \return \c true if any course or skeleton is modified, otherwise \c false */ bool modified() const; /** * \return path to working repository, if one is set */ QUrl storageLocation() const override; /** * Set path to central storage location * \param path the path to the storage location directory */ void setStorageLocation(const QUrl &path); QVector> languages() const override; /** * \return language by \p index */ std::shared_ptr language(int index) const; /** * \return language by \p learningGoal */ Q_INVOKABLE ILanguage * language(LearnerProfile::LearningGoal* learningGoal) const; QVector> courses() const override; QVector> courses(const QString &languageId) const override; QVector> editableCourses() const override; /** * \return list of all loaded courses for language \p language */ QVector> courseResources(std::shared_ptr language); std::shared_ptr editableCourse(std::shared_ptr language, int index) const override; /** * Reset the file for this course or skeleton. * * \param course the course to be reloaded */ void reloadCourseOrSkeleton(std::shared_ptr course); /** * @brief Implementation of course resource reloading */ void reloadCourses() override; void updateCourseFromSkeleton(std::shared_ptr course) override; /** * Add language to resource manager by parsing the given language specification file. * * \param languageFile is the local XML file containing the language */ void addLanguage(const QUrl &languageFile); /** * Adds course to resource manager by parsing the given course specification file. * * \param courseFile is the local XML file containing the course * \return true if loaded successfully, otherwise false */ std::shared_ptr addCourse(const QUrl &courseFile); /** * Adds course to resource manager. If the course's language is not registered, the language * is registered by this method. * * \param resource the course resource to add to resource manager */ std::shared_ptr addCourseResource(std::unique_ptr resource); /** * Remove course from resource manager. If the course is modified its changes are NOT * written. For writing changes, the Course::sync() method must be called directly. * * \param course is the course to be removed */ void removeCourse(std::shared_ptr course); /** * Create new course for \p language and derived from \p skeleton. * * \return created course */ Q_INVOKABLE IEditableCourse * createCourse(std::shared_ptr language, std::shared_ptr skeleton); /** * Adds skeleton resource to resource manager * * \param resource the skeleton resource to add to resource manager */ std::shared_ptr addSkeleton(const QUrl &skeletonFile); /** * Remove skeleton from resource manager. If the skeleton is modified its changes are NOT * written. For writing changes, the Skeleton::sync() method must be called directly. * * \param skeleton is the skeleton to be removed */ void removeSkeleton(SkeletonResource *skeleton); QVector> skeletons() const override; Q_SIGNALS: void languageResourceAdded(); void languageResourceAboutToBeAdded(std::shared_ptr,int); void languageResourceRemoved(); void languageResourceAboutToBeRemoved(int); void repositoryChanged(); void skeletonAdded(); void skeletonAboutToBeAdded(ICourse*,int); void skeletonRemoved(); void skeletonAboutToBeRemoved(int,int); void languageCoursesChanged(); private: /** * This method loads all language files that are provided in the standard directories * for this application. */ void loadLanguageResources(); QUrl m_storageLocation; QVector> m_languages; QMap> > m_courses; //!> (language-id, course-resource) QVector> m_skeletonResources; QStringList m_loadedResources; }; #endif diff --git a/src/core/resourcerepository.cpp b/src/core/resourcerepository.cpp index e2beaf6..613af81 100644 --- a/src/core/resourcerepository.cpp +++ b/src/core/resourcerepository.cpp @@ -1,159 +1,160 @@ /* * Copyright 2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "resourcerepository.h" #include "artikulate_debug.h" #include "resources/courseresource.h" #include "core/language.h" #include #include #include #include ResourceRepository::ResourceRepository() : ResourceRepository(QUrl::fromLocalFile(QStandardPaths::standardLocations(QStandardPaths::DataLocation).constFirst() + QStringLiteral("/courses/"))) { } ResourceRepository::ResourceRepository(const QUrl &storageLocation) : IResourceRepository() - , m_storageLocation(storageLocation.toLocalFile()) + , m_storageLocation(storageLocation) { + qCDebug(ARTIKULATE_CORE()) << "Repository created from with location" << m_storageLocation; // load language resources // all other resources are only loaded on demand QDir dir(":/artikulate/languages/"); dir.setFilter(QDir::Files | QDir::NoSymLinks); QFileInfoList list = dir.entryInfoList(); for (int i = 0; i < list.size(); ++i) { QFileInfo fileInfo = list.at(i); if (fileInfo.completeSuffix() != QLatin1String("xml")) { continue; } loadLanguage(fileInfo.absoluteFilePath()); } } ResourceRepository::~ResourceRepository() = default; QUrl ResourceRepository::storageLocation() const { return m_storageLocation; } QVector> ResourceRepository::courses() const { QVector> courses; for (const auto &course : m_courses) { courses.append(course); } return courses; } QVector> ResourceRepository::courses(const QString &languageId) const { QVector> courses; for (const auto &course : m_courses) { if (course->language() && course->language()->id() == languageId) { continue; } courses.append(course); } return courses; } QVector> ResourceRepository::languages() const { QVector> languages; for (const auto &language : m_languages) { if (language == nullptr) { continue; } languages.append(language); } return languages; } std::shared_ptr ResourceRepository::language(const QString &id) const { if (m_languages.contains(id)) { return m_languages.value(id); } return nullptr; } void ResourceRepository::reloadCourses() { std::function scanDirectoryForXmlCourseFiles = [this](QDir dir) { dir.setFilter(QDir::Files | QDir::NoSymLinks); QFileInfoList list = dir.entryInfoList(); for (int i = 0; i < list.size(); ++i) { QFileInfo fileInfo = list.at(i); if (fileInfo.completeSuffix() != QLatin1String("xml")) { continue; } loadCourse(fileInfo.absoluteFilePath()); } }; QDir rootDirectory = QDir(m_storageLocation.toLocalFile()); QDirIterator it(rootDirectory, QDirIterator::Subdirectories); qCInfo(ARTIKULATE_CORE()) << "Loading courses from" << rootDirectory.absolutePath(); while (it.hasNext()) { scanDirectoryForXmlCourseFiles(it.next()); } } bool ResourceRepository::loadCourse(const QString &resourceFile) { qCDebug(ARTIKULATE_CORE()) << "Loading resource" << resourceFile; // skip already loaded resources if (m_loadedCourses.contains(resourceFile)) { qCWarning(ARTIKULATE_CORE()) << "Reloading of resources not yet supported, skippen course"; return false; } auto resource = CourseResource::create(QUrl::fromLocalFile(resourceFile), this); if (resource->language() == nullptr) { qCCritical(ARTIKULATE_CORE()) << "Could not load course, language unknown:" << resourceFile; return false; } emit courseAboutToBeAdded(resource, m_courses.count() - 1); m_courses.append(resource); emit courseAdded(); m_loadedCourses.append(resourceFile); return true; } bool ResourceRepository::loadLanguage(const QString &resourceFile) { auto language = Language::create(QUrl::fromLocalFile(resourceFile)); if (!language) { qCWarning(ARTIKULATE_CORE()) << "Could not load language" << resourceFile; return false; } if (m_languages.contains(language->id())) { qCWarning(ARTIKULATE_CORE()) << "Could not load language" << resourceFile; return false; } m_languages.insert(language->id(), language); return true; }