From 9293a341ef26bdef8279af867ecd99d3f7b74d8f Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Fri, 6 Oct 2023 13:26:01 +0200 Subject: [PATCH] Add campsite type options, mainly for plots --- demo/demo.sql | 75 +++- deploy/add_campsite_type_option.sql | 26 ++ deploy/campsite_type_option.sql | 54 +++ deploy/campsite_type_option_cost.sql | 56 +++ deploy/campsite_type_option_i18n.sql | 22 ++ deploy/edit_campsite_type_option.sql | 25 ++ deploy/set_campsite_type_option_cost.sql | 25 ++ deploy/translate_campsite_type_option.sql | 24 ++ pkg/campsite/types/admin.go | 8 +- pkg/campsite/types/l10n.go | 68 +++- pkg/campsite/types/option.go | 364 ++++++++++++++++++ pkg/campsite/types/public.go | 38 +- po/ca.po | 227 ++++++++--- po/es.po | 227 ++++++++--- revert/add_campsite_type_option.sql | 7 + revert/campsite_type_option.sql | 7 + revert/campsite_type_option_cost.sql | 7 + revert/campsite_type_option_i18n.sql | 7 + revert/edit_campsite_type_option.sql | 7 + revert/set_campsite_type_option_cost.sql | 7 + revert/translate_campsite_type_option.sql | 7 + sqitch.plan | 7 + test/add_campsite_type_option.sql | 76 ++++ test/campsite_type_option.sql | 208 ++++++++++ test/campsite_type_option_cost.sql | 241 ++++++++++++ test/campsite_type_option_i18n.sql | 44 +++ test/edit_campsite_type_option.sql | 81 ++++ test/set_campsite_type_option_cost.sql | 93 +++++ test/translate_campsite_type_option.sql | 85 ++++ verify/add_campsite_type_option.sql | 7 + verify/campsite_type_option.sql | 19 + verify/campsite_type_option_cost.sql | 18 + verify/campsite_type_option_i18n.sql | 11 + verify/edit_campsite_type_option.sql | 7 + verify/set_campsite_type_option_cost.sql | 7 + verify/translate_campsite_type_option.sql | 7 + .../admin/campsite/option/form.gohtml | 85 ++++ .../admin/campsite/option/index.gohtml | 41 ++ .../admin/campsite/option/l10n.gohtml | 35 ++ .../admin/campsite/type/index.gohtml | 4 +- web/templates/admin/home/index.gohtml | 4 +- web/templates/admin/season/index.gohtml | 2 +- web/templates/admin/services/index.gohtml | 8 +- web/templates/public/campsite/type.gohtml | 3 + 44 files changed, 2223 insertions(+), 158 deletions(-) create mode 100644 deploy/add_campsite_type_option.sql create mode 100644 deploy/campsite_type_option.sql create mode 100644 deploy/campsite_type_option_cost.sql create mode 100644 deploy/campsite_type_option_i18n.sql create mode 100644 deploy/edit_campsite_type_option.sql create mode 100644 deploy/set_campsite_type_option_cost.sql create mode 100644 deploy/translate_campsite_type_option.sql create mode 100644 pkg/campsite/types/option.go create mode 100644 revert/add_campsite_type_option.sql create mode 100644 revert/campsite_type_option.sql create mode 100644 revert/campsite_type_option_cost.sql create mode 100644 revert/campsite_type_option_i18n.sql create mode 100644 revert/edit_campsite_type_option.sql create mode 100644 revert/set_campsite_type_option_cost.sql create mode 100644 revert/translate_campsite_type_option.sql create mode 100644 test/add_campsite_type_option.sql create mode 100644 test/campsite_type_option.sql create mode 100644 test/campsite_type_option_cost.sql create mode 100644 test/campsite_type_option_i18n.sql create mode 100644 test/edit_campsite_type_option.sql create mode 100644 test/set_campsite_type_option_cost.sql create mode 100644 test/translate_campsite_type_option.sql create mode 100644 verify/add_campsite_type_option.sql create mode 100644 verify/campsite_type_option.sql create mode 100644 verify/campsite_type_option_cost.sql create mode 100644 verify/campsite_type_option_i18n.sql create mode 100644 verify/edit_campsite_type_option.sql create mode 100644 verify/set_campsite_type_option_cost.sql create mode 100644 verify/translate_campsite_type_option.sql create mode 100644 web/templates/admin/campsite/option/form.gohtml create mode 100644 web/templates/admin/campsite/option/index.gohtml create mode 100644 web/templates/admin/campsite/option/l10n.gohtml diff --git a/demo/demo.sql b/demo/demo.sql index 2c528f1..16bafdf 100644 --- a/demo/demo.sql +++ b/demo/demo.sql @@ -275,9 +275,9 @@ select set_season_range(93, '[2023-09-29, 2023-09-30]'); select set_season_range(94, '[2023-10-01, 2023-10-12]'); insert into campsite_type_cost (campsite_type_id, season_id, cost_per_night, min_nights) -values (72, 92, 20000, 2) - , (72, 93, 16500, 2) - , (72, 94, 12500, 2) +values (72, 92, 20000, 1) + , (72, 93, 16500, 1) + , (72, 94, 12500, 1) , (73, 92, 20000, 2) , (73, 93, 16500, 2) , (73, 94, 12500, 2) @@ -289,5 +289,74 @@ values (72, 92, 20000, 2) , (75, 94, 12500, 2) ; +alter table campsite_type_option alter column campsite_type_option_id restart with 102; +insert into campsite_type_option (campsite_type_id, name, range) +values (72, 'Adults', '[1, 6]') + , (72, 'Nens (de 2 a 10 anys)', '[0, 4]') + , (72, 'Tenda petita (màx. 2 pers.)', '[0, 3]') + , (72, 'Tenda gran', '[0, 3]') + , (72, 'Caravana', '[0, 3]') + , (72, 'Autocaravana', '[0, 3]') + , (72, 'Furgoneta', '[0, 3]') + , (72, 'Cotxe', '[0, 3]') + , (72, 'Moto', '[0, 3]') + , (72, 'Punt electricitat', '[0, 4]') +; + +insert into campsite_type_option_i18n (campsite_type_option_id, lang_tag, name) +values (102, 'en', 'Adults') + , (102, 'es', 'Adultos') + , (103, 'en', 'Children (from 2 to 10 years)') + , (103, 'es', 'Niños (de 2 a 10 años)') + , (104, 'en', 'Small tent (2 pax max.)') + , (104, 'es', 'Tienda pequeña (máx. 2 pers.)') + , (105, 'en', 'Big tent') + , (105, 'es', 'Tienda grande') + , (106, 'en', 'Caravan') + , (106, 'es', 'Caravana') + , (107, 'en', 'Motorhome') + , (107, 'es', 'Autocaravana') + , (108, 'en', 'Van') + , (108, 'es', 'Furgoneta') + , (109, 'en', 'Car') + , (109, 'es', 'Coche') + , (110, 'en', 'Motorcycle') + , (110, 'es', 'Moto') + , (111, 'en', 'Electricity') + , (111, 'es', 'Puntos de electricidad') +; + +insert into campsite_type_option_cost (campsite_type_option_id, season_id, cost_per_night) +values (102, 92, 795) + , (102, 93, 740) + , (102, 94, 660) + , (103, 92, 640) + , (103, 93, 590) + , (103, 94, 540) + , (104, 92, 620) + , (104, 93, 550) + , (104, 94, 500) + , (105, 92, 800) + , (105, 93, 720) + , (105, 94, 620) + , (106, 92, 900) + , (106, 93, 750) + , (106, 94, 650) + , (107, 92, 1220) + , (107, 93, 1100) + , (107, 94, 950) + , (108, 92, 950) + , (108, 93, 850) + , (108, 94, 750) + , (109, 92, 700) + , (109, 93, 630) + , (109, 94, 530) + , (110, 92, 400) + , (110, 93, 360) + , (110, 94, 360) + , (111, 92, 690) + , (111, 93, 610) + , (111, 94, 590) +; commit; diff --git a/deploy/add_campsite_type_option.sql b/deploy/add_campsite_type_option.sql new file mode 100644 index 0000000..29924d3 --- /dev/null +++ b/deploy/add_campsite_type_option.sql @@ -0,0 +1,26 @@ +-- Deploy camper:add_campsite_type_option to pg +-- requires: roles +-- requires: schema_camper +-- requires: campsite_type_option +-- requires: campsite_type + +begin; + +set search_path to camper, public; + +create or replace function add_campsite_type_option(type_slug uuid, name text, min integer, max integer) returns integer as +$$ + insert into campsite_type_option (campsite_type_id, name, range) + select campsite_type_id, add_campsite_type_option.name, int4range(min, max, '[]') + from campsite_type + where slug = type_slug + returning campsite_type_option_id + ; +$$ + language sql +; + +revoke execute on function add_campsite_type_option(uuid, text, integer, integer) from public; +grant execute on function add_campsite_type_option(uuid, text, integer, integer) to admin; + +commit; diff --git a/deploy/campsite_type_option.sql b/deploy/campsite_type_option.sql new file mode 100644 index 0000000..e3811eb --- /dev/null +++ b/deploy/campsite_type_option.sql @@ -0,0 +1,54 @@ +-- Deploy camper:campsite_type_option to pg +-- requires: roles +-- requires: schema_camper +-- requires: campsite_type +-- requires: user_profile + +begin; + +set search_path to camper, public; + +create table campsite_type_option ( + campsite_type_option_id integer generated by default as identity primary key, + campsite_type_id integer not null references campsite_type, + name text not null constraint name_not_empty check(length(trim(name)) > 0), + range int4range not null constraint range_not_negative check(lower(range) >= 0) +); + +alter table campsite_type_option enable row level security; + +grant select on table campsite_type_option to guest; +grant select on table campsite_type_option to employee; +grant select, insert, update, delete on table campsite_type_option to admin; + +create policy guest_ok +on campsite_type_option +for select +using (true) +; + +create policy insert_to_company +on campsite_type_option +for insert +with check ( + exists (select 1 from campsite_type join user_profile using (company_id) where campsite_type.campsite_type_id = campsite_type_option.campsite_type_id) +) +; + +create policy update_company +on campsite_type_option +for update +using ( + exists (select 1 from campsite_type join user_profile using (company_id) where campsite_type.campsite_type_id = campsite_type_option.campsite_type_id) +) +; + +create policy delete_from_company +on campsite_type_option +for delete +using ( + exists (select 1 from campsite_type join user_profile using (company_id) where campsite_type.campsite_type_id = campsite_type_option.campsite_type_id) +) +; + +commit; diff --git a/deploy/campsite_type_option_cost.sql b/deploy/campsite_type_option_cost.sql new file mode 100644 index 0000000..301b68d --- /dev/null +++ b/deploy/campsite_type_option_cost.sql @@ -0,0 +1,56 @@ +-- Deploy camper:campsite_type_option_cost to pg +-- requires: roles +-- requires: schema_camper +-- requires: campsite_type +-- requires: season +-- requires: campsite_type_option +-- requires: user_profile + +begin; + +set search_path to camper, public; + +create table campsite_type_option_cost ( + campsite_type_option_id integer not null references campsite_type_option, + season_id integer not null references season, + cost_per_night integer not null constraint cost_not_negative check(cost_per_night >= 0), + primary key (campsite_type_option_id, season_id) +); + +grant select on table campsite_type_option_cost to guest; +grant select on table campsite_type_option_cost to employee; +grant select, insert, update, delete on table campsite_type_option_cost to admin; + +alter table campsite_type_option_cost enable row level security; + +create policy guest_ok +on campsite_type_option_cost +for select +using (true) +; + +create policy insert_to_company +on campsite_type_option_cost +for insert +with check ( + exists (select 1 from campsite_type_option join campsite_type using (campsite_type_id) join season using (company_id) join user_profile using (company_id) where campsite_type_option.campsite_type_option_id = campsite_type_option_cost.campsite_type_option_id and season.season_id = campsite_type_option_cost.season_id) +) +; + +create policy update_company +on campsite_type_option_cost +for update +using ( + exists (select 1 from campsite_type_option join campsite_type using (campsite_type_id) join season using (company_id) join user_profile using (company_id) where campsite_type_option.campsite_type_option_id = campsite_type_option_cost.campsite_type_option_id and season.season_id = campsite_type_option_cost.season_id) +) +; + +create policy delete_from_company +on campsite_type_option_cost +for delete +using ( + exists (select 1 from campsite_type_option join campsite_type using (campsite_type_id) join season using (company_id) join user_profile using (company_id) where campsite_type_option.campsite_type_option_id = campsite_type_option_cost.campsite_type_option_id and season.season_id = campsite_type_option_cost.season_id) +) +; + +commit; diff --git a/deploy/campsite_type_option_i18n.sql b/deploy/campsite_type_option_i18n.sql new file mode 100644 index 0000000..9448f55 --- /dev/null +++ b/deploy/campsite_type_option_i18n.sql @@ -0,0 +1,22 @@ +-- Deploy camper:campsite_type_option_i18n to pg +-- requires: roles +-- requires: schema_camper +-- requires: campsite_type_option +-- requires: language + +begin; + +set search_path to camper, public; + +create table campsite_type_option_i18n ( + campsite_type_option_id integer not null references campsite_type_option, + lang_tag text not null references language, + name text not null, + primary key (campsite_type_option_id, lang_tag) +); + +grant select on table campsite_type_option_i18n to guest; +grant select on table campsite_type_option_i18n to employee; +grant select, insert, update, delete on table campsite_type_option_i18n to admin; + +commit; diff --git a/deploy/edit_campsite_type_option.sql b/deploy/edit_campsite_type_option.sql new file mode 100644 index 0000000..624e8d0 --- /dev/null +++ b/deploy/edit_campsite_type_option.sql @@ -0,0 +1,25 @@ +-- Deploy camper:edit_campsite_type_option to pg +-- requires: roles +-- requires: schema_camper +-- requires: campsite_type_option + +begin; + +set search_path to camper, public; + +create or replace function edit_campsite_type_option(option_id integer, name text, min integer, max integer) returns integer as +$$ + update campsite_type_option + set name = edit_campsite_type_option.name + , range = int4range(min, max, '[]') + where campsite_type_option_id = option_id + returning campsite_type_option_id + ; +$$ + language sql +; + +revoke execute on function edit_campsite_type_option(integer, text, integer, integer) from public; +grant execute on function edit_campsite_type_option(integer, text, integer, integer) to admin; + +commit; diff --git a/deploy/set_campsite_type_option_cost.sql b/deploy/set_campsite_type_option_cost.sql new file mode 100644 index 0000000..c5e0cc4 --- /dev/null +++ b/deploy/set_campsite_type_option_cost.sql @@ -0,0 +1,25 @@ +-- Deploy camper:set_campsite_type_option_cost to pg +-- requires: roles +-- requires: schema_camper +-- requires: campsite_type_option_cost +-- requires: parse_price + +begin; + +set search_path to camper, public; + +create or replace function set_campsite_type_option_cost(option_id integer, season_id integer, cost_per_night text, decimal_places integer default 2) returns void as +$$ + insert into campsite_type_option_cost (campsite_type_option_id, season_id, cost_per_night) + values (option_id, season_id, parse_price(cost_per_night, decimal_places)) + on conflict (campsite_type_option_id, season_id) do update + set cost_per_night = excluded.cost_per_night + ; +$$ + language sql +; + +revoke execute on function set_campsite_type_option_cost(integer, integer, text, integer) from public; +grant execute on function set_campsite_type_option_cost(integer, integer, text, integer) to admin; + +commit; diff --git a/deploy/translate_campsite_type_option.sql b/deploy/translate_campsite_type_option.sql new file mode 100644 index 0000000..6212083 --- /dev/null +++ b/deploy/translate_campsite_type_option.sql @@ -0,0 +1,24 @@ +-- Deploy camper:translate_campsite_type_option to pg +-- requires: roles +-- requires: schema_camper +-- requires: campsite_type_option_i18n + +begin; + +set search_path to camper, public; + +create or replace function translate_campsite_type_option(option_id integer, lang_tag text, name text) returns void as +$$ + insert into campsite_type_option_i18n (campsite_type_option_id, lang_tag, name) + values (option_id, lang_tag, name) + on conflict (campsite_type_option_id, lang_tag) do update + set name = excluded.name + ; +$$ + language sql +; + +revoke execute on function translate_campsite_type_option(integer, text, text) from public; +grant execute on function translate_campsite_type_option(integer, text, text) to admin; + +commit; diff --git a/pkg/campsite/types/admin.go b/pkg/campsite/types/admin.go index 210c68b..a3fb09e 100644 --- a/pkg/campsite/types/admin.go +++ b/pkg/campsite/types/admin.go @@ -91,6 +91,8 @@ func (h *AdminHandler) typeHandler(user *auth.User, company *auth.Company, conn default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) } + case "options": + h.optionsHandler(user, company, conn, f.Slug).ServeHTTP(w, r) default: loc, ok := h.locales.Get(head) if !ok { @@ -197,11 +199,11 @@ func addType(w http.ResponseWriter, r *http.Request, user *auth.User, company *a if err != nil { return err } - return setPrices(ctx, tx, slug, f.Prices) + return setTypePrices(ctx, tx, slug, f.Prices) }) } -func setPrices(ctx context.Context, tx *database.Tx, slug string, prices map[int]*typePriceForm) error { +func setTypePrices(ctx context.Context, tx *database.Tx, slug string, prices map[int]*typePriceForm) error { for seasonID, p := range prices { if _, err := tx.Exec(ctx, "select set_campsite_type_cost($1, $2, $3, $4)", slug, seasonID, p.MinNights, p.PricePerNight); err != nil { return err @@ -215,7 +217,7 @@ func editType(w http.ResponseWriter, r *http.Request, user *auth.User, company * if _, err := conn.Exec(ctx, "select edit_campsite_type($1, $2, $3, $4, $5, $6, $7)", f.Slug, f.Media, f.Name, f.Description, f.MaxCampers, f.DogsAllowed, f.Active); err != nil { return err } - return setPrices(ctx, tx, f.Slug, f.Prices) + return setTypePrices(ctx, tx, f.Slug, f.Prices) }) } diff --git a/pkg/campsite/types/l10n.go b/pkg/campsite/types/l10n.go index 13fd4df..2456a9b 100644 --- a/pkg/campsite/types/l10n.go +++ b/pkg/campsite/types/l10n.go @@ -49,18 +49,9 @@ func (l10n *typeL10nForm) MustRender(w http.ResponseWriter, r *http.Request, use } func editTypeL10n(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, l10n *typeL10nForm) { - if err := l10n.Parse(r); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + if ok, err := form.Handle(l10n, w, r, user); err != nil { return - } - if err := user.VerifyCSRFToken(r); err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return - } - if !l10n.Valid(user.Locale) { - if !httplib.IsHTMxRequest(r) { - w.WriteHeader(http.StatusUnprocessableEntity) - } + } else if !ok { l10n.MustRender(w, r, user, company) return } @@ -82,3 +73,58 @@ func (l10n *typeL10nForm) Valid(l *locale.Locale) bool { v.CheckRequired(&l10n.Name.Input, l.GettextNoop("Name can not be empty.")) return v.AllOK } + +type optionL10nForm struct { + Locale *locale.Locale + TypeSlug string + ID int + Name *form.L10nInput +} + +func newOptionL10nForm(f *optionForm, loc *locale.Locale) *optionL10nForm { + return &optionL10nForm{ + Locale: loc, + TypeSlug: f.TypeSlug, + ID: f.ID, + Name: f.Name.L10nInput(), + } +} + +func (l10n *optionL10nForm) FillFromDatabase(ctx context.Context, conn *database.Conn) error { + row := conn.QueryRow(ctx, ` + select coalesce(i18n.name, '') as l10n_name + from campsite_type_option + left join campsite_type_option_i18n as i18n on campsite_type_option.campsite_type_option_id = i18n.campsite_type_option_id and i18n.lang_tag = $1 + where campsite_type_option.campsite_type_option_id = $2 + `, l10n.Locale.Language, l10n.ID) + return row.Scan(&l10n.Name.Val) +} + +func (l10n *optionL10nForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "campsite/option/l10n.gohtml", l10n) +} + +func editOptionL10n(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, l10n *optionL10nForm) { + if ok, err := form.Handle(l10n, w, r, user); err != nil { + return + } else if !ok { + l10n.MustRender(w, r, user, company) + return + } + conn.MustExec(r.Context(), "select translate_campsite_type_option($1, $2, $3)", l10n.ID, l10n.Locale.Language, l10n.Name) + httplib.Redirect(w, r, "/admin/campsites/types/"+l10n.TypeSlug+"/options", http.StatusSeeOther) +} + +func (l10n *optionL10nForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + l10n.Name.FillValue(r) + return nil +} + +func (l10n *optionL10nForm) Valid(l *locale.Locale) bool { + v := form.NewValidator(l) + v.CheckRequired(&l10n.Name.Input, l.GettextNoop("Name can not be empty.")) + return v.AllOK +} diff --git a/pkg/campsite/types/option.go b/pkg/campsite/types/option.go new file mode 100644 index 0000000..295a664 --- /dev/null +++ b/pkg/campsite/types/option.go @@ -0,0 +1,364 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package types + +import ( + "context" + "fmt" + "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) optionsHandler(user *auth.User, company *auth.Company, conn *database.Conn, typeSlug 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: + serveOptionIndex(w, r, user, company, conn, typeSlug) + case http.MethodPost: + addOption(w, r, user, company, conn, typeSlug) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) + } + case "new": + switch r.Method { + case http.MethodGet: + f, err := newOptionForm(r.Context(), company, conn, typeSlug) + if err != nil { + panic(err) + } + f.MustRender(w, r, user, company) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet) + } + default: + id, err := strconv.Atoi(head) + if err != nil { + http.NotFound(w, r) + return + } + f, err := newOptionForm(r.Context(), company, conn, typeSlug) + if err != nil { + panic(err) + } + if err := f.FillFromDatabase(r.Context(), conn, id); err != nil { + if database.ErrorIsNotFound(err) { + http.NotFound(w, r) + return + } + panic(err) + } + h.optionHandler(user, company, conn, f).ServeHTTP(w, r) + } + }) +} + +func (h *AdminHandler) optionHandler(user *auth.User, company *auth.Company, conn *database.Conn, f *optionForm) 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: + editOption(w, r, user, company, conn, f) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) + } + default: + loc, ok := h.locales.Get(head) + if !ok { + http.NotFound(w, r) + return + } + l10n := newOptionL10nForm(f, loc) + if err := l10n.FillFromDatabase(r.Context(), conn); err != nil { + panic(err) + } + switch r.Method { + case http.MethodGet: + l10n.MustRender(w, r, user, company) + case http.MethodPut: + editOptionL10n(w, r, user, company, conn, l10n) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) + } + } + }) +} + +func serveOptionIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, typeSlug string) { + options, err := collectOptionEntries(r.Context(), conn, typeSlug) + if err != nil { + panic(err) + } + page := &optionIndex{ + TypeSlug: typeSlug, + Options: options, + } + page.MustRender(w, r, user, company) +} + +func collectOptionEntries(ctx context.Context, conn *database.Conn, typeSlug string) ([]*optionEntry, error) { + rows, err := conn.Query(ctx, ` + select '/admin/campsites/types/' || campsite_type.slug || '/options/' || campsite_type_option_id + , option.name + , array_agg((lang_tag, endonym, not exists (select 1 from campsite_type_option_i18n as i18n where i18n.campsite_type_option_id = option.campsite_type_option_id and i18n.lang_tag = language.lang_tag)) order by endonym) + from campsite_type_option as option + join campsite_type using (campsite_type_id) + join company using (company_id) + , language + where lang_tag <> default_lang_tag + and language.selectable + and campsite_type.slug = $1 + group by campsite_type_option_id + , campsite_type.slug + , option.name + order by name + `, pgx.QueryResultFormats{pgx.BinaryFormatCode}, typeSlug) + if err != nil { + return nil, err + } + defer rows.Close() + + var options []*optionEntry + for rows.Next() { + option := &optionEntry{} + var translations database.RecordArray + if err = rows.Scan(&option.URL, &option.Name, &translations); err != nil { + return nil, err + } + for _, el := range translations.Elements { + option.Translations = append(option.Translations, &locale.Translation{ + URL: option.URL + "/" + el.Fields[0].Get().(string), + Endonym: el.Fields[1].Get().(string), + Missing: el.Fields[2].Get().(bool), + }) + } + options = append(options, option) + } + + return options, nil +} + +type optionEntry struct { + URL string + Name string + Translations []*locale.Translation +} + +type optionIndex struct { + TypeSlug string + Options []*optionEntry +} + +func (page *optionIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "campsite/option/index.gohtml", page) +} + +func addOption(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, typeSlug string) { + f, err := newOptionForm(r.Context(), company, conn, typeSlug) + if err != nil { + panic(err) + } + processOptionForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { + id, err := tx.GetInt(ctx, "select add_campsite_type_option($1, $2, $3, $4)", typeSlug, f.Name, f.Min, f.Max) + if err != nil { + return err + } + return setOptionPrices(ctx, tx, id, f.Prices) + }) +} + +func editOption(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *optionForm) { + processOptionForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error { + if _, err := conn.Exec(ctx, "select edit_campsite_type_option($1, $2, $3, $4)", f.ID, f.Name, f.Min, f.Max); err != nil { + return err + } + return setOptionPrices(ctx, tx, f.ID, f.Prices) + }) +} + +func processOptionForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *optionForm, 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()) + if err := act(r.Context(), tx); err == nil { + tx.Commit(r.Context()) + } else { + tx.Rollback(r.Context()) + panic(err) + } + httplib.Redirect(w, r, "/admin/campsites/types/"+f.TypeSlug+"/options", http.StatusSeeOther) +} + +func setOptionPrices(ctx context.Context, tx *database.Tx, id int, prices map[int]*optionPriceForm) error { + for seasonID, p := range prices { + if _, err := tx.Exec(ctx, "select set_campsite_type_option_cost($1, $2, $3)", id, seasonID, p.PricePerNight); err != nil { + return err + } + } + return nil +} + +type optionForm struct { + ID int + TypeSlug string + Name *form.Input + Min *form.Input + Max *form.Input + Prices map[int]*optionPriceForm +} + +type optionPriceForm struct { + SeasonName string + PricePerNight *form.Input +} + +func newOptionForm(ctx context.Context, company *auth.Company, conn *database.Conn, typeSlug string) (*optionForm, error) { + f := &optionForm{ + TypeSlug: typeSlug, + Name: &form.Input{ + Name: "name", + }, + Min: &form.Input{ + Name: "min", + Val: "0", + }, + Max: &form.Input{ + Name: "max", + Val: "1", + }, + } + + rows, err := conn.Query(ctx, "select season_id, name from season where active and company_id = $1", company.ID) + if err != nil { + return nil, err + } + defer rows.Close() + + f.Prices = make(map[int]*optionPriceForm) + for rows.Next() { + var id int + var name string + if err = rows.Scan(&id, &name); err != nil { + return nil, err + } + f.Prices[id] = &optionPriceForm{ + SeasonName: name, + PricePerNight: &form.Input{ + Name: fmt.Sprintf("season.%d.price_per_night", id), + Val: "0", + }, + } + } + + return f, nil +} + +func (f *optionForm) FillFromDatabase(ctx context.Context, conn *database.Conn, id int) error { + f.ID = id + row := conn.QueryRow(ctx, ` + select name + , lower(range)::text + , (upper(range) - 1)::text + from campsite_type_option + where campsite_type_option_id = $1 + `, id) + if err := row.Scan(&f.Name.Val, &f.Min.Val, &f.Max.Val); err != nil { + return err + } + + rows, err := conn.Query(ctx, ` + select season_id + , to_price(cost_per_night) + from campsite_type_option + join campsite_type_option_cost using (campsite_type_option_id) + where campsite_type_option_id = $1 + `, id) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var seasonID int + var pricePerNight string + if err := rows.Scan(&seasonID, &pricePerNight); err != nil { + return err + } + if p, ok := f.Prices[seasonID]; ok { + p.PricePerNight.Val = pricePerNight + } + } + return rows.Err() +} + +func (f *optionForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.Name.FillValue(r) + f.Min.FillValue(r) + f.Max.FillValue(r) + for _, p := range f.Prices { + p.PricePerNight.FillValue(r) + } + return nil +} + +func (f *optionForm) Valid(l *locale.Locale) bool { + v := form.NewValidator(l) + if v.CheckRequired(f.Name, l.GettextNoop("Name can not be empty.")) { + v.CheckMinLength(f.Name, 1, l.GettextNoop("Name must have at least one letter.")) + } + minValidInt := false + if v.CheckRequired(f.Min, l.GettextNoop("Minimum can not be empty.")) { + if v.CheckValidInteger(f.Min, l.GettextNoop("Minimum must be an integer number.")) { + minValidInt = true + v.CheckMinInteger(f.Min, 0, l.GettextNoop("Minimum must be zero or greater.")) + } + } + if v.CheckRequired(f.Max, l.GettextNoop("Maximum can not be empty.")) { + if v.CheckValidInteger(f.Min, l.GettextNoop("Maximum must be an integer number.")) && minValidInt { + min, _ := strconv.Atoi(f.Min.Val) + v.CheckMinInteger(f.Min, min, l.GettextNoop("Maximum must be equal or greater than minimum.")) + } + } + for _, p := range f.Prices { + if v.CheckRequired(p.PricePerNight, l.GettextNoop("Price per night can not be empty.")) { + if v.CheckValidDecimal(p.PricePerNight, l.GettextNoop("Price per night must be a decimal number.")) { + v.CheckMinDecimal(p.PricePerNight, 0.0, l.GettextNoop("Price per night must be zero or greater.")) + } + } + } + return v.AllOK +} + +func (f *optionForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "campsite/option/form.gohtml", f) +} diff --git a/pkg/campsite/types/public.go b/pkg/campsite/types/public.go index d3e92ac..6ca8301 100644 --- a/pkg/campsite/types/public.go +++ b/pkg/campsite/types/public.go @@ -10,6 +10,8 @@ import ( gotemplate "html/template" "net/http" + "github.com/jackc/pgx/v4" + "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/database" httplib "dev.tandem.ws/tandem/camper/pkg/http" @@ -58,6 +60,12 @@ type typePrice struct { SeasonColor string MinNights int PricePerNight string + Options []*optionPrice +} + +type optionPrice struct { + OptionName string + PricePerNight string } func newPublicPage(ctx context.Context, conn *database.Conn, loc *locale.Locale, slug string) (*publicPage, error) { @@ -78,25 +86,45 @@ func newPublicPage(ctx context.Context, conn *database.Conn, loc *locale.Locale, rows, err := conn.Query(ctx, ` select coalesce(i18n.name, season.name) as l10n_name - , to_color(season.color) - , coalesce(min_nights, 1) - , to_price(coalesce(cost_per_night, 0))::text + , to_color(season.color)::text + , coalesce(cost.min_nights, 1) + , to_price(coalesce(cost.cost_per_night, 0)) + , array_agg((coalesce(option_i18n.name, option.name), to_price(coalesce(option_cost.cost_per_night, 0)))) filter (where option.campsite_type_option_id is not null) from season left join season_i18n as i18n on season.season_id = i18n.season_id and i18n.lang_tag = $1 left join ( campsite_type_cost as cost join campsite_type as type on cost.campsite_type_id = type.campsite_type_id and type.slug = $2 ) as cost on cost.season_id = season.season_id + left join ( + select option.* + from campsite_type_option as option + join campsite_type as type on option.campsite_type_id = type.campsite_type_id and type.slug = $2 + ) as option on true + left join campsite_type_option_i18n as option_i18n on option_i18n.campsite_type_option_id = option.campsite_type_option_id and option_i18n.lang_tag = $1 + left join campsite_type_option_cost as option_cost on option_cost.campsite_type_option_id = option.campsite_type_option_id and option_cost.season_id = season.season_id where season.active - `, loc.Language, slug) + group by i18n.name + , season.name + , season.color + , cost.min_nights + , cost.cost_per_night + `, pgx.QueryResultFormats{pgx.BinaryFormatCode}, loc.Language, slug) if err != nil { return nil, err } for rows.Next() { price := &typePrice{} - if err := rows.Scan(&price.SeasonName, &price.SeasonColor, &price.MinNights, &price.PricePerNight); err != nil { + var options database.RecordArray + if err := rows.Scan(&price.SeasonName, &price.SeasonColor, &price.MinNights, &price.PricePerNight, &options); err != nil { return nil, err } + for _, el := range options.Elements { + price.Options = append(price.Options, &optionPrice{ + OptionName: el.Fields[0].Get().(string), + PricePerNight: el.Fields[1].Get().(string), + }) + } page.Prices = append(page.Prices, price) } diff --git a/po/ca.po b/po/ca.po index 91a651e..38ac150 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-10-03 21:05+0200\n" +"POT-Creation-Date: 2023-10-06 13:18+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -20,6 +20,7 @@ msgstr "" #: web/templates/public/services.gohtml:6 #: web/templates/public/services.gohtml:15 +#: web/templates/public/layout.gohtml:42 #: web/templates/admin/services/index.gohtml:53 msgctxt "title" msgid "Services" @@ -50,6 +51,7 @@ msgstr "Els nostres serveis" #: web/templates/public/home.gohtml:34 #: web/templates/public/surroundings.gohtml:6 #: web/templates/public/surroundings.gohtml:10 +#: web/templates/public/layout.gohtml:43 msgctxt "title" msgid "Surroundings" msgstr "L’entorn" @@ -75,6 +77,7 @@ msgid "Come and enjoy!" msgstr "Vine a gaudir!" #: web/templates/public/campsite/type.gohtml:17 +#: web/templates/admin/campsite/option/form.gohtml:58 #: web/templates/admin/campsite/type/form.gohtml:73 msgctxt "title" msgid "Prices" @@ -85,6 +88,10 @@ msgid "%s €/night" msgstr "%s €/nit" #: web/templates/public/campsite/type.gohtml:28 +msgid "%s: %s €/night" +msgstr "%s: %s €/nit" + +#: web/templates/public/campsite/type.gohtml:31 msgid "*Minimum %d nights per stay" msgstr "*Mínim %d nits per estada" @@ -147,7 +154,7 @@ msgid "There are several points where you can go by kayak, from sections of the msgstr "Hi ha diversos punts on poder anar amb caiac, des de trams del riu Ter com també a la costa…." #: web/templates/public/layout.gohtml:11 web/templates/public/layout.gohtml:24 -#: web/templates/public/layout.gohtml:59 +#: web/templates/public/layout.gohtml:61 msgid "Campsite Montagut" msgstr "Càmping Montagut" @@ -179,6 +186,7 @@ msgstr "Llegenda" #: web/templates/admin/carousel/form.gohtml:47 #: web/templates/admin/campsite/form.gohtml:70 +#: web/templates/admin/campsite/option/form.gohtml:78 #: web/templates/admin/campsite/type/form.gohtml:108 #: web/templates/admin/season/form.gohtml:64 #: web/templates/admin/services/form.gohtml:69 @@ -189,6 +197,7 @@ msgstr "Actualitza" #: web/templates/admin/carousel/form.gohtml:49 #: web/templates/admin/campsite/form.gohtml:72 +#: web/templates/admin/campsite/option/form.gohtml:80 #: web/templates/admin/campsite/type/form.gohtml:110 #: web/templates/admin/season/form.gohtml:66 #: web/templates/admin/services/form.gohtml:71 @@ -203,6 +212,7 @@ msgid "Translate Carousel Slide to %s" msgstr "Traducció de la diapositiva del carrusel a %s" #: web/templates/admin/carousel/l10n.gohtml:21 +#: web/templates/admin/campsite/option/l10n.gohtml:21 #: web/templates/admin/campsite/type/l10n.gohtml:21 #: web/templates/admin/campsite/type/l10n.gohtml:33 #: web/templates/admin/season/l10n.gohtml:21 @@ -212,6 +222,7 @@ msgid "Source:" msgstr "Origen:" #: web/templates/admin/carousel/l10n.gohtml:23 +#: web/templates/admin/campsite/option/l10n.gohtml:23 #: web/templates/admin/campsite/type/l10n.gohtml:23 #: web/templates/admin/campsite/type/l10n.gohtml:36 #: web/templates/admin/season/l10n.gohtml:23 @@ -222,6 +233,7 @@ msgid "Translation:" msgstr "Traducció:" #: web/templates/admin/carousel/l10n.gohtml:32 +#: web/templates/admin/campsite/option/l10n.gohtml:32 #: web/templates/admin/campsite/type/l10n.gohtml:45 #: web/templates/admin/season/l10n.gohtml:32 #: web/templates/admin/services/l10n.gohtml:45 @@ -261,6 +273,85 @@ msgctxt "input" msgid "Label" msgstr "Etiqueta" +#: web/templates/admin/campsite/option/form.gohtml:8 +#: web/templates/admin/campsite/option/form.gohtml:25 +msgctxt "title" +msgid "Edit Campsite Type Option" +msgstr "Edició de l’opció del tipus d’allotjament" + +#: web/templates/admin/campsite/option/form.gohtml:10 +#: web/templates/admin/campsite/option/form.gohtml:27 +msgctxt "title" +msgid "New Campsite Type Option" +msgstr "Nova opció del tipus d’allotjament" + +#: web/templates/admin/campsite/option/form.gohtml:34 +#: web/templates/admin/campsite/option/l10n.gohtml:20 +#: web/templates/admin/campsite/type/form.gohtml:46 +#: web/templates/admin/campsite/type/l10n.gohtml:20 +#: web/templates/admin/season/form.gohtml:46 +#: web/templates/admin/season/l10n.gohtml:20 +#: web/templates/admin/services/form.gohtml:52 +#: web/templates/admin/services/l10n.gohtml:20 +#: web/templates/admin/profile.gohtml:26 +msgctxt "input" +msgid "Name" +msgstr "Nom" + +#: web/templates/admin/campsite/option/form.gohtml:42 +msgctxt "input" +msgid "Minimum" +msgstr "Mínim" + +#: web/templates/admin/campsite/option/form.gohtml:50 +msgctxt "input" +msgid "Maximum" +msgstr "Màxim" + +#: web/templates/admin/campsite/option/form.gohtml:64 +#: web/templates/admin/campsite/type/form.gohtml:79 +msgctxt "input" +msgid "Price per night" +msgstr "Preu per nit" + +#: web/templates/admin/campsite/option/index.gohtml:6 +#: web/templates/admin/campsite/option/index.gohtml:12 +msgctxt "title" +msgid "Campsite Type Options" +msgstr "Opcions del tipus d’allotjament" + +#: web/templates/admin/campsite/option/index.gohtml:11 +msgctxt "action" +msgid "Add Option" +msgstr "Afegeix opció" + +#: web/templates/admin/campsite/option/index.gohtml:17 +#: web/templates/admin/campsite/type/index.gohtml:17 +#: web/templates/admin/season/index.gohtml:18 +msgctxt "header" +msgid "Name" +msgstr "Nom" + +#: web/templates/admin/campsite/option/index.gohtml:18 +#: web/templates/admin/campsite/type/index.gohtml:18 +#: web/templates/admin/season/index.gohtml:19 +#: web/templates/admin/services/index.gohtml:19 +#: web/templates/admin/services/index.gohtml:60 +#: web/templates/admin/home/index.gohtml:19 +msgctxt "header" +msgid "Translations" +msgstr "Traduccions" + +#: web/templates/admin/campsite/option/index.gohtml:39 +msgid "No campsite type options added yet." +msgstr "No s’ha afegit cap opció al tipus d’allotjament encara." + +#: web/templates/admin/campsite/option/l10n.gohtml:7 +#: web/templates/admin/campsite/option/l10n.gohtml:14 +msgctxt "title" +msgid "Translate Campsite Type Option to %s" +msgstr "Traducció de la opció del tipus d’allotjament a %s" + #: web/templates/admin/campsite/index.gohtml:6 #: web/templates/admin/campsite/index.gohtml:12 #: web/templates/admin/layout.gohtml:40 web/templates/admin/layout.gohtml:71 @@ -284,13 +375,13 @@ msgid "Type" msgstr "Tipus" #: web/templates/admin/campsite/index.gohtml:28 -#: web/templates/admin/campsite/type/index.gohtml:35 +#: web/templates/admin/campsite/type/index.gohtml:37 #: web/templates/admin/season/index.gohtml:39 msgid "Yes" msgstr "Sí" #: web/templates/admin/campsite/index.gohtml:28 -#: web/templates/admin/campsite/type/index.gohtml:35 +#: web/templates/admin/campsite/type/index.gohtml:37 #: web/templates/admin/season/index.gohtml:39 msgid "No" msgstr "No" @@ -312,22 +403,11 @@ msgid "New Campsite Type" msgstr "Nou tipus d’allotjament" #: web/templates/admin/campsite/type/form.gohtml:37 -#: web/templates/admin/campsite/type/index.gohtml:19 +#: web/templates/admin/campsite/type/index.gohtml:20 msgctxt "campsite type" msgid "Active" msgstr "Actiu" -#: web/templates/admin/campsite/type/form.gohtml:46 -#: web/templates/admin/campsite/type/l10n.gohtml:20 -#: web/templates/admin/season/form.gohtml:46 -#: web/templates/admin/season/l10n.gohtml:20 -#: web/templates/admin/services/form.gohtml:52 -#: web/templates/admin/services/l10n.gohtml:20 -#: web/templates/admin/profile.gohtml:26 -msgctxt "input" -msgid "Name" -msgstr "Nom" - #: web/templates/admin/campsite/type/form.gohtml:57 msgctxt "input" msgid "Maximum number of campers" @@ -338,11 +418,6 @@ msgctxt "input" msgid "Dogs allowed" msgstr "Es permeten gossos" -#: web/templates/admin/campsite/type/form.gohtml:79 -msgctxt "input" -msgid "Price per night" -msgstr "Preu per nit" - #: web/templates/admin/campsite/type/form.gohtml:87 msgctxt "input" msgid "Minimum number of nights" @@ -368,22 +443,17 @@ msgctxt "action" msgid "Add Type" msgstr "Afegeix tipus" -#: web/templates/admin/campsite/type/index.gohtml:17 -#: web/templates/admin/season/index.gohtml:18 +#: web/templates/admin/campsite/type/index.gohtml:19 msgctxt "header" -msgid "Name" -msgstr "Nom" +msgid "Options" +msgstr "Opcions" -#: web/templates/admin/campsite/type/index.gohtml:18 -#: web/templates/admin/season/index.gohtml:19 -#: web/templates/admin/services/index.gohtml:19 -#: web/templates/admin/services/index.gohtml:60 -#: web/templates/admin/home/index.gohtml:19 -msgctxt "campsite type" -msgid "Translations" -msgstr "Traduccions" +#: web/templates/admin/campsite/type/index.gohtml:36 +msgctxt "action" +msgid "Edit Options" +msgstr "Edita les opcions" -#: web/templates/admin/campsite/type/index.gohtml:41 +#: web/templates/admin/campsite/type/index.gohtml:43 msgid "No campsite types added yet." msgstr "No s’ha afegit cap tipus d’allotjament encara." @@ -566,7 +636,7 @@ msgstr "Llegenda" #: web/templates/admin/services/index.gohtml:20 #: web/templates/admin/services/index.gohtml:61 #: web/templates/admin/home/index.gohtml:20 -msgctxt "campsite type" +msgctxt "header" msgid "Actions" msgstr "Accions" @@ -846,8 +916,9 @@ msgctxt "language option" msgid "Automatic" msgstr "Automàtic" -#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:82 -#: pkg/campsite/types/admin.go:387 pkg/season/l10n.go:69 +#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:73 +#: pkg/campsite/types/l10n.go:128 pkg/campsite/types/option.go:336 +#: pkg/campsite/types/admin.go:389 pkg/season/l10n.go:69 #: pkg/season/admin.go:382 pkg/services/l10n.go:73 pkg/services/admin.go:266 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." @@ -868,61 +939,85 @@ msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida." msgid "Access forbidden" msgstr "Accés prohibit" -#: pkg/campsite/types/admin.go:278 +#: pkg/campsite/types/option.go:337 pkg/campsite/types/admin.go:390 +msgid "Name must have at least one letter." +msgstr "El nom ha de tenir com a mínim una lletra." + +#: pkg/campsite/types/option.go:340 +msgid "Minimum can not be empty." +msgstr "No podeu deixar el mínim en blanc." + +#: pkg/campsite/types/option.go:341 +msgid "Minimum must be an integer number." +msgstr "El valor del mínim ha de ser un número enter." + +#: pkg/campsite/types/option.go:343 +msgid "Minimum must be zero or greater." +msgstr "El valor del mínim ha de ser com a mínim zero." + +#: pkg/campsite/types/option.go:346 +msgid "Maximum can not be empty." +msgstr "No podeu deixar el màxim en blanc." + +#: pkg/campsite/types/option.go:347 +msgid "Maximum must be an integer number." +msgstr "El valor del màxim ha de ser un número enter." + +#: pkg/campsite/types/option.go:349 +msgid "Maximum must be equal or greater than minimum." +msgstr "El valor del màxim ha de ser igual o superir al del mínim." + +#: pkg/campsite/types/option.go:353 pkg/campsite/types/admin.go:403 +msgid "Price per night can not be empty." +msgstr "No podeu deixar el preu per nit en blanc." + +#: pkg/campsite/types/option.go:354 pkg/campsite/types/admin.go:404 +msgid "Price per night must be a decimal number." +msgstr "El preu per nit ha de ser un número decimal." + +#: pkg/campsite/types/option.go:355 pkg/campsite/types/admin.go:405 +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/admin.go:280 msgctxt "input" msgid "Cover image" msgstr "Imatge de portada" -#: pkg/campsite/types/admin.go:279 +#: pkg/campsite/types/admin.go:281 msgctxt "action" msgid "Set campsite type cover" msgstr "Estableix la portada del tipus d’allotjament" -#: pkg/campsite/types/admin.go:388 -msgid "Name must have at least one letter." -msgstr "El nom ha de tenir com a mínim una lletra." - -#: pkg/campsite/types/admin.go:390 +#: pkg/campsite/types/admin.go:392 msgid "Cover image can not be empty." msgstr "No podeu deixar la imatge de portada en blanc." -#: pkg/campsite/types/admin.go:391 +#: pkg/campsite/types/admin.go:393 msgid "Cover image must be an image media type." msgstr "La imatge de portada ha de ser un mèdia de tipus imatge." -#: pkg/campsite/types/admin.go:395 +#: pkg/campsite/types/admin.go:397 msgid "Maximum number of campers can not be empty." msgstr "No podeu deixar el número màxim de persones en blanc." -#: pkg/campsite/types/admin.go:396 +#: pkg/campsite/types/admin.go:398 msgid "Maximum number of campers must be an integer number." msgstr "El número màxim de persones ha de ser enter." -#: pkg/campsite/types/admin.go:397 +#: pkg/campsite/types/admin.go:399 msgid "Maximum number of campers must be one or greater." msgstr "El número màxim de persones no pot ser zero." -#: pkg/campsite/types/admin.go:401 -msgid "Price per night can not be empty." -msgstr "No podeu deixar el preu per nit en blanc." - -#: pkg/campsite/types/admin.go:402 -msgid "Price per night must be a decimal number." -msgstr "El preu per nit ha de ser un número decimal." - -#: pkg/campsite/types/admin.go:403 -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/admin.go:406 +#: pkg/campsite/types/admin.go:408 msgid "Minimum number of nights can not be empty." msgstr "No podeu deixar el número mínim de nits en blanc." -#: pkg/campsite/types/admin.go:407 +#: pkg/campsite/types/admin.go:409 msgid "Minimum number of nights must be an integer." msgstr "El número mínim de nits ha de ser enter." -#: pkg/campsite/types/admin.go:408 +#: pkg/campsite/types/admin.go:410 msgid "Minimum number of nights must be one or greater." msgstr "El número mínim de nits no pot ser zero." @@ -1097,7 +1192,11 @@ msgstr "No podeu deixar el fitxer del mèdia en blanc." #: pkg/media/admin.go:324 msgid "Filename can not be empty." -msgstr "No podeu deixar el nom del fitxer." +msgstr "No podeu deixar el nom del fitxer en blanc." + +#~ msgctxt "campsite type" +#~ msgid "Translations" +#~ msgstr "Traduccions" #~ msgid "Surroundings" #~ msgstr "Entorn" diff --git a/po/es.po b/po/es.po index e67a9ce..642bacc 100644 --- a/po/es.po +++ b/po/es.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-10-03 21:05+0200\n" +"POT-Creation-Date: 2023-10-06 13:18+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -20,6 +20,7 @@ msgstr "" #: web/templates/public/services.gohtml:6 #: web/templates/public/services.gohtml:15 +#: web/templates/public/layout.gohtml:42 #: web/templates/admin/services/index.gohtml:53 msgctxt "title" msgid "Services" @@ -50,6 +51,7 @@ msgstr "Nuestros servicios" #: web/templates/public/home.gohtml:34 #: web/templates/public/surroundings.gohtml:6 #: web/templates/public/surroundings.gohtml:10 +#: web/templates/public/layout.gohtml:43 msgctxt "title" msgid "Surroundings" msgstr "El entorno" @@ -75,6 +77,7 @@ msgid "Come and enjoy!" msgstr "¡Ven a disfrutar!" #: web/templates/public/campsite/type.gohtml:17 +#: web/templates/admin/campsite/option/form.gohtml:58 #: web/templates/admin/campsite/type/form.gohtml:73 msgctxt "title" msgid "Prices" @@ -85,6 +88,10 @@ msgid "%s €/night" msgstr "%s €/noche" #: web/templates/public/campsite/type.gohtml:28 +msgid "%s: %s €/night" +msgstr ":%s: %s €/noche" + +#: web/templates/public/campsite/type.gohtml:31 msgid "*Minimum %d nights per stay" msgstr "*Mínimo %d noches por estancia" @@ -147,7 +154,7 @@ msgid "There are several points where you can go by kayak, from sections of the msgstr "Hay diversos puntos dónde podéis ir en kayak, desde tramos del río Ter como también en la costa…." #: web/templates/public/layout.gohtml:11 web/templates/public/layout.gohtml:24 -#: web/templates/public/layout.gohtml:59 +#: web/templates/public/layout.gohtml:61 msgid "Campsite Montagut" msgstr "Camping Montagut" @@ -179,6 +186,7 @@ msgstr "Leyenda" #: web/templates/admin/carousel/form.gohtml:47 #: web/templates/admin/campsite/form.gohtml:70 +#: web/templates/admin/campsite/option/form.gohtml:78 #: web/templates/admin/campsite/type/form.gohtml:108 #: web/templates/admin/season/form.gohtml:64 #: web/templates/admin/services/form.gohtml:69 @@ -189,6 +197,7 @@ msgstr "Actualizar" #: web/templates/admin/carousel/form.gohtml:49 #: web/templates/admin/campsite/form.gohtml:72 +#: web/templates/admin/campsite/option/form.gohtml:80 #: web/templates/admin/campsite/type/form.gohtml:110 #: web/templates/admin/season/form.gohtml:66 #: web/templates/admin/services/form.gohtml:71 @@ -203,6 +212,7 @@ msgid "Translate Carousel Slide to %s" msgstr "Traducción de la diapositiva de carrusel a %s" #: web/templates/admin/carousel/l10n.gohtml:21 +#: web/templates/admin/campsite/option/l10n.gohtml:21 #: web/templates/admin/campsite/type/l10n.gohtml:21 #: web/templates/admin/campsite/type/l10n.gohtml:33 #: web/templates/admin/season/l10n.gohtml:21 @@ -212,6 +222,7 @@ msgid "Source:" msgstr "Origen:" #: web/templates/admin/carousel/l10n.gohtml:23 +#: web/templates/admin/campsite/option/l10n.gohtml:23 #: web/templates/admin/campsite/type/l10n.gohtml:23 #: web/templates/admin/campsite/type/l10n.gohtml:36 #: web/templates/admin/season/l10n.gohtml:23 @@ -222,6 +233,7 @@ msgid "Translation:" msgstr "Traducción" #: web/templates/admin/carousel/l10n.gohtml:32 +#: web/templates/admin/campsite/option/l10n.gohtml:32 #: web/templates/admin/campsite/type/l10n.gohtml:45 #: web/templates/admin/season/l10n.gohtml:32 #: web/templates/admin/services/l10n.gohtml:45 @@ -261,6 +273,85 @@ msgctxt "input" msgid "Label" msgstr "Etiqueta" +#: web/templates/admin/campsite/option/form.gohtml:8 +#: web/templates/admin/campsite/option/form.gohtml:25 +msgctxt "title" +msgid "Edit Campsite Type Option" +msgstr "Edición de la opción del tipo de alojamiento" + +#: web/templates/admin/campsite/option/form.gohtml:10 +#: web/templates/admin/campsite/option/form.gohtml:27 +msgctxt "title" +msgid "New Campsite Type Option" +msgstr "Nueva opción del tipo de alojamiento" + +#: web/templates/admin/campsite/option/form.gohtml:34 +#: web/templates/admin/campsite/option/l10n.gohtml:20 +#: web/templates/admin/campsite/type/form.gohtml:46 +#: web/templates/admin/campsite/type/l10n.gohtml:20 +#: web/templates/admin/season/form.gohtml:46 +#: web/templates/admin/season/l10n.gohtml:20 +#: web/templates/admin/services/form.gohtml:52 +#: web/templates/admin/services/l10n.gohtml:20 +#: web/templates/admin/profile.gohtml:26 +msgctxt "input" +msgid "Name" +msgstr "Nombre" + +#: web/templates/admin/campsite/option/form.gohtml:42 +msgctxt "input" +msgid "Minimum" +msgstr "Mínimo" + +#: web/templates/admin/campsite/option/form.gohtml:50 +msgctxt "input" +msgid "Maximum" +msgstr "Màximo" + +#: web/templates/admin/campsite/option/form.gohtml:64 +#: web/templates/admin/campsite/type/form.gohtml:79 +msgctxt "input" +msgid "Price per night" +msgstr "Precio por noche" + +#: web/templates/admin/campsite/option/index.gohtml:6 +#: web/templates/admin/campsite/option/index.gohtml:12 +msgctxt "title" +msgid "Campsite Type Options" +msgstr "Opciones del tipo de alojamiento" + +#: web/templates/admin/campsite/option/index.gohtml:11 +msgctxt "action" +msgid "Add Option" +msgstr "Añadir opción" + +#: web/templates/admin/campsite/option/index.gohtml:17 +#: web/templates/admin/campsite/type/index.gohtml:17 +#: web/templates/admin/season/index.gohtml:18 +msgctxt "header" +msgid "Name" +msgstr "Nombre" + +#: web/templates/admin/campsite/option/index.gohtml:18 +#: web/templates/admin/campsite/type/index.gohtml:18 +#: web/templates/admin/season/index.gohtml:19 +#: web/templates/admin/services/index.gohtml:19 +#: web/templates/admin/services/index.gohtml:60 +#: web/templates/admin/home/index.gohtml:19 +msgctxt "header" +msgid "Translations" +msgstr "Traducciones" + +#: web/templates/admin/campsite/option/index.gohtml:39 +msgid "No campsite type options added yet." +msgstr "No se ha añadido ninguna opció al tipo de alojamiento todavía." + +#: web/templates/admin/campsite/option/l10n.gohtml:7 +#: web/templates/admin/campsite/option/l10n.gohtml:14 +msgctxt "title" +msgid "Translate Campsite Type Option to %s" +msgstr "Traducción de la opción del tipo de alojamiento a %s" + #: web/templates/admin/campsite/index.gohtml:6 #: web/templates/admin/campsite/index.gohtml:12 #: web/templates/admin/layout.gohtml:40 web/templates/admin/layout.gohtml:71 @@ -284,13 +375,13 @@ msgid "Type" msgstr "Tipo" #: web/templates/admin/campsite/index.gohtml:28 -#: web/templates/admin/campsite/type/index.gohtml:35 +#: web/templates/admin/campsite/type/index.gohtml:37 #: web/templates/admin/season/index.gohtml:39 msgid "Yes" msgstr "Sí" #: web/templates/admin/campsite/index.gohtml:28 -#: web/templates/admin/campsite/type/index.gohtml:35 +#: web/templates/admin/campsite/type/index.gohtml:37 #: web/templates/admin/season/index.gohtml:39 msgid "No" msgstr "No" @@ -303,7 +394,7 @@ msgstr "No se ha añadido ningún alojamiento todavía." #: web/templates/admin/campsite/type/form.gohtml:25 msgctxt "title" msgid "Edit Campsite Type" -msgstr "Edición del tipo de alojamientos" +msgstr "Edición del tipo de alojamiento" #: web/templates/admin/campsite/type/form.gohtml:10 #: web/templates/admin/campsite/type/form.gohtml:27 @@ -312,22 +403,11 @@ msgid "New Campsite Type" msgstr "Nuevo tipo de alojamiento" #: web/templates/admin/campsite/type/form.gohtml:37 -#: web/templates/admin/campsite/type/index.gohtml:19 +#: web/templates/admin/campsite/type/index.gohtml:20 msgctxt "campsite type" msgid "Active" msgstr "Activo" -#: web/templates/admin/campsite/type/form.gohtml:46 -#: web/templates/admin/campsite/type/l10n.gohtml:20 -#: web/templates/admin/season/form.gohtml:46 -#: web/templates/admin/season/l10n.gohtml:20 -#: web/templates/admin/services/form.gohtml:52 -#: web/templates/admin/services/l10n.gohtml:20 -#: web/templates/admin/profile.gohtml:26 -msgctxt "input" -msgid "Name" -msgstr "Nombre" - #: web/templates/admin/campsite/type/form.gohtml:57 msgctxt "input" msgid "Maximum number of campers" @@ -338,11 +418,6 @@ msgctxt "input" msgid "Dogs allowed" msgstr "Se permiten perros" -#: web/templates/admin/campsite/type/form.gohtml:79 -msgctxt "input" -msgid "Price per night" -msgstr "Precio por noche" - #: web/templates/admin/campsite/type/form.gohtml:87 msgctxt "input" msgid "Minimum number of nights" @@ -368,22 +443,17 @@ msgctxt "action" msgid "Add Type" msgstr "Añadir tipo" -#: web/templates/admin/campsite/type/index.gohtml:17 -#: web/templates/admin/season/index.gohtml:18 +#: web/templates/admin/campsite/type/index.gohtml:19 msgctxt "header" -msgid "Name" -msgstr "Nombre" +msgid "Options" +msgstr "Opciones" -#: web/templates/admin/campsite/type/index.gohtml:18 -#: web/templates/admin/season/index.gohtml:19 -#: web/templates/admin/services/index.gohtml:19 -#: web/templates/admin/services/index.gohtml:60 -#: web/templates/admin/home/index.gohtml:19 -msgctxt "campsite type" -msgid "Translations" -msgstr "Traducciones" +#: web/templates/admin/campsite/type/index.gohtml:36 +msgctxt "action" +msgid "Edit Options" +msgstr "Editar opciones" -#: web/templates/admin/campsite/type/index.gohtml:41 +#: web/templates/admin/campsite/type/index.gohtml:43 msgid "No campsite types added yet." msgstr "No se ha añadido ningún tipo de alojamiento todavía." @@ -566,7 +636,7 @@ msgstr "Leyenda" #: web/templates/admin/services/index.gohtml:20 #: web/templates/admin/services/index.gohtml:61 #: web/templates/admin/home/index.gohtml:20 -msgctxt "campsite type" +msgctxt "header" msgid "Actions" msgstr "Acciones" @@ -846,8 +916,9 @@ msgctxt "language option" msgid "Automatic" msgstr "Automático" -#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:82 -#: pkg/campsite/types/admin.go:387 pkg/season/l10n.go:69 +#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:73 +#: pkg/campsite/types/l10n.go:128 pkg/campsite/types/option.go:336 +#: pkg/campsite/types/admin.go:389 pkg/season/l10n.go:69 #: pkg/season/admin.go:382 pkg/services/l10n.go:73 pkg/services/admin.go:266 msgid "Name can not be empty." msgstr "No podéis dejar el nombre en blanco." @@ -868,61 +939,85 @@ msgstr "El archivo tiene que ser una imagen PNG o JPEG válida." msgid "Access forbidden" msgstr "Acceso prohibido" -#: pkg/campsite/types/admin.go:278 +#: pkg/campsite/types/option.go:337 pkg/campsite/types/admin.go:390 +msgid "Name must have at least one letter." +msgstr "El nombre tiene que tener como mínimo una letra." + +#: pkg/campsite/types/option.go:340 +msgid "Minimum can not be empty." +msgstr "No podéis dejar el mínimo en blanco." + +#: pkg/campsite/types/option.go:341 +msgid "Minimum must be an integer number." +msgstr "El valor de mínimo tiene que ser un número entero." + +#: pkg/campsite/types/option.go:343 +msgid "Minimum must be zero or greater." +msgstr "El valor de mínimo tiene que ser como mínimo cero." + +#: pkg/campsite/types/option.go:346 +msgid "Maximum can not be empty." +msgstr "No podéis dejar el máxmimo en blanco." + +#: pkg/campsite/types/option.go:347 +msgid "Maximum must be an integer number." +msgstr "El valor del máximo tiene que ser un número entero." + +#: pkg/campsite/types/option.go:349 +msgid "Maximum must be equal or greater than minimum." +msgstr "El valor del máximo tiene que ser igual o mayor al del mínimo." + +#: pkg/campsite/types/option.go:353 pkg/campsite/types/admin.go:403 +msgid "Price per night can not be empty." +msgstr "No podéis dejar el precio por noche en blanco." + +#: pkg/campsite/types/option.go:354 pkg/campsite/types/admin.go:404 +msgid "Price per night must be a decimal number." +msgstr "El precio por noche tien que ser un número decimal." + +#: pkg/campsite/types/option.go:355 pkg/campsite/types/admin.go:405 +msgid "Price per night must be zero or greater." +msgstr "El precio por noche tiene que ser como mínimo cero." + +#: pkg/campsite/types/admin.go:280 msgctxt "input" msgid "Cover image" msgstr "Imagen de portada" -#: pkg/campsite/types/admin.go:279 +#: pkg/campsite/types/admin.go:281 msgctxt "action" msgid "Set campsite type cover" msgstr "Establecer la portada del tipo de alojamiento" -#: pkg/campsite/types/admin.go:388 -msgid "Name must have at least one letter." -msgstr "El nombre tiene que tener como mínimo una letra." - -#: pkg/campsite/types/admin.go:390 +#: pkg/campsite/types/admin.go:392 msgid "Cover image can not be empty." msgstr "No podéis dejar la imagen de portada en blanco." -#: pkg/campsite/types/admin.go:391 +#: pkg/campsite/types/admin.go:393 msgid "Cover image must be an image media type." msgstr "La imagen de portada tiene que ser un medio de tipo imagen." -#: pkg/campsite/types/admin.go:395 +#: pkg/campsite/types/admin.go:397 msgid "Maximum number of campers can not be empty." msgstr "No podéis dejar el número máximo de personas en blanco." -#: pkg/campsite/types/admin.go:396 +#: pkg/campsite/types/admin.go:398 msgid "Maximum number of campers must be an integer number." msgstr "El número máximo de personas tiene que ser entero." -#: pkg/campsite/types/admin.go:397 +#: pkg/campsite/types/admin.go:399 msgid "Maximum number of campers must be one or greater." msgstr "El número máximo de personas no puede ser cero." -#: pkg/campsite/types/admin.go:401 -msgid "Price per night can not be empty." -msgstr "No podéis dejar el precio por noche en blanco." - -#: pkg/campsite/types/admin.go:402 -msgid "Price per night must be a decimal number." -msgstr "El precio por noche tien que ser un número decimal." - -#: pkg/campsite/types/admin.go:403 -msgid "Price per night must be zero or greater." -msgstr "El precio por noche tiene que ser como mínimo cero." - -#: pkg/campsite/types/admin.go:406 +#: pkg/campsite/types/admin.go:408 msgid "Minimum number of nights can not be empty." msgstr "No podéis dejar el número mínimo de noches en blanco." -#: pkg/campsite/types/admin.go:407 +#: pkg/campsite/types/admin.go:409 msgid "Minimum number of nights must be an integer." msgstr "El número mínimo de noches tiene que ser entero." -#: pkg/campsite/types/admin.go:408 +#: pkg/campsite/types/admin.go:410 msgid "Minimum number of nights must be one or greater." msgstr "El número mínimo de noches no puede ser cero." @@ -1099,6 +1194,10 @@ msgstr "No podéis dejar el archivo del medio en blanco." msgid "Filename can not be empty." msgstr "No podéis dejar el nombre del archivo en blanco." +#~ msgctxt "campsite type" +#~ msgid "Translations" +#~ msgstr "Traducciones" + #~ msgid "Surroundings" #~ msgstr "Entorno" diff --git a/revert/add_campsite_type_option.sql b/revert/add_campsite_type_option.sql new file mode 100644 index 0000000..ff13e2e --- /dev/null +++ b/revert/add_campsite_type_option.sql @@ -0,0 +1,7 @@ +-- Revert camper:add_campsite_type_option from pg + +begin; + +drop function if exists camper.add_campsite_type_option(uuid, text, integer, integer); + +commit; diff --git a/revert/campsite_type_option.sql b/revert/campsite_type_option.sql new file mode 100644 index 0000000..31f28ac --- /dev/null +++ b/revert/campsite_type_option.sql @@ -0,0 +1,7 @@ +-- Revert camper:campsite_type_option from pg + +begin; + +drop table if exists camper.campsite_type_option; + +commit; diff --git a/revert/campsite_type_option_cost.sql b/revert/campsite_type_option_cost.sql new file mode 100644 index 0000000..c50db8f --- /dev/null +++ b/revert/campsite_type_option_cost.sql @@ -0,0 +1,7 @@ +-- Revert camper:campsite_type_option_cost from pg + +begin; + +drop table if exists camper.campsite_type_option_cost; + +commit; diff --git a/revert/campsite_type_option_i18n.sql b/revert/campsite_type_option_i18n.sql new file mode 100644 index 0000000..a226d9c --- /dev/null +++ b/revert/campsite_type_option_i18n.sql @@ -0,0 +1,7 @@ +-- Revert camper:campsite_type_option_i18n from pg + +begin; + +drop table if exists camper.campsite_type_option_i18n; + +commit; diff --git a/revert/edit_campsite_type_option.sql b/revert/edit_campsite_type_option.sql new file mode 100644 index 0000000..ec17581 --- /dev/null +++ b/revert/edit_campsite_type_option.sql @@ -0,0 +1,7 @@ +-- Revert camper:edit_campsite_type_option from pg + +begin; + +drop function if exists camper.edit_campsite_type_option(integer, text, integer, integer); + +commit; diff --git a/revert/set_campsite_type_option_cost.sql b/revert/set_campsite_type_option_cost.sql new file mode 100644 index 0000000..0fea3a3 --- /dev/null +++ b/revert/set_campsite_type_option_cost.sql @@ -0,0 +1,7 @@ +-- Revert camper:set_campsite_type_option_cost from pg + +begin; + +drop function if exists camper.set_campsite_type_option_cost(integer, integer, text, integer); + +commit; diff --git a/revert/translate_campsite_type_option.sql b/revert/translate_campsite_type_option.sql new file mode 100644 index 0000000..e192686 --- /dev/null +++ b/revert/translate_campsite_type_option.sql @@ -0,0 +1,7 @@ +-- Revert camper:translate_campsite_type_option from pg + +begin; + +drop function if exists camper.translate_campsite_type_option(integer, text, text); + +commit; diff --git a/sqitch.plan b/sqitch.plan index 9049e97..e3fa6ad 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -90,3 +90,10 @@ to_price [roles schema_camper] 2023-10-01T16:30:40Z jordi fita mas # Add function to set the cost of a campsite type for a given season season_i18n [roles schema_camper season language] 2023-10-03T18:30:42Z jordi fita mas # Add relation for season translations translate_season [roles schema_camper season season_i18n] 2023-10-03T18:37:19Z jordi fita mas # Add function to translate seasons +campsite_type_option [roles schema_camper campsite_type user_profile] 2023-10-05T16:19:03Z jordi fita mas # Add relation for campsite type “extra” options +campsite_type_option_i18n [roles schema_camper campsite_type_option language] 2023-10-05T16:56:57Z jordi fita mas # Add the relation for campsite_type_option internationalization +translate_campsite_type_option [roles schema_camper campsite_type_option_i18n] 2023-10-05T17:05:31Z jordi fita mas # Add function to translate campsite type options +campsite_type_option_cost [roles schema_camper campsite_type season campsite_type_option user_profile] 2023-10-05T17:21:30Z jordi fita mas # Add relation for campsite type option cost +set_campsite_type_option_cost [roles schema_camper campsite_type_option_cost parse_price] 2023-10-05T17:41:58Z jordi fita mas # Add function to set cost of campsite type option +add_campsite_type_option [roles schema_camper campsite_type_option campsite_type] 2023-10-06T09:40:03Z jordi fita mas # Add function to create new campsite type options +edit_campsite_type_option [roles schema_camper campsite_type_option] 2023-10-06T09:51:02Z jordi fita mas # Add function to edit campsite type options diff --git a/test/add_campsite_type_option.sql b/test/add_campsite_type_option.sql new file mode 100644 index 0000000..991deae --- /dev/null +++ b/test/add_campsite_type_option.sql @@ -0,0 +1,76 @@ +-- Test add_campsite_type_option +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', 'add_campsite_type_option', array['uuid', 'text', 'integer', 'integer']); +select function_lang_is('camper', 'add_campsite_type_option', array['uuid', 'text', 'integer', 'integer'], 'sql'); +select function_returns('camper', 'add_campsite_type_option', array['uuid', 'text', 'integer', 'integer'], 'integer'); +select isnt_definer('camper', 'add_campsite_type_option', array['uuid', 'text', 'integer', 'integer']); +select volatility_is('camper', 'add_campsite_type_option', array['uuid', 'text', 'integer', 'integer'], 'volatile'); +select function_privs_are('camper', 'add_campsite_type_option', array ['uuid', 'text', 'integer', 'integer'], 'guest', array[]::text[]); +select function_privs_are('camper', 'add_campsite_type_option', array ['uuid', 'text', 'integer', 'integer'], 'employee', array[]::text[]); +select function_privs_are('camper', 'add_campsite_type_option', array ['uuid', 'text', 'integer', 'integer'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'add_campsite_type_option', array ['uuid', 'text', 'integer', 'integer'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate campsite_type_option_i18n cascade; +truncate campsite_type_option cascade; +truncate campsite_type 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, country_code, currency_code, default_lang_tag) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '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 (2, 1, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) +; + +insert into campsite_type (campsite_type_id, company_id, slug, media_id, name, description, active, dogs_allowed, max_campers) +values (3, 1, '87452b88-b48f-48d3-bb6c-0296de64164e', 2, 'Type A', '

A

', true, false, 4) + , (4, 1, '9ae5cf87-cd69-4541-b5a5-75f937cc9e58', 2, 'Type B', '

B

', true, false, 5) +; + +select lives_ok( + $$ select add_campsite_type_option('87452b88-b48f-48d3-bb6c-0296de64164e', 'Option 1', 0, 10) $$, + 'Should be able to add an option to the first campsite type' +); + +select lives_ok( + $$ select add_campsite_type_option('9ae5cf87-cd69-4541-b5a5-75f937cc9e58', 'Option 2', 5, 15) $$, + 'Should be able to add an option to the second campsite type' +); + +select bag_eq( + $$ select campsite_type_id, name, range from campsite_type_option $$, + $$ values (3, 'Option 1', '[0, 10]'::int4range) + , (4, 'Option 2', '[5, 15]'::int4range) + $$, + 'Should have added all two campsite type options' +); + +select is_empty( + $$ select * from campsite_type_option_i18n $$, + 'Should not have added any translation for campsite type options.' +); + +select * +from finish(); + +rollback; diff --git a/test/campsite_type_option.sql b/test/campsite_type_option.sql new file mode 100644 index 0000000..ad8e1c6 --- /dev/null +++ b/test/campsite_type_option.sql @@ -0,0 +1,208 @@ +-- Test campsite_type_option +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(40); + +set search_path to camper, public; + +select has_table('campsite_type_option'); +select has_pk('campsite_type_option'); +select table_privs_are('campsite_type_option', 'guest', array['SELECT']); +select table_privs_are('campsite_type_option', 'employee', array['SELECT']); +select table_privs_are('campsite_type_option', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('campsite_type_option', 'authenticator', array[]::text[]); + +select has_column('campsite_type_option', 'campsite_type_option_id'); +select col_is_pk('campsite_type_option', 'campsite_type_option_id'); +select col_type_is('campsite_type_option', 'campsite_type_option_id', 'integer'); +select col_not_null('campsite_type_option', 'campsite_type_option_id'); +select col_hasnt_default('campsite_type_option', 'campsite_type_option_id'); + +select has_column('campsite_type_option', 'campsite_type_id'); +select col_is_fk('campsite_type_option', 'campsite_type_id'); +select fk_ok('campsite_type_option', 'campsite_type_id', 'campsite_type', 'campsite_type_id'); +select col_type_is('campsite_type_option', 'campsite_type_id', 'integer'); +select col_not_null('campsite_type_option', 'campsite_type_id'); +select col_hasnt_default('campsite_type_option', 'campsite_type_id'); + +select has_column('campsite_type_option', 'name'); +select col_type_is('campsite_type_option', 'name', 'text'); +select col_not_null('campsite_type_option', 'name'); +select col_hasnt_default('campsite_type_option', 'name'); + +select has_column('campsite_type_option', 'range'); +select col_type_is('campsite_type_option', 'range', 'int4range'); +select col_not_null('campsite_type_option', 'range'); +select col_hasnt_default('campsite_type_option', 'range'); + + +set client_min_messages to warning; +truncate campsite_type_option cascade; +truncate campsite_type 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, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca') +; + +insert into company_user (company_id, user_id, role) +values (2, 1, 'admin') + , (4, 5, 'admin') +; + +insert into company_host (company_id, host) +values (2, 'co2') + , (4, 'co4') +; + +insert into media_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 (6, 2, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) + , (8, 4, 'cover4.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) +; + +insert into campsite_type (campsite_type_id, company_id, name, media_id, dogs_allowed, max_campers) +values (16, 2, 'Wooden lodge', 6, false, 7) + , (18, 4, 'Bungalow', 8, false, 6) +; + +insert into campsite_type_option (campsite_type_id, name, range) +values (16, 'Option 16.1', '[2, 2]') + , (18, 'Option 18.1', '[4, 8]') +; + +prepare campsite_option_data as +select campsite_type_id, name +from campsite_type_option +; + +set role guest; +select bag_eq( + 'campsite_option_data', + $$ values (16, 'Option 16.1') + , (18, 'Option 18.1') + $$, + 'Everyone should be able to list all campsite type options across all companies' +); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2'); + +select lives_ok( + $$ insert into campsite_type_option(campsite_type_id, name, range) values (16, 'Option 16.2', '[3, 3]') $$, + 'Admin from company 2 should be able to insert a new campsite type option to that company.' +); + +select bag_eq( + 'campsite_option_data', + $$ values (16, 'Option 16.1') + , (16, 'Option 16.2') + , (18, 'Option 18.1') + $$, + 'The new row should have been added' +); + +select lives_ok( + $$ update campsite_type_option set name = 'Option 16-2' where campsite_type_id = 16 and name = 'Option 16.2' $$, + 'Admin from company 2 should be able to update campsite type option of that company.' +); + +select bag_eq( + 'campsite_option_data', + $$ values (16, 'Option 16.1') + , (16, 'Option 16-2') + , (18, 'Option 18.1') + $$, + 'The row should have been updated.' +); + +select lives_ok( + $$ delete from campsite_type_option where campsite_type_id = 16 and name = 'Option 16-2' $$, + 'Admin from company 2 should be able to delete campsite type option from that company.' +); + +select bag_eq( + 'campsite_option_data', + $$ values (16, 'Option 16.1') + , (18, 'Option 18.1') + $$, + 'The row should have been deleted.' +); + +select throws_ok( + $$ insert into campsite_type_option (campsite_type_id, name, range) values (18, 'Option 18.2', '[5, 5]') $$, + '42501', 'new row violates row-level security policy for table "campsite_type_option"', + 'Admin from company 2 should NOT be able to insert new campsite type options to company 4.' +); + +select lives_ok( + $$ update campsite_type_option set name = 'Option 18-1' where campsite_type_id = 18 $$, + 'Admin from company 2 should not be able to update campsite types of company 4, but no error if campsite_type_id is not changed.' +); + +select bag_eq( + 'campsite_option_data', + $$ values (16, 'Option 16.1') + , (18, 'Option 18.1') + $$, + 'No row should have been changed.' +); + +select throws_ok( + $$ update campsite_type_option set campsite_type_id = 18 where campsite_type_id = 16 $$, + '42501', 'new row violates row-level security policy for table "campsite_type_option"', + 'Admin from company 2 should NOT be able to move campsite type option to one of company 4' +); + +select lives_ok( + $$ delete from campsite_type_option where campsite_type_id = 18 $$, + 'Admin from company 2 should NOT be able to delete campsite type from company 4, but not error is thrown' +); + +select bag_eq( + 'campsite_option_data', + $$ values (16, 'Option 16.1') + , (18, 'Option 18.1') + $$, + 'No row should have been changed' +); + +select throws_ok( + $$ insert into campsite_type_option (campsite_type_id, name, range) values (16, ' ', '[5, 6]') $$, + '23514', 'new row for relation "campsite_type_option" violates check constraint "name_not_empty"', + 'Should not be able to insert campsite type options with a blank name.' +); + +select throws_ok( + $$ insert into campsite_type_option (campsite_type_id, name, range) values (16, 'Option 16.2', '[-1, 1]') $$, + '23514', 'new row for relation "campsite_type_option" violates check constraint "range_not_negative"', + 'Should not be able to insert campsite type options with a range starting with a negative number' +); + +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/campsite_type_option_cost.sql b/test/campsite_type_option_cost.sql new file mode 100644 index 0000000..b49a924 --- /dev/null +++ b/test/campsite_type_option_cost.sql @@ -0,0 +1,241 @@ +-- Test campsite_type_option_cost +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(42); + +set search_path to camper, public; + +select has_table('campsite_type_option_cost'); +select has_pk('campsite_type_option_cost'); +select col_is_pk('campsite_type_option_cost', array['campsite_type_option_id', 'season_id']); +select table_privs_are('campsite_type_option_cost', 'guest', array['SELECT']); +select table_privs_are('campsite_type_option_cost', 'employee', array['SELECT']); +select table_privs_are('campsite_type_option_cost', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('campsite_type_option_cost', 'authenticator', array[]::text[]); + +select has_column('campsite_type_option_cost', 'campsite_type_option_id'); +select col_is_fk('campsite_type_option_cost', 'campsite_type_option_id'); +select fk_ok('campsite_type_option_cost', 'campsite_type_option_id', 'campsite_type_option', 'campsite_type_option_id'); +select col_type_is('campsite_type_option_cost', 'campsite_type_option_id', 'integer'); +select col_not_null('campsite_type_option_cost', 'campsite_type_option_id'); +select col_hasnt_default('campsite_type_option_cost', 'campsite_type_option_id'); + +select has_column('campsite_type_option_cost', 'season_id'); +select col_is_fk('campsite_type_option_cost', 'season_id'); +select fk_ok('campsite_type_option_cost', 'season_id', 'season', 'season_id'); +select col_type_is('campsite_type_option_cost', 'season_id', 'integer'); +select col_not_null('campsite_type_option_cost', 'season_id'); +select col_hasnt_default('campsite_type_option_cost', 'season_id'); + +select has_column('campsite_type_option_cost', 'cost_per_night'); +select col_type_is('campsite_type_option_cost', 'cost_per_night', 'integer'); +select col_not_null('campsite_type_option_cost', 'cost_per_night'); +select col_hasnt_default('campsite_type_option_cost', 'cost_per_night'); + + +set client_min_messages to warning; +truncate campsite_type_option_cost cascade; +truncate season cascade; +truncate campsite_type_option cascade; +truncate campsite_type 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, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca') +; + +insert into company_user (company_id, user_id, role) +values (2, 1, 'admin') + , (4, 5, 'admin') +; + +insert into company_host (company_id, host) +values (2, 'co2') + , (4, 'co4') +; + +insert into media_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 (6, 2, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) + , (8, 4, 'cover4.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) +; + +insert into campsite_type (campsite_type_id, company_id, name, media_id, dogs_allowed, max_campers) +values (16, 2, 'Wooden lodge', 6, false, 7) + , (18, 4, 'Bungalow', 8, false, 6) +; + +insert into season (season_id, company_id, name) +values (26, 2, 'Low') + , (27, 2, 'Mid') + , (28, 4, 'Low') + , (29, 4, 'Mid') +; + +insert into campsite_type_option (campsite_type_option_id, campsite_type_id, name, range) +values (17, 16, 'Option 16.1', '[1, 16]') + , (19, 18, 'Option 18.1', '[1, 18]') +; + +insert into campsite_type_option_cost (campsite_type_option_id, season_id, cost_per_night) +values (17, 26, 2) + , (19, 28, 4) +; + +prepare option_cost_data as +select campsite_type_option_id, season_id, cost_per_night +from campsite_type_option_cost +; + +set role guest; +select bag_eq( + 'option_cost_data', + $$ values (17, 26, 2) + , (19, 28, 4) + $$, + 'Everyone should be able to list all option costs across all companies' +); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2'); + +select lives_ok( + $$ insert into campsite_type_option_cost(campsite_type_option_id, season_id, cost_per_night) values (17, 27, 3) $$, + 'Admin from company 2 should be able to insert a new option cost to that company.' +); + +select bag_eq( + 'option_cost_data', + $$ values (17, 26, 2) + , (17, 27, 3) + , (19, 28, 4) + $$, + 'The new row should have been added' +); + +select lives_ok( + $$ update campsite_type_option_cost set cost_per_night = 6 where campsite_type_option_id = 17 and season_id = 27 $$, + 'Admin from company 2 should be able to update option cost of that company.' +); + +select bag_eq( + 'option_cost_data', + $$ values (17, 26, 2) + , (17, 27, 6) + , (19, 28, 4) + $$, + 'The row should have been updated.' +); + +select lives_ok( + $$ delete from campsite_type_option_cost where campsite_type_option_id = 17 and season_id = 27 $$, + 'Admin from company 2 should be able to delete option cost from that company.' +); + +select bag_eq( + 'option_cost_data', + $$ values (17, 26, 2) + , (19, 28, 4) + $$, + 'The row should have been deleted.' +); + +select throws_ok( + $$ insert into campsite_type_option_cost (campsite_type_option_id, season_id, cost_per_night) values (19, 29, 5) $$, + '42501', 'new row violates row-level security policy for table "campsite_type_option_cost"', + 'Admin from company 2 should NOT be able to insert new option costs to company 4.' +); + +select throws_ok( + $$ insert into campsite_type_option_cost (campsite_type_option_id, season_id, cost_per_night) values (19, 27, 5) $$, + '42501', 'new row violates row-level security policy for table "campsite_type_option_cost"', + 'Admin from company 2 should NOT be able to insert new row with a campsite type from company 4.' +); + +select throws_ok( + $$ insert into campsite_type_option_cost (campsite_type_option_id, season_id, cost_per_night) values (17, 29, 5) $$, + '42501', 'new row violates row-level security policy for table "campsite_type_option_cost"', + 'Admin from company 2 should NOT be able to insert new row with a season from company 4.' +); + +select lives_ok( + $$ update campsite_type_option_cost set cost_per_night = 1 where campsite_type_option_id = 19 $$, + 'Admin from company 2 should not be able to update campsite types of company 4, but no error if campsite_type_option_id is not changed.' +); + +select lives_ok( + $$ update campsite_type_option_cost set cost_per_night = 1 where season_id = 28 $$, + 'Admin from company 2 should not be able to update seasons of company 4, but no error if season_id is not changed.' +); + +select bag_eq( + 'option_cost_data', + $$ values (17, 26, 2) + , (19, 28, 4) + $$, + 'No row should have been changed.' +); + +select throws_ok( + $$ update campsite_type_option_cost set campsite_type_option_id = 19 where campsite_type_option_id = 17 $$, + '42501', 'new row violates row-level security policy for table "campsite_type_option_cost"', + 'Admin from company 2 should NOT be able to move campsite type to one of company 4' +); + +select throws_ok( + $$ update campsite_type_option_cost set season_id = 29 where season_id = 26 $$, + '42501', 'new row violates row-level security policy for table "campsite_type_option_cost"', + 'Admin from company 2 should NOT be able to move season to one of company 4' +); + +select lives_ok( + $$ delete from campsite_type_option_cost where campsite_type_option_id = 19 $$, + 'Admin from company 2 should NOT be able to delete campsite type from company 4, but not error is thrown' +); + +select lives_ok( + $$ delete from campsite_type_option_cost where season_id = 28 $$, + 'Admin from company 2 should NOT be able to delete season from company 4, but not error is thrown' +); + +select bag_eq( + 'option_cost_data', + $$ values (17, 26, 2) + , (19, 28, 4) + $$, + 'No row should have been changed' +); + +select throws_ok( + $$ insert into campsite_type_option_cost (campsite_type_option_id, season_id, cost_per_night) values (17, 27, -1) $$, + '23514', 'new row for relation "campsite_type_option_cost" violates check constraint "cost_not_negative"', + 'Should not be able to insert option costs with negative cost per night.' +); + +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/campsite_type_option_i18n.sql b/test/campsite_type_option_i18n.sql new file mode 100644 index 0000000..b8741a4 --- /dev/null +++ b/test/campsite_type_option_i18n.sql @@ -0,0 +1,44 @@ +-- Test campsite_type_option_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('campsite_type_option_i18n'); +select has_pk('campsite_type_option_i18n'); +select col_is_pk('campsite_type_option_i18n', array['campsite_type_option_id', 'lang_tag']); +select table_privs_are('campsite_type_option_i18n', 'guest', array['SELECT']); +select table_privs_are('campsite_type_option_i18n', 'employee', array['SELECT']); +select table_privs_are('campsite_type_option_i18n', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('campsite_type_option_i18n', 'authenticator', array[]::text[]); + +select has_column('campsite_type_option_i18n', 'campsite_type_option_id'); +select col_is_fk('campsite_type_option_i18n', 'campsite_type_option_id'); +select fk_ok('campsite_type_option_i18n', 'campsite_type_option_id', 'campsite_type_option', 'campsite_type_option_id'); +select col_type_is('campsite_type_option_i18n', 'campsite_type_option_id', 'integer'); +select col_not_null('campsite_type_option_i18n', 'campsite_type_option_id'); +select col_hasnt_default('campsite_type_option_i18n', 'campsite_type_option_id'); + +select has_column('campsite_type_option_i18n', 'lang_tag'); +select col_is_fk('campsite_type_option_i18n', 'lang_tag'); +select fk_ok('campsite_type_option_i18n', 'lang_tag', 'language', 'lang_tag'); +select col_type_is('campsite_type_option_i18n', 'lang_tag', 'text'); +select col_not_null('campsite_type_option_i18n', 'lang_tag'); +select col_hasnt_default('campsite_type_option_i18n', 'lang_tag'); + +select has_column('campsite_type_option_i18n', 'name'); +select col_type_is('campsite_type_option_i18n', 'name', 'text'); +select col_not_null('campsite_type_option_i18n', 'name'); +select col_hasnt_default('campsite_type_option_i18n', 'name'); + + +select * +from finish(); + +rollback; + diff --git a/test/edit_campsite_type_option.sql b/test/edit_campsite_type_option.sql new file mode 100644 index 0000000..fcef933 --- /dev/null +++ b/test/edit_campsite_type_option.sql @@ -0,0 +1,81 @@ +-- Test edit_campsite_type_option +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', 'edit_campsite_type_option', array['integer', 'text', 'integer', 'integer']); +select function_lang_is('camper', 'edit_campsite_type_option', array['integer', 'text', 'integer', 'integer'], 'sql'); +select function_returns('camper', 'edit_campsite_type_option', array['integer', 'text', 'integer', 'integer'], 'integer'); +select isnt_definer('camper', 'edit_campsite_type_option', array['integer', 'text', 'integer', 'integer']); +select volatility_is('camper', 'edit_campsite_type_option', array['integer', 'text', 'integer', 'integer'], 'volatile'); +select function_privs_are('camper', 'edit_campsite_type_option', array ['integer', 'text', 'integer', 'integer'], 'guest', array[]::text[]); +select function_privs_are('camper', 'edit_campsite_type_option', array ['integer', 'text', 'integer', 'integer'], 'employee', array[]::text[]); +select function_privs_are('camper', 'edit_campsite_type_option', array ['integer', 'text', 'integer', 'integer'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'edit_campsite_type_option', array ['integer', 'text', 'integer', 'integer'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate campsite_type_option_i18n cascade; +truncate campsite_type_option cascade; +truncate campsite_type 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, country_code, currency_code, default_lang_tag) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '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 (2, 1, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) +; + +insert into campsite_type (campsite_type_id, company_id, media_id, name, description, active, dogs_allowed, max_campers) +values (3, 1, 2, 'Type A', '

A

', true, false, 4) +; + +insert into campsite_type_option (campsite_type_option_id, campsite_type_id, name, range) +values (4, 3, 'Option 1', '[0, 10]') + , (5, 3, 'Option 2', '[5, 15]') +; + +select lives_ok( + $$ select edit_campsite_type_option(4, 'Option A', 1, 11) $$, + 'Should be able to edit the first option' +); + +select lives_ok( + $$ select edit_campsite_type_option(5, 'Option B', 6, 14) $$, + 'Should be able to edit the second option' +); + +select bag_eq( + $$ select campsite_type_option_id, campsite_type_id, name, range from campsite_type_option $$, + $$ values (4, 3, 'Option A', '[1, 11]'::int4range) + , (5, 3, 'Option B', '[6, 14]'::int4range) + $$, + 'Should have updated all campsite type options.' +); + +select is_empty( + $$ select * from campsite_type_option_i18n $$, + 'Should not have added any translation for campsite type options.' +); + + +select * +from finish(); + +rollback; diff --git a/test/set_campsite_type_option_cost.sql b/test/set_campsite_type_option_cost.sql new file mode 100644 index 0000000..ffedd98 --- /dev/null +++ b/test/set_campsite_type_option_cost.sql @@ -0,0 +1,93 @@ +-- Test set_campsite_type_option_cost +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', 'set_campsite_type_option_cost', array['integer', 'integer', 'text', 'integer']); +select function_lang_is('camper', 'set_campsite_type_option_cost', array['integer', 'integer', 'text', 'integer'], 'sql'); +select function_returns('camper', 'set_campsite_type_option_cost', array['integer', 'integer', 'text', 'integer'], 'void'); +select isnt_definer('camper', 'set_campsite_type_option_cost', array['integer', 'integer', 'text', 'integer']); +select volatility_is('camper', 'set_campsite_type_option_cost', array['integer', 'integer', 'text', 'integer'], 'volatile'); +select function_privs_are('camper', 'set_campsite_type_option_cost', array ['integer', 'integer', 'text', 'integer'], 'guest', array[]::text[]); +select function_privs_are('camper', 'set_campsite_type_option_cost', array ['integer', 'integer', 'text', 'integer'], 'employee', array[]::text[]); +select function_privs_are('camper', 'set_campsite_type_option_cost', array ['integer', 'integer', 'text', 'integer'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'set_campsite_type_option_cost', array ['integer', 'integer', 'text', 'integer'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate campsite_type_option_cost cascade; +truncate season cascade; +truncate campsite_type_option cascade; +truncate campsite_type 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, country_code, currency_code, default_lang_tag) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '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 (2, 1, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) +; + +insert into campsite_type (campsite_type_id, company_id, slug, media_id, name, description, max_campers, dogs_allowed, active) +values (3, 1, '87452b88-b48f-48d3-bb6c-0296de64164e', 2, 'Type A', '', 5, false, true) +; + +insert into season (season_id, company_id, name) +values (4, 1, 'High') + , (5, 1, 'Mid') + , (6, 1, 'Low') +; + +insert into campsite_type_option (campsite_type_option_id, campsite_type_id, name, range) +values (7, 3, 'Test Option', '[1, 100]') +; + +insert into campsite_type_option_cost (campsite_type_option_id, season_id, cost_per_night) +values (7, 4, 44) + , (7, 5, 55) +; + +select lives_ok( + $$ select set_campsite_type_option_cost(7, 4, '12.34') $$, + 'Should be able to edit the cost for high season' +); + +select lives_ok( + $$ select set_campsite_type_option_cost(7, 5, '0.0') $$, + 'Should be able to set the cost for mid season to zero' +); + +select lives_ok( + $$ select set_campsite_type_option_cost(7, 6, '3.21') $$, + 'Should be able to set the cost for low season, inserting it.' +); + +select bag_eq( + $$ select campsite_type_option_id, season_id, cost_per_night from campsite_type_option_cost $$, + $$ values (7, 4, 1234) + , (7, 5, 0) + , (7, 6, 321) + $$, + 'Should have updated all option costs.' +); + + +select * +from finish(); + +rollback; diff --git a/test/translate_campsite_type_option.sql b/test/translate_campsite_type_option.sql new file mode 100644 index 0000000..c2cb8ba --- /dev/null +++ b/test/translate_campsite_type_option.sql @@ -0,0 +1,85 @@ +-- Test translate_campsite_type_option_option +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_campsite_type_option', array['integer', 'text', 'text']); +select function_lang_is('camper', 'translate_campsite_type_option', array['integer', 'text', 'text'], 'sql'); +select function_returns('camper', 'translate_campsite_type_option', array['integer', 'text', 'text'], 'void'); +select isnt_definer('camper', 'translate_campsite_type_option', array['integer', 'text', 'text']); +select volatility_is('camper', 'translate_campsite_type_option', array['integer', 'text', 'text'], 'volatile'); +select function_privs_are('camper', 'translate_campsite_type_option', array['integer', 'text', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'translate_campsite_type_option', array['integer', 'text', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'translate_campsite_type_option', array['integer', 'text', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'translate_campsite_type_option', array['integer', 'text', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate campsite_type_option_i18n cascade; +truncate campsite_type_option cascade; +truncate campsite_type 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, country_code, currency_code, default_lang_tag) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '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 (2, 1, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) +; + +insert into campsite_type (campsite_type_id, company_id, slug, media_id, name, description, active, dogs_allowed, max_campers) +values (3, 1, '87452b88-b48f-48d3-bb6c-0296de64164e', 2, 'Type A', '

A

', true, false, 4) +; + +insert into campsite_type_option (campsite_type_option_id, campsite_type_id, name, range) +values (4, 3, 'Option 1', '[1, 1]') + , (5, 3, 'Option 2', '[1, 2]') +; + +insert into campsite_type_option_i18n (campsite_type_option_id, lang_tag, name) +values (5, 'ca', 'opció2') +; + +select lives_ok( + $$ select translate_campsite_type_option(4, 'ca', 'Opció 1') $$, + 'Should be able to translate the first option' +); + +select lives_ok( + $$ select translate_campsite_type_option(5, 'es', 'Opción 2') $$, + 'Should be able to translate the second option' +); + +select lives_ok( + $$ select translate_campsite_type_option(5, 'ca', 'Opció 2') $$, + 'Should be able to overwrite the catalan translation of the second option' +); + +select bag_eq( + $$ select campsite_type_option_id, lang_tag, name from campsite_type_option_i18n $$, + $$ values (4, 'ca', 'Opció 1') + , (5, 'ca', 'Opció 2') + , (5, 'es', 'Opción 2') + $$, + 'Should have added and updated all translations.' +); + + +select * +from finish(); + +rollback; diff --git a/verify/add_campsite_type_option.sql b/verify/add_campsite_type_option.sql new file mode 100644 index 0000000..b6b4086 --- /dev/null +++ b/verify/add_campsite_type_option.sql @@ -0,0 +1,7 @@ +-- Verify camper:add_campsite_type_option on pg + +begin; + +select has_function_privilege('camper.add_campsite_type_option(uuid, text, integer, integer)', 'execute'); + +rollback; diff --git a/verify/campsite_type_option.sql b/verify/campsite_type_option.sql new file mode 100644 index 0000000..15fb9e7 --- /dev/null +++ b/verify/campsite_type_option.sql @@ -0,0 +1,19 @@ +-- Verify camper:campsite_type_option on pg + +begin; + +select campsite_type_option_id + , campsite_type_id + , name + , range +from camper.campsite_type_option +where false; + +select 1 / count(*) from pg_class where oid = 'camper.campsite_type_option'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.campsite_type_option'::regclass; +select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.campsite_type_option'::regclass; +select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.campsite_type_option'::regclass; +select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.campsite_type_option'::regclass; + + +rollback; diff --git a/verify/campsite_type_option_cost.sql b/verify/campsite_type_option_cost.sql new file mode 100644 index 0000000..0e88ce7 --- /dev/null +++ b/verify/campsite_type_option_cost.sql @@ -0,0 +1,18 @@ +-- Verify camper:campsite_type_option_cost on pg + +begin; + +select campsite_type_option_id + , season_id + , cost_per_night +from camper.campsite_type_option_cost +where false; + +select 1 / count(*) from pg_class where oid = 'camper.campsite_type_option_cost'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.campsite_type_option_cost'::regclass; +select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.campsite_type_option_cost'::regclass; +select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.campsite_type_option_cost'::regclass; +select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.campsite_type_option_cost'::regclass; + + +rollback; diff --git a/verify/campsite_type_option_i18n.sql b/verify/campsite_type_option_i18n.sql new file mode 100644 index 0000000..6607d0d --- /dev/null +++ b/verify/campsite_type_option_i18n.sql @@ -0,0 +1,11 @@ +-- Verify camper:campsite_type_option_i18n on pg + +begin; + +select campsite_type_option_id + , lang_tag + , name +from camper.campsite_type_option_i18n +where false; + +rollback; diff --git a/verify/edit_campsite_type_option.sql b/verify/edit_campsite_type_option.sql new file mode 100644 index 0000000..df3d454 --- /dev/null +++ b/verify/edit_campsite_type_option.sql @@ -0,0 +1,7 @@ +-- Verify camper:edit_campsite_type_option on pg + +begin; + +select has_function_privilege('camper.edit_campsite_type_option(integer, text, integer, integer)', 'execute'); + +rollback; diff --git a/verify/set_campsite_type_option_cost.sql b/verify/set_campsite_type_option_cost.sql new file mode 100644 index 0000000..2e60d5d --- /dev/null +++ b/verify/set_campsite_type_option_cost.sql @@ -0,0 +1,7 @@ +-- Verify camper:set_campsite_type_option_cost on pg + +begin; + +select has_function_privilege('camper.set_campsite_type_option_cost(integer, integer, text, integer)', 'execute'); + +rollback; diff --git a/verify/translate_campsite_type_option.sql b/verify/translate_campsite_type_option.sql new file mode 100644 index 0000000..5028f7b --- /dev/null +++ b/verify/translate_campsite_type_option.sql @@ -0,0 +1,7 @@ +-- Verify camper:translate_campsite_type_option on pg + +begin; + +select has_function_privilege('camper.translate_campsite_type_option(integer, text, text)', 'execute'); + +rollback; diff --git a/web/templates/admin/campsite/option/form.gohtml b/web/templates/admin/campsite/option/form.gohtml new file mode 100644 index 0000000..3021e3b --- /dev/null +++ b/web/templates/admin/campsite/option/form.gohtml @@ -0,0 +1,85 @@ + +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite/types.optionForm*/ -}} + {{ if .ID}} + {{( pgettext "Edit Campsite Type Option" "title" )}} + {{ else }} + {{( pgettext "New Campsite Type Option" "title" )}} + {{ end }} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite/types.optionForm*/ -}} +
+

+ {{ if .ID }} + {{( pgettext "Edit Campsite Type Option" "title" )}} + {{ else }} + {{( pgettext "New Campsite Type Option" "title" )}} + {{ end }} +

+ {{ CSRFInput }} +
+ {{ with .Name -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Min -}} + + {{- end }} + {{ with .Max -}} + + {{- end }} + {{ with .Prices }} +
+ {{( pgettext "Prices" "title" )}} + {{ range . }} +
+ {{ .SeasonName }} + {{ with .PricePerNight -}} + + {{- end }} +
+ {{- end }} +
+ {{- end }} +
+
+ +
+
+{{- end }} diff --git a/web/templates/admin/campsite/option/index.gohtml b/web/templates/admin/campsite/option/index.gohtml new file mode 100644 index 0000000..ed758c7 --- /dev/null +++ b/web/templates/admin/campsite/option/index.gohtml @@ -0,0 +1,41 @@ + +{{ define "title" -}} + {{( pgettext "Campsite Type Options" "title" )}} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite/types.optionIndex*/ -}} + {{( pgettext "Add Option" "action" )}} +

{{( pgettext "Campsite Type Options" "title" )}}

+ {{ if .Options -}} + + + + + + + + + {{ range .Options -}} + + + + + {{- end }} + +
{{( pgettext "Name" "header" )}}{{( pgettext "Translations" "header" )}}
{{ .Name }} + {{ range .Translations }} + {{ .Endonym }} + {{ end }} +
+ {{ else -}} +

{{( gettext "No campsite type options added yet." )}}

+ {{- end }} +{{- end }} diff --git a/web/templates/admin/campsite/option/l10n.gohtml b/web/templates/admin/campsite/option/l10n.gohtml new file mode 100644 index 0000000..ba13a60 --- /dev/null +++ b/web/templates/admin/campsite/option/l10n.gohtml @@ -0,0 +1,35 @@ + +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite/types.optionL10nForm*/ -}} + {{printf (pgettext "Translate Campsite Type Option to %s" "title") .Locale.Endonym }} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite/types.optionL10nForm*/ -}} +
+

