Add amenities section and public page

This is more or less the same as the campsites, as public information
goes, but for buildings and other amenities that the camping provides
that are not campsites.
This commit is contained in:
jordi fita mas 2024-01-27 22:51:41 +01:00
parent d738603fc5
commit eeaa3b415e
99 changed files with 5002 additions and 137 deletions

View File

@ -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', '<p>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.</p>', '<p>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.</p>');
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 ');
select add_amenity_feature(52, 'edifici-camping', 'electricity', 'Electricitat');
select add_amenity_feature(52, 'edifici-camping', 'shower', 'Accés als serveis');
commit;

23
deploy/add_amenity.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

58
deploy/amenity.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

24
deploy/amenity_i18n.sql Normal file
View File

@ -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;

27
deploy/edit_amenity.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

50
deploy/remove_amenity.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

289
pkg/amenity/admin.go Normal file
View File

@ -0,0 +1,289 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* 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)
}

339
pkg/amenity/carousel.go Normal file
View File

@ -0,0 +1,339 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package amenity
import (
"context"
"net/http"
"strconv"
"github.com/jackc/pgx/v4"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/carousel"
"dev.tandem.ws/tandem/camper/pkg/database"
"dev.tandem.ws/tandem/camper/pkg/form"
httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/template"
)
func (h *AdminHandler) carouselHandler(user *auth.User, company *auth.Company, conn *database.Conn, label string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
case "":
switch r.Method {
case http.MethodGet:
serveCarouselIndex(w, r, user, company, conn, label)
case http.MethodPost:
addSlide(w, r, user, company, conn, label)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodGet)
}
case "new":
switch r.Method {
case http.MethodGet:
f := newSlideForm(company, label)
f.MustRender(w, r, user, company)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
case "order":
switch r.Method {
case http.MethodPost:
orderCarousel(w, r, user, company, conn, label)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
default:
mediaID, err := strconv.Atoi(head)
if err != nil {
http.NotFound(w, r)
return
}
f := newSlideForm(company, label)
if err := f.FillFromDatabase(r.Context(), conn, company, mediaID); err != nil {
if database.ErrorIsNotFound(err) {
http.NotFound(w, r)
return
}
panic(err)
}
var langTag string
langTag, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch langTag {
case "":
switch r.Method {
case http.MethodGet:
f.MustRender(w, r, user, company)
case http.MethodPut:
editSlide(w, r, user, company, conn, f)
case http.MethodDelete:
deleteSlide(w, r, user, company, conn, label, mediaID)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut, http.MethodDelete)
}
default:
http.NotFound(w, r)
}
}
})
}
func serveCarouselIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) {
slides, err := collectSlideEntries(r.Context(), conn, company, label)
if err != nil {
panic(err)
}
page := &carouselIndex{
Label: label,
Slides: slides,
}
page.MustRender(w, r, user, company)
}
type carouselIndex struct {
Label string
Slides []*carousel.SlideEntry
}
func (page *carouselIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "amenity/carousel/index.gohtml", page)
}
func mustCollectSlides(ctx context.Context, conn *database.Conn, company *auth.Company, loc *locale.Locale, label string) []*carousel.Slide {
rows, err := conn.Query(ctx, `
select coalesce(i18n.caption, slide.caption) as l10_caption
, media.path
from amenity_carousel as slide
join amenity using (amenity_id)
join media on media.media_id = slide.media_id
left join amenity_carousel_i18n as i18n
on i18n.amenity_id = slide.amenity_id
and i18n.media_id = slide.media_id
and lang_tag = $1
where amenity.label = $2
and amenity.company_id = $3
order by slide.position, l10_caption
`, loc.Language, label, company.ID)
if err != nil {
panic(err)
}
defer rows.Close()
var slides []*carousel.Slide
for rows.Next() {
slide := &carousel.Slide{}
err = rows.Scan(&slide.Caption, &slide.Media)
if err != nil {
panic(err)
}
slides = append(slides, slide)
}
if rows.Err() != nil {
panic(rows.Err())
}
return slides
}
func collectSlideEntries(ctx context.Context, conn *database.Conn, company *auth.Company, label string) ([]*carousel.SlideEntry, error) {
rows, err := conn.Query(ctx, `
select carousel.media_id
, media.path
, caption
from amenity_carousel as carousel
join amenity using (amenity_id)
join media on media.media_id = carousel.media_id
where amenity.label = $1
and amenity.company_id = $2
order by carousel.position, caption
`, label, company.ID)
if err != nil {
return nil, err
}
defer rows.Close()
var slides []*carousel.SlideEntry
for rows.Next() {
slide := &carousel.SlideEntry{}
if err = rows.Scan(&slide.ID, &slide.Media, &slide.Caption); err != nil {
return nil, err
}
slides = append(slides, slide)
}
return slides, nil
}
func addSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) {
f := newSlideForm(company, label)
editSlide(w, r, user, company, conn, f)
}
func editSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *slideForm) {
f.process(w, r, user, company, conn, func(ctx context.Context, tx *database.Tx) error {
if err := tx.AddAmenityCarouselSlide(ctx, company.ID, f.Label, f.Media.Int(), f.Caption[f.DefaultLang].Val); err != nil {
return nil
}
for lang := range company.Locales {
l := lang.String()
if l == f.DefaultLang {
continue
}
if err := tx.TranslateAmenityCarouselSlide(ctx, company.ID, f.Label, f.Media.Int(), lang, f.Caption[l].Val); err != nil {
return err
}
}
return nil
})
}
func deleteSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string, mediaID int) {
if err := user.VerifyCSRFToken(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if err := conn.RemoveAmenityCarouselSlide(r.Context(), company.ID, label, mediaID); err != nil {
panic(err)
}
httplib.Redirect(w, r, "/admin/amenities/"+label+"/slides", http.StatusSeeOther)
}
type slideForm struct {
DefaultLang string
Label string
MediaID int
Media *form.Media
Caption form.I18nInput
}
func newSlideForm(company *auth.Company, label string) *slideForm {
return &slideForm{
DefaultLang: company.DefaultLanguage.String(),
Label: label,
Media: &form.Media{
Input: &form.Input{
Name: "media",
},
Label: locale.PgettextNoop("Slide image", "input"),
Prompt: locale.PgettextNoop("Set slide image", "action"),
},
Caption: form.NewI18nInput(company.Locales, "caption"),
}
}
func (f *slideForm) FillFromDatabase(ctx context.Context, conn *database.Conn, company *auth.Company, mediaID int) error {
f.MediaID = mediaID
var caption database.RecordArray
row := conn.QueryRow(ctx, `
select carousel.caption
, carousel.media_id::text
, array_agg((lang_tag, i18n.caption))
from amenity_carousel as carousel
left join amenity_carousel_i18n as i18n using (amenity_id, media_id)
join amenity using (amenity_id)
where amenity.label = $1
and amenity.company_id = $2
and carousel.media_id = $3
group by carousel.caption
, carousel.media_id::text
`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, f.Label, company.ID, mediaID)
if err := row.Scan(&f.Caption[f.DefaultLang].Val, &f.Media.Val, &caption); err != nil {
return err
}
if err := f.Caption.FillArray(caption); err != nil {
return err
}
return nil
}
func (f *slideForm) process(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, act func(ctx context.Context, tx *database.Tx) error) {
if err := f.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := user.VerifyCSRFToken(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil {
panic(err)
} else if !ok {
if !httplib.IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
f.MustRender(w, r, user, company)
return
}
tx := conn.MustBegin(r.Context())
if err := act(r.Context(), tx); err == nil {
if err := tx.Commit(r.Context()); err != nil {
panic(err)
}
} else {
if err := tx.Rollback(r.Context()); err != nil {
panic(err)
}
panic(err)
}
httplib.Redirect(w, r, "/admin/amenities/"+f.Label+"/slides", http.StatusSeeOther)
}
func (f *slideForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
f.Caption.FillValue(r)
f.Media.FillValue(r)
return nil
}
func (f *slideForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
v := form.NewValidator(l)
if v.CheckRequired(f.Media.Input, l.GettextNoop("Slide image can not be empty.")) {
if _, err := v.CheckImageMedia(ctx, conn, f.Media.Input, l.GettextNoop("Slide image must be an image media type.")); err != nil {
return false, err
}
}
return v.AllOK, nil
}
func (f *slideForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "amenity/carousel/form.gohtml", f)
}
func orderCarousel(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := user.VerifyCSRFToken(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
input := r.PostForm["media_id"]
if len(input) > 0 {
var ids []int
for _, s := range input {
if id, err := strconv.Atoi(s); err == nil {
ids = append(ids, id)
} else {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
}
if err := conn.OrderAmenityCarousel(r.Context(), company.ID, label, ids); err != nil {
panic(err)
}
}
serveCarouselIndex(w, r, user, company, conn, label)
}

304
pkg/amenity/feature.go Normal file
View File

@ -0,0 +1,304 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package amenity
import (
"context"
"net/http"
"strconv"
"github.com/jackc/pgx/v4"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/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)
}

127
pkg/amenity/public.go Normal file
View File

@ -0,0 +1,127 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* 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
}

View File

@ -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":

View File

@ -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":

View File

@ -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)
}

