I can not reuse the carousel package because these carousels need the campsite site’s slug as a first parameters: i can not have a relation per campsite type, as i do in home and services pages, because the campsite types are added by administration types; even if i had a single relation for slides of home and services pages, these would go in a different relation due to the foreign key to campsite type. What i could reuse, however, is the Slide and SlideEntry types from that package, although i had to export carousel.Translation to be usable from the types package. I should change that to use locale.Translation, but this was the easier option, or i would need to change the queries and templates for carousel package too. Besides that, they work exactly like the slides in home and services pages.
297 lines
7.9 KiB
Go
297 lines
7.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
|
|
locales locale.Locales
|
|
}
|
|
|
|
func NewAdminHandler(name string, locales locale.Locales) *AdminHandler {
|
|
return &AdminHandler{
|
|
name: name,
|
|
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)
|
|
}
|
|
default:
|
|
id, err := strconv.Atoi(head)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
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
|
|
`, 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
|
|
, caption
|
|
order by 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)
|
|
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, conn, func(ctx context.Context) {
|
|
conn.MustExec(ctx, fmt.Sprintf("select add_%s_carousel_slide($1, $2)", f.CarouselName), f.Media, f.Caption)
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|