diff --git a/demo/demo.sql b/demo/demo.sql index fdea1a0..12dd57f 100644 --- a/demo/demo.sql +++ b/demo/demo.sql @@ -1501,4 +1501,20 @@ values (52, 72, 'Juli Verd', current_date + interval '23 days', current_date + i , (52, 76, 'Hortènsia Grisa', current_date + interval '29 days', current_date + interval '34 days', 0, false, 'invoiced') ; + +alter table amenity alter column amenity_id restart with 132; +select add_amenity(52, 'edifici-camping', 'Edifici Càmping', '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec scelerisque lorem vestibulum enim sollicitudin ornare. Aliquam egestas pretium porttitor. Donec iaculis tempus est, id lobortis risus semper vel. Maecenas ut imperdiet neque. Donec mattis purus felis, vitae interdum risus egestas pharetra. Vestibulum dui neque, condimentum ultrices erat sed, fringilla pharetra ante. Maecenas hendrerit neque mattis risus consectetur euismod. Cras urna metus, bibendum a neque sed, pharetra commodo magna.

', '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec scelerisque lorem vestibulum enim sollicitudin ornare. Aliquam egestas pretium porttitor. Donec iaculis tempus est, id lobortis risus semper vel. Maecenas ut imperdiet neque. Donec mattis purus felis, vitae interdum risus egestas pharetra. Vestibulum dui neque, condimentum ultrices erat sed, fringilla pharetra ante. Maecenas hendrerit neque mattis risus consectetur euismod. Cras urna metus, bibendum a neque sed, pharetra commodo magna.

'); + +select add_amenity_carousel_slide(52, 'edifici-camping', 80, 'Llegenda'); +select add_amenity_carousel_slide(52, 'edifici-camping', 81, 'Llegenda'); +select add_amenity_carousel_slide(52, 'edifici-camping', 82, 'Llegenda'); +select add_amenity_carousel_slide(52, 'edifici-camping', 83, 'Llegenda'); +select add_amenity_carousel_slide(52, 'edifici-camping', 84, 'Llegenda'); +select add_amenity_carousel_slide(52, 'edifici-camping', 85, 'Llegenda'); + +select add_amenity_feature(52, 'edifici-camping', 'person', 'Máx. 6 pers.'); +select add_amenity_feature(52, 'edifici-camping', 'area', 'de 55 a 65 m²'); +select add_amenity_feature(52, 'edifici-camping', 'electricity', 'Electricitat'); +select add_amenity_feature(52, 'edifici-camping', 'shower', 'Accés als serveis'); + commit; diff --git a/deploy/add_amenity.sql b/deploy/add_amenity.sql new file mode 100644 index 0000000..70b33a2 --- /dev/null +++ b/deploy/add_amenity.sql @@ -0,0 +1,23 @@ +-- Deploy camper:add_amenity to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity + +begin; + +set search_path to camper, public; + +create or replace function add_amenity(company_id integer, label text, name text, info1 text, info2 text) returns integer as +$$ + insert into amenity (company_id, label, name, info1, info2) + values (company_id, label, name, xmlparse(content info1), xmlparse(content info2)) + returning amenity_id + ; +$$ + language sql +; + +revoke execute on function add_amenity(integer, text, text, text, text) from public; +grant execute on function add_amenity(integer, text, text, text, text) to admin; + +commit; diff --git a/deploy/add_amenity_carousel_slide.sql b/deploy/add_amenity_carousel_slide.sql new file mode 100644 index 0000000..c9fa153 --- /dev/null +++ b/deploy/add_amenity_carousel_slide.sql @@ -0,0 +1,32 @@ +-- Deploy camper:add_amenity_carousel_slide to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity +-- requires: amenity_carousel + +begin; + +set search_path to camper, public; + +create or replace function add_amenity_carousel_slide(company_id integer, label text, media_id integer, caption text) returns integer as +$$ + insert into amenity_carousel (amenity_id, media_id, caption) + select amenity_id, media_id, coalesce(caption, '') + from amenity + where label = add_amenity_carousel_slide.label + and company_id = add_amenity_carousel_slide.company_id + union all + select -1, 1, '' + limit 1 + on conflict (amenity_id, media_id) do update + set caption = excluded.caption + returning amenity_id + ; +$$ + language sql +; + +revoke execute on function add_amenity_carousel_slide(integer, text, integer, text) from public; +grant execute on function add_amenity_carousel_slide(integer, text, integer, text) to admin; + +commit; diff --git a/deploy/add_amenity_feature.sql b/deploy/add_amenity_feature.sql new file mode 100644 index 0000000..df7992e --- /dev/null +++ b/deploy/add_amenity_feature.sql @@ -0,0 +1,30 @@ +-- Deploy camper:add_amenity_feature to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity_feature +-- requires: amenity + +begin; + +set search_path to camper, public; + +create or replace function add_amenity_feature(company_id integer, amenity_label text, icon_name text, name text) returns integer as +$$ + insert into amenity_feature (amenity_id, icon_name, name) + select amenity_id, icon_name, add_amenity_feature.name + from amenity + where label = amenity_label + and company_id = add_amenity_feature.company_id + union all + select -1, 'baby', 'name' + limit 1 + returning amenity_feature_id + ; +$$ + language sql +; + +revoke execute on function add_amenity_feature(integer, text, text, text) from public; +grant execute on function add_amenity_feature(integer, text, text, text) to admin; + +commit; diff --git a/deploy/amenity.sql b/deploy/amenity.sql new file mode 100644 index 0000000..08556f1 --- /dev/null +++ b/deploy/amenity.sql @@ -0,0 +1,58 @@ +-- Deploy camper:amenity to pg +-- requires: roles +-- requires: schema_camper +-- requires: company +-- requires: user_profile + +begin; + +set search_path to camper, public; + +create table amenity ( + amenity_id integer generated by default as identity primary key, + company_id integer not null references company, + label text not null constraint label_not_empty check(length(trim(label)) > 0), + name text not null constraint name_not_empty check(length(trim(name)) > 0), + info1 xml not null default '', + info2 xml not null default '', + active boolean not null default true, + unique (company_id, label) +); + +grant select on table amenity to guest; +grant select on table amenity to employee; +grant select, insert, update, delete on table amenity to admin; + +alter table amenity enable row level security; + +create policy guest_ok +on amenity +for select +using (true) +; + +create policy insert_to_company +on amenity +for insert +with check ( + company_id in (select company_id from user_profile) +) +; + +create policy update_company +on amenity +for update +using ( + company_id in (select company_id from user_profile) +) +; + +create policy delete_from_company +on amenity +for delete +using ( + company_id in (select company_id from user_profile) +) +; + +commit; diff --git a/deploy/amenity_carousel.sql b/deploy/amenity_carousel.sql new file mode 100644 index 0000000..b1d2885 --- /dev/null +++ b/deploy/amenity_carousel.sql @@ -0,0 +1,56 @@ +-- Deploy camper:amenity_carousel to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity +-- requires: media +-- requires: user_profile + +begin; + +set search_path to camper, public; + +create table amenity_carousel ( + amenity_id integer not null references amenity, + media_id integer not null references media, + caption text not null, + position integer not null default 2147483647, + primary key (amenity_id, media_id) +); + +grant select on table amenity_carousel to guest; +grant select on table amenity_carousel to employee; +grant select, insert, update, delete on table amenity_carousel to admin; + +alter table amenity_carousel enable row level security; + +create policy guest_ok +on amenity_carousel +for select +using (true) +; + +create policy insert_to_company +on amenity_carousel +for insert +with check ( + exists (select 1 from amenity join media using (company_id) join user_profile using (company_id) where amenity.amenity_id = amenity_carousel.amenity_id and media.media_id = amenity_carousel.media_id) +) +; + +create policy update_company +on amenity_carousel +for update +using ( + exists (select 1 from amenity join media using (company_id) join user_profile using (company_id) where amenity.amenity_id = amenity_carousel.amenity_id and media.media_id = amenity_carousel.media_id) +) +; + +create policy delete_from_company +on amenity_carousel +for delete +using ( + exists (select 1 from amenity join media using (company_id) join user_profile using (company_id) where amenity.amenity_id = amenity_carousel.amenity_id and media.media_id = amenity_carousel.media_id) +) +; + +commit; diff --git a/deploy/amenity_carousel_i18n.sql b/deploy/amenity_carousel_i18n.sql new file mode 100644 index 0000000..14be918 --- /dev/null +++ b/deploy/amenity_carousel_i18n.sql @@ -0,0 +1,24 @@ +-- Deploy camper:amenity_carousel_i18n to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity_carousel +-- requires: language + +begin; + +set search_path to camper, public; + +create table amenity_carousel_i18n ( + amenity_id integer not null, + media_id integer not null, + lang_tag text not null references language, + caption text, + primary key (amenity_id, media_id, lang_tag), + foreign key (amenity_id, media_id) references amenity_carousel +); + +grant select on table amenity_carousel_i18n to guest; +grant select on table amenity_carousel_i18n to employee; +grant select, insert, update, delete on table amenity_carousel_i18n to admin; + +commit; diff --git a/deploy/amenity_feature.sql b/deploy/amenity_feature.sql new file mode 100644 index 0000000..20c8c9e --- /dev/null +++ b/deploy/amenity_feature.sql @@ -0,0 +1,56 @@ +-- Deploy camper:amenity_feature to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity +-- requires: icon +-- requires: user_profile + +begin; + +set search_path to camper, public; + +create table amenity_feature ( + amenity_feature_id integer generated by default as identity primary key, + amenity_id integer not null references amenity, + icon_name text not null references icon, + name text not null constraint name_not_empty check(length(trim(name)) > 0), + position integer not null default 2147483647 +); + +grant select on table amenity_feature to guest; +grant select on table amenity_feature to employee; +grant select, insert, update, delete on table amenity_feature to admin; + +alter table amenity_feature enable row level security; + +create policy guest_ok +on amenity_feature +for select +using (true) +; + +create policy insert_to_company +on amenity_feature +for insert +with check ( + exists (select 1 from amenity join user_profile using (company_id) where amenity.amenity_id = amenity_feature.amenity_id) +) +; + +create policy update_company +on amenity_feature +for update +using ( + exists (select 1 from amenity join user_profile using (company_id) where amenity.amenity_id = amenity_feature.amenity_id) +) +; + +create policy delete_from_company +on amenity_feature +for delete +using ( + exists (select 1 from amenity join user_profile using (company_id) where amenity.amenity_id = amenity_feature.amenity_id) +) +; + +commit; diff --git a/deploy/amenity_feature_i18n.sql b/deploy/amenity_feature_i18n.sql new file mode 100644 index 0000000..cfba081 --- /dev/null +++ b/deploy/amenity_feature_i18n.sql @@ -0,0 +1,22 @@ +-- Deploy camper:amenity_feature_i18n to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity_feature +-- requires: language + +begin; + +set search_path to camper, public; + +create table amenity_feature_i18n ( + amenity_feature_id integer not null references amenity_feature, + lang_tag text not null references language, + name text, + primary key (amenity_feature_id, lang_tag) +); + +grant select on table amenity_feature_i18n to guest; +grant select on table amenity_feature_i18n to employee; +grant select, insert, update, delete on table amenity_feature_i18n to admin; + +commit; diff --git a/deploy/amenity_i18n.sql b/deploy/amenity_i18n.sql new file mode 100644 index 0000000..0a4a048 --- /dev/null +++ b/deploy/amenity_i18n.sql @@ -0,0 +1,24 @@ +-- Deploy camper:amenity_i18n to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity +-- requires: language + +begin; + +set search_path to camper, public; + +create table amenity_i18n ( + amenity_id integer not null references amenity, + lang_tag text not null references language, + name text, + info1 xml, + info2 xml, + primary key (amenity_id, lang_tag) +); + +grant select on amenity_i18n to guest; +grant select on amenity_i18n to employee; +grant select, insert, update, delete on amenity_i18n to admin; + +commit; diff --git a/deploy/edit_amenity.sql b/deploy/edit_amenity.sql new file mode 100644 index 0000000..6fd6867 --- /dev/null +++ b/deploy/edit_amenity.sql @@ -0,0 +1,27 @@ +-- Deploy camper:edit_amenity to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity + +begin; + +set search_path to camper, public; + +create or replace function edit_amenity(amenity_id integer, new_label text, new_name text, new_info1 text, new_info2 text, active boolean) returns integer as +$$ + update amenity + set label = new_label + , name = new_name + , info1 = xmlparse(content new_info1) + , info2 = xmlparse(content new_info2) + , active = edit_amenity.active + where amenity_id = edit_amenity.amenity_id + returning amenity_id; +$$ + language sql +; + +revoke execute on function edit_amenity(integer, text, text, text, text, boolean) from public; +grant execute on function edit_amenity(integer, text, text, text, text, boolean) to admin; + +commit; diff --git a/deploy/edit_amenity_feature.sql b/deploy/edit_amenity_feature.sql new file mode 100644 index 0000000..4860d14 --- /dev/null +++ b/deploy/edit_amenity_feature.sql @@ -0,0 +1,25 @@ +-- Deploy camper:edit_amenity_feature to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity_feature + +begin; + +set search_path to camper, public; + +create or replace function edit_amenity_feature(feature_id integer, icon_name text, name text) returns integer as +$$ + update amenity_feature + set icon_name = edit_amenity_feature.icon_name + , name = edit_amenity_feature.name + where amenity_feature_id = feature_id + returning amenity_feature_id + ; +$$ + language sql +; + +revoke execute on function edit_amenity_feature(integer, text, text) from public; +grant execute on function edit_amenity_feature(integer, text, text) to admin; + +commit; diff --git a/deploy/order_amenity_carousel.sql b/deploy/order_amenity_carousel.sql new file mode 100644 index 0000000..9844771 --- /dev/null +++ b/deploy/order_amenity_carousel.sql @@ -0,0 +1,27 @@ +-- Deploy camper:order_amenity_carousel to pg +-- requires: roles +-- requires: amenity_carousel +-- requires: amenity + +begin; + +set search_path to camper, public; + +create or replace function order_amenity_carousel(label text, company_id integer, positions integer[]) returns void as +$$ + update amenity_carousel + set position = cast(temp.position as integer) + from unnest(positions) with ordinality as temp(media_id, position) + join amenity on amenity.label = order_amenity_carousel.label + and amenity.company_id = order_amenity_carousel.company_id + where amenity_carousel.amenity_id = amenity.amenity_id + and amenity_carousel.media_id = temp.media_id + ; +$$ + language sql +; + +revoke execute on function order_amenity_carousel(text, integer, integer[]) from public; +grant execute on function order_amenity_carousel(text, integer, integer[]) to admin; + +commit; diff --git a/deploy/order_amenity_features.sql b/deploy/order_amenity_features.sql new file mode 100644 index 0000000..6495053 --- /dev/null +++ b/deploy/order_amenity_features.sql @@ -0,0 +1,24 @@ +-- Deploy camper:order_amenity_features to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity_feature + +begin; + +set search_path to camper, public; + +create or replace function order_amenity_features(positions integer[]) returns void as +$$ + update amenity_feature + set position = cast(temp.position as integer) + from unnest(positions) with ordinality as temp(feature_id, position) + where amenity_feature_id = temp.feature_id + ; +$$ + language sql +; + +revoke execute on function order_amenity_features(integer[]) from public; +grant execute on function order_amenity_features(integer[]) to admin; + +commit; diff --git a/deploy/remove_amenity.sql b/deploy/remove_amenity.sql new file mode 100644 index 0000000..595d046 --- /dev/null +++ b/deploy/remove_amenity.sql @@ -0,0 +1,50 @@ +-- Deploy camper:remove_amenity to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity +-- requires: amenity_i18n +-- requires: amenity_carousel +-- requires: amenity_carousel_i18n +-- requires: amenity_feature +-- requires: amenity_feature_i18n + +begin; + +set search_path to camper, public; + +create or replace function remove_amenity(amenity_id integer) returns void as +$$ + delete from amenity_feature_i18n + where amenity_feature_id in ( + select amenity_feature_id + from amenity_feature + where amenity_id = remove_amenity.amenity_id + ); + + delete from amenity_feature + where amenity_id = remove_amenity.amenity_id + ; + + delete from amenity_carousel_i18n + where amenity_id = remove_amenity.amenity_id + ; + + delete from amenity_carousel + where amenity_id = remove_amenity.amenity_id + ; + + delete from amenity_i18n + where amenity_id = remove_amenity.amenity_id + ; + + delete from amenity + where amenity_id = remove_amenity.amenity_id + ; +$$ + language sql +; + +revoke execute on function remove_amenity(integer) from public; +grant execute on function remove_amenity(integer) to admin; + +commit; diff --git a/deploy/remove_amenity_carousel_slide.sql b/deploy/remove_amenity_carousel_slide.sql new file mode 100644 index 0000000..a3723a3 --- /dev/null +++ b/deploy/remove_amenity_carousel_slide.sql @@ -0,0 +1,40 @@ +-- Deploy camper:remove_amenity_carousel_slide to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity_carousel +-- requires: amenity_carousel_i18n + +begin; + +set search_path to camper, public; + +create or replace function remove_amenity_carousel_slide(company_id integer, label text, media_id integer) returns void as +$$ +declare + csid integer; +begin + select amenity_id + into csid + from amenity + where amenity.label = remove_amenity_carousel_slide.label + and amenity.company_id = remove_amenity_carousel_slide.company_id + ; + + delete from amenity_carousel_i18n + where amenity_id = csid + and amenity_carousel_i18n.media_id = remove_amenity_carousel_slide.media_id + ; + + delete from amenity_carousel + where amenity_id = csid + and amenity_carousel.media_id = remove_amenity_carousel_slide.media_id + ; +end +$$ + language plpgsql +; + +revoke execute on function remove_amenity_carousel_slide(integer, text, integer) from public; +grant execute on function remove_amenity_carousel_slide(integer, text, integer) to admin; + +commit; diff --git a/deploy/remove_amenity_feature.sql b/deploy/remove_amenity_feature.sql new file mode 100644 index 0000000..a2557af --- /dev/null +++ b/deploy/remove_amenity_feature.sql @@ -0,0 +1,22 @@ +-- Deploy camper:remove_amenity_feature to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity_feature +-- requires: amenity_feature_i18n + +begin; + +set search_path to camper, public; + +create or replace function remove_amenity_feature(feature_id integer) returns void as +$$ + delete from amenity_feature_i18n where amenity_feature_id = feature_id; + delete from amenity_feature where amenity_feature_id = feature_id; +$$ + language sql +; + +revoke execute on function remove_amenity_feature(integer) from public; +grant execute on function remove_amenity_feature(integer) to admin; + +commit; diff --git a/deploy/translate_amenity.sql b/deploy/translate_amenity.sql new file mode 100644 index 0000000..6cf72a7 --- /dev/null +++ b/deploy/translate_amenity.sql @@ -0,0 +1,27 @@ +-- Deploy camper:translate_amenity to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity_i18n + +begin; + +set search_path to camper, public; + +create or replace function translate_amenity(amenity_id integer, lang_tag text, name text, info1 text, info2 text) returns void as +$$ + insert into amenity_i18n (amenity_id, lang_tag, name, info1, info2) + values (amenity_id, lang_tag, case trim(name) when '' then null else name end, case trim(info1) when '' then null else xmlparse(content info1) end, case trim(info2) when '' then null else xmlparse(content info2) end) + on conflict (amenity_id, lang_tag) + do update + set name = excluded.name + , info1 = excluded.info1 + , info2 = excluded.info2 + ; +$$ + language sql +; + +revoke execute on function translate_amenity(integer, text, text, text, text) from public; +grant execute on function translate_amenity(integer, text, text, text, text) to admin; + +commit; diff --git a/deploy/translate_amenity_carousel_slide.sql b/deploy/translate_amenity_carousel_slide.sql new file mode 100644 index 0000000..eaa92d6 --- /dev/null +++ b/deploy/translate_amenity_carousel_slide.sql @@ -0,0 +1,28 @@ +-- Deploy camper:translate_amenity_carousel_slide to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity +-- requires: amenity_carousel_i18n + +begin; + +set search_path to camper, public; + +create or replace function translate_amenity_carousel_slide(company_id integer, label text, media_id integer, lang_tag text, caption text) returns void as +$$ + insert into amenity_carousel_i18n (amenity_id, media_id, lang_tag, caption) + select amenity_id, translate_amenity_carousel_slide.media_id, lang_tag, case trim(caption) when '' then null else caption end + from amenity + where label = translate_amenity_carousel_slide.label + and company_id = translate_amenity_carousel_slide.company_id + on conflict (amenity_id, media_id, lang_tag) do update + set caption = excluded.caption + ; +$$ + language sql +; + +revoke execute on function translate_amenity_carousel_slide(integer, text, integer, text, text) from public; +grant execute on function translate_amenity_carousel_slide(integer, text, integer, text, text) to admin; + +commit; diff --git a/deploy/translate_amenity_feature.sql b/deploy/translate_amenity_feature.sql new file mode 100644 index 0000000..00638fc --- /dev/null +++ b/deploy/translate_amenity_feature.sql @@ -0,0 +1,24 @@ +-- Deploy camper:translate_amenity_feature to pg +-- requires: roles +-- requires: schema_camper +-- requires: amenity_feature_i18n + +begin; + +set search_path to camper, public; + +create or replace function translate_amenity_feature(feature_id integer, lang_tag text, name text) returns void as +$$ + insert into amenity_feature_i18n (amenity_feature_id, lang_tag, name) + values (feature_id, lang_tag, case trim(name) when '' then null else name end) + on conflict (amenity_feature_id, lang_tag) do update + set name = excluded.name + ; +$$ + language sql +; + +revoke execute on function translate_amenity_feature(integer, text, text) from public; +grant execute on function translate_amenity_feature(integer, text, text) to admin; + +commit; diff --git a/pkg/amenity/admin.go b/pkg/amenity/admin.go new file mode 100644 index 0000000..8c86e10 --- /dev/null +++ b/pkg/amenity/admin.go @@ -0,0 +1,289 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package amenity + +import ( + "context" + "net/http" + + "github.com/jackc/pgx/v4" + + "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/database" + "dev.tandem.ws/tandem/camper/pkg/form" + httplib "dev.tandem.ws/tandem/camper/pkg/http" + "dev.tandem.ws/tandem/camper/pkg/locale" + "dev.tandem.ws/tandem/camper/pkg/template" +) + +type AdminHandler struct { +} + +func NewAdminHandler() *AdminHandler { + return &AdminHandler{} +} + +func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var head string + head, r.URL.Path = httplib.ShiftPath(r.URL.Path) + + switch head { + case "new": + switch r.Method { + case http.MethodGet: + f := newAmenityForm(company) + f.MustRender(w, r, user, company) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet) + } + case "": + switch r.Method { + case http.MethodGet: + serveAmenityIndex(w, r, user, company, conn) + case http.MethodPost: + addAmenity(w, r, user, company, conn) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) + } + default: + f := newAmenityForm(company) + if err := f.FillFromDatabase(r.Context(), conn, company, head); err != nil { + if database.ErrorIsNotFound(err) { + http.NotFound(w, r) + return + } + panic(err) + } + + head, r.URL.Path = httplib.ShiftPath(r.URL.Path) + switch head { + case "": + switch r.Method { + case http.MethodGet: + f.MustRender(w, r, user, company) + case http.MethodPut: + editAmenity(w, r, user, company, conn, f) + case http.MethodDelete: + deleteAmenity(w, r, user, conn, f.ID) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut, http.MethodDelete) + } + case "slides": + h.carouselHandler(user, company, conn, f.Label.Val).ServeHTTP(w, r) + case "features": + h.featuresHandler(user, company, conn, f.Label.Val).ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + } + } +} + +func serveAmenityIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { + amenities, err := collectAmenityEntries(r.Context(), company, conn) + if err != nil { + panic(err) + } + page := &amenityIndex{ + Amenities: amenities, + } + page.MustRender(w, r, user, company) +} + +func collectAmenityEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*amenityEntry, error) { + rows, err := conn.Query(ctx, ` + select label + , name + , active + from amenity + where company_id = $1 + order by label`, company.ID) + if err != nil { + return nil, err + } + defer rows.Close() + + var amenities []*amenityEntry + for rows.Next() { + entry := &amenityEntry{} + if err = rows.Scan(&entry.Label, &entry.Name, &entry.Active); err != nil { + return nil, err + } + amenities = append(amenities, entry) + } + + return amenities, nil +} + +type amenityEntry struct { + Label string + Name string + Active bool +} + +type amenityIndex struct { + Amenities []*amenityEntry +} + +func (page *amenityIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "amenity/index.gohtml", page) +} + +func addAmenity(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { + f := newAmenityForm(company) + processAmenityForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { + var err error + f.ID, err = tx.AddAmenity(ctx, company.ID, f.Label.Val, f.Name[f.DefaultLang].Val, f.Info1[f.DefaultLang].Val, f.Info2[f.DefaultLang].Val) + if err != nil { + return err + } + return translateAmenity(ctx, tx, company, f) + }) + httplib.Redirect(w, r, "/admin/amenities", http.StatusSeeOther) +} + +func translateAmenity(ctx context.Context, tx *database.Tx, company *auth.Company, f *amenityForm) error { + for lang := range company.Locales { + l := lang.String() + if l == f.DefaultLang { + continue + } + if err := tx.TranslateAmenity(ctx, f.ID, lang, f.Name[l].Val, f.Info1[l].Val, f.Info2[l].Val); err != nil { + return err + } + } + return nil +} + +func editAmenity(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *amenityForm) { + processAmenityForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { + if err := tx.EditAmenity(ctx, f.ID, f.Label.Val, f.Name[f.DefaultLang].Val, f.Info1[f.DefaultLang].Val, f.Info2[f.DefaultLang].Val, f.Active.Checked); err != nil { + return err + } + return translateAmenity(ctx, tx, company, f) + }) + httplib.Redirect(w, r, "/admin/amenities", http.StatusSeeOther) +} + +func processAmenityForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *amenityForm, act func(ctx context.Context, tx *database.Tx) error) { + if ok, err := form.Handle(f, w, r, user); err != nil { + return + } else if !ok { + f.MustRender(w, r, user, company) + return + } + + tx := conn.MustBegin(r.Context()) + defer tx.Rollback(r.Context()) + if err := act(r.Context(), tx); err != nil { + panic(err) + } + tx.MustCommit(r.Context()) + httplib.Redirect(w, r, "/admin/amenities", http.StatusSeeOther) +} + +func deleteAmenity(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 + } + if err := conn.RemoveAmenity(r.Context(), id); err != nil { + panic(err) + } + httplib.Redirect(w, r, "/admin/amenities/", http.StatusSeeOther) +} + +type amenityForm struct { + DefaultLang string + ID int + CurrentLabel string + Active *form.Checkbox + Label *form.Input + Name form.I18nInput + Info1 form.I18nInput + Info2 form.I18nInput +} + +func newAmenityForm(company *auth.Company) *amenityForm { + return &amenityForm{ + DefaultLang: company.DefaultLanguage.String(), + Active: &form.Checkbox{ + Name: "active", + Checked: true, + }, + Label: &form.Input{ + Name: "label", + }, + Name: form.NewI18nInput(company.Locales, "name"), + Info1: form.NewI18nInput(company.Locales, "info1"), + Info2: form.NewI18nInput(company.Locales, "info2"), + } +} + +func (f *amenityForm) FillFromDatabase(ctx context.Context, conn *database.Conn, company *auth.Company, label string) error { + f.CurrentLabel = label + var name database.RecordArray + var info1 database.RecordArray + var info2 database.RecordArray + row := conn.QueryRow(ctx, ` + select amenity_id + , label + , amenity.name + , amenity.info1::text + , amenity.info2::text + , active + , array_agg((lang_tag, i18n.name)) + , array_agg((lang_tag, i18n.info1::text)) + , array_agg((lang_tag, i18n.info2::text)) + from amenity + left join amenity_i18n as i18n using (amenity_id) + where company_id = $1 + and label = $2 + group by amenity_id + , label + , amenity.name + , amenity.info1::text + , amenity.info2::text + , active + `, pgx.QueryResultFormats{pgx.BinaryFormatCode}, company.ID, label) + if err := row.Scan(&f.ID, &f.Label.Val, &f.Name[f.DefaultLang].Val, &f.Info1[f.DefaultLang].Val, &f.Info2[f.DefaultLang].Val, &f.Active.Checked, &name, &info1, &info2); err != nil { + return err + } + if err := f.Name.FillArray(name); err != nil { + return err + } + if err := f.Info1.FillArray(info1); err != nil { + return err + } + if err := f.Info2.FillArray(info2); err != nil { + return err + } + return nil +} + +func (f *amenityForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.Active.FillValue(r) + f.Label.FillValue(r) + f.Name.FillValue(r) + f.Info1.FillValue(r) + f.Info2.FillValue(r) + return nil +} + +func (f *amenityForm) Valid(l *locale.Locale) bool { + v := form.NewValidator(l) + v.CheckRequired(f.Label, l.GettextNoop("Label can not be empty.")) + v.CheckRequired(f.Name[f.DefaultLang], l.GettextNoop("Name can not be empty.")) + return v.AllOK +} + +func (f *amenityForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "amenity/form.gohtml", f) +} diff --git a/pkg/amenity/carousel.go b/pkg/amenity/carousel.go new file mode 100644 index 0000000..3088674 --- /dev/null +++ b/pkg/amenity/carousel.go @@ -0,0 +1,339 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package amenity + +import ( + "context" + "net/http" + "strconv" + + "github.com/jackc/pgx/v4" + + "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/carousel" + "dev.tandem.ws/tandem/camper/pkg/database" + "dev.tandem.ws/tandem/camper/pkg/form" + httplib "dev.tandem.ws/tandem/camper/pkg/http" + "dev.tandem.ws/tandem/camper/pkg/locale" + "dev.tandem.ws/tandem/camper/pkg/template" +) + +func (h *AdminHandler) carouselHandler(user *auth.User, company *auth.Company, conn *database.Conn, label string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var head string + head, r.URL.Path = httplib.ShiftPath(r.URL.Path) + + switch head { + case "": + switch r.Method { + case http.MethodGet: + serveCarouselIndex(w, r, user, company, conn, label) + case http.MethodPost: + addSlide(w, r, user, company, conn, label) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodGet) + } + case "new": + switch r.Method { + case http.MethodGet: + f := newSlideForm(company, label) + f.MustRender(w, r, user, company) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet) + } + case "order": + switch r.Method { + case http.MethodPost: + orderCarousel(w, r, user, company, conn, label) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet) + } + default: + mediaID, err := strconv.Atoi(head) + if err != nil { + http.NotFound(w, r) + return + } + f := newSlideForm(company, label) + if err := f.FillFromDatabase(r.Context(), conn, company, mediaID); err != nil { + if database.ErrorIsNotFound(err) { + http.NotFound(w, r) + return + } + panic(err) + } + + var langTag string + langTag, r.URL.Path = httplib.ShiftPath(r.URL.Path) + + switch langTag { + case "": + switch r.Method { + case http.MethodGet: + f.MustRender(w, r, user, company) + case http.MethodPut: + editSlide(w, r, user, company, conn, f) + case http.MethodDelete: + deleteSlide(w, r, user, company, conn, label, mediaID) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut, http.MethodDelete) + } + default: + http.NotFound(w, r) + } + } + }) +} + +func serveCarouselIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) { + slides, err := collectSlideEntries(r.Context(), conn, company, label) + if err != nil { + panic(err) + } + page := &carouselIndex{ + Label: label, + Slides: slides, + } + page.MustRender(w, r, user, company) +} + +type carouselIndex struct { + Label string + Slides []*carousel.SlideEntry +} + +func (page *carouselIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "amenity/carousel/index.gohtml", page) +} + +func mustCollectSlides(ctx context.Context, conn *database.Conn, company *auth.Company, loc *locale.Locale, label string) []*carousel.Slide { + rows, err := conn.Query(ctx, ` + select coalesce(i18n.caption, slide.caption) as l10_caption + , media.path + from amenity_carousel as slide + join amenity using (amenity_id) + join media on media.media_id = slide.media_id + left join amenity_carousel_i18n as i18n + on i18n.amenity_id = slide.amenity_id + and i18n.media_id = slide.media_id + and lang_tag = $1 + where amenity.label = $2 + and amenity.company_id = $3 + order by slide.position, l10_caption + `, loc.Language, label, company.ID) + if err != nil { + panic(err) + } + defer rows.Close() + + var slides []*carousel.Slide + for rows.Next() { + slide := &carousel.Slide{} + err = rows.Scan(&slide.Caption, &slide.Media) + if err != nil { + panic(err) + } + slides = append(slides, slide) + } + if rows.Err() != nil { + panic(rows.Err()) + } + + return slides +} + +func collectSlideEntries(ctx context.Context, conn *database.Conn, company *auth.Company, label string) ([]*carousel.SlideEntry, error) { + rows, err := conn.Query(ctx, ` + select carousel.media_id + , media.path + , caption + from amenity_carousel as carousel + join amenity using (amenity_id) + join media on media.media_id = carousel.media_id + where amenity.label = $1 + and amenity.company_id = $2 + order by carousel.position, caption + `, label, company.ID) + if err != nil { + return nil, err + } + defer rows.Close() + + var slides []*carousel.SlideEntry + for rows.Next() { + slide := &carousel.SlideEntry{} + if err = rows.Scan(&slide.ID, &slide.Media, &slide.Caption); err != nil { + return nil, err + } + slides = append(slides, slide) + } + + return slides, nil +} + +func addSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) { + f := newSlideForm(company, label) + editSlide(w, r, user, company, conn, f) +} + +func editSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *slideForm) { + f.process(w, r, user, company, conn, func(ctx context.Context, tx *database.Tx) error { + if err := tx.AddAmenityCarouselSlide(ctx, company.ID, f.Label, f.Media.Int(), f.Caption[f.DefaultLang].Val); err != nil { + return nil + } + for lang := range company.Locales { + l := lang.String() + if l == f.DefaultLang { + continue + } + if err := tx.TranslateAmenityCarouselSlide(ctx, company.ID, f.Label, f.Media.Int(), lang, f.Caption[l].Val); err != nil { + return err + } + } + return nil + }) +} + +func deleteSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string, mediaID int) { + if err := user.VerifyCSRFToken(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + if err := conn.RemoveAmenityCarouselSlide(r.Context(), company.ID, label, mediaID); err != nil { + panic(err) + } + httplib.Redirect(w, r, "/admin/amenities/"+label+"/slides", http.StatusSeeOther) +} + +type slideForm struct { + DefaultLang string + Label string + MediaID int + Media *form.Media + Caption form.I18nInput +} + +func newSlideForm(company *auth.Company, label string) *slideForm { + return &slideForm{ + DefaultLang: company.DefaultLanguage.String(), + Label: label, + Media: &form.Media{ + Input: &form.Input{ + Name: "media", + }, + Label: locale.PgettextNoop("Slide image", "input"), + Prompt: locale.PgettextNoop("Set slide image", "action"), + }, + Caption: form.NewI18nInput(company.Locales, "caption"), + } +} + +func (f *slideForm) FillFromDatabase(ctx context.Context, conn *database.Conn, company *auth.Company, mediaID int) error { + f.MediaID = mediaID + var caption database.RecordArray + row := conn.QueryRow(ctx, ` + select carousel.caption + , carousel.media_id::text + , array_agg((lang_tag, i18n.caption)) + from amenity_carousel as carousel + left join amenity_carousel_i18n as i18n using (amenity_id, media_id) + join amenity using (amenity_id) + where amenity.label = $1 + and amenity.company_id = $2 + and carousel.media_id = $3 + group by carousel.caption + , carousel.media_id::text + `, pgx.QueryResultFormats{pgx.BinaryFormatCode}, f.Label, company.ID, mediaID) + if err := row.Scan(&f.Caption[f.DefaultLang].Val, &f.Media.Val, &caption); err != nil { + return err + } + if err := f.Caption.FillArray(caption); err != nil { + return err + } + return nil +} + +func (f *slideForm) process(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, act func(ctx context.Context, tx *database.Tx) error) { + if err := f.Parse(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := user.VerifyCSRFToken(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil { + panic(err) + } else if !ok { + if !httplib.IsHTMxRequest(r) { + w.WriteHeader(http.StatusUnprocessableEntity) + } + f.MustRender(w, r, user, company) + return + } + tx := conn.MustBegin(r.Context()) + if err := act(r.Context(), tx); err == nil { + if err := tx.Commit(r.Context()); err != nil { + panic(err) + } + } else { + if err := tx.Rollback(r.Context()); err != nil { + panic(err) + } + panic(err) + } + httplib.Redirect(w, r, "/admin/amenities/"+f.Label+"/slides", http.StatusSeeOther) +} + +func (f *slideForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.Caption.FillValue(r) + f.Media.FillValue(r) + return nil +} + +func (f *slideForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) { + v := form.NewValidator(l) + if v.CheckRequired(f.Media.Input, l.GettextNoop("Slide image can not be empty.")) { + if _, err := v.CheckImageMedia(ctx, conn, f.Media.Input, l.GettextNoop("Slide image must be an image media type.")); err != nil { + return false, err + } + } + return v.AllOK, nil +} + +func (f *slideForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "amenity/carousel/form.gohtml", f) +} + +func orderCarousel(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := user.VerifyCSRFToken(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + input := r.PostForm["media_id"] + if len(input) > 0 { + var ids []int + for _, s := range input { + if id, err := strconv.Atoi(s); err == nil { + ids = append(ids, id) + } else { + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + } + if err := conn.OrderAmenityCarousel(r.Context(), company.ID, label, ids); err != nil { + panic(err) + } + } + serveCarouselIndex(w, r, user, company, conn, label) +} diff --git a/pkg/amenity/feature.go b/pkg/amenity/feature.go new file mode 100644 index 0000000..96519f9 --- /dev/null +++ b/pkg/amenity/feature.go @@ -0,0 +1,304 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package amenity + +import ( + "context" + "net/http" + "strconv" + + "github.com/jackc/pgx/v4" + + "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/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) featuresHandler(user *auth.User, company *auth.Company, conn *database.Conn, label string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var head string + head, r.URL.Path = httplib.ShiftPath(r.URL.Path) + + switch head { + case "": + switch r.Method { + case http.MethodGet: + serveFeatureIndex(w, r, user, company, conn, label) + case http.MethodPost: + addFeature(w, r, user, company, conn, label) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) + } + case "new": + switch r.Method { + case http.MethodGet: + f := newFeatureForm(r.Context(), company, conn, label) + f.MustRender(w, r, user, company) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet) + } + case "order": + switch r.Method { + case http.MethodPost: + orderFeatures(w, r, user, company, conn, label) + default: + httplib.MethodNotAllowed(w, r, http.MethodPost) + } + default: + id, err := strconv.Atoi(head) + if err != nil { + http.NotFound(w, r) + return + } + f := newFeatureForm(r.Context(), company, conn, label) + if err := f.FillFromDatabase(r.Context(), conn, id); err != nil { + if database.ErrorIsNotFound(err) { + http.NotFound(w, r) + return + } + panic(err) + } + h.featureHandler(user, company, conn, f).ServeHTTP(w, r) + } + }) +} + +func (h *AdminHandler) featureHandler(user *auth.User, company *auth.Company, conn *database.Conn, f *featureForm) 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: + f.MustRender(w, r, user, company) + case http.MethodPut: + editFeature(w, r, user, company, conn, f) + case http.MethodDelete: + deleteFeature(w, r, user, conn, f.Label, f.ID) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut, http.MethodDelete) + } + default: + http.NotFound(w, r) + } + }) +} + +func serveFeatureIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) { + features, err := collectFeatureEntries(r.Context(), conn, company, label) + if err != nil { + panic(err) + } + page := &featureIndex{ + Label: label, + Features: features, + } + page.MustRender(w, r, user, company) +} + +func collectFeatureEntries(ctx context.Context, conn *database.Conn, company *auth.Company, label string) ([]*featureEntry, error) { + rows, err := conn.Query(ctx, ` + select amenity_feature_id + , '/admin/amenities/' || amenity.label || '/features/' || amenity_feature_id + , feature.icon_name + , feature.name + from amenity_feature as feature + join amenity using (amenity_id) + where amenity.label = $1 + and amenity.company_id = $2 + order by feature.position, feature.name + `, label, company.ID) + if err != nil { + return nil, err + } + defer rows.Close() + + var features []*featureEntry + for rows.Next() { + f := &featureEntry{} + if err = rows.Scan(&f.ID, &f.URL, &f.Icon, &f.Name); err != nil { + return nil, err + } + features = append(features, f) + } + + return features, nil +} + +type featureEntry struct { + ID int + URL string + Icon string + Name string +} + +type featureIndex struct { + Label string + Features []*featureEntry +} + +func (page *featureIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "amenity/feature/index.gohtml", page) +} + +func addFeature(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) { + f := newFeatureForm(r.Context(), company, conn, label) + processFeatureForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { + var err error + f.ID, err = tx.AddAmenityFeature(ctx, company.ID, label, f.Icon.String(), f.Name[f.DefaultLang].Val) + if err != nil { + return err + } + return translateFeatures(ctx, tx, company, f) + }) +} + +func translateFeatures(ctx context.Context, tx *database.Tx, company *auth.Company, f *featureForm) error { + for lang := range company.Locales { + l := lang.String() + if l == f.DefaultLang { + continue + } + if err := tx.TranslateAmenityFeature(ctx, f.ID, lang, f.Name[l].Val); err != nil { + return err + } + } + return nil +} + +func editFeature(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *featureForm) { + processFeatureForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { + if _, err := tx.EditAmenityFeature(ctx, f.ID, f.Icon.String(), f.Name[f.DefaultLang].Val); err != nil { + return err + } + return translateFeatures(ctx, tx, company, f) + }) +} + +func processFeatureForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *featureForm, act func(ctx context.Context, tx *database.Tx) error) { + if ok, err := form.Handle(f, w, r, user); err != nil { + return + } else if !ok { + f.MustRender(w, r, user, company) + return + } + + tx := conn.MustBegin(r.Context()) + defer tx.Rollback(r.Context()) + if err := act(r.Context(), tx); err == nil { + tx.MustCommit(r.Context()) + } else { + panic(err) + } + httplib.Redirect(w, r, "/admin/amenities/"+f.Label+"/features", http.StatusSeeOther) +} + +func deleteFeature(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn, label string, id int) { + if err := user.VerifyCSRFToken(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + if err := conn.RemoveAmenityFeature(r.Context(), id); err != nil { + panic(err) + } + httplib.Redirect(w, r, "/admin/amenities/"+label+"/features", http.StatusSeeOther) +} + +type featureForm struct { + DefaultLang string + ID int + Label string + Icon *form.Select + Name form.I18nInput +} + +func newFeatureForm(ctx context.Context, company *auth.Company, conn *database.Conn, label string) *featureForm { + return &featureForm{ + DefaultLang: company.DefaultLanguage.String(), + Label: label, + Icon: &form.Select{ + Name: "icon", + Options: form.MustGetOptions(ctx, conn, "select icon_name, icon_name from icon order by 1"), + }, + Name: form.NewI18nInput(company.Locales, "name"), + } +} + +func (f *featureForm) FillFromDatabase(ctx context.Context, conn *database.Conn, id int) error { + f.ID = id + var name database.RecordArray + row := conn.QueryRow(ctx, ` + select array[icon_name] + , feature.name + , array_agg((lang_tag, i18n.name)) + from amenity_feature as feature + left join amenity_feature_i18n as i18n using (amenity_feature_id) + where amenity_feature_id = $1 + group by icon_name + , feature.name + `, pgx.QueryResultFormats{pgx.BinaryFormatCode}, id) + if err := row.Scan(&f.Icon.Selected, &f.Name[f.DefaultLang].Val, &name); err != nil { + return err + } + if err := f.Name.FillArray(name); err != nil { + return err + } + return nil +} + +func (f *featureForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.Icon.FillValue(r) + f.Name.FillValue(r) + return nil +} + +func (f *featureForm) Valid(l *locale.Locale) bool { + v := form.NewValidator(l) + v.CheckSelectedOptions(f.Icon, l.GettextNoop("Selected icon is not valid.")) + if v.CheckRequired(f.Name[f.DefaultLang], l.GettextNoop("Name can not be empty.")) { + v.CheckMinLength(f.Name[f.DefaultLang], 1, l.GettextNoop("Name must have at least one letter.")) + } + return v.AllOK +} + +func (f *featureForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "amenity/feature/form.gohtml", f) +} + +func orderFeatures(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := user.VerifyCSRFToken(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + input := r.PostForm["feature_id"] + if len(input) > 0 { + var ids []int + for _, s := range input { + if id, err := strconv.Atoi(s); err == nil { + ids = append(ids, id) + } else { + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + } + if err := conn.OrderAmenityFeatures(r.Context(), ids); err != nil { + panic(err) + } + } + serveFeatureIndex(w, r, user, company, conn, label) +} diff --git a/pkg/amenity/public.go b/pkg/amenity/public.go new file mode 100644 index 0000000..3190c78 --- /dev/null +++ b/pkg/amenity/public.go @@ -0,0 +1,127 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package amenity + +import ( + "context" + "net/http" + + "golang.org/x/text/language" + + "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/carousel" + "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) { + var head string + head, r.URL.Path = httplib.ShiftPath(r.URL.Path) + + page, err := newPublicPage(r.Context(), company, conn, user.Locale, head) + if database.ErrorIsNotFound(err) { + http.NotFound(w, r) + return + } else if err != nil { + panic(err) + } + + head, r.URL.Path = httplib.ShiftPath(r.URL.Path) + switch head { + case "": + switch r.Method { + case http.MethodGet: + page.MustRender(w, r, user, company, conn) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet) + } + default: + http.NotFound(w, r) + } + }) +} + +type publicPage struct { + *template.PublicPage + Name string + Label string + Carousel []*carousel.Slide + Features []*feature + Info []string +} + +func newPublicPage(ctx context.Context, company *auth.Company, conn *database.Conn, loc *locale.Locale, label string) (*publicPage, error) { + page := &publicPage{ + PublicPage: template.NewPublicPage(), + Label: label, + Carousel: mustCollectSlides(ctx, conn, company, loc, label), + } + row := conn.QueryRow(ctx, ` + select coalesce(i18n.name, amenity.name) as l10n_name + , array[coalesce(i18n.info1, amenity.info1)::text, coalesce(i18n.info2, amenity.info2)::text] as info + from amenity + left join amenity_i18n i18n on amenity.amenity_id = i18n.amenity_id and i18n.lang_tag = $1 + where amenity.company_id = $2 + and label = $3 + and amenity.active + `, loc.Language, company.ID, label) + if err := row.Scan(&page.Name, &page.Info); err != nil { + return nil, err + } + var err error + page.Features, err = collectFeatures(ctx, conn, company, loc.Language, label) + if err != nil { + return nil, err + } + + return page, nil +} + +func (p *publicPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { + p.Setup(r, user, company, conn) + template.MustRenderPublic(w, r, user, company, "amenity.gohtml", p) +} + +type feature struct { + Icon string + Name string +} + +func collectFeatures(ctx context.Context, conn *database.Conn, company *auth.Company, language language.Tag, label string) ([]*feature, error) { + rows, err := conn.Query(ctx, ` + select feature.icon_name + , coalesce(i18n.name, feature.name) as l10n_name + from amenity_feature as feature + join amenity using (amenity_id) + left join amenity_feature_i18n as i18n on feature.amenity_feature_id = i18n.amenity_feature_id and i18n.lang_tag = $1 + where amenity.label = $2 + and amenity.company_id = $3 + order by feature.position + `, language, label, company.ID) + if err != nil { + return nil, err + } + + var features []*feature + for rows.Next() { + f := &feature{} + if err = rows.Scan(&f.Icon, &f.Name); err != nil { + return nil, err + } + features = append(features, f) + } + return features, nil +} diff --git a/pkg/app/admin.go b/pkg/app/admin.go index 3e4ef49..3bff2bd 100644 --- a/pkg/app/admin.go +++ b/pkg/app/admin.go @@ -8,6 +8,7 @@ package app import ( "net/http" + "dev.tandem.ws/tandem/camper/pkg/amenity" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/booking" "dev.tandem.ws/tandem/camper/pkg/campsite" @@ -26,6 +27,7 @@ import ( ) type adminHandler struct { + amenity *amenity.AdminHandler booking *booking.AdminHandler campsite *campsite.AdminHandler company *company.AdminHandler @@ -41,6 +43,7 @@ type adminHandler struct { func newAdminHandler(mediaDir string) *adminHandler { return &adminHandler{ + amenity: amenity.NewAdminHandler(), booking: booking.NewAdminHandler(), campsite: campsite.NewAdminHandler(), company: company.NewAdminHandler(), @@ -71,6 +74,8 @@ func (h *adminHandler) Handle(user *auth.User, company *auth.Company, conn *data var head string head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { + case "amenities": + h.amenity.Handler(user, company, conn).ServeHTTP(w, r) case "bookings": h.booking.Handler(user, company, conn).ServeHTTP(w, r) case "campsites": diff --git a/pkg/app/public.go b/pkg/app/public.go index b7c2c0b..04dc973 100644 --- a/pkg/app/public.go +++ b/pkg/app/public.go @@ -8,6 +8,7 @@ package app import ( "net/http" + "dev.tandem.ws/tandem/camper/pkg/amenity" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/booking" "dev.tandem.ws/tandem/camper/pkg/campsite" @@ -23,6 +24,7 @@ import ( type publicHandler struct { home *home.PublicHandler + amenity *amenity.PublicHandler booking *booking.PublicHandler campsite *campsite.PublicHandler legal *legal.PublicHandler @@ -34,6 +36,7 @@ type publicHandler struct { func newPublicHandler() *publicHandler { return &publicHandler{ home: home.NewPublicHandler(), + amenity: amenity.NewPublicHandler(), booking: booking.NewPublicHandler(), campsite: campsite.NewPublicHandler(), legal: legal.NewPublicHandler(), @@ -50,6 +53,8 @@ func (h *publicHandler) Handler(user *auth.User, company *auth.Company, conn *da switch head { case "": h.home.Handler(user, company, conn).ServeHTTP(w, r) + case "amenities": + h.amenity.Handler(user, company, conn).ServeHTTP(w, r) case "booking": h.booking.Handler(user, company, conn).ServeHTTP(w, r) case "campground": diff --git a/pkg/database/funcs.go b/pkg/database/funcs.go index 0f84c4d..a970f55 100644 --- a/pkg/database/funcs.go +++ b/pkg/database/funcs.go @@ -11,6 +11,68 @@ import ( "golang.org/x/text/language" ) +func (tx *Tx) AddAmenity(ctx context.Context, companyID int, label string, name string, info1 string, info2 string) (int, error) { + return tx.GetInt(ctx, "select add_amenity($1, $2, $3, $4, $5)", companyID, label, name, info1, info2) +} + +func (tx *Tx) EditAmenity(ctx context.Context, id int, label string, name string, info1 string, info2 string, active bool) error { + _, err := tx.Exec(ctx, "select edit_amenity($1, $2, $3, $4, $5, $6)", id, label, name, info1, info2, active) + return err +} + +func (tx *Tx) TranslateAmenity(ctx context.Context, id int, langTag language.Tag, name string, info1 string, info2 string) error { + _, err := tx.Exec(ctx, "select translate_amenity($1, $2, $3, $4, $5)", id, langTag, name, info1, info2) + return err +} + +func (tx *Tx) AddAmenityCarouselSlide(ctx context.Context, companyID int, label string, mediaID int, caption string) error { + _, err := tx.Exec(ctx, "select add_amenity_carousel_slide($1, $2, $3, $4)", companyID, label, mediaID, caption) + return err +} + +func (tx *Tx) TranslateAmenityCarouselSlide(ctx context.Context, companyID int, label string, mediaID int, langTag language.Tag, caption string) error { + _, err := tx.Exec(ctx, "select translate_amenity_carousel_slide($1, $2, $3, $4, $5)", companyID, label, mediaID, langTag, caption) + return err +} + +func (c *Conn) RemoveAmenityCarouselSlide(ctx context.Context, companyID int, label string, mediaID int) error { + _, err := c.Exec(ctx, "select remove_amenity_carousel_slide($1, $2, $3)", companyID, label, mediaID) + return err +} + +func (c *Conn) OrderAmenityCarousel(ctx context.Context, companyID int, label string, mediaIDs []int) error { + _, err := c.Exec(ctx, "select order_amenity_carousel($1, $2, $3)", companyID, label, mediaIDs) + return err +} + +func (tx *Tx) AddAmenityFeature(ctx context.Context, companyID int, label string, iconName string, name string) (int, error) { + return tx.GetInt(ctx, "select add_amenity_feature($1, $2, $3, $4)", companyID, label, iconName, name) +} + +func (tx *Tx) EditAmenityFeature(ctx context.Context, id int, iconName string, name string) (int, error) { + return tx.GetInt(ctx, "select edit_amenity_feature($1, $2, $3)", id, iconName, name) +} + +func (tx *Tx) TranslateAmenityFeature(ctx context.Context, id int, langTag language.Tag, name string) error { + _, err := tx.Exec(ctx, "select translate_amenity_feature($1, $2, $3)", id, langTag, name) + return err +} + +func (c *Conn) OrderAmenityFeatures(ctx context.Context, ids []int) error { + _, err := c.Exec(ctx, "select order_amenity_features($1)", ids) + return err +} + +func (c *Conn) RemoveAmenityFeature(ctx context.Context, id int) error { + _, err := c.Exec(ctx, "select remove_amenity_feature($1)", id) + return err +} + +func (c *Conn) RemoveAmenity(ctx context.Context, id int) error { + _, err := c.Exec(ctx, "select remove_amenity($1)", id) + return err +} + func (tx *Tx) AddCampsite(ctx context.Context, typeID int, label string, info1 string, info2 string) (int, error) { return tx.GetInt(ctx, "select add_campsite($1, $2, $3, $4)", typeID, label, info1, info2) } diff --git a/po/ca.po b/po/ca.po index 58bbc6c..72d1c53 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-26 22:47+0100\n" +"POT-Creation-Date: 2024-01-27 22:29+0100\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -53,10 +53,17 @@ msgstr "Serveis" msgid "The campsite offers many different services." msgstr "El càmping disposa de diversos serveis." +#: web/templates/public/amenity.gohtml:39 +#: web/templates/public/campsite/type.gohtml:112 +#: web/templates/public/campsite/page.gohtml:39 +msgctxt "title" +msgid "Features" +msgstr "Característiques" + #: web/templates/public/location.gohtml:7 #: web/templates/public/location.gohtml:13 #: web/templates/public/layout.gohtml:69 web/templates/public/layout.gohtml:97 -#: web/templates/admin/layout.gohtml:61 +#: web/templates/admin/layout.gohtml:64 msgctxt "title" msgid "Location" msgstr "Com arribar" @@ -85,7 +92,7 @@ msgstr "Els nostres serveis" #: web/templates/public/surroundings.gohtml:12 #: web/templates/public/layout.gohtml:68 web/templates/public/layout.gohtml:96 #: web/templates/admin/surroundings/form.gohtml:15 -#: web/templates/admin/layout.gohtml:64 +#: web/templates/admin/layout.gohtml:67 msgctxt "title" msgid "Surroundings" msgstr "L’entorn" @@ -164,12 +171,6 @@ msgstr "Gossos: %s/night, lligats, acompanyats i el mínim de lladrucs." msgid "No dogs allowed." msgstr "No es permeten gossos." -#: web/templates/public/campsite/type.gohtml:112 -#: web/templates/public/campsite/page.gohtml:39 -msgctxt "title" -msgid "Features" -msgstr "Característiques" - #: web/templates/public/campsite/type.gohtml:123 msgctxt "title" msgid "Info" @@ -409,7 +410,7 @@ msgstr "Menú" #: web/templates/admin/campsite/type/option/form.gohtml:16 #: web/templates/admin/campsite/type/option/index.gohtml:10 #: web/templates/admin/campsite/type/index.gohtml:10 -#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:92 +#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:95 msgctxt "title" msgid "Campsites" msgstr "Allotjaments" @@ -443,7 +444,7 @@ msgstr "Nou text legal" #: web/templates/admin/legal/form.gohtml:15 #: web/templates/admin/legal/index.gohtml:6 #: web/templates/admin/legal/index.gohtml:15 -#: web/templates/admin/layout.gohtml:67 +#: web/templates/admin/layout.gohtml:70 msgctxt "title" msgid "Legal Texts" msgstr "Texts legals" @@ -462,6 +463,8 @@ msgstr "Àlies" #: web/templates/admin/services/form.gohtml:53 #: web/templates/admin/profile.gohtml:29 #: web/templates/admin/surroundings/form.gohtml:41 +#: web/templates/admin/amenity/feature/form.gohtml:50 +#: web/templates/admin/amenity/form.gohtml:50 msgctxt "input" msgid "Name" msgstr "Nom" @@ -484,6 +487,9 @@ msgstr "Contingut" #: web/templates/admin/services/form.gohtml:81 #: web/templates/admin/surroundings/form.gohtml:69 #: web/templates/admin/surroundings/index.gohtml:58 +#: web/templates/admin/amenity/feature/form.gohtml:65 +#: web/templates/admin/amenity/carousel/form.gohtml:50 +#: web/templates/admin/amenity/form.gohtml:91 #: web/templates/admin/home/index.gohtml:34 #: web/templates/admin/media/form.gohtml:39 msgctxt "action" @@ -502,6 +508,9 @@ msgstr "Actualitza" #: web/templates/admin/season/form.gohtml:75 #: web/templates/admin/services/form.gohtml:83 #: web/templates/admin/surroundings/form.gohtml:71 +#: web/templates/admin/amenity/feature/form.gohtml:67 +#: web/templates/admin/amenity/carousel/form.gohtml:52 +#: web/templates/admin/amenity/form.gohtml:93 msgctxt "action" msgid "Add" msgstr "Afegeix" @@ -519,6 +528,8 @@ msgstr "Afegeix text legal" #: web/templates/admin/season/index.gohtml:29 #: web/templates/admin/user/index.gohtml:20 #: web/templates/admin/surroundings/index.gohtml:83 +#: web/templates/admin/amenity/feature/index.gohtml:30 +#: web/templates/admin/amenity/index.gohtml:21 msgctxt "header" msgid "Name" msgstr "Nom" @@ -542,6 +553,7 @@ msgstr "Nova diapositiva del carrusel" #: web/templates/admin/carousel/form.gohtml:40 #: web/templates/admin/campsite/carousel/form.gohtml:35 #: web/templates/admin/campsite/type/carousel/form.gohtml:44 +#: web/templates/admin/amenity/carousel/form.gohtml:35 msgctxt "input" msgid "Caption" msgstr "Llegenda" @@ -592,12 +604,14 @@ msgstr "Característiques de l’allotjament" #: web/templates/admin/campsite/feature/form.gohtml:32 #: web/templates/admin/campsite/type/feature/form.gohtml:41 #: web/templates/admin/services/form.gohtml:35 +#: web/templates/admin/amenity/feature/form.gohtml:32 msgctxt "input" msgid "Icon" msgstr "Icona" #: web/templates/admin/campsite/feature/index.gohtml:15 #: web/templates/admin/campsite/type/feature/index.gohtml:16 +#: web/templates/admin/amenity/feature/index.gohtml:15 msgctxt "action" msgid "Add Feature" msgstr "Afegeix característica" @@ -611,6 +625,8 @@ msgstr "Afegeix característica" #: web/templates/admin/services/index.gohtml:75 #: web/templates/admin/user/index.gohtml:23 #: web/templates/admin/surroundings/index.gohtml:84 +#: web/templates/admin/amenity/feature/index.gohtml:31 +#: web/templates/admin/amenity/carousel/index.gohtml:31 #: web/templates/admin/home/index.gohtml:54 #: web/templates/admin/home/index.gohtml:99 msgctxt "header" @@ -619,6 +635,7 @@ msgstr "Accions" #: web/templates/admin/campsite/feature/index.gohtml:35 #: web/templates/admin/campsite/type/feature/index.gohtml:36 +#: web/templates/admin/amenity/feature/index.gohtml:35 msgid "Are you sure you wish to delete this feature?" msgstr "Esteu segur de voler esborrar aquesta característica?" @@ -632,6 +649,8 @@ msgstr "Esteu segur de voler esborrar aquesta característica?" #: web/templates/admin/user/index.gohtml:37 #: web/templates/admin/surroundings/index.gohtml:63 #: web/templates/admin/surroundings/index.gohtml:101 +#: web/templates/admin/amenity/feature/index.gohtml:47 +#: web/templates/admin/amenity/carousel/index.gohtml:49 #: web/templates/admin/home/index.gohtml:71 #: web/templates/admin/home/index.gohtml:116 msgctxt "action" @@ -661,6 +680,7 @@ msgstr "Carrusel de l’allotjament" #: web/templates/admin/campsite/carousel/index.gohtml:16 #: web/templates/admin/campsite/type/carousel/index.gohtml:17 #: web/templates/admin/services/index.gohtml:15 +#: web/templates/admin/amenity/carousel/index.gohtml:16 #: web/templates/admin/home/index.gohtml:84 msgctxt "action" msgid "Add slide" @@ -670,6 +690,7 @@ msgstr "Afegeix diapositiva" #: web/templates/admin/campsite/type/carousel/index.gohtml:30 #: web/templates/admin/services/index.gohtml:28 #: web/templates/admin/surroundings/index.gohtml:82 +#: web/templates/admin/amenity/carousel/index.gohtml:29 #: web/templates/admin/home/index.gohtml:52 #: web/templates/admin/home/index.gohtml:97 msgctxt "header" @@ -679,6 +700,7 @@ msgstr "Imatge" #: web/templates/admin/campsite/carousel/index.gohtml:30 #: web/templates/admin/campsite/type/carousel/index.gohtml:31 #: web/templates/admin/services/index.gohtml:29 +#: web/templates/admin/amenity/carousel/index.gohtml:30 #: web/templates/admin/home/index.gohtml:53 #: web/templates/admin/home/index.gohtml:98 msgctxt "header" @@ -688,6 +710,7 @@ msgstr "Llegenda" #: web/templates/admin/campsite/carousel/index.gohtml:35 #: web/templates/admin/campsite/type/carousel/index.gohtml:36 #: web/templates/admin/services/index.gohtml:34 +#: web/templates/admin/amenity/carousel/index.gohtml:35 #: web/templates/admin/home/index.gohtml:103 msgid "Are you sure you wish to delete this slide?" msgstr "Esteu segur de voler esborrar aquesta diapositiva?" @@ -695,6 +718,7 @@ msgstr "Esteu segur de voler esborrar aquesta diapositiva?" #: web/templates/admin/campsite/carousel/index.gohtml:58 #: web/templates/admin/campsite/type/carousel/index.gohtml:59 #: web/templates/admin/services/index.gohtml:56 +#: web/templates/admin/amenity/carousel/index.gohtml:58 #: web/templates/admin/home/index.gohtml:125 msgid "No slides added yet." msgstr "No s’ha afegit cap diapositiva encara." @@ -725,16 +749,19 @@ msgid "Select campsite type" msgstr "Escolliu un tipus d’allotjament" #: web/templates/admin/campsite/form.gohtml:56 +#: web/templates/admin/amenity/form.gohtml:42 msgctxt "input" msgid "Label" msgstr "Etiqueta" #: web/templates/admin/campsite/form.gohtml:64 +#: web/templates/admin/amenity/form.gohtml:63 msgctxt "input" msgid "Info (First Column)" msgstr "Informació (primera columna)" #: web/templates/admin/campsite/form.gohtml:77 +#: web/templates/admin/amenity/form.gohtml:76 msgctxt "input" msgid "Info (Second Column)" msgstr "Informació (segona columna)" @@ -745,6 +772,7 @@ msgid "Add Campsite" msgstr "Afegeix allotjament" #: web/templates/admin/campsite/index.gohtml:23 +#: web/templates/admin/amenity/index.gohtml:20 msgctxt "header" msgid "Label" msgstr "Etiqueta" @@ -756,24 +784,28 @@ msgstr "Tipus" #: web/templates/admin/campsite/index.gohtml:25 #: web/templates/admin/campsite/type/index.gohtml:30 +#: web/templates/admin/amenity/index.gohtml:22 msgctxt "header" msgid "Features" msgstr "Característiques" #: web/templates/admin/campsite/index.gohtml:26 #: web/templates/admin/campsite/type/index.gohtml:32 +#: web/templates/admin/amenity/index.gohtml:23 msgctxt "header" msgid "Carousel" msgstr "Carrusel" #: web/templates/admin/campsite/index.gohtml:36 #: web/templates/admin/campsite/type/index.gohtml:45 +#: web/templates/admin/amenity/index.gohtml:33 msgctxt "action" msgid "Edit Features" msgstr "Edita les característiques" #: web/templates/admin/campsite/index.gohtml:39 #: web/templates/admin/campsite/type/index.gohtml:51 +#: web/templates/admin/amenity/index.gohtml:36 msgctxt "action" msgid "Edit Carousel" msgstr "Edita el carrusel" @@ -782,6 +814,7 @@ msgstr "Edita el carrusel" #: web/templates/admin/campsite/type/index.gohtml:53 #: web/templates/admin/season/index.gohtml:44 #: web/templates/admin/user/login-attempts.gohtml:31 +#: web/templates/admin/amenity/index.gohtml:38 msgid "Yes" msgstr "Sí" @@ -789,6 +822,7 @@ msgstr "Sí" #: web/templates/admin/campsite/type/index.gohtml:53 #: web/templates/admin/season/index.gohtml:44 #: web/templates/admin/user/login-attempts.gohtml:31 +#: web/templates/admin/amenity/index.gohtml:38 msgid "No" msgstr "No" @@ -1004,7 +1038,7 @@ msgstr "Nova temporada" #: web/templates/admin/season/form.gohtml:15 #: web/templates/admin/season/index.gohtml:6 #: web/templates/admin/season/index.gohtml:15 -#: web/templates/admin/layout.gohtml:49 +#: web/templates/admin/layout.gohtml:52 msgctxt "title" msgid "Seasons" msgstr "Temporades" @@ -1041,7 +1075,7 @@ msgid "Cancel" msgstr "Canceŀla" #: web/templates/admin/dashboard.gohtml:6 -#: web/templates/admin/dashboard.gohtml:13 web/templates/admin/layout.gohtml:86 +#: web/templates/admin/dashboard.gohtml:13 web/templates/admin/layout.gohtml:89 msgctxt "title" msgid "Dashboard" msgstr "Tauler" @@ -1075,7 +1109,7 @@ msgstr "Nou servei" #: web/templates/admin/services/form.gohtml:15 #: web/templates/admin/services/index.gohtml:6 -#: web/templates/admin/layout.gohtml:58 +#: web/templates/admin/layout.gohtml:61 msgctxt "title" msgid "Services Page" msgstr "Pàgina de serveis" @@ -1144,7 +1178,7 @@ msgstr "Intents d’entrada" #: web/templates/admin/user/login-attempts.gohtml:10 #: web/templates/admin/user/index.gohtml:6 #: web/templates/admin/user/index.gohtml:16 -#: web/templates/admin/layout.gohtml:70 +#: web/templates/admin/layout.gohtml:73 msgctxt "title" msgid "Users" msgstr "Usuaris" @@ -1330,6 +1364,78 @@ msgstr "Esteu segur de voler esborrar aquest punt d’interès?" msgid "No highlights added yet." msgstr "No s’ha afegit cap punt d’interès encara." +#: web/templates/admin/amenity/feature/form.gohtml:8 +msgctxt "title" +msgid "Edit Amenity Feature" +msgstr "Edició de les característiques de la instaŀlació" + +#: web/templates/admin/amenity/feature/form.gohtml:10 +msgctxt "title" +msgid "New Amenity Feature" +msgstr "Nova característica de la instaŀlació" + +#: web/templates/admin/amenity/feature/form.gohtml:16 +#: web/templates/admin/amenity/feature/index.gohtml:10 +#: web/templates/admin/amenity/carousel/form.gohtml:16 +#: web/templates/admin/amenity/carousel/index.gohtml:10 +#: web/templates/admin/amenity/form.gohtml:15 +#: web/templates/admin/amenity/index.gohtml:6 +#: web/templates/admin/layout.gohtml:49 +msgctxt "title" +msgid "Amenities" +msgstr "Instaŀlacions" + +#: web/templates/admin/amenity/feature/form.gohtml:17 +#: web/templates/admin/amenity/feature/index.gohtml:6 +msgctxt "title" +msgid "Amenity Features" +msgstr "Característiques de la instaŀlació" + +#: web/templates/admin/amenity/feature/index.gohtml:56 +msgid "No amenity features added yet." +msgstr "No s’ha afegit cap característica a la instaŀlació encara." + +#: web/templates/admin/amenity/carousel/form.gohtml:8 +msgctxt "title" +msgid "Edit Amenity Carousel Slide" +msgstr "Edició de la diapositiva del carrusel de la instaŀlació" + +#: web/templates/admin/amenity/carousel/form.gohtml:10 +msgctxt "title" +msgid "New Amenity Carousel Slide" +msgstr "Nova diapositiva del carrusel de la instaŀlació" + +#: web/templates/admin/amenity/carousel/form.gohtml:17 +#: web/templates/admin/amenity/carousel/index.gohtml:6 +msgctxt "title" +msgid "Amenity Carousel" +msgstr "Carrusel de la instaŀlació" + +#: web/templates/admin/amenity/form.gohtml:8 +msgctxt "title" +msgid "Edit Amenity" +msgstr "Edició de la instaŀlació" + +#: web/templates/admin/amenity/form.gohtml:10 +msgctxt "title" +msgid "New Amenity" +msgstr "Nova instaŀlació" + +#: web/templates/admin/amenity/form.gohtml:33 +#: web/templates/admin/amenity/index.gohtml:24 +msgctxt "amenity" +msgid "Active" +msgstr "Activa" + +#: web/templates/admin/amenity/index.gohtml:14 +msgctxt "action" +msgid "Add Amenity" +msgstr "Afegeix instaŀlació" + +#: web/templates/admin/amenity/index.gohtml:44 +msgid "No amenities added yet." +msgstr "No s’ha afegit cap instaŀlació encara." + #: web/templates/admin/layout.gohtml:29 msgctxt "title" msgid "User Menu" @@ -1347,7 +1453,7 @@ msgctxt "title" msgid "Payment Settings" msgstr "Paràmetres de pagament" -#: web/templates/admin/layout.gohtml:52 +#: web/templates/admin/layout.gohtml:55 #: web/templates/admin/media/form.gohtml:10 #: web/templates/admin/media/index.gohtml:6 #: web/templates/admin/media/index.gohtml:14 @@ -1356,28 +1462,28 @@ msgctxt "title" msgid "Media" msgstr "Mèdia" -#: web/templates/admin/layout.gohtml:55 web/templates/admin/home/index.gohtml:6 +#: web/templates/admin/layout.gohtml:58 web/templates/admin/home/index.gohtml:6 msgctxt "title" msgid "Home Page" msgstr "Pàgina d’inici" -#: web/templates/admin/layout.gohtml:75 +#: web/templates/admin/layout.gohtml:78 msgctxt "action" msgid "Logout" msgstr "Surt" -#: web/templates/admin/layout.gohtml:89 +#: web/templates/admin/layout.gohtml:92 #: web/templates/admin/booking/index.gohtml:6 #: web/templates/admin/booking/index.gohtml:16 msgctxt "title" msgid "Bookings" msgstr "Reserves" -#: web/templates/admin/layout.gohtml:98 +#: web/templates/admin/layout.gohtml:101 msgid "Breadcrumb" msgstr "Fil d’Ariadna" -#: web/templates/admin/layout.gohtml:110 +#: web/templates/admin/layout.gohtml:113 msgid "Camper Version: %s" msgstr "Camper versió: %s" @@ -1547,34 +1653,37 @@ msgstr "No s’ha trobat cap reserva." #: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:483 #: pkg/campsite/feature.go:269 pkg/season/admin.go:412 #: pkg/services/admin.go:316 pkg/surroundings/admin.go:340 +#: pkg/amenity/feature.go:269 pkg/amenity/admin.go:270 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." #: pkg/legal/admin.go:259 pkg/campsite/types/option.go:358 #: pkg/campsite/types/feature.go:273 pkg/campsite/types/admin.go:484 -#: pkg/campsite/feature.go:270 +#: pkg/campsite/feature.go:270 pkg/amenity/feature.go:270 msgid "Name must have at least one letter." msgstr "El nom ha de tenir com a mínim una lletra." #: pkg/carousel/admin.go:276 pkg/campsite/types/carousel.go:225 -#: pkg/campsite/carousel.go:227 +#: pkg/campsite/carousel.go:227 pkg/amenity/carousel.go:227 msgctxt "input" msgid "Slide image" msgstr "Imatge de la diapositiva" #: pkg/carousel/admin.go:277 pkg/campsite/types/carousel.go:226 -#: pkg/campsite/carousel.go:228 +#: pkg/campsite/carousel.go:228 pkg/amenity/carousel.go:228 msgctxt "action" msgid "Set slide image" msgstr "Estableix la imatge de la diapositiva" #: pkg/carousel/admin.go:346 pkg/campsite/types/carousel.go:299 #: pkg/campsite/carousel.go:302 pkg/surroundings/admin.go:335 +#: pkg/amenity/carousel.go:302 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/campsite/carousel.go:303 pkg/surroundings/admin.go:336 +#: pkg/amenity/carousel.go:303 msgid "Slide image must be an image media type." msgstr "La imatge de la diapositiva ha de ser un mèdia de tipus imatge." @@ -1613,7 +1722,7 @@ msgstr "L’idioma escollit no és vàlid." 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:67 +#: pkg/app/admin.go:70 msgid "Access forbidden" msgstr "Accés prohibit" @@ -1654,7 +1763,7 @@ msgid "Price per night must be zero or greater." msgstr "El preu per nit ha de ser com a mínim zero." #: pkg/campsite/types/feature.go:271 pkg/campsite/feature.go:268 -#: pkg/services/admin.go:315 +#: pkg/services/admin.go:315 pkg/amenity/feature.go:268 msgid "Selected icon is not valid." msgstr "La icona escollida no és vàlida." @@ -1712,7 +1821,7 @@ msgstr "El número mínim de nits no pot ser zero." msgid "Selected campsite type is not valid." msgstr "El tipus d’allotjament escollit no és vàlid." -#: pkg/campsite/admin.go:276 +#: pkg/campsite/admin.go:276 pkg/amenity/admin.go:269 msgid "Label can not be empty." msgstr "No podeu deixar l’etiqueta en blanc." diff --git a/po/es.po b/po/es.po index 3b89f4f..e739b65 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-26 22:47+0100\n" +"POT-Creation-Date: 2024-01-27 22:29+0100\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -53,10 +53,17 @@ msgstr "Servicios" msgid "The campsite offers many different services." msgstr "El camping dispone de varios servicios." +#: web/templates/public/amenity.gohtml:39 +#: web/templates/public/campsite/type.gohtml:112 +#: web/templates/public/campsite/page.gohtml:39 +msgctxt "title" +msgid "Features" +msgstr "Características" + #: web/templates/public/location.gohtml:7 #: web/templates/public/location.gohtml:13 #: web/templates/public/layout.gohtml:69 web/templates/public/layout.gohtml:97 -#: web/templates/admin/layout.gohtml:61 +#: web/templates/admin/layout.gohtml:64 msgctxt "title" msgid "Location" msgstr "Cómo llegar" @@ -85,7 +92,7 @@ msgstr "Nuestros servicios" #: web/templates/public/surroundings.gohtml:12 #: web/templates/public/layout.gohtml:68 web/templates/public/layout.gohtml:96 #: web/templates/admin/surroundings/form.gohtml:15 -#: web/templates/admin/layout.gohtml:64 +#: web/templates/admin/layout.gohtml:67 msgctxt "title" msgid "Surroundings" msgstr "El entorno" @@ -164,12 +171,6 @@ msgstr "Perros: %s/noche, atados, acompañados y con mínimo de ladrido." msgid "No dogs allowed." msgstr "No se permiten perros" -#: web/templates/public/campsite/type.gohtml:112 -#: web/templates/public/campsite/page.gohtml:39 -msgctxt "title" -msgid "Features" -msgstr "Características" - #: web/templates/public/campsite/type.gohtml:123 msgctxt "title" msgid "Info" @@ -409,7 +410,7 @@ msgstr "Menú" #: web/templates/admin/campsite/type/option/form.gohtml:16 #: web/templates/admin/campsite/type/option/index.gohtml:10 #: web/templates/admin/campsite/type/index.gohtml:10 -#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:92 +#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:95 msgctxt "title" msgid "Campsites" msgstr "Alojamientos" @@ -443,7 +444,7 @@ msgstr "Nuevo texto legal" #: web/templates/admin/legal/form.gohtml:15 #: web/templates/admin/legal/index.gohtml:6 #: web/templates/admin/legal/index.gohtml:15 -#: web/templates/admin/layout.gohtml:67 +#: web/templates/admin/layout.gohtml:70 msgctxt "title" msgid "Legal Texts" msgstr "Textos legales" @@ -462,6 +463,8 @@ msgstr "Álias" #: web/templates/admin/services/form.gohtml:53 #: web/templates/admin/profile.gohtml:29 #: web/templates/admin/surroundings/form.gohtml:41 +#: web/templates/admin/amenity/feature/form.gohtml:50 +#: web/templates/admin/amenity/form.gohtml:50 msgctxt "input" msgid "Name" msgstr "Nombre" @@ -484,6 +487,9 @@ msgstr "Contenido" #: web/templates/admin/services/form.gohtml:81 #: web/templates/admin/surroundings/form.gohtml:69 #: web/templates/admin/surroundings/index.gohtml:58 +#: web/templates/admin/amenity/feature/form.gohtml:65 +#: web/templates/admin/amenity/carousel/form.gohtml:50 +#: web/templates/admin/amenity/form.gohtml:91 #: web/templates/admin/home/index.gohtml:34 #: web/templates/admin/media/form.gohtml:39 msgctxt "action" @@ -502,6 +508,9 @@ msgstr "Actualizar" #: web/templates/admin/season/form.gohtml:75 #: web/templates/admin/services/form.gohtml:83 #: web/templates/admin/surroundings/form.gohtml:71 +#: web/templates/admin/amenity/feature/form.gohtml:67 +#: web/templates/admin/amenity/carousel/form.gohtml:52 +#: web/templates/admin/amenity/form.gohtml:93 msgctxt "action" msgid "Add" msgstr "Añadir" @@ -519,6 +528,8 @@ msgstr "Añadir texto legal" #: web/templates/admin/season/index.gohtml:29 #: web/templates/admin/user/index.gohtml:20 #: web/templates/admin/surroundings/index.gohtml:83 +#: web/templates/admin/amenity/feature/index.gohtml:30 +#: web/templates/admin/amenity/index.gohtml:21 msgctxt "header" msgid "Name" msgstr "Nombre" @@ -542,6 +553,7 @@ msgstr "Nueva diapositiva del carrusel" #: web/templates/admin/carousel/form.gohtml:40 #: web/templates/admin/campsite/carousel/form.gohtml:35 #: web/templates/admin/campsite/type/carousel/form.gohtml:44 +#: web/templates/admin/amenity/carousel/form.gohtml:35 msgctxt "input" msgid "Caption" msgstr "Leyenda" @@ -592,12 +604,14 @@ msgstr "Características del alojamiento" #: web/templates/admin/campsite/feature/form.gohtml:32 #: web/templates/admin/campsite/type/feature/form.gohtml:41 #: web/templates/admin/services/form.gohtml:35 +#: web/templates/admin/amenity/feature/form.gohtml:32 msgctxt "input" msgid "Icon" msgstr "Icono" #: web/templates/admin/campsite/feature/index.gohtml:15 #: web/templates/admin/campsite/type/feature/index.gohtml:16 +#: web/templates/admin/amenity/feature/index.gohtml:15 msgctxt "action" msgid "Add Feature" msgstr "Añadir características" @@ -611,6 +625,8 @@ msgstr "Añadir características" #: web/templates/admin/services/index.gohtml:75 #: web/templates/admin/user/index.gohtml:23 #: web/templates/admin/surroundings/index.gohtml:84 +#: web/templates/admin/amenity/feature/index.gohtml:31 +#: web/templates/admin/amenity/carousel/index.gohtml:31 #: web/templates/admin/home/index.gohtml:54 #: web/templates/admin/home/index.gohtml:99 msgctxt "header" @@ -619,6 +635,7 @@ msgstr "Acciones" #: web/templates/admin/campsite/feature/index.gohtml:35 #: web/templates/admin/campsite/type/feature/index.gohtml:36 +#: web/templates/admin/amenity/feature/index.gohtml:35 msgid "Are you sure you wish to delete this feature?" msgstr "¿Estáis seguro de querer borrar esta característica?" @@ -632,6 +649,8 @@ msgstr "¿Estáis seguro de querer borrar esta característica?" #: web/templates/admin/user/index.gohtml:37 #: web/templates/admin/surroundings/index.gohtml:63 #: web/templates/admin/surroundings/index.gohtml:101 +#: web/templates/admin/amenity/feature/index.gohtml:47 +#: web/templates/admin/amenity/carousel/index.gohtml:49 #: web/templates/admin/home/index.gohtml:71 #: web/templates/admin/home/index.gohtml:116 msgctxt "action" @@ -661,6 +680,7 @@ msgstr "Carrusel del alojamiento" #: web/templates/admin/campsite/carousel/index.gohtml:16 #: web/templates/admin/campsite/type/carousel/index.gohtml:17 #: web/templates/admin/services/index.gohtml:15 +#: web/templates/admin/amenity/carousel/index.gohtml:16 #: web/templates/admin/home/index.gohtml:84 msgctxt "action" msgid "Add slide" @@ -670,6 +690,7 @@ msgstr "Añadir diapositiva" #: web/templates/admin/campsite/type/carousel/index.gohtml:30 #: web/templates/admin/services/index.gohtml:28 #: web/templates/admin/surroundings/index.gohtml:82 +#: web/templates/admin/amenity/carousel/index.gohtml:29 #: web/templates/admin/home/index.gohtml:52 #: web/templates/admin/home/index.gohtml:97 msgctxt "header" @@ -679,6 +700,7 @@ msgstr "Imagen" #: web/templates/admin/campsite/carousel/index.gohtml:30 #: web/templates/admin/campsite/type/carousel/index.gohtml:31 #: web/templates/admin/services/index.gohtml:29 +#: web/templates/admin/amenity/carousel/index.gohtml:30 #: web/templates/admin/home/index.gohtml:53 #: web/templates/admin/home/index.gohtml:98 msgctxt "header" @@ -688,6 +710,7 @@ msgstr "Leyenda" #: web/templates/admin/campsite/carousel/index.gohtml:35 #: web/templates/admin/campsite/type/carousel/index.gohtml:36 #: web/templates/admin/services/index.gohtml:34 +#: web/templates/admin/amenity/carousel/index.gohtml:35 #: web/templates/admin/home/index.gohtml:103 msgid "Are you sure you wish to delete this slide?" msgstr "¿Estáis seguro de querer borrar esta diapositiva?" @@ -695,6 +718,7 @@ msgstr "¿Estáis seguro de querer borrar esta diapositiva?" #: web/templates/admin/campsite/carousel/index.gohtml:58 #: web/templates/admin/campsite/type/carousel/index.gohtml:59 #: web/templates/admin/services/index.gohtml:56 +#: web/templates/admin/amenity/carousel/index.gohtml:58 #: web/templates/admin/home/index.gohtml:125 msgid "No slides added yet." msgstr "No se ha añadido ninguna diapositiva todavía." @@ -725,16 +749,19 @@ msgid "Select campsite type" msgstr "Escoged un tipo de alojamiento" #: web/templates/admin/campsite/form.gohtml:56 +#: web/templates/admin/amenity/form.gohtml:42 msgctxt "input" msgid "Label" msgstr "Etiqueta" #: web/templates/admin/campsite/form.gohtml:64 +#: web/templates/admin/amenity/form.gohtml:63 msgctxt "input" msgid "Info (First Column)" msgstr "Información (primera columna)" #: web/templates/admin/campsite/form.gohtml:77 +#: web/templates/admin/amenity/form.gohtml:76 msgctxt "input" msgid "Info (Second Column)" msgstr "Información (segunda columna)" @@ -745,6 +772,7 @@ msgid "Add Campsite" msgstr "Añadir alojamiento" #: web/templates/admin/campsite/index.gohtml:23 +#: web/templates/admin/amenity/index.gohtml:20 msgctxt "header" msgid "Label" msgstr "Etiqueta" @@ -756,24 +784,28 @@ msgstr "Tipo" #: web/templates/admin/campsite/index.gohtml:25 #: web/templates/admin/campsite/type/index.gohtml:30 +#: web/templates/admin/amenity/index.gohtml:22 msgctxt "header" msgid "Features" msgstr "Características" #: web/templates/admin/campsite/index.gohtml:26 #: web/templates/admin/campsite/type/index.gohtml:32 +#: web/templates/admin/amenity/index.gohtml:23 msgctxt "header" msgid "Carousel" msgstr "Carrusel" #: web/templates/admin/campsite/index.gohtml:36 #: web/templates/admin/campsite/type/index.gohtml:45 +#: web/templates/admin/amenity/index.gohtml:33 msgctxt "action" msgid "Edit Features" msgstr "Editar las características" #: web/templates/admin/campsite/index.gohtml:39 #: web/templates/admin/campsite/type/index.gohtml:51 +#: web/templates/admin/amenity/index.gohtml:36 msgctxt "action" msgid "Edit Carousel" msgstr "Editar el carrusel" @@ -782,6 +814,7 @@ msgstr "Editar el carrusel" #: web/templates/admin/campsite/type/index.gohtml:53 #: web/templates/admin/season/index.gohtml:44 #: web/templates/admin/user/login-attempts.gohtml:31 +#: web/templates/admin/amenity/index.gohtml:38 msgid "Yes" msgstr "Sí" @@ -789,6 +822,7 @@ msgstr "Sí" #: web/templates/admin/campsite/type/index.gohtml:53 #: web/templates/admin/season/index.gohtml:44 #: web/templates/admin/user/login-attempts.gohtml:31 +#: web/templates/admin/amenity/index.gohtml:38 msgid "No" msgstr "No" @@ -1004,7 +1038,7 @@ msgstr "Nueva temporada" #: web/templates/admin/season/form.gohtml:15 #: web/templates/admin/season/index.gohtml:6 #: web/templates/admin/season/index.gohtml:15 -#: web/templates/admin/layout.gohtml:49 +#: web/templates/admin/layout.gohtml:52 msgctxt "title" msgid "Seasons" msgstr "Temporadas" @@ -1041,7 +1075,7 @@ msgid "Cancel" msgstr "Cancelar" #: web/templates/admin/dashboard.gohtml:6 -#: web/templates/admin/dashboard.gohtml:13 web/templates/admin/layout.gohtml:86 +#: web/templates/admin/dashboard.gohtml:13 web/templates/admin/layout.gohtml:89 msgctxt "title" msgid "Dashboard" msgstr "Panel" @@ -1075,7 +1109,7 @@ msgstr "Nuevo servicio" #: web/templates/admin/services/form.gohtml:15 #: web/templates/admin/services/index.gohtml:6 -#: web/templates/admin/layout.gohtml:58 +#: web/templates/admin/layout.gohtml:61 msgctxt "title" msgid "Services Page" msgstr "Página de servicios" @@ -1144,7 +1178,7 @@ msgstr "Intentos de entrada" #: web/templates/admin/user/login-attempts.gohtml:10 #: web/templates/admin/user/index.gohtml:6 #: web/templates/admin/user/index.gohtml:16 -#: web/templates/admin/layout.gohtml:70 +#: web/templates/admin/layout.gohtml:73 msgctxt "title" msgid "Users" msgstr "Usuarios" @@ -1330,6 +1364,78 @@ msgstr "¿Estáis seguro de querer borrar este punto de interés?" msgid "No highlights added yet." msgstr "No se ha añadido ningún punto de interés todavía." +#: web/templates/admin/amenity/feature/form.gohtml:8 +msgctxt "title" +msgid "Edit Amenity Feature" +msgstr "Edición de las características de la instalación" + +#: web/templates/admin/amenity/feature/form.gohtml:10 +msgctxt "title" +msgid "New Amenity Feature" +msgstr "Nueva característica de la instalación" + +#: web/templates/admin/amenity/feature/form.gohtml:16 +#: web/templates/admin/amenity/feature/index.gohtml:10 +#: web/templates/admin/amenity/carousel/form.gohtml:16 +#: web/templates/admin/amenity/carousel/index.gohtml:10 +#: web/templates/admin/amenity/form.gohtml:15 +#: web/templates/admin/amenity/index.gohtml:6 +#: web/templates/admin/layout.gohtml:49 +msgctxt "title" +msgid "Amenities" +msgstr "Instalaciones" + +#: web/templates/admin/amenity/feature/form.gohtml:17 +#: web/templates/admin/amenity/feature/index.gohtml:6 +msgctxt "title" +msgid "Amenity Features" +msgstr "Características de la instalación" + +#: web/templates/admin/amenity/feature/index.gohtml:56 +msgid "No amenity features added yet." +msgstr "No se ha añadido ninguna característica a la instalación todavía." + +#: web/templates/admin/amenity/carousel/form.gohtml:8 +msgctxt "title" +msgid "Edit Amenity Carousel Slide" +msgstr "Edición de la diapositiva del carrusel de la instalación" + +#: web/templates/admin/amenity/carousel/form.gohtml:10 +msgctxt "title" +msgid "New Amenity Carousel Slide" +msgstr "Nueva diapositiva del carrusel de la instalación" + +#: web/templates/admin/amenity/carousel/form.gohtml:17 +#: web/templates/admin/amenity/carousel/index.gohtml:6 +msgctxt "title" +msgid "Amenity Carousel" +msgstr "Carrusel de la instalación" + +#: web/templates/admin/amenity/form.gohtml:8 +msgctxt "title" +msgid "Edit Amenity" +msgstr "Edición de la instalación" + +#: web/templates/admin/amenity/form.gohtml:10 +msgctxt "title" +msgid "New Amenity" +msgstr "Nueva instalación" + +#: web/templates/admin/amenity/form.gohtml:33 +#: web/templates/admin/amenity/index.gohtml:24 +msgctxt "amenity" +msgid "Active" +msgstr "Activa" + +#: web/templates/admin/amenity/index.gohtml:14 +msgctxt "action" +msgid "Add Amenity" +msgstr "Añadir instalación" + +#: web/templates/admin/amenity/index.gohtml:44 +msgid "No amenities added yet." +msgstr "No se ha añadido ninguna instalación todavía." + #: web/templates/admin/layout.gohtml:29 msgctxt "title" msgid "User Menu" @@ -1347,7 +1453,7 @@ msgctxt "title" msgid "Payment Settings" msgstr "Parámetros de pago" -#: web/templates/admin/layout.gohtml:52 +#: web/templates/admin/layout.gohtml:55 #: web/templates/admin/media/form.gohtml:10 #: web/templates/admin/media/index.gohtml:6 #: web/templates/admin/media/index.gohtml:14 @@ -1356,28 +1462,28 @@ msgctxt "title" msgid "Media" msgstr "Medios" -#: web/templates/admin/layout.gohtml:55 web/templates/admin/home/index.gohtml:6 +#: web/templates/admin/layout.gohtml:58 web/templates/admin/home/index.gohtml:6 msgctxt "title" msgid "Home Page" msgstr "Página de inicio" -#: web/templates/admin/layout.gohtml:75 +#: web/templates/admin/layout.gohtml:78 msgctxt "action" msgid "Logout" msgstr "Salir" -#: web/templates/admin/layout.gohtml:89 +#: web/templates/admin/layout.gohtml:92 #: web/templates/admin/booking/index.gohtml:6 #: web/templates/admin/booking/index.gohtml:16 msgctxt "title" msgid "Bookings" msgstr "Reservas" -#: web/templates/admin/layout.gohtml:98 +#: web/templates/admin/layout.gohtml:101 msgid "Breadcrumb" msgstr "Migas de pan" -#: web/templates/admin/layout.gohtml:110 +#: web/templates/admin/layout.gohtml:113 msgid "Camper Version: %s" msgstr "Camper versión: %s" @@ -1547,34 +1653,37 @@ msgstr "No se ha encontrado ninguna reserva." #: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:483 #: pkg/campsite/feature.go:269 pkg/season/admin.go:412 #: pkg/services/admin.go:316 pkg/surroundings/admin.go:340 +#: pkg/amenity/feature.go:269 pkg/amenity/admin.go:270 msgid "Name can not be empty." msgstr "No podéis dejar el nombre en blanco." #: pkg/legal/admin.go:259 pkg/campsite/types/option.go:358 #: pkg/campsite/types/feature.go:273 pkg/campsite/types/admin.go:484 -#: pkg/campsite/feature.go:270 +#: pkg/campsite/feature.go:270 pkg/amenity/feature.go:270 msgid "Name must have at least one letter." msgstr "El nombre tiene que tener como mínimo una letra." #: pkg/carousel/admin.go:276 pkg/campsite/types/carousel.go:225 -#: pkg/campsite/carousel.go:227 +#: pkg/campsite/carousel.go:227 pkg/amenity/carousel.go:227 msgctxt "input" msgid "Slide image" msgstr "Imagen de la diapositiva" #: pkg/carousel/admin.go:277 pkg/campsite/types/carousel.go:226 -#: pkg/campsite/carousel.go:228 +#: pkg/campsite/carousel.go:228 pkg/amenity/carousel.go:228 msgctxt "action" msgid "Set slide image" msgstr "Establecer la imagen de la diapositiva" #: pkg/carousel/admin.go:346 pkg/campsite/types/carousel.go:299 #: pkg/campsite/carousel.go:302 pkg/surroundings/admin.go:335 +#: pkg/amenity/carousel.go:302 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/campsite/carousel.go:303 pkg/surroundings/admin.go:336 +#: pkg/amenity/carousel.go:303 msgid "Slide image must be an image media type." msgstr "La imagen de la diapositiva tiene que ser un medio de tipo imagen." @@ -1613,7 +1722,7 @@ msgstr "El idioma escogido no es válido." 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:67 +#: pkg/app/admin.go:70 msgid "Access forbidden" msgstr "Acceso prohibido" @@ -1654,7 +1763,7 @@ msgid "Price per night must be zero or greater." msgstr "El precio por noche tiene que ser como mínimo cero." #: pkg/campsite/types/feature.go:271 pkg/campsite/feature.go:268 -#: pkg/services/admin.go:315 +#: pkg/services/admin.go:315 pkg/amenity/feature.go:268 msgid "Selected icon is not valid." msgstr "El icono escogido no es válido." @@ -1712,7 +1821,7 @@ msgstr "El número mínimo de noches no puede ser cero." msgid "Selected campsite type is not valid." msgstr "El tipo de alojamiento escogido no es válido." -#: pkg/campsite/admin.go:276 +#: pkg/campsite/admin.go:276 pkg/amenity/admin.go:269 msgid "Label can not be empty." msgstr "No podéis dejar la etiqueta en blanco." diff --git a/po/fr.po b/po/fr.po index a49a0c1..ece2a26 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-26 22:47+0100\n" +"POT-Creation-Date: 2024-01-27 22:29+0100\n" "PO-Revision-Date: 2023-12-20 10:13+0100\n" "Last-Translator: Oriol Carbonell \n" "Language-Team: French \n" @@ -54,10 +54,17 @@ msgstr "Services" msgid "The campsite offers many different services." msgstr "Le camping propose de nombreux services différents." +#: web/templates/public/amenity.gohtml:39 +#: web/templates/public/campsite/type.gohtml:112 +#: web/templates/public/campsite/page.gohtml:39 +msgctxt "title" +msgid "Features" +msgstr "Caractéristiques" + #: web/templates/public/location.gohtml:7 #: web/templates/public/location.gohtml:13 #: web/templates/public/layout.gohtml:69 web/templates/public/layout.gohtml:97 -#: web/templates/admin/layout.gohtml:61 +#: web/templates/admin/layout.gohtml:64 msgctxt "title" msgid "Location" msgstr "Comment nous rejoindre" @@ -86,7 +93,7 @@ msgstr "Nos services" #: web/templates/public/surroundings.gohtml:12 #: web/templates/public/layout.gohtml:68 web/templates/public/layout.gohtml:96 #: web/templates/admin/surroundings/form.gohtml:15 -#: web/templates/admin/layout.gohtml:64 +#: web/templates/admin/layout.gohtml:67 msgctxt "title" msgid "Surroundings" msgstr "Entourage" @@ -165,12 +172,6 @@ msgstr "Chiens : %s/nuit, attachés, accompagnés et aboiements minimes." msgid "No dogs allowed." msgstr "Chiens interdits." -#: web/templates/public/campsite/type.gohtml:112 -#: web/templates/public/campsite/page.gohtml:39 -msgctxt "title" -msgid "Features" -msgstr "Caractéristiques" - #: web/templates/public/campsite/type.gohtml:123 msgctxt "title" msgid "Info" @@ -410,7 +411,7 @@ msgstr "Menu" #: web/templates/admin/campsite/type/option/form.gohtml:16 #: web/templates/admin/campsite/type/option/index.gohtml:10 #: web/templates/admin/campsite/type/index.gohtml:10 -#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:92 +#: web/templates/admin/layout.gohtml:46 web/templates/admin/layout.gohtml:95 msgctxt "title" msgid "Campsites" msgstr "Locatifs" @@ -444,7 +445,7 @@ msgstr "Nouveau texte juridique" #: web/templates/admin/legal/form.gohtml:15 #: web/templates/admin/legal/index.gohtml:6 #: web/templates/admin/legal/index.gohtml:15 -#: web/templates/admin/layout.gohtml:67 +#: web/templates/admin/layout.gohtml:70 msgctxt "title" msgid "Legal Texts" msgstr "Textes juridiques" @@ -463,6 +464,8 @@ msgstr "Slug" #: web/templates/admin/services/form.gohtml:53 #: web/templates/admin/profile.gohtml:29 #: web/templates/admin/surroundings/form.gohtml:41 +#: web/templates/admin/amenity/feature/form.gohtml:50 +#: web/templates/admin/amenity/form.gohtml:50 msgctxt "input" msgid "Name" msgstr "Nom" @@ -485,6 +488,9 @@ msgstr "Contenu" #: web/templates/admin/services/form.gohtml:81 #: web/templates/admin/surroundings/form.gohtml:69 #: web/templates/admin/surroundings/index.gohtml:58 +#: web/templates/admin/amenity/feature/form.gohtml:65 +#: web/templates/admin/amenity/carousel/form.gohtml:50 +#: web/templates/admin/amenity/form.gohtml:91 #: web/templates/admin/home/index.gohtml:34 #: web/templates/admin/media/form.gohtml:39 msgctxt "action" @@ -503,6 +509,9 @@ msgstr "Mettre à jour" #: web/templates/admin/season/form.gohtml:75 #: web/templates/admin/services/form.gohtml:83 #: web/templates/admin/surroundings/form.gohtml:71 +#: web/templates/admin/amenity/feature/form.gohtml:67 +#: web/templates/admin/amenity/carousel/form.gohtml:52 +#: web/templates/admin/amenity/form.gohtml:93 msgctxt "action" msgid "Add" msgstr "Ajouter" @@ -520,6 +529,8 @@ msgstr "Ajouter un texte juridique" #: web/templates/admin/season/index.gohtml:29 #: web/templates/admin/user/index.gohtml:20 #: web/templates/admin/surroundings/index.gohtml:83 +#: web/templates/admin/amenity/feature/index.gohtml:30 +#: web/templates/admin/amenity/index.gohtml:21 msgctxt "header" msgid "Name" msgstr "Nom" @@ -538,11 +549,12 @@ msgstr "Modifier la diapositive du carrousel" #: web/templates/admin/carousel/form.gohtml:30 msgctxt "title" msgid "New Carousel Slide" -msgstr "Nouveau toboggan carrousel" +msgstr "Nouveau diapositive carrousel" #: web/templates/admin/carousel/form.gohtml:40 #: web/templates/admin/campsite/carousel/form.gohtml:35 #: web/templates/admin/campsite/type/carousel/form.gohtml:44 +#: web/templates/admin/amenity/carousel/form.gohtml:35 msgctxt "input" msgid "Caption" msgstr "Légende" @@ -593,12 +605,14 @@ msgstr "Caractéristiques de camping" #: web/templates/admin/campsite/feature/form.gohtml:32 #: web/templates/admin/campsite/type/feature/form.gohtml:41 #: web/templates/admin/services/form.gohtml:35 +#: web/templates/admin/amenity/feature/form.gohtml:32 msgctxt "input" msgid "Icon" msgstr "Icône" #: web/templates/admin/campsite/feature/index.gohtml:15 #: web/templates/admin/campsite/type/feature/index.gohtml:16 +#: web/templates/admin/amenity/feature/index.gohtml:15 msgctxt "action" msgid "Add Feature" msgstr "Ajouter une caractéristique" @@ -612,6 +626,8 @@ msgstr "Ajouter une caractéristique" #: web/templates/admin/services/index.gohtml:75 #: web/templates/admin/user/index.gohtml:23 #: web/templates/admin/surroundings/index.gohtml:84 +#: web/templates/admin/amenity/feature/index.gohtml:31 +#: web/templates/admin/amenity/carousel/index.gohtml:31 #: web/templates/admin/home/index.gohtml:54 #: web/templates/admin/home/index.gohtml:99 msgctxt "header" @@ -620,6 +636,7 @@ msgstr "Actions" #: web/templates/admin/campsite/feature/index.gohtml:35 #: web/templates/admin/campsite/type/feature/index.gohtml:36 +#: web/templates/admin/amenity/feature/index.gohtml:35 msgid "Are you sure you wish to delete this feature?" msgstr "Êtes-vous sûr de vouloir supprimer cette caractéristique ?" @@ -633,6 +650,8 @@ msgstr "Êtes-vous sûr de vouloir supprimer cette caractéristique ?" #: web/templates/admin/user/index.gohtml:37 #: web/templates/admin/surroundings/index.gohtml:63 #: web/templates/admin/surroundings/index.gohtml:101 +#: web/templates/admin/amenity/feature/index.gohtml:47 +#: web/templates/admin/amenity/carousel/index.gohtml:49 #: web/templates/admin/home/index.gohtml:71 #: web/templates/admin/home/index.gohtml:116 msgctxt "action" @@ -651,7 +670,7 @@ msgstr "Modifier la diapositive du carrousel de camping" #: web/templates/admin/campsite/carousel/form.gohtml:10 msgctxt "title" msgid "New Campsite Carousel Slide" -msgstr "Nouveau toboggan de carrousel de camping" +msgstr "Nouveau diapositive de carrousel de camping" #: web/templates/admin/campsite/carousel/form.gohtml:17 #: web/templates/admin/campsite/carousel/index.gohtml:6 @@ -662,6 +681,7 @@ msgstr "Camping Carrousel" #: web/templates/admin/campsite/carousel/index.gohtml:16 #: web/templates/admin/campsite/type/carousel/index.gohtml:17 #: web/templates/admin/services/index.gohtml:15 +#: web/templates/admin/amenity/carousel/index.gohtml:16 #: web/templates/admin/home/index.gohtml:84 msgctxt "action" msgid "Add slide" @@ -671,6 +691,7 @@ msgstr "Ajouter la diapositive" #: web/templates/admin/campsite/type/carousel/index.gohtml:30 #: web/templates/admin/services/index.gohtml:28 #: web/templates/admin/surroundings/index.gohtml:82 +#: web/templates/admin/amenity/carousel/index.gohtml:29 #: web/templates/admin/home/index.gohtml:52 #: web/templates/admin/home/index.gohtml:97 msgctxt "header" @@ -680,6 +701,7 @@ msgstr "Image" #: web/templates/admin/campsite/carousel/index.gohtml:30 #: web/templates/admin/campsite/type/carousel/index.gohtml:31 #: web/templates/admin/services/index.gohtml:29 +#: web/templates/admin/amenity/carousel/index.gohtml:30 #: web/templates/admin/home/index.gohtml:53 #: web/templates/admin/home/index.gohtml:98 msgctxt "header" @@ -689,6 +711,7 @@ msgstr "Légende" #: web/templates/admin/campsite/carousel/index.gohtml:35 #: web/templates/admin/campsite/type/carousel/index.gohtml:36 #: web/templates/admin/services/index.gohtml:34 +#: web/templates/admin/amenity/carousel/index.gohtml:35 #: web/templates/admin/home/index.gohtml:103 msgid "Are you sure you wish to delete this slide?" msgstr "Êtes-vous sûr de vouloir supprimer cette diapositive ?" @@ -696,6 +719,7 @@ msgstr "Êtes-vous sûr de vouloir supprimer cette diapositive ?" #: web/templates/admin/campsite/carousel/index.gohtml:58 #: web/templates/admin/campsite/type/carousel/index.gohtml:59 #: web/templates/admin/services/index.gohtml:56 +#: web/templates/admin/amenity/carousel/index.gohtml:58 #: web/templates/admin/home/index.gohtml:125 msgid "No slides added yet." msgstr "Aucune diapositive n’a encore été ajoutée." @@ -726,16 +750,19 @@ msgid "Select campsite type" msgstr "Sélectionnez le type d’emplacement" #: web/templates/admin/campsite/form.gohtml:56 +#: web/templates/admin/amenity/form.gohtml:42 msgctxt "input" msgid "Label" msgstr "Label" #: web/templates/admin/campsite/form.gohtml:64 +#: web/templates/admin/amenity/form.gohtml:63 msgctxt "input" msgid "Info (First Column)" msgstr "Info (première colonne)" #: web/templates/admin/campsite/form.gohtml:77 +#: web/templates/admin/amenity/form.gohtml:76 msgctxt "input" msgid "Info (Second Column)" msgstr "Info (deuxième colonne)" @@ -746,6 +773,7 @@ msgid "Add Campsite" msgstr "Ajouter un camping" #: web/templates/admin/campsite/index.gohtml:23 +#: web/templates/admin/amenity/index.gohtml:20 msgctxt "header" msgid "Label" msgstr "Label" @@ -757,24 +785,28 @@ msgstr "Type" #: web/templates/admin/campsite/index.gohtml:25 #: web/templates/admin/campsite/type/index.gohtml:30 +#: web/templates/admin/amenity/index.gohtml:22 msgctxt "header" msgid "Features" msgstr "Caractéristiques" #: web/templates/admin/campsite/index.gohtml:26 #: web/templates/admin/campsite/type/index.gohtml:32 +#: web/templates/admin/amenity/index.gohtml:23 msgctxt "header" msgid "Carousel" msgstr "Carrousel" #: web/templates/admin/campsite/index.gohtml:36 #: web/templates/admin/campsite/type/index.gohtml:45 +#: web/templates/admin/amenity/index.gohtml:33 msgctxt "action" msgid "Edit Features" msgstr "Edit caractéristiques" #: web/templates/admin/campsite/index.gohtml:39 #: web/templates/admin/campsite/type/index.gohtml:51 +#: web/templates/admin/amenity/index.gohtml:36 msgctxt "action" msgid "Edit Carousel" msgstr "Modifier le carrousel" @@ -783,6 +815,7 @@ msgstr "Modifier le carrousel" #: web/templates/admin/campsite/type/index.gohtml:53 #: web/templates/admin/season/index.gohtml:44 #: web/templates/admin/user/login-attempts.gohtml:31 +#: web/templates/admin/amenity/index.gohtml:38 msgid "Yes" msgstr "Oui" @@ -790,6 +823,7 @@ msgstr "Oui" #: web/templates/admin/campsite/type/index.gohtml:53 #: web/templates/admin/season/index.gohtml:44 #: web/templates/admin/user/login-attempts.gohtml:31 +#: web/templates/admin/amenity/index.gohtml:38 msgid "No" msgstr "Non" @@ -844,7 +878,7 @@ msgstr "Modifier la diapositive du carrousel de type camping" #: web/templates/admin/campsite/type/carousel/form.gohtml:34 msgctxt "title" msgid "New Campsite Type Carousel Slide" -msgstr "Nouveau toboggan de carrousel de type camping" +msgstr "Nouveau diapositive de carrousel de type camping" #: web/templates/admin/campsite/type/carousel/form.gohtml:18 #: web/templates/admin/campsite/type/carousel/index.gohtml:6 @@ -1005,7 +1039,7 @@ msgstr "Nouvelle saison" #: web/templates/admin/season/form.gohtml:15 #: web/templates/admin/season/index.gohtml:6 #: web/templates/admin/season/index.gohtml:15 -#: web/templates/admin/layout.gohtml:49 +#: web/templates/admin/layout.gohtml:52 msgctxt "title" msgid "Seasons" msgstr "Saisons" @@ -1042,7 +1076,7 @@ msgid "Cancel" msgstr "Annuler" #: web/templates/admin/dashboard.gohtml:6 -#: web/templates/admin/dashboard.gohtml:13 web/templates/admin/layout.gohtml:86 +#: web/templates/admin/dashboard.gohtml:13 web/templates/admin/layout.gohtml:89 msgctxt "title" msgid "Dashboard" msgstr "Tableau de bord" @@ -1076,7 +1110,7 @@ msgstr "Nouveau service" #: web/templates/admin/services/form.gohtml:15 #: web/templates/admin/services/index.gohtml:6 -#: web/templates/admin/layout.gohtml:58 +#: web/templates/admin/layout.gohtml:61 msgctxt "title" msgid "Services Page" msgstr "La page des services" @@ -1145,7 +1179,7 @@ msgstr "Tentatives de connexion" #: web/templates/admin/user/login-attempts.gohtml:10 #: web/templates/admin/user/index.gohtml:6 #: web/templates/admin/user/index.gohtml:16 -#: web/templates/admin/layout.gohtml:70 +#: web/templates/admin/layout.gohtml:73 msgctxt "title" msgid "Users" msgstr "Utilisateurs" @@ -1200,7 +1234,7 @@ msgstr "Détails de la taxe" #: web/templates/admin/taxDetails.gohtml:61 msgctxt "input" msgid "Business Name" -msgstr "Nom de l'entreprise" +msgstr "Nom de l’entreprise" #: web/templates/admin/taxDetails.gohtml:29 msgctxt "input" @@ -1331,6 +1365,78 @@ msgstr "Êtes-vous sûr de vouloir supprimer cette point d’intérêt ?" msgid "No highlights added yet." msgstr "Aucun point d’intérêt n’a encore été ajoutée." +#: web/templates/admin/amenity/feature/form.gohtml:8 +msgctxt "title" +msgid "Edit Amenity Feature" +msgstr "Modifier l’entité de installation" + +#: web/templates/admin/amenity/feature/form.gohtml:10 +msgctxt "title" +msgid "New Amenity Feature" +msgstr "Nouvelle caractéristique de installation" + +#: web/templates/admin/amenity/feature/form.gohtml:16 +#: web/templates/admin/amenity/feature/index.gohtml:10 +#: web/templates/admin/amenity/carousel/form.gohtml:16 +#: web/templates/admin/amenity/carousel/index.gohtml:10 +#: web/templates/admin/amenity/form.gohtml:15 +#: web/templates/admin/amenity/index.gohtml:6 +#: web/templates/admin/layout.gohtml:49 +msgctxt "title" +msgid "Amenities" +msgstr "" + +#: web/templates/admin/amenity/feature/form.gohtml:17 +#: web/templates/admin/amenity/feature/index.gohtml:6 +msgctxt "title" +msgid "Amenity Features" +msgstr "Caractéristiques de installation" + +#: web/templates/admin/amenity/feature/index.gohtml:56 +msgid "No amenity features added yet." +msgstr "Aucune caractéristique de installation n’a encore été ajoutée." + +#: web/templates/admin/amenity/carousel/form.gohtml:8 +msgctxt "title" +msgid "Edit Amenity Carousel Slide" +msgstr "Modifier la diapositive du carrousel de installation" + +#: web/templates/admin/amenity/carousel/form.gohtml:10 +msgctxt "title" +msgid "New Amenity Carousel Slide" +msgstr "Nouveau diapositive de carrousel de installation" + +#: web/templates/admin/amenity/carousel/form.gohtml:17 +#: web/templates/admin/amenity/carousel/index.gohtml:6 +msgctxt "title" +msgid "Amenity Carousel" +msgstr "Carrousel de installation" + +#: web/templates/admin/amenity/form.gohtml:8 +msgctxt "title" +msgid "Edit Amenity" +msgstr "Modifier l’installation" + +#: web/templates/admin/amenity/form.gohtml:10 +msgctxt "title" +msgid "New Amenity" +msgstr "Nouveau installation" + +#: web/templates/admin/amenity/form.gohtml:33 +#: web/templates/admin/amenity/index.gohtml:24 +msgctxt "amenity" +msgid "Active" +msgstr "Actif" + +#: web/templates/admin/amenity/index.gohtml:14 +msgctxt "action" +msgid "Add Amenity" +msgstr "Ajouter un installation" + +#: web/templates/admin/amenity/index.gohtml:44 +msgid "No amenities added yet." +msgstr "Aucun installation n’a encore été ajouté." + #: web/templates/admin/layout.gohtml:29 msgctxt "title" msgid "User Menu" @@ -1339,7 +1445,7 @@ msgstr "Menu utilisateur" #: web/templates/admin/layout.gohtml:37 msgctxt "title" msgid "Company Settings" -msgstr "Paramètres de l'entreprise" +msgstr "Paramètres de l’entreprise" #: web/templates/admin/layout.gohtml:40 #: web/templates/admin/booking/payment.gohtml:6 @@ -1348,7 +1454,7 @@ msgctxt "title" msgid "Payment Settings" msgstr "Paramètres de paiement" -#: web/templates/admin/layout.gohtml:52 +#: web/templates/admin/layout.gohtml:55 #: web/templates/admin/media/form.gohtml:10 #: web/templates/admin/media/index.gohtml:6 #: web/templates/admin/media/index.gohtml:14 @@ -1357,28 +1463,28 @@ msgctxt "title" msgid "Media" msgstr "Média" -#: web/templates/admin/layout.gohtml:55 web/templates/admin/home/index.gohtml:6 +#: web/templates/admin/layout.gohtml:58 web/templates/admin/home/index.gohtml:6 msgctxt "title" msgid "Home Page" msgstr "Page d'accueil" -#: web/templates/admin/layout.gohtml:75 +#: web/templates/admin/layout.gohtml:78 msgctxt "action" msgid "Logout" msgstr "Déconnexion" -#: web/templates/admin/layout.gohtml:89 +#: web/templates/admin/layout.gohtml:92 #: web/templates/admin/booking/index.gohtml:6 #: web/templates/admin/booking/index.gohtml:16 msgctxt "title" msgid "Bookings" msgstr "Réservations" -#: web/templates/admin/layout.gohtml:98 +#: web/templates/admin/layout.gohtml:101 msgid "Breadcrumb" msgstr "Fil d’Ariane" -#: web/templates/admin/layout.gohtml:110 +#: web/templates/admin/layout.gohtml:113 msgid "Camper Version: %s" msgstr "Camper version: %s" @@ -1548,34 +1654,37 @@ msgstr "Aucune réservation trouvée." #: pkg/campsite/types/feature.go:272 pkg/campsite/types/admin.go:483 #: pkg/campsite/feature.go:269 pkg/season/admin.go:412 #: pkg/services/admin.go:316 pkg/surroundings/admin.go:340 +#: pkg/amenity/feature.go:269 pkg/amenity/admin.go:270 msgid "Name can not be empty." msgstr "Le nom ne peut pas être laissé vide." #: pkg/legal/admin.go:259 pkg/campsite/types/option.go:358 #: pkg/campsite/types/feature.go:273 pkg/campsite/types/admin.go:484 -#: pkg/campsite/feature.go:270 +#: pkg/campsite/feature.go:270 pkg/amenity/feature.go:270 msgid "Name must have at least one letter." msgstr "Le nom doit comporter au moins une lettre." #: pkg/carousel/admin.go:276 pkg/campsite/types/carousel.go:225 -#: pkg/campsite/carousel.go:227 +#: pkg/campsite/carousel.go:227 pkg/amenity/carousel.go:227 msgctxt "input" msgid "Slide image" msgstr "Image du diaporama" #: pkg/carousel/admin.go:277 pkg/campsite/types/carousel.go:226 -#: pkg/campsite/carousel.go:228 +#: pkg/campsite/carousel.go:228 pkg/amenity/carousel.go:228 msgctxt "action" msgid "Set slide image" msgstr "Définir l’image de la diapositive" #: pkg/carousel/admin.go:346 pkg/campsite/types/carousel.go:299 #: pkg/campsite/carousel.go:302 pkg/surroundings/admin.go:335 +#: pkg/amenity/carousel.go:302 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/campsite/carousel.go:303 pkg/surroundings/admin.go:336 +#: pkg/amenity/carousel.go:303 msgid "Slide image must be an image media type." msgstr "L’image de la diapositive doit être de type média d’image." @@ -1614,7 +1723,7 @@ msgstr "La langue sélectionnée n’est pas valide." msgid "File must be a valid PNG or JPEG image." msgstr "Le fichier doit être une image PNG ou JPEG valide." -#: pkg/app/admin.go:67 +#: pkg/app/admin.go:70 msgid "Access forbidden" msgstr "Accès interdit" @@ -1655,7 +1764,7 @@ msgid "Price per night must be zero or greater." msgstr "Le prix par nuit doit être égal ou supérieur." #: pkg/campsite/types/feature.go:271 pkg/campsite/feature.go:268 -#: pkg/services/admin.go:315 +#: pkg/services/admin.go:315 pkg/amenity/feature.go:268 msgid "Selected icon is not valid." msgstr "L’icône sélectionnée n’est pas valide." @@ -1713,7 +1822,7 @@ msgstr "Le nombre minimum de nuits doit être supérieur ou égal à une nuit." msgid "Selected campsite type is not valid." msgstr "Le type d’emplacement sélectionné n’est pas valide." -#: pkg/campsite/admin.go:276 +#: pkg/campsite/admin.go:276 pkg/amenity/admin.go:269 msgid "Label can not be empty." msgstr "L'étiquette ne peut pas être vide." diff --git a/revert/add_amenity.sql b/revert/add_amenity.sql new file mode 100644 index 0000000..3ef8357 --- /dev/null +++ b/revert/add_amenity.sql @@ -0,0 +1,7 @@ +-- Revert camper:add_amenity from pg + +begin; + +drop function if exists camper.add_amenity(integer, text, text, text, text); + +commit; diff --git a/revert/add_amenity_carousel_slide.sql b/revert/add_amenity_carousel_slide.sql new file mode 100644 index 0000000..50d9456 --- /dev/null +++ b/revert/add_amenity_carousel_slide.sql @@ -0,0 +1,7 @@ +-- Revert camper:add_amenity_carousel_slide from pg + +begin; + +drop function if exists camper.add_amenity_carousel_slide(integer, text, integer, text); + +commit; diff --git a/revert/add_amenity_feature.sql b/revert/add_amenity_feature.sql new file mode 100644 index 0000000..f2b25c6 --- /dev/null +++ b/revert/add_amenity_feature.sql @@ -0,0 +1,7 @@ +-- Revert camper:add_amenity_feature from pg + +begin; + +drop function if exists camper.add_amenity_feature(integer, text, text, text); + +commit; diff --git a/revert/amenity.sql b/revert/amenity.sql new file mode 100644 index 0000000..048131d --- /dev/null +++ b/revert/amenity.sql @@ -0,0 +1,7 @@ +-- Revert camper:amenity from pg + +begin; + +drop table if exists camper.amenity; + +commit; diff --git a/revert/amenity_carousel.sql b/revert/amenity_carousel.sql new file mode 100644 index 0000000..6fc3162 --- /dev/null +++ b/revert/amenity_carousel.sql @@ -0,0 +1,7 @@ +-- Revert camper:amenity_carousel from pg + +begin; + +drop table if exists camper.amenity_carousel; + +commit; diff --git a/revert/amenity_carousel_i18n.sql b/revert/amenity_carousel_i18n.sql new file mode 100644 index 0000000..c05623b --- /dev/null +++ b/revert/amenity_carousel_i18n.sql @@ -0,0 +1,7 @@ +-- Revert camper:amenity_carousel_i18n from pg + +begin; + +drop table if exists camper.amenity_carousel_i18n; + +commit; diff --git a/revert/amenity_feature.sql b/revert/amenity_feature.sql new file mode 100644 index 0000000..00e53f4 --- /dev/null +++ b/revert/amenity_feature.sql @@ -0,0 +1,7 @@ +-- Revert camper:amenity_feature from pg + +begin; + +drop table if exists camper.amenity_feature; + +commit; diff --git a/revert/amenity_feature_i18n.sql b/revert/amenity_feature_i18n.sql new file mode 100644 index 0000000..6cae1f3 --- /dev/null +++ b/revert/amenity_feature_i18n.sql @@ -0,0 +1,7 @@ +-- Revert camper:amenity_feature_i18n from pg + +begin; + +drop table if exists camper.amenity_feature_i18n; + +commit; diff --git a/revert/amenity_i18n.sql b/revert/amenity_i18n.sql new file mode 100644 index 0000000..942cf71 --- /dev/null +++ b/revert/amenity_i18n.sql @@ -0,0 +1,7 @@ +-- Revert camper:amenity_i18n from pg + +begin; + +drop table if exists camper.amenity_i18n; + +commit; diff --git a/revert/edit_amenity.sql b/revert/edit_amenity.sql new file mode 100644 index 0000000..e18b134 --- /dev/null +++ b/revert/edit_amenity.sql @@ -0,0 +1,7 @@ +-- Revert camper:edit_amenity from pg + +begin; + +drop function if exists camper.edit_amenity(integer, text, text, text, text, boolean); + +commit; diff --git a/revert/edit_amenity_feature.sql b/revert/edit_amenity_feature.sql new file mode 100644 index 0000000..39a0dbd --- /dev/null +++ b/revert/edit_amenity_feature.sql @@ -0,0 +1,7 @@ +-- Revert camper:edit_amenity_feature from pg + +begin; + +drop function if exists camper.edit_amenity_feature(integer, text, text); + +commit; diff --git a/revert/order_amenity_carousel.sql b/revert/order_amenity_carousel.sql new file mode 100644 index 0000000..28e9251 --- /dev/null +++ b/revert/order_amenity_carousel.sql @@ -0,0 +1,7 @@ +-- Revert camper:order_amenity_carousel from pg + +begin; + +drop function if exists camper.order_amenity_carousel(text, integer, integer[]); + +commit; diff --git a/revert/order_amenity_features.sql b/revert/order_amenity_features.sql new file mode 100644 index 0000000..c40a76a --- /dev/null +++ b/revert/order_amenity_features.sql @@ -0,0 +1,7 @@ +-- Revert camper:order_amenity_features from pg + +begin; + +drop function if exists camper.order_amenity_features(integer[]); + +commit; diff --git a/revert/remove_amenity.sql b/revert/remove_amenity.sql new file mode 100644 index 0000000..840ea5f --- /dev/null +++ b/revert/remove_amenity.sql @@ -0,0 +1,7 @@ +-- Revert camper:remove_amenity from pg + +begin; + +drop function if exists camper.remove_amenity(integer); + +commit; diff --git a/revert/remove_amenity_carousel_slide.sql b/revert/remove_amenity_carousel_slide.sql new file mode 100644 index 0000000..d2db258 --- /dev/null +++ b/revert/remove_amenity_carousel_slide.sql @@ -0,0 +1,7 @@ +-- Revert camper:remove_amenity_carousel_slide from pg + +begin; + +drop function if exists camper.remove_amenity_carousel_slide(integer, text, integer); + +commit; diff --git a/revert/remove_amenity_feature.sql b/revert/remove_amenity_feature.sql new file mode 100644 index 0000000..3813c5b --- /dev/null +++ b/revert/remove_amenity_feature.sql @@ -0,0 +1,7 @@ +-- Revert camper:remove_amenity_feature from pg + +begin; + +drop function if exists camper.remove_amenity_feature(integer); + +commit; diff --git a/revert/translate_amenity.sql b/revert/translate_amenity.sql new file mode 100644 index 0000000..0648113 --- /dev/null +++ b/revert/translate_amenity.sql @@ -0,0 +1,7 @@ +-- Revert camper:translate_amenity from pg + +begin; + +drop function if exists camper.translate_amenity(integer, text, text, text, text); + +commit; diff --git a/revert/translate_amenity_carousel_slide.sql b/revert/translate_amenity_carousel_slide.sql new file mode 100644 index 0000000..3d0eb54 --- /dev/null +++ b/revert/translate_amenity_carousel_slide.sql @@ -0,0 +1,7 @@ +-- Revert camper:translate_amenity_carousel_slide from pg + +begin; + +drop function if exists camper.translate_amenity_carousel_slide(integer, text, integer, text, text); + +commit; diff --git a/revert/translate_amenity_feature.sql b/revert/translate_amenity_feature.sql new file mode 100644 index 0000000..6e82659 --- /dev/null +++ b/revert/translate_amenity_feature.sql @@ -0,0 +1,7 @@ +-- Revert camper:translate_amenity_feature from pg + +begin; + +drop function if exists camper.translate_amenity_feature(integer, text, text); + +commit; diff --git a/sqitch.plan b/sqitch.plan index 988365b..690c051 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -197,3 +197,22 @@ campsite_feature_i18n [roles schema_camper campsite_feature language] 2024-01-26 translate_campsite_feature [roles schema_camper campsite_feature_i18n] 2024-01-26T19:26:21Z jordi fita mas # Add function to translate campsite features remove_campsite_feature [roles schema_camper campsite_feature campsite_feature_i18n] 2024-01-26T19:34:52Z jordi fita mas # Add function to remove campsite features remove_campsite_type_feature [roles schema_camper campsite_type_feature campsite_type_feature_i18n] 2024-01-26T21:35:05Z jordi fita mas # Add function to remove campsite type features +amenity [roles schema_camper company user_profile] 2024-01-27T17:54:58Z jordi fita mas # Add relation for amenities +add_amenity [roles schema_camper amenity] 2024-01-27T18:05:08Z jordi fita mas # Add function to create amenities +edit_amenity [roles schema_camper amenity] 2024-01-27T18:13:53Z jordi fita mas # Add function to edit amenities +amenity_i18n [roles schema_camper amenity language] 2024-01-27T19:15:42Z jordi fita mas # Add relation for amenity translation +translate_amenity [roles schema_camper amenity_i18n] 2024-01-27T19:21:58Z jordi fita mas # Add function to translate amenitys +amenity_carousel [roles schema_camper amenity media user_profile] 2024-01-27T01:43:53Z jordi fita mas # Add relation of amenity carousel slides +add_amenity_carousel_slide [roles schema_camper amenity amenity_carousel] 2024-01-27T02:06:17Z jordi fita mas # Add function to add amenity carousel slides +order_amenity_carousel [roles amenity_carousel amenity] 2024-01-27T02:19:24Z jordi fita mas # Add function to order amenity carousel slides +amenity_carousel_i18n [roles schema_camper amenity_carousel language] 2024-01-27T02:32:57Z jordi fita mas # Add relation of amenity carousel translations +translate_amenity_carousel_slide [roles schema_camper amenity amenity_carousel_i18n] 2024-01-27T02:41:27Z jordi fita mas # Add function to translate amenity carousel slides +remove_amenity_carousel_slide [roles schema_camper amenity_carousel amenity_carousel_i18n] 2024-01-27T02:56:26Z jordi fita mas # Add function to remove amenity carousel slide +amenity_feature [roles schema_camper amenity icon user_profile] 2024-01-27T18:36:53Z jordi fita mas # Add relation of amenity features +add_amenity_feature [roles schema_camper amenity_feature amenity] 2024-01-27T18:54:02Z jordi fita mas # Add function to add amenity features +edit_amenity_feature [roles schema_camper amenity_feature] 2024-01-27T19:02:58Z jordi fita mas # Add function to edit amenity features +order_amenity_features [roles schema_camper amenity_feature] 2024-01-27T19:09:46Z jordi fita mas # Add function to order amenity features +amenity_feature_i18n [roles schema_camper amenity_feature language] 2024-01-27T19:17:09Z jordi fita mas # Add relation for amenity features translation +translate_amenity_feature [roles schema_camper amenity_feature_i18n] 2024-01-27T19:26:21Z jordi fita mas # Add function to translate amenity features +remove_amenity_feature [roles schema_camper amenity_feature amenity_feature_i18n] 2024-01-27T19:34:52Z jordi fita mas # Add function to remove amenity features +remove_amenity [roles schema_camper amenity amenity_i18n amenity_carousel amenity_carousel_i18n amenity_feature amenity_feature_i18n] 2024-01-27T18:54:34Z jordi fita mas # Add function to remove amenity diff --git a/test/add_amenity.sql b/test/add_amenity.sql new file mode 100644 index 0000000..0ce21d5 --- /dev/null +++ b/test/add_amenity.sql @@ -0,0 +1,70 @@ +-- Test add_amenity +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(14); + +set search_path to camper, public; + +select has_function('camper', 'add_amenity', array['integer', 'text', 'text', 'text', 'text']); +select function_lang_is('camper', 'add_amenity', array['integer', 'text', 'text', 'text', 'text'], 'sql'); +select function_returns('camper', 'add_amenity', array['integer', 'text', 'text', 'text', 'text'], 'integer'); +select isnt_definer('camper', 'add_amenity', array['integer', 'text', 'text', 'text', 'text']); +select volatility_is('camper', 'add_amenity', array['integer', 'text', 'text', 'text', 'text'], 'volatile'); +select function_privs_are('camper', 'add_amenity', array ['integer', 'text', 'text', 'text', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'add_amenity', array ['integer', 'text', 'text', 'text', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'add_amenity', array ['integer', 'text', 'text', 'text', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'add_amenity', array ['integer', 'text', 'text', 'text', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate amenity 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') +; + +select lives_ok( + $$ select add_amenity(1, 'A1', 'Amenity A1', '

A1.1

', '

A1.2

') $$, + 'Should be able to add a amenity to the first company' +); + +select lives_ok( + $$ select add_amenity(1, 'B1', 'Amenity B1', '

B1.1

', '

B1.2

') $$, + 'Should be able to add a amenity to the same company, but with a different label' +); + +select lives_ok( + $$ select add_amenity(2, 'C1', 'Amenity C1', '

C1.1

', '

C1.2

') $$, + 'Should be able to add a amenity to the second company' +); + +select throws_ok( + $$ select add_amenity(3, 'C2', 'Amenity C2', '', '') $$, + '23503', 'insert or update on table "amenity" violates foreign key constraint "amenity_company_id_fkey"', + 'Should raise an error if the company is not valid.' +); + +select bag_eq( + $$ select company_id, label, name, info1::text, info2::text, active from amenity $$, + $$ values (1, 'A1', 'Amenity A1', '

A1.1

', '

A1.2

', true) + , (1, 'B1', 'Amenity B1', '

B1.1

', '

B1.2

', true) + , (2, 'C1', 'Amenity C1', '

C1.1

', '

C1.2

', true) + $$, + 'Should have added all amenities' +); + +select * +from finish(); + +select * +from finish(); + +rollback; diff --git a/test/add_amenity_carousel_slide.sql b/test/add_amenity_carousel_slide.sql new file mode 100644 index 0000000..e43f489 --- /dev/null +++ b/test/add_amenity_carousel_slide.sql @@ -0,0 +1,102 @@ +-- Test add_amenity_carousel_slide +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', 'add_amenity_carousel_slide', array['integer', 'text', 'integer', 'text']); +select function_lang_is('camper', 'add_amenity_carousel_slide', array['integer', 'text', 'integer', 'text'], 'sql'); +select function_returns('camper', 'add_amenity_carousel_slide', array['integer', 'text', 'integer', 'text'], 'integer'); +select isnt_definer('camper', 'add_amenity_carousel_slide', array['integer', 'text', 'integer', 'text']); +select volatility_is('camper', 'add_amenity_carousel_slide', array['integer', 'text', 'integer', 'text'], 'volatile'); +select function_privs_are('camper', 'add_amenity_carousel_slide', array['integer', 'text', 'integer', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'add_amenity_carousel_slide', array['integer', 'text', 'integer', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'add_amenity_carousel_slide', array['integer', 'text', 'integer', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'add_amenity_carousel_slide', array['integer', 'text', 'integer', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate amenity_carousel cascade; +truncate amenity 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 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca') +; + +insert into media_content (media_type, bytes) +values ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};') + , ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ff00ff","a"};') + , ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffff00","a"};') + , ('text/plain', 'hello, world!') + , ('image/svg+xml', '') +; + +insert into media (media_id, company_id, original_filename, content_hash) +values (3, 1, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) + , (4, 2, 'cover3.xpm', sha256('static char *s[]={"1 1 1 1","a c #ff00ff","a"};')) + , (5, 1, 'cover4.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffff00","a"};')) + , (6, 1, 'text.txt', sha256('hello, world!')) + , (7, 1, 'image.svg', sha256('')) +; + +insert into amenity (amenity_id, company_id, label, name) +values (11, 1, 'A1', 'Amenity A1') + , (12, 1, 'Z9', 'Amenity Z9') + , (13, 2, 'A1', 'Amenity A1') +; + +insert into amenity_carousel (amenity_id, media_id, caption) +values (11, 5, 'A caption') +; + +select lives_ok( + $$ select add_amenity_carousel_slide(1, 'A1', 6, 'A caption') $$, + 'Should be able to add a carousel slide with a caption' +); + +select lives_ok( + $$ select add_amenity_carousel_slide(1, 'Z9', 7, null) $$, + 'Should be able to add a carousel slide without caption' +); + +select lives_ok( + $$ select add_amenity_carousel_slide(1, 'A1', 5, 'New caption') $$, + 'Should be able to overwrite a slide with a new caption' +); + +select throws_ok( + $$ select add_amenity_carousel_slide(1, 'A2', 3, '') $$, + '23503', 'insert or update on table "amenity_carousel" violates foreign key constraint "amenity_carousel_amenity_id_fkey"', + 'Should raise an error if the label is not valid.' +); + +select throws_ok( + $$ select add_amenity_carousel_slide(3, 'A1', 3, '') $$, + '23503', 'insert or update on table "amenity_carousel" violates foreign key constraint "amenity_carousel_amenity_id_fkey"', + 'Should raise an error if the company is not valid.' +); + +select bag_eq( + $$ select amenity_id, media_id, caption from amenity_carousel $$, + $$ values (11, 5, 'New caption') + , (11, 6, 'A caption') + , (12, 7, '') + $$, + 'Should have all three slides' +); + +select * +from finish(); + +rollback; diff --git a/test/add_amenity_feature.sql b/test/add_amenity_feature.sql new file mode 100644 index 0000000..f40554e --- /dev/null +++ b/test/add_amenity_feature.sql @@ -0,0 +1,82 @@ +-- Test add_amenity_feature +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(14); + +set search_path to camper, public; + +select has_function('camper', 'add_amenity_feature', array['integer', 'text', 'text', 'text']); +select function_lang_is('camper', 'add_amenity_feature', array['integer', 'text', 'text', 'text'], 'sql'); +select function_returns('camper', 'add_amenity_feature', array['integer', 'text', 'text', 'text'], 'integer'); +select isnt_definer('camper', 'add_amenity_feature', array['integer', 'text', 'text', 'text']); +select volatility_is('camper', 'add_amenity_feature', array['integer', 'text', 'text', 'text'], 'volatile'); +select function_privs_are('camper', 'add_amenity_feature', array ['integer', 'text', 'text', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'add_amenity_feature', array ['integer', 'text', 'text', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'add_amenity_feature', array ['integer', 'text', 'text', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'add_amenity_feature', array ['integer', 'text', 'text', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate amenity_feature cascade; +truncate amenity 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 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca') +; + +insert into media_content (media_type, bytes) +values ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};') +; + +insert into media (media_id, company_id, original_filename, content_hash) +values (3, 1, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) +; + +insert into amenity (amenity_id, company_id, label, name) +values (6, 1, 'A1', 'Amenity A1') + , (7, 2, 'A1', 'Amenity A1') +; + +select lives_ok( + $$ select add_amenity_feature(1, 'A1', 'wifi', 'Feature 1') $$, + 'Should be able to add an feature to the first amenity' +); + +select lives_ok( + $$ select add_amenity_feature(2, 'A1', 'information', 'Feature 2') $$, + 'Should be able to add an feature to the second amenity' +); + +select throws_ok( + $$ select add_amenity_feature(1, 'A2', 'baby', 'Nope') $$, + '23503', 'insert or update on table "amenity_feature" violates foreign key constraint "amenity_feature_amenity_id_fkey"', + 'Should raise an error if the label is not valid.' +); + +select throws_ok( + $$ select add_amenity_feature(3, 'A1', 'baby', 'Nope') $$, + '23503', 'insert or update on table "amenity_feature" violates foreign key constraint "amenity_feature_amenity_id_fkey"', + 'Should raise an error if the company is not valid.' +); + + +select bag_eq( + $$ select amenity_id, icon_name, name from amenity_feature $$, + $$ values (6, 'wifi', 'Feature 1') + , (7, 'information', 'Feature 2') + $$, + 'Should have added all two amenity features' +); + +select * +from finish(); + +rollback; diff --git a/test/amenity.sql b/test/amenity.sql new file mode 100644 index 0000000..81af06b --- /dev/null +++ b/test/amenity.sql @@ -0,0 +1,209 @@ +-- Test amenity +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(54); + +set search_path to camper, public; + +select has_table('amenity'); +select has_pk('amenity'); +select col_is_unique('amenity', array['company_id', 'label']); +select table_privs_are('amenity', 'guest', array['SELECT']); +select table_privs_are('amenity', 'employee', array['SELECT']); +select table_privs_are('amenity', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('amenity', 'authenticator', array[]::text[]); + +select has_column('amenity', 'amenity_id'); +select col_is_pk('amenity', 'amenity_id'); +select col_type_is('amenity', 'amenity_id', 'integer'); +select col_not_null('amenity', 'amenity_id'); +select col_hasnt_default('amenity', 'amenity_id'); + +select has_column('amenity', 'company_id'); +select col_is_fk('amenity', 'company_id'); +select fk_ok('amenity', 'company_id', 'company', 'company_id'); +select col_type_is('amenity', 'company_id', 'integer'); +select col_not_null('amenity', 'company_id'); +select col_hasnt_default('amenity', 'company_id'); + +select has_column('amenity', 'label'); +select col_type_is('amenity', 'label', 'text'); +select col_not_null('amenity', 'label'); +select col_hasnt_default('amenity', 'label'); + +select has_column('amenity', 'name'); +select col_type_is('amenity', 'name', 'text'); +select col_not_null('amenity', 'name'); +select col_hasnt_default('amenity', 'name'); + +select has_column('amenity', 'info1'); +select col_type_is('amenity', 'info1', 'xml'); +select col_not_null('amenity', 'info1'); +select col_has_default('amenity', 'info1'); + +select has_column('amenity', 'info2'); +select col_type_is('amenity', 'info2', 'xml'); +select col_not_null('amenity', 'info2'); +select col_has_default('amenity', 'info2'); + +select has_column('amenity', 'active'); +select col_type_is('amenity', 'active', 'boolean'); +select col_not_null('amenity', 'active'); +select col_has_default('amenity', 'active'); +select col_default_is('amenity', 'active', 'true'); + + +set client_min_messages to warning; +truncate amenity 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 amenity (company_id, label, name) +values (2, 'W1', 'Amenity W1') + , (4, 'B1', 'Amenity B1') +; + +prepare amenity_data as +select company_id, label +from amenity +order by company_id, label; + +set role guest; +select bag_eq( + 'amenity_data', + $$ values (2, 'W1') + , (4, 'B1') + $$, + 'Everyone should be able to list all amenities across all companies' +); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2'); + +select lives_ok( + $$ insert into amenity(company_id, label, name) values (2, 'w2', 'Amenity W2') $$, + 'Admin from company 2 should be able to insert a new amenity to that company.' +); + +select bag_eq( + 'amenity_data', + $$ values (2, 'W1') + , (2, 'w2') + , (4, 'B1') + $$, + 'The new row should have been added' +); + +select lives_ok( + $$ update amenity set label = 'W2' where company_id = 2 and label = 'w2' $$, + 'Admin from company 2 should be able to update amenities of that company.' +); + +select bag_eq( + 'amenity_data', + $$ values (2, 'W1') + , (2, 'W2') + , (4, 'B1') + $$, + 'The row should have been updated.' +); + +select lives_ok( + $$ delete from amenity where company_id = 2 and label = 'W2' $$, + 'Admin from company 2 should be able to delete amenities from that company.' +); + +select bag_eq( + 'amenity_data', + $$ values (2, 'W1') + , (4, 'B1') + $$, + 'The row should have been deleted.' +); + +select throws_ok( + $$ insert into amenity (company_id, label) values (4, 'W3' ) $$, + '42501', 'new row violates row-level security policy for table "amenity"', + 'Admin from company 2 should NOT be able to insert new amenities to company 4.' +); + +select lives_ok( + $$ update amenity set label = 'Nope' where company_id = 4 $$, + 'Admin from company 2 should NOT be able to update amenity types of company 4, but no error if company_id is not changed.' +); + +select bag_eq( + 'amenity_data', + $$ values (2, 'W1') + , (4, 'B1') + $$, + 'No row should have been changed.' +); + +select throws_ok( + $$ update amenity set company_id = 4 where company_id = 2 $$, + '42501', 'new row violates row-level security policy for table "amenity"', + 'Admin from company 2 should NOT be able to move amenities to company 4' +); + +select lives_ok( + $$ delete from amenity where company_id = 4 $$, + 'Admin from company 2 should NOT be able to delete amenity types from company 4, but not error is thrown' +); + +select bag_eq( + 'amenity_data', + $$ values (2, 'W1') + , (4, 'B1') + $$, + 'No row should have been changed' +); + +select throws_ok( + $$ insert into amenity (company_id, label, name) values (2, ' ', 'Amenity') $$, + '23514', 'new row for relation "amenity" violates check constraint "label_not_empty"', + 'Should not be able to insert amenities with a blank label.' +); + +select throws_ok( + $$ insert into amenity (company_id, label, name) values (2, 'AB', ' ') $$, + '23514', 'new row for relation "amenity" violates check constraint "name_not_empty"', + 'Should not be able to insert amenities with a blank name.' +); + +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/amenity_carousel.sql b/test/amenity_carousel.sql new file mode 100644 index 0000000..1c2d1d5 --- /dev/null +++ b/test/amenity_carousel.sql @@ -0,0 +1,230 @@ +-- Test amenity_carousel +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(45); + +set search_path to camper, public; + +select has_table('amenity_carousel'); +select has_pk('amenity_carousel'); +select col_is_pk('amenity_carousel', array['amenity_id', 'media_id']); +select table_privs_are('amenity_carousel', 'guest', array['SELECT']); +select table_privs_are('amenity_carousel', 'employee', array['SELECT']); +select table_privs_are('amenity_carousel', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('amenity_carousel', 'authenticator', array[]::text[]); + +select has_column('amenity_carousel', 'amenity_id'); +select col_is_fk('amenity_carousel', 'amenity_id'); +select fk_ok('amenity_carousel', 'amenity_id', 'amenity', 'amenity_id'); +select col_type_is('amenity_carousel', 'amenity_id', 'integer'); +select col_not_null('amenity_carousel', 'amenity_id'); +select col_hasnt_default('amenity_carousel', 'amenity_id'); + +select has_column('amenity_carousel', 'media_id'); +select col_is_fk('amenity_carousel', 'media_id'); +select fk_ok('amenity_carousel', 'media_id', 'media', 'media_id'); +select col_type_is('amenity_carousel', 'media_id', 'integer'); +select col_not_null('amenity_carousel', 'media_id'); +select col_hasnt_default('amenity_carousel', 'media_id'); + +select has_column('amenity_carousel', 'caption'); +select col_type_is('amenity_carousel', 'caption', 'text'); +select col_not_null('amenity_carousel', 'caption'); +select col_hasnt_default('amenity_carousel', 'caption'); + +select has_column('amenity_carousel', 'position'); +select col_type_is('amenity_carousel', 'position', 'integer'); +select col_not_null('amenity_carousel', 'position'); +select col_has_default('amenity_carousel', 'position'); +select col_default_is('amenity_carousel', 'position', '2147483647'); + + +set client_min_messages to warning; +truncate amenity_carousel cascade; +truncate amenity 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 ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};') + , ('text/plain', 'content2') + , ('text/plain', 'content3') + , ('text/plain', 'content4') + , ('text/plain', 'content5') +; + +insert into media (media_id, company_id, original_filename, content_hash) +values ( 6, 2, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) + , ( 7, 2, 'text2.txt', sha256('content2')) + , ( 8, 2, 'text3.txt', sha256('content3')) + , ( 9, 4, 'cover4.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) + , (10, 4, 'text4.txt', sha256('content4')) + , (11, 4, 'text5.txt', sha256('content5')) +; + +insert into amenity (amenity_id, company_id, label, name) +values (23, 2, 'W1', 'Amenity W1') + , (24, 4, 'B1', 'Amenity B1') +; + +insert into amenity_carousel (amenity_id, media_id, caption) +values (23, 7, 'Caption 7') + , (24, 10, 'Caption 10') +; + +prepare carousel_data as +select amenity_id, media_id, caption +from amenity_carousel +; + +set role guest; +select bag_eq( + 'carousel_data', + $$ values (23, 7, 'Caption 7') + , (24, 10, 'Caption 10') + $$, + 'Everyone should be able to list all amenity slides across all companies' +); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2'); + +select lives_ok( + $$ insert into amenity_carousel(amenity_id, media_id, caption) values (23, 8, 'Caption 8') $$, + 'Admin from company 2 should be able to insert a new slide to amenitys of that company.' +); + +select bag_eq( + 'carousel_data', + $$ values (23, 7, 'Caption 7') + , (23, 8, 'Caption 8') + , (24, 10, 'Caption 10') + $$, + 'The new row should have been added' +); + +select lives_ok( + $$ update amenity_carousel set caption = 'Caption 8.8' where media_id = 8 $$, + 'Admin from company 2 should be able to update amenity slides of that company.' +); + +select bag_eq( + 'carousel_data', + $$ values (23, 7, 'Caption 7') + , (23, 8, 'Caption 8.8') + , (24, 10, 'Caption 10') + $$, + 'The row should have been updated.' +); + +select lives_ok( + $$ delete from amenity_carousel where media_id = 8 $$, + 'Admin from company 2 should be able to delete amenity slides from that company.' +); + +select bag_eq( + 'carousel_data', + $$ values (23, 7, 'Caption 7') + , (24, 10, 'Caption 10') + $$, + 'The row should have been deleted.' +); + +select throws_ok( + $$ insert into amenity_carousel (amenity_id, media_id, caption) values (23, 10, 'Nope') $$, + '42501', 'new row violates row-level security policy for table "amenity_carousel"', + 'Admin from company 2 should NOT be able to insert a new amenitys slide from a media of company 4.' +); + +select throws_ok( + $$ insert into amenity_carousel (amenity_id, media_id, caption) values (24, 8, 'Nope') $$, + '42501', 'new row violates row-level security policy for table "amenity_carousel"', + 'Admin from company 2 should NOT be able to insert a slide to a amenity of company 4.' +); + +select lives_ok( + $$ update amenity_carousel set caption = 'Nope' where amenity_id = 24 $$, + 'Admin from company 2 should not be able to update slides of amenitys from company 4, but no error if amenity_id or media_id is not changed.' +); + +select lives_ok( + $$ update amenity_carousel set caption = 'Nope' where media_id = 10 $$, + 'Admin from company 2 should not be able to update slides of amenitys from company 4, but no error if amenity_id or media_id is not changed.' +); + +select bag_eq( + 'carousel_data', + $$ values (23, 7, 'Caption 7') + , (24, 10, 'Caption 10') + $$, + 'No row should have been changed.' +); + +select throws_ok( + $$ update amenity_carousel set amenity_id = 24 where amenity_id = 23 $$, + '42501', 'new row violates row-level security policy for table "amenity_carousel"', + 'Admin from company 2 should NOT be able to move slides to amenitys of company 4' +); + +select throws_ok( + $$ update amenity_carousel set media_id = 11 where media_id = 7 $$, + '42501', 'new row violates row-level security policy for table "amenity_carousel"', + 'Admin from company 2 should NOT be able to use media from company 4' +); + +select lives_ok( + $$ delete from amenity_carousel where amenity_id = 24 $$, + 'Admin from company 2 should NOT be able to delete slides of amenitys from company 4, but not error is thrown' +); + +select lives_ok( + $$ delete from amenity_carousel where media_id = 10 $$, + 'Admin from company 2 should NOT be able to delete slides with media from company 4, but not error is thrown' +); + +select bag_eq( + 'carousel_data', + $$ values (23, 7, 'Caption 7') + , (24, 10, 'Caption 10') + $$, + 'No row should have been changed' +); + +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/amenity_carousel_i18n.sql b/test/amenity_carousel_i18n.sql new file mode 100644 index 0000000..11b0b15 --- /dev/null +++ b/test/amenity_carousel_i18n.sql @@ -0,0 +1,49 @@ +-- Test amenity_carousel_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('amenity_carousel_i18n'); +select has_pk('amenity_carousel_i18n'); +select col_is_pk('amenity_carousel_i18n', array['amenity_id', 'media_id', 'lang_tag']); +select col_is_fk('amenity_carousel_i18n', array['amenity_id', 'media_id']); +select fk_ok('amenity_carousel_i18n', array['amenity_id', 'media_id'], 'amenity_carousel', array['amenity_id', 'media_id']); +select table_privs_are('amenity_carousel_i18n', 'guest', array['SELECT']); +select table_privs_are('amenity_carousel_i18n', 'employee', array['SELECT']); +select table_privs_are('amenity_carousel_i18n', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('amenity_carousel_i18n', 'authenticator', array[]::text[]); + +select has_column('amenity_carousel_i18n', 'amenity_id'); +select col_type_is('amenity_carousel_i18n', 'amenity_id', 'integer'); +select col_not_null('amenity_carousel_i18n', 'amenity_id'); +select col_hasnt_default('amenity_carousel_i18n', 'amenity_id'); + +select has_column('amenity_carousel_i18n', 'media_id'); +select col_type_is('amenity_carousel_i18n', 'media_id', 'integer'); +select col_not_null('amenity_carousel_i18n', 'media_id'); +select col_hasnt_default('amenity_carousel_i18n', 'media_id'); + +select has_column('amenity_carousel_i18n', 'lang_tag'); +select col_is_fk('amenity_carousel_i18n', 'lang_tag'); +select fk_ok('amenity_carousel_i18n', 'lang_tag', 'language', 'lang_tag'); +select col_type_is('amenity_carousel_i18n', 'lang_tag', 'text'); +select col_not_null('amenity_carousel_i18n', 'lang_tag'); +select col_hasnt_default('amenity_carousel_i18n', 'lang_tag'); + +select has_column('amenity_carousel_i18n', 'caption'); +select col_type_is('amenity_carousel_i18n', 'caption', 'text'); +select col_is_null('amenity_carousel_i18n', 'caption'); +select col_hasnt_default('amenity_carousel_i18n', 'caption'); + + +select * +from finish(); + +rollback; + diff --git a/test/amenity_feature.sql b/test/amenity_feature.sql new file mode 100644 index 0000000..d99df37 --- /dev/null +++ b/test/amenity_feature.sql @@ -0,0 +1,199 @@ +-- Test amenity_feature +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(46); + +set search_path to camper, public; + +select has_table('amenity_feature'); +select has_pk('amenity_feature'); +select table_privs_are('amenity_feature', 'guest', array['SELECT']); +select table_privs_are('amenity_feature', 'employee', array['SELECT']); +select table_privs_are('amenity_feature', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('amenity_feature', 'authenticator', array[]::text[]); + +select has_column('amenity_feature', 'amenity_feature_id'); +select col_is_pk('amenity_feature', 'amenity_feature_id'); +select col_type_is('amenity_feature', 'amenity_feature_id', 'integer'); +select col_not_null('amenity_feature', 'amenity_feature_id'); +select col_hasnt_default('amenity_feature', 'amenity_feature_id'); + +select has_column('amenity_feature', 'amenity_id'); +select col_is_fk('amenity_feature', 'amenity_id'); +select fk_ok('amenity_feature', 'amenity_id', 'amenity', 'amenity_id'); +select col_type_is('amenity_feature', 'amenity_id', 'integer'); +select col_not_null('amenity_feature', 'amenity_id'); +select col_hasnt_default('amenity_feature', 'amenity_id'); + +select has_column('amenity_feature', 'icon_name'); +select col_is_fk('amenity_feature', 'icon_name'); +select fk_ok('amenity_feature', 'icon_name', 'icon', 'icon_name'); +select col_type_is('amenity_feature', 'icon_name', 'text'); +select col_not_null('amenity_feature', 'icon_name'); +select col_hasnt_default('amenity_feature', 'icon_name'); + +select has_column('amenity_feature', 'name'); +select col_type_is('amenity_feature', 'name', 'text'); +select col_not_null('amenity_feature', 'name'); +select col_hasnt_default('amenity_feature', 'name'); + +select has_column('amenity_feature', 'position'); +select col_type_is('amenity_feature', 'position', 'integer'); +select col_not_null('amenity_feature', 'position'); +select col_has_default('amenity_feature', 'position'); +select col_default_is('amenity_feature', 'position', '2147483647'); + + +set client_min_messages to warning; +truncate amenity_feature cascade; +truncate amenity 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 amenity (amenity_id, company_id, label, name) +values (26, 2, 'W1', 'Amenity W1') + , (28, 4, 'B1', 'Amenity B1') +; + +insert into amenity_feature (amenity_id, icon_name, name) +values (26, 'information', 'Feature 26.1') + , (28, 'wifi', 'Feature 28.1') +; + +prepare amenity_feature_data as +select amenity_id, name +from amenity_feature +; + +set role guest; +select bag_eq( + 'amenity_feature_data', + $$ values (26, 'Feature 26.1') + , (28, 'Feature 28.1') + $$, + 'Everyone should be able to list all amenity features across all companies' +); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2'); + +select lives_ok( + $$ insert into amenity_feature(amenity_id, icon_name, name) values (26, 'castle', 'Feature 26.2') $$, + 'Admin from company 2 should be able to insert a new amenity feature to that company.' +); + +select bag_eq( + 'amenity_feature_data', + $$ values (26, 'Feature 26.1') + , (26, 'Feature 26.2') + , (28, 'Feature 28.1') + $$, + 'The new row should have been added' +); + +select lives_ok( + $$ update amenity_feature set name = 'Feature 26-2' where amenity_id = 26 and name = 'Feature 26.2' $$, + 'Admin from company 2 should be able to update amenity feature of that company.' +); + +select bag_eq( + 'amenity_feature_data', + $$ values (26, 'Feature 26.1') + , (26, 'Feature 26-2') + , (28, 'Feature 28.1') + $$, + 'The row should have been updated.' +); + +select lives_ok( + $$ delete from amenity_feature where amenity_id = 26 and name = 'Feature 26-2' $$, + 'Admin from company 2 should be able to delete amenity feature from that company.' +); + +select bag_eq( + 'amenity_feature_data', + $$ values (26, 'Feature 26.1') + , (28, 'Feature 28.1') + $$, + 'The row should have been deleted.' +); + +select throws_ok( + $$ insert into amenity_feature (amenity_id, icon_name, name) values (28, 'toilet', 'Feature 28.2') $$, + '42501', 'new row violates row-level security policy for table "amenity_feature"', + 'Admin from company 2 should NOT be able to insert new amenity features to company 4.' +); + +select lives_ok( + $$ update amenity_feature set name = 'Feature 28-1' where amenity_id = 28 $$, + 'Admin from company 2 should not be able to update amenitys of company 4, but no error if amenity_id is not changed.' +); + +select bag_eq( + 'amenity_feature_data', + $$ values (26, 'Feature 26.1') + , (28, 'Feature 28.1') + $$, + 'No row should have been changed.' +); + +select throws_ok( + $$ update amenity_feature set amenity_id = 28 where amenity_id = 26 $$, + '42501', 'new row violates row-level security policy for table "amenity_feature"', + 'Admin from company 2 should NOT be able to move amenity feature to one of company 4' +); + +select lives_ok( + $$ delete from amenity_feature where amenity_id = 28 $$, + 'Admin from company 2 should NOT be able to delete amenity from company 4, but not error is thrown' +); + +select bag_eq( + 'amenity_feature_data', + $$ values (26, 'Feature 26.1') + , (28, 'Feature 28.1') + $$, + 'No row should have been changed' +); + +select throws_ok( + $$ insert into amenity_feature (amenity_id, icon_name, name) values (26, 'baby', ' ') $$, + '23514', 'new row for relation "amenity_feature" violates check constraint "name_not_empty"', + 'Should not be able to insert amenity features with a blank name.' +); + +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/amenity_feature_i18n.sql b/test/amenity_feature_i18n.sql new file mode 100644 index 0000000..b1f560a --- /dev/null +++ b/test/amenity_feature_i18n.sql @@ -0,0 +1,44 @@ +-- Test amenity_feature_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('amenity_feature_i18n'); +select has_pk('amenity_feature_i18n'); +select col_is_pk('amenity_feature_i18n', array['amenity_feature_id', 'lang_tag']); +select table_privs_are('amenity_feature_i18n', 'guest', array['SELECT']); +select table_privs_are('amenity_feature_i18n', 'employee', array['SELECT']); +select table_privs_are('amenity_feature_i18n', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('amenity_feature_i18n', 'authenticator', array[]::text[]); + +select has_column('amenity_feature_i18n', 'amenity_feature_id'); +select col_is_fk('amenity_feature_i18n', 'amenity_feature_id'); +select fk_ok('amenity_feature_i18n', 'amenity_feature_id', 'amenity_feature', 'amenity_feature_id'); +select col_type_is('amenity_feature_i18n', 'amenity_feature_id', 'integer'); +select col_not_null('amenity_feature_i18n', 'amenity_feature_id'); +select col_hasnt_default('amenity_feature_i18n', 'amenity_feature_id'); + +select has_column('amenity_feature_i18n', 'lang_tag'); +select col_is_fk('amenity_feature_i18n', 'lang_tag'); +select fk_ok('amenity_feature_i18n', 'lang_tag', 'language', 'lang_tag'); +select col_type_is('amenity_feature_i18n', 'lang_tag', 'text'); +select col_not_null('amenity_feature_i18n', 'lang_tag'); +select col_hasnt_default('amenity_feature_i18n', 'lang_tag'); + +select has_column('amenity_feature_i18n', 'name'); +select col_type_is('amenity_feature_i18n', 'name', 'text'); +select col_is_null('amenity_feature_i18n', 'name'); +select col_hasnt_default('amenity_feature_i18n', 'name'); + + +select * +from finish(); + +rollback; + diff --git a/test/amenity_i18n.sql b/test/amenity_i18n.sql new file mode 100644 index 0000000..0ad945b --- /dev/null +++ b/test/amenity_i18n.sql @@ -0,0 +1,54 @@ +-- Test amenity_i18n +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(31); + +set search_path to camper, public; + +select has_table('amenity_i18n'); +select has_pk('amenity_i18n'); +select col_is_pk('amenity_i18n', array['amenity_id', 'lang_tag']); +select table_privs_are('amenity_i18n', 'guest', array['SELECT']); +select table_privs_are('amenity_i18n', 'employee', array['SELECT']); +select table_privs_are('amenity_i18n', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('amenity_i18n', 'authenticator', array[]::text[]); + +select has_column('amenity_i18n', 'amenity_id'); +select col_is_fk('amenity_i18n', 'amenity_id'); +select fk_ok('amenity_i18n', 'amenity_id', 'amenity', 'amenity_id'); +select col_type_is('amenity_i18n', 'amenity_id', 'integer'); +select col_not_null('amenity_i18n', 'amenity_id'); +select col_hasnt_default('amenity_i18n', 'amenity_id'); + +select has_column('amenity_i18n', 'lang_tag'); +select col_is_fk('amenity_i18n', 'lang_tag'); +select fk_ok('amenity_i18n', 'lang_tag', 'language', 'lang_tag'); +select col_type_is('amenity_i18n', 'lang_tag', 'text'); +select col_not_null('amenity_i18n', 'lang_tag'); +select col_hasnt_default('amenity_i18n', 'lang_tag'); + +select has_column('amenity_i18n', 'name'); +select col_type_is('amenity_i18n', 'name', 'text'); +select col_is_null('amenity_i18n', 'name'); +select col_hasnt_default('amenity_i18n', 'name'); + +select has_column('amenity_i18n', 'info1'); +select col_type_is('amenity_i18n', 'info1', 'xml'); +select col_is_null('amenity_i18n', 'info1'); +select col_hasnt_default('amenity_i18n', 'info1'); + +select has_column('amenity_i18n', 'info2'); +select col_type_is('amenity_i18n', 'info2', 'xml'); +select col_is_null('amenity_i18n', 'info2'); +select col_hasnt_default('amenity_i18n', 'info2'); + + +select * +from finish(); + +rollback; + diff --git a/test/edit_amenity.sql b/test/edit_amenity.sql new file mode 100644 index 0000000..3bef67f --- /dev/null +++ b/test/edit_amenity.sql @@ -0,0 +1,59 @@ +-- Test edit_amenity +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', 'edit_amenity', array['integer', 'text', 'text', 'text', 'text', 'boolean']); +select function_lang_is('camper', 'edit_amenity', array['integer', 'text', 'text', 'text', 'text', 'boolean'], 'sql'); +select function_returns('camper', 'edit_amenity', array['integer', 'text', 'text', 'text', 'text', 'boolean'], 'integer'); +select isnt_definer('camper', 'edit_amenity', array['integer', 'text', 'text', 'text', 'text', 'boolean']); +select volatility_is('camper', 'edit_amenity', array['integer', 'text', 'text', 'text', 'text', 'boolean'], 'volatile'); +select function_privs_are('camper', 'edit_amenity', array ['integer', 'text', 'text', 'text', 'text', 'boolean'], 'guest', array[]::text[]); +select function_privs_are('camper', 'edit_amenity', array ['integer', 'text', 'text', 'text', 'text', 'boolean'], 'employee', array[]::text[]); +select function_privs_are('camper', 'edit_amenity', array ['integer', 'text', 'text', 'text', 'text', 'boolean'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'edit_amenity', array ['integer', 'text', 'text', 'text', 'text', 'boolean'], 'authenticator', array[]::text[]); + +set client_min_messages to warning; +truncate amenity 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') +; + +insert into amenity (amenity_id, company_id, label, name, info1, info2, active) +values (21, 1, 'A1', 'Anemity A1', '

A1.1

', '

A1.2

', true) + , (22, 1, 'B1', 'Anemity B1', '

B1.1

', '

B1.2

', false) +; + +select lives_ok( + $$ select edit_amenity(21, 'C1', 'Anemity C1', '

C1.1

', '

C1.2

', false) $$, + 'Should be able to edit the first amenity.' +); + +select lives_ok( + $$ select edit_amenity(22, 'B2', 'Anemity B2', '

B2.1

', '

B2.2

', true) $$, + 'Should be able to edit the second amenity.' +); + +select bag_eq( + $$ select amenity_id, label, name, info1::text, info2::text, active from amenity $$, + $$ values (21, 'C1', 'Anemity C1', '

C1.1

', '

C1.2

', false) + , (22, 'B2', 'Anemity B2', '

B2.1

', '

B2.2

', true) + $$, + 'Should have updated all amenities.' +); + + +select * +from finish(); + +rollback; diff --git a/test/edit_amenity_feature.sql b/test/edit_amenity_feature.sql new file mode 100644 index 0000000..441ad18 --- /dev/null +++ b/test/edit_amenity_feature.sql @@ -0,0 +1,65 @@ +-- Test edit_amenity_feature +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', 'edit_amenity_feature', array['integer', 'text', 'text']); +select function_lang_is('camper', 'edit_amenity_feature', array['integer', 'text', 'text'], 'sql'); +select function_returns('camper', 'edit_amenity_feature', array['integer', 'text', 'text'], 'integer'); +select isnt_definer('camper', 'edit_amenity_feature', array['integer', 'text', 'text']); +select volatility_is('camper', 'edit_amenity_feature', array['integer', 'text', 'text'], 'volatile'); +select function_privs_are('camper', 'edit_amenity_feature', array ['integer', 'text', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'edit_amenity_feature', array ['integer', 'text', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'edit_amenity_feature', array ['integer', 'text', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'edit_amenity_feature', array ['integer', 'text', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate amenity_feature cascade; +truncate amenity 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') +; + +insert into amenity (amenity_id, company_id, label, name) +values (4, 1, 'A1', 'Amenity A1') +; + +insert into amenity_feature (amenity_feature_id, amenity_id, icon_name, name) +values (5, 4, 'information', 'Feature 1') + , (6, 4, 'wifi', 'Feature 2') +; + +select lives_ok( + $$ select edit_amenity_feature(5, 'toilet', 'Feature A') $$, + 'Should be able to edit the first feature' +); + +select lives_ok( + $$ select edit_amenity_feature(6, 'baby', 'Feature B') $$, + 'Should be able to edit the second feature' +); + +select bag_eq( + $$ select amenity_feature_id, amenity_id, icon_name, name from amenity_feature $$, + $$ values (5, 4, 'toilet', 'Feature A') + , (6, 4, 'baby', 'Feature B') + $$, + 'Should have updated all amenity type features.' +); + + +select * +from finish(); + +rollback; diff --git a/test/order_amenity_carousel.sql b/test/order_amenity_carousel.sql new file mode 100644 index 0000000..221d3cf --- /dev/null +++ b/test/order_amenity_carousel.sql @@ -0,0 +1,101 @@ +-- Test order_amenity_carousel +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(11); + +set search_path to camper, public; + +select has_function('camper', 'order_amenity_carousel', array['text', 'integer', 'integer[]']); +select function_lang_is('camper', 'order_amenity_carousel', array['text', 'integer', 'integer[]'], 'sql'); +select function_returns('camper', 'order_amenity_carousel', array['text', 'integer', 'integer[]'], 'void'); +select isnt_definer('camper', 'order_amenity_carousel', array['text', 'integer', 'integer[]']); +select volatility_is('camper', 'order_amenity_carousel', array['text', 'integer', 'integer[]'], 'volatile'); +select function_privs_are('camper', 'order_amenity_carousel', array ['text', 'integer', 'integer[]'], 'guest', array[]::text[]); +select function_privs_are('camper', 'order_amenity_carousel', array ['text', 'integer', 'integer[]'], 'employee', array[]::text[]); +select function_privs_are('camper', 'order_amenity_carousel', array ['text', 'integer', 'integer[]'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'order_amenity_carousel', array ['text', 'integer', 'integer[]'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate amenity_carousel cascade; +truncate amenity 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 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca') +; + +insert into media_content (media_type, bytes) +values ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};') + , ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ff00ff","a"};') + , ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffff00","a"};') + , ('text/plain', 'hello, world!') + , ('image/svg+xml', '') +; + +insert into media (media_id, company_id, original_filename, content_hash) +values ( 3, 1, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) + , ( 4, 1, 'cover3.xpm', sha256('static char *s[]={"1 1 1 1","a c #ff00ff","a"};')) + , ( 5, 1, 'cover4.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffff00","a"};')) + , ( 6, 1, 'text.txt', sha256('hello, world!')) + , ( 7, 1, 'image.svg', sha256('')) + , ( 8, 1, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) + , ( 9, 1, 'cover3.xpm', sha256('static char *s[]={"1 1 1 1","a c #ff00ff","a"};')) + , (10, 1, 'cover4.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffff00","a"};')) + , (11, 1, 'text.txt', sha256('hello, world!')) + , (12, 1, 'image.svg', sha256('')) +; + +insert into amenity (amenity_id, company_id, label, name) +values (15, 1, 'A1', 'Amenity A1') + , (16, 2, 'A1', 'Amenity A1') +; + +insert into amenity_carousel (amenity_id, media_id, caption) +values (15, 3, '1') + , (15, 4, '2') + , (15, 5, '3') + , (15, 6, '4') + , (15, 7, '5') + , (16, 8, '1') + , (16, 9, '2') + , (16, 10, '3') + , (16, 11, '4') + , (16, 12, '5') +; + +select lives_ok( + $$ select order_amenity_carousel('A1', 1, '{5,7,6,3,4}') $$, + 'Should be able to sort amenity type slides using their media ID' +); + +select bag_eq( + $$ select amenity_id, media_id, position from amenity_carousel $$, + $$ values (15, 5, 1) + , (15, 7, 2) + , (15, 6, 3) + , (15, 3, 4) + , (15, 4, 5) + , (16, 8, 2147483647) + , (16, 9, 2147483647) + , (16, 10, 2147483647) + , (16, 11, 2147483647) + , (16, 12, 2147483647) + $$, + 'Should have sorted all amenity type slides.' +); + + +select * +from finish(); + +rollback; diff --git a/test/order_amenity_features.sql b/test/order_amenity_features.sql new file mode 100644 index 0000000..e43a6ed --- /dev/null +++ b/test/order_amenity_features.sql @@ -0,0 +1,67 @@ +-- Test order_amenity_features +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(11); + +set search_path to camper, public; + +select has_function('camper', 'order_amenity_features', array['integer[]']); +select function_lang_is('camper', 'order_amenity_features', array['integer[]'], 'sql'); +select function_returns('camper', 'order_amenity_features', array['integer[]'], 'void'); +select isnt_definer('camper', 'order_amenity_features', array['integer[]']); +select volatility_is('camper', 'order_amenity_features', array['integer[]'], 'volatile'); +select function_privs_are('camper', 'order_amenity_features', array ['integer[]'], 'guest', array[]::text[]); +select function_privs_are('camper', 'order_amenity_features', array ['integer[]'], 'employee', array[]::text[]); +select function_privs_are('camper', 'order_amenity_features', array ['integer[]'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'order_amenity_features', array ['integer[]'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate amenity_feature cascade; +truncate amenity 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 (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca') +; + +insert into amenity (amenity_id, company_id, label, name) +values (17, 2, 'A1', 'Amenity A1') +; + +insert into amenity_feature (amenity_feature_id, amenity_id, icon_name, name) +values (21, 17, 'information', '1') + , (22, 17, 'ball', '2') + , (23, 17, 'bicycle', '3') + , (24, 17, 'campfire', '4') + , (25, 17, 'castle', '5') +; + + +select lives_ok( + $$ select order_amenity_features('{23,25,24,21,22}') $$, + 'Should be able to sort amenity type features using their ID' +); + +select bag_eq( + $$ select amenity_feature_id, position from amenity_feature $$, + $$ values (23, 1) + , (25, 2) + , (24, 3) + , (21, 4) + , (22, 5) + $$, + 'Should have sorted all amenity type features.' +); + + +select * +from finish(); + +rollback; diff --git a/test/remove_amenity.sql b/test/remove_amenity.sql new file mode 100644 index 0000000..cf71d42 --- /dev/null +++ b/test/remove_amenity.sql @@ -0,0 +1,182 @@ +-- Test remove_amenity +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(17); + +set search_path to camper, public; + +select has_function('camper', 'remove_amenity', array['integer']); +select function_lang_is('camper', 'remove_amenity', array['integer'], 'sql'); +select function_returns('camper', 'remove_amenity', array['integer'], 'void'); +select isnt_definer('camper', 'remove_amenity', array['integer']); +select volatility_is('camper', 'remove_amenity', array['integer'], 'volatile'); +select function_privs_are('camper', 'remove_amenity', array['integer'], 'guest', array[]::text[]); +select function_privs_are('camper', 'remove_amenity', array['integer'], 'employee', array[]::text[]); +select function_privs_are('camper', 'remove_amenity', array['integer'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'remove_amenity', array['integer'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate amenity_feature_i18n cascade; +truncate amenity_feature cascade; +truncate amenity_carousel_i18n cascade; +truncate amenity_carousel cascade; +truncate amenity_i18n cascade; +truncate amenity_feature_i18n 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 1', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca') + , (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca') +; + +insert into media_content (media_type, bytes) +values ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};') + , ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ff00ff","a"};') + , ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffff00","a"};') + , ('text/plain', 'hello, world!') + , ('image/svg+xml', '') +; + +insert into media (media_id, company_id, original_filename, content_hash) +values (3, 1, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) + , (4, 1, 'cover3.xpm', sha256('static char *s[]={"1 1 1 1","a c #ff00ff","a"};')) + , (5, 2, 'cover4.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffff00","a"};')) + , (6, 2, 'text.txt', sha256('hello, world!')) + , (7, 2, 'image.svg', sha256('')) +; + +insert into amenity (amenity_id, company_id, label, name) +values (12, 1, 'A1', 'Amenity A1') + , (13, 2, 'A1', 'Amenity A1') + , (14, 2, 'B1', 'Amenity B1') +; + +insert into amenity_i18n (amenity_id, lang_tag) +values (12, 'ca') + , (12, 'es') + , (13, 'ca') + , (13, 'es') + , (14, 'ca') + , (14, 'es') +; + +insert into amenity_carousel (amenity_id, media_id, caption) +values (12, 3, 'Source caption') + , (12, 4, 'Source caption') + , (13, 5, 'Another caption') + , (13, 6, 'N/A') + , (14, 5, 'Another caption') + , (14, 6, 'N/A') +; + +insert into amenity_carousel_i18n (amenity_id, media_id, lang_tag, caption) +values (12, 3, 'en', 'Target caption') + , (12, 3, 'es', 'Target caption (spanish)') + , (12, 4, 'en', 'Target caption') + , (12, 4, 'es', 'Target caption (spanish)') + , (13, 5, 'en', 'Target caption') + , (13, 5, 'es', 'Target caption (spanish)') + , (13, 6, 'en', 'Target caption') + , (13, 6, 'es', 'Target caption (spanish)') + , (14, 5, 'en', 'Target caption') + , (14, 5, 'es', 'Target caption (spanish)') + , (14, 6, 'en', 'Target caption') + , (14, 6, 'es', 'Target caption (spanish)') +; + +insert into amenity_feature (amenity_feature_id, amenity_id, icon_name, name) +values (15, 12, 'baby', 'Baby') + , (16, 12, 'information', 'Information') + , (17, 13, 'castle', 'Castle') + , (18, 13, 'barbecue', 'Barbecue') + , (19, 14, 'bicycle', 'Bicycle') + , (20, 14, 'puzzle', 'Puzzle') +; + +insert into amenity_feature_i18n (amenity_feature_id, lang_tag, name) +values (15, 'ca', 'Nadó') + , (15, 'es', 'Bebé') + , (16, 'ca', 'Informació') + , (16, 'es', 'Información') + , (17, 'ca', 'Castell') + , (17, 'es', 'Castillo') + , (18, 'ca', 'Barbacoa') + , (18, 'es', 'Barbacoa') + , (19, 'ca', 'Bicicleta') + , (19, 'es', 'Bicicleta') + , (20, 'ca', 'Trencaclosques') + , (20, 'es', 'Rompecabezas') +; + +select lives_ok( + $$ select remove_amenity(12) $$, + 'Should be able to delete an amenity from the first company' +); + +select lives_ok( + $$ select remove_amenity(14) $$, + 'Should be able to delete an amenity from the second company' +); + +select bag_eq( + $$ select amenity_feature_id, lang_tag, name from amenity_feature_i18n $$, + $$ values (17, 'ca', 'Castell') + , (17, 'es', 'Castillo') + , (18, 'ca', 'Barbacoa') + , (18, 'es', 'Barbacoa') + $$, + 'Should have removed features’ translations' +); + +select bag_eq( + $$ select amenity_id, icon_name, name from amenity_feature $$, + $$ values (13, 'castle', 'Castle') + , (13, 'barbecue', 'Barbecue') + $$, + 'Should have removed features' +); + +select bag_eq( + $$ select amenity_id, media_id, lang_tag, caption from amenity_carousel_i18n $$, + $$ values (13, 5, 'en', 'Target caption') + , (13, 5, 'es', 'Target caption (spanish)') + , (13, 6, 'en', 'Target caption') + , (13, 6, 'es', 'Target caption (spanish)') + $$, + 'Should have removed the slides’ translations' +); + +select bag_eq( + $$ select amenity_id, media_id, caption from amenity_carousel $$, + $$ values (13, 5, 'Another caption') + , (13, 6, 'N/A') + $$, + 'Should have removed the slides' +); + +select bag_eq( + $$ select amenity_id, lang_tag from amenity_i18n $$, + $$ values (13, 'ca') + , (13, 'es') + $$, + 'Should have removed the amenities' +); + +select bag_eq( + $$ select amenity_id, label from amenity $$, + $$ values (13, 'A1') $$, + 'Should have removed the amenities' +); + +select * +from finish(); + +rollback; diff --git a/test/remove_amenity_carousel_slide.sql b/test/remove_amenity_carousel_slide.sql new file mode 100644 index 0000000..577afe4 --- /dev/null +++ b/test/remove_amenity_carousel_slide.sql @@ -0,0 +1,108 @@ +-- Test remove_amenity_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', 'remove_amenity_carousel_slide', array['integer', 'text', 'integer']); +select function_lang_is('camper', 'remove_amenity_carousel_slide', array['integer', 'text', 'integer'], 'plpgsql'); +select function_returns('camper', 'remove_amenity_carousel_slide', array['integer', 'text', 'integer'], 'void'); +select isnt_definer('camper', 'remove_amenity_carousel_slide', array['integer', 'text', 'integer']); +select volatility_is('camper', 'remove_amenity_carousel_slide', array['integer', 'text', 'integer'], 'volatile'); +select function_privs_are('camper', 'remove_amenity_carousel_slide', array['integer', 'text', 'integer'], 'guest', array[]::text[]); +select function_privs_are('camper', 'remove_amenity_carousel_slide', array['integer', 'text', 'integer'], 'employee', array[]::text[]); +select function_privs_are('camper', 'remove_amenity_carousel_slide', array['integer', 'text', 'integer'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'remove_amenity_carousel_slide', array['integer', 'text', 'integer'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate amenity_carousel_i18n cascade; +truncate amenity_carousel cascade; +truncate amenity 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 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca') +; + +insert into media_content (media_type, bytes) +values ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};') + , ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ff00ff","a"};') + , ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffff00","a"};') + , ('text/plain', 'hello, world!') + , ('image/svg+xml', '') +; + +insert into media (media_id, company_id, original_filename, content_hash) +values (3, 1, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) + , (4, 1, 'cover3.xpm', sha256('static char *s[]={"1 1 1 1","a c #ff00ff","a"};')) + , (5, 2, 'cover4.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffff00","a"};')) + , (6, 2, 'text.txt', sha256('hello, world!')) + , (7, 2, 'image.svg', sha256('')) +; + +insert into amenity (amenity_id, company_id, label, name) +values (12, 1, 'A1', 'Amenity A1') + , (13, 2, 'A1', 'Amenity A1') +; + +insert into amenity_carousel (amenity_id, media_id, caption) +values (12, 3, 'Source caption') + , (12, 4, 'Source caption') + , (13, 5, 'Another caption') + , (13, 6, 'N/A') +; + +insert into amenity_carousel_i18n (amenity_id, media_id, lang_tag, caption) +values (12, 3, 'en', 'Target caption') + , (12, 3, 'es', 'Target caption (spanish)') + , (12, 4, 'en', 'Target caption') + , (12, 4, 'es', 'Target caption (spanish)') + , (13, 5, 'en', 'Target caption') + , (13, 5, 'es', 'Target caption (spanish)') + , (13, 6, 'en', 'Target caption') + , (13, 6, 'es', 'Target caption (spanish)') +; + +select lives_ok( + $$ select remove_amenity_carousel_slide(1, 'A1', 3) $$, + 'Should be able to delete a slide from the first amenity type' +); + +select lives_ok( + $$ select remove_amenity_carousel_slide(2, 'A1', 6) $$, + 'Should be able to delete a slide from the second amenity type' +); + +select bag_eq( + $$ select amenity_id, media_id, caption from amenity_carousel $$, + $$ values (12, 4, 'Source caption') + , (13, 5, 'Another caption') + $$, + 'Should have removed the slides' +); + +select bag_eq( + $$ select amenity_id, media_id, lang_tag, caption from amenity_carousel_i18n $$, + $$ values (12, 4, 'en', 'Target caption') + , (12, 4, 'es', 'Target caption (spanish)') + , (13, 5, 'en', 'Target caption') + , (13, 5, 'es', 'Target caption (spanish)') + $$, + 'Should have removed the slides’ translations' +); + + +select * +from finish(); + +rollback; diff --git a/test/remove_amenity_feature.sql b/test/remove_amenity_feature.sql new file mode 100644 index 0000000..06b9ad0 --- /dev/null +++ b/test/remove_amenity_feature.sql @@ -0,0 +1,89 @@ +-- Test remove_amenity_feature +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', 'remove_amenity_feature', array['integer']); +select function_lang_is('camper', 'remove_amenity_feature', array['integer'], 'sql'); +select function_returns('camper', 'remove_amenity_feature', array['integer'], 'void'); +select isnt_definer('camper', 'remove_amenity_feature', array['integer']); +select volatility_is('camper', 'remove_amenity_feature', array['integer'], 'volatile'); +select function_privs_are('camper', 'remove_amenity_feature', array['integer'], 'guest', array[]::text[]); +select function_privs_are('camper', 'remove_amenity_feature', array['integer'], 'employee', array[]::text[]); +select function_privs_are('camper', 'remove_amenity_feature', array['integer'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'remove_amenity_feature', array['integer'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate amenity_feature_i18n cascade; +truncate amenity_feature cascade; +truncate amenity 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') +; + +insert into amenity (amenity_id, company_id, label, name) +values (11, 1, 'A1', 'Amenity A1') + , (12, 1, 'A2', 'Amenity A2') +; + +insert into amenity_feature (amenity_feature_id, amenity_id, icon_name, name) +values (13, 11, 'wifi', 'Feature 1') + , (14, 11, 'baby', 'Feature 2') + , (15, 12, 'castle', 'Feature 3') + , (16, 12, 'ecofriendly', 'Feature 4') +; + +insert into amenity_feature_i18n (amenity_feature_id, lang_tag, name) +values (13, 'ca', 'Característica 1') + , (13, 'es', 'ES 1') + , (14, 'ca', 'Característica 2') + , (14, 'es', 'ES 2') + , (15, 'ca', 'Característica 3') + , (15, 'es', 'ES 3') + , (16, 'ca', 'Característica 4') + , (16, 'es', 'ES 4') +; + +select lives_ok( + $$ select remove_amenity_feature(13) $$, + 'Should be able to delete a feature from the first amenity' +); + +select lives_ok( + $$ select remove_amenity_feature(16) $$, + 'Should be able to delete a feature from the second amenity' +); + +select bag_eq( + $$ select amenity_feature_id, name from amenity_feature $$, + $$ values (14, 'Feature 2') + , (15, 'Feature 3') + $$, + 'Should have removed the features' +); + +select bag_eq( + $$ select amenity_feature_id, lang_tag, name from amenity_feature_i18n $$, + $$ values (14, 'ca', 'Característica 2') + , (14, 'es', 'ES 2') + , (15, 'ca', 'Característica 3') + , (15, 'es', 'ES 3') + $$, + 'Should have removed the features’ translations' +); + + +select * +from finish(); + +rollback; diff --git a/test/translate_amenity.sql b/test/translate_amenity.sql new file mode 100644 index 0000000..ec0b3f9 --- /dev/null +++ b/test/translate_amenity.sql @@ -0,0 +1,77 @@ +-- Test translate_amenity +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(14); + +set search_path to camper, public; + +select has_function('camper', 'translate_amenity', array['integer', 'text', 'text', 'text', 'text']); +select function_lang_is('camper', 'translate_amenity', array['integer', 'text', 'text', 'text', 'text'], 'sql'); +select function_returns('camper', 'translate_amenity', array['integer', 'text', 'text', 'text', 'text'], 'void'); +select isnt_definer('camper', 'translate_amenity', array['integer', 'text', 'text', 'text', 'text']); +select volatility_is('camper', 'translate_amenity', array['integer', 'text', 'text', 'text', 'text'], 'volatile'); +select function_privs_are('camper', 'translate_amenity', array['integer', 'text', 'text', 'text', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'translate_amenity', array['integer', 'text', 'text', 'text', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'translate_amenity', array['integer', 'text', 'text', 'text', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'translate_amenity', array['integer', 'text', 'text', 'text', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate amenity_i18n cascade; +truncate amenity 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') +; + +insert into amenity (amenity_id, company_id, label, name, info1, info2, active) +values (21, 1, 'A1', 'Anemity A1', '

Info 1

', '

Info 2

', true) + , (22, 1, 'B1', 'Anemity B1', '

Info 1

', '

Info 2

', true) +; + +insert into amenity_i18n (amenity_id, lang_tag, name, info1, info2) +values (22, 'ca', 'Insta A1', '

i1

', '

i2

') + , (22, 'en', 'Anemity A1', '

i1

', '

i2

') +; + +select lives_ok( + $$ select translate_amenity(21, 'ca', '', '

Informació 1

', '') $$, + 'Should be able to translate the first type' +); + +select lives_ok( + $$ select translate_amenity(22, 'es', 'Instalación B1', '', '

Información 2

') $$, + 'Should be able to translate the second type' +); + +select lives_ok( + $$ select translate_amenity(22, 'ca', 'Instaŀlació A1', '

Informació 1

', '

Informació 2

') $$, + 'Should be able to overwrite the catalan translation of the second type' +); + +select lives_ok( + $$ select translate_amenity(22, 'en', null, null, null) $$, + 'Should be able to overwrite the english translation of the second type with all empty values' +); + +select bag_eq( + $$ select amenity_id, lang_tag, name, info1::text, info2::text from amenity_i18n $$, + $$ values (21, 'ca', null, '

Informació 1

', null) + , (22, 'ca', 'Instaŀlació A1', '

Informació 1

', '

Informació 2

') + , (22, 'es', 'Instalación B1', null, '

Información 2

') + , (22, 'en', null, null, null) + $$, + 'Should have added and updated all translations.' +); + +select * +from finish(); + +rollback; diff --git a/test/translate_amenity_carousel_slide.sql b/test/translate_amenity_carousel_slide.sql new file mode 100644 index 0000000..a7e417f --- /dev/null +++ b/test/translate_amenity_carousel_slide.sql @@ -0,0 +1,96 @@ +-- Test translate_amenity_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_amenity_carousel_slide', array['integer', 'text', 'integer', 'text', 'text']); +select function_lang_is('camper', 'translate_amenity_carousel_slide', array['integer', 'text', 'integer', 'text', 'text'], 'sql'); +select function_returns('camper', 'translate_amenity_carousel_slide', array['integer', 'text', 'integer', 'text', 'text'], 'void'); +select isnt_definer('camper', 'translate_amenity_carousel_slide', array['integer', 'text', 'integer', 'text', 'text']); +select volatility_is('camper', 'translate_amenity_carousel_slide', array['integer', 'text', 'integer', 'text', 'text'], 'volatile'); +select function_privs_are('camper', 'translate_amenity_carousel_slide', array['integer', 'text', 'integer', 'text', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'translate_amenity_carousel_slide', array['integer', 'text', 'integer', 'text', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'translate_amenity_carousel_slide', array['integer', 'text', 'integer', 'text', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'translate_amenity_carousel_slide', array['integer', 'text', 'integer', 'text', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate amenity_carousel_i18n cascade; +truncate amenity_carousel cascade; +truncate amenity 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 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca') +; + +insert into media_content (media_type, bytes) +values ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};') + , ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ff00ff","a"};') + , ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffff00","a"};') + , ('text/plain', 'hello, world!') + , ('image/svg+xml', '') +; + +insert into media (media_id, company_id, original_filename, content_hash) +values (3, 1, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) + , (4, 1, 'cover3.xpm', sha256('static char *s[]={"1 1 1 1","a c #ff00ff","a"};')) + , (5, 1, 'cover4.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffff00","a"};')) + , (6, 1, 'text.txt', sha256('hello, world!')) + , (7, 2, 'image.svg', sha256('')) +; + +insert into amenity (amenity_id, company_id, label, name) +values (12, 1, 'A1', 'Amenity A1') + , (13, 2, 'A1', 'Amenity A1') +; + +insert into amenity_carousel (amenity_id, media_id, caption) +values (13, 7, 'Source caption') + , (12, 5, 'Another caption') + , (12, 6, 'N/A') +; + +insert into amenity_carousel_i18n (amenity_id, media_id, lang_tag, caption) +values (13, 7, 'en', 'Target caption') +; + +select lives_ok( + $$ select translate_amenity_carousel_slide(1, 'A1', 5, 'ca', 'Traducció') $$, + 'Should be able to translate a carousel slide' +); + +select lives_ok( + $$ select translate_amenity_carousel_slide(1, 'A1', 5, 'es', '') $$, + 'Should be able to “translate” a carousel slide to the empty string' +); + +select lives_ok( + $$ select translate_amenity_carousel_slide(2, 'A1', 7, 'en', 'Not anymore') $$, + 'Should be able to overwrite a slide’s translation' +); + +select bag_eq( + $$ select amenity_id, media_id, lang_tag, caption from amenity_carousel_i18n $$, + $$ values (12, 5, 'ca', 'Traducció') + , (12, 5, 'es', null) + , (13, 7, 'en', 'Not anymore') + $$, + 'Should have all three slides' +); + + +select * +from finish(); + +rollback; diff --git a/test/translate_amenity_feature.sql b/test/translate_amenity_feature.sql new file mode 100644 index 0000000..ba50672 --- /dev/null +++ b/test/translate_amenity_feature.sql @@ -0,0 +1,81 @@ +-- Test translate_amenity_feature +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(14); + +set search_path to camper, public; + +select has_function('camper', 'translate_amenity_feature', array['integer', 'text', 'text']); +select function_lang_is('camper', 'translate_amenity_feature', array['integer', 'text', 'text'], 'sql'); +select function_returns('camper', 'translate_amenity_feature', array['integer', 'text', 'text'], 'void'); +select isnt_definer('camper', 'translate_amenity_feature', array['integer', 'text', 'text']); +select volatility_is('camper', 'translate_amenity_feature', array['integer', 'text', 'text'], 'volatile'); +select function_privs_are('camper', 'translate_amenity_feature', array['integer', 'text', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'translate_amenity_feature', array['integer', 'text', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'translate_amenity_feature', array['integer', 'text', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'translate_amenity_feature', array['integer', 'text', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate amenity_feature_i18n cascade; +truncate amenity_feature cascade; +truncate amenity 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') +; + +insert into amenity (amenity_id, company_id, label, name) +values (4, 1, 'A1', 'Amenity A1') +; + +insert into amenity_feature (amenity_feature_id, amenity_id, icon_name, name) +values (5, 4, 'toilet', 'Feature 1') + , (6, 4, 'toilet', 'Feature 2') +; + +insert into amenity_feature_i18n (amenity_feature_id, lang_tag, name) +values (6, 'ca', 'carácter2') +; + +select lives_ok( + $$ select translate_amenity_feature(5, 'ca', 'Carácter 1') $$, + 'Should be able to translate the first feature' +); + +select lives_ok( + $$ select translate_amenity_feature(6, 'es', 'Característica 2') $$, + 'Should be able to translate the second feature' +); + +select lives_ok( + $$ select translate_amenity_feature(6, 'en', '') $$, + 'Should be able to “translate” the second feature with an empty translation' +); + +select lives_ok( + $$ select translate_amenity_feature(6, 'ca', 'Carácter 2') $$, + 'Should be able to overwrite the catalan translation of the second feature' +); + +select bag_eq( + $$ select amenity_feature_id, lang_tag, name from amenity_feature_i18n $$, + $$ values (5, 'ca', 'Carácter 1') + , (6, 'ca', 'Carácter 2') + , (6, 'es', 'Característica 2') + , (6, 'en', null) + $$, + 'Should have added and updated all translations.' +); + + +select * +from finish(); + +rollback; diff --git a/test/translate_campsite.sql b/test/translate_campsite.sql index 214b3c9..31e9d94 100644 --- a/test/translate_campsite.sql +++ b/test/translate_campsite.sql @@ -23,7 +23,7 @@ select function_privs_are('camper', 'translate_campsite', array['integer', 'text set client_min_messages to warning; truncate campsite_i18n cascade; truncate campsite cascade; -truncate campsite cascade; +truncate campsite_type cascade; truncate media cascade; truncate media_content cascade; truncate company cascade; diff --git a/verify/add_amenity.sql b/verify/add_amenity.sql new file mode 100644 index 0000000..cdaa357 --- /dev/null +++ b/verify/add_amenity.sql @@ -0,0 +1,7 @@ +-- Verify camper:add_amenity on pg + +begin; + +select has_function_privilege('camper.add_amenity(integer, text, text, text, text)', 'execute'); + +rollback; diff --git a/verify/add_amenity_carousel_slide.sql b/verify/add_amenity_carousel_slide.sql new file mode 100644 index 0000000..2148651 --- /dev/null +++ b/verify/add_amenity_carousel_slide.sql @@ -0,0 +1,7 @@ +-- Verify camper:add_amenity_carousel_slide on pg + +begin; + +select has_function_privilege('camper.add_amenity_carousel_slide(integer, text, integer, text)', 'execute'); + +rollback; diff --git a/verify/add_amenity_feature.sql b/verify/add_amenity_feature.sql new file mode 100644 index 0000000..8c9500b --- /dev/null +++ b/verify/add_amenity_feature.sql @@ -0,0 +1,7 @@ +-- Verify camper:add_amenity_feature on pg + +begin; + +select has_function_privilege('camper.add_amenity_feature(integer, text, text, text)', 'execute'); + +rollback; diff --git a/verify/amenity.sql b/verify/amenity.sql new file mode 100644 index 0000000..734d02f --- /dev/null +++ b/verify/amenity.sql @@ -0,0 +1,21 @@ +-- Verify camper:amenity on pg + +begin; + +select amenity_id + , company_id + , label + , name + , info1 + , info2 + , active +from camper.amenity +where false; + +select 1 / count(*) from pg_class where oid = 'camper.amenity'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.amenity'::regclass; +select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.amenity'::regclass; +select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.amenity'::regclass; +select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.amenity'::regclass; + +rollback; diff --git a/verify/amenity_carousel.sql b/verify/amenity_carousel.sql new file mode 100644 index 0000000..c89bbb9 --- /dev/null +++ b/verify/amenity_carousel.sql @@ -0,0 +1,18 @@ +-- Verify camper:amenity_carousel on pg + +begin; + +select amenity_id + , media_id + , caption + , position +from camper.amenity_carousel +where false; + +select 1 / count(*) from pg_class where oid = 'camper.amenity_carousel'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.amenity_carousel'::regclass; +select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.amenity_carousel'::regclass; +select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.amenity_carousel'::regclass; +select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.amenity_carousel'::regclass; + +rollback; diff --git a/verify/amenity_carousel_i18n.sql b/verify/amenity_carousel_i18n.sql new file mode 100644 index 0000000..7586b21 --- /dev/null +++ b/verify/amenity_carousel_i18n.sql @@ -0,0 +1,12 @@ +-- Verify camper:amenity_carousel_i18n on pg + +begin; + +select amenity_id + , media_id + , lang_tag + , caption +from camper.amenity_carousel_i18n +where false; + +rollback; diff --git a/verify/amenity_feature.sql b/verify/amenity_feature.sql new file mode 100644 index 0000000..ca990e4 --- /dev/null +++ b/verify/amenity_feature.sql @@ -0,0 +1,19 @@ +-- Verify camper:amenity_feature on pg + +begin; + +select amenity_feature_id + , amenity_id + , icon_name + , name + , position +from camper.amenity_feature +where false; + +select 1 / count(*) from pg_class where oid = 'camper.amenity_feature'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.amenity_feature'::regclass; +select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.amenity_feature'::regclass; +select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.amenity_feature'::regclass; +select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.amenity_feature'::regclass; + +rollback; diff --git a/verify/amenity_feature_i18n.sql b/verify/amenity_feature_i18n.sql new file mode 100644 index 0000000..7597872 --- /dev/null +++ b/verify/amenity_feature_i18n.sql @@ -0,0 +1,11 @@ +-- Verify camper:amenity_feature_i18n on pg + +begin; + +select amenity_feature_id + , lang_tag + , name +from camper.amenity_feature_i18n +where false; + +rollback; diff --git a/verify/amenity_i18n.sql b/verify/amenity_i18n.sql new file mode 100644 index 0000000..7aec25c --- /dev/null +++ b/verify/amenity_i18n.sql @@ -0,0 +1,13 @@ +-- Verify camper:amenity_i18n on pg + +begin; + +select amenity_id + , lang_tag + , name + , info1 + , info2 +from camper.amenity_i18n +where false; + +rollback; diff --git a/verify/edit_amenity.sql b/verify/edit_amenity.sql new file mode 100644 index 0000000..aef680b --- /dev/null +++ b/verify/edit_amenity.sql @@ -0,0 +1,7 @@ +-- Verify camper:edit_amenity on pg + +begin; + +select has_function_privilege('camper.edit_amenity(integer, text, text, text, text, boolean)', 'execute'); + +rollback; diff --git a/verify/edit_amenity_feature.sql b/verify/edit_amenity_feature.sql new file mode 100644 index 0000000..e8984a8 --- /dev/null +++ b/verify/edit_amenity_feature.sql @@ -0,0 +1,7 @@ +-- Verify camper:edit_amenity_feature on pg + +begin; + +select has_function_privilege('camper.edit_amenity_feature(integer, text, text)', 'execute'); + +rollback; diff --git a/verify/order_amenity_carousel.sql b/verify/order_amenity_carousel.sql new file mode 100644 index 0000000..d50d249 --- /dev/null +++ b/verify/order_amenity_carousel.sql @@ -0,0 +1,7 @@ +-- Verify camper:order_amenity_carousel on pg + +begin; + +select has_function_privilege('camper.order_amenity_carousel(text, integer, integer[])', 'execute'); + +rollback; diff --git a/verify/order_amenity_features.sql b/verify/order_amenity_features.sql new file mode 100644 index 0000000..39c72ac --- /dev/null +++ b/verify/order_amenity_features.sql @@ -0,0 +1,7 @@ +-- Verify camper:order_amenity_features on pg + +begin; + +select has_function_privilege('camper.order_amenity_features(integer[])', 'execute'); + +rollback; diff --git a/verify/remove_amenity.sql b/verify/remove_amenity.sql new file mode 100644 index 0000000..2118d3b --- /dev/null +++ b/verify/remove_amenity.sql @@ -0,0 +1,7 @@ +-- Verify camper:remove_amenity on pg + +begin; + +select has_function_privilege('camper.remove_amenity(integer)', 'execute'); + +rollback; diff --git a/verify/remove_amenity_carousel_slide.sql b/verify/remove_amenity_carousel_slide.sql new file mode 100644 index 0000000..eae2ef4 --- /dev/null +++ b/verify/remove_amenity_carousel_slide.sql @@ -0,0 +1,7 @@ +-- Verify camper:remove_amenity_carousel_slide on pg + +begin; + +select has_function_privilege('camper.remove_amenity_carousel_slide(integer, text, integer)', 'execute'); + +rollback; diff --git a/verify/remove_amenity_feature.sql b/verify/remove_amenity_feature.sql new file mode 100644 index 0000000..2ad14b4 --- /dev/null +++ b/verify/remove_amenity_feature.sql @@ -0,0 +1,7 @@ +-- Verify camper:remove_amenity_feature on pg + +begin; + +select has_function_privilege('camper.remove_amenity_feature(integer)', 'execute'); + +rollback; diff --git a/verify/translate_amenity.sql b/verify/translate_amenity.sql new file mode 100644 index 0000000..f7b9a41 --- /dev/null +++ b/verify/translate_amenity.sql @@ -0,0 +1,7 @@ +-- Verify camper:translate_amenity on pg + +begin; + +select has_function_privilege('camper.translate_amenity(integer, text, text, text, text)', 'execute'); + +rollback; diff --git a/verify/translate_amenity_carousel_slide.sql b/verify/translate_amenity_carousel_slide.sql new file mode 100644 index 0000000..3307fa9 --- /dev/null +++ b/verify/translate_amenity_carousel_slide.sql @@ -0,0 +1,7 @@ +-- Verify camper:translate_amenity_carousel_slide on pg + +begin; + +select has_function_privilege('camper.translate_amenity_carousel_slide(integer, text, integer, text, text)', 'execute'); + +rollback; diff --git a/verify/translate_amenity_feature.sql b/verify/translate_amenity_feature.sql new file mode 100644 index 0000000..22cab9a --- /dev/null +++ b/verify/translate_amenity_feature.sql @@ -0,0 +1,7 @@ +-- Verify camper:translate_amenity_feature on pg + +begin; + +select has_function_privilege('camper.translate_amenity_feature(integer, text, text)', 'execute'); + +rollback; diff --git a/web/static/map.js b/web/static/map.js index 53a00d4..8032a84 100644 --- a/web/static/map.js +++ b/web/static/map.js @@ -14,8 +14,8 @@ maxZoom: 2, zoom: -1, scrollWheelZoom: false, - maxBounds: latLngBounds, - maxBoundsViscosity: 1.0, + //maxBounds: latLngBounds, + //maxBoundsViscosity: 1.0, zoomSnap: 0, crs: L.CRS.Simple, }); @@ -78,54 +78,60 @@ // XXX: this is from the “parceles” layer. const ctm = [1, 0, 0, 1, 83.2784, 66.1766]; - const transform = function (x, y) { + + function transform(x, y) { return [ctm[0] * x + ctm[2] * y + ctm[4], ctm[1] * x + ctm[3] * y + ctm[5]]; } - const language = document.documentElement.getAttribute('lang'); - const prefix = 'cp_'; - for (const campsite of Array.from(container.querySelectorAll(`[id^="${prefix}"]`))) { - const label = campsite.id.substring(prefix.length); - if (!label) { - continue; - } - const path = campsite.firstElementChild; - const commands = parse(path.getAttribute('d')); - const points = []; - const p0 = [0, 0]; - for (const cmd of commands) { - switch (cmd[0]) { - case 'M': - case 'L': - const cmdM = transform(cmd[1], cmd[2]); - p0[0] = cmdM[0]; - p0[1] = latLngBounds.getNorth() - cmdM[1]; - break; - case 'm': - case 'l': - p0[0] += cmd[1]; - p0[1] -= cmd[2]; - break; - case 'C': - const cmdC = transform(cmd[5], cmd[6]); - p0[0] = cmdC[0]; - p0[1] = latLngBounds.getNorth() - cmdC[1]; - break; - case 'Z': - case 'z': - continue; - default: - console.error(cmd); + function setupFeatures(prefix, baseURI) { + for (const campsite of Array.from(container.querySelectorAll(`[id^="${prefix}"]`))) { + const label = campsite.id.substring(prefix.length); + if (!label) { + continue; } - points.push([p0[1], p0[0]]); + const path = campsite.firstElementChild; + const commands = parse(path.getAttribute('d')); + const points = []; + const p0 = [0, 0]; + for (const cmd of commands) { + switch (cmd[0]) { + case 'M': + case 'L': + const cmdM = transform(cmd[1], cmd[2]); + p0[0] = cmdM[0]; + p0[1] = latLngBounds.getNorth() - cmdM[1]; + break; + case 'm': + case 'l': + p0[0] += cmd[1]; + p0[1] -= cmd[2]; + break; + case 'C': + const cmdC = transform(cmd[5], cmd[6]); + p0[0] = cmdC[0]; + p0[1] = latLngBounds.getNorth() - cmdC[1]; + break; + case 'Z': + case 'z': + continue; + default: + console.error(cmd); + } + points.push([p0[1], p0[0]]); + } + console.log(points); + const feature = L.polygon(points, {color: 'transparent'}).addTo(map); + feature.on({ + mouseover: highlightAccommodation, + mouseout: resetHighlight, + click: function () { + window.location = `${baseURI}/${label}`; + }, + }) } - const accommodation = L.polygon(points, {color: 'transparent'}).addTo(map); - accommodation.on({ - mouseover: highlightAccommodation, - mouseout: resetHighlight, - click: function () { - window.location = `/${language}/campsites/${label}`; - }, - }) } + + const language = document.documentElement.getAttribute('lang'); + setupFeatures('cp_', `/${language}/campsites`) + setupFeatures('cr_', `/${language}/amenities`) })(); diff --git a/web/templates/admin/amenity/carousel/form.gohtml b/web/templates/admin/amenity/carousel/form.gohtml new file mode 100644 index 0000000..23b3493 --- /dev/null +++ b/web/templates/admin/amenity/carousel/form.gohtml @@ -0,0 +1,57 @@ + +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.slideForm*/ -}} + {{- if .MediaID -}} + {{( pgettext "Edit Amenity Carousel Slide" "title" )}} + {{- else -}} + {{( pgettext "New Amenity Carousel Slide" "title" )}} + {{- end -}} +{{- end }} + +{{ define "breadcrumb" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity/types.slideForm*/ -}} +
  • {{( pgettext "Amenities" "title" )}}
  • +
  • {{( pgettext "Amenity Carousel" "title" )}}
  • +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.slideForm*/ -}} +
    +

    {{ template "title" . }}

    + {{ CSRFInput }} +
    + {{ with .Media -}} + {{ template "media-picker" . }} + {{- end }} + {{ with .Caption -}} +
    + {{( pgettext "Caption" "input")}} + {{ template "lang-selector" . }} + {{ range $lang, $input := . -}} + + {{- end }} + {{ template "error-message" . }} +
    + {{- end }} +
    +
    + +
    +
    +{{- end }} diff --git a/web/templates/admin/amenity/carousel/index.gohtml b/web/templates/admin/amenity/carousel/index.gohtml new file mode 100644 index 0000000..936c141 --- /dev/null +++ b/web/templates/admin/amenity/carousel/index.gohtml @@ -0,0 +1,60 @@ + +{{ define "title" -}} + {{( pgettext "Amenity Carousel" "title" )}} +{{- end }} + +{{ define "breadcrumb" -}} +
  • {{( pgettext "Amenities" "title" )}}
  • +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.carouselIndex*/ -}} +

    {{ template "title" . }}

    + {{( pgettext "Add slide" "action" )}} + {{ if .Slides -}} +
    + {{ CSRFInput }} + + + + + + + + + + {{ $confirm := (gettext "Are you sure you wish to delete this slide?")}} + {{ range $slide := .Slides -}} + + + + + + {{- end }} + +
    {{( pgettext "Image" "header" )}}{{( pgettext "Caption" "header" )}}{{( pgettext "Actions" "header" )}}
    + + + + {{ .Caption }} + +
    +
    + {{ else -}} +

    {{( gettext "No slides added yet." )}}

    + {{- end }} +{{- end }} diff --git a/web/templates/admin/amenity/feature/form.gohtml b/web/templates/admin/amenity/feature/form.gohtml new file mode 100644 index 0000000..ee29d42 --- /dev/null +++ b/web/templates/admin/amenity/feature/form.gohtml @@ -0,0 +1,78 @@ + +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity/types.featureForm*/ -}} + {{- if .ID -}} + {{( pgettext "Edit Amenity Feature" "title" )}} + {{- else -}} + {{( pgettext "New Amenity Feature" "title" )}} + {{- end -}} +{{- end }} + +{{ define "breadcrumb" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.featureForm*/ -}} +
  • {{( pgettext "Amenities" "title" )}}
  • +
  • {{( pgettext "Amenity Features" "title" )}}
  • +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.featureForm*/ -}} +
    +

    {{ template "title" . }}

    + {{ CSRFInput }} +
    + {{ with $field := .Icon -}} +
    + {{( pgettext "Icon" "input")}} + +
      + {{- range .Options }} +
    • + +
    • + {{- end }} +
    + {{ template "error-message" . }} +
    + {{- end }} + {{ with .Name -}} +
    + {{( pgettext "Name" "input")}} + {{ template "lang-selector" . }} + {{ range $lang, $input := . -}} + + {{- end }} + {{ template "error-message" . }} +
    + {{- end }} +
    +
    + +
    +
    + + +{{- end }} diff --git a/web/templates/admin/amenity/feature/index.gohtml b/web/templates/admin/amenity/feature/index.gohtml new file mode 100644 index 0000000..e353bbc --- /dev/null +++ b/web/templates/admin/amenity/feature/index.gohtml @@ -0,0 +1,58 @@ + +{{ define "title" -}} + {{( pgettext "Amenity Features" "title" )}} +{{- end }} + +{{ define "breadcrumb" -}} +
  • {{( pgettext "Amenities" "title" )}}
  • +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.featureIndex*/ -}} + {{( pgettext "Add Feature" "action" )}} +

    {{ template "title" . }}

    + {{ if .Features -}} +
    + {{ CSRFInput }} + + + + + + + + + + {{ $confirm := (gettext "Are you sure you wish to delete this feature?")}} + {{ range .Features -}} + + + + + + {{- end }} + +
    {{( pgettext "Name" "header" )}}{{( pgettext "Actions" "header" )}}
    + + + {{ .Name }} + +
    +
    + {{ else -}} +

    {{( gettext "No amenity features added yet." )}}

    + {{- end }} +{{- end }} diff --git a/web/templates/admin/amenity/form.gohtml b/web/templates/admin/amenity/form.gohtml new file mode 100644 index 0000000..ce33932 --- /dev/null +++ b/web/templates/admin/amenity/form.gohtml @@ -0,0 +1,98 @@ + +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.amenityForm*/ -}} + {{ if .ID }} + {{( pgettext "Edit Amenity" "title" )}} + {{ else }} + {{( pgettext "New Amenity" "title" )}} + {{ end }} +{{- end }} + +{{ define "breadcrumb" -}} +
  • {{( pgettext "Amenities" "title" )}}
  • +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.amenityForm*/ -}} +
    +

    {{ template "title" .}}

    + {{ CSRFInput }} +
    + {{ if .ID }} + {{ with .Active -}} + + {{ template "error-message" . }} + {{- end }} + {{ else }} + + {{ end }} + {{ with .Label -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Name -}} +
    + {{( pgettext "Name" "input")}}
    + {{ template "lang-selector" . }} + {{ range $lang, $input := . -}} + + {{- end }} + {{ template "error-message" . }} +
    + {{ template "error-message" . }} + {{- end }} + {{ with .Info1 -}} +
    + {{( pgettext "Info (First Column)" "input")}}
    + {{ template "lang-selector" . }} + {{ range $lang, $input := . -}} + + {{- end }} + {{ template "error-message" . }} +
    + {{- end }} + {{ with .Info2 -}} +
    + {{( pgettext "Info (Second Column)" "input")}}
    + {{ template "lang-selector" . }} + {{ range $lang, $input := . -}} + + {{- end }} + {{ template "error-message" . }} +
    + {{- end }} +
    +
    + +
    +
    +{{- end }} diff --git a/web/templates/admin/amenity/index.gohtml b/web/templates/admin/amenity/index.gohtml new file mode 100644 index 0000000..b73c657 --- /dev/null +++ b/web/templates/admin/amenity/index.gohtml @@ -0,0 +1,61 @@ + +{{ define "title" -}} + {{( pgettext "Amenities" "title" )}} +{{- end }} + +{{ define "breadcrumb" -}} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.amenityIndex*/ -}} + {{( pgettext "Add Amenity" "action" )}} +

    {{ template "title" . }}

    + {{ if .Amenities -}} + + + + + + + + + + + + + {{ $confirm := (gettext "Are you sure you wish to delete this amenity?")}} + {{ range .Amenities -}} + + + + + + + + + {{- end }} + +
    {{( pgettext "Label" "header" )}}{{( pgettext "Name" "header" )}}{{( pgettext "Features" "header" )}}{{( pgettext "Carousel" "header" )}}{{( pgettext "Active" "amenity" )}}{{( pgettext "Actions" "header" )}}
    {{ .Label }}{{ .Name }} + {{( pgettext "Edit Features" "action" )}} + + {{( pgettext "Edit Carousel" "action" )}} + {{ if .Active }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }} + +
    + {{ else -}} +

    {{( gettext "No amenities added yet." )}}

    + {{- end }} + + +{{- end }} diff --git a/web/templates/admin/layout.gohtml b/web/templates/admin/layout.gohtml index 8e9cda3..763fd32 100644 --- a/web/templates/admin/layout.gohtml +++ b/web/templates/admin/layout.gohtml @@ -45,6 +45,9 @@
  • {{( pgettext "Campsites" "title" )}}
  • +
  • + {{( pgettext "Amenities" "title" )}} +
  • {{( pgettext "Seasons" "title" )}}
  • diff --git a/web/templates/campground_map.svg b/web/templates/campground_map.svg index d5dd9f6..0015b46 100644 --- a/web/templates/campground_map.svg +++ b/web/templates/campground_map.svg @@ -7,10 +7,8 @@ - - - - + + diff --git a/web/templates/public/amenity.gohtml b/web/templates/public/amenity.gohtml new file mode 100644 index 0000000..74af06d --- /dev/null +++ b/web/templates/public/amenity.gohtml @@ -0,0 +1,52 @@ + +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.publicPage*/ -}} + {{ .Name }} +{{- end }} + +{{ define "head" -}} + {{ template "carouselStyle" }} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.publicPage*/ -}} +

    {{ template "title" . }}

    + + {{ with .Carousel -}} + + {{- end }} + +
    + {{ range .Info -}} +
    {{ . | raw }}
    + {{- end }} + {{ with .Features -}} +
    +

    {{( pgettext "Features" "title" )}}

    +
      + {{ range . -}} +
    • {{ .Name }}
    • + {{- end }} +
    +
    + {{- end }} +
    + + {{ if .Carousel }} + {{ template "carouselInit" 3 }} + {{- end }} +{{- end }}