2 Commits
4 changed files with 149 additions and 2 deletions

No files matched your search

@@ -94,13 +94,18 @@ public sealed class ExperimentGradesController(
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var projects = await source var projects = await source
.Where(x => taskIds.Contains(x.TeachingTaskId)) .Where(x => taskIds.Contains(x.TeachingTaskId))
.OrderBy(x => x.Code) .OrderBy(x => x.ScheduleWeek ?? int.MaxValue)
.ThenBy(x => x.ScheduleEntry == null ? int.MaxValue : x.ScheduleEntry.DayOfWeek)
.ThenBy(x => x.ScheduleEntry == null ? int.MaxValue : x.ScheduleEntry.StartPeriod)
.ThenBy(x => x.StartDate)
.ThenBy(x => x.Code)
.Select(x => new .Select(x => new
{ {
x.Id, x.Id,
x.Code, x.Code,
x.Name, x.Name,
x.ArrangementMode, x.ArrangementMode,
x.ScheduleWeek,
ProjectStatus = x.Status, ProjectStatus = x.Status,
x.TeachingTaskId, x.TeachingTaskId,
x.TeachingTask!.TaskNumber, x.TeachingTask!.TaskNumber,
@@ -113,6 +118,14 @@ public sealed class ExperimentGradesController(
TeacherNames = x.TeachingTask.Teachers TeacherNames = x.TeachingTask.Teachers
.OrderByDescending(item => item.IsPrimary) .OrderByDescending(item => item.IsPrimary)
.Select(item => item.Teacher!.Name), .Select(item => item.Teacher!.Name),
ScheduleEntry = x.ScheduleEntry == null
? null
: new
{
x.ScheduleEntry.DayOfWeek,
x.ScheduleEntry.StartPeriod,
x.ScheduleEntry.PeriodCount
},
Sheet = x.GradeSheet == null Sheet = x.GradeSheet == null
? null ? null
: new : new
@@ -356,6 +369,15 @@ public sealed class ExperimentGradesController(
ProjectCode = x.ExperimentProject!.Code, ProjectCode = x.ExperimentProject!.Code,
ProjectName = x.ExperimentProject.Name, ProjectName = x.ExperimentProject.Name,
x.ExperimentProject.ArrangementMode, x.ExperimentProject.ArrangementMode,
x.ExperimentProject.ScheduleWeek,
ScheduleEntry = x.ExperimentProject.ScheduleEntry == null
? null
: new
{
x.ExperimentProject.ScheduleEntry.DayOfWeek,
x.ExperimentProject.ScheduleEntry.StartPeriod,
x.ExperimentProject.ScheduleEntry.PeriodCount
},
x.ExperimentProject.TeachingTaskId, x.ExperimentProject.TeachingTaskId,
x.ExperimentProject.TeachingTask!.TaskNumber, x.ExperimentProject.TeachingTask!.TaskNumber,
TaskName = x.ExperimentProject.TeachingTask.Name, TaskName = x.ExperimentProject.TeachingTask.Name,
@@ -166,6 +166,14 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
configurationBuilder.Properties<TimeOnly>() configurationBuilder.Properties<TimeOnly>()
.HaveConversion<TimeOnlyTimeSpanConverter>() .HaveConversion<TimeOnlyTimeSpanConverter>()
.HaveColumnType("time"); .HaveColumnType("time");
// MySQL DATETIME has no offset or DateTimeKind. All system timestamps
// are persisted as UTC, so restore that contract when materializing
// them. System.Text.Json will then emit the trailing "Z", allowing
// browsers to convert timestamps to the viewer's local time correctly.
configurationBuilder.Properties<DateTime>()
.HaveConversion<UtcDateTimeConverter>()
.HaveColumnType("datetime(6)");
} }
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
@@ -1086,6 +1094,10 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
}); });
builder.Entity<ExamSession>(entity => builder.Entity<ExamSession>(entity =>
{ {
// Exam slot times are China-local wall-clock times, not instants.
// Keep their existing API representation offset-free.
entity.Property(x => x.StartsAt).HasConversion<UnspecifiedDateTimeConverter>();
entity.Property(x => x.EndsAt).HasConversion<UnspecifiedDateTimeConverter>();
entity.Property(x => x.Notes).HasMaxLength(500); entity.Property(x => x.Notes).HasMaxLength(500);
entity.HasIndex(x => new { x.ExamPlanId, x.StartsAt }); entity.HasIndex(x => new { x.ExamPlanId, x.StartsAt });
entity.HasIndex(x => x.TeachingTaskId); entity.HasIndex(x => x.TeachingTaskId);
@@ -1112,6 +1124,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
builder.Entity<ExamRoomAssignment>(entity => builder.Entity<ExamRoomAssignment>(entity =>
{ {
entity.ToTable("ExamRooms"); entity.ToTable("ExamRooms");
entity.Property(x => x.StartsAt).HasConversion<UnspecifiedDateTimeConverter>();
entity.Property(x => x.EndsAt).HasConversion<UnspecifiedDateTimeConverter>();
entity.HasIndex(x => new { x.ExamPlanId, x.StartsAt }) entity.HasIndex(x => new { x.ExamPlanId, x.StartsAt })
.HasDatabaseName("IX_ExamRooms_Plan_Time"); .HasDatabaseName("IX_ExamRooms_Plan_Time");
entity.HasIndex(x => new entity.HasIndex(x => new
@@ -1200,6 +1214,9 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
}); });
builder.Entity<MakeupExamSession>(entity => builder.Entity<MakeupExamSession>(entity =>
{ {
// Makeup-exam slot times follow the same wall-clock convention.
entity.Property(x => x.StartsAt).HasConversion<UnspecifiedDateTimeConverter>();
entity.Property(x => x.EndsAt).HasConversion<UnspecifiedDateTimeConverter>();
entity.Property(x => x.Notes).HasMaxLength(500); entity.Property(x => x.Notes).HasMaxLength(500);
entity.HasIndex(x => new { x.MakeupExamPlanId, x.StartsAt }); entity.HasIndex(x => new { x.MakeupExamPlanId, x.StartsAt });
entity.HasIndex(x => x.TeachingTaskId); entity.HasIndex(x => x.TeachingTaskId);
@@ -1646,3 +1663,15 @@ public sealed class TimeOnlyTimeSpanConverter()
: ValueConverter<TimeOnly, TimeSpan>( : ValueConverter<TimeOnly, TimeSpan>(
time => time.ToTimeSpan(), time => time.ToTimeSpan(),
value => TimeOnly.FromTimeSpan(value)); value => TimeOnly.FromTimeSpan(value));
public sealed class UtcDateTimeConverter()
: ValueConverter<DateTime, DateTime>(
value => value.Kind == DateTimeKind.Local
? value.ToUniversalTime()
: DateTime.SpecifyKind(value, DateTimeKind.Utc),
value => DateTime.SpecifyKind(value, DateTimeKind.Utc));
public sealed class UnspecifiedDateTimeConverter()
: ValueConverter<DateTime, DateTime>(
value => DateTime.SpecifyKind(value, DateTimeKind.Unspecified),
value => DateTime.SpecifyKind(value, DateTimeKind.Unspecified));
@@ -215,6 +215,89 @@ public sealed class ExperimentGradesControllerTests
Assert.Equal(1, Property<int>(filtered.Value, "Total")); Assert.Equal(1, Property<int>(filtered.Value, "Total"));
} }
[Fact]
public async Task ManagementList_OrdersProjectsByScheduledWeekAndPeriod()
{
await using var fixture = await ExperimentGradeFixture.CreateAsync();
var plan = new SchedulePlan
{
AcademicTermId = fixture.Term.Id,
Name = "实验课表",
Version = "v1",
Status = SchedulePlanStatus.Published
};
var laterEntry = new ScheduleEntry
{
SchedulePlanId = plan.Id,
TeachingTaskId = fixture.Task.Id,
ClassroomId = fixture.Classroom.Id,
Kind = ScheduleEntryKind.Experiment,
DayOfWeek = 4,
StartPeriod = 5,
PeriodCount = 2,
StartWeek = 5,
EndWeek = 5,
WeekPattern = WeekPattern.All
};
var earlierEntry = new ScheduleEntry
{
SchedulePlanId = plan.Id,
TeachingTaskId = fixture.Task.Id,
ClassroomId = fixture.Classroom.Id,
Kind = ScheduleEntryKind.Experiment,
DayOfWeek = 2,
StartPeriod = 3,
PeriodCount = 2,
StartWeek = 2,
EndWeek = 2,
WeekPattern = WeekPattern.All
};
fixture.Db.AddRange(plan, laterEntry, earlierEntry);
fixture.Db.AddRange(
new ExperimentProject
{
TeachingTaskId = fixture.Task.Id,
ScheduleEntryId = laterEntry.Id,
ScheduleWeek = 5,
Code = "LAB-A",
Name = "后面的实验",
ArrangementMode = ExperimentArrangementMode.Centralized,
StartDate = fixture.Term.StartDate,
EndDate = fixture.Term.EndDate,
Status = ExperimentProjectStatus.Published
},
new ExperimentProject
{
TeachingTaskId = fixture.Task.Id,
ScheduleEntryId = earlierEntry.Id,
ScheduleWeek = 2,
Code = "LAB-Z",
Name = "前面的实验",
ArrangementMode = ExperimentArrangementMode.Centralized,
StartDate = fixture.Term.StartDate,
EndDate = fixture.Term.EndDate,
Status = ExperimentProjectStatus.Published
});
await fixture.Db.SaveChangesAsync();
fixture.Db.ChangeTracker.Clear();
var result = Assert.IsType<OkObjectResult>(await fixture.ExperimentGrades(
fixture.AdminScope).GetManagement(
fixture.Term.Id, null, null, null, 1, 10, CancellationToken.None));
var course = Assert.Single(Property<System.Collections.IEnumerable>(
result.Value, "Items").Cast<object>());
var projects = Property<System.Collections.IEnumerable>(course, "Projects")
.Cast<object>()
.ToList();
Assert.Equal(["前面的实验", "后面的实验"],
projects.Select(project => Property<string>(project, "Name")));
var schedule = Property<object>(projects[0], "ScheduleEntry");
Assert.Equal(2, Property<int>(projects[0], "ScheduleWeek"));
Assert.Equal(2, Property<int>(schedule, "DayOfWeek"));
Assert.Equal(3, Property<int>(schedule, "StartPeriod"));
}
private static T Property<T>(object? value, string name) => private static T Property<T>(object? value, string name) =>
Assert.IsAssignableFrom<T>( Assert.IsAssignableFrom<T>(
value!.GetType().GetProperty(name)!.GetValue(value)); value!.GetType().GetProperty(name)!.GetValue(value));
Loaded 3 of 4 files, more files were not shown because too many files have changed in this diff. Show more