feat: personal pins on materials
A per-user jz_bm_material_pins table and PUT/DELETE /materials/{id}/pin
on the student API and its token twins: 204 for a material the caller
may see, idempotent, 404 otherwise. List rows, course rows and the
detail carry pinned. The plain list lists the caller's pins first
(newest pin first); with q the relevance order stays. A pin writes only
the pins table and never changes the material's revision.
This commit is contained in:
@@ -52,10 +52,12 @@ func preloadMaterial(q *gorm.DB) *gorm.DB {
|
||||
}
|
||||
|
||||
// listMaterials returns one page of the published materials userID may
|
||||
// see. With ids nil the page is newest first. With ids non-nil (search
|
||||
// see. With ids nil the page lists the caller's pinned materials first
|
||||
// (newest pin first), then newest first. With ids non-nil (search
|
||||
// candidates in the engine's rank order) both the count and the page are
|
||||
// also limited to ids and the page follows their order; the ids never widen
|
||||
// what visibleMaterials and the filter allow. An empty ids is an empty page.
|
||||
// also limited to ids and the page follows their order, where pins are only
|
||||
// marked; the ids never widen what visibleMaterials and the filter allow.
|
||||
// An empty ids is an empty page. The count never depends on pins.
|
||||
func listMaterials(db *gorm.DB, now time.Time, userID uint, f materialFilter, ids []uint, page, perPage int) ([]materialListJSON, []models.Material, int64, error) {
|
||||
if ids != nil && len(ids) == 0 {
|
||||
return []materialListJSON{}, nil, 0, nil
|
||||
@@ -72,19 +74,20 @@ func listMaterials(db *gorm.DB, now time.Time, userID uint, f materialFilter, id
|
||||
if ids != nil {
|
||||
rowsQ = rowsQ.Where("jz_bm_materials.id IN ?", ids).Order(rankOrder(ids))
|
||||
} else {
|
||||
rowsQ = rowsQ.Order("jz_bm_materials.published_at DESC, jz_bm_materials.id DESC")
|
||||
rowsQ = rowsQ.Order(pinnedFirstOrder(userID))
|
||||
}
|
||||
var rows []models.Material
|
||||
err := rowsQ.Offset((page - 1) * perPage).Limit(perPage).Find(&rows).Error
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
out, err := materialsJSON(db, rows)
|
||||
out, err := materialsJSON(db, userID, rows)
|
||||
return out, rows, total, err
|
||||
}
|
||||
|
||||
// materialsJSON builds list rows with their file counts.
|
||||
func materialsJSON(db *gorm.DB, rows []models.Material) ([]materialListJSON, error) {
|
||||
// materialsJSON builds list rows with their file counts and userID's own
|
||||
// pins.
|
||||
func materialsJSON(db *gorm.DB, userID uint, rows []models.Material) ([]materialListJSON, error) {
|
||||
ids := make([]string, 0, len(rows))
|
||||
for _, m := range rows {
|
||||
ids = append(ids, strconv.FormatUint(uint64(m.ID), 10))
|
||||
@@ -107,9 +110,15 @@ func materialsJSON(db *gorm.DB, rows []models.Material) ([]materialListJSON, err
|
||||
counts[row.AttachmentID] = row.Count
|
||||
}
|
||||
}
|
||||
pinned, err := pinnedMaterialIDs(db, userID, materialIDs(rows))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]materialListJSON, 0, len(rows))
|
||||
for _, m := range rows {
|
||||
out = append(out, newMaterialListJSON(m, counts[strconv.FormatUint(uint64(m.ID), 10)]))
|
||||
row := newMaterialListJSON(m, counts[strconv.FormatUint(uint64(m.ID), 10)])
|
||||
row.Pinned = pinned[m.ID]
|
||||
out = append(out, row)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -119,7 +128,9 @@ func materialsJSON(db *gorm.DB, rows []models.Material) ([]materialListJSON, err
|
||||
// shape of lagoon.Paginate. With q (2 to 100 characters) the engine answers
|
||||
// candidates, which SQL re-gates, counts and pages in rank order, each row
|
||||
// with at most one snippet; when the engine cannot answer the request is a
|
||||
// 503. Without q the list is newest first and never calls the engine.
|
||||
// 503. Without q the caller's own pins come first (newest pin first), then
|
||||
// newest first, and the engine is never called. With q the order is
|
||||
// relevance and rows only carry pinned.
|
||||
func (h *Handlers) Materials(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := h.caller(w, r)
|
||||
if !ok {
|
||||
@@ -227,7 +238,7 @@ func (h *Handlers) Material(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// loadMaterialDetail builds the student detail of material id as userID
|
||||
// sees it: the published, visible row with its course and tags, its public
|
||||
// files, can_manage and the revision. found is false when the caller cannot
|
||||
// files, can_manage, the revision and the caller's own pin. found is false when the caller cannot
|
||||
// see it. GET /materials/{id} and the author's routes answer with it.
|
||||
func (h *Handlers) loadMaterialDetail(ctx context.Context, db *gorm.DB, userID, id uint) (materialDetailJSON, bool, error) {
|
||||
var m models.Material
|
||||
@@ -259,6 +270,11 @@ func (h *Handlers) loadMaterialDetail(ctx context.Context, db *gorm.DB, userID,
|
||||
out.Files = append(out.Files, fileJSON{ID: f.ID, URL: f.URL(), Name: f.FileName, Size: f.FileSize, Mime: f.ContentType})
|
||||
}
|
||||
out.FilesCount = int64(len(out.Files))
|
||||
pinned, err := pinnedMaterialIDs(db, userID, []uint{m.ID})
|
||||
if err != nil {
|
||||
return materialDetailJSON{}, false, err
|
||||
}
|
||||
out.Pinned = pinned[m.ID]
|
||||
if m.AuthorUserID == nil || *m.AuthorUserID == userID {
|
||||
manager, err := h.managesMaterials(ctx, db, userID)
|
||||
if err != nil {
|
||||
@@ -270,7 +286,8 @@ func (h *Handlers) loadMaterialDetail(ctx context.Context, db *gorm.DB, userID,
|
||||
}
|
||||
|
||||
// publishedMaterialsOfCourse lists a course's published materials that
|
||||
// userID may see, newest first, for /courses/{id}.
|
||||
// userID may see, newest first, for /courses/{id}. Rows carry the caller's
|
||||
// pins but keep this order.
|
||||
func publishedMaterialsOfCourse(db *gorm.DB, now time.Time, userID uint, courseID uint) ([]materialListJSON, error) {
|
||||
var rows []models.Material
|
||||
err := preloadMaterial(visibleMaterials(db, now, userID)).
|
||||
@@ -280,5 +297,5 @@ func publishedMaterialsOfCourse(db *gorm.DB, now time.Time, userID uint, courseI
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return materialsJSON(db, rows)
|
||||
return materialsJSON(db, userID, rows)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user