We realized that it was impossible to update the image of the home and services carousels: since i am using the add_{type}_carousel() function, it actually added a new slide instead of replacing the old one. We need this one for tomorrow, so i made a workaround creating the new slide, moving the relevant data from the old slide to the new, and then removing the old slide. Yuck.
349 lines
9.9 KiB
Go
349 lines
9.9 KiB
Go
/*
|
|
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
|
* 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)
|
|
}
|