From f514f9132e412365bfd914bfedc7f3cd20a4fd75 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Tue, 23 Jan 2024 14:53:15 +0100 Subject: [PATCH] Add ad management for surroundings They only want a single ad (for now, i guess). --- demo/ad.avif | Bin 0 -> 56858 bytes demo/demo.sql | 6 + deploy/remove_surroundings_ad.sql | 22 ++ deploy/setup_surroundings_ad.sql | 28 +++ deploy/surroundings_ad.sql | 55 +++++ deploy/surroundings_ad_i18n.sql | 23 ++ deploy/translate_surroundings_ad.sql | 26 +++ pkg/database/funcs.go | 15 ++ pkg/surroundings/admin.go | 143 +++++++++++++ pkg/surroundings/public.go | 30 +++ po/ca.po | 114 +++++++--- po/es.po | 114 +++++++--- po/fr.po | 110 +++++++--- revert/remove_surroundings_ad.sql | 7 + revert/setup_surroundings_ad.sql | 7 + revert/surroundings_ad.sql | 7 + revert/surroundings_ad_i18n.sql | 7 + revert/translate_surroundings_ad.sql | 7 + sqitch.plan | 5 + test/remove_surroundings_ad.sql | 99 +++++++++ test/setup_surroundings_ad.sql | 95 +++++++++ test/surroundings_ad.sql | 196 ++++++++++++++++++ test/surroundings_ad_i18n.sql | 49 +++++ test/translate_surroundings_ad.sql | 101 +++++++++ verify/remove_surroundings_ad.sql | 7 + verify/setup_surroundings_ad.sql | 7 + verify/surroundings_ad.sql | 19 ++ verify/surroundings_ad_i18n.sql | 12 ++ verify/translate_surroundings_ad.sql | 7 + web/static/public.css | 85 ++++---- web/templates/admin/surroundings/index.gohtml | 54 +++++ web/templates/public/surroundings.gohtml | 20 +- 32 files changed, 1335 insertions(+), 142 deletions(-) create mode 100644 demo/ad.avif create mode 100644 deploy/remove_surroundings_ad.sql create mode 100644 deploy/setup_surroundings_ad.sql create mode 100644 deploy/surroundings_ad.sql create mode 100644 deploy/surroundings_ad_i18n.sql create mode 100644 deploy/translate_surroundings_ad.sql create mode 100644 revert/remove_surroundings_ad.sql create mode 100644 revert/setup_surroundings_ad.sql create mode 100644 revert/surroundings_ad.sql create mode 100644 revert/surroundings_ad_i18n.sql create mode 100644 revert/translate_surroundings_ad.sql create mode 100644 test/remove_surroundings_ad.sql create mode 100644 test/setup_surroundings_ad.sql create mode 100644 test/surroundings_ad.sql create mode 100644 test/surroundings_ad_i18n.sql create mode 100644 test/translate_surroundings_ad.sql create mode 100644 verify/remove_surroundings_ad.sql create mode 100644 verify/setup_surroundings_ad.sql create mode 100644 verify/surroundings_ad.sql create mode 100644 verify/surroundings_ad_i18n.sql create mode 100644 verify/translate_surroundings_ad.sql diff --git a/demo/ad.avif b/demo/ad.avif new file mode 100644 index 0000000000000000000000000000000000000000..8334116f415da7d784fe589a26bbf5a9a5a995cd GIT binary patch literal 56858 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 SQ;}Y0+KN)67E+T(|JeXpi}Hj3 literal 0 HcmV?d00001 diff --git a/demo/demo.sql b/demo/demo.sql index d36ad28..651bad8 100644 --- a/demo/demo.sql +++ b/demo/demo.sql @@ -73,6 +73,7 @@ select add_media(52, 'wooden_lodges_carousel9.avif', 'image/avif', decode('m4_es select add_media(52, 'wooden_lodges_carouselA.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/wooden_lodges_carouselA.avif]])', 'base64')); select add_media(52, 'wooden_lodges_carouselB.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/wooden_lodges_carouselB.avif]])', 'base64')); select add_media(52, 'wooden_lodges_carouselC.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/wooden_lodges_carouselC.avif]])', 'base64')); +select add_media(52, 'ad.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/ad.avif]])', 'base64')); select setup_home(52, 'Vine a gaudir!'); @@ -659,6 +660,11 @@ select translate_surroundings_highlight(120, 'fr', 'Pyrénées', '

