Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Build backend tests with coverage
run: |
sudo apt-get update
sudo apt-get install -y libgtest-dev cmake
sudo apt-get install -y libgtest-dev cmake qt6-base-dev libqt6svg6-dev
cd /usr/src/gtest && sudo cmake . && sudo make && sudo cp lib/*.a /usr/lib
cd $GITHUB_WORKSPACE
build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} \
Expand All @@ -49,6 +49,18 @@ jobs:
- name: Run tests and dump XML
run: ./test_runner

- name: Build Qt tests
working-directory: tests
run: |
qmake6 tests.pro
make -j"$(nproc)"

- name: Run Qt tests
working-directory: tests
env:
QT_QPA_PLATFORM: offscreen
run: ./.bin/tutor_tests

- name: Install gcovr & generate SonarQube XML coverage
run: |
sudo apt-get update
Expand All @@ -65,4 +77,3 @@ jobs:
--define sonar.cfamily.build-wrapper-output=${{ env.BUILD_WRAPPER_OUT_DIR }}
--define sonar.coverageReportPaths=coverage.xml
--define sonar.qualitygate.breakBuild=false

3 changes: 3 additions & 0 deletions SyntaxTutor.pro
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ SOURCES += \
src/backend/symbol_table.cpp \
src/widgets/customtextedit.cpp \
src/widgets/grammarview.cpp \
src/gui/examreportdialog.cpp \
src/gui/grammareditordialog.cpp \
src/gui/lltabledialog.cpp \
src/gui/lltutorwindow.cpp \
Expand All @@ -55,6 +56,8 @@ HEADERS += \
src/backend/symbol_table.hpp \
src/widgets/customtextedit.h \
src/widgets/grammarview.h \
src/gui/examreportdialog.h \
src/gui/examsession.h \
src/gui/grammareditordialog.h \
src/gui/lltabledialog.h \
src/gui/lltutorwindow.h \
Expand Down
94 changes: 90 additions & 4 deletions resources/styles/app.qss
Original file line number Diff line number Diff line change
Expand Up @@ -721,26 +721,112 @@ QDialog[grammarEditor="true"] QPushButton#grammarEditorStartButton:disabled {
border: 1px solid #353D40;
}

QCheckBox#customGrammarCheck {
QCheckBox#customGrammarCheck,
QCheckBox#examModeCheck {
color: #C9D0D2;
font-size: 13px;
spacing: 8px;
}

QCheckBox#customGrammarCheck::indicator {
QCheckBox#customGrammarCheck::indicator,
QCheckBox#examModeCheck::indicator {
width: 16px;
height: 16px;
border-radius: 4px;
border: 1px solid #3A4144;
background-color: #2A2E30;
}

QCheckBox#customGrammarCheck::indicator:hover {
QCheckBox#customGrammarCheck::indicator:hover,
QCheckBox#examModeCheck::indicator:hover {
border: 1px solid #11B3BC;
}

QCheckBox#customGrammarCheck::indicator:checked {
QCheckBox#customGrammarCheck::indicator:checked,
QCheckBox#examModeCheck::indicator:checked {
background-color: #11B3BC;
border: 1px solid #11B3BC;
image: url(:/resources/check.svg);
}

QDialog[examReport="true"] {
background-color: #1F1F1F;
border: none;
}

QDialog[examReport="true"] QLabel#examReportEyebrow {
color: #11B3BC;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
}

QDialog[examReport="true"] QLabel#examReportTitle {
color: #F4F7F8;
font-size: 28px;
font-weight: 700;
}

QDialog[examReport="true"] QLabel#examReportGrade {
font-size: 56px;
font-weight: 800;
color: #11B3BC;
}

QDialog[examReport="true"] QLabel#examReportGrade[passed="false"] {
color: #E0635F;
}

QDialog[examReport="true"] QLabel#examReportGradeCaption {
color: #9AA5A8;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
}

QDialog[examReport="true"] QLabel#examReportStats {
color: #DEE5E7;
font-size: 15px;
}

QDialog[examReport="true"] QLabel#examReportVerdict {
color: #9FE3E7;
font-size: 13px;
font-weight: 700;
}

QDialog[examReport="true"] QLabel#examReportVerdict[passed="false"] {
color: #F1B0AE;
}

QDialog[examReport="true"] QLabel#examReportReviewCaption {
color: #9FCFD2;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
}

QDialog[examReport="true"] QTextBrowser#examReportReview {
background-color: #212526;
color: #E8ECEE;
border: 1px solid #303638;
border-radius: 12px;
padding: 10px 12px;
selection-background-color: #35515A;
}

QDialog[examReport="true"] QPushButton#examReportCloseButton {
background: transparent;
color: #AAB5B8;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 10px 16px;
font-size: 13px;
font-weight: 600;
}

QDialog[examReport="true"] QPushButton#examReportCloseButton:hover {
background-color: rgba(255, 255, 255, 0.03);
color: #FFFFFF;
border: 1px solid rgba(255, 255, 255, 0.12);
}
179 changes: 179 additions & 0 deletions src/gui/examreportdialog.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* SyntaxTutor - Interactive Tutorial About Syntax Analyzers
* Copyright (C) 2025 Jose R. (jose-rzm)
*
* 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 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU 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 <https://www.gnu.org/licenses/>.
*/

#include "examreportdialog.h"

#include <QHBoxLayout>
#include <QLabel>
#include <QLocale>
#include <QPushButton>
#include <QTextBrowser>
#include <QVBoxLayout>

