Skip to content

feat(schedule): 个人日程表页面(#973) - #999

Open
LumenAurora wants to merge 12 commits into
Yuanpei-Intelligence:developfrom
LumenAurora:fix/issue-973-schedule
Open

feat(schedule): 个人日程表页面(#973)#999
LumenAurora wants to merge 12 commits into
Yuanpei-Intelligence:developfrom
LumenAurora:fix/issue-973-schedule

Conversation

@LumenAurora

Copy link
Copy Markdown
Contributor

聚合书院课程、教务课表、报名活动、地下室预约四类数据源,并支持手动日程/待办管理与门户课表 HTML 导入。修复日历空白(FullCalendar titleFormat 误用)、统一卡片样式、修复 /feedback/ 的 AttributeError。基于 upstream/develop 重建,不涉及其它 issue。

- 新增 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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread feedback/views.py
Comment on lines +465 to 466
can_show = me.incharge.filter(otype_id=feedback.org.otype_id) if feedback.org else []
if can_show.exists():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread api/activity/checkin.py
Comment on lines +85 to +87
participation, created = Participation.objects.get_or_create(
activity=activity, person=person,
defaults={'status': initial})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread api/activity/checkin.py
Comment on lines +85 to +87
participation, created = Participation.objects.get_or_create(
activity=activity, person=person,
defaults={'status': initial})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread app/pku_course_parser.py
Comment on lines +31 to +32
1: ('08:00', '08:45'), 2: ('08:55', '09:40'),
3: ('10:00', '10:45'), 4: ('10:55', '11:40'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread app/models.py
Comment on lines +1441 to +1445
# 加入小组后自动订阅该小组(从该人不订阅名单中移除)
if self.org.otype.allow_unsubscribe:
unsub = self.person.unsubscribe_list
if unsub.filter(id=self.org.id).exists():
unsub.remove(self.org)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@tangmc0210

Copy link
Copy Markdown

感谢这个 PR,日程表页和手动日程/待办这部分很有价值。

关于其中「教务课表」的部分,我们把课表做成了一个独立的可插拔模块并已开出 draft PR #1012pku_account 北大账号绑定 + timetable 课表 / 门户与粘贴导入 / 书院课合并 / ICS + 小程序 API),目的是让课表数据能被学术地图、综测等后续模块复用,也顺带修正了节次时间表(北大校本部是 50 分钟制:08:00–08:50 … 20:40–21:30,#999 里按 45 分钟写的)。app/pku_course_parser.py 的解析思路我们沿用了,谢谢。

想请你把 #999 收窄一下:保留日程表页(FullCalendar 聚合)与手动日程/待办,去掉 AcademicCourse 模型、importCourseTable 和对应迁移,改为读 timetable.servicesweek_view / expand_entries,契约见 #1012timetable/README.md)拿教务课表事件。这样两边不会出现两套课表表。#1012 合并后你 rebase 到 develop 即可,有问题随时在这里讨论。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants