package api import ( "net/http" "strconv" "strings" "time" "git.golem15.com/golem15/summercms/modules/lagoon" "git.golem15.com/golem15/summercms/modules/lagoon/attach" "git.golem15.com/golem15/summercms/modules/surf" "git.golem15.com/jakub/sm-bm-plugin/models" "gorm.io/gorm" ) // published scopes a material query to what students may see: a // publication time that has passed. A NULL published_at is a draft. func published(q *gorm.DB, now time.Time) *gorm.DB { return q.Where("jz_bm_materials.published_at IS NOT NULL AND jz_bm_materials.published_at <= ?", now.UTC()) } // materialFilter holds the validated filters of /materials. type materialFilter struct { CourseID uint Tag string Type string } func (f materialFilter) apply(q *gorm.DB) *gorm.DB { if f.CourseID != 0 { q = q.Where("jz_bm_materials.course_id = ?", f.CourseID) } if f.Type != "" { q = q.Where("jz_bm_materials.type = ?", f.Type) } if f.Tag != "" { q = q.Where(`EXISTS (SELECT 1 FROM jz_bm_material_tag mt JOIN jz_bm_tags t ON t.id = mt.tag_id WHERE mt.material_id = jz_bm_materials.id AND t.slug = ?)`, f.Tag) } return q } func preloadMaterial(q *gorm.DB) *gorm.DB { return q.Preload("Course").Preload("Tags", func(db *gorm.DB) *gorm.DB { return db.Order("jz_bm_tags.name") }) } // listMaterials returns one page of published materials, newest first. func listMaterials(db *gorm.DB, now time.Time, f materialFilter, page, perPage int) ([]materialListJSON, int64, error) { q := f.apply(published(db.Model(&models.Material{}), now)) var total int64 if err := q.Count(&total).Error; err != nil { return nil, 0, err } var rows []models.Material err := preloadMaterial(f.apply(published(db, now))). Order("jz_bm_materials.published_at DESC, jz_bm_materials.id DESC"). Offset((page - 1) * perPage).Limit(perPage). Find(&rows).Error if err != nil { return nil, 0, err } out, err := materialsJSON(db, rows) return out, total, err } // materialsJSON builds list rows with their file counts. func materialsJSON(db *gorm.DB, rows []models.Material) ([]materialListJSON, error) { ids := make([]string, 0, len(rows)) for _, m := range rows { ids = append(ids, strconv.FormatUint(uint64(m.ID), 10)) } counts := map[string]int64{} if len(ids) > 0 { var found []struct { AttachmentID string Count int64 } err := db.Model(&attach.File{}). Select("attachment_id, COUNT(*) AS count"). Where("attachment_type = ? AND field = ? AND is_public AND attachment_id IN ?", models.Material{}.MorphName(), models.MaterialFilesField, ids). Group("attachment_id"). Scan(&found).Error if err != nil { return nil, err } for _, row := range found { counts[row.AttachmentID] = row.Count } } out := make([]materialListJSON, 0, len(rows)) for _, m := range rows { out = append(out, newMaterialListJSON(m, counts[strconv.FormatUint(uint64(m.ID), 10)])) } return out, nil } // Materials answers GET /materials?course=&tag=&type=&page=&per_page=: a // page of published materials in the {data, meta} shape of lagoon.Paginate. func (h *Handlers) Materials(w http.ResponseWriter, r *http.Request) { if _, ok := h.caller(w, r); !ok { return } in := query(r, "course", "tag", "type", "page", "per_page") if !h.validate(w, r, in, materialsRules) { return } db, ok := h.db(w, r) if !ok { return } filter := materialFilter{CourseID: uintParam(in, "course"), Tag: strings.TrimSpace(str(in, "tag")), Type: str(in, "type")} page := min(intParam(in, "page", 1), 100000) perPage := intParam(in, "per_page", h.perPage()) rows, total, err := listMaterials(db, h.now(), filter, page, perPage) if err != nil { writeOpaque500(w) return } writeJSONPage(w, lagoon.Paginate(rows, page, perPage, total)) } // Material answers GET /materials/{id}: one published material with its // body, tags and files. A draft or a future publication is a 404. func (h *Handlers) Material(w http.ResponseWriter, r *http.Request) { if _, ok := h.caller(w, r); !ok { return } db, ok := h.db(w, r) if !ok { return } id, ok := surf.IntParam(r, "id") if !ok { writeNotFound(w) return } var m models.Material res := preloadMaterial(published(db, h.now())).Where("jz_bm_materials.id = ?", id).Limit(1).Find(&m) if res.Error != nil { writeOpaque500(w) return } if res.RowsAffected == 0 { writeNotFound(w) return } var files []attach.File err := db.Where("attachment_type = ? AND attachment_id = ? AND field = ?", m.MorphName(), strconv.FormatUint(uint64(m.ID), 10), models.MaterialFilesField). Order("sort_order, id"). Find(&files).Error if err != nil { writeOpaque500(w) return } out := materialDetailJSON{ materialListJSON: newMaterialListJSON(m, int64(len(files))), Body: m.Body, Files: make([]fileJSON, 0, len(files)), } for i := range files { f := &files[i] if !f.Public() { continue } 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)) writeData(w, out) } // publishedMaterialsOfCourse lists a course's published materials, newest // first, for /courses/{id}. func publishedMaterialsOfCourse(db *gorm.DB, now time.Time, courseID uint) ([]materialListJSON, error) { var rows []models.Material err := preloadMaterial(published(db, now)). Where("jz_bm_materials.course_id = ?", courseID). Order("jz_bm_materials.published_at DESC, jz_bm_materials.id DESC"). Find(&rows).Error if err != nil { return nil, err } return materialsJSON(db, rows) }