Add campsite type options, mainly for plots

This commit is contained in:
jordi fita mas 2023-10-06 13:26:01 +02:00
parent 0611e95ea1
commit 9293a341ef
44 changed files with 2223 additions and 158 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

227
po/ca.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 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 <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\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 "Lentorn"
@ -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 lopció del tipus dallotjament"
#: 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 dallotjament"
#: 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 dallotjament"
#: 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 sha afegit cap opció al tipus dallotjament 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 dallotjament 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 dallotjament"
#: 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 sha afegit cap tipus dallotjament 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 dallotjament"
#: 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"

227
po/es.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 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 <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -90,3 +90,10 @@ to_price [roles schema_camper] 2023-10-01T16:30:40Z jordi fita mas <jordi@tandem
set_campsite_type_cost [roles schema_camper campsite_type_cost parse_price] 2023-10-01T17:51:23Z jordi fita mas <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add relation for season translations
translate_season [roles schema_camper season season_i18n] 2023-10-03T18:37:19Z jordi fita mas <jordi@tandem.blog> # Add function to translate seasons
campsite_type_option [roles schema_camper campsite_type user_profile] 2023-10-05T16:19:03Z jordi fita mas <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add function to edit campsite type options

View File

@ -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', '<p>A</p>', true, false, 4)
, (4, 1, '9ae5cf87-cd69-4541-b5a5-75f937cc9e58', 2, 'Type B', '<p>B</p>', 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;

View File

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

View File

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

View File

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

View File

@ -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', '<p>A</p>', 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;

View File

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

View File

@ -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', '<p>A</p>', 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,85 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/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*/ -}}
<form
{{ if .ID }}
data-hx-put="/admin/campsites/types/{{ .TypeSlug }}/options/{{ .ID }}"
{{ else }}
action="/admin/campsites/types/{{ .TypeSlug }}/options" method="post"
{{ end }}
>
<h2>
{{ if .ID }}
{{( pgettext "Edit Campsite Type Option" "title" )}}
{{ else }}
{{( pgettext "New Campsite Type Option" "title" )}}
{{ end }}
</h2>
{{ CSRFInput }}
<fieldset>
{{ with .Name -}}
<label>
{{( pgettext "Name" "input")}}<br>
<input type="text" name="{{ .Name }}" value="{{ .Val }}"
required {{ template "error-attrs" . }}><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .Min -}}
<label>
{{( pgettext "Minimum" "input")}}<br>
<input type="number" name="{{ .Name }}" value="{{ .Val }}" min="0"
required {{ template "error-attrs" . }}><br>
{{ template "error-message" . }}
</label>
{{- end }}
{{ with .Max -}}
<label>
{{( pgettext "Maximum" "input")}}<br>
<input type="number" name="{{ .Name }}" value="{{ .Val }}" min="0"
required {{ template "error-attrs" . }}><br>
{{ template "error-message" . }}
</label>
{{- end }}
{{ with .Prices }}
<fieldset>
<legend>{{( pgettext "Prices" "title" )}}</legend>
{{ range . }}
<fieldset>
<legend>{{ .SeasonName }}</legend>
{{ with .PricePerNight -}}
<label>
{{( pgettext "Price per night" "input")}}<br>
<input type="number" name="{{ .Name }}" value="{{ .Val }}" min="0" step="0.01"
required {{ template "error-attrs" . }}><br>
{{ template "error-message" . }}
</label>
{{- end }}
</fieldset>
{{- end }}
</fieldset>
{{- end }}
</fieldset>
<footer>
<button type="submit">
{{ if .ID }}
{{( pgettext "Update" "action" )}}
{{ else }}
{{( pgettext "Add" "action" )}}
{{ end }}
</button>
</footer>
</form>
{{- end }}

View File

@ -0,0 +1,41 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Campsite Type Options" "title" )}}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite/types.optionIndex*/ -}}
<a href="/admin/campsites/types/{{ .TypeSlug }}/options/new">{{( pgettext "Add Option" "action" )}}</a>
<h2>{{( pgettext "Campsite Type Options" "title" )}}</h2>
{{ if .Options -}}
<table>
<thead>
<tr>
<th scope="col">{{( pgettext "Name" "header" )}}</th>
<th scope="col">{{( pgettext "Translations" "header" )}}</th>
</tr>
</thead>
<tbody>
{{ range .Options -}}
<tr>
<td><a href="{{ .URL }}">{{ .Name }}</a></td>
<td>
{{ range .Translations }}
<a
{{ if .Missing }}
class="missing-translation"
{{ end }}
href="{{ .URL }}">{{ .Endonym }}</a>
{{ end }}
</td>
</tr>
{{- end }}
</tbody>
</table>
{{ else -}}
<p>{{( gettext "No campsite type options added yet." )}}</p>
{{- end }}
{{- end }}