163
po/ca.po
View File

@ -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 <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\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 "Lentorn"
@ -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 lallotjament"
#: 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 lallotjament"
#: 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 sha afegit cap diapositiva encara."
@ -725,16 +749,19 @@ msgid "Select campsite type"
msgstr "Escolliu un tipus dallotjament"
#: 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 dentrada"
#: 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 dinterès?"
msgid "No highlights added yet."
msgstr "No sha afegit cap punt dinterè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 sha 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 sha 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 dinici"
#: 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 dAriadna"
#: 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 sha 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 "Lidioma 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 dallotjament 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 letiqueta en blanc."

163
po/es.po
View File

@ -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 <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\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."

173
po/fr.po
View File

@ -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 <info@oriolcarbonell.cat>\n"
"Language-Team: French <traduc@traduc.org>\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 na encore été ajoutée."
@ -726,16 +750,19 @@ msgid "Select campsite type"
msgstr "Sélectionnez le type demplacement"
#: 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 lentreprise"
#: web/templates/admin/taxDetails.gohtml:29
msgctxt "input"
@ -1331,6 +1365,78 @@ msgstr "Êtes-vous sûr de vouloir supprimer cette point dintérêt ?"
msgid "No highlights added yet."
msgstr "Aucun point dintérêt na encore été ajoutée."
#: web/templates/admin/amenity/feature/form.gohtml:8
msgctxt "title"
msgid "Edit Amenity Feature"
msgstr "Modifier lentité 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 na 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 linstallation"
#: 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 na 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 lentreprise"
#: 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 dAriane"
#: 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 limage 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 "Limage 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 "Limage de la diapositive doit être de type média dimage."
@ -1614,7 +1723,7 @@ msgstr "La langue sélectionnée nest 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 "Licône sélectionnée nest 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 demplacement sélectionné nest 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."

7
revert/add_amenity.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert camper:add_amenity from pg
begin;
drop function if exists camper.add_amenity(integer, text, text, text, text);
commit;

View File

@ -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;

View File

@ -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;

7
revert/amenity.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert camper:amenity from pg
begin;
drop table if exists camper.amenity;
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:amenity_carousel from pg
begin;
drop table if exists camper.amenity_carousel;
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:amenity_carousel_i18n from pg
begin;
drop table if exists camper.amenity_carousel_i18n;
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:amenity_feature from pg
begin;
drop table if exists camper.amenity_feature;
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:amenity_feature_i18n from pg
begin;
drop table if exists camper.amenity_feature_i18n;
commit;

7
revert/amenity_i18n.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert camper:amenity_i18n from pg
begin;
drop table if exists camper.amenity_i18n;
commit;

7
revert/edit_amenity.sql Normal file
View File

@ -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;

View File

@ -0,0 +1,7 @@
-- Revert camper:edit_amenity_feature from pg
begin;
drop function if exists camper.edit_amenity_feature(integer, text, text);
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:order_amenity_carousel from pg
begin;
drop function if exists camper.order_amenity_carousel(text, integer, integer[]);
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:order_amenity_features from pg
begin;
drop function if exists camper.order_amenity_features(integer[]);
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:remove_amenity from pg
begin;
drop function if exists camper.remove_amenity(integer);
commit;

View File

@ -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;

View File

@ -0,0 +1,7 @@
-- Revert camper:remove_amenity_feature from pg
begin;
drop function if exists camper.remove_amenity_feature(integer);
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:translate_amenity from pg
begin;
drop function if exists camper.translate_amenity(integer, text, text, text, text);
commit;

View File

@ -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;

View File

@ -0,0 +1,7 @@
-- Revert camper:translate_amenity_feature from pg
begin;
drop function if exists camper.translate_amenity_feature(integer, text, text);
commit;

View File

