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"()").arg(tr("Test Case")); - htmlCode += QString(R"()").arg(tr("Input File")); - htmlCode += QString(R"()").arg(tr("Result")); - htmlCode += QString(R"()").arg(tr("Time Used")); - htmlCode += QString(R"()").arg(tr("Memory Used")); - htmlCode += QString(R"()").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"()").arg(inputFiles[j].size()).arg(j + 1); - else - htmlCode += QString(R"()") - .arg(inputFiles[j].size()) - .arg(j + 1) - .arg(tr("Subtask Dependence Status")) - .arg(statusRankingText(score[j].back())); - } - - htmlCode += QString("").arg(inputFiles[j][k]); - QString text, bgColor, frColor; - Settings::setTextAndColor(result[j][k], text, frColor, bgColor); - htmlCode += - QString(""; - htmlCode += ""; - 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"()") - .arg(inputFiles[j].size()) - .arg(bgClass) - .arg(minv) - .arg(maxv); - } - - htmlCode += ""; - } - } - - htmlCode += "
%1%1%1%1%1%1
#%2#%2
%3:%4
%1%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 += ""; - - if (timeUsed[j][k] != -1) { - htmlCode += QString("").asprintf("%.3lf s", double(timeUsed[j][k]) / 1000); - } else { - htmlCode += tr("Invalid"); - } - - htmlCode += ""; - - if (memoryUsed[j][k] != -1) { - htmlCode += QString("").asprintf("%.3lf MiB", double(memoryUsed[j][k]) / 1024 / 1024); - } else { - htmlCode += tr("Invalid"); - } - - htmlCode += "%3 / %4

"; - } - - 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"()").arg(tr("Rank")); - out << QString(R"()").arg(tr("Name")); - out << QString(R"()").arg(tr("Total Score")); + root["task_names"] = taskNames; - for (auto &i : taskList) - out << QString(R"()").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("").arg(rankList[contestant->getContestantName()] + 1); - out << QString(R"()") - .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("") - .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("").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"()") - .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"()") - .arg(tr("Invalid")) - .arg(loc[contestant]) - .arg(j); + tObj["score"] = 0; + tObj["bg"] = QString("0, 0%, 90%"); } - } - out << ""; - } - - out << "
%1%1%1%1
%1%2%1%1%1%1

"; - - 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"(

Lemonlime Version %1:%2

)") - .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("").arg(tr("Test Case")); - htmlCode += QString("").arg(tr("Input File")); - htmlCode += QString("").arg(tr("Result")); - htmlCode += QString("").arg(tr("Time Used")); - htmlCode += QString("").arg(tr("Memory Used")); - htmlCode += QString("").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("").arg(inputFiles[j].size()).arg(j + 1); - else - htmlCode += QString("") - .arg(inputFiles[j].size()) - .arg(j + 1) - .arg(tr("Subtask Dependence Status")) - .arg(statusRankingText(score[j].back())); - } + dObj["input"] = inputFiles[jj][k]; - htmlCode += QString("").arg(inputFiles[j][k]); - QString text; - QString bgColor; - QString frColor; - Settings::setTextAndColor(result[j][k], text, frColor, bgColor); - htmlCode += QString(""; - htmlCode += ""; - 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"()") - .arg(inputFiles[j].size()) - .arg(minv) - .arg(maxv); + detailsArr.append(dObj); + } } - htmlCode += ""; + tObj["details"] = detailsArr; } + + tasksArr.append(tObj); } - htmlCode += "
%1%1%1%1%1%1
#%2#%2
%3:%4
%1%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 += ""; + 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 += ""; + 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 += "%2 / %3

"; + 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("").arg(tr("Rank")); - out << QString("").arg(tr("Name")); - out << QString("").arg(tr("Total Score")); - - for (auto &i : taskList) - out << QString("").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("").arg(rankList[contestant->getContestantName()] + 1); - out << QString("").arg(loc[contestant]).arg(i.second); - int allScore = contestant->getTotalScore(); + QString htmlTemplate = templateFile.readAll(); + templateFile.close(); - if (allScore != -1) { - out << QString("").arg(allScore); - } else { - out << QString("").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("").arg(score); - } else { - out << QString("").arg(tr("Invalid")); - } - } - } + // Replace placeholder with actual data + htmlTemplate.replace("%%DATA%%", jsonStr); - out << "
%1%1%1%1
%1%2%1%1%1%1

"; - - 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: