补考系统
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Exams;
|
||||
|
||||
public sealed class MakeupExamArrangementService(AppDbContext db)
|
||||
{
|
||||
private sealed record RoomOccupancy(Guid ClassroomId, DateTime StartsAt, DateTime EndsAt);
|
||||
private sealed record InvigilatorOccupancy(Guid TeacherId, DateTime StartsAt, DateTime EndsAt);
|
||||
|
||||
public async Task<ExamArrangementResult> ArrangeAsync(
|
||||
Guid planId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var plan = await db.MakeupExamPlans
|
||||
.Include(x => x.AcademicTerm)
|
||||
.Include(x => x.Sessions)
|
||||
.ThenInclude(x => x.Invigilators)
|
||||
.Include(x => x.Sessions)
|
||||
.ThenInclude(x => x.TeachingTask)
|
||||
.ThenInclude(x => x!.Teachers)
|
||||
.FirstOrDefaultAsync(x => x.Id == planId, cancellationToken);
|
||||
|
||||
if (plan is null)
|
||||
return ExamArrangementResult.Fail("补考计划不存在。");
|
||||
if (plan.Status != MakeupExamPlanStatus.Draft)
|
||||
return ExamArrangementResult.Fail("只有草稿状态的补考计划可以自动编排。");
|
||||
|
||||
var sessions = plan.Sessions.ToList();
|
||||
if (sessions.Count == 0)
|
||||
return ExamArrangementResult.Fail("补考计划中没有场次。");
|
||||
|
||||
var termId = plan.AcademicTermId;
|
||||
var timeSlots = await db.ScheduleTimeSlots.AsNoTracking()
|
||||
.Where(x => x.AcademicTermId == termId && x.IsEnabled)
|
||||
.OrderBy(x => x.PeriodNumber)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (timeSlots.Count == 0)
|
||||
return ExamArrangementResult.Fail("当前学期未配置上课时间表,无法计算考试时间段。");
|
||||
|
||||
var timeSlotLookup = timeSlots.ToDictionary(x => x.PeriodNumber);
|
||||
|
||||
int assignedRooms = 0;
|
||||
int assignedInvigilators = 0;
|
||||
int skippedRooms = 0;
|
||||
int skippedInvigilators = 0;
|
||||
var messages = new List<string>();
|
||||
|
||||
var occupiedRooms = sessions
|
||||
.Where(x => x.ClassroomId.HasValue)
|
||||
.Select(x => new RoomOccupancy(x.ClassroomId!.Value, x.StartsAt, x.EndsAt))
|
||||
.ToList();
|
||||
|
||||
var occupiedInvigilators = sessions
|
||||
.SelectMany(x => x.Invigilators.Select(i =>
|
||||
new InvigilatorOccupancy(i.TeacherId, x.StartsAt, x.EndsAt)))
|
||||
.ToList();
|
||||
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
ComputeTimesFromSlots(session, timeSlotLookup);
|
||||
|
||||
var enrolledCount = await db.MakeupExamEnrollments
|
||||
.CountAsync(x => x.MakeupExamSessionId == session.Id, cancellationToken);
|
||||
|
||||
// Auto-assign classroom
|
||||
if (!session.ClassroomId.HasValue)
|
||||
{
|
||||
var room = await FindBestClassroomAsync(
|
||||
session, enrolledCount, occupiedRooms, cancellationToken);
|
||||
if (room is not null)
|
||||
{
|
||||
session.ClassroomId = room.Id;
|
||||
occupiedRooms.Add(new RoomOccupancy(room.Id, session.StartsAt, session.EndsAt));
|
||||
assignedRooms++;
|
||||
messages.Add(
|
||||
$"\"{session.TeachingTask!.Course!.Name}\"→{room.Name}({room.Capacity}座)");
|
||||
}
|
||||
else
|
||||
{
|
||||
skippedRooms++;
|
||||
messages.Add(
|
||||
$"\"{session.TeachingTask!.Course!.Name}\":无可用考场(需≥{enrolledCount}座)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
skippedRooms++;
|
||||
}
|
||||
|
||||
// Auto-assign invigilators
|
||||
var currentInvigilatorCount = session.Invigilators.Count;
|
||||
var needed = session.RequiredInvigilatorCount - currentInvigilatorCount;
|
||||
if (needed > 0)
|
||||
{
|
||||
var courseTeacherIds = session.TeachingTask!.Teachers
|
||||
.Select(x => x.TeacherId).ToHashSet();
|
||||
var newlyAssigned = await FindInvigilatorsAsync(
|
||||
session, needed, courseTeacherIds,
|
||||
occupiedInvigilators, cancellationToken);
|
||||
foreach (var teacher in newlyAssigned)
|
||||
{
|
||||
session.Invigilators.Add(new MakeupExamSessionInvigilator
|
||||
{
|
||||
MakeupExamSessionId = session.Id,
|
||||
TeacherId = teacher.Id
|
||||
});
|
||||
occupiedInvigilators.Add(new InvigilatorOccupancy(
|
||||
teacher.Id, session.StartsAt, session.EndsAt));
|
||||
assignedInvigilators++;
|
||||
}
|
||||
|
||||
if (newlyAssigned.Count < needed)
|
||||
messages.Add(
|
||||
$"\"{session.TeachingTask!.Course!.Name}\":仅找到{newlyAssigned.Count}/{needed}名监考教师");
|
||||
}
|
||||
else
|
||||
{
|
||||
skippedInvigilators++;
|
||||
}
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new ExamArrangementResult(
|
||||
true,
|
||||
$"{assignedRooms}个考场、{assignedInvigilators}名监考已分配。" +
|
||||
(skippedRooms > 0 ? $" {skippedRooms}个场次无可用考场。" : "") +
|
||||
(messages.Count > 0 ? $" 详情:{string.Join(";", messages.Take(10))}" : ""));
|
||||
}
|
||||
|
||||
private static void ComputeTimesFromSlots(
|
||||
MakeupExamSession session,
|
||||
Dictionary<int, ScheduleTimeSlot> timeSlotLookup)
|
||||
{
|
||||
var startSlot = timeSlotLookup.GetValueOrDefault(session.StartPeriod);
|
||||
var endSlot = timeSlotLookup.GetValueOrDefault(
|
||||
session.StartPeriod + session.PeriodCount - 1);
|
||||
if (startSlot is null || endSlot is null) return;
|
||||
|
||||
var examDate = session.ExamDate;
|
||||
session.StartsAt = examDate.ToDateTime(startSlot.StartsAt, DateTimeKind.Utc);
|
||||
session.EndsAt = examDate.ToDateTime(endSlot.EndsAt, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
private async Task<Classroom?> FindBestClassroomAsync(
|
||||
MakeupExamSession session,
|
||||
int enrolledCount,
|
||||
List<RoomOccupancy> occupied,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = db.Classrooms.AsNoTracking()
|
||||
.Where(x => x.IsEnabled && x.Capacity >= enrolledCount);
|
||||
|
||||
if (session.RequiredBuildingId.HasValue)
|
||||
query = query.Where(x => x.BuildingId == session.RequiredBuildingId.Value);
|
||||
|
||||
var occupiedRoomIds = occupied
|
||||
.Where(x => ExamConflictRules.TimeOverlaps(
|
||||
x.StartsAt, x.EndsAt, session.StartsAt, session.EndsAt))
|
||||
.Select(x => x.ClassroomId)
|
||||
.ToHashSet();
|
||||
|
||||
if (occupiedRoomIds.Count > 0)
|
||||
query = query.Where(x => !occupiedRoomIds.Contains(x.Id));
|
||||
|
||||
var dbOccupiedRooms = await db.MakeupExamSessions.AsNoTracking()
|
||||
.Where(x => x.MakeupExamPlanId == session.MakeupExamPlanId &&
|
||||
x.Id != session.Id &&
|
||||
x.ClassroomId != null &&
|
||||
x.StartsAt < session.EndsAt &&
|
||||
session.StartsAt < x.EndsAt)
|
||||
.Select(x => x.ClassroomId!.Value)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (dbOccupiedRooms.Count > 0)
|
||||
query = query.Where(x => !dbOccupiedRooms.Contains(x.Id));
|
||||
|
||||
return await query
|
||||
.OrderBy(x => x.Capacity)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<List<Teacher>> FindInvigilatorsAsync(
|
||||
MakeupExamSession session,
|
||||
int needed,
|
||||
HashSet<Guid> excludeTeacherIds,
|
||||
List<InvigilatorOccupancy> occupied,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var busyTeacherIds = occupied
|
||||
.Where(x => ExamConflictRules.TimeOverlaps(
|
||||
x.StartsAt, x.EndsAt, session.StartsAt, session.EndsAt))
|
||||
.Select(x => x.TeacherId)
|
||||
.ToHashSet();
|
||||
|
||||
var dbBusyIds = await db.MakeupExamSessionInvigilators.AsNoTracking()
|
||||
.Where(x => x.MakeupExamSession!.MakeupExamPlanId == session.MakeupExamPlanId &&
|
||||
x.MakeupExamSessionId != session.Id &&
|
||||
x.MakeupExamSession!.StartsAt < session.EndsAt &&
|
||||
session.StartsAt < x.MakeupExamSession.EndsAt)
|
||||
.Select(x => x.TeacherId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var id in dbBusyIds) busyTeacherIds.Add(id);
|
||||
foreach (var id in excludeTeacherIds) busyTeacherIds.Add(id);
|
||||
|
||||
return await db.Teachers.AsNoTracking()
|
||||
.Where(x => x.Status == TeacherStatus.Active &&
|
||||
!busyTeacherIds.Contains(x.Id))
|
||||
.OrderBy(x => Guid.NewGuid())
|
||||
.Take(needed)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Exams;
|
||||
|
||||
public sealed class MakeupExamEligibilityService(AppDbContext db)
|
||||
{
|
||||
public async Task<List<EligibleStudent>> GetEligibleStudentsAsync(
|
||||
Guid teachingTaskId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var gradeSheet = await db.GradeSheets.AsNoTracking()
|
||||
.Where(x => x.TeachingTaskId == teachingTaskId)
|
||||
.Select(x => new { x.Id, x.TeachingTask!.AcademicTermId })
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (gradeSheet is null)
|
||||
return [];
|
||||
|
||||
// Find students already enrolled in any makeup session for this teaching task
|
||||
var alreadyEnrolledIds = await db.MakeupExamEnrollments.AsNoTracking()
|
||||
.Where(x => x.MakeupExamSession!.TeachingTaskId == teachingTaskId &&
|
||||
x.MakeupExamSession!.MakeupExamPlan!.Status != MakeupExamPlanStatus.Archived)
|
||||
.Select(x => x.StudentId)
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var enrolledSet = alreadyEnrolledIds.ToHashSet();
|
||||
var results = new Dictionary<Guid, EligibleStudent>();
|
||||
|
||||
// 1. Approved deferred exams (highest priority)
|
||||
var deferredStudents = await db.DeferredExams.AsNoTracking()
|
||||
.Where(x => x.TeachingTaskId == teachingTaskId &&
|
||||
x.Status == ApprovalStatus.Approved &&
|
||||
!enrolledSet.Contains(x.StudentId))
|
||||
.Select(x => new
|
||||
{
|
||||
x.StudentId,
|
||||
x.Student!.StudentNumber,
|
||||
x.Student.Name,
|
||||
ClassName = x.Student.AdministrativeClass!.Name,
|
||||
x.Id
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var s in deferredStudents)
|
||||
{
|
||||
if (!results.ContainsKey(s.StudentId))
|
||||
{
|
||||
results[s.StudentId] = new EligibleStudent(
|
||||
s.StudentId, s.StudentNumber, s.Name, s.ClassName,
|
||||
MakeupReason.DeferredApproved, null,
|
||||
SourceDeferredExamId: s.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Absent students
|
||||
var absentStudents = await db.GradeRecords.AsNoTracking()
|
||||
.Where(x => x.GradeSheetId == gradeSheet.Id &&
|
||||
x.ExamStatus == GradeExamStatus.Absent &&
|
||||
!enrolledSet.Contains(x.StudentId))
|
||||
.Select(x => new
|
||||
{
|
||||
x.StudentId,
|
||||
x.Student!.StudentNumber,
|
||||
x.Student.Name,
|
||||
ClassName = x.Student.AdministrativeClass!.Name,
|
||||
GradeRecordId = (Guid?)x.Id
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var s in absentStudents)
|
||||
{
|
||||
if (!results.ContainsKey(s.StudentId))
|
||||
{
|
||||
results[s.StudentId] = new EligibleStudent(
|
||||
s.StudentId, s.StudentNumber, s.Name, s.ClassName,
|
||||
MakeupReason.Absent, null,
|
||||
SourceGradeRecordId: s.GradeRecordId);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Failed students (TotalScore < 60, Normal status)
|
||||
var failedStudents = await db.GradeRecords.AsNoTracking()
|
||||
.Where(x => x.GradeSheetId == gradeSheet.Id &&
|
||||
x.ExamStatus == GradeExamStatus.Normal &&
|
||||
x.TotalScore < 60 &&
|
||||
!enrolledSet.Contains(x.StudentId))
|
||||
.Select(x => new
|
||||
{
|
||||
x.StudentId,
|
||||
x.Student!.StudentNumber,
|
||||
x.Student.Name,
|
||||
ClassName = x.Student.AdministrativeClass!.Name,
|
||||
x.TotalScore,
|
||||
GradeRecordId = (Guid?)x.Id
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var s in failedStudents)
|
||||
{
|
||||
if (!results.ContainsKey(s.StudentId))
|
||||
{
|
||||
results[s.StudentId] = new EligibleStudent(
|
||||
s.StudentId, s.StudentNumber, s.Name, s.ClassName,
|
||||
MakeupReason.Failed, s.TotalScore,
|
||||
SourceGradeRecordId: s.GradeRecordId);
|
||||
}
|
||||
}
|
||||
|
||||
return [.. results.Values.OrderBy(x => x.StudentNumber)];
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record EligibleStudent(
|
||||
Guid StudentId,
|
||||
string StudentNumber,
|
||||
string Name,
|
||||
string ClassName,
|
||||
MakeupReason Reason,
|
||||
decimal? TotalScore = null,
|
||||
Guid? SourceGradeRecordId = null,
|
||||
Guid? SourceDeferredExamId = null,
|
||||
string? Detail = null);
|
||||
@@ -22,7 +22,7 @@ public static class GradeCalculator
|
||||
IReadOnlyCollection<GradeItem> items,
|
||||
GradeExamStatus examStatus)
|
||||
{
|
||||
if (examStatus != GradeExamStatus.Normal)
|
||||
if (examStatus != GradeExamStatus.Normal && examStatus != GradeExamStatus.Makeup)
|
||||
return null;
|
||||
|
||||
if (regularWeight > 0 && !regularScore.HasValue)
|
||||
|
||||
@@ -55,6 +55,12 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
public DbSet<ExamSession> ExamSessions => Set<ExamSession>();
|
||||
public DbSet<ExamSessionInvigilator> ExamSessionInvigilators =>
|
||||
Set<ExamSessionInvigilator>();
|
||||
public DbSet<MakeupExamPlan> MakeupExamPlans => Set<MakeupExamPlan>();
|
||||
public DbSet<MakeupExamSession> MakeupExamSessions => Set<MakeupExamSession>();
|
||||
public DbSet<MakeupExamSessionInvigilator> MakeupExamSessionInvigilators =>
|
||||
Set<MakeupExamSessionInvigilator>();
|
||||
public DbSet<MakeupExamEnrollment> MakeupExamEnrollments =>
|
||||
Set<MakeupExamEnrollment>();
|
||||
public DbSet<CourseAdjustment> CourseAdjustments => Set<CourseAdjustment>();
|
||||
public DbSet<CourseExemption> CourseExemptions => Set<CourseExemption>();
|
||||
public DbSet<DeferredExam> DeferredExams => Set<DeferredExam>();
|
||||
@@ -598,6 +604,52 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
entity.HasOne(x => x.Teacher).WithMany()
|
||||
.HasForeignKey(x => x.TeacherId).OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
builder.Entity<MakeupExamPlan>(entity =>
|
||||
{
|
||||
entity.Property(x => x.Name).HasMaxLength(120);
|
||||
entity.Property(x => x.Notes).HasMaxLength(500);
|
||||
entity.HasIndex(x => new { x.AcademicTermId, x.Status });
|
||||
entity.HasOne(x => x.AcademicTerm).WithMany()
|
||||
.HasForeignKey(x => x.AcademicTermId).OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
builder.Entity<MakeupExamSession>(entity =>
|
||||
{
|
||||
entity.Property(x => x.Notes).HasMaxLength(500);
|
||||
entity.HasIndex(x => new { x.MakeupExamPlanId, x.StartsAt });
|
||||
entity.HasIndex(x => x.TeachingTaskId);
|
||||
entity.HasIndex(x => new { x.MakeupExamPlanId, x.ExamDate });
|
||||
entity.HasIndex(x => x.RequiredBuildingId);
|
||||
entity.HasOne(x => x.MakeupExamPlan).WithMany(x => x.Sessions)
|
||||
.HasForeignKey(x => x.MakeupExamPlanId).OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne(x => x.TeachingTask).WithMany()
|
||||
.HasForeignKey(x => x.TeachingTaskId).OnDelete(DeleteBehavior.Restrict);
|
||||
entity.HasOne(x => x.Classroom).WithMany()
|
||||
.HasForeignKey(x => x.ClassroomId).OnDelete(DeleteBehavior.SetNull);
|
||||
entity.HasOne(x => x.RequiredBuilding).WithMany()
|
||||
.HasForeignKey(x => x.RequiredBuildingId).OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
builder.Entity<MakeupExamSessionInvigilator>(entity =>
|
||||
{
|
||||
entity.HasKey(x => new { x.MakeupExamSessionId, x.TeacherId });
|
||||
entity.HasOne(x => x.MakeupExamSession).WithMany(x => x.Invigilators)
|
||||
.HasForeignKey(x => x.MakeupExamSessionId).OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne(x => x.Teacher).WithMany()
|
||||
.HasForeignKey(x => x.TeacherId).OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
builder.Entity<MakeupExamEnrollment>(entity =>
|
||||
{
|
||||
entity.HasKey(x => new { x.MakeupExamSessionId, x.StudentId });
|
||||
entity.HasIndex(x => new { x.StudentId, x.MakeupExamSessionId });
|
||||
entity.Property(x => x.MakeupScore).HasPrecision(5, 1);
|
||||
entity.HasOne(x => x.MakeupExamSession).WithMany(x => x.Enrollments)
|
||||
.HasForeignKey(x => x.MakeupExamSessionId).OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne(x => x.Student).WithMany()
|
||||
.HasForeignKey(x => x.StudentId).OnDelete(DeleteBehavior.Restrict);
|
||||
entity.HasOne(x => x.SourceGradeRecord).WithMany()
|
||||
.HasForeignKey(x => x.SourceGradeRecordId).OnDelete(DeleteBehavior.SetNull);
|
||||
entity.HasOne(x => x.SourceDeferredExam).WithMany()
|
||||
.HasForeignKey(x => x.SourceDeferredExamId).OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
builder.Entity<StudentStatusChange>(entity =>
|
||||
{
|
||||
entity.Property(x => x.Reason).HasMaxLength(1000);
|
||||
|
||||
@@ -298,6 +298,14 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
TeachingEvaluationMigration,
|
||||
evaluationSetupsExist ? [] : TeachingEvaluationStatements,
|
||||
cancellationToken);
|
||||
|
||||
var makeupExamsExist = await db.Database
|
||||
.SqlQueryRaw<int>("SELECT COUNT(*) AS \"Value\" FROM sqlite_master WHERE type = 'table' AND name = 'MakeupExamPlans'")
|
||||
.AnyAsync(value => value > 0, cancellationToken);
|
||||
await ApplyMigrationAsync(
|
||||
MakeupExamsMigration,
|
||||
makeupExamsExist ? [] : MakeupExamStatements,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ApplyMigrationAsync(
|
||||
@@ -1651,4 +1659,20 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"""CREATE UNIQUE INDEX "IX_EvaluationRecords_SetupId_StudentId_TaskId" ON "EvaluationRecords" ("EvaluationSetupId", "StudentId", "TeachingTaskId");""",
|
||||
"""CREATE TABLE "EvaluationScores" ("EvaluationRecordId" TEXT NOT NULL, "EvaluationDimensionId" TEXT NOT NULL, "Score" INTEGER NOT NULL, CONSTRAINT "PK_EvaluationScores" PRIMARY KEY ("EvaluationRecordId", "EvaluationDimensionId"), CONSTRAINT "FK_EvaluationScores_EvaluationRecords_EvaluationRecordId" FOREIGN KEY ("EvaluationRecordId") REFERENCES "EvaluationRecords" ("Id") ON DELETE CASCADE, CONSTRAINT "FK_EvaluationScores_EvaluationDimensions_EvaluationDimensionId" FOREIGN KEY ("EvaluationDimensionId") REFERENCES "EvaluationDimensions" ("Id") ON DELETE RESTRICT);""",
|
||||
];
|
||||
|
||||
private const string MakeupExamsMigration = "MakeupExams";
|
||||
|
||||
private static readonly string[] MakeupExamStatements =
|
||||
[
|
||||
"""CREATE TABLE "MakeupExamPlans" ("Id" TEXT NOT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, "AcademicTermId" TEXT NOT NULL, "Name" TEXT NOT NULL, "Status" INTEGER NOT NULL, "Notes" TEXT NULL, "PublishedAt" TEXT NULL, CONSTRAINT "PK_MakeupExamPlans" PRIMARY KEY ("Id"), CONSTRAINT "FK_MakeupExamPlans_AcademicTerms_AcademicTermId" FOREIGN KEY ("AcademicTermId") REFERENCES "AcademicTerms" ("Id") ON DELETE RESTRICT);""",
|
||||
"""CREATE INDEX "IX_MakeupExamPlans_AcademicTermId_Status" ON "MakeupExamPlans" ("AcademicTermId", "Status");""",
|
||||
"""CREATE TABLE "MakeupExamSessions" ("Id" TEXT NOT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, "MakeupExamPlanId" TEXT NOT NULL, "TeachingTaskId" TEXT NOT NULL, "ClassroomId" TEXT NULL, "ExamDate" TEXT NOT NULL, "StartPeriod" INTEGER NOT NULL, "PeriodCount" INTEGER NOT NULL, "StartsAt" TEXT NOT NULL, "EndsAt" TEXT NOT NULL, "RequiredBuildingId" TEXT NULL, "RequiredInvigilatorCount" INTEGER NOT NULL, "Notes" TEXT NULL, CONSTRAINT "PK_MakeupExamSessions" PRIMARY KEY ("Id"), CONSTRAINT "FK_MakeupExamSessions_MakeupExamPlans_MakeupExamPlanId" FOREIGN KEY ("MakeupExamPlanId") REFERENCES "MakeupExamPlans" ("Id") ON DELETE CASCADE, CONSTRAINT "FK_MakeupExamSessions_TeachingTasks_TeachingTaskId" FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_MakeupExamSessions_Classrooms_ClassroomId" FOREIGN KEY ("ClassroomId") REFERENCES "Classrooms" ("Id") ON DELETE SET NULL, CONSTRAINT "FK_MakeupExamSessions_Buildings_RequiredBuildingId" FOREIGN KEY ("RequiredBuildingId") REFERENCES "Buildings" ("Id") ON DELETE SET NULL);""",
|
||||
"""CREATE INDEX "IX_MakeupExamSessions_MakeupExamPlanId_StartsAt" ON "MakeupExamSessions" ("MakeupExamPlanId", "StartsAt");""",
|
||||
"""CREATE INDEX "IX_MakeupExamSessions_TeachingTaskId" ON "MakeupExamSessions" ("TeachingTaskId");""",
|
||||
"""CREATE INDEX "IX_MakeupExamSessions_MakeupExamPlanId_ExamDate" ON "MakeupExamSessions" ("MakeupExamPlanId", "ExamDate");""",
|
||||
"""CREATE INDEX "IX_MakeupExamSessions_RequiredBuildingId" ON "MakeupExamSessions" ("RequiredBuildingId");""",
|
||||
"""CREATE TABLE "MakeupExamSessionInvigilators" ("MakeupExamSessionId" TEXT NOT NULL, "TeacherId" TEXT NOT NULL, CONSTRAINT "PK_MakeupExamSessionInvigilators" PRIMARY KEY ("MakeupExamSessionId", "TeacherId"), CONSTRAINT "FK_MakeupExamSessionInvigilators_MakeupExamSessions_MakeupExamSessionId" FOREIGN KEY ("MakeupExamSessionId") REFERENCES "MakeupExamSessions" ("Id") ON DELETE CASCADE, CONSTRAINT "FK_MakeupExamSessionInvigilators_Teachers_TeacherId" FOREIGN KEY ("TeacherId") REFERENCES "Teachers" ("Id") ON DELETE RESTRICT);""",
|
||||
"""CREATE TABLE "MakeupExamEnrollments" ("MakeupExamSessionId" TEXT NOT NULL, "StudentId" TEXT NOT NULL, "Reason" INTEGER NOT NULL, "SourceGradeRecordId" TEXT NULL, "SourceDeferredExamId" TEXT NULL, "MakeupScore" TEXT NULL, CONSTRAINT "PK_MakeupExamEnrollments" PRIMARY KEY ("MakeupExamSessionId", "StudentId"), CONSTRAINT "FK_MakeupExamEnrollments_MakeupExamSessions_MakeupExamSessionId" FOREIGN KEY ("MakeupExamSessionId") REFERENCES "MakeupExamSessions" ("Id") ON DELETE CASCADE, CONSTRAINT "FK_MakeupExamEnrollments_Students_StudentId" FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_MakeupExamEnrollments_GradeRecords_SourceGradeRecordId" FOREIGN KEY ("SourceGradeRecordId") REFERENCES "GradeRecords" ("Id") ON DELETE SET NULL, CONSTRAINT "FK_MakeupExamEnrollments_DeferredExams_SourceDeferredExamId" FOREIGN KEY ("SourceDeferredExamId") REFERENCES "DeferredExams" ("Id") ON DELETE SET NULL);""",
|
||||
"""CREATE INDEX "IX_MakeupExamEnrollments_StudentId_MakeupExamSessionId" ON "MakeupExamEnrollments" ("StudentId", "MakeupExamSessionId");""",
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user