/* * SPDX-FileCopyrightText: 2023 jordi fita mas * SPDX-License-Identifier: AGPL-3.0-only */ package carousel import ( "context" "fmt" "net/http" "strconv" "github.com/jackc/pgx/v4" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/form" httplib "dev.tandem.ws/tandem/camper/pkg/http" "dev.tandem.ws/tandem/camper/pkg/locale" "dev.tandem.ws/tandem/camper/pkg/template" ) type AdminHandler struct { name string indexHandler IndexHandler locales locale.Locales } type IndexHandler func(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) func NewAdminHandler(name string, indexHandler IndexHandler, locales locale.Locales) *AdminHandler { return &AdminHandler{ name: name, indexHandler: indexHandler, locales: locales, } } func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var head string head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { case "": switch r.Method { case http.MethodPost: addSlide(w, r, user, company, conn, h.name) default: httplib.MethodNotAllowed(w, r, http.MethodPost) } case "new": switch r.Method { case http.MethodGet: f := newSlideForm(h.name) f.MustRender(w, r, user, company) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } case "order": switch r.Method { case http.MethodPost: h.orderSlides(w, r, user, company, conn) default: httplib.MethodNotAllowed(w, r, http.MethodPost) } default: id, err := strconv.Atoi(head) if err != nil { http.NotFound(w, r) return } f := newSlideForm(h.name) if err := f.FillFromDatabase(r.Context(), conn, id); err != nil { if database.ErrorIsNotFound(err) { http.NotFound(w, r) return } panic(err) } var langTag string langTag, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch langTag { case "": switch r.Method { case http.MethodGet: f.MustRender(w, r, user, company) case http.MethodPut: editSlide(w, r, user, company, conn, f) case http.MethodDelete: h.deleteSlide(w, r, user, conn, id) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut, http.MethodDelete) } default: loc, ok := h.locales.Get(langTag) if !ok { http.NotFound(w, r) return } l10n := newSlideL10nForm(f, loc) if err := l10n.FillFromDatabase(r.Context(), conn); err != nil { panic(err) } switch r.Method { case http.MethodGet: l10n.MustRender(w, r, user, company) case http.MethodPut: editSlideL10n(w, r, user, company, conn, l10n) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) } } } }) } func (h *AdminHandler) orderSlides(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := user.VerifyCSRFToken(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } input := r.PostForm["media_id"] if len(input) > 0 { var ids []int for _, s := range input { if id, err := strconv.Atoi(s); err == nil { ids = append(ids, id) } else { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } } if _, err := conn.Exec(r.Context(), fmt.Sprintf("select order_%[1]s_carousel($1)", h.name), ids); err != nil { panic(err) } } h.indexHandler(w, r, user, company, conn) } type Slide struct { Media string Caption string } func MustCollectSlides(ctx context.Context, company *auth.Company, conn *database.Conn, loc *locale.Locale, carouselName string) []*Slide { rows, err := conn.Query(ctx, fmt.Sprintf(` select coalesce(i18n.caption, slide.caption) as l10_caption , media.path from %[1]s_carousel as slide join media using (media_id) left join %[1]s_carousel_i18n as i18n on i18n.media_id = slide.media_id and lang_tag = $1 where media.company_id = $2 order by slide.position, l10_caption `, carouselName), loc.Language, company.ID) if err != nil { panic(err) } defer rows.Close() var carousel []*Slide for rows.Next() { slide := &Slide{} err = rows.Scan(&slide.Caption, &slide.Media) if err != nil { panic(err) } carousel = append(carousel, slide) } if rows.Err() != nil { panic(rows.Err()) } return carousel } type SlideEntry struct { Slide ID int Translations []*Translation } type Translation struct { Language string Endonym string Missing bool } func CollectSlideEntries(ctx context.Context, company *auth.Company, conn *database.Conn, carouselName string) ([]*SlideEntry, error) { rows, err := conn.Query(ctx, fmt.Sprintf(` select media_id , media.path , caption , array_agg((lang_tag, endonym, not exists (select 1 from %[1]s_carousel_i18n as i18n where i18n.media_id = carousel.media_id and i18n.lang_tag = language.lang_tag)) order by endonym) from %[1]s_carousel as carousel join media using (media_id) join company using (company_id) , language where lang_tag <> default_lang_tag and language.selectable and media.company_id = $1 group by media_id , media.path , position , caption order by position, caption `, carouselName), pgx.QueryResultFormats{pgx.BinaryFormatCode}, company.ID) if err != nil { return nil, err } defer rows.Close() var slides []*SlideEntry for rows.Next() { slide := &SlideEntry{} var translations database.RecordArray if err = rows.Scan(&slide.ID, &slide.Media, &slide.Caption, &translations); err != nil { return nil, err } for _, el := range translations.Elements { slide.Translations = append(slide.Translations, &Translation{ el.Fields[0].Get().(string), el.Fields[1].Get().(string), el.Fields[2].Get().(bool), }) } slides = append(slides, slide) } return slides, nil } func addSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, carouselName string) { f := newSlideForm(carouselName) f.process(w, r, user, company, conn, func(ctx context.Context) { conn.MustExec(ctx, fmt.Sprintf("select add_%s_carousel_slide($1, $2)", f.CarouselName), f.Media, f.Caption) }) } func editSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *slideForm) { f.process(w, r, user, company, conn, func(ctx context.Context) { if f.ID == f.Media.Int() { conn.MustExec(ctx, fmt.Sprintf("select add_%s_carousel_slide($1, $2)", f.CarouselName), f.Media, f.Caption) } else { tx := conn.MustBegin(ctx) tx.MustExec(ctx, fmt.Sprintf("select add_%s_carousel_slide($1, $2)", f.CarouselName), f.Media, f.Caption) tx.MustExec(ctx, fmt.Sprintf("update %[1]s_carousel set position = (select position from %[1]s_carousel where media_id = $2) where media_id = $1", f.CarouselName), f.Media, f.ID) tx.MustExec(ctx, fmt.Sprintf("update %s_carousel_i18n set media_id = $1 where media_id = $2", f.CarouselName), f.Media, f.ID) tx.MustExec(ctx, fmt.Sprintf("select remove_%s_carousel_slide($1)", f.CarouselName), f.ID) tx.MustCommit(ctx) } }) } func (h *AdminHandler) deleteSlide(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn, id int) { if err := user.VerifyCSRFToken(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } conn.MustExec(r.Context(), fmt.Sprintf("select remove_%s_carousel_slide($1)", h.name), id) httplib.Redirect(w, r, "/admin/"+h.name, http.StatusSeeOther) } type slideForm struct { CarouselName string ID int Media *form.Media Caption *form.Input } func newSlideForm(carouselName string) *slideForm { return &slideForm{ CarouselName: carouselName, Media: &form.Media{ Input: &form.Input{ Name: "media", }, Label: locale.PgettextNoop("Slide image", "input"), Prompt: locale.PgettextNoop("Set slide image", "action"), }, Caption: &form.Input{ Name: "caption", }, } } func (f *slideForm) FillFromDatabase(ctx context.Context, conn *database.Conn, id int) error { f.ID = id row := conn.QueryRow(ctx, fmt.Sprintf(` select caption , media_id::text from %s_carousel where media_id = $1 `, f.CarouselName), id) return row.Scan(&f.Caption.Val, &f.Media.Val) } func (f *slideForm) process(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, act func(ctx context.Context)) { if err := f.Parse(r); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := user.VerifyCSRFToken(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil { panic(err) } else if !ok { if !httplib.IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity) } f.MustRender(w, r, user, company) return } act(r.Context()) httplib.Redirect(w, r, "/admin/"+f.CarouselName, http.StatusSeeOther) } func (f *slideForm) Parse(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } f.Caption.FillValue(r) f.Media.FillValue(r) return nil } func (f *slideForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) { v := form.NewValidator(l) if v.CheckRequired(f.Media.Input, l.GettextNoop("Slide image can not be empty.")) { if _, err := v.CheckImageMedia(ctx, conn, f.Media.Input, l.GettextNoop("Slide image must be an image media type.")); err != nil { return false, err } } return v.AllOK, nil } func (f *slideForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "carousel/form.gohtml", f) }