实验成绩导出
This commit is contained in:
1 parent
bcdb00ee2e
commit
4446cfbd30
3 files changed
+194
No files matched your search
@@ -190,6 +190,84 @@ public sealed class ExperimentGradesController(
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("teaching-tasks/{teachingTaskId:guid}/export.xlsx")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<IActionResult> ExportTeachingTaskScores(
|
||||
Guid teachingTaskId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sheets = await ScopedSheets()
|
||||
.Where(sheet =>
|
||||
sheet.ExperimentProject!.TeachingTaskId == teachingTaskId)
|
||||
.Include(sheet => sheet.Records)
|
||||
.ThenInclude(record => record.Student)
|
||||
.ThenInclude(student => student!.AdministrativeClass)
|
||||
.Include(sheet => sheet.ExperimentProject)
|
||||
.ThenInclude(project => project!.TeachingTask)
|
||||
.ThenInclude(task => task!.AcademicTerm)
|
||||
.Include(sheet => sheet.ExperimentProject)
|
||||
.ThenInclude(project => project!.TeachingTask)
|
||||
.ThenInclude(task => task!.Course)
|
||||
.ThenInclude(course => course!.College)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.OrderBy(sheet => sheet.ExperimentProject!.Code)
|
||||
.ToListAsync(cancellationToken);
|
||||
if (sheets.Count == 0)
|
||||
return NotFound();
|
||||
|
||||
var task = sheets[0].ExperimentProject!.TeachingTask!;
|
||||
var course = task.Course!;
|
||||
var students = sheets
|
||||
.SelectMany(sheet => sheet.Records)
|
||||
.Select(record => record.Student!)
|
||||
.GroupBy(student => student.Id)
|
||||
.Select(group => group.First())
|
||||
.OrderBy(student => student.StudentNumber)
|
||||
.ToList();
|
||||
var aggregate = await ExperimentGradeAggregationService.CalculateAsync(
|
||||
db,
|
||||
teachingTaskId,
|
||||
students.Select(student => student.Id).ToArray(),
|
||||
cancellationToken);
|
||||
var headers = new List<string>
|
||||
{
|
||||
"学期", "开课学院", "课程代码", "课程名称", "教学班",
|
||||
"学号", "姓名", "行政班"
|
||||
};
|
||||
headers.AddRange(sheets.Select(sheet =>
|
||||
$"{sheet.ExperimentProject!.Code}·{sheet.ExperimentProject.Name}" +
|
||||
$"({SheetStatusLabel(sheet.Status)})"));
|
||||
headers.Add("实验合成总分(仅已发布项目)");
|
||||
|
||||
var rows = students.Select(student =>
|
||||
{
|
||||
var values = new List<object?>
|
||||
{
|
||||
task.AcademicTerm!.Name,
|
||||
course.College!.Name,
|
||||
course.Code,
|
||||
course.Name,
|
||||
task.TaskNumber,
|
||||
student.StudentNumber,
|
||||
student.Name,
|
||||
student.AdministrativeClass!.Name
|
||||
};
|
||||
values.AddRange(sheets.Select(sheet => (object?)sheet.Records
|
||||
.FirstOrDefault(record => record.StudentId == student.Id)
|
||||
?.TotalScore));
|
||||
values.Add(aggregate.Scores.GetValueOrDefault(student.Id));
|
||||
return (IReadOnlyList<object?>)values;
|
||||
}).ToList();
|
||||
var bytes = ExcelWorkbookHelper.Create("课程实验成绩", headers, rows);
|
||||
return File(bytes, ExcelWorkbookHelper.ContentType,
|
||||
DownloadFileName.Export(
|
||||
"课程实验成绩",
|
||||
".xlsx",
|
||||
course.Code,
|
||||
task.TaskNumber));
|
||||
}
|
||||
|
||||
[HttpGet("mine")]
|
||||
[Authorize(Roles = SystemRoles.Student)]
|
||||
public async Task<ActionResult> GetMine(CancellationToken cancellationToken)
|
||||
@@ -1300,6 +1378,17 @@ public sealed class ExperimentGradesController(
|
||||
_ => "待登记"
|
||||
};
|
||||
|
||||
private static string SheetStatusLabel(ExperimentGradeSheetStatus status) =>
|
||||
status switch
|
||||
{
|
||||
ExperimentGradeSheetStatus.Draft => "录入中",
|
||||
ExperimentGradeSheetStatus.Submitted => "待审核",
|
||||
ExperimentGradeSheetStatus.Approved => "审核通过",
|
||||
ExperimentGradeSheetStatus.Published => "已发布",
|
||||
ExperimentGradeSheetStatus.Returned => "已退回",
|
||||
_ => status.ToString()
|
||||
};
|
||||
|
||||
private static bool TryParseParticipationStatus(
|
||||
ExcelRow row,
|
||||
out ExperimentParticipationStatus status,
|
||||
|
||||
@@ -14,6 +14,92 @@ namespace Jiaowu.Api.Tests;
|
||||
|
||||
public sealed class ExperimentGradesControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExportTeachingTaskScores_IncludesEveryProjectAndPublishedTotal()
|
||||
{
|
||||
await using var fixture = await ExperimentGradeFixture.CreateAsync();
|
||||
var firstProject = new ExperimentProject
|
||||
{
|
||||
TeachingTaskId = fixture.Task.Id,
|
||||
Code = "LAB-EXPORT-1",
|
||||
Name = "实验一",
|
||||
ArrangementMode = ExperimentArrangementMode.Centralized,
|
||||
StartDate = fixture.Term.StartDate,
|
||||
EndDate = fixture.Term.EndDate,
|
||||
Status = ExperimentProjectStatus.Published,
|
||||
PublishedAt = DateTime.UtcNow
|
||||
};
|
||||
var secondProject = new ExperimentProject
|
||||
{
|
||||
TeachingTaskId = fixture.Task.Id,
|
||||
Code = "LAB-EXPORT-2",
|
||||
Name = "实验二",
|
||||
ArrangementMode = ExperimentArrangementMode.Centralized,
|
||||
StartDate = fixture.Term.StartDate,
|
||||
EndDate = fixture.Term.EndDate,
|
||||
Status = ExperimentProjectStatus.Published,
|
||||
PublishedAt = DateTime.UtcNow
|
||||
};
|
||||
fixture.Db.ExperimentProjects.AddRange(firstProject, secondProject);
|
||||
await fixture.Db.SaveChangesAsync();
|
||||
fixture.Db.ExperimentGradeSheets.AddRange(
|
||||
new ExperimentGradeSheet
|
||||
{
|
||||
ExperimentProjectId = firstProject.Id,
|
||||
ContributionWeight = 1,
|
||||
PassScore = 60,
|
||||
Status = ExperimentGradeSheetStatus.Published,
|
||||
PublishedAt = DateTime.UtcNow,
|
||||
Records =
|
||||
[
|
||||
new ExperimentGradeRecord
|
||||
{
|
||||
StudentId = fixture.Student.Id,
|
||||
ParticipationStatus = ExperimentParticipationStatus.Completed,
|
||||
TotalScore = 80,
|
||||
IsPassed = true
|
||||
}
|
||||
]
|
||||
},
|
||||
new ExperimentGradeSheet
|
||||
{
|
||||
ExperimentProjectId = secondProject.Id,
|
||||
ContributionWeight = 3,
|
||||
PassScore = 60,
|
||||
Status = ExperimentGradeSheetStatus.Published,
|
||||
PublishedAt = DateTime.UtcNow,
|
||||
Records =
|
||||
[
|
||||
new ExperimentGradeRecord
|
||||
{
|
||||
StudentId = fixture.Student.Id,
|
||||
ParticipationStatus = ExperimentParticipationStatus.Completed,
|
||||
TotalScore = 90,
|
||||
IsPassed = true
|
||||
}
|
||||
]
|
||||
});
|
||||
await fixture.Db.SaveChangesAsync();
|
||||
await ExperimentGradeAggregationService.RefreshTeachingTaskAsync(
|
||||
fixture.Db,
|
||||
fixture.Task.Id,
|
||||
CancellationToken.None);
|
||||
fixture.Db.ChangeTracker.Clear();
|
||||
|
||||
var file = Assert.IsType<FileContentResult>(await fixture
|
||||
.ExperimentGrades(fixture.AdminScope)
|
||||
.ExportTeachingTaskScores(fixture.Task.Id, CancellationToken.None));
|
||||
await using var stream = new MemoryStream(file.FileContents);
|
||||
using var workbook = new XLWorkbook(stream);
|
||||
var worksheet = workbook.Worksheet("课程实验成绩");
|
||||
Assert.Equal("LAB-EXPORT-1·实验一(已发布)", worksheet.Cell(1, 9).GetString());
|
||||
Assert.Equal("LAB-EXPORT-2·实验二(已发布)", worksheet.Cell(1, 10).GetString());
|
||||
Assert.Equal("实验合成总分(仅已发布项目)", worksheet.Cell(1, 11).GetString());
|
||||
Assert.Equal("80", worksheet.Cell(2, 9).GetFormattedString());
|
||||
Assert.Equal("90", worksheet.Cell(2, 10).GetFormattedString());
|
||||
Assert.Equal("87.5", worksheet.Cell(2, 11).GetFormattedString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateSettings_ReplacesItemsAndRecreatesScores()
|
||||
{
|
||||
|
||||
@@ -376,6 +376,18 @@ function downloadTemplate() {
|
||||
)
|
||||
}
|
||||
|
||||
async function exportCourseScores(course: any) {
|
||||
try {
|
||||
await downloadApiFile(
|
||||
`/experiment-grades/teaching-tasks/${course.teachingTaskId}/export.xlsx`,
|
||||
`${course.courseCode}-${course.taskNumber}-课程实验成绩.xlsx`,
|
||||
)
|
||||
ElMessage.success('课程实验成绩已导出')
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
function chooseImportFile() {
|
||||
importFileInput.value?.click()
|
||||
}
|
||||
@@ -650,6 +662,13 @@ onMounted(async () => {
|
||||
<b>{{ course.projectCount }}</b><span>个实验项目</span>
|
||||
<small v-if="course.sheetCount">{{ course.sheetCount }} 份成绩单 · {{ course.scoredCount }}/{{ course.studentCount }} 已计分</small>
|
||||
<small v-else>尚未建立成绩单</small>
|
||||
<el-button
|
||||
v-if="course.sheetCount"
|
||||
type="primary"
|
||||
text
|
||||
:icon="Download"
|
||||
@click="exportCourseScores(course)"
|
||||
>导出实验成绩</el-button>
|
||||
</div>
|
||||
</header>
|
||||
<el-table :data="course.projects" size="small" class="course-project-table">
|
||||
|
||||
Reference in new issue
Block a user