/* * SPDX-FileCopyrightText: 2023 jordi fita mas * SPDX-License-Identifier: AGPL-3.0-only */ package home import ( "context" "io" "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" ) func (h *AdminHandler) carouselHandler(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) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } case "new": switch r.Method { case http.MethodGet: f := newSlideForm() f.MustRender(w, r, user, company) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } default: id, err := strconv.Atoi(head) if err != nil { http.NotFound(w, r) } f := newSlideForm() 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: deleteSlide(w, r, user, conn, id) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) } 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) } } } }) } type carouselSlide struct { Media string Caption string } func mustCollectCarouselSlides(ctx context.Context, company *auth.Company, conn *database.Conn, loc *locale.Locale) []*carouselSlide { rows, err := conn.Query(ctx, ` select coalesce(i18n.caption, slide.caption) as l10_caption , media.path from home_carousel as slide join media using (media_id) left join home_carousel_i18n as i18n on i18n.media_id = slide.media_id and lang_tag = $1 where media.company_id = $2 `, loc.Language, company.ID) if err != nil { panic(err) } defer rows.Close() var carousel []*carouselSlide for rows.Next() { slide := &carouselSlide{} 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 { carouselSlide ID int Translations []*translation } type translation struct { Language string Endonym string Missing bool } func collectSlideEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*slideEntry, error) { rows, err := conn.Query(ctx, ` select media_id , media.path , caption , array_agg((lang_tag, endonym, not exists (select 1 from home_carousel_i18n as i18n where i18n.media_id = home_carousel.media_id and i18n.lang_tag = language.lang_tag)) order by endonym) from home_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 , caption order by caption `, 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) { f := newSlideForm() editSlide(w, r, user, company, conn, f) } 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, false, func(ctx context.Context) { bytes := f.MustReadAllMedia() if bytes == nil { conn.MustExec(ctx, "select add_home_carousel_slide($1, $2)", f.ID, f.Caption) } else { tx := conn.MustBegin(ctx) defer tx.Rollback(ctx) f.ID = tx.MustGetInt(ctx, "select add_media($1, $2, $3, $4)", company.ID, f.Media.Filename(), f.Media.ContentType, bytes) tx.MustExec(ctx, "select add_home_carousel_slide($1, $2)", f.ID, f.Caption) tx.MustCommit(ctx) } }) } func 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(), "select remove_home_carousel_slide($1)", id) httplib.Redirect(w, r, "/admin/home", http.StatusSeeOther) } type slideForm struct { ID int Media *form.File Caption *form.Input } func newSlideForm() *slideForm { return &slideForm{ Media: &form.File{ Name: "media", MaxSize: 1 << 20, }, 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, ` select caption , media.path from home_carousel join media using (media_id) where media_id = $1 `, 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, mediaRequired bool, act func(ctx context.Context)) { if err := f.Parse(w, r); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } defer f.Close() if err := user.VerifyCSRFToken(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } if !f.Valid(user.Locale, mediaRequired) { if !httplib.IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity) } f.MustRender(w, r, user, company) return } act(r.Context()) httplib.Redirect(w, r, "/admin/home", http.StatusSeeOther) } func (f *slideForm) Parse(w http.ResponseWriter, r *http.Request) error { maxSize := f.Media.MaxSize + 1024 r.Body = http.MaxBytesReader(w, r.Body, maxSize) if err := r.ParseMultipartForm(maxSize); err != nil { return err } f.Caption.FillValue(r) if err := f.Media.FillValue(r); err != nil { return err } return nil } func (f *slideForm) Close() error { return f.Media.Close() } func (f *slideForm) Valid(l *locale.Locale, mediaRequired bool) bool { v := form.NewValidator(l) if f.HasMediaFile() { v.CheckImageFile(f.Media, l.GettextNoop("File must be a valid PNG or JPEG image.")) } else { v.Check(f.Media, !mediaRequired, l.GettextNoop("Slide image can not be empty.")) } return v.AllOK } func (f *slideForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "home/carousel/form.gohtml", f) } func (f *slideForm) HasMediaFile() bool { return f.Media.HasData() } func (f *slideForm) MustReadAllMedia() []byte { if !f.HasMediaFile() { return nil } bytes, err := io.ReadAll(f.Media) if err != nil { panic(err) } return bytes }