Compare commits
2
Commits
a942fe34ca
...
949f389854
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
Reference in new issue
Block a user