Giảng viên gắn một contest riêng tư vào một lesson. Học viên mở lesson, bấm Join, và trang contest mở ra với mật khẩu đã điền sẵn. Người không mở được lesson chỉ thấy trang Contest not found.
SP-763 · 29/09/2026
Trạng thái tại ngày 29/09/2026
Giai đoạn
Đã release. PR #491 được merge vào main ngày 28/09/2026 và nằm trong tag production v0.0.49. GitHub release v0.0.49 đăng ngày 29/09/2026. Trang này chỉ dựa vào tag. Nó không xác nhận bản đang chạy trên production.
PR
#491 gồm backend (loại lesson CONTEST, migration, API mới, chặn người ngoài), frontend (lesson view cho học viên, form cho giảng viên, dòng Linked lesson trong contest editor) và bản dịch en + vi. Không có PR nào khác trong stack. SP-762 (flexible contest timing, PR #497) được merge vào main trước, rồi gộp vào nhánh này.
Tất cả ảnh và video đến từ local stack với dữ liệu mock. Tên và số không phải dữ liệu thật.
Sau lần chụp đầu
Ngày 28/09/2026, PR đổi hai chỗ. Danh sách chọn contest chỉ còn contest riêng tư do chính giảng viên tạo và chưa gắn vào lesson nào. Contest đã gắn vào lesson thì trả về 404 cho người không mở được lesson đó. Ảnh form, danh sách chọn, lesson đã lưu, trang not found, bản tiếng Việt của form và ảnh API 404 được chụp lại. Ảnh contest editor (Linked lesson), ảnh danh sách linkable và ảnh học viên mất enrollment là ảnh mới. Video chưa quay lại hết. Xem ghi chú dưới video.
Vì sao cần
Contest riêng tư cần mật khẩu. Trong contest editor, giảng viên được nhắc gửi mật khẩu hoặc link mời cho học viên. Học viên phải nhận mật khẩu từ giảng viên rồi tự nhập vào hộp thoại.
Mục tiêu của PR có hai phần. Học viên của một course vào contest ngay từ lesson, không cần gõ mật khẩu. Người ngoài course không đoán được mật khẩu và cũng không biết contest có tồn tại.
Thay đổi, trong một hình
Bước 1
Giảng viên gắn contest
Trong course editor, giảng viên thêm lesson loại Contest và chọn một contest riêng tư của chính mình.
→
Bước 2
Học viên thấy card
Học viên đã enroll mở lesson. Card hiện phase, thời gian và nút Join contest.
→
Bước 3
Join và Register
Nút Join mở trang contest với mật khẩu điền sẵn. Học viên bấm Register.
→
Bước 4
Người ngoài bị chặn
Ai không mở được lesson thì thấy Contest not found, kể cả khi có mật khẩu đúng.
Video demo
Khoảng 2 phút, không có tiếng. Chấm xanh là con chuột.
Trong video
Giây 0 đến 10. Giảng viên mở course editor, mở Chapter 2, mở menu Add Lesson và trỏ vào loại Contest.
Giây 10 đến 30. Form Contest lesson. Giảng viên gõ tiêu đề, mở ô Private contest (có 2 contest riêng tư, contest public không có), chọn Course Challenge: Bike Demand Forecast và gõ description. Dòng gợi ý dưới ô chọn trong đoạn này là chữ cũ.
Giây 30 đến 46. Giảng viên bấm Save. Toast "Contest lesson created successfully" hiện, form đổi thành Edit Contest Lesson, và lesson xuất hiện trong sidebar với icon cúp.
Giây 46 đến 67. Học viên đã enroll mở một reading lesson, rồi bấm lesson mới. Card contest hiện badge Running và ba mốc thời gian. Đoạn giây 60 đến 67 là hình đứng yên, vì trang lesson cuộn trong khung riêng nên lần cuộn đã định không chạy.
Giây 67 đến 84. Học viên bấm Join contest. Trang contest mất khoảng 3 giây để tải, vì demo chạy trên dev server. Học viên bấm Register, hộp thoại hiện với mật khẩu đã điền sẵn, bấm Register lần nữa và thấy You're registered.
Giây 84 đến 100. Học viên cuộn xuống 2 problems rồi lên lại, bấm Back của trình duyệt về lesson. Nút trên card giờ là Open contest.
Giây 100 đến 123,5. Học viên chưa enroll mở thẳng link contest, có mật khẩu đúng trong URL. Trang hiện Contest not found. Bấm All contests thì danh sách chỉ có contest public Open Autumn Cup 2026.
Video và ảnh khác nhau ở đâu
Dòng gợi ý dưới ô Private contest. Video (giây 10 đến 30) còn chữ cũ, bắt đầu bằng "Only your private contests are listed...". Các ảnh form và danh sách chọn bên dưới hiện chữ mới. Chữ mới nói danh sách chỉ có contest riêng tư do bạn tạo và chưa gắn vào lesson khác.
Danh sách chọn trong video. Video hiện 2 contest riêng tư của giảng viên. Với quy tắc mới, lúc đó danh sách cũng ra đúng 2 contest này, vì chưa contest nào được gắn. Phần giảng viên nhờ vậy vẫn đúng, chỉ sai chữ ở dòng gợi ý.
Phần học viên chưa enroll. Đoạn này đã được quay lại sau khi PR đổi, nên khớp với hành vi hiện tại (trang Contest not found). Video cũ, có thông báo từ chối khi bấm Register, không còn dùng.
Mật khẩu. Mật khẩu trong ảnh hộp thoại khác mật khẩu trong video và trong ảnh contest editor, vì dữ liệu demo được dựng lại giữa các lần chụp. Cả hai đều là contest demo trên local.
Cách dùng
Giảng viên
Thêm lesson loại Contest
Mở course editor, mở một chapter và bấm Add Lesson. Chọn Contest, loại cuối cùng trong danh sách. Loại này chỉ hiện khi tenant bật tính năng Contests.
Menu Add Lesson của Chapter 2 có 9 loại lesson. Contest là loại cuối, có icon cúp.
Chọn một contest riêng tư
Gõ tiêu đề rồi chọn contest ở ô Private contest. Ô này bắt buộc. Danh sách chỉ có contest riêng tư do chính bạn tạo và chưa gắn vào lesson khác. Contest ở trạng thái DRAFT vẫn chọn được, và danh sách ghi "(Draft)" bên cạnh.
Form Create New Contest Lesson trong Chapter 1. Dòng nhỏ dưới ô chọn nói: chỉ hiện contest riêng tư do bạn tạo và chưa gắn vào lesson khác, và học viên mở được lesson thì join mà không cần gõ mật khẩu.Danh sách chọn chỉ có Final Project Showdown. Ba contest không có trong danh sách. Course Challenge: Bike Demand Forecast đã gắn vào một lesson khác. Open Autumn Cup 2026 là contest public. Churn Prediction Cup thuộc về một giảng viên khác.
Lưu
Bấm Save. Form đổi thành Edit Contest Lesson và lesson hiện trong sidebar với icon cúp. Khi sửa lại lesson, contest đang gắn vẫn nằm trong danh sách chọn.
Lesson Week 4 challenge: bike demand đã lưu, gắn với Course Challenge: Bike Demand Forecast. Có hai nút Delete và Update. Trong sidebar, lesson nằm dưới Chapter 2 với icon cúp.
Học viên
Mở lesson
Học viên đã enroll mở lesson. Card contest hiện badge Running, ba mốc thời gian và nút Join contest. Bên dưới card là description của contest.
Registration opens 26/09/2026 09:00 AM, Starts 28/09/2026 09:00 AM, Ends 05/10/2026 09:00 AM. Dưới card là phần About the challenge, Rules và Scoring, lấy từ description của contest. Bấm vào ảnh để xem cỡ đầy đủ.
Bấm Join contest, rồi Register
Nút Join contest mở trang contest trong cùng tab. Bấm Register thì hộp thoại mật khẩu hiện ra, và mật khẩu đã nằm sẵn trong ô. Hệ thống không tự gửi. Học viên bấm Register để đăng ký.
Hộp thoại Enter contest password. Ô Contest password đã có sẵn mật khẩu, chưa gửi.
Kiểm tra đã đăng ký
Trang contest hiện badge You're registered, nút ở đầu trang bị khóa và số registered là 1. Phần Problems liệt kê 2 problems.
Ends in 6d 21:06:53, Duration 168h, 1 registered. Hai problem là Capital Bike Share Hourly Demand (RMSE) và Environmental Sound Classification (ESC-50). Ảnh này chụp trước khi gộp SP-762. Chụp kiểm tra lại sau khi gộp cho thấy bố cục giống, chỉ khác đồng hồ đếm. Nút Final standings hiện trên contest đang chạy. Đây là hành vi có từ trước, không thuộc PR này. Bấm vào ảnh để xem cỡ đầy đủ.
Quay lại lesson
Card trong lesson giờ có thêm badge You're registered, và nút đổi từ Join contest sang Open contest. Hai trạng thái nằm cạnh nhau ở phần dưới.
Đọc màn hình
Card contest trong lesson
Phần tử
Ý nghĩa
Ví dụ trong ảnh
Badge phase
Contest đang ở giai đoạn nào.
Running
Badge You're registered
Học viên này đã Register. Chỉ hiện sau khi đăng ký.
You're registered
Registration opens
Từ lúc này học viên được Register.
26/09/2026 09:00 AM
Starts
Contest bắt đầu.
28/09/2026 09:00 AM
Ends
Contest kết thúc.
05/10/2026 09:00 AM
Nút Join contest
Học viên chưa Register. Nút mở trang contest với mật khẩu điền sẵn.
Join contest
Nút Open contest
Học viên đã Register. Nút mở trang contest.
Open contest
Nút View
PR có nhắc tới nút này. Không có ảnh demo nào cho trạng thái này.
Không có ảnh
Hai trạng thái của card
Chưa đăng ký. Chỉ có badge Running và nút Join contest.Đã đăng ký. Thêm badge You're registered, nút đổi thành Open contest.
Contest editor của giảng viên
Mở contest editor của một contest đã gắn vào lesson. Trong card Access, công tắc Private contest bị khóa ở trạng thái bật. Bên dưới có dòng Linked lesson, là link mở thẳng lesson editor của lesson đó.
Card Access của Course Challenge: Bike Demand Forecast. Công tắc Private contest bật và mờ đi, không bấm được. Dòng Linked lesson trỏ tới Applied Machine Learning / Week 4 challenge: bike demand và nói contest phải giữ ở chế độ private, chỉ học viên mở được lesson mới đăng ký được. Hộp Registration password vẫn ở dưới, cùng nút Copy invite link và Regenerate.
Bản tiếng Việt
Cả lesson view và form đều có bản dịch en + vi. Trong ảnh dưới, giờ dùng định dạng 24 giờ.
Lesson view tiếng Việt, học viên đã đăng ký. Badge Đang diễn ra và Bạn đã đăng ký, ba ô MỞ ĐĂNG KÝ, BẮT ĐẦU, KẾT THÚC (09:00 theo 24 giờ), nút Mở cuộc thi.Form "Chỉnh sửa bài học cuộc thi". Dòng gợi ý mới nói danh sách chỉ hiện cuộc thi riêng tư do bạn tạo và chưa liên kết với bài học khác.
Vì sao tin được
Người ngoài lesson không thấy contest. Contest đã gắn vào lesson trả về 404 "Contest not found" cho người không mở được lesson đó. Quy tắc này áp dụng cho mọi trang và API contest của học viên: chi tiết, register, start, problems, submissions, leaderboard và standings. Người chưa đăng nhập luôn nhận 404 với contest đã gắn.
Không phân biệt được với contest không có thật. Trang và phản hồi giống hệt trường hợp contest không tồn tại. Người ngoài không thể thử mật khẩu, và cũng không xác nhận được contest có tồn tại.
Ai vẫn vào được. Học viên đã Register, chủ contest, tenant admin và người mở được lesson (học viên đã enroll, chủ course). Giới hạn học thử của course vẫn áp dụng.
Mật khẩu chỉ đi theo lesson. Với học viên, mật khẩu chỉ nằm trong phản hồi của lesson contest, và API này kiểm tra quyền vào lesson. API lesson chung không trả thông tin contest. Các phản hồi contest khác cho học viên không có mật khẩu.
Chỉ chủ contest được gắn nó. Người gắn phải là người tạo contest. Tenant admin không có ngoại lệ. Contest public không gắn được.
Contest đã gắn giữ chế độ private. Đổi sang public bị từ chối, và công tắc trong editor bị khóa.
Học viên chưa enroll mở thẳng link contest, có mật khẩu đúng trong URL. Trang hiện Contest not found và nút All contests, giống trang của một contest không tồn tại. Bấm vào ảnh để xem cỡ đầy đủ.
Bằng chứng từ API
Các ảnh terminal dưới đây dành cho ai muốn xem bằng chứng kỹ thuật. Đọc caption là đủ.
Học viên chưa enroll gọi bốn lần. Xem chi tiết contest, đăng ký với mật khẩu đúng (đã che), đăng ký với mật khẩu sai, và xem một contest không có thật. Cả bốn đều là HTTP 404 với cùng nội dung. Dòng cuối "identical" cho biết bốn phản hồi khớp từng byte.Học viên đã Register, sau đó enrollment bị đặt thành INACTIVE (chỉ trong database demo). Lesson trả 403 "You must be enrolled in this course to view this lesson". Contest vẫn trả 200 với phase RUNNING, is_registered true và cả hai problem. Trang problem cho can_submit true. Ở cuối, enrollment được đặt lại thành ACTIVE.Danh sách contest có thể gắn của giảng viên có một mục, Final Project Showdown. Khi sửa lesson đã gắn, danh sách có thêm contest hiện tại của lesson đó. Giảng viên thử gắn contest riêng tư của một giảng viên khác thì nhận 403 "You do not have permission to link this contest".Giảng viên đổi contest đã gắn sang public thì nhận 409 "Contest is linked to a lesson and must stay private". Contest vẫn private sau đó.Học viên đã enroll gọi hai API. API lesson chung trả loại CONTEST nhưng không có thông tin contest. API lesson contest trả khối contest với slug, tên, phase RUNNING, thời gian, is_registered true, và mật khẩu đã che thành dấu sao.
Kiểm tra đã làm
Theo PR, backend chạy lint và unit test (1918 test đạt), tầng database (2124 test đạt, chạy không dùng xdist), và migration được chạy thử upgrade, downgrade, upgrade trên database tạm. Frontend: lint không lỗi, 472 test đạt, build đạt. Tôi chưa chạy lại các kiểm tra này khi viết trang.
Trong demo trên local, người làm PR đã đi qua ba vai. Giảng viên gắn contest, học viên đã enroll join và đăng ký, học viên chưa enroll thấy trang not found. Demo cũng bắt được một lỗi: thông báo sai khi đổi contest đã gắn sang public. Lỗi này được sửa trước khi merge.
Giới hạn đã biết
Nhân bản course bỏ link. Khi nhân bản course trong cùng tenant, lesson contest ở bản sao không còn gắn contest. Giảng viên phải gắn lại. Đây là quyết định lúc làm, vì mỗi contest chỉ gắn được một lesson.
Contest DRAFT gắn được. Học viên bấm Join sẽ thấy Contest not found cho đến khi giảng viên publish contest. Danh sách chọn ghi "(Draft)" để giảng viên biết.
Admin không gắn hộ được. Chỉ người tạo contest gắn được nó, kể cả tenant admin. Muốn gắn contest của người khác, chủ contest phải tự làm.
Mật khẩu contest lưu dạng văn bản thường. Mật khẩu này cũng nằm trong Redis cache. Việc này có từ trước PR và nằm ngoài phạm vi. Nó được ghi trên ticket.
Contest riêng tư chưa gắn vào lesson chưa có giới hạn số lần thử mật khẩu. Cũng có từ trước PR, ngoài phạm vi, ghi trên ticket. Contest đã gắn thì người ngoài không thử được, vì họ nhận 404.
Demo không chụp contest FLEXIBLE. Editor có thêm lựa chọn Timing (FIXED hoặc FLEXIBLE) từ SP-762. Không ảnh nào cho thấy lựa chọn này, và không có bằng chứng demo cho contest FLEXIBLE gắn với lesson.
Chưa kiểm tra trên beta hoặc production. Mọi ảnh và video đến từ local stack. Chúng cho thấy code chạy đúng với dữ liệu mock, không cho thấy môi trường thật.
Quyết định đã chọn
Câu hỏi
Lựa chọn
Người ngoài lesson thấy gì?
Cùng lỗi 404 "Contest not found" như contest không có thật. Không dùng 403, vì 403 tiết lộ contest có tồn tại.
Ai gắn được contest vào lesson?
Chỉ người tạo contest. Tenant admin không có ngoại lệ.
Contest nào gắn được?
Contest riêng tư, cùng tenant, chưa gắn vào lesson sống nào. Contest public bị từ chối (422).
Một contest gắn vào bao nhiêu lesson?
Một. Gắn lần hai bị từ chối (409).
Đổi contest đã gắn sang public?
Không được (409). Công tắc trong editor bị khóa.
Mật khẩu có tự gửi khi Join không?
Không. Mật khẩu chỉ được điền sẵn trong hộp thoại, học viên bấm Register.
Nhân bản course cùng tenant?
Bỏ link. Phương án khác là bỏ ràng buộc một lesson cho mỗi contest. Không chọn phương án này.
Lưu trữ (archive) lesson hoặc chapter?
Archive lesson contest, archive chapter chứa nó, hoặc đặt lesson thành ARCHIVED thì contest được thả ra và gắn lại được.
Tenant tắt tính năng Contests?
Các API lesson contest trả 404 và menu Add Lesson không có loại Contest.
Mật khẩu plaintext và giới hạn số lần thử?
Ngoài phạm vi PR. Ghi trên ticket.
Checklist rollout
PR #491 đã merge vào main ngày 28/09/2026, sau SP-762 (PR #497). Tag production v0.0.49 chứa commit này.
Migration: một migration database thêm loại lesson CONTEST và cột contest_id cho lesson. Theo PR, migration đã được chạy thử upgrade, downgrade, upgrade. PR không nêu job định kỳ hay backfill.
Cấu hình: tính năng cần tenant bật Contests. PR không nêu biến cấu hình mới.
Walkthrough trên môi trường thật: chưa có trong các nguồn đã đọc. Nên đi ba bước với course và contest test. Giảng viên tạo lesson Contest. Học viên đã enroll bấm Join rồi Register. Tài khoản chưa enroll mở link contest và phải thấy Contest not found.
Sau khi xác nhận production đã chạy v0.0.49, chuyển SP-763 sang Done.