diff --git a/src/Jiaowu.Api/Controllers/CourseSelectionsController.cs b/src/Jiaowu.Api/Controllers/CourseSelectionsController.cs index 20b6f28..6511760 100644 --- a/src/Jiaowu.Api/Controllers/CourseSelectionsController.cs +++ b/src/Jiaowu.Api/Controllers/CourseSelectionsController.cs @@ -1011,7 +1011,11 @@ public sealed class CourseSelectionsController( x.TeachingTask!.Status == TeachingTaskStatus.Published && (x.IsOpenToAll || x.TeachingTask.Classes.Any(item => - item.AdministrativeClassId == student.AdministrativeClassId))) + item.AdministrativeClassId == student.AdministrativeClassId) || + x.Enrollments.Any(item => + item.StudentId == student.Id && + (item.Status == CourseEnrollmentStatus.Enrolled || + item.Status == CourseEnrollmentStatus.Waitlisted)))) .OrderBy(x => x.TeachingTask!.Course!.Code) .Select(x => new StudentOfferingDto( x.Id, diff --git a/tests/Jiaowu.Api.Tests/CourseSelectionsControllerTests.cs b/tests/Jiaowu.Api.Tests/CourseSelectionsControllerTests.cs index bff206d..58e3f25 100644 --- a/tests/Jiaowu.Api.Tests/CourseSelectionsControllerTests.cs +++ b/tests/Jiaowu.Api.Tests/CourseSelectionsControllerTests.cs @@ -205,6 +205,79 @@ public sealed class CourseSelectionsControllerTests x.Grade == 2026)); } + [Fact] + public async Task Student_options_include_an_administrator_assigned_offering_outside_the_students_class_scope() + { + await using var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + await using var db = new AppDbContext(options); + await db.Database.EnsureCreatedAsync(); + var data = await SeedFullOfferingAsync(db); + + var originalClass = await db.AdministrativeClasses.SingleAsync(); + var otherClass = new AdministrativeClass + { + Code = "CS2026-02", + Name = "计科 2026-2 班", + MajorId = originalClass.MajorId, + Grade = 2026 + }; + var taskId = await db.CourseSelectionOfferings + .Where(x => x.Id == data.OfferingId) + .Select(x => x.TeachingTaskId) + .SingleAsync(); + var task = await db.TeachingTasks + .Include(x => x.Classes) + .SingleAsync(x => x.Id == taskId); + task.Classes.Clear(); + task.Classes.Add(new TeachingTaskClass { AdministrativeClassId = otherClass.Id }); + task.SchedulingMode = TeachingTaskSchedulingMode.Standard; + var termId = await db.CourseSelectionRounds + .Where(x => x.Id == data.RoundId) + .Select(x => x.AcademicTermId) + .SingleAsync(); + var plan = new SchedulePlan + { + AcademicTermId = termId, + Name = "正式课表", + Version = "V1", + Status = SchedulePlanStatus.Published, + PublishedAt = DateTime.UtcNow + }; + db.AddRange(otherClass, plan); + await db.SaveChangesAsync(); + db.ScheduleEntries.Add(new ScheduleEntry + { + SchedulePlanId = plan.Id, + TeachingTaskId = task.Id, + DayOfWeek = 1, + StartPeriod = 1, + PeriodCount = 2, + StartWeek = 1, + EndWeek = 16, + WeekPattern = WeekPattern.All + }); + await db.SaveChangesAsync(); + db.ChangeTracker.Clear(); + + var controller = new CourseSelectionsController( + db, + new StudentDataScope(data.EnrolledUserId)); + var result = Assert.IsType(await controller.GetStudentOptions( + data.RoundId, + CancellationToken.None)); + var offerings = ReadProperty>( + result.Value, "Offerings"); + var assignedOffering = Assert.Single(offerings); + + Assert.Equal(data.OfferingId, assignedOffering.Id); + Assert.Equal(CourseEnrollmentStatus.Enrolled, assignedOffering.EnrollmentStatus); + Assert.Single(assignedOffering.Schedules); + } + private static async Task SeedFullOfferingAsync(AppDbContext db) { var college = new College { Code = "CS", Name = "计算机学院" }; @@ -358,6 +431,14 @@ public sealed class CourseSelectionsControllerTests return Assert.IsType(property.GetValue(value)); } + private static T ReadProperty(object? value, string propertyName) + { + Assert.NotNull(value); + var property = value.GetType().GetProperty(propertyName); + Assert.NotNull(property); + return Assert.IsAssignableFrom(property.GetValue(value)); + } + private static Student CreateStudent( string number, string name,