2 Commits
4 changed files with 113 additions and 45 deletions

No files matched your search

@@ -503,12 +503,32 @@ public sealed class ExperimentsController(
x.CourseId != first.CourseId)) x.CourseId != first.CourseId))
return ValidationProblem("批量设置仅支持同一学期、同一课程的教学任务。"); return ValidationProblem("批量设置仅支持同一学期、同一课程的教学任务。");
if (request.ArrangementMode == ExperimentArrangementMode.SelfScheduled &&
request.ProjectCount is < 1 or > 100)
return ValidationProblem("自行安排一次可生成 1 至 100 个实验项目。");
var code = request.Code.Trim(); var code = request.Code.Trim();
var names = NormalizeBatchProjectNames(request.Names);
var definitionCount = request.ArrangementMode == ExperimentArrangementMode.SelfScheduled
? request.ProjectCount
: 1;
if (request.ArrangementMode == ExperimentArrangementMode.SelfScheduled &&
names is not null && names.Count != definitionCount)
return ValidationProblem($"已填写 {names.Count} 个项目名称,请将生成项目数设为相同数量。");
if (request.ArrangementMode == ExperimentArrangementMode.SelfScheduled &&
taskIds.Count * definitionCount > 1000)
return ValidationProblem("本次最多生成 1000 个自行安排实验项目,请缩小教学任务或项目数量。");
var projectCodes = Enumerable.Range(1, definitionCount)
.Select(index => ProjectCode(code, index, definitionCount))
.ToList();
if (projectCodes.Any(item => item.Length > 40))
return ValidationProblem("项目编码前缀过长,无法追加顺序号。");
if (request.ArrangementMode == ExperimentArrangementMode.SelfScheduled) if (request.ArrangementMode == ExperimentArrangementMode.SelfScheduled)
{ {
var conflictingTaskNumbers = await db.ExperimentProjects.AsNoTracking() var conflictingTaskNumbers = await db.ExperimentProjects.AsNoTracking()
.WhereIn(taskIds, x => x.TeachingTaskId) .WhereIn(taskIds, x => x.TeachingTaskId)
.Where(x => x.Code == code) .Where(x => projectCodes.Contains(x.Code))
.Select(x => x.TeachingTask!.TaskNumber) .Select(x => x.TeachingTask!.TaskNumber)
.OrderBy(x => x) .OrderBy(x => x)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -565,7 +585,7 @@ public sealed class ExperimentsController(
return ConflictProblem("所选实验课中已存在相同实验项目编码和周次,不能重复生成。"); return ConflictProblem("所选实验课中已存在相同实验项目编码和周次,不能重复生成。");
} }
var projects = new List<ExperimentProject>(tasks.Count); var projects = new List<ExperimentProject>(tasks.Count * definitionCount);
foreach (var task in tasks) foreach (var task in tasks)
{ {
var item = request.ForTeachingTask(task.Id); var item = request.ForTeachingTask(task.Id);
@@ -594,43 +614,57 @@ public sealed class ExperimentsController(
continue; continue;
} }
} }
projects.Add(new ExperimentProject for (var definitionIndex = 1; definitionIndex <= definitionCount; definitionIndex++)
{ {
TeachingTaskId = task.Id, projects.Add(new ExperimentProject
ScheduleEntryId = occurrence.Entry?.Id, {
ScheduleWeek = occurrence.Week, TeachingTaskId = task.Id,
Code = code, ScheduleEntryId = occurrence.Entry?.Id,
Name = request.Name.Trim(), ScheduleWeek = occurrence.Week,
ArrangementMode = request.ArrangementMode, Code = ProjectCode(code, definitionIndex, definitionCount),
Description = Normalize(request.Description), Name = request.Name.Trim(),
Requirements = Normalize(request.Requirements), ArrangementMode = request.ArrangementMode,
StartDate = request.StartDate, Description = Normalize(request.Description),
EndDate = request.EndDate, Requirements = Normalize(request.Requirements),
SelectionStartsAt = NormalizeSelectionTime(request.SelectionStartsAt), StartDate = request.StartDate,
SelectionEndsAt = NormalizeSelectionTime(request.SelectionEndsAt) EndDate = request.EndDate,
}); SelectionStartsAt = NormalizeSelectionTime(request.SelectionStartsAt),
SelectionEndsAt = NormalizeSelectionTime(request.SelectionEndsAt)
});
}
} }
} }
var names = NormalizeBatchProjectNames(request.Names);
if (request.GenerateSequentialNames) if (request.GenerateSequentialNames)
{ {
if (names is not null) if (names is not null)
return ValidationProblem("自动生成顺序号时无需填写逐行项目名称。"); return ValidationProblem("自动生成顺序号时无需填写逐行项目名称。");
var width = Math.Max(2, projects.Count.ToString().Length); var width = Math.Max(2, (request.ArrangementMode == ExperimentArrangementMode.SelfScheduled
? definitionCount
: projects.Count).ToString().Length);
if (request.Name.Trim().Length + width + 1 > 120) if (request.Name.Trim().Length + width + 1 > 120)
return ValidationProblem("项目名称前缀过长,无法追加顺序号。"); return ValidationProblem("项目名称前缀过长,无法追加顺序号。");
for (var index = 0; index < projects.Count; index++) for (var index = 0; index < projects.Count; index++)
projects[index].Name = $"{request.Name.Trim()} {(index + 1).ToString($"D{width}")}"; {
var number = request.ArrangementMode == ExperimentArrangementMode.SelfScheduled
? index % definitionCount + 1
: index + 1;
projects[index].Name = $"{request.Name.Trim()} {number.ToString($"D{width}")}";
}
} }
else if (names is not null) else if (names is not null)
{ {
if (names.Any(name => string.IsNullOrWhiteSpace(name) || name.Length > 120)) if (names.Any(name => string.IsNullOrWhiteSpace(name) || name.Length > 120))
return ValidationProblem("每个实验项目名称均不能为空,且不能超过 120 个字符。"); return ValidationProblem("每个实验项目名称均不能为空,且不能超过 120 个字符。");
if (names.Count != projects.Count) var expectedNameCount = request.ArrangementMode == ExperimentArrangementMode.SelfScheduled
? definitionCount
: projects.Count;
if (names.Count != expectedNameCount)
return ValidationProblem($"已填写 {names.Count} 个项目名称,但本次将生成 {projects.Count} 个实验项目;请确保每行对应一个项目。"); return ValidationProblem($"已填写 {names.Count} 个项目名称,但本次将生成 {projects.Count} 个实验项目;请确保每行对应一个项目。");
for (var index = 0; index < projects.Count; index++) for (var index = 0; index < projects.Count; index++)
projects[index].Name = names[index]; projects[index].Name = names[request.ArrangementMode == ExperimentArrangementMode.SelfScheduled
? index % definitionCount
: index];
} }
db.ExperimentProjects.AddRange(projects); db.ExperimentProjects.AddRange(projects);
@@ -1370,9 +1404,7 @@ public sealed class ExperimentsController(
.FirstOrDefaultAsync(x => .FirstOrDefaultAsync(x =>
x.Id == request.ClassroomId && x.IsEnabled, x.Id == request.ClassroomId && x.IsEnabled,
cancellationToken); cancellationToken);
if (classroom is null) return "实验教室不存在或已停用。"; if (classroom is null) return "实验场地不存在或已停用。";
if (!TeachingVenueNatureRules.SupportsExperiment(classroom.TeachingVenueNature))
return "所选场地未标注实验教学性质。";
if (request.Capacity > classroom.Capacity) if (request.Capacity > classroom.Capacity)
return $"场次容量不能超过教室容量 {classroom.Capacity} 人。"; return $"场次容量不能超过教室容量 {classroom.Capacity} 人。";
if (project.ArrangementMode == if (project.ArrangementMode ==
@@ -1737,6 +1769,11 @@ public sealed class ExperimentsController(
return names.Select(name => name.Trim()).ToList(); return names.Select(name => name.Trim()).ToList();
} }
private static string ProjectCode(string prefix, int index, int count) =>
count == 1
? prefix
: $"{prefix}-{index.ToString($"D{Math.Max(2, count.ToString().Length)}")}";
private ActionResult ConflictProblem(string detail) => private ActionResult ConflictProblem(string detail) =>
Conflict(new ProblemDetails Conflict(new ProblemDetails
{ {
@@ -1776,7 +1813,8 @@ public sealed record ExperimentProjectBatchRequest(
IReadOnlyList<string>? Names = null, IReadOnlyList<string>? Names = null,
bool GenerateSequentialNames = false, bool GenerateSequentialNames = false,
DateTime? SelectionStartsAt = null, DateTime? SelectionStartsAt = null,
DateTime? SelectionEndsAt = null) DateTime? SelectionEndsAt = null,
[Range(1, 100)] int ProjectCount = 1)
{ {
public ExperimentProjectRequest ForTeachingTask(Guid teachingTaskId) => public ExperimentProjectRequest ForTeachingTask(Guid teachingTaskId) =>
new( new(
@@ -45,24 +45,28 @@ public sealed class ExperimentsControllerTests
var pasted = fixture.BatchProjectRequest(ExperimentArrangementMode.SelfScheduled) with var pasted = fixture.BatchProjectRequest(ExperimentArrangementMode.SelfScheduled) with
{ {
Names = ["实验一", "实验二"] Names = ["实验一", "实验二"],
ProjectCount = 2
}; };
Assert.IsType<CreatedResult>(await controller.CreateProjects(pasted, CancellationToken.None)); Assert.IsType<CreatedResult>(await controller.CreateProjects(pasted, CancellationToken.None));
Assert.Equal(["实验一", "实验二"], await fixture.Db.ExperimentProjects Assert.Equal(["实验一", "实验二", "实验一", "实验二"], await fixture.Db.ExperimentProjects
.OrderBy(x => x.TeachingTask!.TaskNumber) .OrderBy(x => x.TeachingTask!.TaskNumber).ThenBy(x => x.Code)
.Select(x => x.Name) .Select(x => x.Name)
.ToListAsync()); .ToListAsync());
Assert.Equal(["LAB-BATCH-01", "LAB-BATCH-02"], await fixture.Db.ExperimentProjects
.Select(x => x.Code).Distinct().OrderBy(x => x).ToListAsync());
fixture.Db.ExperimentProjects.RemoveRange(fixture.Db.ExperimentProjects); fixture.Db.ExperimentProjects.RemoveRange(fixture.Db.ExperimentProjects);
await fixture.Db.SaveChangesAsync(); await fixture.Db.SaveChangesAsync();
var sequential = fixture.BatchProjectRequest(ExperimentArrangementMode.SelfScheduled) with var sequential = fixture.BatchProjectRequest(ExperimentArrangementMode.SelfScheduled) with
{ {
Name = "数据库实验", Name = "数据库实验",
GenerateSequentialNames = true GenerateSequentialNames = true,
ProjectCount = 3
}; };
Assert.IsType<CreatedResult>(await controller.CreateProjects(sequential, CancellationToken.None)); Assert.IsType<CreatedResult>(await controller.CreateProjects(sequential, CancellationToken.None));
Assert.Equal(["数据库实验 01", "数据库实验 02"], await fixture.Db.ExperimentProjects Assert.Equal(["数据库实验 01", "数据库实验 02", "数据库实验 03", "数据库实验 01", "数据库实验 02", "数据库实验 03"], await fixture.Db.ExperimentProjects
.OrderBy(x => x.TeachingTask!.TaskNumber) .OrderBy(x => x.TeachingTask!.TaskNumber).ThenBy(x => x.Code)
.Select(x => x.Name) .Select(x => x.Name)
.ToListAsync()); .ToListAsync());
} }
@@ -254,6 +258,23 @@ public sealed class ExperimentsControllerTests
await fixture.Controller(fixture.StudentScope).Book(session.Id, CancellationToken.None)); await fixture.Controller(fixture.StudentScope).Book(session.Id, CancellationToken.None));
} }
[Fact]
public async Task SelfScheduledSession_AllowsAnyEnabledTeachingVenue()
{
await using var fixture = await ExperimentFixture.CreateAsync();
fixture.Classroom.TeachingVenueNature = TeachingVenueNature.GeneralClassroom;
await fixture.Db.SaveChangesAsync();
var manager = fixture.Controller(fixture.ManagerScope);
await manager.CreateProject(
fixture.ProjectRequest(ExperimentArrangementMode.SelfScheduled),
CancellationToken.None);
var project = await fixture.Db.ExperimentProjects.SingleAsync();
Assert.IsType<CreatedResult>(await manager.CreateSession(
project.Id, fixture.SessionRequest(1, 2), CancellationToken.None));
}
[Fact] [Fact]
public async Task BoundInstructor_CanCancelOwnSession() public async Task BoundInstructor_CanCancelOwnSession()
{ {
+3 -3
View File
@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<JiaowuBackendVersion>2.4.2-beta3</JiaowuBackendVersion> <JiaowuBackendVersion>2.4.2-rc1</JiaowuBackendVersion>
<JiaowuFrontendVersion>2.4.2-beta3</JiaowuFrontendVersion> <JiaowuFrontendVersion>2.4.2-rc1</JiaowuFrontendVersion>
<JiaowuSwaggerVersion>2.4.2-beta3</JiaowuSwaggerVersion> <JiaowuSwaggerVersion>2.4.2-rc1</JiaowuSwaggerVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>
Loaded 3 of 4 files, more files were not shown because too many files have changed in this diff. Show more