diff --git a/.github/workflows/windows-qt6.yml b/.github/workflows/windows-qt6.yml
index 333a8d6e..b6f5067c 100644
--- a/.github/workflows/windows-qt6.yml
+++ b/.github/workflows/windows-qt6.yml
@@ -82,8 +82,7 @@ jobs:
-GNinja \
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
-DLEMON_BUILD_INFO="Build for Windows" \
- -DLEMON_BUILD_EXTRA_INFO="Build on Windows x64" \
- -DENABLE_XLS_EXPORT=ON
+ -DLEMON_BUILD_EXTRA_INFO="Build on Windows x64"
cmake --build . --parallel $(nproc)
- name: Deploy Qt for tests
shell: pwsh
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f0dba817..874760db 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -19,8 +19,6 @@ option(EMBED_TRANSLATIONS "Embed translations" ON)
LEMONLOG(EMBED_TRANSLATIONS)
option(EMBED_DOCS "Embed documents" ON)
LEMONLOG(EMBED_DOCS)
-option(ENABLE_XLS_EXPORT "XLS Result Export Support (Windows Only)" OFF)
-LEMONLOG(ENABLE_XLS_EXPORT)
option(BUILD_DEB "Build deb format package" OFF)
LEMONLOG(BUILD_DEB)
option(BUILD_RPM "Build rpm format package" OFF)
@@ -287,12 +285,6 @@ endif()
target_include_directories(lemon PUBLIC ${CMAKE_SOURCE_DIR}/src)
-if(WIN32 AND ENABLE_XLS_EXPORT)
- find_package(${LEMON_QT_LIBNAME} COMPONENTS AxContainer REQUIRED)
- list(APPEND LEMON_QT_LIBS ${LEMON_QT_LIBNAME}::AxContainer)
- add_definitions(-DENABLE_XLS_EXPORT)
-endif()
-
target_precompile_headers(lemon PUBLIC ${CMAKE_SOURCE_DIR}/src/pch.h)
target_link_libraries(lemon PRIVATE ${LEMON_QT_LIBS} lemon-core lemon-base SingleApplication::SingleApplication spdlog::spdlog)
diff --git a/resource.qrc b/resource.qrc
index af75dcc7..727252e0 100755
--- a/resource.qrc
+++ b/resource.qrc
@@ -7,6 +7,9 @@
makespec/VERSIONSUFFIX
assets/lemon-lime.ico
+
+ src/component/exportutil/export_template.html
+
assets/icons/lemon-lime.png
assets/pics/code-function.svg
diff --git a/src/component/exportutil/README.md b/src/component/exportutil/README.md
new file mode 100644
index 00000000..77f66afc
--- /dev/null
+++ b/src/component/exportutil/README.md
@@ -0,0 +1,151 @@
+# ExportUtil 导出模块
+
+本模块负责将比赛成绩导出为 HTML 或 CSV 格式。
+
+HTML 导出采用 **JSON + 模板** 方案:C++ 端通过 `buildExportJson()` 构建包含全部比赛数据的 JSON 对象,然后将其嵌入 `export_template.html` 中的 `%%DATA%%` 占位符,由浏览器端 JavaScript 完成页面渲染。这种方式将数据构造与页面展示解耦,C++ 代码只需关注 JSON 结构,无需拼接 HTML 字符串。
+
+以下是 JSON 数据的结构说明。
+
+### 顶级字段
+
+| 字段名 | 类型 | 说明 | 举例 |
+| ------------- | ---------- | ---------------- | ------------------------------------- |
+| `name` | `string` | 比赛名称 | `"contest"` |
+| `version` | `string` | LemonLime 版本 | `"Lemonlime Version 0.3.6.1:7545e02"` |
+| `task_names` | `string[]` | 题目名称数组 | `["plus", "minus"]` |
+| `i18n` | `object` | i18n 字典 | 见下节 |
+| `contestants` | `object[]` | 选手成绩数据数组 | 见下节 |
+
+### i18n
+
+| 字段名 | 举例 |
+| ------------- | --------------------------------------------------------- |
+| `rank_list` | `"排名表"` |
+| `hint` | `"点击名字或单题分数跳转到详细信息。使用 LemonLime 评测"` |
+| `rank` | `"排名"` |
+| `name` | `"名称"` |
+| `total` | `"总分"` |
+| `contestant` | `"选手"` |
+| `task` | `"试题"` |
+| `source_file` | `"源程序:"` |
+| `no_source` | `"未找到选手程序"` |
+| `testcase` | `"测试点"` |
+| `input` | `"输入文件"` |
+| `result` | `"测试结果"` |
+| `time` | `"运行用时"` |
+| `memory` | `"内存消耗"` |
+| `score` | `"得分"` |
+| `return_to_top` | `"返回顶部"` |
+
+---
+
+### contestants 数组内对象
+
+| 字段名 | 类型 | 说明 | 举例 |
+| -------------- | ---------- | -------------------- | --------------- |
+| `name` | `string` | 选手姓名 | `"Alice"` |
+| `rank` | `number` | 选手排名 | `1` |
+| `total_score` | `number` | 选手总分 | `200` |
+| `total_bg` | `string` | 总分单元格背景 (HSL) | `"0, 70%, 90%"` |
+| `total_border` | `string` | 总分单元格边框 (HSL) | `"0, 70%, 50%"` |
+| `tasks` | `object[]` | 题目结果数组 | 见下节 |
+
+---
+
+### tasks 数组内对象
+
+| 字段名 | 类型 | 说明 | 举例 |
+| --------- | ---------- | ------------------ | ----------------- |
+| `score` | `number` | 该题单项得分 | `100` |
+| `bg` | `string` | 该题得分背景 (HSL) | `"120, 30%, 60%"` |
+| `file` | `string` | 选手程序文件名(可选,若无该字段则表示找不到选手程序) | `"plus.cpp"` |
+| `details` | `object[]` | 测试点详情数组 | 见下节 |
+
+---
+
+### details 数组内对象
+
+| 字段名 | 类型 | 说明 | 举例 |
+| ------------ | -------- | ---------------------------------------------------- | ------------------------------ |
+| `label` | `string` | 测试点编号或子任务信息,允许使用 `
` | `"#2
子任务依赖情况:Pure"` |
+| `row_span` | `number` | 合并单元格行数(0 表示该行被合并,1 表示该行不合并) | `1` |
+| `input` | `string` | 该测试点输入文件名 | `"plus2.in"` |
+| `result` | `string` | 评测状态结果文字 | `"评测通过"` |
+| `time` | `string` | 运行耗时字符串 | `"0.005 s"` |
+| `memory` | `string` | 内存占用字符串 | `"5.34 MB"` |
+| `score` | `number` | 该项得分 | `20` |
+| `full_score` | `number` | 该项满分 | `20` |
+| `bg` | `string` | 状态背景 (RGB) | `"rgb(192, 255, 192)"` |
+| `info` | `string` | 测评信息(可选) | `在第四行,读取到 123456,但期望 789123` |
+
+---
+
+### 示例 JSON
+
+以下 JSON 可用于调试模板渲染:
+
+```json
+{
+ "name": "example contest",
+ "version": "Lemonlime Version 0.3.6.1:7545e02",
+ "i18n": {
+ "rank_list": "排名表",
+ "hint": "点击名字或单题分数跳转到详细信息。使用 LemonLime 评测",
+ "rank": "排名",
+ "name": "名称",
+ "total": "总分",
+ "contestant": "选手",
+ "task": "试题",
+ "source_file": "源程序:",
+ "no_source": "未找到选手程序",
+ "testcase": "测试点",
+ "input": "输入文件",
+ "result": "测试结果",
+ "time": "运行用时",
+ "memory": "内存消耗",
+ "score": "得分"
+ },
+ "task_names": ["plus", "minus"],
+ "contestants": [
+ {
+ "name": "Alice",
+ "rank": 1,
+ "total_score": 150,
+ "total_bg": "120, 30%, 90%",
+ "total_border": "120, 30%, 50%",
+ "tasks": [
+ {
+ "score": 100,
+ "bg": "120, 30%, 70%",
+ "file": "plus.cpp",
+ "details": [
+ { "label": "#1", "row_span": 1, "input": "plus1.in", "result": "评测通过", "time": "0.001 s", "memory": "1.2 MB", "score": 50, "full_score": 50, "bg": "rgb(192, 255, 192)" },
+ { "label": "#2", "row_span": 1, "input": "plus2.in", "result": "评测通过", "time": "0.002 s", "memory": "1.2 MB", "score": 50, "full_score": 50, "bg": "rgb(192, 255, 192)" }
+ ]
+ },
+ {
+ "score": 50,
+ "bg": "120,28.9006%,82.3499%",
+ "file": "minus.cpp",
+ "details": [
+ { "label": "#1", "row_span": 1, "input": "minus1.in", "result": "评测通过", "time": "0.001 s", "memory": "1.2 MB", "score": 50, "full_score": 50, "bg": "rgb(192, 255, 192)" },
+ { "label": "#2
子任务依赖情况:Pure", "row_span": 2, "input": "minus2.in", "result": "答案错误", "time": "0.001 s", "memory": "2.0 MB", "score": 0, "full_score": 50, "bg": "rgb(255, 192, 192)", "info": "在第四行,读取到 123456,但期望 789123" },
+ { "label": "", "row_span": 0, "input": "minus3.in", "result": "运行错误", "time": "0.001 s", "memory": "2.0 MB", "score": 0, "full_score": 50, "bg": "rgb(255, 192, 192)" }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Bob",
+ "rank": 2,
+ "total_score": 0,
+ "total_bg": "0, 0%, 90%",
+ "total_border": "0, 0%, 70%",
+ "tasks": [
+ { "score": 0, "bg": "0, 0%, 90%" },
+ { "score": 0, "bg": "0, 0%, 90%" }
+ ]
+ }
+ ]
+}
+```
\ No newline at end of file
diff --git a/src/component/exportutil/export_template.html b/src/component/exportutil/export_template.html
new file mode 100644
index 00000000..bce047ce
--- /dev/null
+++ b/src/component/exportutil/export_template.html
@@ -0,0 +1,305 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/component/exportutil/exportutil.cpp b/src/component/exportutil/exportutil.cpp
index 3cb35652..71dfeb32 100644
--- a/src/component/exportutil/exportutil.cpp
+++ b/src/component/exportutil/exportutil.cpp
@@ -22,6 +22,9 @@
#include
#include
+#include
+#include
+#include
#include
#include
#include
@@ -29,246 +32,29 @@
ExportUtil::ExportUtil(QObject *parent) : QObject(parent) {}
-const auto resultStateMap = []() {
- QMap> resMap;
- for (int i = static_cast(CorrectAnswer); i != static_cast(LastResultState); i++) {
- QString text, frColor, bgColor;
- Settings::setTextAndColor(static_cast(i), text, frColor, bgColor);
- resMap[static_cast(i)] = {text, frColor, bgColor};
- }
- return resMap;
-}();
-auto ExportUtil::getContestantHtmlCode(Contest *contest, Contestant *contestant, int num) -> QString {
- QString htmlCode;
- QList taskList = contest->getTaskList();
-
- for (int i = 0; i < taskList.size(); i++) {
- htmlCode += QString(R"()").arg(num).arg(i);
- htmlCode += QString("%1 %2
").arg(tr("Task")).arg(taskList[i]->getProblemTitle());
-
- if (! contestant->getCheckJudged(i)) {
- htmlCode += QString(" %1
").arg(tr("Not judged"));
- continue;
- }
-
- if (taskList[i]->getTaskType() == Task::Traditional ||
- taskList[i]->getTaskType() == Task::Interaction ||
- taskList[i]->getTaskType() == Task::Communication ||
- taskList[i]->getTaskType() == Task::CommunicationExec) {
- if (contestant->getCompileState(i) != CompileSuccessfully) {
- switch (contestant->getCompileState(i)) {
- case NoValidGraderFile:
- htmlCode += QString(" %1
")
- .arg(tr("Main grader (grader.*) cannot be found"));
- break;
-
- case NoValidSourceFile:
- htmlCode +=
- QString(" %1").arg(tr("Cannot find valid source file"));
- break;
-
- case CompileTimeLimitExceeded:
- htmlCode += QString(" %1%2
")
- .arg(tr("Source file: "))
- .arg(contestant->getSourceFile(i));
- htmlCode +=
- QString(" %1").arg(tr("Compile time limit exceeded"));
- break;
-
- case InvalidCompiler:
- htmlCode += QString(" %1").arg(tr("Cannot run given compiler"));
- break;
-
- case CompileError:
- htmlCode += QString(" %1%2
")
- .arg(tr("Source file: "))
- .arg(contestant->getSourceFile(i));
- htmlCode += QString(" %1").arg(tr("Compile error"));
-
- if (! contestant->getCompileMessage(i).isEmpty()) {
- QString compileMessage = contestant->getCompileMessage(i);
- compileMessage.replace("\r\n", "
");
- compileMessage.replace("\n", "
");
- compileMessage.replace("\r", "
");
-
- if (compileMessage.endsWith("
"))
- compileMessage.chop(4);
-
- htmlCode += R"()";
- htmlCode += "";
- htmlCode += compileMessage;
- htmlCode += " |
";
- }
-
- htmlCode += "";
- break;
-
- default:
- break;
- }
-
- continue;
- }
-
- htmlCode +=
- QString(" %1%2").arg(tr("Source file: ")).arg(contestant->getSourceFile(i));
- }
-
- htmlCode += "";
- htmlCode += QString(R"(| %1 | )").arg(tr("Test Case"));
- htmlCode += QString(R"(%1 | )").arg(tr("Input File"));
- htmlCode += QString(R"(%1 | )").arg(tr("Result"));
- htmlCode += QString(R"(%1 | )").arg(tr("Time Used"));
- htmlCode += QString(R"(%1 | )").arg(tr("Memory Used"));
- htmlCode += QString(R"(%1 |
)").arg(tr("Score"));
- QList testCases = taskList[i]->getTestCaseList();
- QList inputFiles = contestant->getInputFiles(i);
- QList> result = contestant->getResult(i);
- QList message = contestant->getMessage(i);
- QList> timeUsed = contestant->getTimeUsed(i);
- QList> memoryUsed = contestant->getMemoryUsed(i);
- QList> score = contestant->getScore(i);
-
- for (int j = 0; j < inputFiles.size(); j++) {
- for (int k = 0; k < inputFiles[j].size(); k++) {
- htmlCode += "";
-
- if (k == 0) {
- if (score[j].size() == inputFiles[j].size())
- htmlCode +=
- QString(R"(| #%2 | )").arg(inputFiles[j].size()).arg(j + 1);
- else
- htmlCode += QString(R"(#%2 %3:%4 | )")
- .arg(inputFiles[j].size())
- .arg(j + 1)
- .arg(tr("Subtask Dependence Status"))
- .arg(statusRankingText(score[j].back()));
- }
-
- htmlCode += QString("%1 | ").arg(inputFiles[j][k]);
- QString text, bgColor, frColor;
- Settings::setTextAndColor(result[j][k], text, frColor, bgColor);
- htmlCode +=
- QString("%1").arg(text).arg(static_cast(result[j][k]));
-
- if (! message[j][k].isEmpty()) {
- QString tmp = message[j][k];
- tmp.replace("\n", "\\n");
- tmp.replace("\"", "\\"");
- htmlCode += QString(" (...)").arg(tmp);
- }
-
- htmlCode += " | ";
- htmlCode += "";
-
- if (timeUsed[j][k] != -1) {
- htmlCode += QString("").asprintf("%.3lf s", double(timeUsed[j][k]) / 1000);
- } else {
- htmlCode += tr("Invalid");
- }
-
- htmlCode += " | ";
- htmlCode += "";
-
- if (memoryUsed[j][k] != -1) {
- htmlCode += QString("").asprintf("%.3lf MiB", double(memoryUsed[j][k]) / 1024 / 1024);
- } else {
- htmlCode += tr("Invalid");
- }
-
- htmlCode += " | ";
-
- if (k == 0) {
- int minv = 2147483647;
- int maxv = testCases[j]->getFullScore();
-
- for (int t = 0; t < inputFiles[j].size(); t++)
- if (score[j][t] < minv)
- minv = score[j][t];
-
- QString bgClass = "zero-score";
-
- if (minv >= maxv)
- bgClass = "full-score";
- else if (minv > 0)
- bgClass = "partial-score";
-
- htmlCode += QString(R"(%3 / %4 | )")
- .arg(inputFiles[j].size())
- .arg(bgClass)
- .arg(minv)
- .arg(maxv);
- }
-
- htmlCode += "
";
- }
- }
-
- htmlCode += "
";
- }
-
- htmlCode += QString("%1
").arg(tr("Return to top"));
- return htmlCode;
-}
-/*
- * Generate the HTML code for the summary page
- * Might be difficult to maintain
- * Use Javascript to shrink the filesize
- */
-void ExportUtil::exportHtml(QWidget *widget, Contest *contest, const QString &fileName) {
+QJsonObject ExportUtil::buildExportJson(Contest *contest) {
Settings settings;
contest->copySettings(settings);
ColorTheme colors = settings.getCurrentColorTheme();
- QFile file(fileName);
-
- if (! file.open(QFile::WriteOnly)) {
- QMessageBox::warning(widget, tr("LemonLime"),
- tr("Cannot open file %1").arg(QFileInfo(file).fileName()), QMessageBox::Ok);
- return;
- }
-
- QApplication::setOverrideCursor(Qt::WaitCursor);
- QTextStream out(&file);
QList contestantList = contest->getContestantList();
QList taskList = contest->getTaskList();
- out << "";
- out << R"()";
-
- // Style sheet
- out << "";
+ int sfullScore = contest->getTotalScore();
- out << "" << contest->getContestTitle() << " : " << tr("Contest Result") << "";
- out << "";
+ // Sort and rank
QList> sortList;
for (auto &i : contestantList) {
int totalScore = i->getTotalScore();
if (totalScore != -1) {
- sortList.append(std::make_pair(-totalScore, i->getContestantName()));
+ sortList.append(std::make_pair(totalScore, i->getContestantName()));
} else {
- sortList.append(std::make_pair(1, i->getContestantName()));
+ sortList.append(std::make_pair(-1, i->getContestantName()));
}
}
- std::sort(sortList.begin(), sortList.end());
+ std::sort(sortList.begin(), sortList.end(), std::greater<>());
QMap rankList;
for (int i = 0; i < sortList.size(); i++) {
@@ -279,77 +65,82 @@ void ExportUtil::exportHtml(QWidget *widget, Contest *contest, const QString &fi
}
}
- QHash loc;
-
- for (int i = 0; i < contestantList.size(); i++) {
- loc.insert(contestantList[i], i);
+ // Top-level fields
+ QJsonObject root;
+ root["name"] = contest->getContestTitle();
+ root["version"] = QString("Lemonlime Version %1:%2").arg(LEMON_VERSION_STRING).arg(LEMON_VERSION_BUILD);
+
+ // i18n
+ QJsonObject i18n;
+ i18n["rank_list"] = tr("Rank List");
+ i18n["hint"] = tr("Click names or task scores to jump to details. Judged By LemonLime");
+ i18n["rank"] = tr("Rank");
+ i18n["name"] = tr("Name");
+ i18n["total"] = tr("Total Score");
+ i18n["contestant"] = tr("Contestant");
+ i18n["task"] = tr("Task");
+ i18n["source_file"] = tr("Source file: ");
+ i18n["no_source"] = tr("Cannot find valid source file");
+ i18n["testcase"] = tr("Test Case");
+ i18n["input"] = tr("Input File");
+ i18n["result"] = tr("Result");
+ i18n["time"] = tr("Time Used");
+ i18n["memory"] = tr("Memory Used");
+ i18n["score"] = tr("Score");
+ i18n["return_to_top"] = tr("Return to top");
+ root["i18n"] = i18n;
+
+ // task_names
+ QJsonArray taskNames;
+
+ for (auto &task : taskList) {
+ taskNames.append(task->getProblemTitle());
}
- out << "";
- out << "" << contest->getContestTitle() << " : " << tr("Rank List") << "
";
- out << "" << tr("Click names or task scores to jump to details. Judged By LemonLime") << "
";
- out << R"()";
- out << QString(R"(| %1 | )").arg(tr("Rank"));
- out << QString(R"(%1 | )").arg(tr("Name"));
- out << QString(R"(%1 | )").arg(tr("Total Score"));
+ root["task_names"] = taskNames;
- for (auto &i : taskList)
- out << QString(R"(%1 | )").arg(i->getProblemTitle());
+ // contestants
+ QJsonArray contestantsArr;
- out << "
";
- QList fullScore;
- int sfullScore = contest->getTotalScore();
-
- for (auto &i : taskList) {
- fullScore.append(i->getTotalScore());
- }
+ for (int idx = 0; idx < contestantList.size(); idx++) {
+ Contestant *contestant = contestantList[idx];
+ QJsonObject cObj;
+ cObj["name"] = contestant->getContestantName();
+ cObj["rank"] = rankList[contestant->getContestantName()] + 1;
- for (auto &i : sortList) {
- Contestant *contestant = contest->getContestant(i.second);
- out << "";
- out << QString("| %1 | ").arg(rankList[contestant->getContestantName()] + 1);
- out << QString(R"(%2 | )")
- .arg(loc[contestant])
- .arg(i.second);
int allScore = contestant->getTotalScore();
if (allScore >= 0) {
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ cObj["total_score"] = allScore;
+
float h = NAN;
float s = NAN;
float l = NAN;
-#else
- double h = NAN;
- double s = NAN;
- double l = NAN;
-#endif
+
colors.getColorGrand(allScore, sfullScore).getHslF(&h, &s, &l);
h *= 360, s *= 100, l *= 100;
- out << QString("%1 | ")
- .arg(allScore)
- .arg(h)
- .arg(s)
- .arg(l)
- .arg(qMax(l - 20, 0.00));
+ cObj["total_bg"] = QString("%1, %2%, %3%").arg(h).arg(s).arg(l);
+ cObj["total_border"] = QString("%1, %2%, %3%").arg(h).arg(s).arg(qMax(l - 20, 0.00));
} else {
- out << QString("%1 | ").arg(tr("Invalid"));
+ cObj["total_score"] = 0;
+ cObj["total_bg"] = QString("0, 0%, 90%");
+ cObj["total_border"] = QString("0, 0%, 70%");
}
+ QJsonArray tasksArr;
+
for (int j = 0; j < taskList.size(); j++) {
+ QJsonObject tObj;
int score = contestant->getTaskScore(j);
- if (score != -1) {
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ if (score >= 0) {
+ tObj["score"] = score;
+
float h = NAN;
float s = NAN;
float l = NAN;
-#else
- double h = NAN;
- double s = NAN;
- double l = NAN;
-#endif
- QColor col = colors.getColorPer(score, fullScore[j]);
+
+ QColor col = colors.getColorPer(score, taskList[j]->getTotalScore());
col.getHslF(&h, &s, &l);
if (taskList[j]->getTaskType() != Task::AnswersOnly &&
@@ -362,216 +153,119 @@ void ExportUtil::exportHtml(QWidget *widget, Contest *contest, const QString &fi
}
h *= 360, s *= 100, l *= 100;
- out << QString(
- R"(%1 | )")
- .arg(score)
- .arg(h)
- .arg(s)
- .arg(l)
- .arg(loc[contestant])
- .arg(j);
+ tObj["bg"] = QString("%1, %2%, %3%").arg(h).arg(s).arg(l);
} else {
- out << QString(R"(%1 | )")
- .arg(tr("Invalid"))
- .arg(loc[contestant])
- .arg(j);
+ tObj["score"] = 0;
+ tObj["bg"] = QString("0, 0%, 90%");
}
- }
- out << "
";
- }
-
- out << "
";
-
- for (int i = 0; i < contestantList.size(); i++) {
- out << QString("
").arg(i) << "";
- out << tr("Contestant: %1").arg(contestantList[i]->getContestantName()) << "";
- out << getContestantHtmlCode(contest, contestantList[i], i);
- }
-
- out << QString(R"()")
- .arg(LEMON_VERSION_STRING)
- .arg(LEMON_VERSION_BUILD);
-
- out << R"(
-
- )";
- out << "";
- out << "";
- QApplication::restoreOverrideCursor();
- QMessageBox::information(widget, tr("LemonLime"), tr("Export is done"), QMessageBox::Ok);
-}
-
-auto ExportUtil::getSmallerContestantHtmlCode(Contest *contest, Contestant *contestant) -> QString {
- QString htmlCode;
- QList taskList = contest->getTaskList();
-
- for (int i = 0; i < taskList.size(); i++) {
- htmlCode += "";
- htmlCode += QString("%1 %2
").arg(tr("Task")).arg(taskList[i]->getProblemTitle());
-
- if (! contestant->getCheckJudged(i)) {
- htmlCode += QString(" %1
").arg(tr("Not judged"));
- continue;
- }
-
- if (taskList[i]->getTaskType() == Task::Traditional ||
- taskList[i]->getTaskType() == Task::Interaction ||
- taskList[i]->getTaskType() == Task::Communication ||
- taskList[i]->getTaskType() == Task::CommunicationExec) {
- if (contestant->getCompileState(i) != CompileSuccessfully) {
- switch (contestant->getCompileState(i)) {
- case NoValidGraderFile:
- htmlCode += QString(" %1")
- .arg(tr("Main grader (grader.*) cannot be found"));
- break;
-
- case NoValidSourceFile:
- htmlCode += QString(" %1").arg(tr("Cannot find valid source file"));
- break;
-
- case CompileTimeLimitExceeded:
- htmlCode += QString(" %1%2
")
- .arg(tr("Source file: "))
- .arg(contestant->getSourceFile(i));
- htmlCode += QString(" %1").arg(tr("Compile time limit exceeded"));
- break;
-
- case InvalidCompiler:
- htmlCode += QString(" %1").arg(tr("Cannot run given compiler"));
- break;
-
- case CompileError:
- htmlCode += QString(" %1%2
")
- .arg(tr("Source file: "))
- .arg(contestant->getSourceFile(i));
- htmlCode += QString(" %1").arg(tr("Compile error"));
-
- if (! contestant->getCompileMessage(i).isEmpty()) {
- QString compileMessage = contestant->getCompileMessage(i);
- compileMessage.replace("\r\n", "
");
- compileMessage.replace("\n", "
");
- compileMessage.replace("\r", "
");
-
- if (compileMessage.endsWith("
"))
- compileMessage.chop(4);
-
- htmlCode += R"()";
- htmlCode += "";
- htmlCode += compileMessage;
- htmlCode += " |
";
- }
-
- htmlCode += "";
- break;
- default:
- break;
+ // source file
+ if (taskList[j]->getTaskType() == Task::Traditional ||
+ taskList[j]->getTaskType() == Task::Interaction ||
+ taskList[j]->getTaskType() == Task::Communication ||
+ taskList[j]->getTaskType() == Task::CommunicationExec) {
+ if (contestant->getCheckJudged(j) && contestant->getCompileState(j) == CompileSuccessfully) {
+ tObj["file"] = contestant->getSourceFile(j);
}
-
- continue;
}
- htmlCode +=
- QString(" %1%2").arg(tr("Source file: ")).arg(contestant->getSourceFile(i));
- }
+ // details
+ bool isAnswersOnly = taskList[j]->getTaskType() == Task::AnswersOnly;
+ bool canShowDetails = contestant->getCheckJudged(j) &&
+ (isAnswersOnly || contestant->getCompileState(j) == CompileSuccessfully);
+
+ if (canShowDetails) {
+ QList testCases = taskList[j]->getTestCaseList();
+ QList inputFiles = contestant->getInputFiles(j);
+ QList> result = contestant->getResult(j);
+ QList message = contestant->getMessage(j);
+ QList> timeUsed = contestant->getTimeUsed(j);
+ QList> memoryUsed = contestant->getMemoryUsed(j);
+ QList> score = contestant->getScore(j);
+
+ QJsonArray detailsArr;
+
+ for (int jj = 0; jj < inputFiles.size(); jj++) {
+ for (int k = 0; k < inputFiles[jj].size(); k++) {
+ QJsonObject dObj;
+
+ if (k == 0) {
+ QString label = QString("#%1").arg(jj + 1);
+
+ if (score[jj].size() != inputFiles[jj].size()) {
+ label += QString("
%1:%2")
+ .arg(tr("Subtask Dependence Status"))
+ .arg(statusRankingText(score[jj].back()));
+ }
+
+ dObj["label"] = label;
+ dObj["row_span"] = inputFiles[jj].size();
+ } else {
+ dObj["label"] = QString("");
+ dObj["row_span"] = 0;
+ }
- htmlCode += R"()";
- htmlCode += QString("| %1 | ").arg(tr("Test Case"));
- htmlCode += QString("%1 | ").arg(tr("Input File"));
- htmlCode += QString("%1 | ").arg(tr("Result"));
- htmlCode += QString("%1 | ").arg(tr("Time Used"));
- htmlCode += QString("%1 | ").arg(tr("Memory Used"));
- htmlCode += QString("%1 |
").arg(tr("Score"));
- QList testCases = taskList[i]->getTestCaseList();
- QList inputFiles = contestant->getInputFiles(i);
- QList> result = contestant->getResult(i);
- QList message = contestant->getMessage(i);
- QList> timeUsed = contestant->getTimeUsed(i);
- QList> memoryUsed = contestant->getMemoryUsed(i);
- QList> score = contestant->getScore(i);
-
- for (int j = 0; j < inputFiles.size(); j++) {
- for (int k = 0; k < inputFiles[j].size(); k++) {
- htmlCode += "";
-
- if (k == 0) {
- if (score[j].size() == inputFiles[j].size())
- htmlCode +=
- QString("| #%2 | ").arg(inputFiles[j].size()).arg(j + 1);
- else
- htmlCode += QString("#%2 %3:%4 | ")
- .arg(inputFiles[j].size())
- .arg(j + 1)
- .arg(tr("Subtask Dependence Status"))
- .arg(statusRankingText(score[j].back()));
- }
+ dObj["input"] = inputFiles[jj][k];
- htmlCode += QString("%1 | ").arg(inputFiles[j][k]);
- QString text;
- QString bgColor;
- QString frColor;
- Settings::setTextAndColor(result[j][k], text, frColor, bgColor);
- htmlCode += QString("%1").arg(text);
-
- if (! message[j][k].isEmpty()) {
- QString tmp = message[j][k];
- tmp.replace("\n", "\\n");
- tmp.replace("\"", "\\"");
- htmlCode += QString(" (...)").arg(tmp);
- }
+ QString text;
+ QString frColor;
+ QString bgColor;
+ Settings::setTextAndColor(result[jj][k], text, frColor, bgColor);
+ dObj["result"] = text;
- htmlCode += " | ";
- htmlCode += "";
+ dObj["bg"] = bgColor;
- if (timeUsed[j][k] != -1) {
- htmlCode += QString("").asprintf("%.3lf s", double(timeUsed[j][k]) / 1000);
- } else {
- htmlCode += tr("Invalid");
- }
+ if (timeUsed[jj][k] != -1) {
+ dObj["time"] = QString("").asprintf("%.3lf s", double(timeUsed[jj][k]) / 1000);
+ } else {
+ dObj["time"] = tr("Invalid");
+ }
- htmlCode += " | ";
- htmlCode += "";
+ if (memoryUsed[jj][k] != -1) {
+ dObj["memory"] =
+ QString("").asprintf("%.3lf MB", double(memoryUsed[jj][k]) / 1024 / 1024);
+ } else {
+ dObj["memory"] = tr("Invalid");
+ }
- if (memoryUsed[j][k] != -1) {
- htmlCode += QString("").asprintf("%.3lf MiB", double(memoryUsed[j][k]) / 1024 / 1024);
- } else {
- htmlCode += tr("Invalid");
- }
+ if (k == 0) {
+ int minv = 2147483647;
+ int maxv = testCases[jj]->getFullScore();
- htmlCode += " | ";
+ for (int t = 0; t < inputFiles[jj].size(); t++)
+ if (score[jj][t] < minv)
+ minv = score[jj][t];
- if (k == 0) {
- int minv = 2147483647;
- int maxv = testCases[j]->getFullScore();
+ dObj["score"] = minv;
+ dObj["full_score"] = maxv;
+ } else {
+ dObj["score"] = 0;
+ dObj["full_score"] = 0;
+ }
- for (int t = 0; t < inputFiles[j].size(); t++)
- if (score[j][t] < minv)
- minv = score[j][t];
+ if (! message[jj][k].isEmpty()) {
+ dObj["info"] = message[jj][k];
+ }
- htmlCode += QString(R"(%2 / %3 | )")
- .arg(inputFiles[j].size())
- .arg(minv)
- .arg(maxv);
+ detailsArr.append(dObj);
+ }
}
- htmlCode += "
";
+ tObj["details"] = detailsArr;
}
+
+ tasksArr.append(tObj);
}
- htmlCode += "
";
+ cObj["tasks"] = tasksArr;
+ contestantsArr.append(cObj);
}
- htmlCode += QString("%1
").arg(tr("Return to top"));
- return htmlCode;
+ root["contestants"] = contestantsArr;
+ return root;
}
-void ExportUtil::exportSmallerHtml(QWidget *widget, Contest *contest, const QString &fileName) {
+void ExportUtil::exportHtml(QWidget *widget, Contest *contest, const QString &fileName) {
QFile file(fileName);
if (! file.open(QFile::WriteOnly)) {
@@ -581,99 +275,33 @@ void ExportUtil::exportSmallerHtml(QWidget *widget, Contest *contest, const QStr
}
QApplication::setOverrideCursor(Qt::WaitCursor);
- QTextStream out(&file);
- QList contestantList = contest->getContestantList();
- QList taskList = contest->getTaskList();
- out << "";
- out << R"()";
- out << "";
- out << "" << contest->getContestTitle() << " : " << tr("Contest Result") << "";
- out << "";
- QList> sortList;
- for (auto &i : contestantList) {
- int totalScore = i->getTotalScore();
-
- if (totalScore != -1) {
- sortList.append(std::make_pair(-totalScore, i->getContestantName()));
- } else {
- sortList.append(std::make_pair(1, i->getContestantName()));
- }
- }
+ // Read template from Qt resource
+ QFile templateFile(":/export/export_template.html");
- std::sort(sortList.begin(), sortList.end());
- QMap rankList;
-
- for (int i = 0; i < sortList.size(); i++) {
- if (i > 0 && sortList[i].first == sortList[i - 1].first) {
- rankList.insert(sortList[i].second, rankList[sortList[i - 1].second]);
- } else {
- rankList.insert(sortList[i].second, i);
- }
- }
-
- QHash loc;
-
- for (int i = 0; i < contestantList.size(); i++) {
- loc.insert(contestantList[i], i);
- }
-
- out << "";
- out << "" << contest->getContestTitle() << " : " << tr("Rank List") << "
";
- out << "" << tr("Judged By LemonLime") << "
";
- out << R"()";
- out << QString("| %1 | ").arg(tr("Rank"));
- out << QString("%1 | ").arg(tr("Name"));
- out << QString("%1 | ").arg(tr("Total Score"));
-
- for (auto &i : taskList)
- out << QString("%1 | ").arg(i->getProblemTitle());
-
- out << QString("
");
- QList fullScore;
-
- for (auto &i : taskList) {
- int a = i->getTotalScore();
- fullScore.append(a);
+ if (! templateFile.open(QFile::ReadOnly | QFile::Text)) {
+ QApplication::restoreOverrideCursor();
+ QMessageBox::warning(widget, tr("LemonLime"), tr("Cannot read export template"), QMessageBox::Ok);
+ return;
}
- for (auto &i : sortList) {
- Contestant *contestant = contest->getContestant(i.second);
- out << QString("| %1 | ").arg(rankList[contestant->getContestantName()] + 1);
- out << QString("%2 | ").arg(loc[contestant]).arg(i.second);
- int allScore = contestant->getTotalScore();
+ QString htmlTemplate = templateFile.readAll();
+ templateFile.close();
- if (allScore != -1) {
- out << QString("%1 | ").arg(allScore);
- } else {
- out << QString("%1 | ").arg(tr("Invalid"));
- }
+ // Build JSON data
+ QJsonObject jsonData = buildExportJson(contest);
+ QJsonDocument doc(jsonData);
+ QString jsonStr = doc.toJson(QJsonDocument::Compact);
- for (int j = 0; j < taskList.size(); j++) {
- int score = contestant->getTaskScore(j);
-
- if (score != -1) {
- out << QString("%1 | ").arg(score);
- } else {
- out << QString("%1 | ").arg(tr("Invalid"));
- }
- }
- }
+ // Replace placeholder with actual data
+ htmlTemplate.replace("%%DATA%%", jsonStr);
- out << "
";
-
- for (int i = 0; i < contestantList.size(); i++) {
- out << QString("
").arg(i) << "";
- out << tr("Contestant: %1").arg(contestantList[i]->getContestantName()) << "";
- out << getSmallerContestantHtmlCode(contest, contestantList[i]);
- }
+ // Write output
+ QTextStream out(&file);
+ out << htmlTemplate;
+ out.flush();
+ file.close();
- out << "";
QApplication::restoreOverrideCursor();
QMessageBox::information(widget, tr("LemonLime"), tr("Export is done"), QMessageBox::Ok);
}
@@ -697,13 +325,13 @@ void ExportUtil::exportCsv(QWidget *widget, Contest *contest, const QString &fil
int totalScore = i->getTotalScore();
if (totalScore != -1) {
- sortList.append(std::make_pair(-totalScore, i->getContestantName()));
+ sortList.append(std::make_pair(totalScore, i->getContestantName()));
} else {
- sortList.append(std::make_pair(1, i->getContestantName()));
+ sortList.append(std::make_pair(-1, i->getContestantName()));
}
}
- std::sort(sortList.begin(), sortList.end());
+ std::sort(sortList.begin(), sortList.end(), std::greater<>());
QMap rankList;
for (int i = 0; i < sortList.size(); i++) {
@@ -714,12 +342,6 @@ void ExportUtil::exportCsv(QWidget *widget, Contest *contest, const QString &fil
}
}
- QHash loc;
-
- for (int i = 0; i < contestantList.size(); i++) {
- loc.insert(contestantList[i], i);
- }
-
out << "\"" << tr("Rank") << "\"" << "," << "\"" << tr("Name") << "\"" << ",";
for (auto &i : taskList) {
@@ -752,103 +374,12 @@ void ExportUtil::exportCsv(QWidget *widget, Contest *contest, const QString &fil
}
}
- QApplication::restoreOverrideCursor();
- QMessageBox::information(widget, tr("LemonLime"), tr("Export is done"), QMessageBox::Ok);
-}
-
-#ifdef ENABLE_XLS_EXPORT
-void ExportUtil::exportXls(QWidget *widget, Contest *contest, const QString &fileName) {
-
- if (QFile(fileName).exists()) {
- if (! QFile(fileName).remove()) {
- QMessageBox::warning(widget, tr("LemonLime"),
- tr("Cannot open file %1").arg(QFileInfo(fileName).fileName()),
- QMessageBox::Ok);
- return;
- }
- }
-
- QApplication::setOverrideCursor(Qt::WaitCursor);
- QList contestantList = contest->getContestantList();
- QList taskList = contest->getTaskList();
- QList> sortList;
-
- for (int i = 0; i < contestantList.size(); i++) {
- int totalScore = contestantList[i]->getTotalScore();
-
- if (totalScore != -1) {
- sortList.append(std::make_pair(-totalScore, contestantList[i]->getContestantName()));
- } else {
- sortList.append(std::make_pair(1, contestantList[i]->getContestantName()));
- }
- }
-
- std::sort(sortList.begin(), sortList.end());
- QMap rankList;
-
- for (int i = 0; i < sortList.size(); i++) {
- if (i > 0 && sortList[i].first == sortList[i - 1].first) {
- rankList.insert(sortList[i].second, rankList[sortList[i - 1].second]);
- } else {
- rankList.insert(sortList[i].second, i);
- }
- }
-
- QMap loc;
-
- for (int i = 0; i < contestantList.size(); i++) {
- loc.insert(contestantList[i], i);
- }
-
- QAxObject *excel = new QAxObject("Excel.Application", widget);
- QAxObject *workbook = excel->querySubObject("Workbooks")->querySubObject("Add");
- QAxObject *sheet = workbook->querySubObject("ActiveSheet");
- sheet->setProperty("Name", QDate::currentDate().toString("yyyy-MM-dd"));
- sheet->querySubObject("Cells(int, int)", 1, 1)->setProperty("Value", tr("Rank"));
- sheet->querySubObject("Cells(int, int)", 1, 2)->setProperty("Value", tr("Name"));
-
- for (int i = 0; i < taskList.size(); i++)
- sheet->querySubObject("Cells(int, int)", 1, 3 + i)
- ->setProperty("Value", taskList[i]->getProblemTitle());
-
- sheet->querySubObject("Cells(int, int)", 1, 3 + taskList.size())->setProperty("Value", tr("Total Score"));
-
- for (int i = 0; i < taskList.size() + 3; i++)
- sheet->querySubObject("Cells(int, int)", 1, i + 1)->querySubObject("Font")->setProperty("Bold", true);
+ out.flush();
+ file.close();
- for (int i = 0; i < sortList.size(); i++) {
- Contestant *contestant = contest->getContestant(sortList[i].second);
- sheet->querySubObject("Cells(int, int)", 2 + i, 1)
- ->setProperty("Value", rankList[contestant->getContestantName()] + 1);
- sheet->querySubObject("Cells(int, int)", 2 + i, 2)->setProperty("Value", sortList[i].second);
-
- for (int j = 0; j < taskList.size(); j++) {
- int score = contestant->getTaskScore(j);
-
- if (score != -1) {
- sheet->querySubObject("Cells(int, int)", 2 + i, 3 + j)->setProperty("Value", score);
- } else {
- sheet->querySubObject("Cells(int, int)", 2 + i, 3 + j)->setProperty("Value", tr("Invalid"));
- }
- }
-
- int score = contestant->getTotalScore();
-
- if (score != -1) {
- sheet->querySubObject("Cells(int, int)", 2 + i, 3 + taskList.size())->setProperty("Value", score);
- } else {
- sheet->querySubObject("Cells(int, int)", 2 + i, 3 + taskList.size())
- ->setProperty("Value", tr("Invalid"));
- }
- }
-
- workbook->dynamicCall("SaveAs(const QString&, int)", QDir::toNativeSeparators(fileName), -4143);
- excel->dynamicCall("Quit()");
- delete excel;
QApplication::restoreOverrideCursor();
QMessageBox::information(widget, tr("LemonLime"), tr("Export is done"), QMessageBox::Ok);
}
-#endif
void ExportUtil::exportResult(QWidget *widget, Contest *contest) {
QList contestantList = contest->getContestantList();
@@ -865,31 +396,16 @@ void ExportUtil::exportResult(QWidget *widget, Contest *contest) {
return;
}
- QString filter = tr("HTML Document (*.html *.htm);;CSV (*.csv)");
-#ifdef ENABLE_XLS_EXPORT
- QAxObject *excel = new QAxObject("Excel.Application", widget);
-
- if (! excel->isNull())
- filter = filter + tr(";;Excel Workbook (*.xls)");
-
- delete excel;
-#endif
+ QString filter = tr("HTML Document (*.html);;CSV (*.csv)");
QString fileName = QFileDialog::getSaveFileName(
widget, tr("Export Result"), QDir::currentPath() + QDir::separator() + "result.html", filter);
if (fileName.isEmpty())
return;
- // TODO: refactor
+
if (QFileInfo(fileName).suffix() == "html")
exportHtml(widget, contest, fileName);
- if (QFileInfo(fileName).suffix() == "htm")
- exportSmallerHtml(widget, contest, fileName);
-
if (QFileInfo(fileName).suffix() == "csv")
exportCsv(widget, contest, fileName);
-#ifdef ENABLE_XLS_EXPORT
- if (QFileInfo(fileName).suffix() == "xls")
- exportXls(widget, contest, fileName);
-#endif
}
diff --git a/src/component/exportutil/exportutil.h b/src/component/exportutil/exportutil.h
index e112b077..3393c1e2 100644
--- a/src/component/exportutil/exportutil.h
+++ b/src/component/exportutil/exportutil.h
@@ -11,12 +11,9 @@
//
#include "base/LemonType.hpp"
+#include
#include
-#ifdef ENABLE_XLS_EXPORT
-#include
-#endif
-
class Contest;
class Contestant;
@@ -27,14 +24,9 @@ class ExportUtil : public QObject {
static void exportResult(QWidget *, Contest *);
private:
- static QString getContestantHtmlCode(Contest *, Contestant *, int);
- static QString getSmallerContestantHtmlCode(Contest *, Contestant *);
+ static QJsonObject buildExportJson(Contest *);
static void exportHtml(QWidget *, Contest *, const QString &);
- static void exportSmallerHtml(QWidget *, Contest *, const QString &);
static void exportCsv(QWidget *, Contest *, const QString &);
-#ifdef ENABLE_XLS_EXPORT
- static void exportXls(QWidget *, Contest *, const QString &);
-#endif
signals:
public slots: