diff --git a/autotests/testdata/courses/fr.xml b/autotests/testdata/courses/fr.xml
index 3ed0e11..589df0c 100644
--- a/autotests/testdata/courses/fr.xml
+++ b/autotests/testdata/courses/fr.xml
@@ -1,38 +1,38 @@
fr
ArtiKulate Français
Course française.
fr
1
Cuisine Français
1
Qu'est-ce que vous avez choisi?
-
+ de_01.ogg
sentence
2
Moi, comme entrée, une salade exotique.
sentence
3
eau
word
oh
diff --git a/autotests/unittests/courseresource/test_courseresource.cpp b/autotests/unittests/courseresource/test_courseresource.cpp
index 7e47504..c2fd67f 100644
--- a/autotests/unittests/courseresource/test_courseresource.cpp
+++ b/autotests/unittests/courseresource/test_courseresource.cpp
@@ -1,192 +1,217 @@
/*
* 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 "../mocks/languagestub.h"
#include "core/language.h"
#include "core/phonemegroup.h"
#include "core/phrase.h"
#include "core/resources/courseresource.h"
#include "core/unit.h"
#include "resourcerepositorystub.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 = qApp->applicationDirPath() + "/../autotests/unittests/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::loadCourseResourceSkipIncomplete()
+{
+ 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 courseFile = courseDirectory + "de.xml";
+
+ auto course = CourseResource::create(QUrl::fromLocalFile(courseFile), &repository, true);
+ QCOMPARE(course->file().toLocalFile(), courseFile);
+ QCOMPARE(course->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->phrases().count(), 2);
+}
+
void TestCourseResource::unitAddAndRemoveHandling()
{
// boilerplate
std::shared_ptr language(new LanguageStub("de"));
ResourceRepositoryStub repository({language});
const QString courseDirectory = qApp->applicationDirPath() + "/../autotests/unittests/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 = qApp->applicationDirPath() + "/../autotests/unittests/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/courseresource/test_courseresource.h b/autotests/unittests/courseresource/test_courseresource.h
index 4a5591d..e9cd0e3 100644
--- a/autotests/unittests/courseresource/test_courseresource.h
+++ b/autotests/unittests/courseresource/test_courseresource.h
@@ -1,64 +1,70 @@
/*
* 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 TESTCOURSERESOURCE_H
#define TESTCOURSERESOURCE_H
#include
#include
class TestCourseResource : public QObject
{
Q_OBJECT
public:
TestCourseResource();
private slots:
/**
* @brief Called before every test case.
*/
void init();
/**
* @brief Called after every test case.
*/
void cleanup();
/**
* @brief Test simple loading of course resource XML file
*/
void loadCourseResource();
+
+ /**
+ * @brief Test simple loading of course resource XML file and skip all incomplete units/phrases
+ */
+ void loadCourseResourceSkipIncomplete();
+
/**
* @brief Test handling of unit insertions (specifically, the signals)
*/
void unitAddAndRemoveHandling();
/**
* @brief Test of all course property changes except unit handling
*/
void coursePropertyChanges();
private:
bool m_systemUseCourseRepositoryValue;
};
#endif
diff --git a/src/core/resourcerepository.cpp b/src/core/resourcerepository.cpp
index 5ce2f6e..c89d784 100644
--- a/src/core/resourcerepository.cpp
+++ b/src/core/resourcerepository.cpp
@@ -1,160 +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 "core/language.h"
#include "resources/courseresource.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)
{
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);
+ auto resource = CourseResource::create(QUrl::fromLocalFile(resourceFile), this, true);
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;
}
diff --git a/src/core/resources/courseparser.cpp b/src/core/resources/courseparser.cpp
index bbd0acb..83afa5a 100644
--- a/src/core/resources/courseparser.cpp
+++ b/src/core/resources/courseparser.cpp
@@ -1,409 +1,412 @@
/*
* 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 "courseparser.h"
#include "artikulate_debug.h"
#include "core/ieditablecourse.h"
#include "core/language.h"
#include "core/phoneme.h"
#include "core/phrase.h"
#include "core/unit.h"
#include
#include
#include
#include
#include
#include
#include
#include
QXmlSchema CourseParser::loadXmlSchema(const QString &schemeName)
{
QString relPath = QStringLiteral(":/artikulate/schemes/%1.xsd").arg(schemeName);
QUrl file = QUrl::fromLocalFile(relPath);
QXmlSchema schema;
if (file.isEmpty() || schema.load(file) == false) {
qCWarning(ARTIKULATE_PARSER()) << "Schema at file " << file.toLocalFile() << " is invalid.";
}
return schema;
}
QDomDocument CourseParser::loadDomDocument(const QUrl &path, const QXmlSchema &schema)
{
QDomDocument document;
QXmlSchemaValidator validator(schema);
if (!validator.validate(path)) {
qCWarning(ARTIKULATE_PARSER()) << "Schema is not valid, aborting loading of XML document:" << path.toLocalFile();
return document;
}
QString errorMsg;
QFile file(path.toLocalFile());
if (file.open(QIODevice::ReadOnly)) {
if (!document.setContent(&file, &errorMsg)) {
qCWarning(ARTIKULATE_PARSER()) << errorMsg;
}
} else {
qCWarning(ARTIKULATE_PARSER()) << "Could not open XML document " << path.toLocalFile() << " for reading, aborting.";
}
return document;
}
-std::vector> CourseParser::parseUnits(const QUrl &path, QVector> phonemes)
+std::vector> CourseParser::parseUnits(const QUrl &path, QVector> phonemes, bool skipIncomplete)
{
std::vector> units;
QFileInfo info(path.toLocalFile());
if (!info.exists()) {
qCCritical(ARTIKULATE_PARSER()()) << "No course file available at location" << path.toLocalFile();
return units;
}
QXmlStreamReader xml;
QFile file(path.toLocalFile());
if (file.open(QIODevice::ReadOnly)) {
xml.setDevice(&file);
xml.readNextStartElement();
while (!xml.atEnd() && !xml.hasError()) {
bool elementOk {false};
QXmlStreamReader::TokenType token = xml.readNext();
if (token == QXmlStreamReader::StartDocument) {
continue;
}
if (token == QXmlStreamReader::StartElement) {
if (xml.name() == "units") {
continue;
} else if (xml.name() == "unit") {
- auto unit = parseUnit(xml, path, phonemes, elementOk);
+ auto unit = parseUnit(xml, path, phonemes, skipIncomplete, elementOk);
if (elementOk) {
units.push_back(std::move(unit));
}
}
}
}
if (xml.hasError()) {
qCCritical(ARTIKULATE_PARSER()) << "Error occurred when reading Course XML file:" << path.toLocalFile();
}
} else {
qCCritical(ARTIKULATE_PARSER()) << "Could not open course file" << path.toLocalFile();
}
xml.clear();
file.close();
return units;
}
-std::shared_ptr CourseParser::parseUnit(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool &ok)
+std::shared_ptr CourseParser::parseUnit(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool skipIncomplete, bool &ok)
{
std::shared_ptr unit = Unit::create();
ok = true;
if (xml.tokenType() != QXmlStreamReader::StartElement && xml.name() == "unit") {
qCWarning(ARTIKULATE_PARSER()) << "Expected to parse 'unit' element, aborting here";
return unit;
}
xml.readNext();
while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == "unit")) {
if (xml.tokenType() == QXmlStreamReader::StartElement) {
bool elementOk {false};
if (xml.name() == "id") {
unit->setId(parseElement(xml, elementOk));
ok &= elementOk;
} else if (xml.name() == "foreignId") {
unit->setForeignId(parseElement(xml, elementOk));
ok &= elementOk;
} else if (xml.name() == "title") {
unit->setTitle(parseElement(xml, elementOk));
ok &= elementOk;
} else if (xml.name() == "phrases") {
// nothing to do
} else if (xml.name() == "phrase") {
auto phrase = parsePhrase(xml, path, phonemes, elementOk);
- if (elementOk) {
+ if (elementOk && (!skipIncomplete || !phrase->soundFileUrl().isEmpty())) {
unit->addPhrase(phrase, unit->phrases().size());
}
ok &= elementOk;
} else {
qCWarning(ARTIKULATE_PARSER()) << "Skipping unknown token" << xml.name();
}
}
xml.readNext();
}
if (!ok) {
qCWarning(ARTIKULATE_PARSER()) << "Errors occurred while parsing unit" << unit->title() << unit->id();
}
return unit;
}
std::shared_ptr CourseParser::parsePhrase(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool &ok)
{
std::shared_ptr phrase = Phrase::create();
ok = true;
if (xml.tokenType() != QXmlStreamReader::StartElement && xml.name() == "phrase") {
qCWarning(ARTIKULATE_PARSER()) << "Expected to parse 'phrase' element, aborting here";
ok = false;
return phrase;
}
xml.readNext();
while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == "phrase")) {
if (xml.tokenType() == QXmlStreamReader::StartElement) {
bool elementOk {false};
if (xml.name() == "id") {
phrase->setId(parseElement(xml, elementOk));
ok &= elementOk;
} else if (xml.name() == "foreignId") {
phrase->setForeignId(parseElement(xml, elementOk));
ok &= elementOk;
} else if (xml.name() == "text") {
phrase->setText(parseElement(xml, elementOk));
ok &= elementOk;
} else if (xml.name() == "i18nText") {
phrase->seti18nText(parseElement(xml, elementOk));
ok &= elementOk;
} else if (xml.name() == "soundFile") {
- phrase->setSound(QUrl::fromLocalFile(path.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path() + '/' + parseElement(xml, elementOk)));
+ QString fileName = parseElement(xml, elementOk);
+ if (!fileName.isEmpty()) {
+ phrase->setSound(QUrl::fromLocalFile(path.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path() + '/' + fileName));
+ }
ok &= elementOk;
} else if (xml.name() == "phonemes") {
auto parsedPhonemeIds = parsePhonemeIds(xml, elementOk);
for (auto phoneme : phonemes) {
if (parsedPhonemeIds.contains(phoneme->id())) {
phrase->addPhoneme(phoneme.get());
}
}
ok &= elementOk;
} else if (xml.name() == "type") {
const QString type = parseElement(xml, elementOk);
if (type == "word") {
phrase->setType(IPhrase::Type::Word);
} else if (type == "expression") {
phrase->setType(IPhrase::Type::Expression);
} else if (type == "sentence") {
phrase->setType(IPhrase::Type::Sentence);
} else if (type == "paragraph") {
phrase->setType(IPhrase::Type::Paragraph);
}
ok &= elementOk;
} else if (xml.name() == "editState") {
const QString type = parseElement(xml, elementOk);
if (type == "translated") {
phrase->setEditState(Phrase::EditState::Translated);
} else if (type == "completed") {
phrase->setEditState(Phrase::EditState::Completed);
} else if (type == "unknown") {
phrase->setEditState(Phrase::EditState::Completed);
}
ok &= elementOk;
} else {
qCWarning(ARTIKULATE_PARSER()) << "Skipping unknown token" << xml.name();
}
}
xml.readNext();
}
if (!ok) {
qCWarning(ARTIKULATE_PARSER()) << "Errors occurred while parsing phrase" << phrase->text() << phrase->id();
}
return phrase;
}
QStringList CourseParser::parsePhonemeIds(QXmlStreamReader &xml, bool &ok)
{
QStringList ids;
ok = true;
if (xml.tokenType() != QXmlStreamReader::StartElement && xml.name() == "phonemes") {
qCWarning(ARTIKULATE_PARSER()) << "Expected to parse 'phonemes' element, aborting here";
ok = false;
return ids;
}
xml.readNext();
while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == "phonemes")) {
xml.readNext();
if (xml.tokenType() == QXmlStreamReader::StartElement) {
if (xml.name() == "phonemeID") {
bool elementOk {false};
ids.append(parseElement(xml, elementOk));
ok &= elementOk;
} else {
qCWarning(ARTIKULATE_PARSER()) << "Skipping unknown token" << xml.name();
}
}
}
return ids;
}
QString CourseParser::parseElement(QXmlStreamReader &xml, bool &ok)
{
ok = true;
if (xml.tokenType() != QXmlStreamReader::StartElement) {
qCCritical(ARTIKULATE_PARSER()) << "Parsing element that does not start with a start element";
ok = false;
return QString();
}
QString elementName = xml.name().toString();
xml.readNext();
- qCDebug(ARTIKULATE_PARSER()) << "parsed: " << elementName << " / " << xml.text().toString();
+ //qCDebug(ARTIKULATE_PARSER()) << "parsed: " << elementName << " / " << xml.text().toString();
return xml.text().toString();
}
QDomDocument CourseParser::serializedDocument(std::shared_ptr course, bool trainingExport)
{
QDomDocument document;
// prepare xml header
QDomProcessingInstruction header = document.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\""));
document.appendChild(header);
// create main element
QDomElement root = document.createElement(QStringLiteral("course"));
document.appendChild(root);
QDomElement idElement = document.createElement(QStringLiteral("id"));
QDomElement titleElement = document.createElement(QStringLiteral("title"));
QDomElement descriptionElement = document.createElement(QStringLiteral("description"));
QDomElement languageElement = document.createElement(QStringLiteral("language"));
idElement.appendChild(document.createTextNode(course->id()));
titleElement.appendChild(document.createTextNode(course->title()));
descriptionElement.appendChild(document.createTextNode(course->description()));
languageElement.appendChild(document.createTextNode(course->id()));
QDomElement unitListElement = document.createElement(QStringLiteral("units"));
// create units
for (auto unit : course->units()) {
QDomElement unitElement = document.createElement(QStringLiteral("unit"));
QDomElement unitIdElement = document.createElement(QStringLiteral("id"));
QDomElement unitTitleElement = document.createElement(QStringLiteral("title"));
QDomElement unitPhraseListElement = document.createElement(QStringLiteral("phrases"));
unitIdElement.appendChild(document.createTextNode(unit->id()));
unitTitleElement.appendChild(document.createTextNode(unit->title()));
// construct phrases
for (auto &phrase : unit->phrases()) {
if (trainingExport && phrase->soundFileUrl().isEmpty()) {
continue;
}
unitPhraseListElement.appendChild(serializedPhrase(std::static_pointer_cast(phrase), document));
}
if (trainingExport && unitPhraseListElement.childNodes().isEmpty()) {
continue;
}
// construct the unit element
unitElement.appendChild(unitIdElement);
if (!unit->foreignId().isEmpty()) {
QDomElement unitForeignIdElement = document.createElement(QStringLiteral("foreignId"));
unitForeignIdElement.appendChild(document.createTextNode(unit->foreignId()));
unitElement.appendChild(unitForeignIdElement);
}
unitElement.appendChild(unitTitleElement);
unitElement.appendChild(unitPhraseListElement);
unitListElement.appendChild(unitElement);
}
root.appendChild(idElement);
if (!course->foreignId().isEmpty()) {
QDomElement courseForeignIdElement = document.createElement(QStringLiteral("foreignId"));
courseForeignIdElement.appendChild(document.createTextNode(course->foreignId()));
root.appendChild(courseForeignIdElement);
}
root.appendChild(titleElement);
root.appendChild(descriptionElement);
root.appendChild(languageElement);
root.appendChild(unitListElement);
return document;
}
QDomElement CourseParser::serializedPhrase(std::shared_ptr phrase, QDomDocument &document)
{
QDomElement phraseElement = document.createElement(QStringLiteral("phrase"));
QDomElement phraseIdElement = document.createElement(QStringLiteral("id"));
QDomElement phraseTextElement = document.createElement(QStringLiteral("text"));
QDomElement phrasei18nTextElement = document.createElement(QStringLiteral("i18nText"));
QDomElement phraseSoundFileElement = document.createElement(QStringLiteral("soundFile"));
QDomElement phraseTypeElement = document.createElement(QStringLiteral("type"));
QDomElement phraseEditStateElement = document.createElement(QStringLiteral("editState"));
QDomElement phrasePhonemeListElement = document.createElement(QStringLiteral("phonemes"));
phraseIdElement.appendChild(document.createTextNode(phrase->id()));
phraseTextElement.appendChild(document.createTextNode(phrase->text()));
phrasei18nTextElement.appendChild(document.createTextNode(phrase->i18nText()));
phraseSoundFileElement.appendChild(document.createTextNode(phrase->sound().fileName()));
phraseTypeElement.appendChild(document.createTextNode(phrase->typeString()));
phraseEditStateElement.appendChild(document.createTextNode(phrase->editStateString()));
// add phonemes
for (auto &phoneme : phrase->phonemes()) {
QDomElement phonemeElement = document.createElement(QStringLiteral("phonemeID"));
phonemeElement.appendChild(document.createTextNode(phoneme->id()));
phrasePhonemeListElement.appendChild(phonemeElement);
}
phraseElement.appendChild(phraseIdElement);
if (!phrase->foreignId().isEmpty()) {
QDomElement phraseForeignIdElement = document.createElement(QStringLiteral("foreignId"));
phraseForeignIdElement.appendChild(document.createTextNode(phrase->foreignId()));
phraseElement.appendChild(phraseForeignIdElement);
}
phraseElement.appendChild(phraseTextElement);
phraseElement.appendChild(phrasei18nTextElement);
phraseElement.appendChild(phraseSoundFileElement);
phraseElement.appendChild(phraseTypeElement);
phraseElement.appendChild(phraseEditStateElement);
phraseElement.appendChild(phrasePhonemeListElement);
return phraseElement;
}
bool CourseParser::exportCourseToGhnsPackage(std::shared_ptr course, const QString &exportPath)
{
// filename
const QString fileName = course->id() + ".tar.bz2";
KTar tar = KTar(exportPath + '/' + fileName, QStringLiteral("application/x-bzip"));
if (!tar.open(QIODevice::WriteOnly)) {
qCWarning(ARTIKULATE_CORE()) << "Unable to open tar file" << exportPath + '/' + fileName << "in write mode, aborting.";
return false;
}
for (auto &unit : course->units()) {
for (auto &phrase : unit->phrases()) {
if (QFile::exists(phrase->soundFileUrl())) {
tar.addLocalFile(phrase->soundFileUrl(), phrase->id() + ".ogg");
}
}
}
tar.writeFile(course->id() + ".xml", CourseParser::serializedDocument(course, true).toByteArray());
tar.close();
return true;
}
diff --git a/src/core/resources/courseparser.h b/src/core/resources/courseparser.h
index d6c0559..1a65b2e 100644
--- a/src/core/resources/courseparser.h
+++ b/src/core/resources/courseparser.h
@@ -1,77 +1,84 @@
/*
* 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 COURSEPARSER_H
#define COURSEPARSER_H
#include "artikulatecore_export.h"
#include
#include
class IEditableCourse;
class IEditablePhrase;
class IUnit;
class Unit;
class Phrase;
class Phoneme;
class IResourceRepository;
class QXmlSchema;
class QJSonDocument;
class QDomDocument;
class QDomElement;
class QXmlStreamReader;
class QString;
class QUrl;
class ARTIKULATECORE_EXPORT CourseParser
{
public:
/**
* Load XSD file given by its file name (without ".xsd" suffix). The method searches exclusively
* the standard install dir for XSD files in subdirectory "schemes/".
*
* \param schemeName name of the Xml schema without suffix
* \return loaded XML Schema
*/
static QXmlSchema loadXmlSchema(const QString &schemeName);
/**
* Load XML file given by \p file that confirms with XML schema \p scheme.
*
* \param path is the path to the XML file to be loaded
* \param scheme is the XML schema describing the DOM
* \return the loaded DOM document
*/
static QDomDocument loadDomDocument(const QUrl &path, const QXmlSchema &schema);
- static std::vector> parseUnits(const QUrl &path, QVector> phonemes = QVector>());
+ /**
+ * @brief Parse unit from XML file
+ * @param path the path to the file
+ * @param phonemes list of phonemes that are generated for the language of the unit
+ * @param skipIncomplete if set to true, empty units and phrases without native sound files are skipped
+ * @return parsed unit
+ */
+ static std::vector> parseUnits(const QUrl &path, QVector> phonemes = QVector>(), bool skipIncomplete = false);
static QDomDocument serializedDocument(std::shared_ptr course, bool trainingExport);
static QDomElement serializedPhrase(std::shared_ptr phrase, QDomDocument &document);
static bool exportCourseToGhnsPackage(std::shared_ptr course, const QString &exportPath);
private:
- static std::shared_ptr parseUnit(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool &ok);
+ static std::shared_ptr parseUnit(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool skipIncomplete, bool &ok);
static std::shared_ptr parsePhrase(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool &ok);
static QStringList parsePhonemeIds(QXmlStreamReader &xml, bool &ok);
static QString parseElement(QXmlStreamReader &xml, bool &ok);
};
#endif
diff --git a/src/core/resources/courseresource.cpp b/src/core/resources/courseresource.cpp
index 05c6cbe..3681669 100644
--- a/src/core/resources/courseresource.cpp
+++ b/src/core/resources/courseresource.cpp
@@ -1,287 +1,291 @@
/*
* Copyright 2013-2015 Andreas Cord-Landwehr
* Copyright 2013 Oindrila Gupta
*
* 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 "courseresource.h"
#include "core/iresourcerepository.h"
#include "core/language.h"
#include "core/phoneme.h"
#include "core/phonemegroup.h"
#include "core/unit.h"
#include "courseparser.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include "artikulate_debug.h"
class CourseResourcePrivate
{
public:
CourseResourcePrivate() = default;
~CourseResourcePrivate();
- void loadCourse(CourseResource *parent);
+ void loadCourse(CourseResource *parent, bool skipIncomplete);
std::weak_ptr m_self;
IResourceRepository *m_repository {nullptr};
QUrl m_file;
QString m_identifier;
QString m_foreignId;
QString m_title;
QString m_languageId;
std::shared_ptr m_language;
QString m_i18nTitle;
QString m_description;
QVector> m_units;
bool m_courseLoaded {false}; ///> phonemes = m_language->phonemes();
- auto units = CourseParser::parseUnits(m_file, phonemes);
+ auto units = CourseParser::parseUnits(m_file, phonemes, skipIncomplete);
for (auto &unit : units) {
- parent->addUnit(std::move(unit));
+ if (!skipIncomplete || unit->phrases().count() > 0) {
+ parent->addUnit(std::move(unit));
+ }
}
}
-std::shared_ptr CourseResource::create(const QUrl &path, IResourceRepository *repository)
+std::shared_ptr CourseResource::create(const QUrl &path, IResourceRepository *repository, bool skipIncomplete)
{
- std::shared_ptr course(new CourseResource(path, repository));
+ std::shared_ptr course(new CourseResource(path, repository, skipIncomplete));
course->setSelf(course);
return course;
}
void CourseResource::setSelf(std::shared_ptr self)
{
Q_ASSERT(d->m_self.expired());
d->m_self = self;
}
std::shared_ptr CourseResource::self() const
{
return d->m_self.lock();
}
-CourseResource::CourseResource(const QUrl &path, IResourceRepository *repository)
+CourseResource::CourseResource(const QUrl &path, IResourceRepository *repository, bool skipIncomplete)
: ICourse()
, d(new CourseResourcePrivate())
{
QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
d->m_file = path;
d->m_repository = repository;
+ d->m_skipIncomplete = skipIncomplete;
// load basic information from language file, but does not parse everything
QXmlStreamReader xml;
QFile file(path.toLocalFile());
if (file.open(QIODevice::ReadOnly)) {
xml.setDevice(&file);
xml.readNextStartElement();
while (xml.readNext() && !xml.atEnd()) {
if (xml.name() == "id") {
d->m_identifier = xml.readElementText();
continue;
}
if (xml.name() == "foreignId") {
d->m_foreignId = xml.readElementText();
continue;
}
// TODO i18nTitle must be implemented, currently missing and hence not parsed
if (xml.name() == "title") {
d->m_title = xml.readElementText();
d->m_i18nTitle = d->m_title;
continue;
}
if (xml.name() == "description") {
d->m_description = xml.readElementText();
continue;
}
if (xml.name() == "language") {
d->m_languageId = xml.readElementText();
continue;
}
// quit reading when basic elements are read
if (!d->m_identifier.isEmpty() && !d->m_title.isEmpty() && !d->m_i18nTitle.isEmpty() && !d->m_description.isEmpty() && !d->m_languageId.isEmpty() && !d->m_foreignId.isEmpty()) {
break;
}
}
if (xml.hasError()) {
qCritical() << "Error occurred when reading Course XML file:" << path.toLocalFile();
}
} else {
qCCritical(ARTIKULATE_CORE()) << "Could not open course file" << path.toLocalFile();
}
xml.clear();
file.close();
// find correct language
if (repository != nullptr) {
for (const auto &language : repository->languages()) {
if (language == nullptr) {
continue;
}
if (language->id() == d->m_languageId) {
d->m_language = language;
}
}
}
if (d->m_language == nullptr) {
qCCritical(ARTIKULATE_CORE()) << "A course with an unknown language was loaded";
}
}
CourseResource::~CourseResource() = default;
QString CourseResource::id() const
{
return d->m_identifier;
}
void CourseResource::setId(const QString &id)
{
if (d->m_identifier == id) {
return;
}
d->m_identifier = id;
emit idChanged();
}
QString CourseResource::foreignId() const
{
return d->m_foreignId;
}
void CourseResource::setForeignId(const QString &foreignId)
{
if (d->m_foreignId == foreignId) {
return;
}
d->m_foreignId = foreignId;
emit foreignIdChanged();
}
QString CourseResource::title() const
{
return d->m_title;
}
void CourseResource::setTitle(const QString &title)
{
if (d->m_title == title) {
return;
}
d->m_title = title;
emit titleChanged();
}
QString CourseResource::i18nTitle() const
{
return d->m_i18nTitle;
}
void CourseResource::setI18nTitle(const QString &i18nTitle)
{
if (d->m_i18nTitle == i18nTitle) {
return;
}
d->m_i18nTitle = i18nTitle;
emit i18nTitleChanged();
}
QString CourseResource::description() const
{
return d->m_description;
}
void CourseResource::setDescription(const QString &description)
{
if (d->m_description == description) {
return;
}
d->m_description = description;
emit descriptionChanged();
}
std::shared_ptr CourseResource::language() const
{
return d->m_language;
}
QString CourseResource::languageTitle() const
{
if (d->m_language) {
return d->m_language->title();
}
return QString();
}
void CourseResource::setLanguage(std::shared_ptr language)
{
if (d->m_language == language) {
return;
}
d->m_language = language;
emit languageChanged();
}
std::shared_ptr CourseResource::addUnit(std::shared_ptr unit)
{
std::shared_ptr storedUnit(std::move(unit));
storedUnit->setCourse(self());
emit unitAboutToBeAdded(storedUnit, d->m_units.count() - 1);
d->m_units.append(storedUnit);
emit unitAdded();
return storedUnit;
}
QVector> CourseResource::units()
{
if (d->m_courseLoaded == false) {
- d->loadCourse(this);
+ d->loadCourse(this, d->m_skipIncomplete);
}
return d->m_units;
}
QUrl CourseResource::file() const
{
return d->m_file;
}
diff --git a/src/core/resources/courseresource.h b/src/core/resources/courseresource.h
index 8e6b341..a70533c 100644
--- a/src/core/resources/courseresource.h
+++ b/src/core/resources/courseresource.h
@@ -1,120 +1,120 @@
/*
* 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 .
*/
#ifndef COURSERESOURCE_H
#define COURSERESOURCE_H
#include "artikulatecore_export.h"
#include "core/icourse.h"
#include
#include
#include
class QString;
class CourseResourcePrivate;
class Unit;
class Phrase;
class ILanguage;
class IResourceRepository;
class EditableCourseResource;
class ARTIKULATECORE_EXPORT CourseResource : public ICourse
{
Q_OBJECT
Q_INTERFACES(ICourse)
public:
- static std::shared_ptr create(const QUrl &path, IResourceRepository *repository);
+ static std::shared_ptr create(const QUrl &path, IResourceRepository *repository, bool skipIncomplete = false);
~CourseResource() override;
/**
* \return unique identifier
*/
QString id() const override;
void setId(const QString &id);
/**
* \return global ID for this course
*/
QString foreignId() const override;
void setForeignId(const QString &foreignId);
/**
* \return human readable localized title
*/
QString title() const override;
void setTitle(const QString &title);
/**
* \return human readable title in English
*/
QString i18nTitle() const override;
void setI18nTitle(const QString &i18nTitle);
/**
* \return description text for course
*/
QString description() const override;
void setDescription(const QString &description);
/**
* \return language identifier of this course
*/
std::shared_ptr language() const override;
QString languageTitle() const override;
void setLanguage(std::shared_ptr language);
std::shared_ptr addUnit(std::shared_ptr unit);
void sync();
QUrl file() const override;
QVector> units() override;
Q_SIGNALS:
void idChanged();
void foreignIdChanged();
void titleChanged();
void i18nTitleChanged();
void descriptionChanged();
void languageChanged();
private:
/**
* Create course resource from file.
*/
- explicit CourseResource(const QUrl &path, IResourceRepository *repository);
+ explicit CourseResource(const QUrl &path, IResourceRepository *repository, bool skipIncomplete);
void setSelf(std::shared_ptr self) override;
std::shared_ptr self() const;
const std::unique_ptr d;
friend EditableCourseResource;
};
#endif
diff --git a/src/core/resources/editablecourseresource.cpp b/src/core/resources/editablecourseresource.cpp
index 17dae98..6d1f70b 100644
--- a/src/core/resources/editablecourseresource.cpp
+++ b/src/core/resources/editablecourseresource.cpp
@@ -1,344 +1,344 @@
/*
* 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 "editablecourseresource.h"
#include "artikulate_debug.h"
#include "core/phoneme.h"
#include "core/phrase.h"
#include "core/unit.h"
#include "courseparser.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
EditableCourseResource::EditableCourseResource(const QUrl &path, IResourceRepository *repository)
: IEditableCourse()
- , m_course(new CourseResource(path, repository))
+ , m_course(new CourseResource(path, repository, false))
{
QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
connect(m_course.get(), &ICourse::unitAboutToBeAdded, this, &ICourse::unitAboutToBeAdded);
connect(m_course.get(), &ICourse::unitAdded, this, &ICourse::unitAdded);
connect(m_course.get(), &CourseResource::idChanged, this, &EditableCourseResource::idChanged);
connect(m_course.get(), &CourseResource::foreignIdChanged, this, &EditableCourseResource::foreignIdChanged);
connect(m_course.get(), &CourseResource::titleChanged, this, &EditableCourseResource::titleChanged);
connect(m_course.get(), &CourseResource::descriptionChanged, this, &EditableCourseResource::descriptionChanged);
connect(m_course.get(), &CourseResource::languageChanged, this, &EditableCourseResource::languageChanged);
for (auto &unit : m_course->units()) {
connect(unit.get(), &Unit::phrasesChanged, this, &IEditableCourse::unitChanged);
}
}
std::shared_ptr EditableCourseResource::create(const QUrl &path, IResourceRepository *repository)
{
std::shared_ptr course(new EditableCourseResource(path, repository));
course->setSelf(course);
return course;
}
void EditableCourseResource::setSelf(std::shared_ptr self)
{
m_course->setSelf(self);
}
QString EditableCourseResource::id() const
{
return m_course->id();
}
void EditableCourseResource::setId(QString id)
{
if (m_course->id() != id) {
m_course->setId(id);
m_modified = true;
}
}
QString EditableCourseResource::foreignId() const
{
return m_course->foreignId();
}
void EditableCourseResource::setForeignId(QString foreignId)
{
m_course->setForeignId(std::move(foreignId));
}
QString EditableCourseResource::title() const
{
return m_course->title();
}
void EditableCourseResource::setTitle(QString title)
{
if (m_course->title() != title) {
m_course->setTitle(title);
m_modified = true;
}
}
QString EditableCourseResource::i18nTitle() const
{
return m_course->i18nTitle();
}
void EditableCourseResource::setI18nTitle(QString i18nTitle)
{
if (m_course->i18nTitle() != i18nTitle) {
m_course->setI18nTitle(i18nTitle);
m_modified = true;
}
}
QString EditableCourseResource::description() const
{
return m_course->description();
}
void EditableCourseResource::setDescription(QString description)
{
if (m_course->description() != description) {
m_course->setDescription(description);
m_modified = true;
}
}
std::shared_ptr EditableCourseResource::language() const
{
return m_course->language();
}
QString EditableCourseResource::languageTitle() const
{
return m_course->languageTitle();
}
void EditableCourseResource::setLanguage(std::shared_ptr language)
{
if (m_course->language() != language) {
m_course->setLanguage(language);
m_modified = true;
}
}
QUrl EditableCourseResource::file() const
{
return m_course->file();
}
std::shared_ptr EditableCourseResource::self() const
{
return std::static_pointer_cast(m_course->self());
}
bool EditableCourseResource::sync()
{
Q_ASSERT(file().isValid());
Q_ASSERT(file().isLocalFile());
Q_ASSERT(!file().isEmpty());
// not writing back if not modified
if (!m_modified) {
qCDebug(ARTIKULATE_LOG()) << "Aborting sync, course was not modified.";
return false;
}
bool ok = exportToFile(file());
if (ok) {
m_modified = false;
}
return ok;
}
bool EditableCourseResource::exportToFile(const QUrl &filePath) const
{
// write back to file
// create directories if necessary
QFileInfo info(filePath.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path());
if (!info.exists()) {
qCDebug(ARTIKULATE_LOG()) << "create xml output file directory, not existing";
QDir dir;
dir.mkpath(filePath.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path());
}
// TODO port to KSaveFile
QFile file(filePath.toLocalFile());
if (!file.open(QIODevice::WriteOnly)) {
qCWarning(ARTIKULATE_LOG()) << "Unable to open file " << file.fileName() << " in write mode, aborting.";
return false;
}
file.write(CourseParser::serializedDocument(self(), false).toByteArray());
return true;
}
std::shared_ptr EditableCourseResource::addUnit(std::shared_ptr unit)
{
m_modified = true;
m_course->addUnit(unit);
unit->setCourse(self());
connect(unit.get(), &Unit::phrasesChanged, this, &IEditableCourse::unitChanged);
return unit;
}
QVector> EditableCourseResource::units()
{
if (!m_unitsLoaded) {
for (auto &unit : m_course->units()) {
unit->setCourse(self());
}
m_unitsLoaded = true;
}
return m_course->units();
}
void EditableCourseResource::updateFrom(std::shared_ptr skeleton)
{
for (auto skeletonUnit : skeleton->units()) {
// find matching unit or create one
std::shared_ptr matchingUnit;
auto it = std::find_if(m_course->units().cbegin(), m_course->units().cend(), [skeletonUnit](std::shared_ptr compareUnit) { return compareUnit->foreignId() == skeletonUnit->id(); });
if (it == m_course->units().cend()) {
// import complete unit
auto importUnit = Unit::create();
importUnit->setId(skeletonUnit->id());
importUnit->setForeignId(skeletonUnit->id());
importUnit->setTitle(skeletonUnit->title());
matchingUnit = m_course->addUnit(std::move(importUnit));
} else {
matchingUnit = *it;
}
// import phrases
for (auto skeletonPhrase : skeletonUnit->phrases()) {
auto it = std::find_if(matchingUnit->phrases().cbegin(), matchingUnit->phrases().cend(), [skeletonPhrase](std::shared_ptr comparePhrase) { return comparePhrase->foreignId() == skeletonPhrase->id(); });
if (it == matchingUnit->phrases().cend()) {
// import complete Phrase
std::shared_ptr importPhrase = Phrase::create();
importPhrase->setId(skeletonPhrase->id());
importPhrase->setForeignId(skeletonPhrase->id());
importPhrase->setText(skeletonPhrase->text());
importPhrase->seti18nText(skeletonPhrase->i18nText());
importPhrase->setType(skeletonPhrase->type());
importPhrase->setUnit(matchingUnit);
matchingUnit->addPhrase(importPhrase, matchingUnit->phrases().size());
}
}
}
qCInfo(ARTIKULATE_LOG()) << "Update performed!";
}
bool EditableCourseResource::isModified() const
{
return m_modified;
}
Unit *EditableCourseResource::createUnit()
{
// find first unused id
QStringList unitIds;
for (auto unit : m_course->units()) {
unitIds.append(unit->id());
}
QString id = QUuid::createUuid().toString();
while (unitIds.contains(id)) {
id = QUuid::createUuid().toString();
qCWarning(ARTIKULATE_LOG) << "Unit id generator has found a collision, recreating id.";
}
// create unit
std::shared_ptr unit = Unit::create();
unit->setCourse(self());
unit->setId(id);
unit->setTitle(i18n("New Unit"));
auto sharedUnit = addUnit(std::move(unit));
return sharedUnit.get();
}
bool EditableCourseResource::createPhraseAfter(IPhrase *previousPhrase)
{
std::shared_ptr parentUnit = units().last();
if (previousPhrase) {
for (const auto &unit : units()) {
if (previousPhrase->unit()->id() == unit->id()) {
parentUnit = unit;
break;
}
}
}
// find index
int index = parentUnit->phrases().size();
for (int i = 0; i < parentUnit->phrases().size(); ++i) {
if (parentUnit->phrases().at(i)->id() == previousPhrase->id()) {
index = i;
break;
}
}
// find globally unique phrase id inside course
QStringList phraseIds;
for (auto unit : m_course->units()) {
for (auto &phrase : unit->phrases()) {
phraseIds.append(phrase->id());
}
}
QString id = QUuid::createUuid().toString();
while (phraseIds.contains(id)) {
id = QUuid::createUuid().toString();
qCWarning(ARTIKULATE_LOG) << "Phrase id generator has found a collision, recreating id.";
}
// create unit
std::shared_ptr phrase = Phrase::create();
phrase->setId(id);
phrase->setText(QLatin1String(""));
phrase->setType(IPhrase::Type::Word);
parentUnit->addPhrase(phrase, index + 1);
qCDebug(ARTIKULATE_CORE()) << "Created phrase at index" << index + 1;
return true;
}
bool EditableCourseResource::deletePhrase(IPhrase *phrase)
{
Q_ASSERT(phrase);
if (!phrase) {
return false;
}
auto unitId = phrase->unit()->id();
for (auto &unit : units()) {
if (unit->id() == unitId) {
unit->removePhrase(phrase->self());
return true;
}
}
return false;
}