View File

@ -0,0 +1,35 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/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*/ -}}
<form data-hx-put="/admin/campsites/types/{{ .TypeSlug }}/options/{{ .ID }}/{{ .Locale.Language }}">
<h2>
{{printf (pgettext "Translate Campsite Type Option to %s" "title") .Locale.Endonym }}
</h2>
{{ CSRFInput }}
<fieldset>
{{ with .Name -}}
<fieldset>
<legend>{{( pgettext "Name" "input")}}</legend>
{{( gettext "Source:" )}} {{ .Source }}<br>
<label>
{{( pgettext "Translation:" "input" )}}
<input type="text" name="{{ .Name }}" value="{{ .Val }}"
required {{ template "error-attrs" . }}><br>
</label>
{{ template "error-message" . }}
</fieldset>
{{- end }}
</fieldset>
<footer>
<button type="submit">{{( pgettext "Translate" "action" )}}</button>
</footer>
</form>
{{- end }}

View File

@ -15,7 +15,8 @@
<thead>
<tr>
<th scope="col">{{( pgettext "Name" "header" )}}</th>
<th scope="col">{{( pgettext "Translations" "campsite type" )}}</th>
<th scope="col">{{( pgettext "Translations" "header" )}}</th>
<th scope="col">{{( pgettext "Options" "header" )}}</th>
<th scope="col">{{( pgettext "Active" "campsite type" )}}</th>
</tr>
</thead>
@ -32,6 +33,7 @@
href="/admin/campsites/types/{{ $type.Slug }}/{{ .Language }}">{{ .Endonym }}</a>
{{ end }}
</td>
<td><a href="/admin/campsites/types/{{ .Slug }}/options">{{( pgettext "Edit Options" "action" )}}</a></td>
<td>{{ if .Active }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }}</td>
</tr>
{{- end }}

View File

@ -16,8 +16,8 @@
<tr>
<th scope="col">{{( pgettext "Image" "header" )}}</th>
<th scope="col">{{( pgettext "Caption" "header" )}}</th>
<th scope="col">{{( pgettext "Translations" "campsite type" )}}</th>
<th scope="col">{{( pgettext "Actions" "campsite type" )}}</th>
<th scope="col">{{( pgettext "Translations" "header" )}}</th>
<th scope="col">{{( pgettext "Actions" "header" )}}</th>
</tr>
</thead>
<tbody>

View File

@ -16,7 +16,7 @@
<tr>
<th scope="col">{{( pgettext "Color" "header" )}}</th>
<th scope="col">{{( pgettext "Name" "header" )}}</th>
<th scope="col">{{( pgettext "Translations" "campsite type" )}}</th>
<th scope="col">{{( pgettext "Translations" "header" )}}</th>
<th scope="col">{{( pgettext "Active" "season" )}}</th>
</tr>
</thead>

View File

@ -16,8 +16,8 @@
<tr>
<th scope="col">{{( pgettext "Image" "header" )}}</th>
<th scope="col">{{( pgettext "Caption" "header" )}}</th>
<th scope="col">{{( pgettext "Translations" "campsite type" )}}</th>
<th scope="col">{{( pgettext "Actions" "campsite type" )}}</th>
<th scope="col">{{( pgettext "Translations" "header" )}}</th>
<th scope="col">{{( pgettext "Actions" "header" )}}</th>
</tr>
</thead>
<tbody>
@ -57,8 +57,8 @@
<thead>
<tr>
<th scope="col">{{( pgettext "Service" "header" )}}</th>
<th scope="col">{{( pgettext "Translations" "campsite type" )}}</th>
<th scope="col">{{( pgettext "Actions" "campsite type" )}}</th>
<th scope="col">{{( pgettext "Translations" "header" )}}</th>
<th scope="col">{{( pgettext "Actions" "header" )}}</th>
</tr>
</thead>
<tbody>

View File

@ -24,6 +24,9 @@
{{ .SeasonName }}
</dt>
<dd>{{ printf (gettext "%s €/night") .PricePerNight }}</dd>
{{ range .Options }}
<dd>{{ printf (gettext "%s: %s €/night") .OptionName .PricePerNight }}</dd>
{{- end }}
{{ if gt .MinNights 1 -}}
<dd>{{ printf (gettext "*Minimum %d nights per stay") .MinNights }}</dd>
{{- end }}