@ -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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add function to remove campsite type features
amenity [roles schema_camper company user_profile] 2024-01-27T17:54:58Z jordi fita mas <jordi@tandem.blog> # Add relation for amenities
add_amenity [roles schema_camper amenity] 2024-01-27T18:05:08Z jordi fita mas <jordi@tandem.blog> # Add function to create amenities
edit_amenity [roles schema_camper amenity] 2024-01-27T18:13:53Z jordi fita mas <jordi@tandem.blog> # Add function to edit amenities
amenity_i18n [roles schema_camper amenity language] 2024-01-27T19:15:42Z jordi fita mas <jordi@tandem.blog> # Add relation for amenity translation
translate_amenity [roles schema_camper amenity_i18n] 2024-01-27T19:21:58Z jordi fita mas <jordi@tandem.blog> # Add function to translate amenitys
amenity_carousel [roles schema_camper amenity media user_profile] 2024-01-27T01:43:53Z jordi fita mas <jordi@tandem.blog> # Add relation of amenity carousel slides
add_amenity_carousel_slide [roles schema_camper amenity amenity_carousel] 2024-01-27T02:06:17Z jordi fita mas <jordi@tandem.blog> # Add function to add amenity carousel slides
order_amenity_carousel [roles amenity_carousel amenity] 2024-01-27T02:19:24Z jordi fita mas <jordi@tandem.blog> # Add function to order amenity carousel slides
amenity_carousel_i18n [roles schema_camper amenity_carousel language] 2024-01-27T02:32:57Z jordi fita mas <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add function to remove amenity carousel slide
amenity_feature [roles schema_camper amenity icon user_profile] 2024-01-27T18:36:53Z jordi fita mas <jordi@tandem.blog> # Add relation of amenity features
add_amenity_feature [roles schema_camper amenity_feature amenity] 2024-01-27T18:54:02Z jordi fita mas <jordi@tandem.blog> # Add function to add amenity features
edit_amenity_feature [roles schema_camper amenity_feature] 2024-01-27T19:02:58Z jordi fita mas <jordi@tandem.blog> # Add function to edit amenity features
order_amenity_features [roles schema_camper amenity_feature] 2024-01-27T19:09:46Z jordi fita mas <jordi@tandem.blog> # Add function to order amenity features
amenity_feature_i18n [roles schema_camper amenity_feature language] 2024-01-27T19:17:09Z jordi fita mas <jordi@tandem.blog> # Add relation for amenity features translation
translate_amenity_feature [roles schema_camper amenity_feature_i18n] 2024-01-27T19:26:21Z jordi fita mas <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add function to remove amenity

70
test/add_amenity.sql Normal file
View File

@ -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', '<p>A1.1</p>', '<p>A1.2</p>') $$,
'Should be able to add a amenity to the first company'
);
select lives_ok(
$$ select add_amenity(1, 'B1', 'Amenity B1', '<p>B1.1</p>', '<p>B1.2</p>') $$,
'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', '<p>C1.1</p>', '<p>C1.2</p>') $$,
'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', '<p>A1.1</p>', '<p>A1.2</p>', true)
, (1, 'B1', 'Amenity B1', '<p>B1.1</p>', '<p>B1.2</p>', true)
, (2, 'C1', 'Amenity C1', '<p>C1.1</p>', '<p>C1.2</p>', true)
$$,
'Should have added all amenities'
);
select *
from finish();
select *
from finish();
rollback;

View File

@ -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', '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>')
;
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('<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>'))
;
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;

View File

@ -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;

209
test/amenity.sql Normal file
View File

@ -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;

230
test/amenity_carousel.sql Normal file
View File

@ -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;

View File

@ -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;

199
test/amenity_feature.sql Normal file
View File

@ -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;

View File

@ -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;

54
test/amenity_i18n.sql Normal file
View File

@ -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;

59
test/edit_amenity.sql Normal file
View File

@ -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', '<p>A1.1</p>', '<p>A1.2</p>', true)
, (22, 1, 'B1', 'Anemity B1', '<p>B1.1</p>', '<p>B1.2</p>', false)
;
select lives_ok(
$$ select edit_amenity(21, 'C1', 'Anemity C1', '<p>C1.1</p>', '<p>C1.2</p>', false) $$,
'Should be able to edit the first amenity.'
);
select lives_ok(
$$ select edit_amenity(22, 'B2', 'Anemity B2', '<p>B2.1</p>', '<p>B2.2</p>', 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', '<p>C1.1</p>', '<p>C1.2</p>', false)
, (22, 'B2', 'Anemity B2', '<p>B2.1</p>', '<p>B2.2</p>', true)
$$,
'Should have updated all amenities.'
);
select *
from finish();
rollback;

View File

@ -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;

View File

@ -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', '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>')
;
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('<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>'))
, ( 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('<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>'))
;
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;

