feat: materials by zjazd in the student API and admin
The material admin form gets the "Na zjazd" picker, every student material JSON carries meeting_id, and GET /materials accepts meeting= inside the published-only scope, so the Pulpit hero can list the zjazd's materials next to its readings.
This commit is contained in:
@@ -148,6 +148,7 @@ func TestQueryValidation(t *testing.T) {
|
||||
{"materials page zero", h.Materials, "/?page=0", "page"},
|
||||
{"materials per_page too large", h.Materials, "/?per_page=1000", "per_page"},
|
||||
{"materials course negative", h.Materials, "/?course=-1", "course"},
|
||||
{"materials meeting zero", h.Materials, "/?meeting=0", "meeting"},
|
||||
{"materials tag too long", h.Materials, "/?tag=" + longTag(), "tag"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
@@ -188,6 +189,7 @@ func TestValidQueriesReachTheDatabase(t *testing.T) {
|
||||
{h.Readings, "/?course=3"},
|
||||
{h.Courses, "/?semester=2"},
|
||||
{h.Materials, "/?course=1&tag=geopolityka&type=pdf&page=2&per_page=10"},
|
||||
{h.Materials, "/?meeting=4&per_page=100"},
|
||||
} {
|
||||
if code, body := serve(t, tc.fn, http.MethodGet, tc.target, user, nil); code != http.StatusServiceUnavailable {
|
||||
t.Errorf("%s = %d %v, want to pass validation (503 without a database)", tc.target, code, body)
|
||||
|
||||
@@ -204,6 +204,7 @@ type materialListJSON struct {
|
||||
ID uint `json:"id"`
|
||||
CourseID *uint `json:"course_id"`
|
||||
Course *courseRef `json:"course"`
|
||||
MeetingID *uint `json:"meeting_id"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
Source *string `json:"source"`
|
||||
@@ -218,6 +219,7 @@ func newMaterialListJSON(m models.Material, files int64) materialListJSON {
|
||||
ID: m.ID,
|
||||
CourseID: m.CourseID,
|
||||
Course: newCourseRef(m.Course),
|
||||
MeetingID: m.MeetingID,
|
||||
Title: m.Title,
|
||||
Type: m.Type,
|
||||
Source: m.Source,
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/jakub/sm-bm-plugin/models"
|
||||
)
|
||||
|
||||
// TestReadingJSONCarriesMeetingID checks that meeting_id is always present
|
||||
@@ -26,3 +28,24 @@ func TestReadingJSONCarriesMeetingID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMaterialJSONCarriesMeetingID checks that meeting_id is always present
|
||||
// in a material, as a number or as null.
|
||||
func TestMaterialJSONCarriesMeetingID(t *testing.T) {
|
||||
id := uint(4)
|
||||
for _, tc := range []struct {
|
||||
meeting *uint
|
||||
want string
|
||||
}{
|
||||
{&id, `"meeting_id":4`},
|
||||
{nil, `"meeting_id":null`},
|
||||
} {
|
||||
b, err := json.Marshal(newMaterialListJSON(models.Material{ID: 1, Title: "Materiał", Type: "pdf", MeetingID: tc.meeting}, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(b), tc.want) {
|
||||
t.Errorf("material JSON %s does not contain %s", b, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,15 +21,19 @@ func published(q *gorm.DB, now time.Time) *gorm.DB {
|
||||
|
||||
// materialFilter holds the validated filters of /materials.
|
||||
type materialFilter struct {
|
||||
CourseID uint
|
||||
Tag string
|
||||
Type string
|
||||
CourseID uint
|
||||
MeetingID 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.MeetingID != 0 {
|
||||
q = q.Where("jz_bm_materials.meeting_id = ?", f.MeetingID)
|
||||
}
|
||||
if f.Type != "" {
|
||||
q = q.Where("jz_bm_materials.type = ?", f.Type)
|
||||
}
|
||||
@@ -94,13 +98,13 @@ func materialsJSON(db *gorm.DB, rows []models.Material) ([]materialListJSON, err
|
||||
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.
|
||||
// Materials answers GET /materials?course=&meeting=&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")
|
||||
in := query(r, "course", "meeting", "tag", "type", "page", "per_page")
|
||||
if !h.validate(w, r, in, materialsRules) {
|
||||
return
|
||||
}
|
||||
@@ -108,7 +112,7 @@ func (h *Handlers) Materials(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
filter := materialFilter{CourseID: uintParam(in, "course"), Tag: strings.TrimSpace(str(in, "tag")), Type: str(in, "type")}
|
||||
filter := materialFilter{CourseID: uintParam(in, "course"), MeetingID: uintParam(in, "meeting"), 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)
|
||||
|
||||
@@ -32,6 +32,7 @@ var (
|
||||
}
|
||||
materialsRules = []lagoon.RequestRule{
|
||||
{Field: "course", Rules: lagoon.ParseRules("nullable|integer|min:1")},
|
||||
{Field: "meeting", Rules: lagoon.ParseRules("nullable|integer|min:1")},
|
||||
{Field: "tag", Rules: lagoon.ParseRules("nullable|string|max:100")},
|
||||
{Field: "type", Rules: append(lagoon.ParseRules("nullable"), lagoon.In(models.MaterialTypes...))},
|
||||
{Field: "page", Rules: lagoon.ParseRules("nullable|integer|min:1")},
|
||||
|
||||
@@ -29,11 +29,12 @@ func (materialsAdmin) RequiredPermissions() []string {
|
||||
return []string{PermMaterials}
|
||||
}
|
||||
|
||||
// AdminFieldRelations binds the course picker and the tags. The files field
|
||||
// AdminFieldRelations binds the course and meeting pickers and the tags. The files field
|
||||
// is bound by models.Material.AttachRelations.
|
||||
func (materialsAdmin) AdminFieldRelations() []cabana.FieldRelationContract {
|
||||
return []cabana.FieldRelationContract{
|
||||
courseRelation("course_id"),
|
||||
meetingRelation(),
|
||||
{
|
||||
Field: "tags",
|
||||
Kind: "belongsToMany",
|
||||
|
||||
Reference in New Issue
Block a user