Contest riêng tư gắn vào lesson, nút Join điền sẵn mật khẩu

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.
Ticket
SP-763
Ảnh và video
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

  1. 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.
  2. 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ũ.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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

Cách dùng

Giảng viên

  1. 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 với 9 loại lesson, loại cuối là Contest
    Menu Add Lesson của Chapter 2 có 9 loại lesson. Contest là loại cuối, có icon cúp.
  2. 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 với tiêu đề Final project: model evaluation showdown và ô Private contest
    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 contest chỉ có một dòng, Final Project Showdown
    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.
  3. 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.

    Edit Contest Lesson của Week 4 challenge: bike demand, gắn với Course Challenge: Bike Demand Forecast
    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

  1. 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.

    Trang lesson của học viên với card contest và nút Join 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 đủ.
  2. 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 với mật khẩu đã điền sẵn
    Hộp thoại Enter contest password. Ô Contest password đã có sẵn mật khẩu, chưa gửi.
  3. 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.

    Trang contest sau khi đăng ký, có badge You're registered, 1 registered và 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 đủ.
  4. 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ĩaVí dụ trong ảnh
Badge phaseContest đang ở giai đoạn nào.Running
Badge You're registeredHọc viên này đã Register. Chỉ hiện sau khi đăng ký.You're registered
Registration opensTừ lúc này học viên được Register.26/09/2026 09:00 AM
StartsContest bắt đầu.28/09/2026 09:00 AM
EndsContest kết thúc.05/10/2026 09:00 AM
Nút Join contestHọ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 contestHọc viên đã Register. Nút mở trang contest.Open contest
Nút ViewPR 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

Card contest với badge Running và nút Join contest
Chưa đăng ký. Chỉ có badge Running và nút Join contest.
Card contest với badge Running, You're registered và nút Open 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 contest editor, công tắc Private contest bị khóa và dòng Linked 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 với badge Đang diễn ra, Bạn đã đăng ký và nút Mở cuộc thi
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 tiếng Việt với ô Cuộc thi riêng tư
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

Trang Contest not found: This contest doesn't exist or has been removed
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à đủ.

Bốn lệnh gọi API của học viên chưa enroll, cả bốn đều trả HTTP 404 Contest not found, và dòng cuối là identical
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 mất enrollment: lesson trả 403, contest và problem vẫn trả 200
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, và lỗi 403 khi gắn contest của người khác
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".
Đổi contest đã gắn sang public bị từ chối với HTTP 409
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 đó.
API lesson chung không có thông tin contest, API lesson contest có khối contest với mật khẩu được che
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

Quyết định đã chọn

Câu hỏiLự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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Sau khi xác nhận production đã chạy v0.0.49, chuyển SP-763 sang Done.