实验课表

This commit is contained in:
2026-07-28 21:25:21 +08:00 Unverified
parent ec979f640d
commit 51c3b81e27
7 changed files with 687 additions and 28 deletions
@@ -138,6 +138,107 @@ public sealed class ExperimentsControllerTests
Assert.Empty(fixture.Db.ExperimentBookings);
}
[Fact]
public async Task StudentTimetable_IncludesCentralizedAndBookedSelfScheduledSessions()
{
await using var fixture = await ExperimentFixture.CreateAsync();
var manager = fixture.Controller(fixture.ManagerScope);
await manager.CreateProject(
fixture.ProjectRequest(ExperimentArrangementMode.Centralized),
CancellationToken.None);
var centralized = await fixture.Db.ExperimentProjects.SingleAsync();
await manager.CreateSession(
centralized.Id,
fixture.SessionRequest(1, 2),
CancellationToken.None);
await manager.PublishProject(centralized.Id, CancellationToken.None);
await manager.CreateProject(
fixture.ProjectRequest(ExperimentArrangementMode.SelfScheduled),
CancellationToken.None);
var selfScheduled = await fixture.Db.ExperimentProjects.SingleAsync(
x => x.ArrangementMode == ExperimentArrangementMode.SelfScheduled);
await manager.CreateSession(
selfScheduled.Id,
fixture.SessionRequest(4, 2),
CancellationToken.None);
await manager.PublishProject(selfScheduled.Id, CancellationToken.None);
var selfSession = await fixture.Db.ExperimentSessions.SingleAsync(
x => x.ExperimentProjectId == selfScheduled.Id);
var studentController = fixture.Controller(fixture.StudentScope);
Assert.IsType<NoContentResult>(
await studentController.Book(
selfSession.Id,
CancellationToken.None));
var student = await fixture.Db.Students.AsNoTracking().SingleAsync();
var service = new TimetableDataService(fixture.Db);
var timetable = await service.BuildAsync(
TimetableResourceType.Class,
student.AdministrativeClassId,
fixture.Term.Id,
null,
false,
student.Id,
new TimetableStudentDto(student.StudentNumber, student.Name),
CancellationToken.None);
Assert.NotNull(timetable);
Assert.Equal(2, timetable.ExperimentEntries.Count);
Assert.Contains(
timetable.ExperimentEntries,
x => x.ExperimentArrangementMode ==
ExperimentArrangementMode.Centralized);
Assert.Contains(
timetable.ExperimentEntries,
x => x.Id == selfSession.Id &&
x.ExperimentArrangementMode ==
ExperimentArrangementMode.SelfScheduled);
Assert.All(
timetable.ExperimentEntries,
x =>
{
Assert.True(x.IsExperiment);
Assert.Equal(fixture.Term.StartDate, x.ExperimentDate);
Assert.Equal(1, x.StartWeek);
});
var publicClassTimetable = await service.BuildAsync(
TimetableResourceType.Class,
student.AdministrativeClassId,
fixture.Term.Id,
null,
false,
null,
null,
CancellationToken.None);
Assert.NotNull(publicClassTimetable);
Assert.Empty(publicClassTimetable.ExperimentEntries);
var booking = await fixture.Db.ExperimentBookings.SingleAsync();
Assert.IsType<NoContentResult>(
await studentController.CancelBooking(
booking.Id,
CancellationToken.None));
timetable = await service.BuildAsync(
TimetableResourceType.Class,
student.AdministrativeClassId,
fixture.Term.Id,
null,
false,
student.Id,
new TimetableStudentDto(student.StudentNumber, student.Name),
CancellationToken.None);
Assert.NotNull(timetable);
Assert.Single(timetable.ExperimentEntries);
Assert.Equal(
ExperimentArrangementMode.Centralized,
timetable.ExperimentEntries[0].ExperimentArrangementMode);
}
private sealed class ExperimentFixture : IAsyncDisposable
{
private ExperimentFixture(
@@ -251,6 +251,198 @@ public sealed class PersonalCalendarServiceTests : IAsyncLifetime
$"ICS line exceeds 75 octets: {line}"));
}
[Fact]
public async Task Build_creates_student_feed_with_booked_experiment()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var mondayOffset = today.DayOfWeek == DayOfWeek.Sunday
? 6
: (int)today.DayOfWeek - 1;
var termStart = today.AddDays(-mondayOffset);
var user = new ApplicationUser
{
Id = Guid.NewGuid(),
UserName = "calendar-student",
NormalizedUserName = "CALENDAR-STUDENT",
DisplayName = "实验学生",
CalendarSubscriptionStamp = PersonalCalendarService.CreateStamp(),
CalendarSubscriptionCreatedAt = DateTime.UtcNow
};
var campus = new Campus { Code = "LAB-MAIN", Name = "主校区" };
var college = new College
{
Code = "LAB-CS",
Name = "计算机学院",
CampusId = campus.Id
};
var major = new Major
{
Code = "LAB-SE",
Name = "软件工程",
CollegeId = college.Id,
DegreeType = "工学"
};
var administrativeClass = new AdministrativeClass
{
Code = "LAB-CLASS",
Name = "软件工程实验班",
MajorId = major.Id,
Grade = today.Year
};
var student = new Student
{
StudentNumber = "LAB-STUDENT-01",
Name = "实验学生",
AdministrativeClassId = administrativeClass.Id,
EnrollmentYear = today.Year,
EnrollmentDate = termStart,
UserId = user.Id,
Status = StudentStatus.Active
};
var teacher = new Teacher
{
TeacherNumber = "LAB-TEACHER-01",
Name = "实验教师",
CollegeId = college.Id,
Status = TeacherStatus.Active
};
var course = new Course
{
Code = "LAB-CALENDAR",
Name = "嵌入式系统实验",
CollegeId = college.Id,
Credits = 2,
TotalHours = 32,
LectureHours = 16,
PracticeHours = 16,
Nature = CourseNature.Practice,
AssessmentMethod = AssessmentMethod.Assessment
};
var term = new AcademicTerm
{
Code = "LAB-CALENDAR-TERM",
Name = "实验日历测试学期",
AcademicYear = $"{today.Year}-{today.Year + 1}",
Season = TermSeason.Autumn,
StartDate = termStart,
EndDate = termStart.AddDays(40),
IsCurrent = true
};
var building = new Building
{
Code = "LAB-CENTER",
Name = "实验中心",
CampusId = campus.Id
};
var classroom = new Classroom
{
Code = "LAB-CENTER-301",
Name = "301",
BuildingId = building.Id,
Capacity = 30
};
_db.AddRange(
user,
campus,
college,
major,
administrativeClass,
student,
teacher,
course,
term,
building,
classroom);
await _db.SaveChangesAsync();
var task = new TeachingTask
{
TaskNumber = "LAB-CALENDAR-TASK",
Name = "嵌入式系统实验教学班",
AcademicTermId = term.Id,
CourseId = course.Id,
Capacity = 30,
Status = TeachingTaskStatus.Published,
Teachers =
[
new TeachingTaskTeacher
{
TeacherId = teacher.Id,
IsPrimary = true
}
],
Classes =
[
new TeachingTaskClass
{
AdministrativeClassId = administrativeClass.Id
}
]
};
_db.AddRange(
task,
new ScheduleTimeSlot
{
AcademicTermId = term.Id,
PeriodNumber = 3,
Name = "第三节",
StartsAt = new TimeOnly(10, 0),
EndsAt = new TimeOnly(10, 45)
},
new ScheduleTimeSlot
{
AcademicTermId = term.Id,
PeriodNumber = 4,
Name = "第四节",
StartsAt = new TimeOnly(10, 55),
EndsAt = new TimeOnly(11, 40)
});
await _db.SaveChangesAsync();
var project = new ExperimentProject
{
TeachingTaskId = task.Id,
Code = "LAB-SELF-01",
Name = "自主硬件实验",
ArrangementMode = ExperimentArrangementMode.SelfScheduled,
StartDate = termStart,
EndDate = termStart.AddDays(14),
Status = ExperimentProjectStatus.Published,
PublishedAt = DateTime.UtcNow
};
var session = new ExperimentSession
{
ExperimentProject = project,
ClassroomId = classroom.Id,
SessionDate = termStart.AddDays(2),
StartPeriod = 3,
PeriodCount = 2,
Capacity = 30,
ReservedCount = 1
};
session.Bookings.Add(new ExperimentBooking
{
ExperimentProject = project,
StudentId = student.Id,
Status = ExperimentBookingStatus.Booked,
BookedAt = DateTime.UtcNow
});
_db.Add(session);
await _db.SaveChangesAsync();
var result = await CreateService().BuildAsync(
user,
CancellationToken.None);
Assert.NotNull(result);
Assert.Equal(1, result.EventCount);
var ics = Encoding.UTF8.GetString(result.Content);
Assert.Contains("SUMMARY:[实验] 自主硬件实验", ics);
Assert.Contains("CATEGORIES:实验", ics);
Assert.Contains("LOCATION:主校区 · 实验中心 · 301", ics);
Assert.Contains("LAB-SELF-01 · 自主预约", ics);
}
[Fact]
public void Access_token_is_bound_to_user_stamp_and_can_be_rotated()
{
@@ -108,7 +108,41 @@ public sealed class TimetableExcelExporterTests
"2026-2027 学年第一学期期末考试",
new DateOnly(2027, 1, 5),
DateTime.UtcNow)
]);
])
{
ExperimentEntries =
[
new TimetableEntryDto(
Guid.NewGuid(),
Guid.NewGuid(),
"TASK-LAB",
"程序设计实验教学班",
"CSLAB101",
"程序设计实验",
["陈老师"],
["计算机科学 2601 班"],
"301",
"实验中心",
"主校区",
4,
1,
2,
2,
2,
WeekPattern.All,
null,
false,
null,
null,
DateTime.UtcNow,
true,
Guid.NewGuid(),
"LAB-01",
"基础控制实验",
new DateOnly(2026, 9, 10),
ExperimentArrangementMode.SelfScheduled)
]
};
var bytes = TimetableExcelExporter.Create(timetable);
@@ -117,8 +151,11 @@ public sealed class TimetableExcelExporterTests
var sheet = workbook.Worksheet("课表");
Assert.Contains("计算机科学 2601 班", sheet.Cell("A1").GetString());
Assert.Contains("已融入 1 场考试", sheet.Cell("A2").GetString());
Assert.Contains("已融入 1 场实验", sheet.Cell("A2").GetString());
Assert.Contains("高等数学", sheet.Cell(5, 2).GetString());
Assert.Contains("【考试】高等数学", sheet.Cell(5, 3).GetString());
Assert.Contains("【实验】基础控制实验", sheet.Cell(5, 5).GetString());
Assert.Contains("自主预约", sheet.Cell(5, 5).GetString());
Assert.Contains("2027-01-05", sheet.Cell(5, 3).GetString());
Assert.Contains(
sheet.CellsUsed(),