diff --git a/demo/demo.sql b/demo/demo.sql index f24db0f..39dd4ad 100644 --- a/demo/demo.sql +++ b/demo/demo.sql @@ -29,6 +29,34 @@ values (52, 'plots.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/plo , (52, 'safari_tents.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/safari_tents.avif]])', 'base64')) , (52, 'bungalows.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/bungalows.avif]])', 'base64')) , (52, 'wooden_lodges.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/wooden_lodges.avif]])', 'base64')) + , (52, 'home_carousel0.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel0.jpg]])', 'base64')) + , (52, 'home_carousel1.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel1.jpg]])', 'base64')) + , (52, 'home_carousel2.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel2.jpg]])', 'base64')) + , (52, 'home_carousel3.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel3.jpg]])', 'base64')) + , (52, 'home_carousel4.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel4.jpg]])', 'base64')) + , (52, 'home_carousel5.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel5.jpg]])', 'base64')) + , (52, 'home_carousel6.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel6.jpg]])', 'base64')) + , (52, 'home_carousel7.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel7.jpg]])', 'base64')) + , (52, 'home_carousel8.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel8.jpg]])', 'base64')) +; + +insert into home_carousel (media_id, caption) +values (66, 'Volcà de Santa Margarida') + , (67, 'Gorga fosca Sadernes') + , (68, 'Castellfollit de la Roca') + , (69, 'Besalú') + , (70, 'Santa Pau') + , (71, 'Banyoles') + , (72, 'Girona') + , (73, 'Costa Brava') + , (74, 'Barcelona') +; + +insert into home_carousel_i18n (media_id, lang_tag, caption) +values (66, 'en', 'Santa Margarida volcano') + , (66, 'es', 'Volcán de Santa Margarida') + , (67, 'en', 'Sadernes dark gorge') + , (67, 'es', 'Piletón oscuro Sadernes') ; alter sequence campsite_type_campsite_type_id_seq restart with 72; diff --git a/web/static/images/Volca_de_Santa_Margarida.jpg b/demo/home_carousel0.jpg similarity index 100% rename from web/static/images/Volca_de_Santa_Margarida.jpg rename to demo/home_carousel0.jpg diff --git a/web/static/images/Gorga_fosca_Sadernes.jpg b/demo/home_carousel1.jpg similarity index 100% rename from web/static/images/Gorga_fosca_Sadernes.jpg rename to demo/home_carousel1.jpg diff --git a/web/static/images/castellfolit_de_la_roca.jpg b/demo/home_carousel2.jpg similarity index 100% rename from web/static/images/castellfolit_de_la_roca.jpg rename to demo/home_carousel2.jpg diff --git a/web/static/images/besalu.jpg b/demo/home_carousel3.jpg similarity index 100% rename from web/static/images/besalu.jpg rename to demo/home_carousel3.jpg diff --git a/web/static/images/santa_pau.jpg b/demo/home_carousel4.jpg similarity index 100% rename from web/static/images/santa_pau.jpg rename to demo/home_carousel4.jpg diff --git a/web/static/images/banyoles.jpg b/demo/home_carousel5.jpg similarity index 100% rename from web/static/images/banyoles.jpg rename to demo/home_carousel5.jpg diff --git a/web/static/images/girn-a.jpg b/demo/home_carousel6.jpg similarity index 100% rename from web/static/images/girn-a.jpg rename to demo/home_carousel6.jpg diff --git a/web/static/images/costa_brava.jpg b/demo/home_carousel7.jpg similarity index 100% rename from web/static/images/costa_brava.jpg rename to demo/home_carousel7.jpg diff --git a/web/static/images/barcelona-1.jpg b/demo/home_carousel8.jpg similarity index 100% rename from web/static/images/barcelona-1.jpg rename to demo/home_carousel8.jpg diff --git a/deploy/add_home_carousel_slide.sql b/deploy/add_home_carousel_slide.sql new file mode 100644 index 0000000..d371489 --- /dev/null +++ b/deploy/add_home_carousel_slide.sql @@ -0,0 +1,25 @@ +-- Deploy camper:add_home_carousel_slide to pg +-- requires: roles +-- requires: schema_camper +-- requires: home_carousel + +begin; + +set search_path to camper, public; + +create or replace function add_home_carousel_slide(media_id integer, caption text) returns integer as +$$ + insert into home_carousel (media_id, caption) + values (media_id, coalesce(caption, '')) + on conflict (media_id) do update + set caption = excluded.caption + returning media_id + ; +$$ + language sql +; + +revoke execute on function add_home_carousel_slide(integer, text) from public; +grant execute on function add_home_carousel_slide(integer, text) to admin; + +commit; diff --git a/deploy/home_carousel.sql b/deploy/home_carousel.sql new file mode 100644 index 0000000..24bc6c5 --- /dev/null +++ b/deploy/home_carousel.sql @@ -0,0 +1,53 @@ +-- Deploy camper:home_carousel to pg +-- requires: roles +-- requires: schema_public +-- requires: company +-- requires: media +-- requires: user_profile + +begin; + +set search_path to camper, public; + +create table home_carousel ( + media_id integer not null references media primary key, + caption text not null +); + +grant select on table home_carousel to guest; +grant select on table home_carousel to employee; +grant select, insert, update, delete on table home_carousel to admin; + +alter table home_carousel enable row level security; + +create policy guest_ok +on home_carousel +for select +using (true) +; + +create policy insert_to_company +on home_carousel +for insert +with check ( + exists (select 1 from media join user_profile using (company_id) where media.media_id = home_carousel.media_id) +) +; + +create policy update_company +on home_carousel +for update +using ( + exists (select 1 from media join user_profile using (company_id) where media.media_id = home_carousel.media_id) +) +; + +create policy delete_from_company +on home_carousel +for delete +using ( + exists (select 1 from media join user_profile using (company_id) where media.media_id = home_carousel.media_id) +) +; + +commit; diff --git a/deploy/home_carousel_i18n.sql b/deploy/home_carousel_i18n.sql new file mode 100644 index 0000000..ca1eb57 --- /dev/null +++ b/deploy/home_carousel_i18n.sql @@ -0,0 +1,22 @@ +-- Deploy camper:home_carousel_i18n to pg +-- requires: roles +-- requires: schema_camper +-- requires: home_carousel +-- requires: language + +begin; + +set search_path to camper, public; + +create table home_carousel_i18n ( + media_id integer not null references home_carousel, + lang_tag text not null references language, + caption text not null, + primary key (media_id, lang_tag) +); + +grant select on table home_carousel_i18n to guest; +grant select on table home_carousel_i18n to employee; +grant select, insert, update, delete on table home_carousel_i18n to admin; + +commit; diff --git a/deploy/media_path.sql b/deploy/media_path.sql new file mode 100644 index 0000000..d506d4c --- /dev/null +++ b/deploy/media_path.sql @@ -0,0 +1,23 @@ +-- Deploy camper:media_path to pg +-- requires: roles +-- requires: schema_camper +-- requires: media + +begin; + +set search_path to camper, public; + +create or replace function path(media) returns text as +$$ + select '/media/' || encode($1.hash, 'hex') || '/' || $1.original_filename; +$$ + language sql + stable +; + +revoke execute on function path(media) from public; +grant execute on function path(media) to guest; +grant execute on function path(media) to employee; +grant execute on function path(media) to admin; + +commit; diff --git a/deploy/remove_home_carousel_slide.sql b/deploy/remove_home_carousel_slide.sql new file mode 100644 index 0000000..6557add --- /dev/null +++ b/deploy/remove_home_carousel_slide.sql @@ -0,0 +1,22 @@ +-- Deploy camper:remove_home_carousel_slide to pg +-- requires: roles +-- requires: schema_camper +-- requires: home_carousel +-- requires: home_carousel_i18n + +begin; + +set search_path to camper, public; + +create or replace function remove_home_carousel_slide (media_id integer) returns void as +$$ + delete from home_carousel_i18n where media_id = $1; + delete from home_carousel where media_id = $1; +$$ + language sql +; + +revoke execute on function remove_home_carousel_slide (integer) from public; +grant execute on function remove_home_carousel_slide (integer) to admin; + +commit; diff --git a/deploy/translate_home_carousel_slide.sql b/deploy/translate_home_carousel_slide.sql new file mode 100644 index 0000000..6476366 --- /dev/null +++ b/deploy/translate_home_carousel_slide.sql @@ -0,0 +1,23 @@ +-- Deploy camper:translate_home_carousel_slide to pg +-- requires: roles +-- requires: schema_camper +-- requires: home_carousel_i18n + +begin; + +set search_path to camper, public; + +create or replace function translate_home_carousel_slide (media_id integer, lang_tag text, caption text) returns void as +$$ + insert into home_carousel_i18n (media_id, lang_tag, caption) + values (media_id, lang_tag, coalesce(caption, '')) + on conflict (media_id, lang_tag) do update + set caption = excluded.caption +$$ + language sql +; + +revoke execute on function translate_home_carousel_slide(integer, text, text) from public; +grant execute on function translate_home_carousel_slide(integer, text, text) to admin; + +commit; diff --git a/pkg/app/admin.go b/pkg/app/admin.go index 6847549..0485027 100644 --- a/pkg/app/admin.go +++ b/pkg/app/admin.go @@ -12,6 +12,7 @@ import ( "dev.tandem.ws/tandem/camper/pkg/campsite" "dev.tandem.ws/tandem/camper/pkg/company" "dev.tandem.ws/tandem/camper/pkg/database" + "dev.tandem.ws/tandem/camper/pkg/home" httplib "dev.tandem.ws/tandem/camper/pkg/http" "dev.tandem.ws/tandem/camper/pkg/locale" "dev.tandem.ws/tandem/camper/pkg/season" @@ -21,6 +22,7 @@ import ( type adminHandler struct { campsite *campsite.AdminHandler company *company.AdminHandler + home *home.AdminHandler season *season.AdminHandler } @@ -28,6 +30,7 @@ func newAdminHandler(locales locale.Locales) *adminHandler { return &adminHandler{ campsite: campsite.NewAdminHandler(locales), company: company.NewAdminHandler(), + home: home.NewAdminHandler(locales), season: season.NewAdminHandler(), } } @@ -52,6 +55,8 @@ func (h *adminHandler) Handle(user *auth.User, company *auth.Company, conn *data h.campsite.Handler(user, company, conn).ServeHTTP(w, r) case "company": h.company.Handler(user, company, conn).ServeHTTP(w, r) + case "home": + h.home.Handler(user, company, conn).ServeHTTP(w, r) case "seasons": h.season.Handler(user, company, conn).ServeHTTP(w, r) case "": diff --git a/pkg/app/public.go b/pkg/app/public.go index 25ed24a..89d61a2 100644 --- a/pkg/app/public.go +++ b/pkg/app/public.go @@ -6,23 +6,23 @@ package app import ( - "context" "net/http" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/campsite" "dev.tandem.ws/tandem/camper/pkg/database" + "dev.tandem.ws/tandem/camper/pkg/home" httplib "dev.tandem.ws/tandem/camper/pkg/http" - "dev.tandem.ws/tandem/camper/pkg/locale" - "dev.tandem.ws/tandem/camper/pkg/template" ) type publicHandler struct { + home *home.PublicHandler campsite *campsite.PublicHandler } func newPublicHandler() *publicHandler { return &publicHandler{ + home: home.NewPublicHandler(), campsite: campsite.NewPublicHandler(), } } @@ -33,8 +33,7 @@ func (h *publicHandler) Handler(user *auth.User, company *auth.Company, conn *da head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { case "": - home := newHomePage() - home.MustRender(w, r, user, company, conn) + h.home.Handler(user, company, conn).ServeHTTP(w, r) case "campsites": h.campsite.Handler(user, company, conn).ServeHTTP(w, r) default: @@ -42,58 +41,3 @@ func (h *publicHandler) Handler(user *auth.User, company *auth.Company, conn *da } }) } - -type homePage struct { - *template.PublicPage - CampsiteTypes []*campsiteType -} - -func newHomePage() *homePage { - return &homePage{template.NewPublicPage(), nil} -} - -func (p *homePage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { - p.Setup(r, user, company, conn) - p.CampsiteTypes = mustCollectCampsiteTypes(r.Context(), company, conn, user.Locale) - template.MustRenderPublic(w, r, user, company, "home.gohtml", p) -} - -type campsiteType struct { - Label string - HRef string - Media string -} - -func mustCollectCampsiteTypes(ctx context.Context, company *auth.Company, conn *database.Conn, loc *locale.Locale) []*campsiteType { - rows, err := conn.Query(ctx, ` - select coalesce(i18n.name, campsite_type.name) as l10_name - , '/campsites/types/' || slug - , '/media/' || encode(hash, 'hex') || '/' || original_filename - from campsite_type - left join campsite_type_i18n as i18n on campsite_type.campsite_type_id = i18n.campsite_type_id and lang_tag = $1 - join media using (media_id) - where campsite_type.company_id = $2 - and campsite_type.active - `, loc.Language, company.ID) - if err != nil { - panic(err) - } - defer rows.Close() - - localePath := "/" + loc.Language.String() - var items []*campsiteType - for rows.Next() { - item := &campsiteType{} - err = rows.Scan(&item.Label, &item.HRef, &item.Media) - if err != nil { - panic(err) - } - item.HRef = localePath + item.HRef - items = append(items, item) - } - if rows.Err() != nil { - panic(rows.Err()) - } - - return items -} diff --git a/pkg/campsite/types/admin.go b/pkg/campsite/types/admin.go index e1a64da..d2c2eb7 100644 --- a/pkg/campsite/types/admin.go +++ b/pkg/campsite/types/admin.go @@ -11,7 +11,6 @@ import ( "net/http" "github.com/jackc/pgx/v4" - "golang.org/x/text/language" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/database" @@ -66,12 +65,12 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat } panic(err) } - h.TypeHandler(user, company, conn, f).ServeHTTP(w, r) + h.typeHandler(user, company, conn, f).ServeHTTP(w, r) } }) } -func (h *AdminHandler) TypeHandler(user *auth.User, company *auth.Company, conn *database.Conn, f *typeForm) http.Handler { +func (h *AdminHandler) typeHandler(user *auth.User, company *auth.Company, conn *database.Conn, f *typeForm) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var head string head, r.URL.Path = httplib.ShiftPath(r.URL.Path) @@ -87,18 +86,13 @@ func (h *AdminHandler) TypeHandler(user *auth.User, company *auth.Company, conn httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) } default: - tag, err := language.Parse(head) - if err != nil { - http.NotFound(w, r) - return - } - loc, ok := h.locales[tag] + loc, ok := h.locales.Get(head) if !ok { http.NotFound(w, r) return } l10n := newTypeL10nForm(f, loc) - if err = l10n.FillFromDatabase(r.Context(), conn); err != nil { + if err := l10n.FillFromDatabase(r.Context(), conn); err != nil { panic(err) } switch r.Method { @@ -267,7 +261,7 @@ func (f *typeForm) FillFromDatabase(ctx context.Context, conn *database.Conn, sl row := conn.QueryRow(ctx, ` select name , description - , encode(hash, 'hex') + , media.path , active from campsite_type join media using (media_id) diff --git a/pkg/campsite/types/l10n.go b/pkg/campsite/types/l10n.go index 5c43ea4..1528da8 100644 --- a/pkg/campsite/types/l10n.go +++ b/pkg/campsite/types/l10n.go @@ -17,34 +17,19 @@ import ( "dev.tandem.ws/tandem/camper/pkg/template" ) -type L10nInput struct { - form.Input - Source string -} - type typeL10nForm struct { Locale *locale.Locale Slug string - Name *L10nInput - Description *L10nInput + Name *form.L10nInput + Description *form.L10nInput } func newTypeL10nForm(f *typeForm, loc *locale.Locale) *typeL10nForm { return &typeL10nForm{ - Locale: loc, - Slug: f.Slug, - Name: &L10nInput{ - Input: form.Input{ - Name: f.Name.Name, - }, - Source: f.Name.Val, - }, - Description: &L10nInput{ - Input: form.Input{ - Name: f.Description.Name, - }, - Source: f.Description.Val, - }, + Locale: loc, + Slug: f.Slug, + Name: f.Name.L10nInput(), + Description: f.Description.L10nInput(), } } diff --git a/pkg/form/input.go b/pkg/form/input.go index e0713bc..3de6bf3 100644 --- a/pkg/form/input.go +++ b/pkg/form/input.go @@ -27,3 +27,17 @@ func (input *Input) FillValue(r *http.Request) { func (input *Input) Value() (driver.Value, error) { return input.Val, nil } + +func (input *Input) L10nInput() *L10nInput { + return &L10nInput{ + Input: Input{ + Name: input.Name, + }, + Source: input.Val, + } +} + +type L10nInput struct { + Input + Source string +} diff --git a/pkg/home/admin.go b/pkg/home/admin.go new file mode 100644 index 0000000..211472a --- /dev/null +++ b/pkg/home/admin.go @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package home + +import ( + "net/http" + + "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/database" + 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 { + locales locale.Locales +} + +func NewAdminHandler(locales locale.Locales) *AdminHandler { + return &AdminHandler{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.MethodGet: + serveHomeIndex(w, r, user, company, conn) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet) + } + case "slides": + h.carouselHandler(user, company, conn).ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +func serveHomeIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { + slides, err := collectSlideEntries(r.Context(), company, conn) + if err != nil { + panic(err) + } + page := &homeIndex{ + Slides: slides, + } + page.MustRender(w, r, user, company) +} + +type homeIndex struct { + Slides []*slideEntry +} + +func (page *homeIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "home/index.gohtml", page) +} diff --git a/pkg/home/carousel.go b/pkg/home/carousel.go new file mode 100644 index 0000000..c558a58 --- /dev/null +++ b/pkg/home/carousel.go @@ -0,0 +1,311 @@ +/* + * 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 +} diff --git a/pkg/home/l10n.go b/pkg/home/l10n.go new file mode 100644 index 0000000..efe1a43 --- /dev/null +++ b/pkg/home/l10n.go @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package home + +import ( + "context" + "net/http" + + "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 slideL10nForm struct { + Locale *locale.Locale + ID int + Caption *form.L10nInput +} + +func newSlideL10nForm(f *slideForm, loc *locale.Locale) *slideL10nForm { + return &slideL10nForm{ + Locale: loc, + ID: f.ID, + Caption: f.Caption.L10nInput(), + } +} + +func (l10n *slideL10nForm) FillFromDatabase(ctx context.Context, conn *database.Conn) error { + row := conn.QueryRow(ctx, ` + select coalesce(i18n.caption, '') as l10n_caption + from home_carousel + left join home_carousel_i18n as i18n on home_carousel.media_id = i18n.media_id and i18n.lang_tag = $1 + where home_carousel.media_id = $2 + `, l10n.Locale.Language, l10n.ID) + return row.Scan(&l10n.Caption.Val) +} + +func (l10n *slideL10nForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "home/carousel/l10n.gohtml", l10n) +} + +func editSlideL10n(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, l10n *slideL10nForm) { + if err := l10n.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 !l10n.Valid(user.Locale) { + if !httplib.IsHTMxRequest(r) { + w.WriteHeader(http.StatusUnprocessableEntity) + } + l10n.MustRender(w, r, user, company) + return + } + conn.MustExec(r.Context(), "select translate_home_carousel_slide($1, $2, $3)", l10n.ID, l10n.Locale.Language, l10n.Caption) + httplib.Redirect(w, r, "/admin/home", http.StatusSeeOther) +} + +func (l10n *slideL10nForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + l10n.Caption.FillValue(r) + return nil +} + +func (l10n *slideL10nForm) Valid(l *locale.Locale) bool { + v := form.NewValidator(l) + return v.AllOK +} diff --git a/pkg/home/public.go b/pkg/home/public.go new file mode 100644 index 0000000..434ccd9 --- /dev/null +++ b/pkg/home/public.go @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package home + +import ( + "context" + "net/http" + + "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/database" + httplib "dev.tandem.ws/tandem/camper/pkg/http" + "dev.tandem.ws/tandem/camper/pkg/locale" + "dev.tandem.ws/tandem/camper/pkg/template" +) + +type PublicHandler struct { +} + +func NewPublicHandler() *PublicHandler { + return &PublicHandler{} +} + +func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + home := newHomePage() + home.MustRender(w, r, user, company, conn) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet) + } + }) +} + +type homePage struct { + *template.PublicPage + CampsiteTypes []*campsiteType + Carousel []*carouselSlide +} + +func newHomePage() *homePage { + return &homePage{PublicPage: template.NewPublicPage()} +} + +func (p *homePage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { + p.Setup(r, user, company, conn) + p.CampsiteTypes = mustCollectCampsiteTypes(r.Context(), company, conn, user.Locale) + p.Carousel = mustCollectCarouselSlides(r.Context(), company, conn, user.Locale) + template.MustRenderPublic(w, r, user, company, "home.gohtml", p) +} + +type campsiteType struct { + Label string + HRef string + Media string +} + +func mustCollectCampsiteTypes(ctx context.Context, company *auth.Company, conn *database.Conn, loc *locale.Locale) []*campsiteType { + rows, err := conn.Query(ctx, ` + select coalesce(i18n.name, campsite_type.name) as l10_name + , '/campsites/types/' || slug + , media.path + from campsite_type + left join campsite_type_i18n as i18n on campsite_type.campsite_type_id = i18n.campsite_type_id and lang_tag = $1 + join media using (media_id) + where campsite_type.company_id = $2 + and campsite_type.active + `, loc.Language, company.ID) + if err != nil { + panic(err) + } + defer rows.Close() + + localePath := "/" + loc.Language.String() + var items []*campsiteType + for rows.Next() { + item := &campsiteType{} + err = rows.Scan(&item.Label, &item.HRef, &item.Media) + if err != nil { + panic(err) + } + item.HRef = localePath + item.HRef + items = append(items, item) + } + if rows.Err() != nil { + panic(rows.Err()) + } + + return items +} diff --git a/pkg/locale/locale.go b/pkg/locale/locale.go index 8a3fc92..cb7d576 100644 --- a/pkg/locale/locale.go +++ b/pkg/locale/locale.go @@ -33,6 +33,15 @@ func (m Locales) Tags() []language.Tag { return keys } +func (m Locales) Get(lang string) (loc *Locale, ok bool) { + tag, err := language.Parse(lang) + if err != nil { + return + } + loc, ok = m[tag] + return +} + func GetAll(ctx context.Context, db *database.DB) (Locales, error) { availableLanguages, err := getAvailableLanguages(ctx, db) if err != nil { diff --git a/po/ca.po b/po/ca.po index a33a3ae..b72d552 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-09-12 20:18+0200\n" +"POT-Creation-Date: 2023-09-15 00:05+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -56,15 +56,7 @@ msgstr "A menys d’una hora de Girona, a una de La Bis msgid "Discover the surroundings" msgstr "Descobreix l’entorn" -#: web/templates/public/home.gohtml:44 web/templates/public/home.gohtml:48 -#: web/templates/public/home.gohtml:52 web/templates/public/home.gohtml:56 -#: web/templates/public/home.gohtml:60 web/templates/public/home.gohtml:64 -#: web/templates/public/home.gohtml:68 web/templates/public/home.gohtml:72 -#: web/templates/public/home.gohtml:76 -msgid "Legend" -msgstr "Llegenda" - -#: web/templates/public/home.gohtml:80 +#: web/templates/public/home.gohtml:54 msgid "Come and enjoy!" msgstr "Vine a gaudir!" @@ -116,6 +108,7 @@ msgstr "Etiqueta" #: web/templates/admin/campsite/form.gohtml:71 #: web/templates/admin/campsite/type/form.gohtml:77 #: web/templates/admin/season/form.gohtml:65 +#: web/templates/admin/home/carousel/form.gohtml:58 msgctxt "action" msgid "Update" msgstr "Actualitza" @@ -123,6 +116,7 @@ msgstr "Actualitza" #: web/templates/admin/campsite/form.gohtml:73 #: web/templates/admin/campsite/type/form.gohtml:79 #: web/templates/admin/season/form.gohtml:67 +#: web/templates/admin/home/carousel/form.gohtml:60 msgctxt "action" msgid "Add" msgstr "Afegeix" @@ -192,6 +186,7 @@ msgid "Name" msgstr "Nom" #: web/templates/admin/campsite/type/form.gohtml:59 +#: web/templates/admin/home/carousel/form.gohtml:39 msgctxt "input" msgid "Cover image" msgstr "Imatge de portada" @@ -221,6 +216,7 @@ msgid "Name" msgstr "Nom" #: web/templates/admin/campsite/type/index.gohtml:19 +#: web/templates/admin/home/index.gohtml:20 msgctxt "campsite type" msgid "Translations" msgstr "Traduccions" @@ -237,16 +233,19 @@ msgstr "Traducció del tipus d’allotjament a %s" #: web/templates/admin/campsite/type/l10n.gohtml:22 #: web/templates/admin/campsite/type/l10n.gohtml:34 +#: web/templates/admin/home/carousel/l10n.gohtml:22 msgid "Source:" msgstr "Origen:" #: web/templates/admin/campsite/type/l10n.gohtml:24 #: web/templates/admin/campsite/type/l10n.gohtml:37 +#: web/templates/admin/home/carousel/l10n.gohtml:24 msgctxt "input" msgid "Translation:" msgstr "Traducció:" #: web/templates/admin/campsite/type/l10n.gohtml:46 +#: web/templates/admin/home/carousel/l10n.gohtml:33 msgctxt "action" msgid "Translate" msgstr "Tradueix" @@ -442,6 +441,69 @@ msgctxt "action" msgid "Logout" msgstr "Surt" +#: web/templates/admin/layout.gohtml:76 web/templates/admin/home/index.gohtml:6 +msgctxt "title" +msgid "Home Page" +msgstr "Pàgina d’inici" + +#: web/templates/admin/home/carousel/form.gohtml:8 +#: web/templates/admin/home/carousel/form.gohtml:27 +msgctxt "title" +msgid "Edit Carousel Slide" +msgstr "Edició de la diapositiva del carrusel" + +#: web/templates/admin/home/carousel/form.gohtml:10 +#: web/templates/admin/home/carousel/form.gohtml:29 +msgctxt "title" +msgid "New Carousel Slide" +msgstr "Nova diapositiva del carrusel" + +#: web/templates/admin/home/carousel/form.gohtml:48 +#: web/templates/admin/home/carousel/l10n.gohtml:21 +msgctxt "input" +msgid "Caption" +msgstr "Llegenda" + +#: web/templates/admin/home/carousel/l10n.gohtml:7 +#: web/templates/admin/home/carousel/l10n.gohtml:15 +msgctxt "title" +msgid "Translate Carousel Slide to %s" +msgstr "Traducció de la diapositiva del carrusel a %s" + +#: web/templates/admin/home/index.gohtml:12 +msgctxt "title" +msgid "Carousel" +msgstr "Carrusel" + +#: web/templates/admin/home/index.gohtml:13 +msgctxt "action" +msgid "Add slide" +msgstr "Afegeix diapositiva" + +#: web/templates/admin/home/index.gohtml:18 +msgctxt "header" +msgid "Image" +msgstr "Imatge" + +#: web/templates/admin/home/index.gohtml:19 +msgctxt "header" +msgid "Caption" +msgstr "Llegenda" + +#: web/templates/admin/home/index.gohtml:21 +msgctxt "campsite type" +msgid "Actions" +msgstr "Accions" + +#: web/templates/admin/home/index.gohtml:40 +msgctxt "action" +msgid "Delete" +msgstr "Esborra" + +#: web/templates/admin/home/index.gohtml:48 +msgid "No slides added yet." +msgstr "No s’ha afegit cap diapositiva encara." + #: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:203 msgid "Email can not be empty." msgstr "No podeu deixar el correu-e en blanc." @@ -463,8 +525,8 @@ msgctxt "language option" msgid "Automatic" msgstr "Automàtic" -#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:105 -#: pkg/campsite/types/admin.go:301 pkg/season/admin.go:203 +#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:90 +#: pkg/campsite/types/admin.go:293 pkg/season/admin.go:203 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." @@ -476,15 +538,15 @@ msgstr "La confirmació no es correspon amb la contrasenya." msgid "Selected language is not valid." msgstr "L’idioma escollit no és vàlid." -#: pkg/app/user.go:253 pkg/campsite/types/admin.go:303 +#: pkg/app/user.go:253 pkg/campsite/types/admin.go:295 pkg/home/carousel.go:286 msgid "File must be a valid PNG or JPEG image." msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida." -#: pkg/app/admin.go:44 +#: pkg/app/admin.go:47 msgid "Access forbidden" msgstr "Accés prohibit" -#: pkg/campsite/types/admin.go:305 +#: pkg/campsite/types/admin.go:297 msgid "Cover image can not be empty." msgstr "No podeu deixar la imatge de portada en blanc." @@ -568,13 +630,16 @@ msgstr "No podeu deixar el format del número de factura en blanc." msgid "Cross-site request forgery detected." msgstr "S’ha detectat un intent de falsificació de petició a llocs creuats." +#: pkg/home/carousel.go:288 +msgid "Slide image can not be empty." +msgstr "No podeu deixar la imatge de la diapositiva en blanc." + +#~ msgid "Legend" +#~ msgstr "Llegenda" + #~ msgid "Environment" #~ msgstr "Entorn" -#~ msgctxt "title" -#~ msgid "New Page" -#~ msgstr "Nova pàgina" - #~ msgctxt "input" #~ msgid "Title" #~ msgstr "Títol" diff --git a/po/es.po b/po/es.po index 1dfec60..0bf06d6 100644 --- a/po/es.po +++ b/po/es.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-09-12 20:18+0200\n" +"POT-Creation-Date: 2023-09-15 00:05+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -56,15 +56,7 @@ msgstr "A menos de una hora de Girona, a una de La Bisb msgid "Discover the surroundings" msgstr "Descubre el entorno" -#: web/templates/public/home.gohtml:44 web/templates/public/home.gohtml:48 -#: web/templates/public/home.gohtml:52 web/templates/public/home.gohtml:56 -#: web/templates/public/home.gohtml:60 web/templates/public/home.gohtml:64 -#: web/templates/public/home.gohtml:68 web/templates/public/home.gohtml:72 -#: web/templates/public/home.gohtml:76 -msgid "Legend" -msgstr "Leyenda" - -#: web/templates/public/home.gohtml:80 +#: web/templates/public/home.gohtml:54 msgid "Come and enjoy!" msgstr "¡Ven a disfrutar!" @@ -116,6 +108,7 @@ msgstr "Etiqueta" #: web/templates/admin/campsite/form.gohtml:71 #: web/templates/admin/campsite/type/form.gohtml:77 #: web/templates/admin/season/form.gohtml:65 +#: web/templates/admin/home/carousel/form.gohtml:58 msgctxt "action" msgid "Update" msgstr "Actualizar" @@ -123,6 +116,7 @@ msgstr "Actualizar" #: web/templates/admin/campsite/form.gohtml:73 #: web/templates/admin/campsite/type/form.gohtml:79 #: web/templates/admin/season/form.gohtml:67 +#: web/templates/admin/home/carousel/form.gohtml:60 msgctxt "action" msgid "Add" msgstr "Añadir" @@ -192,6 +186,7 @@ msgid "Name" msgstr "Nombre" #: web/templates/admin/campsite/type/form.gohtml:59 +#: web/templates/admin/home/carousel/form.gohtml:39 msgctxt "input" msgid "Cover image" msgstr "Imagen de portada" @@ -221,6 +216,7 @@ msgid "Name" msgstr "Nombre" #: web/templates/admin/campsite/type/index.gohtml:19 +#: web/templates/admin/home/index.gohtml:20 msgctxt "campsite type" msgid "Translations" msgstr "Traducciones" @@ -237,16 +233,19 @@ msgstr "Traducción de tipo de alojamiento a %s" #: web/templates/admin/campsite/type/l10n.gohtml:22 #: web/templates/admin/campsite/type/l10n.gohtml:34 +#: web/templates/admin/home/carousel/l10n.gohtml:22 msgid "Source:" msgstr "Origen:" #: web/templates/admin/campsite/type/l10n.gohtml:24 #: web/templates/admin/campsite/type/l10n.gohtml:37 +#: web/templates/admin/home/carousel/l10n.gohtml:24 msgctxt "input" msgid "Translation:" msgstr "Traducción" #: web/templates/admin/campsite/type/l10n.gohtml:46 +#: web/templates/admin/home/carousel/l10n.gohtml:33 msgctxt "action" msgid "Translate" msgstr "Traducir" @@ -442,6 +441,69 @@ msgctxt "action" msgid "Logout" msgstr "Salir" +#: web/templates/admin/layout.gohtml:76 web/templates/admin/home/index.gohtml:6 +msgctxt "title" +msgid "Home Page" +msgstr "Página de inicio" + +#: web/templates/admin/home/carousel/form.gohtml:8 +#: web/templates/admin/home/carousel/form.gohtml:27 +msgctxt "title" +msgid "Edit Carousel Slide" +msgstr "Edición de la diapositiva del carrusel" + +#: web/templates/admin/home/carousel/form.gohtml:10 +#: web/templates/admin/home/carousel/form.gohtml:29 +msgctxt "title" +msgid "New Carousel Slide" +msgstr "Nueva diapositiva del carrusel" + +#: web/templates/admin/home/carousel/form.gohtml:48 +#: web/templates/admin/home/carousel/l10n.gohtml:21 +msgctxt "input" +msgid "Caption" +msgstr "Leyenda" + +#: web/templates/admin/home/carousel/l10n.gohtml:7 +#: web/templates/admin/home/carousel/l10n.gohtml:15 +msgctxt "title" +msgid "Translate Carousel Slide to %s" +msgstr "Traducción de la diapositiva de carrusel a %s" + +#: web/templates/admin/home/index.gohtml:12 +msgctxt "title" +msgid "Carousel" +msgstr "Carrusel" + +#: web/templates/admin/home/index.gohtml:13 +msgctxt "action" +msgid "Add slide" +msgstr "Añadir diapositiva" + +#: web/templates/admin/home/index.gohtml:18 +msgctxt "header" +msgid "Image" +msgstr "Imagen" + +#: web/templates/admin/home/index.gohtml:19 +msgctxt "header" +msgid "Caption" +msgstr "Leyenda" + +#: web/templates/admin/home/index.gohtml:21 +msgctxt "campsite type" +msgid "Actions" +msgstr "Acciones" + +#: web/templates/admin/home/index.gohtml:40 +msgctxt "action" +msgid "Delete" +msgstr "Borrar" + +#: web/templates/admin/home/index.gohtml:48 +msgid "No slides added yet." +msgstr "No se ha añadido ninguna diapositiva todavía." + #: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:203 msgid "Email can not be empty." msgstr "No podéis dejar el correo-e en blanco." @@ -463,8 +525,8 @@ msgctxt "language option" msgid "Automatic" msgstr "Automático" -#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:105 -#: pkg/campsite/types/admin.go:301 pkg/season/admin.go:203 +#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:90 +#: pkg/campsite/types/admin.go:293 pkg/season/admin.go:203 msgid "Name can not be empty." msgstr "No podéis dejar el nombre en blanco." @@ -476,15 +538,15 @@ msgstr "La confirmación no se corresponde con la contraseña." msgid "Selected language is not valid." msgstr "El idioma escogido no es válido." -#: pkg/app/user.go:253 pkg/campsite/types/admin.go:303 +#: pkg/app/user.go:253 pkg/campsite/types/admin.go:295 pkg/home/carousel.go:286 msgid "File must be a valid PNG or JPEG image." msgstr "El archivo tiene que ser una imagen PNG o JPEG válida." -#: pkg/app/admin.go:44 +#: pkg/app/admin.go:47 msgid "Access forbidden" msgstr "Acceso prohibido" -#: pkg/campsite/types/admin.go:305 +#: pkg/campsite/types/admin.go:297 msgid "Cover image can not be empty." msgstr "No podéis dejar la imagen de portada en blanco." @@ -568,13 +630,16 @@ msgstr "No podéis dejar el formato de número de factura en blanco." msgid "Cross-site request forgery detected." msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados." +#: pkg/home/carousel.go:288 +msgid "Slide image can not be empty." +msgstr "No podéis dejar la imagen de la diapositiva en blanco." + +#~ msgid "Legend" +#~ msgstr "Leyenda" + #~ msgid "Environment" #~ msgstr "Entorno" -#~ msgctxt "title" -#~ msgid "New Page" -#~ msgstr "Nueva página" - #~ msgctxt "input" #~ msgid "Title" #~ msgstr "Título" diff --git a/revert/add_home_carousel_slide.sql b/revert/add_home_carousel_slide.sql new file mode 100644 index 0000000..2075319 --- /dev/null +++ b/revert/add_home_carousel_slide.sql @@ -0,0 +1,7 @@ +-- Revert camper:add_home_carousel_slide from pg + +begin; + +drop function if exists camper.add_home_carousel_slide(integer, text); + +commit; diff --git a/revert/home_carousel.sql b/revert/home_carousel.sql new file mode 100644 index 0000000..185d62e --- /dev/null +++ b/revert/home_carousel.sql @@ -0,0 +1,7 @@ +-- Revert camper:home_carousel from pg + +begin; + +drop table if exists camper.home_carousel; + +commit; diff --git a/revert/home_carousel_i18n.sql b/revert/home_carousel_i18n.sql new file mode 100644 index 0000000..67407aa --- /dev/null +++ b/revert/home_carousel_i18n.sql @@ -0,0 +1,7 @@ +-- Revert camper:home_carousel_i18n from pg + +begin; + +drop table if exists camper.home_carousel_i18n; + +commit; diff --git a/revert/media_path.sql b/revert/media_path.sql new file mode 100644 index 0000000..bea5dca --- /dev/null +++ b/revert/media_path.sql @@ -0,0 +1,7 @@ +-- Revert camper:media_path from pg + +begin; + +drop function if exists camper.path(camper.media); + +commit; diff --git a/revert/remove_home_carousel_slide.sql b/revert/remove_home_carousel_slide.sql new file mode 100644 index 0000000..5b1e063 --- /dev/null +++ b/revert/remove_home_carousel_slide.sql @@ -0,0 +1,7 @@ +-- Revert camper:remove_home_carousel_slide from pg + +begin; + +drop function if exists camper.remove_home_carousel_slide(integer); + +commit; diff --git a/revert/translate_home_carousel_slide.sql b/revert/translate_home_carousel_slide.sql new file mode 100644 index 0000000..b9b252d --- /dev/null +++ b/revert/translate_home_carousel_slide.sql @@ -0,0 +1,7 @@ +-- Revert camper:translate_home_carousel_slide from pg + +begin; + +drop function if exists camper.translate_home_carousel_slide(integer, text, text); + +commit; diff --git a/sqitch.plan b/sqitch.plan index c127b6b..7894401 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -42,6 +42,7 @@ change_password [roles schema_auth schema_camper user] 2023-07-21T23:54:52Z jord media_type [schema_camper] 2023-09-08T17:17:02Z jordi fita mas # Add domain for media type media [roles schema_camper company user_profile media_type] 2023-09-08T16:50:55Z jordi fita mas # Add relation of uploaded media add_media [roles schema_camper media media_type] 2023-09-08T17:40:28Z jordi fita mas # Add function to create media +media_path [roles schema_camper media] 2023-09-13T22:50:14Z jordi fita mas # Add function to get the URL path of a media campsite_type [roles schema_camper company media user_profile] 2023-07-31T11:20:29Z jordi fita mas # Add relation of campsite type campsite_type_i18n [roles schema_camper campsite_type language] 2023-09-12T10:31:29Z jordi fita mas # Add relation for campsite_type translations add_campsite_type [roles schema_camper campsite_type company] 2023-08-04T16:14:48Z jordi fita mas # Add function to create campsite types @@ -57,3 +58,8 @@ to_color [roles schema_camper color] 2023-08-16T13:11:32Z jordi fita mas # Add relation of (tourist) season add_season [roles schema_camper season color to_integer] 2023-08-16T16:59:17Z jordi fita mas # Add function to create seasons edit_season [roles schema_camper season color to_integer] 2023-08-16T17:09:02Z jordi fita mas # Add function to update seasons +home_carousel [roles schema_public company media user_profile] 2023-09-13T17:16:34Z jordi fita mas # Add relation for home page’s image carousel +home_carousel_i18n [roles schema_camper home_carousel language] 2023-09-13T23:22:42Z jordi fita mas # Add relation for home carousel translations +add_home_carousel_slide [roles schema_camper home_carousel] 2023-09-14T17:49:21Z jordi fita mas # Add function to create slides for the home carousel +translate_home_carousel_slide [roles schema_camper home_carousel_i18n] 2023-09-14T18:17:36Z jordi fita mas # Add function to translate a home carousel slider +remove_home_carousel_slide [roles schema_camper home_carousel home_carousel_i18n] 2023-09-14T21:57:48Z jordi fita mas # Add function to remove sliders from the home carousel diff --git a/test/add_home_carousel_slide.sql b/test/add_home_carousel_slide.sql new file mode 100644 index 0000000..fac1178 --- /dev/null +++ b/test/add_home_carousel_slide.sql @@ -0,0 +1,75 @@ +-- Test add_home_carousel_slide +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +set search_path to camper, public; + +select plan(14); + +select has_function('camper', 'add_home_carousel_slide', array['integer', 'text']); +select function_lang_is('camper', 'add_home_carousel_slide', array['integer', 'text'], 'sql'); +select function_returns('camper', 'add_home_carousel_slide', array['integer', 'text'], 'integer'); +select isnt_definer('camper', 'add_home_carousel_slide', array['integer', 'text']); +select volatility_is('camper', 'add_home_carousel_slide', array['integer', 'text'], 'volatile'); +select function_privs_are('camper', 'add_home_carousel_slide', array['integer', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'add_home_carousel_slide', array['integer', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'add_home_carousel_slide', array['integer', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'add_home_carousel_slide', array['integer', 'text'], 'authenticator', array[]::text[]); + +set client_min_messages to warning; +truncate home_carousel_i18n cascade; +truncate home_carousel cascade; +truncate media cascade; +truncate company cascade; +reset client_min_messages; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca') +; + +insert into media (media_id, company_id, original_filename, media_type, content) +values (5, 1, 'text.txt', 'text/plain', 'hello, world!') + , (6, 1, 'image.svg', 'image/svg+xml', '') + , (7, 1, 'cover4.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};') +; + +insert into home_carousel (media_id, caption) +values (5, 'Previous caption') +; + +select lives_ok( + $$ select add_home_carousel_slide(6, 'A caption') $$, + 'Should be able to add a carousel slide with a caption' +); + +select lives_ok( + $$ select add_home_carousel_slide(7, null) $$, + 'Should be able to add a carousel slide without caption' +); + +select lives_ok( + $$ select add_home_carousel_slide(5, 'New caption') $$, + 'Should be able to overwrite a slide with a new caption' +); + +select bag_eq( + $$ select media_id, caption from home_carousel $$, + $$ values (5, 'New caption') + , (6, 'A caption') + , (7, '') + $$, + 'Should have all three slides' +); + +select is_empty( + $$ select * from home_carousel_i18n $$, + 'Should not have added any translation' +); + +select * +from finish(); + +rollback; diff --git a/test/home_carousel.sql b/test/home_carousel.sql new file mode 100644 index 0000000..5d3f9f1 --- /dev/null +++ b/test/home_carousel.sql @@ -0,0 +1,178 @@ +-- Test home_carousel +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(30); + +set search_path to camper, public; + +select has_table('home_carousel'); +select has_pk('home_carousel'); +select table_privs_are('home_carousel', 'guest', array['SELECT']); +select table_privs_are('home_carousel', 'employee', array['SELECT']); +select table_privs_are('home_carousel', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('home_carousel', 'authenticator', array[]::text[]); + +select has_column('home_carousel', 'media_id'); +select col_is_pk('home_carousel', 'media_id'); +select col_is_fk('home_carousel', 'media_id'); +select fk_ok('home_carousel', 'media_id', 'media', 'media_id'); +select col_type_is('home_carousel', 'media_id', 'integer'); +select col_not_null('home_carousel', 'media_id'); +select col_hasnt_default('home_carousel', 'media_id'); + +select has_column('home_carousel', 'caption'); +select col_type_is('home_carousel', 'caption', 'text'); +select col_not_null('home_carousel', 'caption'); +select col_hasnt_default('home_carousel', 'caption'); + +set client_min_messages to warning; +truncate home_carousel cascade; +truncate media cascade; +truncate company_host cascade; +truncate company_user cascade; +truncate company cascade; +truncate auth."user" cascade; +reset client_min_messages; + + +insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at) +values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month') + , (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month') +; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca') +; + +insert into company_user (company_id, user_id, role) +values (2, 1, 'admin') + , (4, 5, 'admin') +; + +insert into company_host (company_id, host) +values (2, 'co2') + , (4, 'co4') +; + +insert into media (media_id, company_id, original_filename, media_type, content) +values ( 7, 2, 'text2.txt', 'text/plain', 'content2') + , ( 8, 2, 'text3.txt', 'text/plain', 'content3') + , ( 9, 4, 'text4.txt', 'text/plain', 'content4') + , (10, 4, 'text5.txt', 'text/plain', 'content5') +; + +insert into home_carousel (media_id, caption) +values (7, 'Caption 7') + , (9, 'Caption 9') +; + +prepare carousel_data as +select media_id, caption +from home_carousel +order by media_id, caption; + +set role guest; +select bag_eq( + 'carousel_data', + $$ values (7, 'Caption 7') + , (9, 'Caption 9') + $$, + 'Everyone should be able to list all carousel media across all companies' +); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2'); + +select lives_ok( + $$ insert into home_carousel(media_id, caption) + values (8, 'Caption 8') $$, + 'Admin from company 2 should be able to insert a new carousel media to that company.' +); + +select bag_eq( + 'carousel_data', + $$ values (7, 'Caption 7') + , (8, 'Caption 8') + , (9, 'Caption 9') + $$, + 'The new row should have been added' +); + +select lives_ok( + $$ update home_carousel set caption = 'Caption 8.8' where media_id = 8 $$, + 'Admin from company 2 should be able to update carousel media of that company.' +); + +select bag_eq( + 'carousel_data', + $$ values (7, 'Caption 7') + , (8, 'Caption 8.8') + , (9, 'Caption 9') + $$, + 'The row should have been updated.' +); + +select lives_ok( + $$ delete from home_carousel where media_id = 8 $$, + 'Admin from company 2 should be able to delete carousel media from that company.' +); + +select bag_eq( + 'carousel_data', + $$ values (7, 'Caption 7') + , (9, 'Caption 9') + $$, + 'The row should have been deleted.' +); + +select throws_ok( + $$ insert into home_carousel (media_id, caption) + values (10, 'Caption 10') $$, + '42501', 'new row violates row-level security policy for table "home_carousel"', + 'Admin from company 2 should NOT be able to insert new media to company 4.' +); + +select lives_ok( + $$ update home_carousel set caption = 'Nope' where media_id = 9 $$, + 'Admin from company 2 should not be able to update new carousel media of company 4, but no error if media_id is not changed.' +); + +select bag_eq( + 'carousel_data', + $$ values (7, 'Caption 7') + , (9, 'Caption 9') + $$, + 'No row should have been changed.' +); + +select throws_ok( + $$ update home_carousel set media_id = 10 where media_id = 7 $$, + '42501', 'new row violates row-level security policy for table "home_carousel"', + 'Admin from company 2 should NOT be able to move carousel media to company 4' +); + +select lives_ok( + $$ delete from home_carousel where media_id = 9 $$, + 'Admin from company 2 should NOT be able to delete carousel media from company 4, but no error is thrown' +); + +select bag_eq( + 'carousel_data', + $$ values (7, 'Caption 7') + , (9, 'Caption 9') + $$, + 'No row should have been changed' +); + +reset role; + +select * +from finish(); + +rollback; + diff --git a/test/home_carousel_i18n.sql b/test/home_carousel_i18n.sql new file mode 100644 index 0000000..c76d367 --- /dev/null +++ b/test/home_carousel_i18n.sql @@ -0,0 +1,44 @@ +-- Test home_carousel_i18n +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(23); + +set search_path to camper, public; + +select has_table('home_carousel_i18n'); +select has_pk('home_carousel_i18n'); +select col_is_pk('home_carousel_i18n', array['media_id', 'lang_tag']); +select table_privs_are('home_carousel_i18n', 'guest', array['SELECT']); +select table_privs_are('home_carousel_i18n', 'employee', array['SELECT']); +select table_privs_are('home_carousel_i18n', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('home_carousel_i18n', 'authenticator', array[]::text[]); + +select has_column('home_carousel_i18n', 'media_id'); +select col_is_fk('home_carousel_i18n', 'media_id'); +select fk_ok('home_carousel_i18n', 'media_id', 'home_carousel', 'media_id'); +select col_type_is('home_carousel_i18n', 'media_id', 'integer'); +select col_not_null('home_carousel_i18n', 'media_id'); +select col_hasnt_default('home_carousel_i18n', 'media_id'); + +select has_column('home_carousel_i18n', 'lang_tag'); +select col_is_fk('home_carousel_i18n', 'lang_tag'); +select fk_ok('home_carousel_i18n', 'lang_tag', 'language', 'lang_tag'); +select col_type_is('home_carousel_i18n', 'lang_tag', 'text'); +select col_not_null('home_carousel_i18n', 'lang_tag'); +select col_hasnt_default('home_carousel_i18n', 'lang_tag'); + +select has_column('home_carousel_i18n', 'caption'); +select col_type_is('home_carousel_i18n', 'caption', 'text'); +select col_not_null('home_carousel_i18n', 'caption'); +select col_hasnt_default('home_carousel_i18n', 'caption'); + + +select * +from finish(); + +rollback; + diff --git a/test/media_path.sql b/test/media_path.sql new file mode 100644 index 0000000..60c31ee --- /dev/null +++ b/test/media_path.sql @@ -0,0 +1,53 @@ +-- Test media_path +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(10); + +set search_path to camper, public; + +select has_function('camper', 'path', array['media']); +select function_lang_is('camper', 'path', array['media'], 'sql'); +select function_returns('camper', 'path', array['media'], 'text'); +select isnt_definer('camper', 'path', array['media']); +select volatility_is('camper', 'path', array['media'], 'stable'); +select function_privs_are('camper', 'path', array['media'], 'guest', array['EXECUTE']); +select function_privs_are('camper', 'path', array['media'], 'employee', array['EXECUTE']); +select function_privs_are('camper', 'path', array['media'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'path', array['media'], 'authenticator', array[]::text[]); + +set client_min_messages to warning; +truncate media cascade; +truncate company cascade; +reset client_min_messages; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca') +; + +insert into media (media_id, company_id, original_filename, media_type, content) +values (3, 1, 'cover1.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ff0000","a"};') + , (4, 1, 'cover2.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","b c #00ff00","b"};') + , (5, 1, 'cover3.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","c c #0000ff","c"};') + , (6, 1, 'image.svg', 'image/svg+xml', '') + , (7, 1, 'text.txt', 'text/plain', 'hello, world!') +; + +select bag_eq( + $$ select media_id, media.path from media order by media_id $$, + $$ values (3, '/media/fbbac389f80d12ef13a3dd6e5dec9541622b7f870f22cfa94c3ca374d6dbc6b4/cover1.xpm') + , (4, '/media/b1ac118723104963266ceff35cc5170802885412d1bc6ea247981c15eb4b00b1/cover2.xpm') + , (5, '/media/72ccd5c651416d714740715035e0e099c18785ce9f9a8f4222bfb72f663b1c18/cover3.xpm') + , (6, '/media/ffc9f5e4fdeea83920c171e2bd17577127c5d1a2c3c76f07440e10d387132280/image.svg') + , (7, '/media/68e656b251e67e8358bef8483ab0d51c6619f3e7a1a9f0e75838d41ff368f728/text.txt') + $$, + 'Should give out the URL path for each media' +); + +select * +from finish(); + +rollback; diff --git a/test/remove_home_carousel_slide.sql b/test/remove_home_carousel_slide.sql new file mode 100644 index 0000000..90fe059 --- /dev/null +++ b/test/remove_home_carousel_slide.sql @@ -0,0 +1,80 @@ +-- Test remove_home_carousel_slide +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(12); + +set search_path to camper, public; + +select has_function('camper', 'remove_home_carousel_slide', array['integer']); +select function_lang_is('camper', 'remove_home_carousel_slide', array['integer'], 'sql'); +select function_returns('camper', 'remove_home_carousel_slide', array['integer'], 'void'); +select isnt_definer('camper', 'remove_home_carousel_slide', array['integer']); +select volatility_is('camper', 'remove_home_carousel_slide', array['integer'], 'volatile'); +select function_privs_are('camper', 'remove_home_carousel_slide', array['integer'], 'guest', array[]::text[]); +select function_privs_are('camper', 'remove_home_carousel_slide', array['integer'], 'employee', array[]::text[]); +select function_privs_are('camper', 'remove_home_carousel_slide', array['integer'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'remove_home_carousel_slide', array['integer'], 'authenticator', array[]::text[]); + +set client_min_messages to warning; +truncate home_carousel_i18n cascade; +truncate home_carousel cascade; +truncate media cascade; +truncate company cascade; +reset client_min_messages; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca') +; + +insert into media (media_id, company_id, original_filename, media_type, content) +values (5, 1, 'text.txt', 'text/plain', 'hello, world!') + , (6, 1, 'image.svg', 'image/svg+xml', '') + , (7, 1, 'cover4.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};') +; + +insert into home_carousel (media_id, caption) +values (5, 'Source caption') + , (6, 'Another caption') + , (7, 'N/A') +; + +insert into home_carousel_i18n (media_id, lang_tag, caption) +values (5, 'en', 'Target caption') + , (5, 'es', 'Target caption (spanish)') + , (6, 'en', 'Target caption') + , (6, 'es', 'Target caption (spanish)') + , (7, 'en', 'Target caption') + , (7, 'es', 'Target caption (spanish)') +; + +select lives_ok( + $$ select remove_home_carousel_slide(6) $$, + 'Should be able to delete a slide' +); + +select bag_eq( + $$ select media_id, caption from home_carousel $$, + $$ values (5, 'Source caption') + , (7, 'N/A') + $$, + 'Should have removed the slide' +); + +select bag_eq( + $$ select media_id, lang_tag, caption from home_carousel_i18n $$, + $$ values (5, 'en', 'Target caption') + , (5, 'es', 'Target caption (spanish)') + , (7, 'en', 'Target caption') + , (7, 'es', 'Target caption (spanish)') + $$, + 'Should have removed the slide’s translations' +); + +select * +from finish(); + +rollback; diff --git a/test/translate_home_carousel_slide.sql b/test/translate_home_carousel_slide.sql new file mode 100644 index 0000000..6ab4be0 --- /dev/null +++ b/test/translate_home_carousel_slide.sql @@ -0,0 +1,78 @@ +-- Test translate_home_carousel_slide +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(13); + +set search_path to camper, public; + +select has_function('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text']); +select function_lang_is('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text'], 'sql'); +select function_returns('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text'], 'void'); +select isnt_definer('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text']); +select volatility_is('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text'], 'volatile'); +select function_privs_are('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'translate_home_carousel_slide', array['integer', 'text', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate home_carousel_i18n cascade; +truncate home_carousel cascade; +truncate media cascade; +truncate company cascade; +reset client_min_messages; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca') +; + +insert into media (media_id, company_id, original_filename, media_type, content) +values (5, 1, 'text.txt', 'text/plain', 'hello, world!') + , (6, 1, 'image.svg', 'image/svg+xml', '') + , (7, 1, 'cover4.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};') +; + +insert into home_carousel (media_id, caption) +values (5, 'Source caption') + , (6, 'Another caption') + , (7, 'N/A') +; + +insert into home_carousel_i18n (media_id, lang_tag, caption) +values (5, 'en', 'Target caption') +; + +select lives_ok( + $$ select translate_home_carousel_slide(5, 'ca', 'Traducció') $$, + 'Should be able to translate a carousel slide' +); + +select lives_ok( + $$ select translate_home_carousel_slide(6, 'es', null) $$, + 'Should be able to “translate” a carousel slide to the empty string' +); + +select lives_ok( + $$ select translate_home_carousel_slide(5, 'en', 'Not anymore') $$, + 'Should be able to overwrite a slide’s translation' +); + +select bag_eq( + $$ select media_id, lang_tag, caption from home_carousel_i18n $$, + $$ values (5, 'ca', 'Traducció') + , (5, 'en', 'Not anymore') + , (6, 'es', '') + $$, + 'Should have all three slides' +); + + +select * +from finish(); + +rollback; diff --git a/verify/add_home_carousel_slide.sql b/verify/add_home_carousel_slide.sql new file mode 100644 index 0000000..4fb6d5f --- /dev/null +++ b/verify/add_home_carousel_slide.sql @@ -0,0 +1,7 @@ +-- Verify camper:add_home_carousel_slide on pg + +begin; + +select has_function_privilege('camper.add_home_carousel_slide(integer, text)', 'execute'); + +rollback; diff --git a/verify/home_carousel.sql b/verify/home_carousel.sql new file mode 100644 index 0000000..2a8efc4 --- /dev/null +++ b/verify/home_carousel.sql @@ -0,0 +1,16 @@ +-- Verify camper:home_carousel on pg + +begin; + +select media_id + , caption +from camper.home_carousel +where false; + +select 1 / count(*) from pg_class where oid = 'camper.home_carousel'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.home_carousel'::regclass; +select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.home_carousel'::regclass; +select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.home_carousel'::regclass; +select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.home_carousel'::regclass; + +rollback; diff --git a/verify/home_carousel_i18n.sql b/verify/home_carousel_i18n.sql new file mode 100644 index 0000000..0388e50 --- /dev/null +++ b/verify/home_carousel_i18n.sql @@ -0,0 +1,11 @@ +-- Verify camper:home_carousel_i18n on pg + +begin; + +select media_id + , lang_tag + , caption +from camper.home_carousel_i18n +where false; + +rollback; diff --git a/verify/media_path.sql b/verify/media_path.sql new file mode 100644 index 0000000..13ad59b --- /dev/null +++ b/verify/media_path.sql @@ -0,0 +1,7 @@ +-- Verify camper:media_path on pg + +begin; + +select has_function_privilege('camper.path(camper.media)', 'execute'); + +rollback; diff --git a/verify/remove_home_carousel_slide.sql b/verify/remove_home_carousel_slide.sql new file mode 100644 index 0000000..6698a43 --- /dev/null +++ b/verify/remove_home_carousel_slide.sql @@ -0,0 +1,7 @@ +-- Verify camper:remove_home_carousel_slide on pg + +begin; + +select has_function_privilege('camper.remove_home_carousel_slide(integer)', 'execute'); + +rollback; diff --git a/verify/translate_home_carousel_slide.sql b/verify/translate_home_carousel_slide.sql new file mode 100644 index 0000000..62b4f54 --- /dev/null +++ b/verify/translate_home_carousel_slide.sql @@ -0,0 +1,7 @@ +-- Verify camper:translate_home_carousel_slide on pg + +begin; + +select has_function_privilege('camper.translate_home_carousel_slide(integer, text, text)', 'execute'); + +rollback; diff --git a/web/static/public.css b/web/static/public.css index db0305a..0aaf625 100644 --- a/web/static/public.css +++ b/web/static/public.css @@ -432,12 +432,12 @@ nav .has-submenu:hover ul, nav .has-submenu:focus-within ul { margin-bottom: 2rem; } -.surroundings figure { +.surroundings figure, .surroundings .slick-track > img { margin-right: 5rem; position: relative; } -.surroundings figure img { +.surroundings img { height: 40rem; width: 100%; border-radius: 5px; diff --git a/web/templates/admin/campsite/type/form.gohtml b/web/templates/admin/campsite/type/form.gohtml index 5894b77..221a2ab 100644 --- a/web/templates/admin/campsite/type/form.gohtml +++ b/web/templates/admin/campsite/type/form.gohtml @@ -53,7 +53,7 @@ {{- end }} {{ with .Media -}} {{ if .Val -}} - + {{- end }}