View File

@ -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;

182
test/remove_amenity.sql Normal file
View File

@ -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', '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>')
;
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('<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>'))
;
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;

View File

@ -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', '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>')
;
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('<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>'))
;
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;

View File

@ -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;

View File

@ -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', '<p>Info 1</p>', '<p>Info 2</p>', true)
, (22, 1, 'B1', 'Anemity B1', '<p>Info 1</p>', '<p>Info 2</p>', true)
;
insert into amenity_i18n (amenity_id, lang_tag, name, info1, info2)
values (22, 'ca', 'Insta A1', '<p>i1</p>', '<p>i2</p>')
, (22, 'en', 'Anemity A1', '<p>i1</p>', '<p>i2</p>')
;
select lives_ok(
$$ select translate_amenity(21, 'ca', '', '<p>Informació 1</p>', '') $$,
'Should be able to translate the first type'
);
select lives_ok(
$$ select translate_amenity(22, 'es', 'Instalación B1', '', '<p>Información 2</p>') $$,
'Should be able to translate the second type'
);
select lives_ok(
$$ select translate_amenity(22, 'ca', 'Instaŀlació A1', '<p>Informació 1</p>', '<p>Informació 2</p>') $$,
'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, '<p>Informació 1</p>', null)
, (22, 'ca', 'Instaŀlació A1', '<p>Informació 1</p>', '<p>Informació 2</p>')
, (22, 'es', 'Instalación B1', null, '<p>Información 2</p>')
, (22, 'en', null, null, null)
$$,
'Should have added and updated all translations.'
);
select *
from finish();
rollback;

View File

@ -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', '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>')
;
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('<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>'))
;
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 slides 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;

View File

@ -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;

View File

@ -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;

7
verify/add_amenity.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

21
verify/amenity.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

13
verify/amenity_i18n.sql Normal file
View File

@ -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;