Pyrénées select translate_surroundings_highlight(121, 'fr', 'Gérone', '

Visite incontournable, principalement: le quartier juifs, les Rambles, la cathédrale et les jardins qui l’entourent … Sans oublier ses nombreuses boutiques !

Turisme Girona

'); select translate_surroundings_highlight(122, 'fr', 'Barcelone', '

Barcelone c’est plus que des boutiques et le Barça … Découvrez la richesse de ces quartiers: Gràcia, Barceloneta, …

Turisme Barcelona

'); +select setup_surroundings_ad(52, 104, 'Vine a fer barranquisme per Sadernes!', 'Reserva el teu dia', 'https://www.guiesarania.com/ca/activitats/barrancs-de-perfeccionament/barranc-de-sant-aniol/'); +select translate_surroundings_ad(52, 'en', 'Canyoneering in Sadernes!', 'Book your day'); +select translate_surroundings_ad(52, 'es', '¡Ven a hacer barranquismo en Sadernes!', 'Reserva tu día'); +select translate_surroundings_ad(52, 'fr', 'Venez faire du canyoning à Sadernes !', 'Réservez votre journée'); + alter table booking alter column booking_id restart with 122; insert into booking (company_id, campsite_type_id, holder_name, arrival_date, departure_date, number_dogs, acsi_card, booking_status) diff --git a/deploy/remove_surroundings_ad.sql b/deploy/remove_surroundings_ad.sql new file mode 100644 index 0000000..13f4343 --- /dev/null +++ b/deploy/remove_surroundings_ad.sql @@ -0,0 +1,22 @@ +-- Deploy camper:remove_surroundings_ad to pg +-- requires: roles +-- requires: schema_camper +-- requires: surroundings_ad +-- requires: surroundings_ad_i18n + +begin; + +set search_path to camper, public; + +create or replace function remove_surroundings_ad(company_id integer) returns void as +$$ + delete from surroundings_ad_i18n where company_id = remove_surroundings_ad.company_id; + delete from surroundings_ad where company_id = remove_surroundings_ad.company_id; +$$ + language sql +; + +revoke execute on function remove_surroundings_ad(integer) from public; +grant execute on function remove_surroundings_ad(integer) to admin; + +commit; diff --git a/deploy/setup_surroundings_ad.sql b/deploy/setup_surroundings_ad.sql new file mode 100644 index 0000000..b246dc0 --- /dev/null +++ b/deploy/setup_surroundings_ad.sql @@ -0,0 +1,28 @@ +-- Deploy camper:setup_surroundings_ad to pg +-- requires: roles +-- requires: schema_camper +-- requires: surroundings_ad + +begin; + +set search_path to camper, public; + +create or replace function setup_surroundings_ad(company integer, media_id integer, title text, anchor text, href uri) returns void as +$$ + insert into surroundings_ad (company_id, media_id, title, anchor, href) + values (company, media_id, title, anchor, href) + on conflict (company_id) + do update + set media_id = excluded.media_id + , title = excluded.title + , anchor = excluded.anchor + , href = excluded.href + ; +$$ + language sql +; + +revoke execute on function setup_surroundings_ad(integer, integer, text, text, uri) from public; +grant execute on function setup_surroundings_ad(integer, integer, text, text, uri) to admin; + +commit; diff --git a/deploy/surroundings_ad.sql b/deploy/surroundings_ad.sql new file mode 100644 index 0000000..1d06084 --- /dev/null +++ b/deploy/surroundings_ad.sql @@ -0,0 +1,55 @@ +-- Deploy camper:surroundings_ad to pg +-- requires: roles +-- requires: schema_camper +-- requires: company +-- requires: user_profile + +begin; + +set search_path to camper, public; + +create table surroundings_ad ( + company_id integer not null primary key references company, + media_id integer not null references media, + title text not null, + anchor text not null, + href uri not null +); + +grant select on table surroundings_ad to guest; +grant select on table surroundings_ad to employee; +grant select, insert, update, delete on table surroundings_ad to admin; + +alter table surroundings_ad enable row level security; + +create policy guest_ok +on surroundings_ad +for select +using (true) +; + +create policy insert_to_company +on surroundings_ad +for insert +with check ( + company_id in (select company_id from user_profile) +) +; + +create policy update_company +on surroundings_ad +for update +using ( + company_id in (select company_id from user_profile) +) +; + +create policy delete_from_company +on surroundings_ad +for delete +using ( + company_id in (select company_id from user_profile) +) +; + +commit; diff --git a/deploy/surroundings_ad_i18n.sql b/deploy/surroundings_ad_i18n.sql new file mode 100644 index 0000000..6c8cb88 --- /dev/null +++ b/deploy/surroundings_ad_i18n.sql @@ -0,0 +1,23 @@ +-- Deploy camper:surroundings_ad_i18n to pg +-- requires: roles +-- requires: schema_camper +-- requires: surroundings_ad +-- requires: language + +begin; + +set search_path to camper, public; + +create table surroundings_ad_i18n ( + company_id integer not null references surroundings_ad, + lang_tag text not null references language, + title text not null, + anchor text not null, + primary key (company_id, lang_tag) +); + +grant select on table surroundings_ad_i18n to guest; +grant select on table surroundings_ad_i18n to employee; +grant select, insert, update, delete on table surroundings_ad_i18n to admin; + +commit; diff --git a/deploy/translate_surroundings_ad.sql b/deploy/translate_surroundings_ad.sql new file mode 100644 index 0000000..aaf68dd --- /dev/null +++ b/deploy/translate_surroundings_ad.sql @@ -0,0 +1,26 @@ +-- Deploy camper:translate_surroundings_ad to pg +-- requires: roles +-- requires: schema_camper +-- requires: surroundings_ad_i18n + +begin; + +set search_path to camper, public; + +create or replace function translate_surroundings_ad(company_id integer, lang_tag text, title text, anchor text) returns void as +$$ + insert into surroundings_ad_i18n (company_id, lang_tag, title, anchor) + values (company_id, lang_tag, title, anchor) + on conflict (company_id, lang_tag) + do update + set title = excluded.title + , anchor = excluded.anchor + ; +$$ + language sql +; + +revoke execute on function translate_surroundings_ad(integer, text, text, text) from public; +grant execute on function translate_surroundings_ad(integer, text, text, text) to admin; + +commit; diff --git a/pkg/database/funcs.go b/pkg/database/funcs.go index 2ce8f94..66c94d4 100644 --- a/pkg/database/funcs.go +++ b/pkg/database/funcs.go @@ -190,6 +190,21 @@ func (c *Conn) RemoveSurroundingsHighlight(ctx context.Context, id int) error { return err } +func (tx *Tx) SetupSurroundingsAd(ctx context.Context, companyID int, mediaID int, title string, anchor string, href string) error { + _, err := tx.Exec(ctx, "select setup_surroundings_ad($1, $2, $3, $4, $5)", companyID, mediaID, title, anchor, href) + return err +} + +func (tx *Tx) TranslateSurroundingsAd(ctx context.Context, companyID int, langTag language.Tag, title string, anchor string) error { + _, err := tx.Exec(ctx, "select translate_surroundings_ad($1, $2, $3, $4)", companyID, langTag, title, anchor) + return err +} + +func (c *Conn) RemoveSurroundingsAd(ctx context.Context, companyID int) error { + _, err := c.Exec(ctx, "select remove_surroundings_ad($1)", companyID) + return err +} + func (tx *Tx) SetupHome(ctx context.Context, companyID int, slogan string) error { _, err := tx.Exec(ctx, "select setup_home($1, $2)", companyID, slogan) return err diff --git a/pkg/surroundings/admin.go b/pkg/surroundings/admin.go index 14349c5..366d337 100644 --- a/pkg/surroundings/admin.go +++ b/pkg/surroundings/admin.go @@ -57,6 +57,15 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat default: httplib.MethodNotAllowed(w, r, http.MethodPost) } + case "ad": + switch r.Method { + case http.MethodPut: + updateAd(w, r, user, company, conn) + case http.MethodDelete: + removeAd(w, r, company, conn) + default: + httplib.MethodNotAllowed(w, r, http.MethodPut, http.MethodDelete) + } default: id, err := strconv.Atoi(head) if err != nil { @@ -120,17 +129,27 @@ func orderHighlights(w http.ResponseWriter, r *http.Request, user *auth.User, co } func serveHighlightsIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { + f := newAdForm(company) + if err := f.FillFromDatabase(r.Context(), company, conn); err != nil && !database.ErrorIsNotFound(err) { + panic(err) + } + serveHighlightsIndexWithForm(w, r, user, company, conn, f) +} + +func serveHighlightsIndexWithForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *adForm) { highlights, err := collectHighlightEntries(r.Context(), company, conn) if err != nil { panic(err) } page := &highlightIndex{ + Ad: f, Highlights: highlights, } page.MustRender(w, r, user, company) } type highlightIndex struct { + Ad *adForm Highlights []*highlightEntry } @@ -325,3 +344,127 @@ func (f *highlightForm) Valid(ctx context.Context, conn *database.Conn, l *local func (f *highlightForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "surroundings/form.gohtml", f) } + +func updateAd(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { + f := newAdForm(company) + 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) + } + serveHighlightsIndexWithForm(w, r, user, company, conn, f) + return + } + tx := conn.MustBegin(r.Context()) + defer tx.Rollback(r.Context()) + if err := tx.SetupSurroundingsAd(r.Context(), company.ID, f.Media.Int(), f.Title[f.DefaultLang].Val, f.Anchor[f.DefaultLang].Val, f.HRef.Val); err != nil { + panic(err) + } + for lang := range company.Locales { + l := lang.String() + if l == f.DefaultLang { + continue + } + if err := tx.TranslateSurroundingsAd(r.Context(), company.ID, lang, f.Title[l].Val, f.Anchor[l].Val); err != nil { + panic(err) + } + } + tx.MustCommit(r.Context()) + httplib.Redirect(w, r, "/admin/surroundings", http.StatusSeeOther) +} + +func removeAd(w http.ResponseWriter, r *http.Request, company *auth.Company, conn *database.Conn) { + if err := conn.RemoveSurroundingsAd(r.Context(), company.ID); err != nil { + panic(err) + } + httplib.Redirect(w, r, "/admin/surroundings", http.StatusSeeOther) +} + +type adForm struct { + DefaultLang string + Media *form.Media + Title form.I18nInput + Anchor form.I18nInput + HRef *form.Input +} + +func newAdForm(company *auth.Company) *adForm { + return &adForm{ + DefaultLang: company.DefaultLanguage.String(), + Media: &form.Media{ + Input: &form.Input{ + Name: "media", + }, + Label: locale.PgettextNoop("Ad image", "input"), + Prompt: locale.PgettextNoop("Set ad image", "action"), + }, + Title: form.NewI18nInput(company.Locales, "title"), + Anchor: form.NewI18nInput(company.Locales, "anchor"), + HRef: &form.Input{Name: "href"}, + } +} + +func (f *adForm) FillFromDatabase(ctx context.Context, company *auth.Company, conn *database.Conn) error { + var titles database.RecordArray + var anchors database.RecordArray + err := conn.QueryRow(ctx, ` + select ad.media_id::text + , ad.title + , ad.anchor + , ad.href::text + , array_agg((lang_tag, i18n.title)) + , array_agg((lang_tag, i18n.anchor)) + from surroundings_ad as ad + left join surroundings_ad_i18n as i18n using (company_id) + where company_id = $1 + group by ad.media_id + , ad.title + , ad.anchor + , ad.href +`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, company.ID).Scan(&f.Media.Val, &f.Title[f.DefaultLang].Val, &f.Anchor[f.DefaultLang].Val, &f.HRef.Val, &titles, &anchors) + if err != nil { + return err + } + if err := f.Title.FillArray(titles); err != nil { + return err + } + if err := f.Anchor.FillArray(anchors); err != nil { + return err + } + return nil +} + +func (f *adForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.Media.FillValue(r) + f.Title.FillValue(r) + f.Anchor.FillValue(r) + f.HRef.FillValue(r) + return nil +} + +func (f *adForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) { + v := form.NewValidator(l) + if v.CheckRequired(f.Media.Input, l.GettextNoop("Ad image can not be empty.")) { + if _, err := v.CheckImageMedia(ctx, conn, f.Media.Input, l.GettextNoop("Ad image must be an image media type.")); err != nil { + return false, err + } + } + v.CheckRequired(f.Title[f.DefaultLang], l.GettextNoop("The title can not be empty.")) + v.CheckRequired(f.Anchor[f.DefaultLang], l.GettextNoop("The link text can not be empty.")) + if v.CheckRequired(f.HRef, l.GettextNoop("The ad URL can not be empty")) { + v.CheckValidURL(f.HRef, l.GettextNoop("This web address is not valid. It should be like https://domain.com/.")) + } + return v.AllOK, nil +} diff --git a/pkg/surroundings/public.go b/pkg/surroundings/public.go index 16ff5d2..ceba16d 100644 --- a/pkg/surroundings/public.go +++ b/pkg/surroundings/public.go @@ -45,6 +45,7 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da type surroundingsPage struct { *template.PublicPage + Ad *surroundingsAd Highlights []*highlight } @@ -55,6 +56,7 @@ func newSurroundingsPage() *surroundingsPage { func (p *surroundingsPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { p.Setup(r, user, company, conn) p.Highlights = mustCollectSurroundings(r.Context(), company, conn, user.Locale) + p.Ad = mustCollectAd(r.Context(), company, conn, user.Locale) template.MustRenderPublic(w, r, user, company, "surroundings.gohtml", p) } @@ -96,3 +98,31 @@ func mustCollectSurroundings(ctx context.Context, company *auth.Company, conn *d return items } + +type surroundingsAd struct { + MediaURL string + Title string + Anchor string + HRef string +} + +func mustCollectAd(ctx context.Context, company *auth.Company, conn *database.Conn, loc *locale.Locale) *surroundingsAd { + ad := &surroundingsAd{} + err := conn.QueryRow(ctx, ` + select media.path + , coalesce(i18n.title, ad.title) as l10_title + , coalesce(i18n.anchor, ad.anchor) as l10_anchor + , href::text + from surroundings_ad as ad + join media using (media_id) + left join surroundings_ad_i18n as i18n on ad.company_id = i18n.company_id and lang_tag = $1 + where ad.company_id = $2 + `, loc.Language, company.ID).Scan(&ad.MediaURL, &ad.Title, &ad.Anchor, &ad.HRef) + if err != nil { + if database.ErrorIsNotFound(err) { + return nil + } + panic(err) + } + return ad +} diff --git a/po/ca.po b/po/ca.po index 914fe4a..3d77bc4 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: 2024-01-23 11:46+0100\n" +"POT-Creation-Date: 2024-01-23 14:38+0100\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -241,45 +241,45 @@ msgctxt "day" msgid "Sun" msgstr "dg" -#: web/templates/public/surroundings.gohtml:32 +#: web/templates/public/surroundings.gohtml:30 msgctxt "title" msgid "What to Do Outside the Campsite?" msgstr "Què fer des del càmping?" -#: web/templates/public/surroundings.gohtml:52 +#: web/templates/public/surroundings.gohtml:50 msgctxt "title" msgid "Once at the Campsite, We Can Inform You about What Activities are Available" msgstr "Un cop en el càmping, us podem informar de quines activitats fer" -#: web/templates/public/surroundings.gohtml:55 +#: web/templates/public/surroundings.gohtml:53 msgid "Cycle routes" msgstr "Rutes amb bicicleta" -#: web/templates/public/surroundings.gohtml:56 +#: web/templates/public/surroundings.gohtml:54 msgid "There are many bicycle rental companies in Olot." msgstr "A Olot podeu trobar empreses de lloguer de bicicletes." -#: web/templates/public/surroundings.gohtml:60 +#: web/templates/public/surroundings.gohtml:58 msgid "Routes" msgstr "Rutes" -#: web/templates/public/surroundings.gohtml:61 +#: web/templates/public/surroundings.gohtml:59 msgid "Routes of all kinds, climbing, mountain passes, for all levels." msgstr "Rutes de tota mena, escalada, ports de muntanya, per a tots els nivells." -#: web/templates/public/surroundings.gohtml:65 +#: web/templates/public/surroundings.gohtml:63 msgid "Family outing" msgstr "Excursions familiars" -#: web/templates/public/surroundings.gohtml:66 +#: web/templates/public/surroundings.gohtml:64 msgid "Many outing possibilities, for all ages." msgstr "Múltiples excursions per a totes les edats." -#: web/templates/public/surroundings.gohtml:70 +#: web/templates/public/surroundings.gohtml:68 msgid "Kayak" msgstr "Caiac" -#: web/templates/public/surroundings.gohtml:71 +#: web/templates/public/surroundings.gohtml:69 msgid "There are several points where you can go by kayak, from sections of the Ter river as well as on the coast…." msgstr "Hi ha diversos punts on poder anar amb caiac, des de trams del riu Ter com també a la costa…." @@ -475,6 +475,7 @@ msgstr "Contingut" #: web/templates/admin/season/form.gohtml:73 #: web/templates/admin/services/form.gohtml:80 #: web/templates/admin/surroundings/form.gohtml:69 +#: web/templates/admin/surroundings/index.gohtml:58 #: web/templates/admin/home/index.gohtml:34 #: web/templates/admin/media/form.gohtml:39 msgctxt "action" @@ -506,7 +507,7 @@ msgstr "Afegeix text legal" #: web/templates/admin/campsite/type/index.gohtml:29 #: web/templates/admin/season/index.gohtml:29 #: web/templates/admin/user/index.gohtml:20 -#: web/templates/admin/surroundings/index.gohtml:29 +#: web/templates/admin/surroundings/index.gohtml:83 msgctxt "header" msgid "Name" msgstr "Nom" @@ -636,7 +637,7 @@ msgstr "Afegeix diapositiva" #: web/templates/admin/campsite/carousel/index.gohtml:30 #: web/templates/admin/services/index.gohtml:28 -#: web/templates/admin/surroundings/index.gohtml:28 +#: web/templates/admin/surroundings/index.gohtml:82 #: web/templates/admin/home/index.gohtml:52 #: web/templates/admin/home/index.gohtml:97 msgctxt "header" @@ -656,7 +657,7 @@ msgstr "Llegenda" #: web/templates/admin/services/index.gohtml:30 #: web/templates/admin/services/index.gohtml:75 #: web/templates/admin/user/index.gohtml:23 -#: web/templates/admin/surroundings/index.gohtml:30 +#: web/templates/admin/surroundings/index.gohtml:84 #: web/templates/admin/home/index.gohtml:54 #: web/templates/admin/home/index.gohtml:99 msgctxt "header" @@ -674,7 +675,8 @@ msgstr "Esteu segur de voler esborrar aquesta diapositiva?" #: web/templates/admin/services/index.gohtml:47 #: web/templates/admin/services/index.gohtml:91 #: web/templates/admin/user/index.gohtml:37 -#: web/templates/admin/surroundings/index.gohtml:47 +#: web/templates/admin/surroundings/index.gohtml:63 +#: web/templates/admin/surroundings/index.gohtml:101 #: web/templates/admin/home/index.gohtml:71 #: web/templates/admin/home/index.gohtml:116 msgctxt "action" @@ -1209,19 +1211,43 @@ msgstr "Pàgina de l’entorn" #: web/templates/admin/surroundings/index.gohtml:14 msgctxt "title" +msgid "Ad" +msgstr "Anunci" + +#: web/templates/admin/surroundings/index.gohtml:21 +msgctxt "input" +msgid "Title" +msgstr "Títol" + +#: web/templates/admin/surroundings/index.gohtml:37 +msgctxt "input" +msgid "Link Text" +msgstr "Text de l’enllaç" + +#: web/templates/admin/surroundings/index.gohtml:50 +msgctxt "input" +msgid "Link URL" +msgstr "Adreça de l’enllaç" + +#: web/templates/admin/surroundings/index.gohtml:59 +msgid "Are you sure you wish to delete the ad?" +msgstr "Esteu segur de voler esborrar aquest anunci?" + +#: web/templates/admin/surroundings/index.gohtml:68 +msgctxt "title" msgid "Highlights" msgstr "Punts d’interès" -#: web/templates/admin/surroundings/index.gohtml:15 +#: web/templates/admin/surroundings/index.gohtml:69 msgctxt "action" msgid "Add highlight" msgstr "Afegeix punt d’interès" -#: web/templates/admin/surroundings/index.gohtml:34 +#: web/templates/admin/surroundings/index.gohtml:88 msgid "Are you sure you wish to delete this highlight?" msgstr "Esteu segur de voler esborrar aquest punt d’interès?" -#: web/templates/admin/surroundings/index.gohtml:56 +#: web/templates/admin/surroundings/index.gohtml:110 msgid "No highlights added yet." msgstr "No s’ha afegit cap punt d’interès encara." @@ -1441,7 +1467,7 @@ msgstr "No s’ha trobat cap reserva." #: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:357 #: pkg/campsite/types/feature.go:259 pkg/campsite/types/admin.go:483 #: pkg/season/admin.go:412 pkg/services/admin.go:316 -#: pkg/surroundings/admin.go:321 +#: pkg/surroundings/admin.go:340 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." @@ -1461,12 +1487,12 @@ msgid "Set slide image" msgstr "Estableix la imatge de la diapositiva" #: pkg/carousel/admin.go:346 pkg/campsite/types/carousel.go:299 -#: pkg/surroundings/admin.go:316 +#: pkg/surroundings/admin.go:335 msgid "Slide image can not be empty." msgstr "No podeu deixar la imatge de la diapositiva en blanc." #: pkg/carousel/admin.go:347 pkg/campsite/types/carousel.go:300 -#: pkg/surroundings/admin.go:317 +#: pkg/surroundings/admin.go:336 msgid "Slide image must be an image media type." msgstr "La imatge de la diapositiva ha de ser un mèdia de tipus imatge." @@ -1711,16 +1737,50 @@ msgctxt "role" msgid "admin" msgstr "administrador" -#: pkg/surroundings/admin.go:267 +#: pkg/surroundings/admin.go:286 msgctxt "input" msgid "Highlight image" msgstr "Imatge del punt d’interès" -#: pkg/surroundings/admin.go:268 +#: pkg/surroundings/admin.go:287 msgctxt "action" msgid "Set highlight image" msgstr "Estableix la imatge del punt d’interès" +#: pkg/surroundings/admin.go:407 +msgctxt "input" +msgid "Ad image" +msgstr "Imatge de l’anunci" + +#: pkg/surroundings/admin.go:408 +msgctxt "action" +msgid "Set ad image" +msgstr "Estableix la imatge de l’anunci" + +#: pkg/surroundings/admin.go:459 +msgid "Ad image can not be empty." +msgstr "No podeu deixar la imatge de l’anunci en blanc." + +#: pkg/surroundings/admin.go:460 +msgid "Ad image must be an image media type." +msgstr "La imatge de l’anunci ha de ser un mèdia de tipus imatge." + +#: pkg/surroundings/admin.go:464 +msgid "The title can not be empty." +msgstr "No podeu deixar el títol en blanc." + +#: pkg/surroundings/admin.go:465 +msgid "The link text can not be empty." +msgstr "No podeu deixar el text de l’enllaç en blanc." + +#: pkg/surroundings/admin.go:466 +msgid "The ad URL can not be empty" +msgstr "No podeu deixar l’adreça de l’enllaç en blanc." + +#: pkg/surroundings/admin.go:467 pkg/company/admin.go:221 +msgid "This web address is not valid. It should be like https://domain.com/." +msgstr "Aquesta adreça web no és vàlida. Hauria de ser similar a https://domini.com/." + #: pkg/company/admin.go:200 pkg/booking/public.go:256 msgid "Selected country is not valid." msgstr "El país escollit no és vàlid." @@ -1749,10 +1809,6 @@ msgstr "No podeu deixar el telèfon en blanc." msgid "This phone number is not valid." msgstr "Aquest número de telèfon no és vàlid." -#: pkg/company/admin.go:221 -msgid "This web address is not valid. It should be like https://domain.com/." -msgstr "Aquesta adreça web no és vàlida. Hauria de ser similar a https://domini.com/." - #: pkg/company/admin.go:223 msgid "Address can not be empty." msgstr "No podeu deixar l’adreça en blanc." @@ -2057,10 +2113,6 @@ msgstr "El valor de %s ha de ser com a màxim %d." #~ msgid "Legend" #~ msgstr "Llegenda" -#~ msgctxt "input" -#~ msgid "Title" -#~ msgstr "Títol" - #~ msgctxt "title" #~ msgid "Pages" #~ msgstr "Pàgines" diff --git a/po/es.po b/po/es.po index 522d29d..57db608 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: 2024-01-23 11:46+0100\n" +"POT-Creation-Date: 2024-01-23 14:38+0100\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -241,45 +241,45 @@ msgctxt "day" msgid "Sun" msgstr "do" -#: web/templates/public/surroundings.gohtml:32 +#: web/templates/public/surroundings.gohtml:30 msgctxt "title" msgid "What to Do Outside the Campsite?" msgstr "¿Qué hacer desde el camping?" -#: web/templates/public/surroundings.gohtml:52 +#: web/templates/public/surroundings.gohtml:50 msgctxt "title" msgid "Once at the Campsite, We Can Inform You about What Activities are Available" msgstr "Una vez en el camping, os podemos informar de qué actividades hacer" -#: web/templates/public/surroundings.gohtml:55 +#: web/templates/public/surroundings.gohtml:53 msgid "Cycle routes" msgstr "Rutas en bicicleta" -#: web/templates/public/surroundings.gohtml:56 +#: web/templates/public/surroundings.gohtml:54 msgid "There are many bicycle rental companies in Olot." msgstr "A Olot podéis encontrar empresas de alquiler de bicicletas." -#: web/templates/public/surroundings.gohtml:60 +#: web/templates/public/surroundings.gohtml:58 msgid "Routes" msgstr "Rutas" -#: web/templates/public/surroundings.gohtml:61 +#: web/templates/public/surroundings.gohtml:59 msgid "Routes of all kinds, climbing, mountain passes, for all levels." msgstr "Rutas de todo tipo, escalada, puertos de montaña, para todos los niveles." -#: web/templates/public/surroundings.gohtml:65 +#: web/templates/public/surroundings.gohtml:63 msgid "Family outing" msgstr "Excusiones familiares" -#: web/templates/public/surroundings.gohtml:66 +#: web/templates/public/surroundings.gohtml:64 msgid "Many outing possibilities, for all ages." msgstr "Múltiples excursiones para todas las edades." -#: web/templates/public/surroundings.gohtml:70 +#: web/templates/public/surroundings.gohtml:68 msgid "Kayak" msgstr "Kayak" -#: web/templates/public/surroundings.gohtml:71 +#: web/templates/public/surroundings.gohtml:69 msgid "There are several points where you can go by kayak, from sections of the Ter river as well as on the coast…." msgstr "Hay diversos puntos dónde podéis ir en kayak, desde tramos del río Ter como también en la costa…." @@ -475,6 +475,7 @@ msgstr "Contenido" #: web/templates/admin/season/form.gohtml:73 #: web/templates/admin/services/form.gohtml:80 #: web/templates/admin/surroundings/form.gohtml:69 +#: web/templates/admin/surroundings/index.gohtml:58 #: web/templates/admin/home/index.gohtml:34 #: web/templates/admin/media/form.gohtml:39 msgctxt "action" @@ -506,7 +507,7 @@ msgstr "Añadir texto legal" #: web/templates/admin/campsite/type/index.gohtml:29 #: web/templates/admin/season/index.gohtml:29 #: web/templates/admin/user/index.gohtml:20 -#: web/templates/admin/surroundings/index.gohtml:29 +#: web/templates/admin/surroundings/index.gohtml:83 msgctxt "header" msgid "Name" msgstr "Nombre" @@ -636,7 +637,7 @@ msgstr "Añadir diapositiva" #: web/templates/admin/campsite/carousel/index.gohtml:30 #: web/templates/admin/services/index.gohtml:28 -#: web/templates/admin/surroundings/index.gohtml:28 +#: web/templates/admin/surroundings/index.gohtml:82 #: web/templates/admin/home/index.gohtml:52 #: web/templates/admin/home/index.gohtml:97 msgctxt "header" @@ -656,7 +657,7 @@ msgstr "Leyenda" #: web/templates/admin/services/index.gohtml:30 #: web/templates/admin/services/index.gohtml:75 #: web/templates/admin/user/index.gohtml:23 -#: web/templates/admin/surroundings/index.gohtml:30 +#: web/templates/admin/surroundings/index.gohtml:84 #: web/templates/admin/home/index.gohtml:54 #: web/templates/admin/home/index.gohtml:99 msgctxt "header" @@ -674,7 +675,8 @@ msgstr "¿Estáis seguro de querer borrar esta diapositiva?" #: web/templates/admin/services/index.gohtml:47 #: web/templates/admin/services/index.gohtml:91 #: web/templates/admin/user/index.gohtml:37 -#: web/templates/admin/surroundings/index.gohtml:47 +#: web/templates/admin/surroundings/index.gohtml:63 +#: web/templates/admin/surroundings/index.gohtml:101 #: web/templates/admin/home/index.gohtml:71 #: web/templates/admin/home/index.gohtml:116 msgctxt "action" @@ -1209,19 +1211,43 @@ msgstr "Página del entorno" #: web/templates/admin/surroundings/index.gohtml:14 msgctxt "title" +msgid "Ad" +msgstr "Anuncio" + +#: web/templates/admin/surroundings/index.gohtml:21 +msgctxt "input" +msgid "Title" +msgstr "Título" + +#: web/templates/admin/surroundings/index.gohtml:37 +msgctxt "input" +msgid "Link Text" +msgstr "Texto del enlace" + +#: web/templates/admin/surroundings/index.gohtml:50 +msgctxt "input" +msgid "Link URL" +msgstr "Dirección del enlace" + +#: web/templates/admin/surroundings/index.gohtml:59 +msgid "Are you sure you wish to delete the ad?" +msgstr "¿Estáis seguro de querer borrar este enlace?" + +#: web/templates/admin/surroundings/index.gohtml:68 +msgctxt "title" msgid "Highlights" msgstr "Puntos de interés" -#: web/templates/admin/surroundings/index.gohtml:15 +#: web/templates/admin/surroundings/index.gohtml:69 msgctxt "action" msgid "Add highlight" msgstr "Añadir punto de interés" -#: web/templates/admin/surroundings/index.gohtml:34 +#: web/templates/admin/surroundings/index.gohtml:88 msgid "Are you sure you wish to delete this highlight?" msgstr "¿Estáis seguro de querer borrar este punto de interés?" -#: web/templates/admin/surroundings/index.gohtml:56 +#: web/templates/admin/surroundings/index.gohtml:110 msgid "No highlights added yet." msgstr "No se ha añadido ningún punto de interés todavía." @@ -1441,7 +1467,7 @@ msgstr "No se ha encontrado ninguna reserva." #: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:357 #: pkg/campsite/types/feature.go:259 pkg/campsite/types/admin.go:483 #: pkg/season/admin.go:412 pkg/services/admin.go:316 -#: pkg/surroundings/admin.go:321 +#: pkg/surroundings/admin.go:340 msgid "Name can not be empty." msgstr "No podéis dejar el nombre en blanco." @@ -1461,12 +1487,12 @@ msgid "Set slide image" msgstr "Establecer la imagen de la diapositiva" #: pkg/carousel/admin.go:346 pkg/campsite/types/carousel.go:299 -#: pkg/surroundings/admin.go:316 +#: pkg/surroundings/admin.go:335 msgid "Slide image can not be empty." msgstr "No podéis dejar la imagen de la diapositiva en blanco." #: pkg/carousel/admin.go:347 pkg/campsite/types/carousel.go:300 -#: pkg/surroundings/admin.go:317 +#: pkg/surroundings/admin.go:336 msgid "Slide image must be an image media type." msgstr "La imagen de la diapositiva tiene que ser un medio de tipo imagen." @@ -1711,16 +1737,50 @@ msgctxt "role" msgid "admin" msgstr "administrador" -#: pkg/surroundings/admin.go:267 +#: pkg/surroundings/admin.go:286 msgctxt "input" msgid "Highlight image" msgstr "Imagen del punto de interés" -#: pkg/surroundings/admin.go:268 +#: pkg/surroundings/admin.go:287 msgctxt "action" msgid "Set highlight image" msgstr "Establecer la imagen del punto de interés" +#: pkg/surroundings/admin.go:407 +msgctxt "input" +msgid "Ad image" +msgstr "Imagen del anuncio" + +#: pkg/surroundings/admin.go:408 +msgctxt "action" +msgid "Set ad image" +msgstr "Establecer la imagen del anuncio" + +#: pkg/surroundings/admin.go:459 +msgid "Ad image can not be empty." +msgstr "No podéis dejar la imagen del anuncio en blanco." + +#: pkg/surroundings/admin.go:460 +msgid "Ad image must be an image media type." +msgstr "La imagen del anuncio tiene que ser un medio de tipo imagen." + +#: pkg/surroundings/admin.go:464 +msgid "The title can not be empty." +msgstr "No podéis dejar el título en blanco." + +#: pkg/surroundings/admin.go:465 +msgid "The link text can not be empty." +msgstr "No podéis dejar el texto del enlace en blanco." + +#: pkg/surroundings/admin.go:466 +msgid "The ad URL can not be empty" +msgstr "No podéis dejar la dirección del enlace en blanco." + +#: pkg/surroundings/admin.go:467 pkg/company/admin.go:221 +msgid "This web address is not valid. It should be like https://domain.com/." +msgstr "Esta dirección web no es válida. Tiene que ser parecido a https://dominio.com/." + #: pkg/company/admin.go:200 pkg/booking/public.go:256 msgid "Selected country is not valid." msgstr "El país escogido no es válido." @@ -1749,10 +1809,6 @@ msgstr "No podéis dejar el teléfono en blanco." msgid "This phone number is not valid." msgstr "Este teléfono no es válido." -#: pkg/company/admin.go:221 -msgid "This web address is not valid. It should be like https://domain.com/." -msgstr "Esta dirección web no es válida. Tiene que ser parecido a https://dominio.com/." - #: pkg/company/admin.go:223 msgid "Address can not be empty." msgstr "No podéis dejar la dirección en blanco." @@ -2057,10 +2113,6 @@ msgstr "%s tiene que ser como máximo %d" #~ msgid "Legend" #~ msgstr "Leyenda" -#~ msgctxt "input" -#~ msgid "Title" -#~ msgstr "Título" - #~ msgctxt "title" #~ msgid "Pages" #~ msgstr "Páginas" diff --git a/po/fr.po b/po/fr.po index 8499d72..f1ae6cf 100644 --- a/po/fr.po +++ b/po/fr.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2024-01-23 11:46+0100\n" +"POT-Creation-Date: 2024-01-23 14:38+0100\n" "PO-Revision-Date: 2023-12-20 10:13+0100\n" "Last-Translator: Oriol Carbonell \n" "Language-Team: French \n" @@ -242,45 +242,45 @@ msgctxt "day" msgid "Sun" msgstr "Dim." -#: web/templates/public/surroundings.gohtml:32 +#: web/templates/public/surroundings.gohtml:30 msgctxt "title" msgid "What to Do Outside the Campsite?" msgstr "Que faire à l’extérieur du camping ?" -#: web/templates/public/surroundings.gohtml:52 +#: web/templates/public/surroundings.gohtml:50 msgctxt "title" msgid "Once at the Campsite, We Can Inform You about What Activities are Available" msgstr "Une fois au camping, nous pourrons vous informer sur les activités disponibles" -#: web/templates/public/surroundings.gohtml:55 +#: web/templates/public/surroundings.gohtml:53 msgid "Cycle routes" msgstr "Pistes cyclables" -#: web/templates/public/surroundings.gohtml:56 +#: web/templates/public/surroundings.gohtml:54 msgid "There are many bicycle rental companies in Olot." msgstr "Il existe de nombreuses sociétés de location de vélos à Olot." -#: web/templates/public/surroundings.gohtml:60 +#: web/templates/public/surroundings.gohtml:58 msgid "Routes" msgstr "Itinéraires" -#: web/templates/public/surroundings.gohtml:61 +#: web/templates/public/surroundings.gohtml:59 msgid "Routes of all kinds, climbing, mountain passes, for all levels." msgstr "Itinéraires de toutes sortes, escalade, cols de montagne, pour tous les niveaux." -#: web/templates/public/surroundings.gohtml:65 +#: web/templates/public/surroundings.gohtml:63 msgid "Family outing" msgstr "Sortie en famille" -#: web/templates/public/surroundings.gohtml:66 +#: web/templates/public/surroundings.gohtml:64 msgid "Many outing possibilities, for all ages." msgstr "Nombreuses possibilités de sorties, pour tous les âges." -#: web/templates/public/surroundings.gohtml:70 +#: web/templates/public/surroundings.gohtml:68 msgid "Kayak" msgstr "Kayak" -#: web/templates/public/surroundings.gohtml:71 +#: web/templates/public/surroundings.gohtml:69 msgid "There are several points where you can go by kayak, from sections of the Ter river as well as on the coast…." msgstr "Il y a plusieurs points où vous pouvez aller en kayak, à partir de sections de la rivière Ter ainsi que sur la côte…." @@ -476,6 +476,7 @@ msgstr "Contenu" #: web/templates/admin/season/form.gohtml:73 #: web/templates/admin/services/form.gohtml:80 #: web/templates/admin/surroundings/form.gohtml:69 +#: web/templates/admin/surroundings/index.gohtml:58 #: web/templates/admin/home/index.gohtml:34 #: web/templates/admin/media/form.gohtml:39 msgctxt "action" @@ -507,7 +508,7 @@ msgstr "Ajouter un texte juridique" #: web/templates/admin/campsite/type/index.gohtml:29 #: web/templates/admin/season/index.gohtml:29 #: web/templates/admin/user/index.gohtml:20 -#: web/templates/admin/surroundings/index.gohtml:29 +#: web/templates/admin/surroundings/index.gohtml:83 msgctxt "header" msgid "Name" msgstr "Nom" @@ -637,7 +638,7 @@ msgstr "Ajouter la diapositive" #: web/templates/admin/campsite/carousel/index.gohtml:30 #: web/templates/admin/services/index.gohtml:28 -#: web/templates/admin/surroundings/index.gohtml:28 +#: web/templates/admin/surroundings/index.gohtml:82 #: web/templates/admin/home/index.gohtml:52 #: web/templates/admin/home/index.gohtml:97 msgctxt "header" @@ -657,7 +658,7 @@ msgstr "Légende" #: web/templates/admin/services/index.gohtml:30 #: web/templates/admin/services/index.gohtml:75 #: web/templates/admin/user/index.gohtml:23 -#: web/templates/admin/surroundings/index.gohtml:30 +#: web/templates/admin/surroundings/index.gohtml:84 #: web/templates/admin/home/index.gohtml:54 #: web/templates/admin/home/index.gohtml:99 msgctxt "header" @@ -675,7 +676,8 @@ msgstr "Êtes-vous sûr de vouloir supprimer cette diapositive ?" #: web/templates/admin/services/index.gohtml:47 #: web/templates/admin/services/index.gohtml:91 #: web/templates/admin/user/index.gohtml:37 -#: web/templates/admin/surroundings/index.gohtml:47 +#: web/templates/admin/surroundings/index.gohtml:63 +#: web/templates/admin/surroundings/index.gohtml:101 #: web/templates/admin/home/index.gohtml:71 #: web/templates/admin/home/index.gohtml:116 msgctxt "action" @@ -1210,19 +1212,43 @@ msgstr "Page de l’entourage" #: web/templates/admin/surroundings/index.gohtml:14 msgctxt "title" +msgid "Ad" +msgstr "Annonce" + +#: web/templates/admin/surroundings/index.gohtml:21 +msgctxt "input" +msgid "Title" +msgstr "Titre" + +#: web/templates/admin/surroundings/index.gohtml:37 +msgctxt "input" +msgid "Link Text" +msgstr "Texte du lien" + +#: web/templates/admin/surroundings/index.gohtml:50 +msgctxt "input" +msgid "Link URL" +msgstr "Address du lien" + +#: web/templates/admin/surroundings/index.gohtml:59 +msgid "Are you sure you wish to delete the ad?" +msgstr "Êtes-vous sûr de vouloir supprimer cette annonce ?" + +#: web/templates/admin/surroundings/index.gohtml:68 +msgctxt "title" msgid "Highlights" msgstr "Points d’intérêt" -#: web/templates/admin/surroundings/index.gohtml:15 +#: web/templates/admin/surroundings/index.gohtml:69 msgctxt "action" msgid "Add highlight" msgstr "Ajouter un point d’intérêt" -#: web/templates/admin/surroundings/index.gohtml:34 +#: web/templates/admin/surroundings/index.gohtml:88 msgid "Are you sure you wish to delete this highlight?" msgstr "Êtes-vous sûr de vouloir supprimer cette point d’intérêt ?" -#: web/templates/admin/surroundings/index.gohtml:56 +#: web/templates/admin/surroundings/index.gohtml:110 msgid "No highlights added yet." msgstr "Aucun point d’intérêt n’a encore été ajoutée." @@ -1442,7 +1468,7 @@ msgstr "Aucune réservation trouvée." #: pkg/legal/admin.go:258 pkg/app/user.go:249 pkg/campsite/types/option.go:357 #: pkg/campsite/types/feature.go:259 pkg/campsite/types/admin.go:483 #: pkg/season/admin.go:412 pkg/services/admin.go:316 -#: pkg/surroundings/admin.go:321 +#: pkg/surroundings/admin.go:340 msgid "Name can not be empty." msgstr "Le nom ne peut pas être laissé vide." @@ -1462,12 +1488,12 @@ msgid "Set slide image" msgstr "Définir l’image de la diapositive" #: pkg/carousel/admin.go:346 pkg/campsite/types/carousel.go:299 -#: pkg/surroundings/admin.go:316 +#: pkg/surroundings/admin.go:335 msgid "Slide image can not be empty." msgstr "L’image de la diapositive ne peut pas être vide." #: pkg/carousel/admin.go:347 pkg/campsite/types/carousel.go:300 -#: pkg/surroundings/admin.go:317 +#: pkg/surroundings/admin.go:336 msgid "Slide image must be an image media type." msgstr "L’image de la diapositive doit être de type média d’image." @@ -1712,16 +1738,50 @@ msgctxt "role" msgid "admin" msgstr "administrateur" -#: pkg/surroundings/admin.go:267 +#: pkg/surroundings/admin.go:286 msgctxt "input" msgid "Highlight image" msgstr "Image du point d’intérêt" -#: pkg/surroundings/admin.go:268 +#: pkg/surroundings/admin.go:287 msgctxt "action" msgid "Set highlight image" msgstr "Définir l’image du point d’intérêt" +#: pkg/surroundings/admin.go:407 +msgctxt "input" +msgid "Ad image" +msgstr "Image de l’annonce" + +#: pkg/surroundings/admin.go:408 +msgctxt "action" +msgid "Set ad image" +msgstr "Définir l’image de l’annonce" + +#: pkg/surroundings/admin.go:459 +msgid "Ad image can not be empty." +msgstr "L’image de l’annonce ne peut pas être vide." + +#: pkg/surroundings/admin.go:460 +msgid "Ad image must be an image media type." +msgstr "L’image de l’annonce doit être de type média d’image." + +#: pkg/surroundings/admin.go:464 +msgid "The title can not be empty." +msgstr "Le titre ne peut pas être vide." + +#: pkg/surroundings/admin.go:465 +msgid "The link text can not be empty." +msgstr "Le texte du lien ne peut pas être vide." + +#: pkg/surroundings/admin.go:466 +msgid "The ad URL can not be empty" +msgstr "L’addresse du lien ne peut pas être vide." + +#: pkg/surroundings/admin.go:467 pkg/company/admin.go:221 +msgid "This web address is not valid. It should be like https://domain.com/." +msgstr "Cette adresse web n’est pas valide. Il devrait en être https://domain.com/." + #: pkg/company/admin.go:200 pkg/booking/public.go:256 msgid "Selected country is not valid." msgstr "Le pays sélectionné n’est pas valide." @@ -1750,10 +1810,6 @@ msgstr "Le téléphone ne peut pas être vide." msgid "This phone number is not valid." msgstr "Ce numéro de téléphone n’est pas valide." -#: pkg/company/admin.go:221 -msgid "This web address is not valid. It should be like https://domain.com/." -msgstr "Cette adresse web n’est pas valide. Il devrait en être https://domain.com/." - #: pkg/company/admin.go:223 msgid "Address can not be empty." msgstr "L’adresse ne peut pas être vide." diff --git a/revert/remove_surroundings_ad.sql b/revert/remove_surroundings_ad.sql new file mode 100644 index 0000000..a788596 --- /dev/null +++ b/revert/remove_surroundings_ad.sql @@ -0,0 +1,7 @@ +-- Revert camper:remove_surroundings_ad from pg + +begin; + +drop function if exists camper.remove_surroundings_ad(integer); + +commit; diff --git a/revert/setup_surroundings_ad.sql b/revert/setup_surroundings_ad.sql new file mode 100644 index 0000000..2ed6f65 --- /dev/null +++ b/revert/setup_surroundings_ad.sql @@ -0,0 +1,7 @@ +-- Revert camper:setup_surroundings_ad from pg + +begin; + +drop function if exists camper.setup_surroundings_ad(integer, integer, text, text, uri); + +commit; diff --git a/revert/surroundings_ad.sql b/revert/surroundings_ad.sql new file mode 100644 index 0000000..766618a --- /dev/null +++ b/revert/surroundings_ad.sql @@ -0,0 +1,7 @@ +-- Revert camper:surroundings_ad from pg + +begin; + +drop table if exists camper.surroundings_ad; + +commit; diff --git a/revert/surroundings_ad_i18n.sql b/revert/surroundings_ad_i18n.sql new file mode 100644 index 0000000..6ce9818 --- /dev/null +++ b/revert/surroundings_ad_i18n.sql @@ -0,0 +1,7 @@ +-- Revert camper:surroundings_ad_i18n from pg + +begin; + +drop table if exists camper.surroundings_ad_i18n; + +commit; diff --git a/revert/translate_surroundings_ad.sql b/revert/translate_surroundings_ad.sql new file mode 100644 index 0000000..e9029b7 --- /dev/null +++ b/revert/translate_surroundings_ad.sql @@ -0,0 +1,7 @@ +-- Revert camper:translate_surroundings_ad from pg + +begin; + +drop function if exists camper.translate_surroundings_ad(integer, text, text, text); + +commit; diff --git a/sqitch.plan b/sqitch.plan index d01074f..bc390ab 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -172,3 +172,8 @@ home [roles schema_camper company user_profile] 2024-01-23T10:02:08Z jordi fita setup_home [roles schema_camper home] 2024-01-23T10:14:14Z jordi fita mas # Add function to set up home page home_i18n [roles schema_camper home] 2024-01-23T10:19:47Z jordi fita mas # Add table to hold translated texts for home page translate_home [roles schema_camper home_i18n] 2024-01-23T10:24:02Z jordi fita mas # Add function to translate home texts +surroundings_ad [roles schema_camper company user_profile] 2024-01-23T11:23:58Z jordi fita mas # Add table to hold the ad in surroundings page +setup_surroundings_ad [roles schema_camper surroundings_ad] 2024-01-23T11:32:26Z jordi fita mas # Add function to set up surroundings ad +surroundings_ad_i18n [roles schema_camper surroundings_ad language] 2024-01-23T11:44:44Z jordi fita mas # Add relation for the translation of the surrounding ad +remove_surroundings_ad [roles schema_camper surroundings_ad surroundings_ad_i18n] 2024-01-23T11:41:47Z jordi fita mas # Add function to remove surroundings ad +translate_surroundings_ad [roles schema_camper surroundings_ad_i18n] 2024-01-23T12:06:32Z jordi fita mas # Add function to translate surroundings ad diff --git a/test/remove_surroundings_ad.sql b/test/remove_surroundings_ad.sql new file mode 100644 index 0000000..93d7996 --- /dev/null +++ b/test/remove_surroundings_ad.sql @@ -0,0 +1,99 @@ +-- Test remove_surroundings_ad +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_surroundings_ad', array['integer']); +select function_lang_is('camper', 'remove_surroundings_ad', array['integer'], 'sql'); +select function_returns('camper', 'remove_surroundings_ad', array['integer'], 'void'); +select isnt_definer('camper', 'remove_surroundings_ad', array['integer']); +select volatility_is('camper', 'remove_surroundings_ad', array['integer'], 'volatile'); +select function_privs_are('camper', 'remove_surroundings_ad', array['integer'], 'guest', array[]::text[]); +select function_privs_are('camper', 'remove_surroundings_ad', array['integer'], 'employee', array[]::text[]); +select function_privs_are('camper', 'remove_surroundings_ad', array['integer'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'remove_surroundings_ad', array['integer'], 'authenticator', array[]::text[]); + +set client_min_messages to warning; +truncate surroundings_ad_i18n cascade; +truncate surroundings_ad cascade; +truncate media cascade; +truncate media_content 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, rtc_number, tourist_tax, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, '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_content (media_type, bytes) +values ('text/plain', 'content2') + , ('text/plain', 'content4') +; + +insert into media (media_id, company_id, original_filename, content_hash) +values ( 7, 2, 'text2.txt', sha256('content2')) + , ( 8, 4, 'text3.txt', sha256('content4')) +; + +insert into surroundings_ad (company_id, media_id, title, anchor, href) +values (2, 7, 'Ad 2', 'Go!', 'https://ddg.gg/') + , (4, 8, 'Ad 4', 'Go!', 'https://ddg.gg/') +; + +insert into surroundings_ad_i18n (company_id, lang_tag, title, anchor) +values (2, 'ca', 'Anunci 2', 'Ves!') + , (2, 'es', 'Anuncio 2', '¡Ve!') + , (4, 'ca', 'Anunci 4', 'Ves!') + , (4, 'es', 'Anuncio 4', '¡Ve!') +; + +select lives_ok( + $$ select remove_surroundings_ad(2) $$, + 'Should be able to delete the ad from the first company' +); + +select bag_eq( + $$ select company_id, media_id, title, anchor, href::text from surroundings_ad $$, + $$ values (4, 8, 'Ad 4', 'Go!', 'https://ddg.gg/') + $$, + 'The row should have been deleted.' +); + +select bag_eq( + $$ select company_id, lang_tag, title, anchor from surroundings_ad_i18n $$, + $$ values (4, 'ca', 'Anunci 4', 'Ves!') + , (4, 'es', 'Anuncio 4', '¡Ve!') + $$, + 'The translations should have been deleted.' +); + + +select * +from finish(); + +rollback; diff --git a/test/setup_surroundings_ad.sql b/test/setup_surroundings_ad.sql new file mode 100644 index 0000000..2a6303f --- /dev/null +++ b/test/setup_surroundings_ad.sql @@ -0,0 +1,95 @@ +-- Test setup_surroundings_ad +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(15); + +set search_path to camper, public; + +select has_function('camper', 'setup_surroundings_ad', array['integer', 'integer', 'text', 'text', 'uri']); +select function_lang_is('camper', 'setup_surroundings_ad', array['integer', 'integer', 'text', 'text', 'uri'], 'sql'); +select function_returns('camper', 'setup_surroundings_ad', array['integer', 'integer', 'text', 'text', 'uri'], 'void'); +select isnt_definer('camper', 'setup_surroundings_ad', array['integer', 'integer', 'text', 'text', 'uri']); +select volatility_is('camper', 'setup_surroundings_ad', array['integer', 'integer', 'text', 'text', 'uri'], 'volatile'); +select function_privs_are('camper', 'setup_surroundings_ad', array ['integer', 'integer', 'text', 'text', 'uri'], 'guest', array[]::text[]); +select function_privs_are('camper', 'setup_surroundings_ad', array ['integer', 'integer', 'text', 'text', 'uri'], 'employee', array[]::text[]); +select function_privs_are('camper', 'setup_surroundings_ad', array ['integer', 'integer', 'text', 'text', 'uri'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'setup_surroundings_ad', array ['integer', 'integer', 'text', 'text', 'uri'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate surroundings_ad cascade; +truncate media cascade; +truncate media_content 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, rtc_number, tourist_tax, country_code, currency_code, default_lang_tag) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca') + , (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, 'FR', 'USD', 'ca') +; + +insert into media_content (media_type, bytes) +values ('text/plain', 'content2') + , ('text/plain', 'content3') + , ('text/plain', 'content4') + , ('text/plain', 'content5') +; + +insert into media (media_id, company_id, original_filename, content_hash) +values ( 7, 1, 'text2.txt', sha256('content2')) + , ( 8, 1, 'text3.txt', sha256('content3')) + , ( 9, 2, 'text4.txt', sha256('content4')) + , (10, 2, 'text5.txt', sha256('content5')) +; + + +prepare surroundings_ad_data as +select company_id, media_id, title, anchor, href::text +from surroundings_ad +; + +select lives_ok( + $$ select setup_surroundings_ad(1, 7, 'Enjoy!', 'Good Link', 'https://ddg.gg/') $$, + 'Should be able to setup surroundings_ad for the first company' +); + +select lives_ok( + $$ select setup_surroundings_ad(2, 9, 'Go Away!', 'Bad Link', 'https://google.es/') $$, + 'Should be able to setup surroundings_ad for the second company' +); + +select bag_eq( + 'surroundings_ad_data', + $$ values (1, 7, 'Enjoy!', 'Good Link', 'https://ddg.gg/') + , (2, 9, 'Go Away!', 'Bad Link', 'https://google.es/') + $$, + 'Should have inserted all surrounding ad' +); + +select lives_ok( + $$ select setup_surroundings_ad(1, 8, 'Come Back!', 'Good Memories', 'https://astalavista.box.sk/') $$, + 'Should be able to update surroundings_ad for the first company' +); + +select lives_ok( + $$ select setup_surroundings_ad(2, 10, 'Quoi?', 'Ecs', 'https://yahoo.fr/') $$, + 'Should be able to update surroundings_ad for the second company' +); + +select bag_eq( + 'surroundings_ad_data', + $$ values (1, 8, 'Come Back!', 'Good Memories', 'https://astalavista.box.sk/') + , (2, 10, 'Quoi?', 'Ecs', 'https://yahoo.fr/') + $$, + 'Should have updated all surrounding ad' +); + + +select * +from finish(); + +rollback; diff --git a/test/surroundings_ad.sql b/test/surroundings_ad.sql new file mode 100644 index 0000000..a3c903e --- /dev/null +++ b/test/surroundings_ad.sql @@ -0,0 +1,196 @@ +-- Test surroundings_ad +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(44); + +set search_path to camper, public; + +select has_table('surroundings_ad'); +select has_pk('surroundings_ad'); +select table_privs_are('surroundings_ad', 'guest', array['SELECT']); +select table_privs_are('surroundings_ad', 'employee', array['SELECT']); +select table_privs_are('surroundings_ad', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('surroundings_ad', 'authenticator', array[]::text[]); + +select has_column('surroundings_ad', 'company_id'); +select col_is_pk('surroundings_ad', 'company_id'); +select col_is_fk('surroundings_ad', 'company_id'); +select fk_ok('surroundings_ad', 'company_id', 'company', 'company_id'); +select col_type_is('surroundings_ad', 'company_id', 'integer'); +select col_not_null('surroundings_ad', 'company_id'); +select col_hasnt_default('surroundings_ad', 'company_id'); + +select has_column('surroundings_ad', 'media_id'); +select col_is_fk('surroundings_ad', 'media_id'); +select fk_ok('surroundings_ad', 'media_id', 'media', 'media_id'); +select col_type_is('surroundings_ad', 'media_id', 'integer'); +select col_not_null('surroundings_ad', 'media_id'); +select col_hasnt_default('surroundings_ad', 'media_id'); + +select has_column('surroundings_ad', 'title'); +select col_type_is('surroundings_ad', 'title', 'text'); +select col_not_null('surroundings_ad', 'title'); +select col_hasnt_default('surroundings_ad', 'title'); + +select has_column('surroundings_ad', 'anchor'); +select col_type_is('surroundings_ad', 'anchor', 'text'); +select col_not_null('surroundings_ad', 'anchor'); +select col_hasnt_default('surroundings_ad', 'anchor'); + +select has_column('surroundings_ad', 'href'); +select col_type_is('surroundings_ad', 'href', 'uri'); +select col_not_null('surroundings_ad', 'href'); +select col_hasnt_default('surroundings_ad', 'href'); + + +set client_min_messages to warning; +truncate surroundings_ad cascade; +truncate media cascade; +truncate media_content 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, rtc_number, tourist_tax, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, 'FR', 'USD', 'ca') + , (6, 'Company 5', 'XX345', '', '777-777-777', 'c@c', '', '', '', '', '', '', 60, '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_content (media_type, bytes) +values ('text/plain', 'content2') + , ('text/plain', 'content4') +; + +insert into media (media_id, company_id, original_filename, content_hash) +values ( 7, 2, 'text2.txt', sha256('content2')) + , ( 8, 4, 'text3.txt', sha256('content4')) +; + +insert into surroundings_ad (company_id, media_id, title, anchor, href) +values (2, 7, 'Ad 2', 'Go!', 'https://ddg.gg/') + , (4, 8, 'Ad 4', 'Go!', 'https://ddg.gg/') +; + +prepare surroundings_ad_data as +select company_id, title +from surroundings_ad +order by company_id, title; + +set role guest; +select bag_eq( + 'surroundings_ad_data', + $$ values (2, 'Ad 2') + , (4, 'Ad 4') + $$, + 'Everyone should be able to list all surrounding ad across all companies' +); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2'); + +select lives_ok( + $$ delete from surroundings_ad where company_id = 2 $$, + 'Admin from company 2 should be able to delete surrounding ad from that company.' +); + +select bag_eq( + 'surroundings_ad_data', + $$ values (4, 'Ad 4') + $$, + 'The row should have been deleted.' +); + +select lives_ok( + $$ insert into surroundings_ad(company_id, media_id, title, anchor, href) values (2, 7, 'Another Ad', 'Go!', 'https://ddg.gg/') $$, + 'Admin from company 2 should be able to insert a new surrounding ad to that company.' +); + +select bag_eq( + 'surroundings_ad_data', + $$ values (2, 'Another Ad') + , (4, 'Ad 4') + $$, + 'The new row should have been added' +); + +select lives_ok( + $$ update surroundings_ad set title = 'Ad 2' where company_id = 2 $$, + 'Admin from company 2 should be able to update surrounding ad of that company.' +); + +select bag_eq( + 'surroundings_ad_data', + $$ values (2, 'Ad 2') + , (4, 'Ad 4') + $$, + 'The row should have been updated.' +); + +select throws_ok( + $$ insert into surroundings_ad (company_id, title) values (6, 'Ad 6') $$, + '42501', 'new row violates row-level security policy for table "surroundings_ad"', + 'Admin from company 2 should NOT be able to insert new surrounding ad to company 6.' +); + +select lives_ok( + $$ update surroundings_ad set title = 'Nope' where company_id = 4 $$, + 'Admin from company 2 should not be able to update new surrounding ad of company 4, but no error if company_id is not changed.' +); + +select bag_eq( + 'surroundings_ad_data', + $$ values (2, 'Ad 2') + , (4, 'Ad 4') + $$, + 'No row should have been changed.' +); + +select throws_ok( + $$ update surroundings_ad set company_id = 6 where company_id = 2 $$, + '42501', 'new row violates row-level security policy for table "surroundings_ad"', + 'Admin from company 2 should NOT be able to move surrounding ad to company 6' +); + +select lives_ok( + $$ delete from surroundings_ad where company_id = 4 $$, + 'Admin from company 2 should NOT be able to delete surrounding ad from company 4, but not error is thrown' +); + +select bag_eq( + 'surroundings_ad_data', + $$ values (2, 'Ad 2') + , (4, 'Ad 4') + $$, + 'No row should have been changed' +); + +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/surroundings_ad_i18n.sql b/test/surroundings_ad_i18n.sql new file mode 100644 index 0000000..d262e14 --- /dev/null +++ b/test/surroundings_ad_i18n.sql @@ -0,0 +1,49 @@ +-- Test surroundings_ad_i18n +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(27); + +set search_path to camper, public; + +select has_table('surroundings_ad_i18n'); +select has_pk('surroundings_ad_i18n'); +select col_is_pk('surroundings_ad_i18n', array['company_id', 'lang_tag']); +select table_privs_are('surroundings_ad_i18n', 'guest', array['SELECT']); +select table_privs_are('surroundings_ad_i18n', 'employee', array['SELECT']); +select table_privs_are('surroundings_ad_i18n', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('surroundings_ad_i18n', 'authenticator', array[]::text[]); + +select has_column('surroundings_ad_i18n', 'company_id'); +select col_is_fk('surroundings_ad_i18n', 'company_id'); +select fk_ok('surroundings_ad_i18n', 'company_id', 'surroundings_ad', 'company_id'); +select col_type_is('surroundings_ad_i18n', 'company_id', 'integer'); +select col_not_null('surroundings_ad_i18n', 'company_id'); +select col_hasnt_default('surroundings_ad_i18n', 'company_id'); + +select has_column('surroundings_ad_i18n', 'lang_tag'); +select col_is_fk('surroundings_ad_i18n', 'lang_tag'); +select fk_ok('surroundings_ad_i18n', 'lang_tag', 'language', 'lang_tag'); +select col_type_is('surroundings_ad_i18n', 'lang_tag', 'text'); +select col_not_null('surroundings_ad_i18n', 'lang_tag'); +select col_hasnt_default('surroundings_ad_i18n', 'lang_tag'); + +select has_column('surroundings_ad_i18n', 'title'); +select col_type_is('surroundings_ad_i18n', 'title', 'text'); +select col_not_null('surroundings_ad_i18n', 'title'); +select col_hasnt_default('surroundings_ad_i18n', 'title'); + +select has_column('surroundings_ad_i18n', 'anchor'); +select col_type_is('surroundings_ad_i18n', 'anchor', 'text'); +select col_not_null('surroundings_ad_i18n', 'anchor'); +select col_hasnt_default('surroundings_ad_i18n', 'anchor'); + + +select * +from finish(); + +rollback; + diff --git a/test/translate_surroundings_ad.sql b/test/translate_surroundings_ad.sql new file mode 100644 index 0000000..c90350c --- /dev/null +++ b/test/translate_surroundings_ad.sql @@ -0,0 +1,101 @@ +-- Test translate_surroundings_ad +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_surroundings_ad', array['integer', 'text', 'text', 'text']); +select function_lang_is('camper', 'translate_surroundings_ad', array['integer', 'text', 'text', 'text'], 'sql'); +select function_returns('camper', 'translate_surroundings_ad', array['integer', 'text', 'text', 'text'], 'void'); +select isnt_definer('camper', 'translate_surroundings_ad', array['integer', 'text', 'text', 'text']); +select volatility_is('camper', 'translate_surroundings_ad', array['integer', 'text', 'text', 'text'], 'volatile'); +select function_privs_are('camper', 'translate_surroundings_ad', array['integer', 'text', 'text', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'translate_surroundings_ad', array['integer', 'text', 'text', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'translate_surroundings_ad', array['integer', 'text', 'text', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'translate_surroundings_ad', array['integer', 'text', 'text', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate surroundings_ad_i18n cascade; +truncate surroundings_ad cascade; +truncate media cascade; +truncate media_content 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, rtc_number, tourist_tax, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, '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_content (media_type, bytes) +values ('text/plain', 'content2') + , ('text/plain', 'content4') +; + +insert into media (media_id, company_id, original_filename, content_hash) +values ( 7, 2, 'text2.txt', sha256('content2')) + , ( 8, 4, 'text3.txt', sha256('content4')) +; + +insert into surroundings_ad (company_id, media_id, title, anchor, href) +values (2, 7, 'Ad 2', 'Go!', 'https://ddg.gg/') + , (4, 8, 'Ad 4', 'Go!', 'https://ddg.gg/') +; + +insert into surroundings_ad_i18n (company_id, lang_tag, title, anchor) +values (2, 'ca', 'anun 2', 'au!') +; + +select lives_ok( + $$ select translate_surroundings_ad(4, 'es', 'Anuncio 4', '¡Ve!') $$, + 'Should be able to translate the ad from the first company to Spanish' +); + +select lives_ok( + $$ select translate_surroundings_ad(4, 'ca', 'Anunci 4', 'Ves!') $$, + 'Should be able to translate the ad from the first company to Catalan' +); + +select lives_ok( + $$ select translate_surroundings_ad(2, 'ca', 'Anunci 2', 'Ups!') $$, + 'Should be able to update Catalan translation of the ad from the first company' +); + +select bag_eq( + $$ select company_id, lang_tag, title, anchor from surroundings_ad_i18n $$, + $$ values (2, 'ca', 'Anunci 2', 'Ups!') + , (4, 'ca', 'Anunci 4', 'Ves!') + , (4, 'es', 'Anuncio 4', '¡Ve!') + $$, + 'The translations should have been updated.' +); + + +select * +from finish(); + +rollback; diff --git a/verify/remove_surroundings_ad.sql b/verify/remove_surroundings_ad.sql new file mode 100644 index 0000000..c9cfa4d --- /dev/null +++ b/verify/remove_surroundings_ad.sql @@ -0,0 +1,7 @@ +-- Verify camper:remove_surroundings_ad on pg + +begin; + +select has_function_privilege('camper.remove_surroundings_ad(integer)', 'execute'); + +rollback; diff --git a/verify/setup_surroundings_ad.sql b/verify/setup_surroundings_ad.sql new file mode 100644 index 0000000..9c3c38b --- /dev/null +++ b/verify/setup_surroundings_ad.sql @@ -0,0 +1,7 @@ +-- Verify camper:setup_surroundings_ad on pg + +begin; + +select has_function_privilege('camper.setup_surroundings_ad(integer, integer, text, text, uri)', 'execute'); + +rollback; diff --git a/verify/surroundings_ad.sql b/verify/surroundings_ad.sql new file mode 100644 index 0000000..7b75f61 --- /dev/null +++ b/verify/surroundings_ad.sql @@ -0,0 +1,19 @@ +-- Verify camper:surroundings_ad on pg + +begin; + +select company_id + , media_id + , title + , anchor + , href +from camper.surroundings_ad +where false; + +select 1 / count(*) from pg_class where oid = 'camper.surroundings_ad'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.surroundings_ad'::regclass; +select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.surroundings_ad'::regclass; +select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.surroundings_ad'::regclass; +select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.surroundings_ad'::regclass; + +rollback; diff --git a/verify/surroundings_ad_i18n.sql b/verify/surroundings_ad_i18n.sql new file mode 100644 index 0000000..3081a1d --- /dev/null +++ b/verify/surroundings_ad_i18n.sql @@ -0,0 +1,12 @@ +-- Verify camper:surroundings_ad_i18n on pg + +begin; + +select company_id + , lang_tag + , title + , anchor +from camper.surroundings_ad_i18n +where false; + +rollback; diff --git a/verify/translate_surroundings_ad.sql b/verify/translate_surroundings_ad.sql new file mode 100644 index 0000000..dc84217 --- /dev/null +++ b/verify/translate_surroundings_ad.sql @@ -0,0 +1,7 @@ +-- Verify camper:translate_surroundings_ad on pg + +begin; + +select has_function_privilege('camper.translate_surroundings_ad(integer, text, text, text)', 'execute'); + +rollback; diff --git a/web/static/public.css b/web/static/public.css index b2184fe..9b7dc2b 100644 --- a/web/static/public.css +++ b/web/static/public.css @@ -530,7 +530,7 @@ dl, .nature > div, .outside_activities > div { padding: .55rem 1.5rem 1rem; background-color: var(--accent); color: var(--base); - border-top-left-radius: .6rem; + border-top-left-radius: .6rem; border-bottom-right-radius: .6rem; } @@ -675,7 +675,7 @@ dt { } .outside_activities img { - border-radius: .6rem; + border-radius: .6rem; } .outside_activities h3, .campsite_services .spiel { @@ -1443,65 +1443,66 @@ address { /* surrounding */ -.ad-container { +#surroundings-ad { display: flex; - flex-direction: row; - flex-wrap: wrap; - margin: 50px 0; -} - -.ad-image-container { - width: 50%; - min-height: 300px; - border-top-left-radius: .6rem; - border-bottom-left-radius:.8rem; - background-image: url(https://campingmontagut.tandem.ws/media/0c0625998215b6caec6e871ab26d791c330f34bb46a2facc9d502d769d207e7b/barranquisme.jpg); - background-repeat: no-repeat; - background-position: center center; - background-size: cover; -} - -.ad-description-container { - width: 50%; - min-height: 300px; - display: flex; - flex-direction: column; - justify-content: center; - padding: 50px; - border-top-right-radius: .6rem; - border-bottom-right-radius: .6rem; + margin: 5rem 0; + border-radius: .6rem; + overflow: hidden; background-color: var(--accent-2); } -.ad-description-container h3 { - font-size: calc(16px + .7vw); - font-weight: 400; - line-height: 9rem; +#surroundings-ad::before { + content: ''; + background: var(--background-image) center center no-repeat; + background-size: cover; + min-height: 30rem; } -.ad-button { - font-size: calc(18px + 1.7vw); +#surroundings-ad::before, #surroundings-ad > div { + flex: 1; +} + +#surroundings-ad > div { + display: flex; + flex-direction: column; + justify-content: center; + padding: 5rem; +} + +#surroundings-ad h3 { + font-size: calc(1.6rem + .7vw); + font-weight: 400; + margin-bottom: 2.585rem; +} + +#surroundings-ad a { + font-size: calc(1.8rem + 1.7vw); font-weight: 600; line-height: 0.9em; display: flex; align-items: flex-start; - column-gap: 0.5em; - border-radius: .6rem; + gap: 0.5em; color: var(--contrast); } -.ad-button .ad-icon svg { - width: 40px; +#surroundings-ad svg { + width: 4rem; transform: rotate(320deg) translate3d(-5px, -10px, 0); - transition: all 0.5s ease; + transition: transform 0.5s ease; } -.ad-button:hover .gb-icon svg { +#surroundings-ad a:hover svg { transform: rotate(320deg) translate3d(10px, 0, 0); } -.arrow_link { - fill: var(--contrast); +@media (max-width: 48rem) { + #surroundings-ad { + flex-direction: column-reverse; + } + + #surroundings-ad > div { + justify-content: start; + } } /* services */ diff --git a/web/templates/admin/surroundings/index.gohtml b/web/templates/admin/surroundings/index.gohtml index 18a36f0..dc4ba62 100644 --- a/web/templates/admin/surroundings/index.gohtml +++ b/web/templates/admin/surroundings/index.gohtml @@ -11,6 +11,60 @@ {{ define "content" -}} {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/surroundings.highlightIndex*/ -}} +

{{( pgettext "Ad" "title" )}}

+ {{ with .Ad -}} +
+ {{ CSRFInput }} +
+ {{ with .Title -}} +
+ {{( pgettext "Title" "input")}} + {{ template "lang-selector" . }} + {{ range $lang, $input := . -}} + + {{- end }} + {{ template "error-message" . }} +
+ {{- end }} + {{ with .Media -}} + {{ template "media-picker" . }} + {{- end }} + {{ with .Anchor -}} +
+ {{( pgettext "Link Text" "input")}} + {{ template "lang-selector" . }} + {{ range $lang, $input := . -}} + + {{- end }} + {{ template "error-message" . }} +
+ {{- end }} + {{ with .HRef -}} + + {{ template "error-message" . }} + {{- end }} +
+
+ + {{ $confirm := ( gettext "Are you sure you wish to delete the ad?" )}} + +
+
+ {{- end }}

{{( pgettext "Highlights" "title" )}}

{{( pgettext "Add highlight" "action" )}} {{ if .Highlights -}} diff --git a/web/templates/public/surroundings.gohtml b/web/templates/public/surroundings.gohtml index 12f6f25..54b3471 100644 --- a/web/templates/public/surroundings.gohtml +++ b/web/templates/public/surroundings.gohtml @@ -11,22 +11,20 @@ {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/surroundings.surroundingsPage*/ -}}

{{( pgettext "Surroundings" "title" )}}

-
-
+ {{- end }}

{{( pgettext "What to Do Outside the Campsite?" "title" )}}