diff --git a/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs b/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs index 81d8dd2..b9143f1 100644 --- a/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs +++ b/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs @@ -34,7 +34,8 @@ public sealed class ExperimentGradesController( private const string Publishers = SystemRoles.SuperAdmin + "," + - SystemRoles.AcademicAdmin; + SystemRoles.AcademicAdmin + "," + + SystemRoles.CollegeAdmin; [HttpGet("management")] [Authorize(Roles = Managers)] @@ -1220,7 +1221,8 @@ public sealed class ExperimentGradesController( private bool IsPublisher() => currentUserDataScope.Current.IsInRole(SystemRoles.SuperAdmin) || - currentUserDataScope.Current.IsInRole(SystemRoles.AcademicAdmin); + currentUserDataScope.Current.IsInRole(SystemRoles.AcademicAdmin) || + currentUserDataScope.Current.IsInRole(SystemRoles.CollegeAdmin); private Task CurrentStudentAsync( CancellationToken cancellationToken) => diff --git a/src/Jiaowu.Api/Controllers/GradesController.cs b/src/Jiaowu.Api/Controllers/GradesController.cs index a73c8a0..2553fc9 100644 --- a/src/Jiaowu.Api/Controllers/GradesController.cs +++ b/src/Jiaowu.Api/Controllers/GradesController.cs @@ -549,6 +549,8 @@ public sealed class GradesController( .Include(x => x.Records) .ThenInclude(x => x.ItemScores) .Include(x => x.TeachingTask) + .ThenInclude(x => x!.Course) + .Include(x => x.TeachingTask) .ThenInclude(x => x!.Teachers) .ThenInclude(x => x.Teacher) .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); diff --git a/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs b/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs index 37435f5..489f905 100644 --- a/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs +++ b/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs @@ -14,6 +14,58 @@ namespace Jiaowu.Api.Tests; public sealed class ExperimentGradesControllerTests { + [Fact] + public async Task CollegeAdmin_CanPublishApprovedExperimentGradeSheet() + { + await using var fixture = await ExperimentGradeFixture.CreateAsync(); + var project = new ExperimentProject + { + TeachingTaskId = fixture.Task.Id, + Code = "LAB-COLLEGE-PUBLISH", + Name = "学院发布实验", + ArrangementMode = ExperimentArrangementMode.Centralized, + StartDate = fixture.Term.StartDate, + EndDate = fixture.Term.EndDate, + Status = ExperimentProjectStatus.Published, + PublishedAt = DateTime.UtcNow + }; + fixture.Db.ExperimentProjects.Add(project); + await fixture.Db.SaveChangesAsync(); + + var administrator = fixture.ExperimentGrades(fixture.AdminScope); + await administrator.CreateSheet( + new ExperimentGradeSheetRequest( + project.Id, + 1, + 60, + [new ExperimentGradeItemRequest( + "操作", + ExperimentGradeItemKind.Operation, + 100)]), + CancellationToken.None); + var sheet = await fixture.Db.ExperimentGradeSheets.SingleAsync(); + sheet.Status = ExperimentGradeSheetStatus.Approved; + await fixture.Db.SaveChangesAsync(); + + var collegeId = await fixture.Db.TeachingTasks + .Where(x => x.Id == fixture.Task.Id) + .Select(x => x.Course!.CollegeId) + .SingleAsync(); + var collegeScope = new FixedScope(new CurrentUserScope( + Guid.NewGuid(), + "学院管理员", + collegeId, + DataScope.College, + new HashSet([SystemRoles.CollegeAdmin]))); + + Assert.IsType(await fixture + .ExperimentGrades(collegeScope) + .Publish(sheet.Id, CancellationToken.None)); + Assert.Equal( + ExperimentGradeSheetStatus.Published, + (await fixture.Db.ExperimentGradeSheets.SingleAsync()).Status); + } + [Fact] public async Task AssignedTeacher_CanSubmitExperimentGradeSheet() { diff --git a/tests/Jiaowu.Api.Tests/GradesPaginationTests.cs b/tests/Jiaowu.Api.Tests/GradesPaginationTests.cs index 162f697..5528a6c 100644 --- a/tests/Jiaowu.Api.Tests/GradesPaginationTests.cs +++ b/tests/Jiaowu.Api.Tests/GradesPaginationTests.cs @@ -148,6 +148,101 @@ public sealed class GradesPaginationTests Assert.Contains("F2*60/100", worksheet.Cell("G2").FormulaA1); } + [Fact] + public async Task SubmittingCourseGradeSheet_LoadsCourseForNotificationAndReturnsSuccess() + { + await using var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(); + await using var db = new AppDbContext( + new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options); + await db.Database.EnsureCreatedAsync(); + + var college = new College { Code = "CS", Name = "计算机学院" }; + var major = new Major + { + Code = "080901", + Name = "计算机科学与技术", + CollegeId = college.Id, + DegreeType = "工学学士" + }; + var administrativeClass = new AdministrativeClass + { + Code = "CS2026-01", + Name = "计科 2026-1 班", + MajorId = major.Id, + Grade = 2026 + }; + var term = new AcademicTerm + { + Code = "2026-1", + Name = "2026—2027 学年第一学期", + AcademicYear = "2026-2027", + Season = TermSeason.Autumn, + StartDate = new DateOnly(2026, 9, 1), + EndDate = new DateOnly(2027, 1, 20) + }; + var course = new Course + { + Code = "CS101", + Name = "程序设计基础", + CollegeId = college.Id, + Credits = 4, + TotalHours = 64, + LectureHours = 48, + PracticeHours = 16, + Nature = CourseNature.MajorRequired, + AssessmentMethod = AssessmentMethod.Examination + }; + var task = new TeachingTask + { + TaskNumber = "2026-1-CS101-01", + Name = "程序设计基础教学班", + AcademicTermId = term.Id, + CourseId = course.Id, + Capacity = 60, + Status = TeachingTaskStatus.Published + }; + var student = new Student + { + StudentNumber = "202601001", + Name = "学生 1", + AdministrativeClassId = administrativeClass.Id, + EnrollmentYear = 2026, + EnrollmentDate = new DateOnly(2026, 9, 1) + }; + var sheet = new GradeSheet + { + TeachingTaskId = task.Id, + RegularWeight = 40, + FinalWeight = 60, + Records = + [ + new GradeRecord + { + StudentId = student.Id, + RegularScore = 80, + FinalScore = 90 + } + ] + }; + db.AddRange(college, major, administrativeClass, term, course, task, student, sheet); + await db.SaveChangesAsync(); + db.ChangeTracker.Clear(); + + var result = await new GradesController(db, new AllScope()).Submit( + sheet.Id, + CancellationToken.None); + + Assert.IsType(result); + Assert.Equal( + GradeSheetStatus.Submitted, + await db.GradeSheets.Where(x => x.Id == sheet.Id) + .Select(x => x.Status) + .SingleAsync()); + } + private static int ReadInt(object value, string property) => (int)value.GetType().GetProperty(property)!.GetValue(value)!;