7
verify/edit_amenity.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,7 @@
-- Verify camper:order_amenity_features on pg
begin;
select has_function_privilege('camper.order_amenity_features(integer[])', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify camper:remove_amenity on pg
begin;
select has_function_privilege('camper.remove_amenity(integer)', 'execute');
rollback;

View File

@ -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;

View File

@ -0,0 +1,7 @@
-- Verify camper:remove_amenity_feature on pg
begin;
select has_function_privilege('camper.remove_amenity_feature(integer)', 'execute');
rollback;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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`)
})();

View File

@ -0,0 +1,57 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ 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*/ -}}
<li><a href="/admin/amenities">{{( pgettext "Amenities" "title" )}}</a></li>
<li><a href="/admin/amenities/{{ .Label }}/slides">{{( pgettext "Amenity Carousel" "title" )}}</a></li>
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.slideForm*/ -}}
<form
{{- if .MediaID }} data-hx-put="/admin/amenities/{{ .Label }}/slides/{{ .MediaID }}"
{{- else }} action="/admin/amenities/{{ .Label }}/slides" method="post"
{{- end -}}
>
<h2>{{ template "title" . }}</h2>
{{ CSRFInput }}
<fieldset {{ template "init-lang" . }}>
{{ with .Media -}}
{{ template "media-picker" . }}
{{- end }}
{{ with .Caption -}}
<fieldset>
<legend>{{( pgettext "Caption" "input")}}</legend>
{{ template "lang-selector" . }}
{{ range $lang, $input := . -}}
<label x-cloak x-show="lang === '{{ $lang }}'"><span>{{ $lang }}</span><br>
<input type="text" name="{{ $input.Name }}" value="{{ $input.Val }}"
{{ template "error-attrs" . }}><br>
</label>
{{- end }}
{{ template "error-message" . }}
</fieldset>
{{- end }}
</fieldset>
<footer>
<button type="submit">
{{ if .MediaID }}
{{( pgettext "Update" "action" )}}
{{ else }}
{{( pgettext "Add" "action" )}}
{{ end }}
</button>
</footer>
</form>
{{- end }}

View File

@ -0,0 +1,60 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Amenity Carousel" "title" )}}
{{- end }}
{{ define "breadcrumb" -}}
<li><a href="/admin/amenities">{{( pgettext "Amenities" "title" )}}</a></li>
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.carouselIndex*/ -}}
<h2>{{ template "title" . }}</h2>
<a href="/admin/amenities/{{ .Label }}/slides/new">{{( pgettext "Add slide" "action" )}}</a>
{{ if .Slides -}}
<form id="slide-index"
class="sortable"
data-hx-post="/admin/amenities/{{ .Label }}/slides/order"
data-hx-trigger="end"
data-hx-select="#slide-index"
data-hx-swap="outerHTML"
>
{{ CSRFInput }}
<table>
<thead>
<tr>
<th scope="col">{{( pgettext "Image" "header" )}}</th>
<th scope="col">{{( pgettext "Caption" "header" )}}</th>
<th scope="col">{{( pgettext "Actions" "header" )}}</th>
</tr>
</thead>
<tbody>
{{ $confirm := (gettext "Are you sure you wish to delete this slide?")}}
{{ range $slide := .Slides -}}
<tr>
<td>
<span class="handle"></span>
<input type="hidden" name="media_id" value="{{ .ID }}">
<a href="/admin/amenities/{{ $.Label }}/slides/{{ .ID }}"><img src="{{ .Media }}"
alt=""></a>
</td>
<td><a href="/admin/amenities/{{ $.Label }}/slides/{{ .ID }}">{{ .Caption }}</a></td>
<td>
<button data-hx-delete="/admin/amenities/{{ $.Label }}/slides/{{ .ID }}"
data-hx-confirm="{{ $confirm }}"
data-hx-headers='{ {{ CSRFHeader }} }'>
{{( pgettext "Delete" "action" )}}
</button>
</td>
</tr>
{{- end }}
</tbody>
</table>
</form>
{{ else -}}
<p>{{( gettext "No slides added yet." )}}</p>
{{- end }}
{{- end }}

View File

@ -0,0 +1,78 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ 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*/ -}}
<li><a href="/admin/amenities">{{( pgettext "Amenities" "title" )}}</a></li>
<li><a href="/admin/amenities/{{ .Label }}/features">{{( pgettext "Amenity Features" "title" )}}</a></li>
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.featureForm*/ -}}
<form
{{- if .ID }} data-hx-put="/admin/amenities/{{ .Label }}/features/{{ .ID }}"
{{- else }} action="/admin/amenities/{{ .Label }}/features" method="post"
{{- end -}}
>
<h2>{{ template "title" . }}</h2>
{{ CSRFInput }}
<fieldset {{ template "init-lang" . }}>
{{ with $field := .Icon -}}
<fieldset class="icon-input">
<legend>{{( pgettext "Icon" "input")}}</legend>
<input type="hidden" name="{{ .Name }}"
{{- range .Options }}
{{ if $field.IsSelected .Value }} value="{{ .Value }}"{{ end }}
{{- end }}
>
<ul>
{{- range .Options }}
<li>
<button type="button" data-icon-name="{{ .Value }}" class="icon_{{ .Value }}"></button>
</li>
{{- end }}
</ul>
{{ template "error-message" . }}
</fieldset>
{{- end }}
{{ with .Name -}}
<fieldset>
<legend>{{( pgettext "Name" "input")}}</legend>
{{ template "lang-selector" . }}
{{ range $lang, $input := . -}}
<label x-cloak x-show="lang === '{{ $lang }}'"><span>{{ $lang }}</span><br>
<input type="text" name="{{ $input.Name }}" value="{{ $input.Val }}"
{{ template "error-attrs" . }}><br>
</label>
{{- end }}
{{ template "error-message" . }}
</fieldset>
{{- end }}
</fieldset>
<footer>
<button type="submit">
{{- if .ID -}}
{{( pgettext "Update" "action" )}}
{{- else -}}
{{( pgettext "Add" "action" )}}
{{- end -}}
</button>
</footer>
</form>
<script type="module">
import {setupIconInput} from "/static/camper.js?v={{ camperVersion }}";
setupIconInput(document.querySelector('.icon-input'));
</script>
{{- end }}

View File

@ -0,0 +1,58 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Amenity Features" "title" )}}
{{- end }}
{{ define "breadcrumb" -}}
<li><a href="/admin/amenities">{{( pgettext "Amenities" "title" )}}</a></li>
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.featureIndex*/ -}}
<a href="/admin/amenities/{{ .Label }}/features/new">{{( pgettext "Add Feature" "action" )}}</a>
<h2>{{ template "title" . }}</h2>
{{ if .Features -}}
<form id="feature-index"
class="sortable"
data-hx-post="/admin/amenities/{{ .Label }}/features/order"
data-hx-trigger="end"
data-hx-select="#feature-index"
data-hx-swap="outerHTML"
>
{{ CSRFInput }}
<table class="services">
<thead>
<tr>
<th scope="col" style="width: 1.5em"></th>
<th scope="col">{{( pgettext "Name" "header" )}}</th>
<th scope="col">{{( pgettext "Actions" "header" )}}</th>
</tr>
</thead>
<tbody>
{{ $confirm := (gettext "Are you sure you wish to delete this feature?")}}
{{ range .Features -}}
<tr>
<td>
<span class="handle"></span>
<input type="hidden" name="feature_id" value="{{ .ID }}">
</td>
<td class="icon_{{ .Icon }}"><a href="{{ .URL }}">{{ .Name }}</a></td>
<td>
<button data-hx-delete="{{ .URL }}"
data-hx-confirm="{{ $confirm }}"
data-hx-headers='{ {{ CSRFHeader }} }'>
{{( pgettext "Delete" "action" )}}
</button>
</td>
</tr>
{{- end }}
</tbody>
</table>
</form>
{{ else -}}
<p>{{( gettext "No amenity features added yet." )}}</p>
{{- end }}
{{- end }}

View File

@ -0,0 +1,98 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ 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" -}}
<li><a href="./">{{( pgettext "Amenities" "title" )}}</a></li>
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.amenityForm*/ -}}
<form
{{- if .ID }} data-hx-put="/admin/amenities/{{ .CurrentLabel }}"
{{- else }} action="/admin/amenities" method="post"
{{- end -}}
>
<h2>{{ template "title" .}}</h2>
{{ CSRFInput }}
<fieldset {{ template "init-lang" . }}>
{{ if .ID }}
{{ with .Active -}}
<label>
<input type="checkbox" name="{{ .Name }}" {{ if .Checked}}checked{{ end }}
{{ template "error-attrs" . }}>
{{( pgettext "Active" "amenity" )}}<br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ else }}
<input type="hidden" name="{{ .Active.Name }}" value="true">
{{ end }}
{{ with .Label -}}
<label>
{{( pgettext "Label" "input")}}<br>
<input type="text" name="{{ .Name }}" value="{{ .Val }}"
required {{ template "error-attrs" . }}><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .Name -}}
<fieldset>
<legend>{{( pgettext "Name" "input")}}<br></legend>
{{ template "lang-selector" . }}
{{ range $lang, $input := . -}}
<label x-cloak x-show="lang === '{{ $lang }}'"><span>{{ $lang }}</span><br>
<input name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}><br>
</label>
{{- end }}
{{ template "error-message" . }}
</fieldset>
{{ template "error-message" . }}
{{- end }}
{{ with .Info1 -}}
<fieldset>
<legend>{{( pgettext "Info (First Column)" "input")}}<br></legend>
{{ template "lang-selector" . }}
{{ range $lang, $input := . -}}
<label x-cloak x-show="lang === '{{ $lang }}'"><span>{{ $lang }}</span><br>
<textarea class="html"
name="{{ .Name }}" {{ template "error-attrs" . }}>{{ .Val }}</textarea><br>
</label>
{{- end }}
{{ template "error-message" . }}
</fieldset>
{{- end }}
{{ with .Info2 -}}
<fieldset>
<legend>{{( pgettext "Info (Second Column)" "input")}}<br></legend>
{{ template "lang-selector" . }}
{{ range $lang, $input := . -}}
<label x-cloak x-show="lang === '{{ $lang }}'"><span>{{ $lang }}</span><br>
<textarea class="html"
name="{{ .Name }}" {{ template "error-attrs" . }}>{{ .Val }}</textarea><br>
</label>
{{- end }}
{{ template "error-message" . }}
</fieldset>
{{- end }}
</fieldset>
<footer>
<button type="submit">
{{- if .ID -}}
{{( pgettext "Update" "action" )}}
{{- else -}}
{{( pgettext "Add" "action" )}}
{{- end -}}
</button>
</footer>
</form>
{{- end }}

View File

@ -0,0 +1,61 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Amenities" "title" )}}
{{- end }}
{{ define "breadcrumb" -}}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/amenity.amenityIndex*/ -}}
<a href="/admin/amenities/new">{{( pgettext "Add Amenity" "action" )}}</a>
<h2>{{ template "title" . }}</h2>
{{ if .Amenities -}}
<table>
<thead>
<tr>
<th scope="col">{{( pgettext "Label" "header" )}}</th>
<th scope="col">{{( pgettext "Name" "header" )}}</th>
<th scope="col">{{( pgettext "Features" "header" )}}</th>
<th scope="col">{{( pgettext "Carousel" "header" )}}</th>
<th scope="col">{{( pgettext "Active" "amenity" )}}</th>
<th scope="col">{{( pgettext "Actions" "header" )}}</th>
</tr>
</thead>
<tbody>
{{ $confirm := (gettext "Are you sure you wish to delete this amenity?")}}
{{ range .Amenities -}}
<tr>
<td><a href="/admin/amenities/{{ .Label }}">{{ .Label }}</a></td>
<td>{{ .Name }}</td>
<td>
<a href="/admin/amenities/{{ .Label }}/features">{{( pgettext "Edit Features" "action" )}}</a>
</td>
<td>
<a href="/admin/amenities/{{ .Label }}/slides">{{( pgettext "Edit Carousel" "action" )}}</a>
</td>
<td>{{ if .Active }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }}</td>
<td>
<button data-hx-delete="/admin/amenities/{{ .Label }}"
data-hx-confirm="{{ $confirm }}"
data-hx-headers='{ {{ CSRFHeader }} }'>
{{( pgettext "Delete" "action" )}}
</button>
</td>
</tr>
{{- end }}
</tbody>
</table>
{{ else -}}
<p>{{( gettext "No amenities added yet." )}}</p>
{{- end }}
<script type="module">
import {setupCampgroundMap} from "/static/camper.js?v=";
setupCampgroundMap(document.getElementById('campground_map'));
</script>
{{- end }}

View File

@ -45,6 +45,9 @@
<li>
<a href="/admin/campsites">{{( pgettext "Campsites" "title" )}}</a>
</li>
<li>
<a href="/admin/amenities">{{( pgettext "Amenities" "title" )}}</a>
</li>
<li>
<a href="/admin/seasons">{{( pgettext "Seasons" "title" )}}</a>
</li>

View File

@ -7,10 +7,8 @@
<g id="eines" transform="matrix(1,0,0,1,83.2784,66.1766)">
<path d="M11.541,925.361L38.913,909.5L51.009,939.291L89.29,969.419L103.048,961.658L131.041,998.72L104.606,1020.12L76.766,1024.71L55.903,1023.95L24.785,1007.89L22.491,984.196L11.025,958.97L27.62,948.085C27.62,948.085 16.461,939.674 11.541,925.361Z" style="fill:rgb(239,237,236);fill-rule:nonzero;stroke:rgb(82,84,83);stroke-width:1px;"/>
</g>
<g id="edifici-camping" transform="matrix(1,0,0,1,83.2784,66.1766)">
<g transform="matrix(0.825088,-0.565004,0.565004,0.825088,-456.158,258.83)">
<rect x="148.497" y="822.888" width="82.927" height="86.544" style="fill:rgb(255,174,138);stroke:rgb(82,84,83);stroke-width:1px;"/>
</g>
<g id="cr_edifici-camping" transform="matrix(1,0,0,1,83.2784,66.1766)">
<path d="M 131.30007,853.88361 l 68.42209,-46.8541 48.89771,71.40643 -68.42209,46.85409 z" style="fill:#ffae8a;stroke:#525453;stroke-width:1px"/>
<g transform="matrix(0.825085,-0.565008,0.565008,0.825085,-482.232,279.177)">
<rect x="192.19" y="907.607" width="35.182" height="21.665" style="fill:rgb(255,174,138);stroke:rgb(82,84,83);stroke-width:1px;"/>
</g>

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 328 KiB

View File

@ -0,0 +1,52 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ 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*/ -}}
<h2>{{ template "title" . }}</h2>
{{ with .Carousel -}}
<div class="campsite carousel">
{{ range . -}}
{{ if .Caption -}}
<figure>
<img src="{{ .Media }}" alt=""/>
<figcaption>{{ .Caption }}</figcaption>
</figure>
{{- else -}}
<img src="{{ .Media }}" alt=""/>
{{- end }}
{{- end }}
</div>
{{- end }}
<div class="campsite_info">
{{ range .Info -}}
<div>{{ . | raw }}</div>
{{- end }}
{{ with .Features -}}
<article class="campsite_features">
<h3 class="sr-only">{{( pgettext "Features" "title" )}}</h3>
<ul>
{{ range . -}}
<li class="icon_{{ .Icon }}">{{ .Name }}</li>
{{- end }}
</ul>
</article>
{{- end }}
</div>
{{ if .Carousel }}
{{ template "carouselInit" 3 }}
{{- end }}
{{- end }}