Compare commits
36
Commits
v2.4.2
...
c9947068d1
No files matched your search
@@ -1,96 +1,101 @@
|
||||
一、系统角色
|
||||
超级管理员:系统配置、权限、数据字典、日志
|
||||
教务处:校级教学计划、排课、选课、考试、成绩、学籍管理
|
||||
院系教务员:本院专业、课程、教学任务、成绩审核
|
||||
教师:课表、学生名单、考勤、成绩录入、调停课申请
|
||||
辅导员:学生信息、考勤、学业预警
|
||||
学生:课表、选课、成绩、考试、学籍申请
|
||||
校领导:统计分析和决策看板
|
||||
可选角色:财务人员、宿管人员、用人单位或家长
|
||||
权限建议采用“角色 + 数据范围”模式,例如院系教务员只能管理本学院数据。
|
||||
二、核心功能模块
|
||||
模块 主要功能
|
||||
基础数据 校区、学院、系部、专业、班级、年级、教室、学期、课程性质、成绩等级等
|
||||
用户与权限 用户、角色、菜单权限、操作权限、数据权限、登录日志、操作审计
|
||||
学籍管理 学生档案、照片、班级、学籍状态、转专业、休学、复学、退学、留级、异动审批
|
||||
课程库 课程编号、学分、学时、课程性质、先修课程、课程简介、开课学院
|
||||
培养方案 专业培养方案、课程模块、必修/选修要求、学分要求、版本管理
|
||||
教学任务 学期课程开设、授课教师、教学班、容量、授课周次、合班拆班
|
||||
智能排课 教师、班级、教室、时间冲突检测,手工调整,课表发布
|
||||
选课管理 选课轮次、选课限制、容量、退补选、冲突校验、先修条件、候补名单
|
||||
课表管理 学生课表、教师课表、班级课表、教室课表、周次课表
|
||||
调停课管理 调课、停课、补课、代课申请,审批及消息通知
|
||||
考勤管理 点名、迟到、早退、请假、缺勤,批量导入和统计
|
||||
考试管理 考试安排、考场、监考教师、座位表、缓考、补考、重修考试
|
||||
成绩管理 成绩项目、权重、录入、导入、提交、审核、发布、修改留痕
|
||||
补考与重修 补考资格、报名、重修选课、成绩认定、历史记录
|
||||
教学评价 学生评教、同行评价、督导评价、问卷配置、匿名统计
|
||||
教师管理 教师档案、职称、所属院系、授课资格、教学工作量
|
||||
教室资源 教室容量、类型、设备、占用情况、借用申请
|
||||
毕业审核 学分完成度、必修课程、实践环节、毕业资格、学位资格
|
||||
学业预警 不及格学分、低绩点、缺勤、延毕风险,预警规则与处理记录
|
||||
审批中心 学籍异动、调课、缓考、成绩修改、免修、课程替代等统一审批
|
||||
消息通知 站内消息、公告、待办提醒,可扩展短信、邮件、企业微信
|
||||
统计报表 学生、课程、成绩、通过率、教师工作量、教室利用率等
|
||||
数据导入导出 Excel模板、批量校验、错误报告、导入记录、报表导出
|
||||
系统运维 参数配置、字典管理、任务调度、日志、备份、异常监控
|
||||
# 教务系统功能清单
|
||||
|
||||
三、学生端主要功能
|
||||
查看个人信息和学籍状态
|
||||
查看培养方案及学分完成情况
|
||||
在线选课、退课和候补
|
||||
查看课表、考试安排和教室
|
||||
查看成绩、绩点及成绩单
|
||||
发起缓考、免修、重修、学籍异动等申请
|
||||
查看审批进度和消息通知
|
||||
教学评价
|
||||
查看学业预警和毕业审核进度
|
||||
四、教师端主要功能
|
||||
查看个人课表和教学任务
|
||||
查看教学班学生名单
|
||||
考勤和请假确认
|
||||
平时成绩、考试成绩录入及Excel导入
|
||||
设置成绩构成与权重
|
||||
提交成绩并查看审核状态
|
||||
调课、停课、补课、代课申请
|
||||
查看评教结果和教学工作量
|
||||
导出点名册、成绩单等材料
|
||||
五、关键业务规则
|
||||
系统设计时应重点保证:
|
||||
排课不能造成教师、班级或教室冲突
|
||||
选课需要校验时间冲突、容量、先修课程和选课资格
|
||||
已发布成绩不能直接修改,必须申请、审批并保留修改记录
|
||||
培养方案需要版本化,不能因新方案影响旧年级学生
|
||||
学籍异动必须保留完整历史
|
||||
重要发布操作应支持撤回限制、二次确认和审计
|
||||
Excel导入应先预检,确认无误后再正式写入
|
||||
权限不仅控制菜单,还要控制学院、专业、班级等数据范围
|
||||
六、推荐建设阶段
|
||||
第一阶段:核心可用版
|
||||
组织、用户、权限和基础数据
|
||||
学生、教师、课程、班级、教室管理
|
||||
培养方案
|
||||
教学任务
|
||||
排课与课表
|
||||
学生选课
|
||||
成绩录入、审核和查询
|
||||
基础学籍管理
|
||||
通知、审批、Excel导入导出
|
||||
操作日志
|
||||
第二阶段:完整教务版
|
||||
考试与考场安排
|
||||
补考、缓考、重修
|
||||
考勤与请假
|
||||
调停课
|
||||
教学评价
|
||||
学业预警
|
||||
教师工作量
|
||||
毕业与学位审核
|
||||
综合统计报表
|
||||
第三阶段:智慧教务版
|
||||
智能排课优化
|
||||
移动端或微信端
|
||||
电子成绩单与电子证明
|
||||
数据驾驶舱
|
||||
校园统一身份认证
|
||||
对接财务、一卡通、宿舍、图书馆和教育厅平台
|
||||
> 说明:`[x]` 表示主业务链路已实现;`[-]` 表示已覆盖部分能力,仍需补齐;`[ ]` 表示尚未纳入当前实现。
|
||||
|
||||
## 一、角色与权限
|
||||
|
||||
- [x] 超级管理员:系统配置、权限、数据字典、日志与运维
|
||||
- [x] 教务处:校级培养方案、排课、选课、考试、成绩、学籍管理
|
||||
- [x] 院系教务员:按学院数据范围管理专业、课程、教学任务及成绩审核
|
||||
- [x] 教师:课表、教学班名单、考勤、成绩录入、调停课申请
|
||||
- [x] 辅导员:学生信息、考勤记录与学业预警处理
|
||||
- [x] 学生:课表、选课、成绩、考试、学籍申请与个人档案
|
||||
- [x] 校领导:统计报表、运行看板与决策数据
|
||||
- [ ] 可选角色:财务人员、宿管人员、用人单位或家长
|
||||
- [x] 权限采用“角色 + 数据范围”模式,学院管理员的查询、导出与写入均受学院范围约束
|
||||
|
||||
## 二、核心功能模块
|
||||
|
||||
- [x] 基础数据:组织、校区、学院、专业、班级、教室、学期、课程分类等
|
||||
- [x] 用户与权限:用户、角色、菜单与操作权限、登录日志、操作审计
|
||||
- [x] 学籍管理:学生档案、班级、学籍状态及转专业、休复退留等异动审批
|
||||
- [x] 课程库:编号、学分、学时、课程性质、先修课程、课程简介与开课学院
|
||||
- [x] 培养方案:专业培养方案、课程模块、必修/选修与学分要求、版本管理
|
||||
- [x] 教学任务:开课、授课教师、教学班、容量、授课周次、合班拆班
|
||||
- [x] 智能排课:教师/班级/教室冲突校验、手工调整、发布与可配置优化偏好
|
||||
- [x] 选课管理:选课轮次、资格/容量/时间冲突/先修校验、退补选与候补
|
||||
- [x] 课表管理:学生、教师、班级、教室课表及空教室查询、场地展示
|
||||
- [x] 调停课管理:调课、停课、补课、代课申请、审批和通知
|
||||
- [x] 考勤管理:点名、请假、迟到早退缺勤、二维码签到、导入导出与统计
|
||||
- [x] 考试管理:考试计划、考场、监考教师、座位、签到表、自动排考与发布
|
||||
- [x] 成绩管理:成绩构成/权重、录入、导入、提交、审核、发布、修改留痕与分析
|
||||
- [x] 补考与重修:补考资格、报名、考场安排、成绩录入与历史记录
|
||||
- [x] 教学评价:学生评教、问卷配置、匿名提交、教师查看结果与统计
|
||||
- [x] 教师管理:教师档案、院系归属、授课任务与教学工作量统计
|
||||
- [x] 教室资源:容量、类型、设备、占用、空闲查询与借用预约
|
||||
- [x] 毕业与学位审核:学分完成度、必修课、实践环节、毕业资格和学位资格
|
||||
- [x] 学业预警:规则配置、不及格学分/低绩点/缺勤识别、处理记录与学生确认
|
||||
- [x] 审批中心:统一处理学籍异动、调停课、缓考、成绩修改等申请
|
||||
- [-] 消息通知:站内消息、公告、待办提醒与审批深链;短信、邮件、企业微信尚未接入
|
||||
- [x] 统计报表:学生、课程、成绩、通过率、教师工作量、教室利用率及 Excel 导出
|
||||
- [x] 数据导入导出:Excel 模板、校验、错误反馈、导入记录与报表导出
|
||||
- [x] 系统运维:参数配置、任务调度、审计日志、备份、健康与异常任务监控
|
||||
|
||||
## 三、学生端
|
||||
|
||||
- [x] 查看个人信息和学籍状态
|
||||
- [x] 查看培养方案、课程修读情况、学分完成度和毕业审核结果
|
||||
- [x] 在线选课、退课和候补
|
||||
- [x] 查看个人课表、考试安排、考场与座位
|
||||
- [x] 查看成绩、绩点、成绩明细与成绩单
|
||||
- [x] 发起缓考、重修、学籍异动等申请并查看审批进度
|
||||
- [x] 完成教学评价
|
||||
- [x] 查看学业预警并确认/申诉考勤异常
|
||||
|
||||
## 四、教师端
|
||||
|
||||
- [x] 查看个人课表和教学任务
|
||||
- [x] 查看教学班学生名单
|
||||
- [x] 创建考勤、二维码签到、处理请假与导出统计
|
||||
- [x] 录入平时/考试/实验成绩,支持 Excel 导入与自动保存
|
||||
- [x] 设置成绩构成与权重,提交成绩并查看审核状态
|
||||
- [x] 发起调课、停课、补课、代课申请
|
||||
- [x] 查看评教结果和教学工作量
|
||||
- [x] 导出点名册、成绩单、考试签到表等材料
|
||||
|
||||
## 五、已落实的关键规则
|
||||
|
||||
- [x] 排课校验教师、班级和教室冲突;容量与数据范围属于硬约束
|
||||
- [x] 选课校验时间冲突、容量、先修条件与选课资格,并支持候补
|
||||
- [x] 已发布成绩的修改受审批、审计记录与数据范围控制
|
||||
- [x] 培养方案按版本管理,避免新方案影响历史年级
|
||||
- [x] 学籍异动保留历史并进入审批链路
|
||||
- [x] 重要发布操作支持确认、异步进度/撤回边界和审计
|
||||
- [x] Excel 导入提供模板、字段校验与错误反馈后再落库
|
||||
- [x] 菜单权限之外,服务端同时校验学院等数据范围
|
||||
|
||||
## 六、建议新增功能(按优先级)
|
||||
|
||||
### P0:补齐教学闭环
|
||||
|
||||
- [x] **课程替代、免修与学分认定**:学生线上申请,教务员审批;用可追溯的等价课程规则自动重新计算培养方案和毕业审核结果。
|
||||
- [x] **候补自动递补可解释化**:明确候补队列、递补触发时间、递补失败原因与通知记录,避免人工解释和争议。
|
||||
|
||||
### P1:提升教学质量与服务体验
|
||||
|
||||
- [x] **教学评价闭环**:在现有学生评教之外加入同行/督导评价、改进计划、复评和按学期追踪,避免评价只形成分数。
|
||||
- [x] **学业风险处置工作流**:预警后生成辅导员跟进任务、谈话记录、帮扶措施与复核日期;仅向授权人员可见敏感信息。
|
||||
- [x] **学期校历与关键节点中心**:统一配置选课、成绩、评教、考试、毕业审核等截止时间,自动显示角色待办并在临期提醒。
|
||||
- [x] **教室设备与故障闭环**:场地二维码报修、设备可用状态、维修工单和排课时的不可用自动避让。
|
||||
|
||||
### P2:数据治理与开放能力
|
||||
|
||||
- [ ] **数据质量中心**:发现重复人员、缺失学籍、异常选课、超容量、孤立数据等问题,提供责任人、修复建议和处理进度。
|
||||
- [ ] **集成中心**:为统一身份认证、财务、一卡通、宿舍、图书馆及教育厅提供可配置的同步任务、字段映射、重试和对账日志。
|
||||
- [ ] **隐私与数据留存策略**:敏感字段脱敏、导出水印/审批、访问审计检索和按规则归档或清理。
|
||||
|
||||
## 七、建设节奏
|
||||
|
||||
- [x] 第一阶段:核心可用版(基础数据、权限、培养方案、教学任务、排课课表、选课、成绩、学籍、通知审批、Excel、审计)
|
||||
- [x] 第二阶段:完整教务版(考试、补考重修、考勤、调停课、评教、预警、工作量、毕业学位审核、统计)
|
||||
- [-] 第三阶段:智慧教务版(智能排课优化、移动端、电子成绩单/证明、数据看板、统一身份认证已具备;跨系统集成待完善)
|
||||
@@ -0,0 +1,164 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Jiaowu.Api.Infrastructure.AcademicCalendar;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/academic-calendar")]
|
||||
public sealed class AcademicCalendarController(AppDbContext db, HolidayCnClient holidayCnClient) : ControllerBase
|
||||
{
|
||||
private const string Managers = SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult> GetMyCalendar(Guid? academicTermId, CancellationToken cancellationToken)
|
||||
{
|
||||
var roles = User.FindAll(ClaimTypes.Role)
|
||||
.Select(claim => claim.Value)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var events = db.AcademicCalendarEvents.AsNoTracking().Where(x => x.IsEnabled);
|
||||
if (academicTermId.HasValue)
|
||||
events = events.Where(x => x.AcademicTermId == academicTermId.Value);
|
||||
else
|
||||
events = events.Where(x => x.AcademicTerm!.IsCurrent);
|
||||
|
||||
var items = await events
|
||||
.Include(x => x.AcademicTerm)
|
||||
.OrderBy(x => x.StartsOn)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Ok(items.Where(item => IsForRoles(item.AudienceRoles, roles))
|
||||
.Select(ToResponse));
|
||||
}
|
||||
|
||||
[HttpGet("manage")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> GetAll(Guid? academicTermId, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = db.AcademicCalendarEvents.AsNoTracking().Include(x => x.AcademicTerm).AsQueryable();
|
||||
if (academicTermId.HasValue)
|
||||
query = query.Where(x => x.AcademicTermId == academicTermId.Value);
|
||||
return Ok((await query.OrderBy(x => x.StartsOn).ToListAsync(cancellationToken)).Select(ToResponse));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> Create(AcademicCalendarEventRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var validation = await ValidateAsync(request, cancellationToken);
|
||||
if (validation is not null) return validation;
|
||||
var item = new AcademicCalendarEvent { AcademicTermId = request.AcademicTermId, Title = request.Title.Trim(), Kind = request.Kind, StartsOn = request.StartsOn, EndsOn = request.EndsOn, ReminderDays = request.ReminderDays, AudienceRoles = SerializeRoles(request.AudienceRoles), Description = request.Description?.Trim(), IsEnabled = request.IsEnabled };
|
||||
db.AcademicCalendarEvents.Add(item);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await db.Entry(item).Reference(x => x.AcademicTerm).LoadAsync(cancellationToken);
|
||||
return CreatedAtAction(nameof(GetAll), new { academicTermId = item.AcademicTermId }, ToResponse(item));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> Update(Guid id, AcademicCalendarEventRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var validation = await ValidateAsync(request, cancellationToken);
|
||||
if (validation is not null) return validation;
|
||||
var item = await db.AcademicCalendarEvents.Include(x => x.AcademicTerm).FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (item is null) return NotFound();
|
||||
item.AcademicTermId = request.AcademicTermId; item.Title = request.Title.Trim(); item.Kind = request.Kind; item.StartsOn = request.StartsOn; item.EndsOn = request.EndsOn; item.ReminderDays = request.ReminderDays; item.AudienceRoles = SerializeRoles(request.AudienceRoles); item.Description = request.Description?.Trim(); item.IsEnabled = request.IsEnabled;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
item.AcademicTerm = await db.AcademicTerms.AsNoTracking()
|
||||
.FirstAsync(x => x.Id == item.AcademicTermId, cancellationToken);
|
||||
return Ok(ToResponse(item));
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> Delete(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var item = await db.AcademicCalendarEvents.FindAsync([id], cancellationToken);
|
||||
if (item is null) return NotFound();
|
||||
db.AcademicCalendarEvents.Remove(item);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("holidays/{year:int}")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> PreviewHolidays(int year, CancellationToken cancellationToken)
|
||||
{
|
||||
if (year is < 2000 or > 2100) return ValidationProblem("年份必须在 2000 至 2100 之间。");
|
||||
try
|
||||
{
|
||||
var result = await holidayCnClient.GetYearAsync(year, cancellationToken);
|
||||
return Ok(new { result.Year, result.Papers, Days = result.Days.Select(day => new { day.Name, day.Date, day.IsOffDay }) });
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return Problem("无法获取节假日数据,请检查网络后重试。", statusCode: StatusCodes.Status502BadGateway);
|
||||
}
|
||||
catch (InvalidOperationException exception)
|
||||
{
|
||||
return ValidationProblem(exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("holidays/sync")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> SyncHolidays(HolidaySyncRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.Year is < 2000 or > 2100) return ValidationProblem("年份必须在 2000 至 2100 之间。");
|
||||
if (!await db.AcademicTerms.AnyAsync(item => item.Id == request.AcademicTermId && item.IsEnabled, cancellationToken)) return ValidationProblem("学期不存在或已停用。");
|
||||
HolidayCnYear source;
|
||||
try { source = await holidayCnClient.GetYearAsync(request.Year, cancellationToken); }
|
||||
catch (HttpRequestException) { return Problem("无法获取节假日数据,请检查网络后重试。", statusCode: StatusCodes.Status502BadGateway); }
|
||||
catch (InvalidOperationException exception) { return ValidationProblem(exception.Message); }
|
||||
|
||||
var existing = await db.AcademicCalendarEvents.Where(item => item.AcademicTermId == request.AcademicTermId && item.Source == AcademicCalendarEventSource.HolidayCn && item.SourceYear == request.Year).ToListAsync(cancellationToken);
|
||||
db.AcademicCalendarEvents.RemoveRange(existing);
|
||||
db.AcademicCalendarEvents.AddRange(source.Days.Select(day => new AcademicCalendarEvent
|
||||
{
|
||||
AcademicTermId = request.AcademicTermId,
|
||||
Title = day.Name,
|
||||
Kind = AcademicCalendarEventKind.PublicHoliday,
|
||||
StartsOn = day.Date,
|
||||
ReminderDays = day.IsOffDay ? 3 : 0,
|
||||
Description = day.IsOffDay ? "法定节假日或调休休息日" : "调休上班日",
|
||||
Source = AcademicCalendarEventSource.HolidayCn,
|
||||
SourceYear = request.Year,
|
||||
IsOffDay = day.IsOffDay
|
||||
}));
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return Ok(new { Year = request.Year, SyncedCount = source.Days.Count, ReplacedCount = existing.Count, source.Papers });
|
||||
}
|
||||
|
||||
private async Task<ActionResult?> ValidateAsync(AcademicCalendarEventRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.EndsOn.HasValue && request.EndsOn < request.StartsOn) return ValidationProblem("结束日期不能早于开始日期。");
|
||||
if (!await db.AcademicTerms.AnyAsync(x => x.Id == request.AcademicTermId && x.IsEnabled, cancellationToken)) return ValidationProblem("学期不存在或已停用。");
|
||||
if (request.AudienceRoles.Any(role => !KnownRoles.Contains(role))) return ValidationProblem("包含无效的目标角色。");
|
||||
return null;
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> KnownRoles = [SystemRoles.SuperAdmin, SystemRoles.AcademicAdmin, SystemRoles.CollegeAdmin, SystemRoles.Leader, SystemRoles.Counselor, SystemRoles.Teacher, SystemRoles.Student, SystemRoles.MaintenanceAdmin, SystemRoles.MaintenanceStaff];
|
||||
private static bool IsForRoles(string? audienceRoles, HashSet<string> roles) => string.IsNullOrWhiteSpace(audienceRoles) || audienceRoles.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Any(roles.Contains);
|
||||
private static string? SerializeRoles(IReadOnlyCollection<string> roles) => roles.Count == 0 ? null : string.Join(',', roles.Distinct().Order());
|
||||
private static object ToResponse(AcademicCalendarEvent item) => new { item.Id, item.AcademicTermId, AcademicTermName = item.AcademicTerm?.Name, item.Title, item.Kind, item.StartsOn, item.EndsOn, item.ReminderDays, AudienceRoles = string.IsNullOrWhiteSpace(item.AudienceRoles) ? Array.Empty<string>() : item.AudienceRoles.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), item.Description, item.IsEnabled, item.Source, item.SourceYear, item.IsOffDay };
|
||||
}
|
||||
|
||||
public sealed record AcademicCalendarEventRequest(
|
||||
Guid AcademicTermId,
|
||||
[Required, MaxLength(120)] string Title,
|
||||
AcademicCalendarEventKind Kind,
|
||||
DateOnly StartsOn,
|
||||
DateOnly? EndsOn,
|
||||
[Range(0, 90)] int ReminderDays,
|
||||
IReadOnlyCollection<string> AudienceRoles,
|
||||
[MaxLength(1000)] string? Description,
|
||||
bool IsEnabled = true);
|
||||
|
||||
public sealed record HolidaySyncRequest(Guid AcademicTermId, int Year);
|
||||
@@ -446,6 +446,15 @@ public sealed class AcademicPlanningController(
|
||||
.Select(x => x.CourseId)
|
||||
.Distinct()
|
||||
.ToHashSet();
|
||||
var approvedSubstitutions = await db.CourseSubstitutions.AsNoTracking()
|
||||
.Where(x => x.StudentId == student.Id && x.Status == ApprovalStatus.Approved)
|
||||
.Select(x => new { x.OriginalCourseId, x.SubstituteCourseId })
|
||||
.ToListAsync(cancellationToken);
|
||||
foreach (var substitution in approvedSubstitutions)
|
||||
{
|
||||
if (passedCourseIds.Contains(substitution.SubstituteCourseId))
|
||||
passedCourseIds.Add(substitution.OriginalCourseId);
|
||||
}
|
||||
var failedCourseIds = gradeAttempts
|
||||
.GroupBy(x => x.CourseId)
|
||||
.Where(group => !group.Any(x =>
|
||||
|
||||
Loaded 3 of 103 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user