实验成绩导出
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")]
|
[HttpGet("mine")]
|
||||||
[Authorize(Roles = SystemRoles.Student)]
|
[Authorize(Roles = SystemRoles.Student)]
|
||||||
public async Task<ActionResult> GetMine(CancellationToken cancellationToken)
|
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(
|
private static bool TryParseParticipationStatus(
|
||||||
ExcelRow row,
|
ExcelRow row,
|
||||||
out ExperimentParticipationStatus status,
|
out ExperimentParticipationStatus status,
|
||||||
|
|||||||
@@ -14,6 +14,92 @@ namespace Jiaowu.Api.Tests;
|
|||||||
|
|
||||||
public sealed class ExperimentGradesControllerTests
|
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]
|
[Fact]
|
||||||
public async Task UpdateSettings_ReplacesItemsAndRecreatesScores()
|
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() {
|
function chooseImportFile() {
|
||||||
importFileInput.value?.click()
|
importFileInput.value?.click()
|
||||||
}
|
}
|
||||||
@@ -650,6 +662,13 @@ onMounted(async () => {
|
|||||||
<b>{{ course.projectCount }}</b><span>个实验项目</span>
|
<b>{{ course.projectCount }}</b><span>个实验项目</span>
|
||||||
<small v-if="course.sheetCount">{{ course.sheetCount }} 份成绩单 · {{ course.scoredCount }}/{{ course.studentCount }} 已计分</small>
|
<small v-if="course.sheetCount">{{ course.sheetCount }} 份成绩单 · {{ course.scoredCount }}/{{ course.studentCount }} 已计分</small>
|
||||||
<small v-else>尚未建立成绩单</small>
|
<small v-else>尚未建立成绩单</small>
|
||||||
|
<el-button
|
||||||
|
v-if="course.sheetCount"
|
||||||
|
type="primary"
|
||||||
|
text
|
||||||
|
:icon="Download"
|
||||||
|
@click="exportCourseScores(course)"
|
||||||
|
>导出实验成绩</el-button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<el-table :data="course.projects" size="small" class="course-project-table">
|
<el-table :data="course.projects" size="small" class="course-project-table">
|
||||||
|
|||||||
Reference in new issue
Block a user