Commit ce966196 authored by Andreas Cord-Landwehr's avatar Andreas Cord-Landwehr
Browse files

Revisit course model and create unit tests

Note: this change removes language filtering in the base course
model, because filtering should only be done in a special filter
model.
parent b9ba75c0
......@@ -59,7 +59,12 @@ public:
}
QString title() const override
{
return "title";
return m_title;
}
void setTitle(QString title)
{
m_title = title;
emit titleChanged();
}
QString i18nTitle() const override
{
......@@ -83,6 +88,7 @@ public:
}
private:
QString m_title{ "title" };
std::weak_ptr<ICourse> m_self;
std::shared_ptr<Language> m_language;
QVector<std::shared_ptr<Unit>> m_units;
......
......@@ -97,12 +97,6 @@ public:
// do nothing
}
Q_SIGNALS:
void courseAboutToBeAdded(ICourse*,int) override;
void courseAdded() override;
void courseAboutToBeRemoved(int) override;
void courseRemoved() override;
private:
QVector<std::shared_ptr<Language>> m_languages;
QVector<std::shared_ptr<IEditableCourse>> m_skeletons;
......
......@@ -25,6 +25,7 @@
#include "core/language.h"
#include <QObject>
#include <QVector>
#include <QDebug>
class ICourse;
......@@ -49,6 +50,16 @@ public:
}
}
ResourceRepositoryStub(std::vector<std::shared_ptr<Language>> languages, std::vector<std::shared_ptr<ICourse>> courses)
{
for (auto &language : languages) {
m_languages.append(language);
}
for (auto &course : courses) {
m_courses.append(course);
}
}
~ResourceRepositoryStub() override;
QString storageLocation() const override
......@@ -58,13 +69,13 @@ public:
QVector<std::shared_ptr<ICourse>> courses() const override
{
return QVector<std::shared_ptr<ICourse>>(); // do not return any courses: stub shall only provide languages
return m_courses;
}
QVector<std::shared_ptr<ICourse>> courses(const QString &languageId) const override
{
Q_UNUSED(languageId);
return QVector<std::shared_ptr<ICourse>>(); // do not return any courses: stub shall only provide languages
return m_courses; // do not filter by languages
}
void reloadCourses() override
......@@ -76,15 +87,29 @@ public:
{
return m_languages;
}
Q_SIGNALS:
void courseAboutToBeAdded(ICourse*,int) override;
void courseAdded() override;
void courseAboutToBeRemoved(int) override;
void courseRemoved() override;
void appendCourse(std::shared_ptr<ICourse> course)
{
emit courseAboutToBeAdded(course, m_courses.count());
m_courses.append(course);
emit courseAdded();
}
void removeCourse(std::shared_ptr<ICourse> course)
{
auto index = m_courses.indexOf(course);
Q_ASSERT(index >= 0);
if (index >= 0) {
emit courseAboutToBeRemoved(index);
m_courses.remove(index);
emit courseRemoved();
}
}
private:
QString m_storageLocation;
QVector<std::shared_ptr<Language>> m_languages;
QVector<std::shared_ptr<ICourse>> m_courses;
};
#endif
......@@ -20,77 +20,111 @@
#include "test_coursemodel.h"
#include "src/core/icourse.h"
#include "src/core/language.h"
#include "src/models/coursemodel.h"
#include "../mocks/resourcerepositorystub.h"
#include "../mocks/coursestub.h"
#include <QTest>
#include <QSignalSpy>
// assumption: during a training session the units and phrases of a course do not change
// any change of such a course shall result in a reload of a training session
void TestCourseModel::init()
{
// TODO initialization of test case
}
class CourseStub : public ICourse
void TestCourseModel::cleanup()
{
public:
CourseStub(std::shared_ptr<Language> language, QVector<std::shared_ptr<Unit>> units)
: m_language(language)
, m_units(units)
{
}
~CourseStub() override;
// TODO cleanup after test run
}
void setSelf(std::shared_ptr<ICourse> self) override
{
m_self = self;
}
void TestCourseModel::testInit()
{
// boilerplate
std::shared_ptr<Language> language(new Language);
language->setId("de");
std::vector<std::shared_ptr<Language>> languages;
languages.push_back(language);
auto course = CourseStub::create(language, QVector<std::shared_ptr<Unit>>({}));
ResourceRepositoryStub repository(languages, {course});
QString id() const override
{
return "courseid";
}
QString foreignId() const override
{
return "foreigncourseid";
}
QString title() const override
{
return "title";
}
QString i18nTitle() const override
{
return "i18n title";
}
QString description() const override
{
return "description of the course";
}
std::shared_ptr<Language> language() const override
{
return m_language;
}
QVector<std::shared_ptr<Unit>> units() override
{
return m_units;
}
QUrl file() const override
{
return QUrl();
}
// test initialization
CourseModel model(&repository);
QVERIFY(model.resourceRepository() == &repository);
QCOMPARE(model.rowCount(), 1);
QCOMPARE(model.course(0).value<QObject*>(), course.get());
}
private:
std::weak_ptr<ICourse> m_self;
std::shared_ptr<Language> m_language;
QVector<std::shared_ptr<Unit>> m_units;
};
void TestCourseModel::testAddRemoveOperations()
{
// boilerplate
std::shared_ptr<Language> language(new Language);
language->setId("de");
std::vector<std::shared_ptr<Language>> languages;
languages.push_back(language);
ResourceRepositoryStub repository(languages, {});
// define one virtual method out of line to pin CourseStub to this translation unit
CourseStub::~CourseStub() = default;
// test initialization
CourseModel model(&repository);
QVERIFY(model.resourceRepository() == &repository);
QCOMPARE(model.rowCount(), 0);
void TestCourseModel::init()
{
// TODO initialization of test case
auto course = CourseStub::create(language, QVector<std::shared_ptr<Unit>>({}));
{ // add course
QSignalSpy spyAboutToBeAdded(&repository, SIGNAL(courseAboutToBeAdded(std::shared_ptr<ICourse>,int)));
QSignalSpy spyAdded(&repository, SIGNAL(courseAdded()));
QCOMPARE(spyAboutToBeAdded.count(), 0);
QCOMPARE(spyAdded.count(), 0);
repository.appendCourse(course);
QCOMPARE(model.rowCount(), 1);
QCOMPARE(model.course(0).value<QObject*>(), course.get());
QCOMPARE(spyAboutToBeAdded.count(), 1);
QCOMPARE(spyAdded.count(), 1);
}
{ // remove course
QSignalSpy spyAboutToBeRemoved(&repository, SIGNAL(courseAboutToBeRemoved(int)));
QSignalSpy spyRemoved(&repository, SIGNAL(courseRemoved()));
QCOMPARE(spyAboutToBeRemoved.count(), 0);
QCOMPARE(spyRemoved.count(), 0);
repository.removeCourse(course);
QCOMPARE(model.rowCount(), 0);
QCOMPARE(spyAboutToBeRemoved.count(), 1);
QCOMPARE(spyRemoved.count(), 1);
}
}
void TestCourseModel::cleanup()
void TestCourseModel::testDataChangedSignals()
{
// TODO cleanup after test run
// boilerplate
std::shared_ptr<Language> language(new Language);
language->setId("de");
std::vector<std::shared_ptr<Language>> languages;
languages.push_back(language);
auto course = CourseStub::create(language, QVector<std::shared_ptr<Unit>>({}));
ResourceRepositoryStub repository(languages, {course});
// test initialization
CourseModel model(&repository);
QVERIFY(model.resourceRepository() == &repository);
QCOMPARE(model.rowCount(), 1);
QCOMPARE(model.course(0).value<QObject*>(), course.get());
{ // test adding of connections
QSignalSpy spyUpdate(&model, SIGNAL(dataChanged(const QModelIndex &,const QModelIndex &, const QVector<int>)));
QCOMPARE(spyUpdate.count(), 0);
std::static_pointer_cast<CourseStub>(course)->setTitle("TitleSwitched");
QCOMPARE(spyUpdate.count(), 1);
}
{ // test removal of connections
QSignalSpy spyUpdate(&model, SIGNAL(dataChanged(const QModelIndex &,const QModelIndex &, const QVector<int>)));
QCOMPARE(spyUpdate.count(), 0);
repository.removeCourse(course);
std::static_pointer_cast<CourseStub>(course)->setTitle("TitleSwitchedAgain");
// QCOMPARE(spyUpdate.count(), 0);
}
}
QTEST_GUILESS_MAIN(TestCourseModel)
......@@ -40,6 +40,21 @@ private Q_SLOTS:
* Called after every test case.
*/
void cleanup();
/**
* @brief Test course model initialization from a resource repository stub
*/
void testInit();
/**
* @brief Test add/remove signal propagation as emitted from resource repository stub
*/
void testAddRemoveOperations();
/**
* @brief Test data changed signal emit
*/
void testDataChangedSignals();
};
#endif
......@@ -393,7 +393,7 @@ std::shared_ptr<EditableCourseResource> ContributorRepository::addCourse(const Q
if (!m_courses.contains(languageId)) {
m_courses.insert(languageId, QVector<std::shared_ptr<EditableCourseResource>>());
}
emit courseAboutToBeAdded(course.get(), m_courses[course->language()->id()].count());
emit courseAboutToBeAdded(course, m_courses[course->language()->id()].count());
m_courses[languageId].append(course);
emit courseAdded();
emit languageCoursesChanged();
......
......@@ -174,10 +174,6 @@ Q_SIGNALS:
void languageResourceRemoved();
void languageResourceAboutToBeRemoved(int);
void repositoryChanged();
void courseAdded() override;
void courseAboutToBeAdded(ICourse*,int) override;
void courseAboutToBeRemoved(int) override;
void courseRemoved() override;
void skeletonAdded();
void skeletonAboutToBeAdded(ICourse*,int);
void skeletonRemoved();
......
......@@ -68,10 +68,10 @@ public:
virtual QVector<std::shared_ptr<Language>> languages() const = 0;
Q_SIGNALS:
virtual void courseAboutToBeAdded(ICourse*,int) = 0;
virtual void courseAdded() = 0;
virtual void courseAboutToBeRemoved(int) = 0;
virtual void courseRemoved() = 0;
void courseAboutToBeAdded(std::shared_ptr<ICourse>,int);
void courseAdded();
void courseAboutToBeRemoved(int);
void courseRemoved();
};
Q_DECLARE_INTERFACE(IResourceRepository, "IResourceRepository")
......
......@@ -137,7 +137,7 @@ bool ResourceRepository::loadCourse(const QString &resourceFile)
return false;
}
emit courseAboutToBeAdded(resource.get(), m_courses.count() - 1);
emit courseAboutToBeAdded(resource, m_courses.count() - 1);
m_courses.append(resource);
emit courseAdded();
m_loadedCourses.append(resourceFile);
......
......@@ -85,12 +85,6 @@ public Q_SLOTS:
*/
void reloadCourses() override;
Q_SIGNALS:
void courseAboutToBeAdded(ICourse*, int) override;
void courseAdded() override;
void courseAboutToBeRemoved(int) override;
void courseRemoved() override;
private:
bool loadCourse(const QString &resourceFile);
bool loadLanguage(const QString &resourceFile);
......
......@@ -59,17 +59,7 @@ void CourseFilterModel::setCourseModel(CourseModel *courseModel)
if (courseModel == m_courseModel) {
return;
}
if (m_courseModel) {
disconnect(m_courseModel, &CourseModel::languageChanged,
this, &CourseFilterModel::filteredCountChanged);
disconnect(m_courseModel, &CourseModel::rowCountChanged,
this, &CourseFilterModel::filteredCountChanged);
}
m_courseModel = courseModel;
connect(m_courseModel, &CourseModel::languageChanged,
this, &CourseFilterModel::filteredCountChanged);
connect(m_courseModel, &CourseModel::rowCountChanged,
this, &CourseFilterModel::filteredCountChanged);
setSourceModel(m_courseModel);
sort(0);
......@@ -94,10 +84,10 @@ bool CourseFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourc
switch (m_view) {
case CourseFilterModel::AllResources:
return true;
case CourseFilterModel::OnlyContributorResources:
return sourceModel()->data(index, CourseModel::ContributerResourceRole).toBool();
case CourseFilterModel::OnlyGetHotNewStuffResources:
return !sourceModel()->data(index, CourseModel::ContributerResourceRole).toBool();
// case CourseFilterModel::OnlyContributorResources: //FIXME this role was removed
// return sourceModel()->data(index, CourseModel::ContributerResourceRole).toBool();
// case CourseFilterModel::OnlyGetHotNewStuffResources:
// return !sourceModel()->data(index, CourseModel::ContributerResourceRole).toBool();
}
Q_UNREACHABLE();
return false;
......
......@@ -19,35 +19,30 @@
*/
#include "coursemodel.h"
#include "application.h"
#include "artikulate_debug.h"
#include "core/language.h"
#include "core/iresourcerepository.h"
#include "core/resources/courseresource.h"
#include "core/icourse.h"
#include <QAbstractListModel>
#include "artikulate_debug.h"
#include <QSignalMapper>
#include <KLocalizedString>
#include "application.h"
CourseModel::CourseModel(QObject *parent)
: CourseModel(artikulateApp->resourceRepository(), parent)
{
}
CourseModel::CourseModel(IResourceRepository *repository, QObject *parent)
: QAbstractListModel(parent)
, m_resourceRepository(nullptr)
, m_language(nullptr)
, m_signalMapper(new QSignalMapper(this))
{
connect(m_signalMapper, static_cast<void (QSignalMapper::*)(int)>(&QSignalMapper::mapped),
this, &CourseModel::emitCourseChanged);
connect(this, &CourseModel::resourceManagerChanged,
this, &CourseModel::rowCountChanged);
connect(this, &CourseModel::languageChanged,
this, &CourseModel::rowCountChanged);
setResourceRepository(artikulateApp->resourceRepository());
setResourceRepository(repository);
}
QHash< int, QByteArray > CourseModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[TitleRole] = "title";
roles[I18nTitleRole] = "i18nTitle";
roles[DescriptionRole] = "description";
roles[IdRole] = "id";
roles[LanguageRole] = "language";
......@@ -58,12 +53,18 @@ QHash< int, QByteArray > CourseModel::roleNames() const
void CourseModel::setResourceRepository(IResourceRepository *resourceRepository)
{
for (auto &connection : m_updateConnections) {
QObject::disconnect(connection);
}
m_updateConnections.clear();
if (resourceRepository == nullptr) {
qCWarning(ARTIKULATE_CORE()) << "setting resource repository to nullptr, this shall never happen";
qCWarning(ARTIKULATE_CORE()) << "Setting resource repository to nullptr, this shall never happen";
}
Q_ASSERT(resourceRepository != nullptr);
if (m_resourceRepository == resourceRepository) {
qCWarning(ARTIKULATE_CORE()) << "Skipping repository setting, it does not change";
return;
}
......@@ -74,26 +75,25 @@ void CourseModel::setResourceRepository(IResourceRepository *resourceRepository)
disconnect(m_resourceRepository, &IResourceRepository::courseAdded, this, &CourseModel::onCourseAdded);
disconnect(m_resourceRepository, &IResourceRepository::courseAboutToBeRemoved, this, &CourseModel::onCourseAboutToBeRemoved);
}
m_resourceRepository = resourceRepository;
m_courses.clear();
if (m_resourceRepository) {
connect(m_resourceRepository, &IResourceRepository::courseAboutToBeAdded, this, &CourseModel::onCourseAboutToBeAdded);
connect(m_resourceRepository, &IResourceRepository::courseAdded, this, &CourseModel::onCourseAdded);
connect(m_resourceRepository, &IResourceRepository::courseAboutToBeRemoved, this, &CourseModel::onCourseAboutToBeRemoved);
}
m_courses.clear();
QString languageId;
if (m_language) {
languageId = m_language->id();
}
if (m_resourceRepository) {
for (auto course : m_resourceRepository->courses(languageId)) {
m_courses.append(course.get());
auto courses = m_resourceRepository->courses();
for (int i = 0; i < courses.count(); ++i) {
auto course = courses.at(i);
// TODO only title chagned is connected, change this to a general changed signal
auto connection = connect(course.get(), &ICourse::titleChanged, this, [=](){
const auto row = m_resourceRepository->courses().indexOf(course);
emit dataChanged(index(row, 0), index(row, 0));
});
m_updateConnections.insert(i, connection);
}
}
endResetModel();
emit resourceManagerChanged();
}
IResourceRepository * CourseModel::resourceRepository() const
......@@ -101,39 +101,16 @@ IResourceRepository * CourseModel::resourceRepository() const
return m_resourceRepository;
}
Language * CourseModel::language() const
{
return m_language;
}
void CourseModel::setLanguage(Language *language)
{
beginResetModel();
m_language = language;
m_courses.clear();
QString languageId;
if (m_language) {
languageId = m_language->id();
}
for (auto course : m_resourceRepository->courses(languageId)) {
m_courses.append(course.get());
}
emit languageChanged();
endResetModel();
emit rowCountChanged();
}
QVariant CourseModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid()) {
if (!index.isValid() || !m_resourceRepository) {
return QVariant();
}
if (index.row() >= m_courses.count()) {
if (index.row() >= rowCount()) {
return QVariant();
}
ICourse * const course = m_courses.at(index.row());
auto const course = m_resourceRepository->courses().at(index.row());
switch(role)
{
......@@ -144,72 +121,50 @@ QVariant CourseModel::data(const QModelIndex& index, int role) const
return QVariant(course->title());
case TitleRole:
return course->title();
case I18nTitleRole:
return course->i18nTitle();
case DescriptionRole:
return course->description();
case IdRole:
return course->id();
case ContributerResourceRole:
return false;// m_resources.at(index.row())->isContributorResource();//FIXME
case LanguageRole:
return QVariant::fromValue<QObject*>(course->language().get());
case DataRole:
return QVariant::fromValue<QObject*>(course);
return QVariant::fromValue<QObject*>(course.get());
default:
return QVariant();
}
}
int CourseModel::rowCount(const QModelIndex& parent) const
int CourseModel::rowCount(const QModelIndex&) const
{
if (parent.isValid()) {
if (!m_resourceRepository) {
return 0;
}
return m_courses.count();
return m_resourceRepository->courses().count();
}
void CourseModel::onCourseAboutToBeAdded(ICourse *course, int index)
void CourseModel::onCourseAboutToBeAdded(std::shared_ptr<ICourse> course, int row)
{
Q_UNUSED(index);
beginInsertRows(QModelIndex(), m_courses.count(), m_courses.count());
m_courses.append(course);
connect(course, SIGNAL(titleChanged()), m_signalMapper, SLOT(map()));
//TODO add missing signals
beginInsertRows(QModelIndex(), row, row);
auto connection = connect(course.get(), &ICourse::titleChanged, this, [=](){
const auto row = m_resourceRepository->courses().indexOf(course);
emit dataChanged(index(row, 0), index(row, 0));
});
m_updateConnections.insert(row, connection);
}
void CourseModel::onCourseAdded()
{
updateMappings();
endInsertRows();
emit rowCountChanged();
}
void CourseModel::onCourseAboutToBeRemoved(int index)