namespace {
constexpr auto kPassColor = "#11B3BC";
constexpr auto kFailColor = "#E0635F";

QString escaped(QString text) {
return text.toHtmlEscaped().replace(QStringLiteral("\n"),
QStringLiteral("<br/>"));
}
} // namespace

ExamReportDialog::ExamReportDialog(const ExamSession& session,
const QString& examTitle, QWidget* parent)
: QDialog(parent) {
setObjectName("examReportDialog");
setProperty("examReport", true);
setWindowTitle(tr("Informe del examen"));
setModal(true);
resize(720, 600);
setMinimumSize(620, 480);

const double grade = session.grade();
const QString gradeText = QLocale().toString(grade, 'f', 1);
const bool passed = grade >= 5.0;
const int percent =
session.total() == 0
? 0
: static_cast<int>(100.0 * session.right() / session.total());

auto* rootLayout = new QVBoxLayout(this);
rootLayout->setContentsMargins(28, 24, 28, 24);
rootLayout->setSpacing(12);

auto* eyebrow = new QLabel(tr("MODO EXAMEN"), this);
eyebrow->setObjectName("examReportEyebrow");
rootLayout->addWidget(eyebrow);

auto* title = new QLabel(examTitle, this);
title->setObjectName("examReportTitle");
rootLayout->addWidget(title);

auto* headerLayout = new QHBoxLayout();
headerLayout->setSpacing(16);

auto* gradeLabel = new QLabel(gradeText, this);
gradeLabel->setObjectName("examReportGrade");
gradeLabel->setProperty("passed", passed);
headerLayout->addWidget(gradeLabel, 0, Qt::AlignTop);

auto* statsColumn = new QVBoxLayout();
statsColumn->setSpacing(2);
auto* gradeCaption = new QLabel(tr("sobre 10"), this);
gradeCaption->setObjectName("examReportGradeCaption");
statsColumn->addWidget(gradeCaption);
auto* statsLabel = new QLabel(tr("%1 de %2 respuestas correctas (%3%)")
.arg(session.right())
.arg(session.total())
.arg(percent),
this);
statsLabel->setObjectName("examReportStats");
statsColumn->addWidget(statsLabel);
auto* verdictLabel =
new QLabel(passed ? tr("Aprobado") : tr("Suspenso"), this);
verdictLabel->setObjectName("examReportVerdict");
verdictLabel->setProperty("passed", passed);
statsColumn->addWidget(verdictLabel);
statsColumn->addStretch(1);
headerLayout->addLayout(statsColumn, 1);

rootLayout->addLayout(headerLayout);

auto* reviewCaption = new QLabel(tr("REVISIÓN"), this);
reviewCaption->setObjectName("examReportReviewCaption");
rootLayout->addWidget(reviewCaption);

auto* review = new QTextBrowser(this);
review->setObjectName("examReportReview");
review->setOpenExternalLinks(false);
const QString reviewHtml = buildReviewHtml(session);
review->setHtml(reviewHtml);
rootLayout->addWidget(review, 1);

reportHtml_ = QStringLiteral("<h1>%1</h1>"
"<p><b>%2</b> %3 — %4</p>%5")
.arg(escaped(examTitle), tr("Calificación:"),
tr("%1 / 10").arg(gradeText),
tr("%1 de %2 respuestas correctas (%3%)")
.arg(session.right())
.arg(session.total())
.arg(percent),
reviewHtml);

auto* footerLayout = new QHBoxLayout();
footerLayout->setSpacing(12);
footerLayout->addStretch(1);

auto* exportButton = new QPushButton(tr("Exportar PDF"), this);
exportButton->setObjectName("examReportExportButton");
exportButton->setProperty("role", "primary");
exportButton->setAutoDefault(false);
exportButton->setCursor(Qt::PointingHandCursor);
footerLayout->addWidget(exportButton);

auto* closeButton = new QPushButton(tr("Cerrar"), this);
closeButton->setObjectName("examReportCloseButton");
closeButton->setAutoDefault(false);
closeButton->setCursor(Qt::PointingHandCursor);
footerLayout->addWidget(closeButton);

rootLayout->addLayout(footerLayout);

connect(exportButton, &QPushButton::clicked, this,
&ExamReportDialog::exportRequested);
connect(closeButton, &QPushButton::clicked, this, &QDialog::accept);
}

QString ExamReportDialog::buildReviewHtml(const ExamSession& session) const {
QString html = QStringLiteral("<table width='100%' cellspacing='0' "
"cellpadding='6' style='font-size:13px;'>");
html +=
QStringLiteral("<tr>"
"<th align='left'>%1</th>"
"<th align='left'>%2</th>"
"<th align='left'>%3</th>"
"<th align='left'></th>"
"</tr>")
.arg(tr("Pregunta"), tr("Tu respuesta"), tr("Respuesta correcta"));

int index = 0;
for (const ExamRecord& record : session.records()) {
const QString rowColor = (index % 2 == 0) ? QStringLiteral("#212526")
: QStringLiteral("#1B1F20");
const QString mark =
record.correct ? QStringLiteral("<span style='color:%1;'>✔</span>")
.arg(kPassColor)
: QStringLiteral("<span style='color:%1;'>✘</span>")
.arg(kFailColor);
html += QStringLiteral("<tr style='background-color:%1;'>"
"<td>%2</td>"
"<td>%3</td>"
"<td>%4</td>"
"<td align='center'>%5</td>"
"</tr>")
.arg(rowColor, escaped(record.question),
record.userAnswer.trimmed().isEmpty()
? tr("(vacía)")
: escaped(record.userAnswer),
escaped(record.correctAnswer), mark);
++index;
}
html += QStringLiteral("</table>");
return html;
}
Loading
Loading