+ {{printf (pgettext "Translate Campsite Type Option to %s" "title") .Locale.Endonym }} +

+ {{ CSRFInput }} +
+ {{ with .Name -}} +
+ {{( pgettext "Name" "input")}} + {{( gettext "Source:" )}} {{ .Source }}
+ + {{ template "error-message" . }} +
+ {{- end }} +
+
+ +
+
+{{- end }} diff --git a/web/templates/admin/campsite/type/index.gohtml b/web/templates/admin/campsite/type/index.gohtml index 6b3aff2..459c99f 100644 --- a/web/templates/admin/campsite/type/index.gohtml +++ b/web/templates/admin/campsite/type/index.gohtml @@ -15,7 +15,8 @@ {{( pgettext "Name" "header" )}} - {{( pgettext "Translations" "campsite type" )}} + {{( pgettext "Translations" "header" )}} + {{( pgettext "Options" "header" )}} {{( pgettext "Active" "campsite type" )}} @@ -32,6 +33,7 @@ href="/admin/campsites/types/{{ $type.Slug }}/{{ .Language }}">{{ .Endonym }} {{ end }} + {{( pgettext "Edit Options" "action" )}} {{ if .Active }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }} {{- end }} diff --git a/web/templates/admin/home/index.gohtml b/web/templates/admin/home/index.gohtml index e7aa246..f8759c6 100644 --- a/web/templates/admin/home/index.gohtml +++ b/web/templates/admin/home/index.gohtml @@ -16,8 +16,8 @@ {{( pgettext "Image" "header" )}} {{( pgettext "Caption" "header" )}} - {{( pgettext "Translations" "campsite type" )}} - {{( pgettext "Actions" "campsite type" )}} + {{( pgettext "Translations" "header" )}} + {{( pgettext "Actions" "header" )}} diff --git a/web/templates/admin/season/index.gohtml b/web/templates/admin/season/index.gohtml index 31539a5..660e0fd 100644 --- a/web/templates/admin/season/index.gohtml +++ b/web/templates/admin/season/index.gohtml @@ -16,7 +16,7 @@ {{( pgettext "Color" "header" )}} {{( pgettext "Name" "header" )}} - {{( pgettext "Translations" "campsite type" )}} + {{( pgettext "Translations" "header" )}} {{( pgettext "Active" "season" )}} diff --git a/web/templates/admin/services/index.gohtml b/web/templates/admin/services/index.gohtml index 8408c31..70f2dbd 100644 --- a/web/templates/admin/services/index.gohtml +++ b/web/templates/admin/services/index.gohtml @@ -16,8 +16,8 @@ {{( pgettext "Image" "header" )}} {{( pgettext "Caption" "header" )}} - {{( pgettext "Translations" "campsite type" )}} - {{( pgettext "Actions" "campsite type" )}} + {{( pgettext "Translations" "header" )}} + {{( pgettext "Actions" "header" )}} @@ -57,8 +57,8 @@ {{( pgettext "Service" "header" )}} - {{( pgettext "Translations" "campsite type" )}} - {{( pgettext "Actions" "campsite type" )}} + {{( pgettext "Translations" "header" )}} + {{( pgettext "Actions" "header" )}} diff --git a/web/templates/public/campsite/type.gohtml b/web/templates/public/campsite/type.gohtml index aaf071f..9d4aa70 100644 --- a/web/templates/public/campsite/type.gohtml +++ b/web/templates/public/campsite/type.gohtml @@ -24,6 +24,9 @@ {{ .SeasonName }}
{{ printf (gettext "%s €/night") .PricePerNight }}
+ {{ range .Options }} +
{{ printf (gettext "%s: %s €/night") .OptionName .PricePerNight }}
+ {{- end }} {{ if gt .MinNights 1 -}}
{{ printf (gettext "*Minimum %d nights per stay") .MinNights }}
{{- end }}