feat: private materials with per-user shares

A material may be private: only its author and the users it is shared
with see it. Every student read (list, detail, course detail and
counters, the checklist linked material, the token twins) goes through
one visibility fragment. Existing materials stay public with no author.

GET/PUT /_bm/api/v1/materials/{id}/access let the author, a material
manager, toggle the flag and pick the users; the first change of an
author-less material makes the caller its author.
This commit is contained in:
Jakub Zych
2026-11-08 11:52:47 +01:00
parent 5ed02ed9f7
commit 418f8240a4
14 changed files with 688 additions and 65 deletions

View File

@@ -23,7 +23,7 @@ func (c courseRow) counts() courseCounts {
}
// coursesWithCounts selects courses with their session, reading and
// published material counts and the caller's read readings.
// visible published material counts and the caller's read readings.
func coursesWithCounts(db *gorm.DB, userID uint, now time.Time) *gorm.DB {
return db.Table("jz_bm_courses AS c").Select(`c.*,
(SELECT COUNT(*) FROM jz_bm_class_sessions s WHERE s.course_id = c.id) AS sessions_count,
@@ -31,7 +31,7 @@ func coursesWithCounts(db *gorm.DB, userID uint, now time.Time) *gorm.DB {
(SELECT COUNT(*) FROM jz_bm_reading_progress p JOIN jz_bm_readings r ON r.id = p.reading_id
WHERE r.course_id = c.id AND p.user_id = ?) AS readings_read,
(SELECT COUNT(*) FROM jz_bm_materials m WHERE m.course_id = c.id
AND m.published_at IS NOT NULL AND m.published_at <= ?) AS materials_count`, userID, now.UTC())
AND m.published_at IS NOT NULL AND m.published_at <= ? AND `+visibleMaterialSQL("m")+`) AS materials_count`, userID, now.UTC(), userID, userID)
}
// Courses answers GET /courses?semester=N: every course in programme order
@@ -103,7 +103,7 @@ func (h *Handlers) Course(w http.ResponseWriter, r *http.Request) {
writeOpaque500(w)
return
}
materials, err := publishedMaterialsOfCourse(db, now, row.ID)
materials, err := publishedMaterialsOfCourse(db, now, userID, row.ID)
if err != nil {
writeOpaque500(w)
return