276 lines
6.9 KiB
Go
276 lines
6.9 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
|
|
"git.loyso.art/frx/kurious/internal/common/errors"
|
|
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
|
"git.loyso.art/frx/kurious/internal/common/xslices"
|
|
"git.loyso.art/frx/kurious/internal/kurious/app/command"
|
|
"git.loyso.art/frx/kurious/internal/kurious/app/query"
|
|
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
|
"git.loyso.art/frx/kurious/internal/kurious/service"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
type courseServer struct {
|
|
app service.Application
|
|
log *slog.Logger
|
|
}
|
|
|
|
type pagination struct {
|
|
nextPageToken string
|
|
perPage int
|
|
}
|
|
|
|
func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
|
query := r.URL.Query()
|
|
out.nextPageToken = query.Get("next")
|
|
|
|
if query.Has("per_page") {
|
|
out.perPage, err = strconv.Atoi(query.Get("per_page"))
|
|
if err != nil {
|
|
return out, errors.NewValidationError("per_page", "bad per_page value")
|
|
}
|
|
} else {
|
|
out.perPage = 50
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error) {
|
|
out.pagination, err = parsePaginationFromQuery(r)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
|
|
vars := mux.Vars(r)
|
|
out.learningType = vars["learning_type"]
|
|
out.courseThematic = vars["thematic_type"]
|
|
|
|
return out, nil
|
|
}
|
|
|
|
type listCoursesParams struct {
|
|
pagination
|
|
|
|
courseThematic string
|
|
learningType string
|
|
}
|
|
|
|
type baseInfo struct {
|
|
ID string
|
|
Name string
|
|
Description string
|
|
}
|
|
|
|
type categoryInfo struct {
|
|
baseInfo
|
|
|
|
Subcategories []subcategoryInfo
|
|
}
|
|
|
|
type subcategoryInfo struct {
|
|
baseInfo
|
|
|
|
Courses []domain.Course
|
|
}
|
|
|
|
type listCoursesTemplateParams struct {
|
|
Categories []categoryInfo
|
|
NextPageToken string
|
|
}
|
|
|
|
func mapDomainCourseToTemplate(in ...domain.Course) listCoursesTemplateParams {
|
|
coursesBySubcategory := make(map[string][]domain.Course, len(in))
|
|
subcategoriesByCategories := make(map[string]map[string]struct{}, len(in))
|
|
xslices.ForEach(in, func(c domain.Course) {
|
|
coursesBySubcategory[c.Thematic] = append(coursesBySubcategory[c.Thematic], c)
|
|
if _, ok := subcategoriesByCategories[c.LearningType]; !ok {
|
|
subcategoriesByCategories[c.LearningType] = map[string]struct{}{}
|
|
}
|
|
subcategoriesByCategories[c.LearningType][c.Thematic] = struct{}{}
|
|
})
|
|
|
|
var out listCoursesTemplateParams
|
|
for category, subcategoryMap := range subcategoriesByCategories {
|
|
outCategory := categoryInfo{}
|
|
outCategory.ID = category
|
|
outCategory.Name = category
|
|
outCategory.Description = ""
|
|
|
|
for subcategory := range subcategoryMap {
|
|
outSubCategory := subcategoryInfo{
|
|
Courses: coursesBySubcategory[subcategory],
|
|
}
|
|
outSubCategory.ID = subcategory
|
|
outSubCategory.Name = subcategory
|
|
outSubCategory.Description = ""
|
|
|
|
outCategory.Subcategories = append(outCategory.Subcategories, outSubCategory)
|
|
}
|
|
sort.Slice(outCategory.Subcategories, func(i, j int) bool {
|
|
return outCategory.Subcategories[i].ID < outCategory.Subcategories[j].ID
|
|
})
|
|
out.Categories = append(out.Categories, outCategory)
|
|
}
|
|
sort.Slice(out.Categories, func(i, j int) bool {
|
|
return out.Categories[i].ID < out.Categories[j].ID
|
|
})
|
|
|
|
return out
|
|
}
|
|
|
|
func (c courseServer) List(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
params, err := parseListCoursesParams(r)
|
|
if handleError(ctx, err, w, c.log, "unable to parse list courses params") {
|
|
return
|
|
}
|
|
|
|
result, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
|
|
CourseThematic: params.courseThematic,
|
|
LearningType: params.learningType,
|
|
Limit: params.perPage,
|
|
NextPageToken: params.nextPageToken,
|
|
})
|
|
if handleError(ctx, err, w, c.log, "unable to list courses") {
|
|
return
|
|
}
|
|
|
|
courses := result.Courses
|
|
templateCourses := mapDomainCourseToTemplate(courses...)
|
|
templateCourses.NextPageToken = result.NextPageToken
|
|
|
|
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "courses", templateCourses)
|
|
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
|
return
|
|
}
|
|
}
|
|
|
|
func (c courseServer) Get(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
id := mux.Vars(r)["course_id"]
|
|
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
|
|
ID: id,
|
|
})
|
|
if handleError(ctx, err, w, c.log, "unable to get course") {
|
|
return
|
|
}
|
|
|
|
payload, err := json.MarshalIndent(course, "", " ")
|
|
if handleError(ctx, err, w, c.log, "unable to marshal json") {
|
|
return
|
|
}
|
|
w.Header().Set("content-type", "application/json")
|
|
w.Header().Set("content-length", strconv.Itoa(len(payload)))
|
|
|
|
_, err = w.Write([]byte(payload))
|
|
if err != nil {
|
|
xcontext.LogWithWarnError(ctx, c.log, err, "unable to write a message")
|
|
}
|
|
}
|
|
|
|
func (c courseServer) GetShort(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
id := mux.Vars(r)["course_id"]
|
|
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
|
|
ID: id,
|
|
})
|
|
if handleError(ctx, err, w, c.log, "unable to get course") {
|
|
return
|
|
}
|
|
|
|
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "course_info", course)
|
|
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
|
return
|
|
}
|
|
}
|
|
|
|
func (c courseServer) RenderEditDescription(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
id := mux.Vars(r)["course_id"]
|
|
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
|
|
ID: id,
|
|
})
|
|
if handleError(ctx, err, w, c.log, "unable to get course") {
|
|
return
|
|
}
|
|
|
|
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "edit_description", course)
|
|
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
|
return
|
|
}
|
|
}
|
|
|
|
func (c courseServer) UpdateCourseDescription(w http.ResponseWriter, r *http.Request) {
|
|
type requestModel struct {
|
|
ID string `json:"-"`
|
|
Text string `json:"description"`
|
|
}
|
|
|
|
ctx := r.Context()
|
|
|
|
var req requestModel
|
|
req.ID = mux.Vars(r)["course_id"]
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if handleError(ctx, err, w, c.log, "unable to read body") {
|
|
return
|
|
}
|
|
|
|
err = c.app.Commands.UpdateCourseDescription.Handle(ctx, command.UpdateCourseDescription{
|
|
ID: req.ID,
|
|
Description: req.Text,
|
|
})
|
|
if handleError(ctx, err, w, c.log, "unable to update course description") {
|
|
return
|
|
}
|
|
|
|
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
|
|
ID: req.ID,
|
|
})
|
|
if handleError(ctx, err, w, c.log, "unable to get course") {
|
|
return
|
|
}
|
|
|
|
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "course_info", course)
|
|
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
|
return
|
|
}
|
|
}
|
|
|
|
func (c courseServer) UdpateDescription(w http.ResponseWriter, r *http.Request) {
|
|
type requestModel struct {
|
|
ID string `json:"id"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
ctx := r.Context()
|
|
|
|
var req requestModel
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if handleError(ctx, err, w, c.log, "unable to read body") {
|
|
return
|
|
}
|
|
|
|
err = c.app.Commands.UpdateCourseDescription.Handle(ctx, command.UpdateCourseDescription{
|
|
ID: req.ID,
|
|
Description: req.Text,
|
|
})
|
|
if handleError(ctx, err, w, c.log, "unable to update course description") {
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|