feat(schedule): 个人日程表页面(#973) - #999
Conversation
- 新增 app/schedule_views.py: mySchedule 视图继承 ProfileTemplateView, 聚合三类事件: 已报名活动(Participation)、书院课表(CourseTime 按周展开)、 地下室预约(get_appoints future),统一渲染到 FullCalendar 月/周视图。 - 新增 templates/schedule/index.html: FullCalendar 初始化、四色图例、 eventRender tooltip 显示类别与地点、eventClick 跳转活动详情。 - templates/user_left_navbar.html: 侧边栏新增“我的日程表”入口。 - app/urls.py: 注册 /schedule/ 路由。 - app/test/test_schedule.py: 8 项测试覆盖三类事件聚合与页面渲染。
- 新增 app/pku_course_parser.py:纯标准库解析门户「我的课表」另存为的 HTML, 定位 id=mon1..sun12 格子,提取课名/周次/单双周/教室/教师,合并连续节次, 并按学期第一周周一对开为日历事件。零凭证、不触碰密码。 - 新增模型 AcademicCourse(app/models.py):存储同学导入的教务课程(结构化, 不含任何门户凭证),含 migration 0017_academic_course。 - 新增上传视图 importCourseTable(app/schedule_views.py):GET 显示表单、 POST 接收 HTML 与学期第一周周一,内存解析后覆盖式写入本人课表,绝不落盘原始文件。 - 日程表页聚合第 4 数据源:_collect_academic_course_events 按周展开为 「教务课表」事件(紫色 #9b59b6)。 - 路由 /importCourseTable/ 与日程表页「导入教务课表」入口、教务课表图例。 - 测试 app/test/test_pku_course.py:解析/展开/上传视图/聚合 共 10 项。 - .gitignore 增加隐私文件与本地测试配置保护。
- 新增模型 UserSchedule(单一模型,category 区分日程/待办, repeat/repeat_end/series_id 支持重复;migration 0018_user_schedule)。 - 重复项在创建时按 repeat_end 实体化为多条独立行并共享 series_id, 每条可单独编辑/删除,「完成」状态各自独立,避免动态展开的状态 bug。 - 日程(SCHEDULE) 作为第5数据源进入 FullCalendar(青绿 #16a085); 待办(TODO) 仅显示在底部「当日待办」面板,按 ?date= 服务端渲染。 - 新增写操作视图 addScheduleItem/editScheduleItem/deleteScheduleItem/ toggleTodo/manageSchedule,统一采用 PRG(写后重定向)且提示随 URL 传递, 归属用 get_object_or_404(person=me) 强制隔离,杜绝越权。 - 日程页:点击日期跳 ?date=、defaultDate 定位、手动日程图例与快速添加表单; 管理页列出本人全部条目并支持删单条/删整系列。 - 测试 app/test/test_manual_schedule.py:重复展开/越界、增/改/删/切换、 越权拒绝(403/404)、当日待办按日期过滤、第5源聚合、管理页隔离,共 17 项。 - 全量回归 251 项 OK;CRLF 合规。
原实现以 NaturalPerson.unsubscribe_list 作为反向黑名单,空名单即订阅全部 组织,导致新账号默认订阅所有小组、通知泛滥。 改为: - 新增 app/org_utils.set_default_subscription/subscribe_org/unsubscribe_org; - 新建账号(注册、批量导入)时把"非学院机构且未加入"的组织写入不订阅名单, otype.allow_unsubscribe=False 的强制订阅机构保持订阅; - 加入/退出小组(ModifyPosition 通过时)与选课成功入组时自动维护订阅状态。 新增 app/test/test_subscription_default.py 覆盖默认订阅范围与订阅/退订辅助函数。
课程活动在发布时按当时选课名单一次性生成 Participation;由于课程活动有发布 提前量,补退选阶段加入的同学不在该快照中,首次课无法扫码签到,需要助教手动 补签、也影响学时统计。 在不改动活动/签到根基的前提下做增量修复: - app/course_utils 新增 backfill_course_participation / remove_course_participation, 在补退选阶段(STAGE2)选课成功时为"已发布且未结束"的课程活动补齐签到记录并 同步活动人数与容量,退选时对称清理; - api/activity/checkin 在找不到 Participation 时,对确为该课程 SUCCESS 名单内的 学生惰性补建记录后再签到,作为兜底。 新增 app/test/test_course_signin_backfill.py 覆盖补选补齐、补选后可签到、退选清理。
FullCalendar requires explicit width and height to render properly. The calendar div now has height: 600px; width: 100% in addition to min-height, ensuring the calendar renders correctly on desktop. This fixes the blank calendar display issue reported by user. Files changed: - templates/schedule/index.html: Add explicit dimensions to #calendar div
The schedule page was missing the standard YPPF page container structure: - <div id="content" class="main-content"> wrapper - <div class="container"> inner container - Warning alert blocks for html_display This caused the FullCalendar widget to render without proper styling context, resulting in a blank/chaotic calendar display on desktop. Also added explicit dimensions to #calendar div (height: 600px; width: 100%) to ensure FullCalendar renders correctly. Files changed: - templates/schedule/index.html: Add proper page structure and calendar dimensions
- index.html: Restructure with proper main-content container and container hierarchy - upload.html: Fix layout to match standard YPPF page structure - manage.html: Fix layout to match standard YPPF page structure All pages now properly render within the right sidebar content area.
修复内容: - test_manual_schedule: _make_person 姓名截断至 10 字符,避免 MySQL DataError - test_course_signin_backfill: fixture 补设 need_checkin=True,匹配上游 Yuanpei-Intelligence#986 - neworganization_show.html: 补全 {% endblock %} 闭合 mainpage block - schedule/index.html: CSS 路径修正(fullcalendar.min.css → fullcalendar.css),移除多余 enctype - schedule_views.py: UserScheduleForm 允许 color/repeat 缺省回落到默认值;加 csrf_protect;课表导入原子化+term 长度校验 - stuinfo.html: 成就徽章空值防御,badge.url 前加 {% if atype.badge %} 判断 - checkin.py: 标注惰性补建在事务外执行的并发风险(留档说明) - check_db_integrity C3: 子查询限定 status=SUCCESS,排除 FAILED/UNSELECT 验证:docker 容器内 mysql 全量测试 483/483 OK,check_db_integrity 0 违规。
- FullCalendar 3.4.0 的 titleFormat 对象触发 toUpperCase 错误,导致 init 中断、日历空白 - 删除 titleFormat / axisFormat,增加 lang:'zh-cn',加载 custom-fullcalendar.advance.css - 为 #calendar 设置 height:650 并移除内联高度,确保 agenda 视图可见 - index/upload/manage 统一使用 statbox widget box box-shadow + widget-header + widget-content-area
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 33d76d4503
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| can_show = me.incharge.filter(otype_id=feedback.org.otype_id) if feedback.org else [] | ||
| if can_show.exists(): |
There was a problem hiding this comment.
Handle feedback without an organization before calling exists
When a teacher opens /feedback/ and any of these querysets contains a feedback whose nullable org is NULL, this branch assigns a plain list to can_show, and the following can_show.exists() raises AttributeError, so the attempted null fix still crashes the entire page. Test feedback.org before constructing/querying can_show, or use an empty queryset; the same defect is repeated in the two loops below.
Useful? React with 👍 / 👎.
| participation, created = Participation.objects.get_or_create( | ||
| activity=activity, person=person, | ||
| defaults={'status': initial}) |
There was a problem hiding this comment.
Serialize lazy course check-in creation
When two first-time check-in requests for the same backfilled course attendee overlap, both can leave the activity-locking transaction after Participation.DoesNotExist and execute this get_or_create() without any (activity, person) uniqueness constraint. Both requests can therefore insert a row; later check-ins use .get() and can raise MultipleObjectsReturned, while participation statistics become corrupted. Keep the eligibility check and creation inside the locked transaction and enforce database uniqueness.
AGENTS.md reference: AGENTS.md:L429-L437
Useful? React with 👍 / 👎.
| participation, created = Participation.objects.get_or_create( | ||
| activity=activity, person=person, | ||
| defaults={'status': initial}) |
There was a problem hiding this comment.
Update activity counters during lazy participation creation
Whenever this fallback actually creates the missing participation, it never increments Activity.current_participants or capacity, unlike backfill_course_participation(). This path exists specifically for historical or missed backfills, so the activity API and attendance statistics will permanently undercount a student who successfully checked in. Create the participation and update both counters atomically.
AGENTS.md reference: AGENTS.md:L451-L454
Useful? React with 👍 / 👎.
| 1: ('08:00', '08:45'), 2: ('08:55', '09:40'), | ||
| 3: ('10:00', '10:45'), 4: ('10:55', '11:40'), |
There was a problem hiding this comment.
Align imported section times with the upload-page contract
Every imported course is assigned times that conflict with the instructions shown to users: the upload page says section 1 is 08:00–08:50 and section 2 is 09:00–09:50, while this table produces 08:00–08:45 and 08:55–09:40 (with similar discrepancies through section 12). Consequently the calendar can show incorrect start and end times even when the user follows the documented standard; either correct this mapping or make the UI state the actual mapping.
Useful? React with 👍 / 👎.
| # 加入小组后自动订阅该小组(从该人不订阅名单中移除) | ||
| if self.org.otype.allow_unsubscribe: | ||
| unsub = self.person.unsubscribe_list | ||
| if unsub.filter(id=self.org.id).exists(): | ||
| unsub.remove(self.org) |
There was a problem hiding this comment.
Subscribe members created outside ModifyPosition
This hook only covers membership created through an accepted ModifyPosition. In accept_modifyorg_submit(), a new organization first adds every active person to org.unsubscribers and then directly creates the responsible person's Position, so that initial joined member remains unsubscribed and misses their own organization's notifications. Route that direct membership creation through the same subscription transition as well.
AGENTS.md reference: AGENTS.md:L445-L450
Useful? React with 👍 / 👎.
|
感谢这个 PR,日程表页和手动日程/待办这部分很有价值。 关于其中「教务课表」的部分,我们把课表做成了一个独立的可插拔模块并已开出 draft PR #1012( 想请你把 #999 收窄一下:保留日程表页(FullCalendar 聚合)与手动日程/待办,去掉 |
聚合书院课程、教务课表、报名活动、地下室预约四类数据源,并支持手动日程/待办管理与门户课表 HTML 导入。修复日历空白(FullCalendar titleFormat 误用)、统一卡片样式、修复 /feedback/ 的 AttributeError。基于 upstream/develop 重建,不涉及其它 issue。