diff --git a/src/data/graph.hpp b/src/data/graph.hpp index 75271a18dd60006057266dffe390bb470a09d6b2..3d570985c8804e1809b5ebc1868df3c7b95b910c 100644 --- a/src/data/graph.hpp +++ b/src/data/graph.hpp @@ -19,11 +19,14 @@ #include "nodes/user_point_config.hpp" #include "util/enums.hpp" +#include "util/testing.hpp" #include <cassert> #include <tuple> #include <utility> +class QTextStream; + /** * \brief If N has a member function 'recalculate' and any of ChangedNodes is a * predecessor to N, returns true if 'recalculate' is declared to not throw any @@ -72,6 +75,7 @@ struct DAG { StudentData mStudents{}; BonusPoints mBonus{}; +private: PointsWithoutBonus<Closed> mClosedPointsWithoutBonus{mExam}; PointsWithoutBonus<Open> mOpenPointsWithoutBonus{mExam}; PointsWithoutBonus<Combined> mCombinedPointsWithoutBonus{mExam}; @@ -138,6 +142,7 @@ struct DAG { mClosedGrades, mOpenGrades, mGrades, mGradeAverage); } +public: DAG(DynexiteExam exam, StudentData students, BonusPoints bonus, UserGradeConfig gradeConfig, UserPointConfig pointConfig) : mUserPointConfig(std::move(pointConfig)), @@ -184,6 +189,12 @@ struct DAG { return mOpenPassingLimit.mMaxPassingLimit; } } + [[nodiscard]] constexpr auto getRelPassingLimit() const noexcept { + return mClosedPassingLimit.mRelPassingLimit; + } + [[nodiscard]] constexpr auto getAbsPassingLimit() const noexcept { + return mClosedPassingLimit.mAbsPassingLimit; + } [[nodiscard]] constexpr const auto & getUserGradeBoundaries(const GradingType grading) const noexcept { switch (grading) { @@ -196,6 +207,18 @@ struct DAG { return mOpenUserBoundaries.mBoundaries; } } + [[nodiscard]] constexpr auto + getUserGradeBoundarySetAuto(const GradingType grading) const noexcept { + switch (grading) { + default: + assert(false); + [[fallthrough]]; + case Closed: + return mClosedUserBoundaries.mSetAutomatically; + case Open: + return mOpenUserBoundaries.mSetAutomatically; + } + } [[nodiscard]] constexpr const auto & getGradeList(const GradingType grading) const noexcept { switch (grading) { @@ -238,6 +261,12 @@ struct DAG { return mCombinedPointsWithoutBonus.mMaxPoints; } } + [[nodiscard]] constexpr auto getAvgFirstTryPoints() const noexcept { + + return mUserPointConfig.mCountZerothAttemptAsFirstAttempt + ? mFirstTry.mAvgFirstAndZerothTryPoints + : mFirstTry.mAvgFirstTryPoints; + } [[nodiscard]] constexpr auto getOriginalMaxPointsBoundary(const GradingType grading) const noexcept { switch (grading) { @@ -307,4 +336,14 @@ struct DAG { [[nodiscard]] auto hasBonus() const noexcept { return !mBonus.mTotalBonusPointsPerStudent.empty(); } + + [[nodiscard]] auto getStats() const noexcept { return mStats; } + + [[nodiscard]] auto getGradeAverages() const noexcept { + return mGradeAverage; + } + + friend class DataManager; + friend void writeCurrentSettingsToD2r(QTextStream &); + FRIEND_TEST(FirstTryDagTest, CorrectlyCalculatedInDag); }; diff --git a/src/file_io/d2r_parser.cpp b/src/file_io/d2r_parser.cpp index dfe6a807e4654e52e2ce72f2326d886ca2ca21d6..ff9b0deb1c2a29ccb1dfd2b34ff4bcb512968342 100644 --- a/src/file_io/d2r_parser.cpp +++ b/src/file_io/d2r_parser.cpp @@ -384,6 +384,7 @@ QSharedPointer<ExamSave> parseD2r(QString d2rString, QWidget *parentWidget) { return readStringsToData(readData, parentWidget); } +// Is a friend of struct DAG void writeCurrentSettingsToD2r(QTextStream &out) { const auto cur = DataManager::getCurrent(); if (!cur) @@ -442,12 +443,8 @@ void writeCurrentSettingsToD2r(QTextStream &out) { writeSingleSetting(out, QString("UserGradeBoundaries") + toString(type), ""); { - writeSingleSetting( - out, "SetAuto", - type == GradingType::Closed - ? cur->mClosedUserBoundaries.mSetAutomatically - : cur->mOpenUserBoundaries.mSetAutomatically, - true); + writeSingleSetting(out, "SetAuto", + cur->getUserGradeBoundarySetAuto(type), true); for (const auto &g : grades) { writeSingleSetting( out, "Boundary" + QString::number(static_cast<int>(g)), diff --git a/src/model/grade_model.cpp b/src/model/grade_model.cpp index 73674f8cdb5f9c8e385b565d183c8980b3f75b8a..3e9bdfc417316ada97da2ebf8505a54ba30e0234 100644 --- a/src/model/grade_model.cpp +++ b/src/model/grade_model.cpp @@ -17,10 +17,7 @@ void GradeModel::updateData() { // count how many students have each respective grade - const QList<Grade> &gradeList = - mGradingType == GradingType::Closed ? cur->mClosedGrades.mGrades - : mGradingType == GradingType::Open ? cur->mOpenGrades.mGrades - : cur->mGrades.mGrades; + const auto &gradeList = cur->getGradeList(mGradingType); for (const auto &g : gradeList) { if (static_cast<int>(g) < mGradeCounts.size()) diff --git a/src/view/boundary_window/boundary_tab.cpp b/src/view/boundary_window/boundary_tab.cpp index d1086680a5d8802a71399aa706660b804256e608..b3d9724113a8dfa80cc7af039433e7d7e9201856 100644 --- a/src/view/boundary_window/boundary_tab.cpp +++ b/src/view/boundary_window/boundary_tab.cpp @@ -220,9 +220,7 @@ void BoundaryTab::updateBoundaryTableValues( QSignalBlocker blocker(mAutoCheckBox); const auto autoChangeBoundaries = - mGrading == GradingType::Closed - ? cur->mClosedUserBoundaries.mSetAutomatically - : cur->mOpenUserBoundaries.mSetAutomatically; + cur->getUserGradeBoundarySetAuto(mGrading); mAutoCheckBox.setCheckState(autoChangeBoundaries ? Qt::CheckState::Checked @@ -251,9 +249,7 @@ void BoundaryTab::updateBoundaryTableValues( // The current value of the spin-box, // which is the grade boundary for the current grade - const auto &val = mGrading == GradingType::Closed - ? cur->mClosedUserBoundaries.mBoundaries[grade] - : cur->mOpenUserBoundaries.mBoundaries[grade]; + const auto &val = cur->getUserGradeBoundaries(mGrading)[grade]; // String for the max number of points for the current grade, // equal to the next grade boundary minus one step, or the max amount of @@ -264,11 +260,7 @@ void BoundaryTab::updateBoundaryTableValues( // Special case for grade 1.0: use max overall points as max points // of current grade boundary if (gradeIdx == 0) { - if (grading == GradingType::Closed) { - res = cur->mChangedMaxPointsClosed.mChangedMaxPointBoundary; - } else { - res = cur->mChangedMaxPointsOpen.mChangedMaxPointBoundary; - } + res = cur->getChangedMaxPointsBoundary(grading); } else { // max is one step below the next boundary // If that is below the current boundary, write "-" instead @@ -289,12 +281,7 @@ void BoundaryTab::updateBoundaryTableValues( const auto &rangeMax = [cur, grading = mGrading, gradeIdx] { // Special case for grade 1.0 if (gradeIdx == 0) { - if (grading == GradingType::Closed) { - return cur->mChangedMaxPointsClosed - .mChangedMaxPointBoundary; - } else { - return cur->mChangedMaxPointsOpen.mChangedMaxPointBoundary; - } + return cur->getChangedMaxPointsBoundary(grading); } else { return cur->getUserGradeBoundaries( grading)[static_cast<Grade>(gradeIdx - 1)]; @@ -305,13 +292,8 @@ void BoundaryTab::updateBoundaryTableValues( if (gradeIdx == static_cast<std::size_t>(Grade::grade5_0)) { return GradeBoundary{0}; } else { - if (grading == GradingType::Closed) { - return cur->mClosedUserBoundaries - .mBoundaries[static_cast<Grade>(gradeIdx + 1)]; - } else { - return cur->mOpenUserBoundaries - .mBoundaries[static_cast<Grade>(gradeIdx + 1)]; - } + return cur->getUserGradeBoundaries( + grading)[static_cast<Grade>(gradeIdx + 1)]; } }(); diff --git a/src/view/main_window/export_rwthonline.cpp b/src/view/main_window/export_rwthonline.cpp index 7fb660cdb39b6b8cce112afa50cce49e1797b8f6..cd5425b0bf6a9a7e13e19df5859b244e1a6afd69 100644 --- a/src/view/main_window/export_rwthonline.cpp +++ b/src/view/main_window/export_rwthonline.cpp @@ -40,10 +40,7 @@ qsizetype getColumn(const CsvLine &headerLine, const QString &headerText) { const auto nrBonusPoints = cur->mBonus.mTotalBonusPointsPerStudent .value(cur->mExam.mIdentifier[idx]) .toString(); - const auto maxGradeBoundary = - type == GradingType::Closed - ? cur->mChangedMaxPointsClosed.mChangedMaxPointBoundary - : cur->mChangedMaxPointsOpen.mChangedMaxPointBoundary; + const auto maxGradeBoundary = cur->getChangedMaxPointsBoundary(type); const auto max = maxGradeBoundary.toString(cur->mUserPointConfig.mGradeBoundaryStepSize); @@ -68,11 +65,12 @@ qsizetype getColumn(const CsvLine &headerLine, const QString &headerText) { if (type == GradingType::Closed) { // Add info on average points of candidates taking the exam for the // first time - if (!cur->mFirstTry.mAvgFirstTryPoints.has_value()) { + const auto avgFirstTryPoints = cur->getAvgFirstTryPoints(); + if (!avgFirstTryPoints.has_value()) { remark += str::fileExport::rwthOnlineRemarkAverageFirstNone; } else { remark += QString(str::fileExport::rwthOnlineRemarkAverageFirstAny) - .arg(cur->mFirstTry.mAvgFirstTryPoints->toString()); + .arg(avgFirstTryPoints->toString()); } } diff --git a/src/view/main_window/grade_histogram.cpp b/src/view/main_window/grade_histogram.cpp index e2ebbcc9552ecc5731e9364be3e4fd56fe1cbce7..2199272f94f752bab8751114fede4743bea0b00f 100644 --- a/src/view/main_window/grade_histogram.cpp +++ b/src/view/main_window/grade_histogram.cpp @@ -183,7 +183,7 @@ void GradeHistogram::gradesChanged(const EnumStorage<bool> &changedTypes) { // not attended mBarSets[modelIdx]->replace( static_cast<int>(Grade::NR_VALS), - DataManager::getCurrent()->mStats.mNrRegisteredNotAttended); + DataManager::getCurrent()->getStats().mNrRegisteredNotAttended); // color non-passed-grades (5.0 and not attended) red by selecting // them and setting the selection-color to red @@ -201,7 +201,7 @@ void GradeHistogram::updateYAxis() { // find largest bar (either the not-attended bar or the bar of a grade of a // grading type) const auto cur = DataManager::getCurrent(); - int largestBar = cur ? cur->mStats.mNrRegisteredNotAttended : 0; + int largestBar = cur ? cur->getStats().mNrRegisteredNotAttended : 0; for (const auto &model : mModels) { largestBar = std::max(largestBar, *std::ranges::max_element(model->mGradeCounts)); diff --git a/src/view/main_window/main_window.cpp b/src/view/main_window/main_window.cpp index 40892fd7c4b5099cf1741af08e87eb898b1ec175..1e73dbe8eda0b588e301369c06147af657240f24 100644 --- a/src/view/main_window/main_window.cpp +++ b/src/view/main_window/main_window.cpp @@ -60,8 +60,8 @@ void printBoundaries(QString &str, const GradingType type, const bool hasType, " part:\n" "Grade --- [credits >=] --- [credits <]\n"; - auto lastGradeBoundary = - cur->mChangedMaxPointsOpen.mChangedMaxPointBoundary; + // Initial value doesn't matter, because it gets overwritten with "max" + auto lastGradeBoundary = cur->getChangedMaxPointsBoundary(type); for (const auto g : grades) { const auto minBoundary = cur->getUserGradeBoundaries(type)[g]; @@ -378,7 +378,7 @@ void MainWindow::updateMenuItems() const { const auto cur = DataManager::getCurrent(); const auto nrUnknownStudents = - cur ? cur->mStats.mNrAttendedNotRegistered : 0; + cur ? cur->getStats().mNrAttendedNotRegistered : 0; mExportUnknownStudentsAct->setText("Manual: Unknown Students (" + QString::number(nrUnknownStudents) + diff --git a/src/view/main_window/min_max_point_screws.cpp b/src/view/main_window/min_max_point_screws.cpp index f0fce86baf5c17ff9da398261a3f36b775c2d75a..d6a9d51019a909713bab8e69be945a7a5c905cc8 100644 --- a/src/view/main_window/min_max_point_screws.cpp +++ b/src/view/main_window/min_max_point_screws.cpp @@ -255,10 +255,10 @@ void MinMaxPointScrewColumn::updateHelpText() noexcept { */ const GradeBoundaryFactory f{cur->mUserPointConfig.mGradeBoundaryStepSize, cur->mUserPointConfig.mGradeBoundaryRounding}; + const auto relPassMarkDecisive = - cur->mClosedPassingLimit.mRelPassingLimit.has_value() && - f(*cur->mClosedPassingLimit.mRelPassingLimit) < - f(cur->mClosedPassingLimit.mAbsPassingLimit); + cur->getRelPassingLimit().has_value() && + f(*cur->getRelPassingLimit()) < f(cur->getAbsPassingLimit()); // Use rich text here for smart line breaks QString questionIconHoverText; @@ -377,25 +377,27 @@ void MinMaxPointScrews::gradesChanged() noexcept { return; } + const auto gradeAverages = cur->getGradeAverages(); + // Number of decimal places for average grade is always 2, independent of // the grade boundary step size, because this is an averaged grade, not a // point count - if (cur->mGradeAverage.mAverageGrade.has_value()) { + if (gradeAverages.mAverageGrade.has_value()) { mAverageGrade.setText( - QString::number(*cur->mGradeAverage.mAverageGrade, 'f', 2)); + QString::number(*gradeAverages.mAverageGrade, 'f', 2)); } else { mAverageGrade.setText("-"); } - if (cur->mGradeAverage.mAverageGradePassed.has_value()) { + if (gradeAverages.mAverageGradePassed.has_value()) { mAverageGradePassed.setText( - QString::number(*cur->mGradeAverage.mAverageGradePassed, 'f', 2)); + QString::number(*gradeAverages.mAverageGradePassed, 'f', 2)); } else { mAverageGradePassed.setText("-"); } { - const auto &nrPassed = cur->mGradeAverage.mNrPassed; - const auto &nrAttended = static_cast<int>(cur->mGrades.mGrades.size()); + const auto &nrPassed = gradeAverages.mNrPassed; + const auto &nrAttended = cur->getStats().mNrAttended; const auto &nrFailed = nrAttended - nrPassed; if (nrAttended == 0) { mNrPassed.setText(attendedResultNone); diff --git a/src/view/statistic_window/calculation_window.cpp b/src/view/statistic_window/calculation_window.cpp index 577040c6cf5a08110a7470d0d6311ee5f706db76..508821678217df1ee97cacc4193f96ba5780a58b 100644 --- a/src/view/statistic_window/calculation_window.cpp +++ b/src/view/statistic_window/calculation_window.cpp @@ -119,16 +119,13 @@ void CalculationWindow::gradesChanged() { return; } - const auto firstTryPoints = - cur->mUserPointConfig.mCountZerothAttemptAsFirstAttempt - ? cur->mFirstTry.mAvgFirstAndZerothTryPoints - : cur->mFirstTry.mAvgFirstTryPoints; + const auto firstTryPoints = cur->getAvgFirstTryPoints(); const auto hasFirstTry = firstTryPoints.has_value(); const auto stepSize = cur->mUserPointConfig.mGradeBoundaryStepSize; const auto changedMaxPoints = - cur->mChangedMaxPointsClosed.mChangedMaxPointBoundary; + cur->getChangedMaxPointsBoundary(GradingType::Closed); mMaxClosed.setText(changedMaxPoints.toString(stepSize)); if (hasFirstTry) { mAvgFirstTry.setText(firstTryPoints->toStringLowPrecision()); @@ -136,14 +133,14 @@ void CalculationWindow::gradesChanged() { mAvgFirstTry.setText("None"); } - const auto absPassMark = cur->mClosedPassingLimit.mAbsPassingLimit; + const auto absPassMark = cur->getAbsPassingLimit(); mAbsPassMark.setText(QString("0.6 * %1 = <b>%2</b>") .arg(changedMaxPoints.toString(stepSize), absPassMark.toStringLowPrecision())); // Relative pass mark const auto relPassMark0_5 = - toPoints(cur->mChangedMaxPointsClosed.mChangedMaxPointBoundary, + toPoints(changedMaxPoints, cur->mUserPointConfig.mGradeBoundaryStepSize) * 0.5; const auto relPassMark0_78 = firstTryPoints.value_or(Points{0.}) * 0.78; diff --git a/src/view/statistic_window/statistic_window.cpp b/src/view/statistic_window/statistic_window.cpp index 9aebea0a7490ce8deaa823af335095c0c11bf6db..ce1ab5eec3f0c14124cf8829df6253d1924775de 100644 --- a/src/view/statistic_window/statistic_window.cpp +++ b/src/view/statistic_window/statistic_window.cpp @@ -41,7 +41,7 @@ void StatisticWindow::nrStudentsChanged() { return; } - const auto &stats = cur->mStats; + const auto &stats = cur->getStats(); mNrAttended.setText(QString::number(stats.mNrAttended)); mNrAttendedRegistered.setText(QString::number(stats.mNrAttendedRegistered)); diff --git a/test/data/nodes/user_grade_boundaries_test.cpp b/test/data/nodes/user_grade_boundaries_test.cpp index e06f1da76009a1466a0985c72957c71b5e97a51d..d1a4f1d914c4d7fa2f72532deb18a16204c866a5 100644 --- a/test/data/nodes/user_grade_boundaries_test.cpp +++ b/test/data/nodes/user_grade_boundaries_test.cpp @@ -60,8 +60,8 @@ TEST_P(UserGradeBoundaryTest, boundariesDescending) { TEST_P(UserGradeBoundaryTest, defaultFollowDefault) { const auto &graph = GetParam().second; - EXPECT_TRUE(graph.mClosedUserBoundaries.mSetAutomatically); - EXPECT_TRUE(graph.mOpenUserBoundaries.mSetAutomatically); + EXPECT_TRUE(graph.getUserGradeBoundarySetAuto(GradingType::Closed)); + EXPECT_TRUE(graph.getUserGradeBoundarySetAuto(GradingType::Open)); } // todo: split up?