This is more or less the same as the campsites, as public information goes, but for buildings and other amenities that the camping provides that are not campsites.
340 lines
9.7 KiB
Go
340 lines
9.7 KiB
Go
/*
|
|
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package amenity
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/jackc/pgx/v4"
|
|
|
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
|
"dev.tandem.ws/tandem/camper/pkg/carousel"
|
|
"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, label string) 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.MethodGet:
|
|
serveCarouselIndex(w, r, user, company, conn, label)
|
|
case http.MethodPost:
|
|
addSlide(w, r, user, company, conn, label)
|
|
default:
|
|
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodGet)
|
|
}
|
|
case "new":
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
f := newSlideForm(company, label)
|
|
f.MustRender(w, r, user, company)
|
|
default:
|
|
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
|
}
|
|
case "order":
|
|
switch r.Method {
|
|
case http.MethodPost:
|
|
orderCarousel(w, r, user, company, conn, label)
|
|
default:
|
|
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
|
}
|
|
default:
|
|
mediaID, err := strconv.Atoi(head)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
f := newSlideForm(company, label)
|
|
if err := f.FillFromDatabase(r.Context(), conn, company, mediaID); 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, company, conn, label, mediaID)
|
|
default:
|
|
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut, http.MethodDelete)
|
|
}
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func serveCarouselIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) {
|
|
slides, err := collectSlideEntries(r.Context(), conn, company, label)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
page := &carouselIndex{
|
|
Label: label,
|
|
Slides: slides,
|
|
}
|
|
page.MustRender(w, r, user, company)
|
|
}
|
|
|
|
type carouselIndex struct {
|
|
Label string
|
|
Slides []*carousel.SlideEntry
|
|
}
|
|
|
|
func (page *carouselIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
|
template.MustRenderAdmin(w, r, user, company, "amenity/carousel/index.gohtml", page)
|
|
}
|
|
|
|
func mustCollectSlides(ctx context.Context, conn *database.Conn, company *auth.Company, loc *locale.Locale, label string) []*carousel.Slide {
|
|
rows, err := conn.Query(ctx, `
|
|
select coalesce(i18n.caption, slide.caption) as l10_caption
|
|
, media.path
|
|
from amenity_carousel as slide
|
|
join amenity using (amenity_id)
|
|
join media on media.media_id = slide.media_id
|
|
left join amenity_carousel_i18n as i18n
|
|
on i18n.amenity_id = slide.amenity_id
|
|
and i18n.media_id = slide.media_id
|
|
and lang_tag = $1
|
|
where amenity.label = $2
|
|
and amenity.company_id = $3
|
|
order by slide.position, l10_caption
|
|
`, loc.Language, label, company.ID)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var slides []*carousel.Slide
|
|
for rows.Next() {
|
|
slide := &carousel.Slide{}
|
|
err = rows.Scan(&slide.Caption, &slide.Media)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
slides = append(slides, slide)
|
|
}
|
|
if rows.Err() != nil {
|
|
panic(rows.Err())
|
|
}
|
|
|
|
return slides
|
|
}
|
|
|
|
func collectSlideEntries(ctx context.Context, conn *database.Conn, company *auth.Company, label string) ([]*carousel.SlideEntry, error) {
|
|
rows, err := conn.Query(ctx, `
|
|
select carousel.media_id
|
|
, media.path
|
|
, caption
|
|
from amenity_carousel as carousel
|
|
join amenity using (amenity_id)
|
|
join media on media.media_id = carousel.media_id
|
|
where amenity.label = $1
|
|
and amenity.company_id = $2
|
|
order by carousel.position, caption
|
|
`, label, company.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var slides []*carousel.SlideEntry
|
|
for rows.Next() {
|
|
slide := &carousel.SlideEntry{}
|
|
if err = rows.Scan(&slide.ID, &slide.Media, &slide.Caption); err != nil {
|
|
return nil, err
|
|
}
|
|
slides = append(slides, slide)
|
|
}
|
|
|
|
return slides, nil
|
|
}
|
|
|
|
func addSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) {
|
|
f := newSlideForm(company, label)
|
|
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, tx *database.Tx) error {
|
|
if err := tx.AddAmenityCarouselSlide(ctx, company.ID, f.Label, f.Media.Int(), f.Caption[f.DefaultLang].Val); err != nil {
|
|
return nil
|
|
}
|
|
for lang := range company.Locales {
|
|
l := lang.String()
|
|
if l == f.DefaultLang {
|
|
continue
|
|
}
|
|
if err := tx.TranslateAmenityCarouselSlide(ctx, company.ID, f.Label, f.Media.Int(), lang, f.Caption[l].Val); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func deleteSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string, mediaID int) {
|
|
if err := user.VerifyCSRFToken(r); err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
if err := conn.RemoveAmenityCarouselSlide(r.Context(), company.ID, label, mediaID); err != nil {
|
|
panic(err)
|
|
}
|
|
httplib.Redirect(w, r, "/admin/amenities/"+label+"/slides", http.StatusSeeOther)
|
|
}
|
|
|
|
type slideForm struct {
|
|
DefaultLang string
|
|
Label string
|
|
MediaID int
|
|
Media *form.Media
|
|
Caption form.I18nInput
|
|
}
|
|
|
|
func newSlideForm(company *auth.Company, label string) *slideForm {
|
|
return &slideForm{
|
|
DefaultLang: company.DefaultLanguage.String(),
|
|
Label: label,
|
|
Media: &form.Media{
|
|
Input: &form.Input{
|
|
Name: "media",
|
|
},
|
|
Label: locale.PgettextNoop("Slide image", "input"),
|
|
Prompt: locale.PgettextNoop("Set slide image", "action"),
|
|
},
|
|
Caption: form.NewI18nInput(company.Locales, "caption"),
|
|
}
|
|
}
|
|
|
|
func (f *slideForm) FillFromDatabase(ctx context.Context, conn *database.Conn, company *auth.Company, mediaID int) error {
|
|
f.MediaID = mediaID
|
|
var caption database.RecordArray
|
|
row := conn.QueryRow(ctx, `
|
|
select carousel.caption
|
|
, carousel.media_id::text
|
|
, array_agg((lang_tag, i18n.caption))
|
|
from amenity_carousel as carousel
|
|
left join amenity_carousel_i18n as i18n using (amenity_id, media_id)
|
|
join amenity using (amenity_id)
|
|
where amenity.label = $1
|
|
and amenity.company_id = $2
|
|
and carousel.media_id = $3
|
|
group by carousel.caption
|
|
, carousel.media_id::text
|
|
`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, f.Label, company.ID, mediaID)
|
|
if err := row.Scan(&f.Caption[f.DefaultLang].Val, &f.Media.Val, &caption); err != nil {
|
|
return err
|
|
}
|
|
if err := f.Caption.FillArray(caption); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *slideForm) process(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, act func(ctx context.Context, tx *database.Tx) error) {
|
|
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
|
|
}
|
|
tx := conn.MustBegin(r.Context())
|
|
if err := act(r.Context(), tx); err == nil {
|
|
if err := tx.Commit(r.Context()); err != nil {
|
|
panic(err)
|
|
}
|
|
} else {
|
|
if err := tx.Rollback(r.Context()); err != nil {
|
|
panic(err)
|
|
}
|
|
panic(err)
|
|
}
|
|
httplib.Redirect(w, r, "/admin/amenities/"+f.Label+"/slides", 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, "amenity/carousel/form.gohtml", f)
|
|
}
|
|
|
|
func orderCarousel(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) {
|
|
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.OrderAmenityCarousel(r.Context(), company.ID, label, ids); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
serveCarouselIndex(w, r, user, company, conn, label)
|
|
}
|