Add the list of features for campsite type

This commit is contained in:
jordi fita mas 2023-10-13 20:30:31 +02:00
parent d784291a04
commit 2e10966ad7
39 changed files with 1693 additions and 171 deletions

View File

@ -130,6 +130,93 @@ values (72, 'en', 'Plots', '<h3>Camp in the middle of nature</h3><p>Located on t
, (76, 'es', 'Cabañas de madera', '<h3>Alojamientos de lujo</h3><p>Ubicadas al lado montaña del camping y con vista a la naturaleza que nos rodea.</p><p>Dos cabañas de madera maciza de dos plantas y con porche cubierto para disfutrar entre árboles.</p>', '<h4>Proche/terraza (13 m²)</h4><ul><li>Moblado</li></ul><h4>Planta baja (32 m²)</h4><ul><li>Sala comedor</li><li>Cocina equipada</li><li>Una habitación cama doble (150×200)</li><li>Baño completo</li></ul><h4>Planta altillo (16 m²)</h4><ul><li>Tres camas individuales (90×200)</li></ul>', '<h4>El precio incluye</h4><ul><li>Sábanas y nórdico</li><li>Cesto de bienvenida: aceite de oliva, sal, azúcar, café y té</li><li>WiFi</li><li>Plaza de aparcamiento para un coche</li><li>Kit bebé (cuna, trona y bañera) <em>bajo reserva</em></li></ul><p>* Toallas: precio extra</p>', '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p><p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p><p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>')
;
alter table campsite_type_feature alter column campsite_type_feature_id restart with 82;
insert into campsite_type_feature (campsite_type_id, icon_name, name)
values (72, 'person', 'Máx. 5 pers.')
, (72, 'area', 'Cabana 48 m² Porxo 13 ')
, (72, 'wifi', 'WiFi')
, (72, 'hvac', 'Climatització')
, (72, 'ecofriendly', 'Eco-sostenible')
, (72, 'nopet', 'Gossos NO')
, (73, 'person', 'Máx. 5 pers.')
, (73, 'area', 'Cabana 48 m² Porxo 13 ')
, (73, 'wifi', 'WiFi')
, (73, 'hvac', 'Climatització')
, (73, 'ecofriendly', 'Eco-sostenible')
, (73, 'nopet', 'Gossos NO')
, (74, 'person', 'Máx. 5 pers.')
, (74, 'area', 'Cabana 48 m² Porxo 13 ')
, (74, 'wifi', 'WiFi')
, (74, 'hvac', 'Climatització')
, (74, 'ecofriendly', 'Eco-sostenible')
, (74, 'nopet', 'Gossos NO')
, (75, 'person', 'Máx. 5 pers.')
, (75, 'area', 'Cabana 48 m² Porxo 13 ')
, (75, 'wifi', 'WiFi')
, (75, 'hvac', 'Climatització')
, (75, 'ecofriendly', 'Eco-sostenible')
, (75, 'nopet', 'Gossos NO')
, (76, 'person', 'Máx. 5 pers.')
, (76, 'area', 'Cabana 48 m² Porxo 13 ')
, (76, 'wifi', 'WiFi')
, (76, 'hvac', 'Climatització')
, (76, 'ecofriendly', 'Eco-sostenible')
, (76, 'nopet', 'Gossos NO')
;
insert into campsite_type_feature_i18n (campsite_type_feature_id, lang_tag, name)
values (82, 'en', 'Max. 5 pax')
, (82, 'es', 'Máx. 5 pers.')
, (83, 'en', 'Cabin 48 m² Porch 13 ')
, (83, 'es', 'Cabaña 48 m² Porche 13 ')
, (85, 'en', 'Climate Control')
, (85, 'es', 'Climatización')
, (86, 'en', 'Eco-sustainable')
, (86, 'es', 'Eco-sostenible')
, (87, 'en', 'Dogs NOT allowed')
, (87, 'es', 'Perros NO')
, (88, 'en', 'Max. 5 pax')
, (88, 'es', 'Máx. 5 pers.')
, (89, 'en', 'Cabin 48 m² Porch 13 ')
, (89, 'es', 'Cabaña 48 m² Porche 13 ')
, (91, 'en', 'Climate Control')
, (91, 'es', 'Climatización')
, (92, 'en', 'Eco-sustainable')
, (92, 'es', 'Eco-sostenible')
, (93, 'en', 'Dogs NOT allowed')
, (93, 'es', 'Perros NO')
, (94, 'en', 'Max. 5 pax')
, (94, 'es', 'Máx. 5 pers.')
, (95, 'en', 'Cabin 48 m² Porch 13 ')
, (95, 'es', 'Cabaña 48 m² Porche 13 ')
, (97, 'en', 'Climate Control')
, (97, 'es', 'Climatización')
, (98, 'en', 'Eco-sustainable')
, (98, 'es', 'Eco-sostenible')
, (99, 'en', 'Dogs NOT allowed')
, (99, 'es', 'Perros NO')
, (100, 'en', 'Max. 5 pax')
, (100, 'es', 'Máx. 5 pers.')
, (101, 'en', 'Cabin 48 m² Porch 13 ')
, (101, 'es', 'Cabaña 48 m² Porche 13 ')
, (103, 'en', 'Climate Control')
, (103, 'es', 'Climatización')
, (104, 'en', 'Eco-sustainable')
, (104, 'es', 'Eco-sostenible')
, (105, 'en', 'Dogs NOT allowed')
, (105, 'es', 'Perros NO')
, (106, 'en', 'Max. 5 pax')
, (106, 'es', 'Máx. 5 pers.')
, (107, 'en', 'Cabin 48 m² Porch 13 ')
, (107, 'es', 'Cabaña 48 m² Porche 13 ')
, (109, 'en', 'Climate Control')
, (109, 'es', 'Climatización')
, (110, 'en', 'Eco-sustainable')
, (110, 'es', 'Eco-sostenible')
, (111, 'en', 'Dogs NOT allowed')
, (111, 'es', 'Perros NO')
;
alter table campsite alter column campsite_id restart with 82;
select add_campsite(72, '2');
select add_campsite(72, '3');

View File

@ -0,0 +1,26 @@
-- Deploy camper:add_campsite_type_feature to pg
-- requires: roles
-- requires: schema_camper
-- requires: campsite_type_feature
-- requires: campsite_type
begin;
set search_path to camper, public;
create or replace function add_campsite_type_feature(type_slug uuid, icon_name text, name text) returns integer as
$$
insert into campsite_type_feature (campsite_type_id, icon_name, name)
select campsite_type_id, add_campsite_type_feature.icon_name, add_campsite_type_feature.name
from campsite_type
where slug = type_slug
returning campsite_type_feature_id
;
$$
language sql
;
revoke execute on function add_campsite_type_feature(uuid, text, text) from public;
grant execute on function add_campsite_type_feature(uuid, text, text) to admin;
commit;

View File

@ -5,15 +5,20 @@
begin;
insert into camper.icon (icon_name)
values ('baby')
values ('area')
, ('baby')
, ('ball')
, ('bicycle')
, ('campfire')
, ('castle')
, ('ecofriendly')
, ('fridge')
, ('hvac')
, ('information')
, ('kayak')
, ('nopet')
, ('outing')
, ('person')
, ('pool')
, ('puzzle')
, ('restaurant')

View File

@ -0,0 +1,55 @@
-- Deploy camper:campsite_type_feature to pg
-- requires: roles
-- requires: schema_camper
-- requires: campsite_type
-- requires: icon
-- requires: user_profile
begin;
set search_path to camper, public;
create table campsite_type_feature (
campsite_type_feature_id integer generated by default as identity primary key,
campsite_type_id integer not null references campsite_type,
icon_name text not null references icon,
name text not null constraint name_not_empty check(length(trim(name)) > 0)
);
grant select on campsite_type_feature to guest;
grant select on campsite_type_feature to employee;
grant select, insert, update, delete on campsite_type_feature to admin;
alter table campsite_type_feature enable row level security;
create policy guest_ok
on campsite_type_feature
for select
using (true)
;
create policy insert_to_company
on campsite_type_feature
for insert
with check (
exists (select 1 from campsite_type join user_profile using (company_id) where campsite_type.campsite_type_id = campsite_type_feature.campsite_type_id)
)
;
create policy update_company
on campsite_type_feature
for update
using (
exists (select 1 from campsite_type join user_profile using (company_id) where campsite_type.campsite_type_id = campsite_type_feature.campsite_type_id)
)
;
create policy delete_from_company
on campsite_type_feature
for delete
using (
exists (select 1 from campsite_type join user_profile using (company_id) where campsite_type.campsite_type_id = campsite_type_feature.campsite_type_id)
)
;
commit;

View File

@ -0,0 +1,22 @@
-- Deploy camper:campsite_type_feature_i18n to pg
-- requires: roles
-- requires: schema_camper
-- requires: campsite_type_feature
-- requires: language
begin;
set search_path to camper, public;
create table campsite_type_feature_i18n (
campsite_type_feature_id integer not null references campsite_type_feature,
lang_tag text not null references language,
name text not null,
primary key (campsite_type_feature_id, lang_tag)
);
grant select on table campsite_type_feature_i18n to guest;
grant select on table campsite_type_feature_i18n to employee;
grant select, insert, update, delete on table campsite_type_feature_i18n to admin;
commit;

View File

@ -0,0 +1,25 @@
-- Deploy camper:edit_campsite_type_feature to pg
-- requires: roles
-- requires: schema_camper
-- requires: campsite_type_feature
begin;
set search_path to camper, public;
create or replace function edit_campsite_type_feature(feature_id integer, icon_name text, name text) returns integer as
$$
update campsite_type_feature
set icon_name = edit_campsite_type_feature.icon_name
, name = edit_campsite_type_feature.name
where campsite_type_feature_id = feature_id
returning campsite_type_feature_id
;
$$
language sql
;
revoke execute on function edit_campsite_type_feature(integer, text, text) from public;
grant execute on function edit_campsite_type_feature(integer, text, text) to admin;
commit;

View File

@ -0,0 +1,24 @@
-- Deploy camper:translate_campsite_type_feature to pg
-- requires: roles
-- requires: schema_camper
-- requires: campsite_type_feature_i18n
begin;
set search_path to camper, public;
create or replace function translate_campsite_type_feature(feature_id integer, lang_tag text, name text) returns void as
$$
insert into campsite_type_feature_i18n (campsite_type_feature_id, lang_tag, name)
values (feature_id, lang_tag, name)
on conflict (campsite_type_feature_id, lang_tag) do update
set name = excluded.name
;
$$
language sql
;
revoke execute on function translate_campsite_type_feature(integer, text, text) from public;
grant execute on function translate_campsite_type_feature(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 "features":
h.featuresHandler(user, company, conn, f.Slug).ServeHTTP(w, r)
case "options":
h.optionsHandler(user, company, conn, f.Slug).ServeHTTP(w, r)
case "slides":

View File

@ -0,0 +1,251 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package types
import (
"context"
"net/http"
"strconv"
"github.com/jackc/pgx/v4"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database"
"dev.tandem.ws/tandem/camper/pkg/form"
httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/template"
)
func (h *AdminHandler) featuresHandler(user *auth.User, company *auth.Company, conn *database.Conn, 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:
serveFeatureIndex(w, r, user, company, conn, typeSlug)
case http.MethodPost:
addFeature(w, r, user, company, conn, typeSlug)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
}
case "new":
switch r.Method {
case http.MethodGet:
f := newFeatureForm(r.Context(), conn, typeSlug)
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 := newFeatureForm(r.Context(), conn, typeSlug)
if err := f.FillFromDatabase(r.Context(), conn, id); err != nil {
if database.ErrorIsNotFound(err) {
http.NotFound(w, r)
return
}
panic(err)
}
h.featureHandler(user, company, conn, f).ServeHTTP(w, r)
}
})
}
func (h *AdminHandler) featureHandler(user *auth.User, company *auth.Company, conn *database.Conn, f *featureForm) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
case "":
switch r.Method {
case http.MethodGet:
f.MustRender(w, r, user, company)
case http.MethodPut:
editFeature(w, r, user, company, conn, f)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
}
default:
loc, ok := h.locales.Get(head)
if !ok {
http.NotFound(w, r)
return
}
l10n := newFeatureL10nForm(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:
editFeatureL10n(w, r, user, company, conn, l10n)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
}
}
})
}
func serveFeatureIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, typeSlug string) {
features, err := collectFeatureEntries(r.Context(), conn, typeSlug)
if err != nil {
panic(err)
}
page := &featureIndex{
TypeSlug: typeSlug,
Features: features,
}
page.MustRender(w, r, user, company)
}
func collectFeatureEntries(ctx context.Context, conn *database.Conn, typeSlug string) ([]*featureEntry, error) {
rows, err := conn.Query(ctx, `
select '/admin/campsites/types/' || campsite_type.slug || '/features/' || campsite_type_feature_id
, feature.icon_name
, feature.name
, array_agg((lang_tag, endonym, not exists (select 1 from campsite_type_feature_i18n as i18n where i18n.campsite_type_feature_id = feature.campsite_type_feature_id and i18n.lang_tag = language.lang_tag)) order by endonym)
from campsite_type_feature as feature
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_feature_id
, campsite_type.slug
, feature.name
order by name
`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, typeSlug)
if err != nil {
return nil, err
}
defer rows.Close()
var features []*featureEntry
for rows.Next() {
feature := &featureEntry{}
var translations database.RecordArray
if err = rows.Scan(&feature.URL, &feature.Icon, &feature.Name, &translations); err != nil {
return nil, err
}
for _, el := range translations.Elements {
feature.Translations = append(feature.Translations, &locale.Translation{
URL: feature.URL + "/" + el.Fields[0].Get().(string),
Endonym: el.Fields[1].Get().(string),
Missing: el.Fields[2].Get().(bool),
})
}
features = append(features, feature)
}
return features, nil
}
type featureEntry struct {
URL string
Icon string
Name string
Translations []*locale.Translation
}
type featureIndex struct {
TypeSlug string
Features []*featureEntry
}
func (page *featureIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "campsite/feature/index.gohtml", page)
}
func addFeature(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, typeSlug string) {
f := newFeatureForm(r.Context(), conn, typeSlug)
processFeatureForm(w, r, user, company, f, func(ctx context.Context) (int, error) {
return conn.AddCampsiteTypeFeature(ctx, typeSlug, f.Icon.String(), f.Name.Val)
})
}
func editFeature(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *featureForm) {
processFeatureForm(w, r, user, company, f, func(ctx context.Context) (int, error) {
return conn.EditCampsiteTypeFeature(ctx, f.ID, f.Icon.String(), f.Name.Val)
})
}
func processFeatureForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, f *featureForm, act func(ctx context.Context) (int, error)) {
if ok, err := form.Handle(f, w, r, user); err != nil {
return
} else if !ok {
f.MustRender(w, r, user, company)
return
}
if _, err := act(r.Context()); err != nil {
panic(err)
}
httplib.Redirect(w, r, "/admin/campsites/types/"+f.TypeSlug+"/features", http.StatusSeeOther)
}
type featureForm struct {
ID int
TypeSlug string
Icon *form.Select
Name *form.Input
}
func newFeatureForm(ctx context.Context, conn *database.Conn, typeSlug string) *featureForm {
return &featureForm{
TypeSlug: typeSlug,
Icon: &form.Select{
Name: "icon",
Options: form.MustGetOptions(ctx, conn, "select icon_name, icon_name from icon order by 1"),
},
Name: &form.Input{
Name: "name",
},
}
}
func (f *featureForm) FillFromDatabase(ctx context.Context, conn *database.Conn, id int) error {
f.ID = id
row := conn.QueryRow(ctx, `
select array[icon_name]
, name
from campsite_type_feature
where campsite_type_feature_id = $1
`, id)
return row.Scan(&f.Icon.Selected, &f.Name.Val)
}
func (f *featureForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
f.Icon.FillValue(r)
f.Name.FillValue(r)
return nil
}
func (f *featureForm) Valid(l *locale.Locale) bool {
v := form.NewValidator(l)
v.CheckSelectedOptions(f.Icon, l.GettextNoop("Selected icon is not valid."))
if v.CheckRequired(f.Name, l.GettextNoop("Name can not be empty.")) {
v.CheckMinLength(f.Name, 1, l.GettextNoop("Name must have at least one letter."))
}
return v.AllOK
}
func (f *featureForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "campsite/feature/form.gohtml", f)
}

View File

@ -211,3 +211,60 @@ func (l10n *slideL10nForm) Valid(l *locale.Locale) bool {
v := form.NewValidator(l)
return v.AllOK
}
type featureL10nForm struct {
Locale *locale.Locale
TypeSlug string
ID int
Name *form.L10nInput
}
func newFeatureL10nForm(f *featureForm, loc *locale.Locale) *featureL10nForm {
return &featureL10nForm{
Locale: loc,
TypeSlug: f.TypeSlug,
ID: f.ID,
Name: f.Name.L10nInput(),
}
}
func (l10n *featureL10nForm) FillFromDatabase(ctx context.Context, conn *database.Conn) error {
row := conn.QueryRow(ctx, `
select coalesce(i18n.name, '') as l10n_name
from campsite_type_feature
left join campsite_type_feature_i18n as i18n on campsite_type_feature.campsite_type_feature_id = i18n.campsite_type_feature_id and i18n.lang_tag = $1
where campsite_type_feature.campsite_type_feature_id = $2
`, l10n.Locale.Language, l10n.ID)
return row.Scan(&l10n.Name.Val)
}
func (l10n *featureL10nForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "campsite/feature/l10n.gohtml", l10n)
}
func editFeatureL10n(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, l10n *featureL10nForm) {
if ok, err := form.Handle(l10n, w, r, user); err != nil {
return
} else if !ok {
l10n.MustRender(w, r, user, company)
return
}
if err := conn.TranslateCampsiteTypeFeature(r.Context(), l10n.ID, l10n.Locale.Language, l10n.Name.Val); err != nil {
panic(err)
}
httplib.Redirect(w, r, "/admin/campsites/types/"+l10n.TypeSlug+"/features", http.StatusSeeOther)
}
func (l10n *featureL10nForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
l10n.Name.FillValue(r)
return nil
}
func (l10n *featureL10nForm) 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

@ -11,6 +11,7 @@ import (
"net/http"
"github.com/jackc/pgx/v4"
"golang.org/x/text/language"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/carousel"
@ -54,6 +55,7 @@ type publicPage struct {
Name string
Carousel []*carousel.Slide
Prices []*typePrice
Features []*typeFeature
Spiel gotemplate.HTML
Info gotemplate.HTML
Facilities gotemplate.HTML
@ -73,10 +75,25 @@ type optionPrice struct {
PricePerNight string
}
type typeFeature struct {
Icon string
Name string
}
func newPublicPage(ctx context.Context, conn *database.Conn, loc *locale.Locale, slug string) (*publicPage, error) {
prices, err := collectPrices(ctx, conn, loc.Language, slug)
if err != nil {
return nil, err
}
features, err := collectFeatures(ctx, conn, loc.Language, slug)
if err != nil {
return nil, err
}
page := &publicPage{
PublicPage: template.NewPublicPage(),
Carousel: mustCollectSlides(ctx, conn, loc, slug),
Prices: prices,
Features: features,
}
row := conn.QueryRow(ctx, `
select coalesce(i18n.name, campsite_type.name) as l10n_name
@ -93,6 +110,10 @@ func newPublicPage(ctx context.Context, conn *database.Conn, loc *locale.Locale,
return nil, err
}
return page, nil
}
func collectPrices(ctx context.Context, conn *database.Conn, language language.Tag, slug string) ([]*typePrice, error) {
rows, err := conn.Query(ctx, `
select coalesce(i18n.name, season.name) as l10n_name
, to_color(season.color)::text
@ -117,11 +138,12 @@ func newPublicPage(ctx context.Context, conn *database.Conn, loc *locale.Locale,
, season.color
, cost.min_nights
, cost.cost_per_night
`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, loc.Language, slug)
`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, language, slug)
if err != nil {
return nil, err
}
var prices []*typePrice
for rows.Next() {
price := &typePrice{}
var options database.RecordArray
@ -134,10 +156,33 @@ func newPublicPage(ctx context.Context, conn *database.Conn, loc *locale.Locale,
PricePerNight: el.Fields[1].Get().(string),
})
}
page.Prices = append(page.Prices, price)
prices = append(prices, price)
}
return prices, nil
}
func collectFeatures(ctx context.Context, conn *database.Conn, language language.Tag, slug string) ([]*typeFeature, error) {
rows, err := conn.Query(ctx, `
select feature.icon_name
, coalesce(i18n.name, feature.name) as l10n_name
from campsite_type_feature as feature
join campsite_type using (campsite_type_id)
left join campsite_type_feature_i18n as i18n on feature.campsite_type_feature_id = i18n.campsite_type_feature_id and i18n.lang_tag = $1
where campsite_type.slug = $2
`, language, slug)
if err != nil {
return nil, err
}
return page, nil
var features []*typeFeature
for rows.Next() {
feature := &typeFeature{}
if err := rows.Scan(&feature.Icon, &feature.Name); err != nil {
return nil, err
}
features = append(features, feature)
}
return features, nil
}
func (p *publicPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {

View File

@ -103,6 +103,12 @@ func (c *Conn) GetBool(ctx context.Context, sql string, args ...interface{}) (bo
return result, nil
}
func (c *Conn) GetInt(ctx context.Context, sql string, args ...interface{}) (int, error) {
var result int
err := c.QueryRow(ctx, sql, args...).Scan(&result)
return result, err
}
func (c *Conn) GetBytes(ctx context.Context, sql string, args ...interface{}) ([]byte, error) {
var result []byte
err := c.QueryRow(ctx, sql, args...).Scan(&result)

View File

@ -45,3 +45,16 @@ func (c *Conn) TranslateCampsiteTypeOption(ctx context.Context, id int, langTag
_, err := c.Exec(ctx, "select translate_campsite_type_option($1, $2, $3)", id, langTag, name)
return err
}
func (c *Conn) AddCampsiteTypeFeature(ctx context.Context, typeSlug string, iconName string, name string) (int, error) {
return c.GetInt(ctx, "select add_campsite_type_feature($1, $2, $3)", typeSlug, iconName, name)
}
func (c *Conn) EditCampsiteTypeFeature(ctx context.Context, id int, iconName string, name string) (int, error) {
return c.GetInt(ctx, "select edit_campsite_type_feature($1, $2, $3)", id, iconName, name)
}
func (c *Conn) TranslateCampsiteTypeFeature(ctx context.Context, id int, langTag language.Tag, name string) error {
_, err := c.Exec(ctx, "select translate_campsite_type_feature($1, $2, $3)", id, langTag, name)
return err
}

216
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-13 18:05+0200\n"
"POT-Creation-Date: 2023-10-13 20:28+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"
@ -84,29 +84,34 @@ msgctxt "title"
msgid "Prices"
msgstr "Preus"
#: web/templates/public/campsite/type.gohtml:47
#: web/templates/public/campsite/type.gohtml:48
msgid "%s €/night"
msgstr "%s €/nit"
#: web/templates/public/campsite/type.gohtml:49
#: web/templates/public/campsite/type.gohtml:50
msgid "%s: %s €/night"
msgstr "%s: %s €/nit"
#: web/templates/public/campsite/type.gohtml:52
#: web/templates/public/campsite/type.gohtml:53
msgid "*Minimum %d nights per stay"
msgstr "*Mínim %d nits per estada"
#: web/templates/public/campsite/type.gohtml:61
#: web/templates/public/campsite/type.gohtml:62
msgctxt "title"
msgid "Features"
msgstr "Característiques"
#: web/templates/public/campsite/type.gohtml:73
msgctxt "title"
msgid "Info"
msgstr "Informació"
#: web/templates/public/campsite/type.gohtml:65
#: web/templates/public/campsite/type.gohtml:77
msgctxt "title"
msgid "Facilities"
msgstr "Equipaments"
#: web/templates/public/campsite/type.gohtml:69
#: web/templates/public/campsite/type.gohtml:81
msgctxt "title"
msgid "Description"
msgstr "Descripció"
@ -225,6 +230,7 @@ msgid "Caption"
msgstr "Llegenda"
#: web/templates/admin/carousel/form.gohtml:47
#: web/templates/admin/campsite/feature/form.gohtml:62
#: web/templates/admin/campsite/carousel/form.gohtml:47
#: web/templates/admin/campsite/form.gohtml:70
#: web/templates/admin/campsite/option/form.gohtml:78
@ -237,6 +243,7 @@ msgid "Update"
msgstr "Actualitza"
#: web/templates/admin/carousel/form.gohtml:49
#: web/templates/admin/campsite/feature/form.gohtml:64
#: web/templates/admin/campsite/carousel/form.gohtml:49
#: web/templates/admin/campsite/form.gohtml:72
#: web/templates/admin/campsite/option/form.gohtml:80
@ -254,6 +261,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/feature/l10n.gohtml:21
#: web/templates/admin/campsite/carousel/l10n.gohtml:21
#: web/templates/admin/campsite/option/l10n.gohtml:21
#: web/templates/admin/campsite/type/l10n.gohtml:21
@ -268,6 +276,7 @@ msgid "Source:"
msgstr "Origen:"
#: web/templates/admin/carousel/l10n.gohtml:23
#: web/templates/admin/campsite/feature/l10n.gohtml:23
#: web/templates/admin/campsite/carousel/l10n.gohtml:23
#: web/templates/admin/campsite/option/l10n.gohtml:23
#: web/templates/admin/campsite/type/l10n.gohtml:23
@ -283,6 +292,7 @@ msgid "Translation:"
msgstr "Traducció:"
#: web/templates/admin/carousel/l10n.gohtml:32
#: web/templates/admin/campsite/feature/l10n.gohtml:32
#: web/templates/admin/campsite/carousel/l10n.gohtml:32
#: web/templates/admin/campsite/option/l10n.gohtml:32
#: web/templates/admin/campsite/type/l10n.gohtml:84
@ -292,6 +302,80 @@ msgctxt "action"
msgid "Translate"
msgstr "Tradueix"
#: web/templates/admin/campsite/feature/form.gohtml:8
#: web/templates/admin/campsite/feature/form.gohtml:25
msgctxt "title"
msgid "Edit Campsite Type Feature"
msgstr "Edició de les característiques del tipus dallotjament"
#: web/templates/admin/campsite/feature/form.gohtml:10
#: web/templates/admin/campsite/feature/form.gohtml:27
msgctxt "title"
msgid "New Campsite Type Feature"
msgstr "Nova característica de tipus dallotjament"
#: web/templates/admin/campsite/feature/form.gohtml:34
#: web/templates/admin/services/form.gohtml:34
msgctxt "input"
msgid "Icon"
msgstr "Icona"
#: web/templates/admin/campsite/feature/form.gohtml:52
#: web/templates/admin/campsite/feature/l10n.gohtml:20
#: 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/feature/index.gohtml:6
#: web/templates/admin/campsite/feature/index.gohtml:12
msgctxt "title"
msgid "Campsite Type Features"
msgstr "Característiques del tipus dallotjaments"
#: web/templates/admin/campsite/feature/index.gohtml:11
msgctxt "action"
msgid "Add Feature"
msgstr "Afegeix característica"
#: web/templates/admin/campsite/feature/index.gohtml:17
#: 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/feature/index.gohtml:18
#: web/templates/admin/campsite/carousel/index.gohtml:19
#: 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/feature/index.gohtml:39
msgid "No campsite type features added yet."
msgstr "No sha afegit cap característica al tipus dallotjament encara."
#: web/templates/admin/campsite/feature/l10n.gohtml:7
#: web/templates/admin/campsite/feature/l10n.gohtml:14
msgctxt "title"
msgid "Translate Campsite Type Feature to %s"
msgstr "Traducció de la característica del tipus dallotjament a %s"
#: web/templates/admin/campsite/carousel/form.gohtml:8
#: web/templates/admin/campsite/carousel/form.gohtml:25
msgctxt "title"
@ -331,17 +415,6 @@ msgctxt "header"
msgid "Caption"
msgstr "Llegenda"
#: web/templates/admin/campsite/carousel/index.gohtml:19
#: 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/carousel/index.gohtml:20
#: web/templates/admin/services/index.gohtml:20
#: web/templates/admin/services/index.gohtml:61
@ -420,19 +493,6 @@ 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"
@ -460,13 +520,6 @@ 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:39
msgid "No campsite type options added yet."
msgstr "No sha afegit cap opció al tipus dallotjament encara."
@ -493,13 +546,13 @@ msgid "Type"
msgstr "Tipus"
#: web/templates/admin/campsite/index.gohtml:28
#: web/templates/admin/campsite/type/index.gohtml:39
#: web/templates/admin/campsite/type/index.gohtml:47
#: 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:39
#: web/templates/admin/campsite/type/index.gohtml:47
#: web/templates/admin/season/index.gohtml:39
msgid "No"
msgstr "No"
@ -521,7 +574,7 @@ msgid "New Campsite Type"
msgstr "Nou tipus dallotjament"
#: web/templates/admin/campsite/type/form.gohtml:37
#: web/templates/admin/campsite/type/index.gohtml:21
#: web/templates/admin/campsite/type/index.gohtml:22
msgctxt "campsite type"
msgid "Active"
msgstr "Actiu"
@ -581,25 +634,35 @@ msgstr "Afegeix tipus"
#: web/templates/admin/campsite/type/index.gohtml:19
msgctxt "header"
msgid "Features"
msgstr "Característiques"
#: web/templates/admin/campsite/type/index.gohtml:20
msgctxt "header"
msgid "Options"
msgstr "Opcions"
#: web/templates/admin/campsite/type/index.gohtml:20
#: web/templates/admin/campsite/type/index.gohtml:21
msgctxt "header"
msgid "Carousel"
msgstr "Carrusel"
#: web/templates/admin/campsite/type/index.gohtml:37
#: web/templates/admin/campsite/type/index.gohtml:39
msgctxt "action"
msgid "Edit Features"
msgstr "Edita les característiques"
#: web/templates/admin/campsite/type/index.gohtml:42
msgctxt "action"
msgid "Edit Options"
msgstr "Edita les opcions"
#: web/templates/admin/campsite/type/index.gohtml:38
#: web/templates/admin/campsite/type/index.gohtml:45
msgctxt "action"
msgid "Edit Carousel"
msgstr "Edita el carrusel"
#: web/templates/admin/campsite/type/index.gohtml:45
#: web/templates/admin/campsite/type/index.gohtml:53
msgid "No campsite types added yet."
msgstr "No sha afegit cap tipus dallotjament encara."
@ -744,11 +807,6 @@ msgctxt "title"
msgid "New Service"
msgstr "Nou servei"
#: web/templates/admin/services/form.gohtml:34
msgctxt "input"
msgid "Icon"
msgstr "Icona"
#: web/templates/admin/services/index.gohtml:6
#: web/templates/admin/layout.gohtml:52
msgctxt "title"
@ -1021,8 +1079,9 @@ msgid "Automatic"
msgstr "Automàtic"
#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:87
#: pkg/campsite/types/l10n.go:142 pkg/campsite/types/option.go:336
#: pkg/campsite/types/admin.go:413 pkg/season/l10n.go:69
#: pkg/campsite/types/l10n.go:144 pkg/campsite/types/l10n.go:268
#: pkg/campsite/types/option.go:340 pkg/campsite/types/feature.go:243
#: pkg/campsite/types/admin.go:415 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."
@ -1043,85 +1102,90 @@ msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida."
msgid "Access forbidden"
msgstr "Accés prohibit"
#: pkg/campsite/types/option.go:337 pkg/campsite/types/admin.go:414
#: pkg/campsite/types/option.go:341 pkg/campsite/types/feature.go:244
#: pkg/campsite/types/admin.go:416
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
#: pkg/campsite/types/option.go:344
msgid "Minimum can not be empty."
msgstr "No podeu deixar el mínim en blanc."
#: pkg/campsite/types/option.go:341
#: pkg/campsite/types/option.go:345
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
#: pkg/campsite/types/option.go:347
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
#: pkg/campsite/types/option.go:350
msgid "Maximum can not be empty."
msgstr "No podeu deixar el màxim en blanc."
#: pkg/campsite/types/option.go:347
#: pkg/campsite/types/option.go:351
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
#: pkg/campsite/types/option.go:353
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:427
#: pkg/campsite/types/option.go:357 pkg/campsite/types/admin.go:429
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:428
#: pkg/campsite/types/option.go:358 pkg/campsite/types/admin.go:430
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:429
#: pkg/campsite/types/option.go:359 pkg/campsite/types/admin.go:431
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:289
#: pkg/campsite/types/feature.go:242 pkg/services/admin.go:265
msgid "Selected icon is not valid."
msgstr "La icona escollida no és vàlida."
#: pkg/campsite/types/admin.go:291
msgctxt "input"
msgid "Cover image"
msgstr "Imatge de portada"
#: pkg/campsite/types/admin.go:290
#: pkg/campsite/types/admin.go:292
msgctxt "action"
msgid "Set campsite type cover"
msgstr "Estableix la portada del tipus dallotjament"
#: pkg/campsite/types/admin.go:416
#: pkg/campsite/types/admin.go:418
msgid "Cover image can not be empty."
msgstr "No podeu deixar la imatge de portada en blanc."
#: pkg/campsite/types/admin.go:417
#: pkg/campsite/types/admin.go:419
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:421
#: pkg/campsite/types/admin.go:423
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:422
#: pkg/campsite/types/admin.go:424
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:423
#: pkg/campsite/types/admin.go:425
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:432
#: pkg/campsite/types/admin.go:434
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:433
#: pkg/campsite/types/admin.go:435
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:434
#: pkg/campsite/types/admin.go:436
msgid "Minimum number of nights must be one or greater."
msgstr "El número mínim de nits no pot ser zero."
@ -1222,10 +1286,6 @@ msgstr "No podeu deixar la data de fi en blanc."
msgid "End date must be a valid date."
msgstr "La data de fi ha de ser una data vàlida."
#: pkg/services/admin.go:265
msgid "Selected icon is not valid."
msgstr "La icona escollida no és vàlida."
#: pkg/company/admin.go:186
msgid "Selected country is not valid."
msgstr "El país escollit no és vàlid."
@ -1306,10 +1366,6 @@ msgstr "No podeu deixar el nom del fitxer en blanc."
#~ msgid "Pricing"
#~ msgstr "Preus"
#~ msgctxt "title"
#~ msgid "Features"
#~ msgstr "Característiques"
#~ msgctxt "input"
#~ msgid "Features"
#~ msgstr "Característiques"

218
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-13 18:05+0200\n"
"POT-Creation-Date: 2023-10-13 20:28+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"
@ -84,29 +84,34 @@ msgctxt "title"
msgid "Prices"
msgstr "Precios"
#: web/templates/public/campsite/type.gohtml:47
#: web/templates/public/campsite/type.gohtml:48
msgid "%s €/night"
msgstr "%s €/noche"
#: web/templates/public/campsite/type.gohtml:49
#: web/templates/public/campsite/type.gohtml:50
msgid "%s: %s €/night"
msgstr ":%s: %s €/noche"
msgstr "%s: %s €/noche"
#: web/templates/public/campsite/type.gohtml:52
#: web/templates/public/campsite/type.gohtml:53
msgid "*Minimum %d nights per stay"
msgstr "*Mínimo %d noches por estancia"
#: web/templates/public/campsite/type.gohtml:61
#: web/templates/public/campsite/type.gohtml:62
msgctxt "title"
msgid "Features"
msgstr "Características"
#: web/templates/public/campsite/type.gohtml:73
msgctxt "title"
msgid "Info"
msgstr "Información"
#: web/templates/public/campsite/type.gohtml:65
#: web/templates/public/campsite/type.gohtml:77
msgctxt "title"
msgid "Facilities"
msgstr "Equipamento"
#: web/templates/public/campsite/type.gohtml:69
#: web/templates/public/campsite/type.gohtml:81
msgctxt "title"
msgid "Description"
msgstr "Descripción"
@ -225,6 +230,7 @@ msgid "Caption"
msgstr "Leyenda"
#: web/templates/admin/carousel/form.gohtml:47
#: web/templates/admin/campsite/feature/form.gohtml:62
#: web/templates/admin/campsite/carousel/form.gohtml:47
#: web/templates/admin/campsite/form.gohtml:70
#: web/templates/admin/campsite/option/form.gohtml:78
@ -237,6 +243,7 @@ msgid "Update"
msgstr "Actualizar"
#: web/templates/admin/carousel/form.gohtml:49
#: web/templates/admin/campsite/feature/form.gohtml:64
#: web/templates/admin/campsite/carousel/form.gohtml:49
#: web/templates/admin/campsite/form.gohtml:72
#: web/templates/admin/campsite/option/form.gohtml:80
@ -254,6 +261,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/feature/l10n.gohtml:21
#: web/templates/admin/campsite/carousel/l10n.gohtml:21
#: web/templates/admin/campsite/option/l10n.gohtml:21
#: web/templates/admin/campsite/type/l10n.gohtml:21
@ -268,6 +276,7 @@ msgid "Source:"
msgstr "Origen:"
#: web/templates/admin/carousel/l10n.gohtml:23
#: web/templates/admin/campsite/feature/l10n.gohtml:23
#: web/templates/admin/campsite/carousel/l10n.gohtml:23
#: web/templates/admin/campsite/option/l10n.gohtml:23
#: web/templates/admin/campsite/type/l10n.gohtml:23
@ -283,6 +292,7 @@ msgid "Translation:"
msgstr "Traducción"
#: web/templates/admin/carousel/l10n.gohtml:32
#: web/templates/admin/campsite/feature/l10n.gohtml:32
#: web/templates/admin/campsite/carousel/l10n.gohtml:32
#: web/templates/admin/campsite/option/l10n.gohtml:32
#: web/templates/admin/campsite/type/l10n.gohtml:84
@ -292,6 +302,80 @@ msgctxt "action"
msgid "Translate"
msgstr "Traducir"
#: web/templates/admin/campsite/feature/form.gohtml:8
#: web/templates/admin/campsite/feature/form.gohtml:25
msgctxt "title"
msgid "Edit Campsite Type Feature"
msgstr "Edición de las características del tipo de alojamiento"
#: web/templates/admin/campsite/feature/form.gohtml:10
#: web/templates/admin/campsite/feature/form.gohtml:27
msgctxt "title"
msgid "New Campsite Type Feature"
msgstr "Nueva característica del tipo de alojamiento"
#: web/templates/admin/campsite/feature/form.gohtml:34
#: web/templates/admin/services/form.gohtml:34
msgctxt "input"
msgid "Icon"
msgstr "Icono"
#: web/templates/admin/campsite/feature/form.gohtml:52
#: web/templates/admin/campsite/feature/l10n.gohtml:20
#: 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/feature/index.gohtml:6
#: web/templates/admin/campsite/feature/index.gohtml:12
msgctxt "title"
msgid "Campsite Type Features"
msgstr "Características del tipo de alojamiento"
#: web/templates/admin/campsite/feature/index.gohtml:11
msgctxt "action"
msgid "Add Feature"
msgstr "Añadir características"
#: web/templates/admin/campsite/feature/index.gohtml:17
#: 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/feature/index.gohtml:18
#: web/templates/admin/campsite/carousel/index.gohtml:19
#: 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/feature/index.gohtml:39
msgid "No campsite type features added yet."
msgstr "No se ha añadido ninguna característica al tipo de alojamiento todavía."
#: web/templates/admin/campsite/feature/l10n.gohtml:7
#: web/templates/admin/campsite/feature/l10n.gohtml:14
msgctxt "title"
msgid "Translate Campsite Type Feature to %s"
msgstr "Traducción de la característica del tipo de alojamiento a %s"
#: web/templates/admin/campsite/carousel/form.gohtml:8
#: web/templates/admin/campsite/carousel/form.gohtml:25
msgctxt "title"
@ -331,17 +415,6 @@ msgctxt "header"
msgid "Caption"
msgstr "Leyenda"
#: web/templates/admin/campsite/carousel/index.gohtml:19
#: 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/carousel/index.gohtml:20
#: web/templates/admin/services/index.gohtml:20
#: web/templates/admin/services/index.gohtml:61
@ -420,19 +493,6 @@ 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"
@ -460,13 +520,6 @@ 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:39
msgid "No campsite type options added yet."
msgstr "No se ha añadido ninguna opció al tipo de alojamiento todavía."
@ -493,13 +546,13 @@ msgid "Type"
msgstr "Tipo"
#: web/templates/admin/campsite/index.gohtml:28
#: web/templates/admin/campsite/type/index.gohtml:39
#: web/templates/admin/campsite/type/index.gohtml:47
#: 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:39
#: web/templates/admin/campsite/type/index.gohtml:47
#: web/templates/admin/season/index.gohtml:39
msgid "No"
msgstr "No"
@ -521,7 +574,7 @@ msgid "New Campsite Type"
msgstr "Nuevo tipo de alojamiento"
#: web/templates/admin/campsite/type/form.gohtml:37
#: web/templates/admin/campsite/type/index.gohtml:21
#: web/templates/admin/campsite/type/index.gohtml:22
msgctxt "campsite type"
msgid "Active"
msgstr "Activo"
@ -581,25 +634,35 @@ msgstr "Añadir tipo"
#: web/templates/admin/campsite/type/index.gohtml:19
msgctxt "header"
msgid "Features"
msgstr "Características"
#: web/templates/admin/campsite/type/index.gohtml:20
msgctxt "header"
msgid "Options"
msgstr "Opciones"
#: web/templates/admin/campsite/type/index.gohtml:20
#: web/templates/admin/campsite/type/index.gohtml:21
msgctxt "header"
msgid "Carousel"
msgstr "Carrusel"
#: web/templates/admin/campsite/type/index.gohtml:37
#: web/templates/admin/campsite/type/index.gohtml:39
msgctxt "action"
msgid "Edit Features"
msgstr "Editar las características"
#: web/templates/admin/campsite/type/index.gohtml:42
msgctxt "action"
msgid "Edit Options"
msgstr "Editar opciones"
#: web/templates/admin/campsite/type/index.gohtml:38
#: web/templates/admin/campsite/type/index.gohtml:45
msgctxt "action"
msgid "Edit Carousel"
msgstr "Editar el carrusel"
#: web/templates/admin/campsite/type/index.gohtml:45
#: web/templates/admin/campsite/type/index.gohtml:53
msgid "No campsite types added yet."
msgstr "No se ha añadido ningún tipo de alojamiento todavía."
@ -744,11 +807,6 @@ msgctxt "title"
msgid "New Service"
msgstr "Nuevo servicio"
#: web/templates/admin/services/form.gohtml:34
msgctxt "input"
msgid "Icon"
msgstr "Icono"
#: web/templates/admin/services/index.gohtml:6
#: web/templates/admin/layout.gohtml:52
msgctxt "title"
@ -1021,8 +1079,9 @@ msgid "Automatic"
msgstr "Automático"
#: pkg/app/user.go:249 pkg/campsite/types/l10n.go:87
#: pkg/campsite/types/l10n.go:142 pkg/campsite/types/option.go:336
#: pkg/campsite/types/admin.go:413 pkg/season/l10n.go:69
#: pkg/campsite/types/l10n.go:144 pkg/campsite/types/l10n.go:268
#: pkg/campsite/types/option.go:340 pkg/campsite/types/feature.go:243
#: pkg/campsite/types/admin.go:415 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."
@ -1043,85 +1102,90 @@ msgstr "El archivo tiene que ser una imagen PNG o JPEG válida."
msgid "Access forbidden"
msgstr "Acceso prohibido"
#: pkg/campsite/types/option.go:337 pkg/campsite/types/admin.go:414
#: pkg/campsite/types/option.go:341 pkg/campsite/types/feature.go:244
#: pkg/campsite/types/admin.go:416
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
#: pkg/campsite/types/option.go:344
msgid "Minimum can not be empty."
msgstr "No podéis dejar el mínimo en blanco."
#: pkg/campsite/types/option.go:341
#: pkg/campsite/types/option.go:345
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
#: pkg/campsite/types/option.go:347
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
#: pkg/campsite/types/option.go:350
msgid "Maximum can not be empty."
msgstr "No podéis dejar el máxmimo en blanco."
#: pkg/campsite/types/option.go:347
#: pkg/campsite/types/option.go:351
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
#: pkg/campsite/types/option.go:353
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:427
#: pkg/campsite/types/option.go:357 pkg/campsite/types/admin.go:429
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:428
#: pkg/campsite/types/option.go:358 pkg/campsite/types/admin.go:430
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:429
#: pkg/campsite/types/option.go:359 pkg/campsite/types/admin.go:431
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:289
#: pkg/campsite/types/feature.go:242 pkg/services/admin.go:265
msgid "Selected icon is not valid."
msgstr "El icono escogido no es válido."
#: pkg/campsite/types/admin.go:291
msgctxt "input"
msgid "Cover image"
msgstr "Imagen de portada"
#: pkg/campsite/types/admin.go:290
#: pkg/campsite/types/admin.go:292
msgctxt "action"
msgid "Set campsite type cover"
msgstr "Establecer la portada del tipo de alojamiento"
#: pkg/campsite/types/admin.go:416
#: pkg/campsite/types/admin.go:418
msgid "Cover image can not be empty."
msgstr "No podéis dejar la imagen de portada en blanco."
#: pkg/campsite/types/admin.go:417
#: pkg/campsite/types/admin.go:419
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:421
#: pkg/campsite/types/admin.go:423
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:422
#: pkg/campsite/types/admin.go:424
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:423
#: pkg/campsite/types/admin.go:425
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:432
#: pkg/campsite/types/admin.go:434
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:433
#: pkg/campsite/types/admin.go:435
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:434
#: pkg/campsite/types/admin.go:436
msgid "Minimum number of nights must be one or greater."
msgstr "El número mínimo de noches no puede ser cero."
@ -1222,10 +1286,6 @@ msgstr "No podéis dejar la fecha final en blanco."
msgid "End date must be a valid date."
msgstr "La fecha final tiene que ser una fecha válida."
#: pkg/services/admin.go:265
msgid "Selected icon is not valid."
msgstr "El icono escogido no es válido."
#: pkg/company/admin.go:186
msgid "Selected country is not valid."
msgstr "El país escogido no es válido."
@ -1306,10 +1366,6 @@ msgstr "No podéis dejar el nombre del archivo en blanco."
#~ msgid "Pricing"
#~ msgstr "Precios"
#~ msgctxt "title"
#~ msgid "Features"
#~ msgstr "Características"
#~ msgctxt "input"
#~ msgid "Features"
#~ msgstr "Características"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -104,3 +104,8 @@ campsite_type_carousel_i18n [roles schema_camper campsite_type_carousel language
add_campsite_type_carousel_slide [roles schema_camper campsite_type_carousel campsite_type] 2023-10-09T17:59:49Z jordi fita mas <jordi@tandem.blog> # Add function to create slides for the campsite type carousel
translate_campsite_type_carousel_slide [roles schema_camper campsite_type campsite_type_carousel_i18n] 2023-10-09T18:17:13Z jordi fita mas <jordi@tandem.blog> # Add function to translate a campsite type slide
remove_campsite_type_carousel_slide [roles schema_camper campsite_type_carousel campsite_type_carousel_i18n] 2023-10-09T18:26:49Z jordi fita mas <jordi@tandem.blog> # Add function to remove campsite type slides
campsite_type_feature [roles schema_camper campsite_type icon user_profile] 2023-10-13T16:14:27Z jordi fita mas <jordi@tandem.blog> # Add the relation of campsite type feature
campsite_type_feature_i18n [roles schema_camper campsite_type_feature language] 2023-10-13T16:29:07Z jordi fita mas <jordi@tandem.blog> # Add relation for campsite_type_feature internationalization
add_campsite_type_feature [roles schema_camper campsite_type_feature campsite_type] 2023-10-13T16:26:04Z jordi fita mas <jordi@tandem.blog> # Add function to create new campsite type features
edit_campsite_type_feature [roles schema_camper campsite_type_feature] 2023-10-13T16:37:43Z jordi fita mas <jordi@tandem.blog> # Add function to update campsite type features
translate_campsite_type_feature [roles schema_camper campsite_type_feature_i18n] 2023-10-13T16:43:55Z jordi fita mas <jordi@tandem.blog> # Add function to translate campsite type features

View File

@ -0,0 +1,77 @@
-- Test add_campsite_type_feature
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(13);
set search_path to camper, public;
select has_function('camper', 'add_campsite_type_feature', array['uuid', 'text', 'text']);
select function_lang_is('camper', 'add_campsite_type_feature', array['uuid', 'text', 'text'], 'sql');
select function_returns('camper', 'add_campsite_type_feature', array['uuid', 'text', 'text'], 'integer');
select isnt_definer('camper', 'add_campsite_type_feature', array['uuid', 'text', 'text']);
select volatility_is('camper', 'add_campsite_type_feature', array['uuid', 'text', 'text'], 'volatile');
select function_privs_are('camper', 'add_campsite_type_feature', array ['uuid', 'text', 'text'], 'guest', array[]::text[]);
select function_privs_are('camper', 'add_campsite_type_feature', array ['uuid', 'text', 'text'], 'employee', array[]::text[]);
select function_privs_are('camper', 'add_campsite_type_feature', array ['uuid', 'text', 'text'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'add_campsite_type_feature', array ['uuid', 'text', 'text'], 'authenticator', array[]::text[]);
set client_min_messages to warning;
truncate campsite_type_feature_i18n cascade;
truncate campsite_type_feature 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_feature('87452b88-b48f-48d3-bb6c-0296de64164e', 'wifi', 'Feature 1') $$,
'Should be able to add an feature to the first campsite type'
);
select lives_ok(
$$ select add_campsite_type_feature('9ae5cf87-cd69-4541-b5a5-75f937cc9e58', 'information', 'Feature 2') $$,
'Should be able to add an feature to the second campsite type'
);
select bag_eq(
$$ select campsite_type_id, icon_name, name from campsite_type_feature $$,
$$ values (3, 'wifi', 'Feature 1')
, (4, 'information', 'Feature 2')
$$,
'Should have added all two campsite type features'
);
select is_empty(
$$ select * from campsite_type_feature_i18n $$,
'Should not have added any translation for campsite type features.'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,204 @@
-- Test campsite_type_feature
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(41);
set search_path to camper, public;
select has_table('campsite_type_feature');
select has_pk('campsite_type_feature');
select table_privs_are('campsite_type_feature', 'guest', array['SELECT']);
select table_privs_are('campsite_type_feature', 'employee', array['SELECT']);
select table_privs_are('campsite_type_feature', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('campsite_type_feature', 'authenticator', array[]::text[]);
select has_column('campsite_type_feature', 'campsite_type_feature_id');
select col_is_pk('campsite_type_feature', 'campsite_type_feature_id');
select col_type_is('campsite_type_feature', 'campsite_type_feature_id', 'integer');
select col_not_null('campsite_type_feature', 'campsite_type_feature_id');
select col_hasnt_default('campsite_type_feature', 'campsite_type_feature_id');
select has_column('campsite_type_feature', 'campsite_type_id');
select col_is_fk('campsite_type_feature', 'campsite_type_id');
select fk_ok('campsite_type_feature', 'campsite_type_id', 'campsite_type', 'campsite_type_id');
select col_type_is('campsite_type_feature', 'campsite_type_id', 'integer');
select col_not_null('campsite_type_feature', 'campsite_type_id');
select col_hasnt_default('campsite_type_feature', 'campsite_type_id');
select has_column('campsite_type_feature', 'icon_name');
select col_is_fk('campsite_type_feature', 'icon_name');
select fk_ok('campsite_type_feature', 'icon_name', 'icon', 'icon_name');
select col_type_is('campsite_type_feature', 'icon_name', 'text');
select col_not_null('campsite_type_feature', 'icon_name');
select col_hasnt_default('campsite_type_feature', 'icon_name');
select has_column('campsite_type_feature', 'name');
select col_type_is('campsite_type_feature', 'name', 'text');
select col_not_null('campsite_type_feature', 'name');
select col_hasnt_default('campsite_type_feature', 'name');
set client_min_messages to warning;
truncate campsite_type_feature 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_feature (campsite_type_id, icon_name, name)
values (16, 'information', 'Feature 16.1')
, (18, 'wifi', 'Feature 18.1')
;
prepare campsite_feature_data as
select campsite_type_id, name
from campsite_type_feature
;
set role guest;
select bag_eq(
'campsite_feature_data',
$$ values (16, 'Feature 16.1')
, (18, 'Feature 18.1')
$$,
'Everyone should be able to list all campsite type features across all companies'
);
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2');
select lives_ok(
$$ insert into campsite_type_feature(campsite_type_id, icon_name, name) values (16, 'castle', 'Feature 16.2') $$,
'Admin from company 2 should be able to insert a new campsite type feature to that company.'
);
select bag_eq(
'campsite_feature_data',
$$ values (16, 'Feature 16.1')
, (16, 'Feature 16.2')
, (18, 'Feature 18.1')
$$,
'The new row should have been added'
);
select lives_ok(
$$ update campsite_type_feature set name = 'Feature 16-2' where campsite_type_id = 16 and name = 'Feature 16.2' $$,
'Admin from company 2 should be able to update campsite type feature of that company.'
);
select bag_eq(
'campsite_feature_data',
$$ values (16, 'Feature 16.1')
, (16, 'Feature 16-2')
, (18, 'Feature 18.1')
$$,
'The row should have been updated.'
);
select lives_ok(
$$ delete from campsite_type_feature where campsite_type_id = 16 and name = 'Feature 16-2' $$,
'Admin from company 2 should be able to delete campsite type feature from that company.'
);
select bag_eq(
'campsite_feature_data',
$$ values (16, 'Feature 16.1')
, (18, 'Feature 18.1')
$$,
'The row should have been deleted.'
);
select throws_ok(
$$ insert into campsite_type_feature (campsite_type_id, icon_name, name) values (18, 'toilet', 'Feature 18.2') $$,
'42501', 'new row violates row-level security policy for table "campsite_type_feature"',
'Admin from company 2 should NOT be able to insert new campsite type features to company 4.'
);
select lives_ok(
$$ update campsite_type_feature set name = 'Feature 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_feature_data',
$$ values (16, 'Feature 16.1')
, (18, 'Feature 18.1')
$$,
'No row should have been changed.'
);
select throws_ok(
$$ update campsite_type_feature set campsite_type_id = 18 where campsite_type_id = 16 $$,
'42501', 'new row violates row-level security policy for table "campsite_type_feature"',
'Admin from company 2 should NOT be able to move campsite type feature to one of company 4'
);
select lives_ok(
$$ delete from campsite_type_feature 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_feature_data',
$$ values (16, 'Feature 16.1')
, (18, 'Feature 18.1')
$$,
'No row should have been changed'
);
select throws_ok(
$$ insert into campsite_type_feature (campsite_type_id, icon_name, name) values (16, 'baby', ' ') $$,
'23514', 'new row for relation "campsite_type_feature" violates check constraint "name_not_empty"',
'Should not be able to insert campsite type features with a blank name.'
);
reset role;
select *
from finish();
rollback;

View File

@ -0,0 +1,44 @@
-- Test campsite_type_feature_i18n
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(23);
set search_path to camper, public;
select has_table('campsite_type_feature_i18n');
select has_pk('campsite_type_feature_i18n');
select col_is_pk('campsite_type_feature_i18n', array['campsite_type_feature_id', 'lang_tag']);
select table_privs_are('campsite_type_feature_i18n', 'guest', array['SELECT']);
select table_privs_are('campsite_type_feature_i18n', 'employee', array['SELECT']);
select table_privs_are('campsite_type_feature_i18n', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('campsite_type_feature_i18n', 'authenticator', array[]::text[]);
select has_column('campsite_type_feature_i18n', 'campsite_type_feature_id');
select col_is_fk('campsite_type_feature_i18n', 'campsite_type_feature_id');
select fk_ok('campsite_type_feature_i18n', 'campsite_type_feature_id', 'campsite_type_feature', 'campsite_type_feature_id');
select col_type_is('campsite_type_feature_i18n', 'campsite_type_feature_id', 'integer');
select col_not_null('campsite_type_feature_i18n', 'campsite_type_feature_id');
select col_hasnt_default('campsite_type_feature_i18n', 'campsite_type_feature_id');
select has_column('campsite_type_feature_i18n', 'lang_tag');
select col_is_fk('campsite_type_feature_i18n', 'lang_tag');
select fk_ok('campsite_type_feature_i18n', 'lang_tag', 'language', 'lang_tag');
select col_type_is('campsite_type_feature_i18n', 'lang_tag', 'text');
select col_not_null('campsite_type_feature_i18n', 'lang_tag');
select col_hasnt_default('campsite_type_feature_i18n', 'lang_tag');
select has_column('campsite_type_feature_i18n', 'name');
select col_type_is('campsite_type_feature_i18n', 'name', 'text');
select col_not_null('campsite_type_feature_i18n', 'name');
select col_hasnt_default('campsite_type_feature_i18n', 'name');
select *
from finish();
rollback;

View File

@ -0,0 +1,81 @@
-- Test edit_campsite_type_feature
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(13);
set search_path to camper, public;
select has_function('camper', 'edit_campsite_type_feature', array['integer', 'text', 'text']);
select function_lang_is('camper', 'edit_campsite_type_feature', array['integer', 'text', 'text'], 'sql');
select function_returns('camper', 'edit_campsite_type_feature', array['integer', 'text', 'text'], 'integer');
select isnt_definer('camper', 'edit_campsite_type_feature', array['integer', 'text', 'text']);
select volatility_is('camper', 'edit_campsite_type_feature', array['integer', 'text', 'text'], 'volatile');
select function_privs_are('camper', 'edit_campsite_type_feature', array ['integer', 'text', 'text'], 'guest', array[]::text[]);
select function_privs_are('camper', 'edit_campsite_type_feature', array ['integer', 'text', 'text'], 'employee', array[]::text[]);
select function_privs_are('camper', 'edit_campsite_type_feature', array ['integer', 'text', 'text'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'edit_campsite_type_feature', array ['integer', 'text', 'text'], 'authenticator', array[]::text[]);
set client_min_messages to warning;
truncate campsite_type_feature_i18n cascade;
truncate campsite_type_feature 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_feature (campsite_type_feature_id, campsite_type_id, icon_name, name)
values (4, 3, 'information', 'Feature 1')
, (5, 3, 'wifi', 'Feature 2')
;
select lives_ok(
$$ select edit_campsite_type_feature(4, 'toilet', 'Feature A') $$,
'Should be able to edit the first feature'
);
select lives_ok(
$$ select edit_campsite_type_feature(5, 'baby', 'Feature B') $$,
'Should be able to edit the second feature'
);
select bag_eq(
$$ select campsite_type_feature_id, campsite_type_id, icon_name, name from campsite_type_feature $$,
$$ values (4, 3, 'toilet', 'Feature A')
, (5, 3, 'baby', 'Feature B')
$$,
'Should have updated all campsite type features.'
);
select is_empty(
$$ select * from campsite_type_feature_i18n $$,
'Should not have added any translation for campsite type features.'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,85 @@
-- Test translate_campsite_type_feature
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(13);
set search_path to camper, public;
select has_function('camper', 'translate_campsite_type_feature', array['integer', 'text', 'text']);
select function_lang_is('camper', 'translate_campsite_type_feature', array['integer', 'text', 'text'], 'sql');
select function_returns('camper', 'translate_campsite_type_feature', array['integer', 'text', 'text'], 'void');
select isnt_definer('camper', 'translate_campsite_type_feature', array['integer', 'text', 'text']);
select volatility_is('camper', 'translate_campsite_type_feature', array['integer', 'text', 'text'], 'volatile');
select function_privs_are('camper', 'translate_campsite_type_feature', array['integer', 'text', 'text'], 'guest', array[]::text[]);
select function_privs_are('camper', 'translate_campsite_type_feature', array['integer', 'text', 'text'], 'employee', array[]::text[]);
select function_privs_are('camper', 'translate_campsite_type_feature', array['integer', 'text', 'text'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'translate_campsite_type_feature', array['integer', 'text', 'text'], 'authenticator', array[]::text[]);
set client_min_messages to warning;
truncate campsite_type_feature_i18n cascade;
truncate campsite_type_feature 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_feature (campsite_type_feature_id, campsite_type_id, icon_name, name)
values (4, 3, 'toilet', 'Feature 1')
, (5, 3, 'toilet', 'Feature 2')
;
insert into campsite_type_feature_i18n (campsite_type_feature_id, lang_tag, name)
values (5, 'ca', 'carácter2')
;
select lives_ok(
$$ select translate_campsite_type_feature(4, 'ca', 'Carácter 1') $$,
'Should be able to translate the first feature'
);
select lives_ok(
$$ select translate_campsite_type_feature(5, 'es', 'Característica 2') $$,
'Should be able to translate the second feature'
);
select lives_ok(
$$ select translate_campsite_type_feature(5, 'ca', 'Carácter 2') $$,
'Should be able to overwrite the catalan translation of the second feature'
);
select bag_eq(
$$ select campsite_type_feature_id, lang_tag, name from campsite_type_feature_i18n $$,
$$ values (4, 'ca', 'Carácter 1')
, (5, 'ca', 'Carácter 2')
, (5, 'es', 'Característica 2')
$$,
'Should have added and updated all translations.'
);
select *
from finish();
rollback;

View File

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

View File

@ -4,15 +4,20 @@ begin;
set search_path to camper;
select 1 / count(*) from icon where icon_name = 'area';
select 1 / count(*) from icon where icon_name = 'baby';
select 1 / count(*) from icon where icon_name = 'ball';
select 1 / count(*) from icon where icon_name = 'bicycle';
select 1 / count(*) from icon where icon_name = 'campfire';
select 1 / count(*) from icon where icon_name = 'castle';
select 1 / count(*) from icon where icon_name = 'ecofriendly';
select 1 / count(*) from icon where icon_name = 'fridge';
select 1 / count(*) from icon where icon_name = 'hvac';
select 1 / count(*) from icon where icon_name = 'information';
select 1 / count(*) from icon where icon_name = 'kayak';
select 1 / count(*) from icon where icon_name = 'nopet';
select 1 / count(*) from icon where icon_name = 'outing';
select 1 / count(*) from icon where icon_name = 'person';
select 1 / count(*) from icon where icon_name = 'pool';
select 1 / count(*) from icon where icon_name = 'puzzle';
select 1 / count(*) from icon where icon_name = 'restaurant';

View File

@ -0,0 +1,18 @@
-- Verify camper:campsite_type_feature on pg
begin;
select campsite_type_feature_id
, campsite_type_id
, icon_name
, name
from camper.campsite_type_feature
where false;
select 1 / count(*) from pg_class where oid = 'camper.campsite_type_feature'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.campsite_type_feature'::regclass;
select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.campsite_type_feature'::regclass;
select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.campsite_type_feature'::regclass;
select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.campsite_type_feature'::regclass;
rollback;

View File

@ -0,0 +1,11 @@
-- Verify camper:campsite_type_feature_i18n on pg
begin;
select campsite_type_feature_id
, lang_tag
, name
from camper.campsite_type_feature_i18n
where false;
rollback;

View File

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

View File

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

View File

@ -3,6 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
.icon_area {
background-image: url('data:image/svg+xml,%3Csvg viewBox="0 0 29.52905 28.08545" xmlns="http://www.w3.org/2000/svg"%3E%3Cpolyline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" points="24.65173 25.48701 24.65173 26.48701 23.65173 26.48701"/%3E%%3Cline stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="0 0 1.87099 4.67747" stroke="%23303334" fill="none" y2="26.48701" x2="8.21606" y1="26.48701" x1="18.97425"/%3E%3Cpolyline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" points="5.87732 26.48701 4.87732 26.48701 4.87732 25.48701"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="0 0 1.76066 4.40165" stroke="%23303334" fill="none" y2="4.79927" x2="4.87732" y1="21.08536" x1="4.87732"/%3E%3Cpolyline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" points="4.87732 2.59844 4.87732 1.59844 5.87732 1.59844"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="0 0 1.87099 4.67747" stroke="%23303334" fill="none" y2="1.59844" x2="21.31299" y1="1.59844" x1="10.5548"/%3E%3Cpolyline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" points="23.65173 1.59844 24.65173 1.59844 24.65173 2.59844"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="0 0 1.76066 4.40165" stroke="%23303334" fill="none" y2="23.28618" x2="24.65173" y1="7.00009" x1="24.65173"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="0 0 2 5" stroke="%23303334" fill="none" y2="20.16764" x2="22.97111" y1="20.16764" x1="6.55794"/%3E%3C/svg%3E');
}
.icon_baby {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M92,136a8,8,0,1,1,8-8A8,8,0,0,1,92,136Zm72-16a8,8,0,1,0,8,8A8,8,0,0,0,164,120Zm-10.13,44.62a49,49,0,0,1-51.74,0,4,4,0,0,0-4.26,6.76,57,57,0,0,0,60.26,0,4,4,0,1,0-4.26-6.76ZM228,128A100,100,0,1,1,128,28,100.11,100.11,0,0,1,228,128Zm-8,0a92.11,92.11,0,0,0-90.06-92C116.26,54.07,116,71.83,116,72a12,12,0,0,0,24,0,4,4,0,0,1,8,0,20,20,0,0,1-40,0c0-.78.16-17.31,12-35.64A92,92,0,1,0,220,128Z"/%3E%3C/svg%3E');
}
@ -23,10 +26,18 @@
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M200,28H184a12,12,0,0,0-12,12V56a4,4,0,0,1-4,4H152a4,4,0,0,1-4-4V40a12,12,0,0,0-12-12H120a12,12,0,0,0-12,12V56a4,4,0,0,1-4,4H88a4,4,0,0,1-4-4V40A12,12,0,0,0,72,28H56A12,12,0,0,0,44,40V84.69a11.93,11.93,0,0,0,3.51,8.48l11.32,11.32A4,4,0,0,1,60,107.31V216a12,12,0,0,0,12,12H184a12,12,0,0,0,12-12V107.31a4,4,0,0,1,1.17-2.82l11.32-11.32A11.93,11.93,0,0,0,212,84.69V40A12,12,0,0,0,200,28ZM148,220H108V152a20,20,0,0,1,40,0ZM204,84.69a4,4,0,0,1-1.17,2.82L191.51,98.83a11.93,11.93,0,0,0-3.51,8.48V216a4,4,0,0,1-4,4H156V152a28,28,0,0,0-56,0v68H72a4,4,0,0,1-4-4V107.31a11.93,11.93,0,0,0-3.51-8.48L53.17,87.51A4,4,0,0,1,52,84.69V40a4,4,0,0,1,4-4H72a4,4,0,0,1,4,4V56A12,12,0,0,0,88,68h16a12,12,0,0,0,12-12V40a4,4,0,0,1,4-4h16a4,4,0,0,1,4,4V56a12,12,0,0,0,12,12h16a12,12,0,0,0,12-12V40a4,4,0,0,1,4-4h16a4,4,0,0,1,4,4Z"/%3E%3C/svg%3E');
}
.icon_ecofriendly {
background-image: url('data:image/svg+xml,%3Csvg viewBox="0 0 29.52905 28.08545" xmlns="http://www.w3.org/2000/svg"%3E%3Cpolygon stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" points="20.03209 6.02239 14.30328 13.46984 17.16768 13.46984 13.15752 18.62577 26.90666 18.62577 22.89649 13.46984 25.7609 13.46984 20.03209 6.02239"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" y2="22.06306" x2="20.03209" y1="18.62577" x1="20.03209"/%3E%3Cpath stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" d="m12.38607,21.36565v-4.18444c0-.38517-.31224-.69741-.69741-.69741h-2.78962c-.38517,0-.69741.31224-.69741.69741v4.18444c0,.38517-.31224.69741-.69741.69741H3.31979c-.38517,0-.69741-.31224-.69741-.69741v-8.06027c.00002-.19652.08295-.38392.2284-.51608l6.97406-6.58526c.2661-.24221.67278-.24221.93888,0l6.97406,6.58526c.14545.13216.22838.31956.2284.51608v8.06027c0,.38517-.31224.69741-.69741.69741h-4.18531c-.38517,0-.69741-.31224-.69741-.69741Z"/%3E%3C/svg%3E');
}
.icon_fridge {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17.57617 28.04395"%3E%3Cpath stroke-width="0" d="m15.62988,0H1.94629C.87305,0,0,.87305,0,1.94629v25.59766c0,.27637.22363.5.5.5h16.57617c.27637,0,.5-.22363.5-.5V1.94629c0-1.07324-.87305-1.94629-1.94629-1.94629ZM1.94629,1h13.68359c.52148,0,.94629.42432.94629.94629v7.41357H1V1.94629c0-.52197.42432-.94629.94629-.94629Zm-.94629,26.04395V10.35986h15.57617v16.68408H1Z"/%3E%3Cpath stroke-width="0" d="m3.64453,5.36426h2.04248c.27637,0,.5-.22363.5-.5s-.22363-.5-.5-.5h-2.04248c-.27637,0-.5.22363-.5.5s.22363.5.5.5Z"/%3E%3Cpath stroke-width="0" d="m5.68701,13.271h-2.04248c-.27637,0-.5.22363-.5.5s.22363.5.5.5h2.04248c.27637,0,.5-.22363.5-.5s-.22363-.5-.5-.5Z"/%3E%3C/svg%3E');
}
.icon_hvac {
background-image: url('data:image/svg+xml,%3Csvg viewBox="0 0 29.52905 28.08545" xmlns="http://www.w3.org/2000/svg"%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" y2="22.04885" x2="12.97909" y1="12.87674" x1="12.97909"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" y2="12.87674" x2="12.97909" y1="11.15696" x1="11.25932"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" y2="22.04885" x2="12.97909" y1="23.76863" x1="11.25932"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" y2="17.4628" x2="12.97909" y1="15.16977" x1="9.00713"/%3E%3Cpolyline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" points="6.67326 15.74302 9.00713 15.16977 8.39303 12.87674"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" y2="17.4628" x2="12.97909" y1="19.75582" x1="9.00713"/%3E%3Cpolyline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" points="8.39303 22.04885 9.00713 19.75582 6.67326 19.18257"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" y2="9.63923" x2="15.03222" y1="11.3157" x1="15.03222"/%3E%3Cpath stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" d="m15.03222,13.55101c2.16042,0,3.91179,1.75137,3.91179,3.91179s-1.75137,3.91179-3.91179,3.91179"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" y2="11.87453" x2="20.62049" y1="12.99218" x1="19.50284"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" y2="23.05106" x2="20.62049" y1="21.93341" x1="19.50284"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" y2="25.28637" x2="15.03222" y1="23.60989" x1="15.03222"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" y2="17.4628" x2="22.85579" y1="17.4628" x1="21.17931"/%3E%3Cpolyline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" points="27.00983 19.18257 27.00983 11.50591 14.76453 2.79908 2.51922 11.50591 2.51922 19.18257 2.51922 11.50591 14.76453 2.79908"/%3E%3C/svg%3E');
}
.icon_information {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M140,176a4,4,0,0,1-4,4,12,12,0,0,1-12-12V128a4,4,0,0,0-4-4,4,4,0,0,1,0-8,12,12,0,0,1,12,12v40a4,4,0,0,0,4,4A4,4,0,0,1,140,176ZM124,92a8,8,0,1,0-8-8A8,8,0,0,0,124,92Zm104,36A100,100,0,1,1,128,28,100.11,100.11,0,0,1,228,128Zm-8,0a92,92,0,1,0-92,92A92.1,92.1,0,0,0,220,128Z"/%3E%3C/svg%3E');
}
@ -35,10 +46,18 @@
background-image: url('data:image/svg+xml,%3Csvg viewBox="0 0 29.52905 28.08545" xmlns="http://www.w3.org/2000/svg" id="uuid-87ec619a-4896-40b8-8478-aa076dee3f7c"%3E%3Cpath stroke-width="0" d="m29.46728,11.26074l-.57617-2.15186c-.08398-.31299-.24609-.59229-.45801-.78613-.26758-.24658-.59863-.3418-.90625-.25879l-3.4834.93213c-.26953.07227-.48535.26807-.6084.54932l-.5752,1.30713c-.0719.16376-.10333.34686-.10742.53485l-3.05627.81787c-.23956-3.37067-1.03204-8.79126-2.35193-10.66425l-.22363-.31836C16.58057.45752,15.69971.00049,14.76318,0h-.00049c-.94873,0-1.80566.44434-2.35156,1.21875-1.71973,2.44043-2.65771,9.75146-2.65771,12.82422,0,.24457.00812.52399.0199.81769l-3.25604.87134c-.09741-.16071-.21613-.30347-.36005-.40924l-1.15088-.84424c-.24854-.18213-.53125-.24463-.80127-.17236l-3.48389.93164c-.56689.15234-.85693.84961-.65918,1.58691l.57617,2.15137c.0835.31348.24561.59277.45654.78613.20508.18848.4458.28809.68506.28809.07471,0,.14893-.00879.22168-.02832l3.48486-.93359c.26807-.07227.48389-.26758.60693-.54883l.57568-1.30664c.07172-.16357.10309-.3465.10718-.5343l3.05591-.81793c.25531,3.52673,1.14368,8.94983,2.5791,10.98602.5459.77441,1.40332,1.21875,2.35205,1.21875.93652,0,1.81738-.45703,2.35645-1.22266l.22461-.31738c1.60352-2.27637,2.43066-9.79492,2.43066-12.50244,0-.23499-.00854-.51709-.021-.81805l3.25732-.87183c.09747.16071.21625.30347.36035.40912l1.15039.84473c.18066.13281.38086.20166.5791.20166.07422,0,.14941-.00977.22168-.0293l3.48535-.93262c.56641-.15234.85645-.84863.65918-1.58594Zm-24.24072,6.86182l-3.41602.93262c-.03516-.01465-.14844-.12109-.20654-.33691l-.57617-2.15137c-.05713-.21484-.01318-.3623-.04688-.3623h-.00098l3.4165-.93164s.00586.00293.01709.01074c.00049,0,.00098.00098.00098.00098l1.00842.74017-1.90881.5108c-.2666.07227-.4248.3457-.35352.6123.05957.22363.26172.37109.48242.37109.04297,0,.08643-.00586.12988-.0166l1.91559-.5127-.46198,1.13281Zm5.52686-4.07959c0-3.07959.93994-10.06934,2.4751-12.24805.35596-.50488.91504-.79492,1.53418-.79492.62109.00049,1.18213.2915,1.54053.79932l.22266.31689c1.17902,1.67261,1.96533,7.08936,2.18457,10.35278l-1.50305.40228c-.19397-2.55505-1.02423-4.83514-2.44324-4.83514-1.63379,0-2.48828,3.02197-2.48828,6.00684,0,.04913.00214.09814.00262.14728l-1.51538.40552c-.00574-.19403-.0097-.38147-.0097-.5528Zm2.52692-.12079c.02441-2.93951.89984-4.88605,1.48383-4.88605.53003,0,1.29828,1.6062,1.45496,4.09961l-2.93878.78644Zm2.96765.24115c-.02441,2.93933-.89978,4.88599-1.48383,4.88599-.53003,0-1.29834-1.60632-1.45496-4.09937l2.93878-.78662Zm2.52692-.12036c0,2.66846-.84961,9.94092-2.24902,11.92725l-.22363.31641c-.71387,1.01367-2.36084,1.01562-3.07373.00391-1.31152-1.86127-2.18622-7.22681-2.414-10.67285l1.50641-.4032c.19397,2.55505,1.02417,4.83484,2.44324,4.83484,1.63379,0,2.48828-3.02148,2.48828-6.00635,0-.04926-.00214-.09845-.00262-.14777l1.51514-.40558c.00659.20087.00995.38617.00995.55334Zm6.35742-1.23047s-.00684-.00293-.01855-.01123l-1.00897-.74091,1.90936-.51105c.26758-.07129.42578-.34521.35449-.6123-.07227-.26562-.3418-.42725-.6123-.35352l-1.91577.5127.46069-1.13379,3.41699-.93164c.03516.01416.14844.12012.20605.33691l.57617,2.15186c.05957.21973.01953.36816.04883.36133l-3.41699.93164Z"/%3E%3C/svg%3E');
}
.icon_nopet {
background-image: url('data:image/svg+xml,%3Csvg viewBox="0 0 29.52905 28.08545" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" d="m17.21239,7.49301s-4.87977.84632-8.52421,5.99327c-2.16998,3.06461-2.2348,5.76054-2.16343,6.96413-.20971.12828-.41646.23744-.60915.31138-1.52884.5874-3.2956.47276-3.31213.47143-.4288-.03178-.80523.29289-.83649.72349-.03114.43058.29272.80523.72347.83649.02465.00178.21278.01443.51249.01443.7371,0,2.14964-.07768,3.47368-.58659.40917-.15748.83392-.40365,1.23448-.67824-.00552.04298-.00973.08677-.00973.1312,0,.54621.44274.98878.98878.98878h6.45055l1.19972-.00985-.13038-.52151c-.09972-.39887-.3556-.74091-.71009-.94918h0c-.23523-.1382-.5031-.21107-.77593-.21107h-2.41299c1.63184-.71439,2.2461-2.1492,1.45294-3.47113l1.43136-.57254,2.78807,4.33263c.01879.03918.04916.06965.06582.10978.29509.71048,1.15391,1.28302,2.02047,1.28302h1.70989v-.33504c0-.49573-.28234-.94819-.72763-1.16605l-.35082-.11881c-.36735-.12441-.66131-.40423-.80365-.76502l-1.90756-4.83493,1.54591-4.46211,4.3585-1.15763.43585-1.29053c.16876-.49968-.12132-1.03771-.63151-1.17133l-1.93096-.54479c-.13345-1.31408-1.37293-1.72864-2.77284-1.4432-1.14081.23261-1.87678.73329-1.81146,1.89575.00439.07821.01285.15499.02897.23377h0Z"/%3E%3Cpath stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" d="m19.69654,6.92365l-.56872,1.6591c-.13062.38105-.45388.66399-.84887.74299h0c-.45328.09066-.90313-.1704-1.04931-.60894l-.0304-.09119"/%3E%3Cline stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" y2="25.06162" x2="24.09052" y1="3.02383" x1="2.05273"/%3E%3C/svg%3E');
}
.icon_outing {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M164,44.17V32a20,20,0,0,0-20-20H112A20,20,0,0,0,92,32V44.17A52.05,52.05,0,0,0,44,96V216a12,12,0,0,0,12,12H200a12,12,0,0,0,12-12V96A52.05,52.05,0,0,0,164,44.17ZM112,20h32a12,12,0,0,1,12,12V44H100V32A12,12,0,0,1,112,20Zm60,144H84V152a12,12,0,0,1,12-12h64a12,12,0,0,1,12,12Zm-88,8h56v12a4,4,0,0,0,8,0V172h24v48H84Zm120,44a4,4,0,0,1-4,4H180V152a20,20,0,0,0-20-20H96a20,20,0,0,0-20,20v68H56a4,4,0,0,1-4-4V96A44.05,44.05,0,0,1,96,52h64a44.05,44.05,0,0,1,44,44ZM148,88a4,4,0,0,1-4,4H112a4,4,0,0,1,0-8h32A4,4,0,0,1,148,88Z"/%3E%3C/svg%3E');
}
.icon_person {
background-image: url('data:image/svg+xml,%3Csvg viewBox="0 0 29.52905 28.08545" xmlns="http://www.w3.org/2000/svg"%3E%3Ccircle stroke-linejoin="round" stroke-linecap="round" stroke="%23000" fill="none" r="2.55477" cy="5.10916" cx="14.76453"/%3E%3Cpath stroke-linejoin="round" stroke-linecap="round" stroke="%23303334" fill="none" d="m17.01911,9.36711c.7333.00012,1.43119.31534,1.91608.86543l4.82426,5.46934c.49972.49972.49972,1.30991,0,1.80963s-1.30991.49972-1.80963,0l-3.77787-3.03485,2.43448,9.25359c.28892.64361.00138,1.39957-.64222,1.68848-.63062.28309-1.37202.01317-1.67304-.60909l-3.52558-6.07503-3.52558,6.07503c-.30721.63508-1.07109.90086-1.70617.59365-.62226-.30102-.89218-1.04242-.60909-1.67304l2.43448-9.25359-3.78,3.03273c-.49972.49972-1.30991.49972-1.80963,0s-.49972-1.30991,0-1.80963l4.82639-5.46721c.48489-.55009,1.18278-.86531,1.91608-.86543h4.50704Z"/%3E%3C/svg%3E');
}
.icon_pool {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M88,145.39a4,4,0,0,0,4-4V124h72v19.29a4,4,0,0,0,8,0V32a4,4,0,0,0-8,0V52H92V32a4,4,0,0,0-8,0V141.39A4,4,0,0,0,88,145.39ZM92,116V92h72v24Zm72-56V84H92V60ZM28,168a4,4,0,0,1,4-4c13.21,0,20.12,4.61,26.22,8.67,5.9,3.93,11,7.33,21.78,7.33s15.88-3.4,21.78-7.33c6.09-4.06,13-8.67,26.21-8.67s20.13,4.61,26.22,8.67c5.9,3.93,11,7.33,21.79,7.33s15.88-3.4,21.78-7.33c6.1-4.06,13-8.67,26.22-8.67a4,4,0,0,1,0,8c-10.79,0-15.88,3.4-21.78,7.33-6.1,4.06-13,8.67-26.22,8.67s-20.13-4.61-26.22-8.67c-5.9-3.93-11-7.33-21.79-7.33s-15.88,3.4-21.78,7.33c-6.09,4.06-13,8.67-26.21,8.67s-20.12-4.61-26.22-8.67C47.88,175.4,42.79,172,32,172A4,4,0,0,1,28,168Zm200,40a4,4,0,0,1-4,4c-10.79,0-15.88,3.4-21.78,7.33-6.1,4.06-13,8.67-26.22,8.67s-20.13-4.61-26.22-8.67c-5.9-3.93-11-7.33-21.79-7.33s-15.88,3.4-21.78,7.33c-6.09,4.06-13,8.67-26.21,8.67s-20.12-4.61-26.22-8.67C47.88,215.4,42.79,212,32,212a4,4,0,0,1,0-8c13.21,0,20.12,4.61,26.22,8.67,5.9,3.93,11,7.33,21.78,7.33s15.88-3.4,21.78-7.33c6.09-4.06,13-8.67,26.21-8.67s20.13,4.61,26.22,8.67c5.9,3.93,11,7.33,21.79,7.33s15.88-3.4,21.78-7.33c6.1-4.06,13-8.67,26.22-8.67A4,4,0,0,1,228,208Z"/%3E%3C/svg%3E');
}

View File

@ -235,10 +235,13 @@ h1 a .name {
left: -9999em;
}
nav ul {
nav ul, .campsite_type_features ul {
list-style: none;
padding-left: 0;
display: flex;
}
nav ul {
flex-wrap: wrap;
align-items: center;
}
@ -666,18 +669,32 @@ dt {
align-items: center;
}
.campsite_type_features li {
flex: 1;
font-size: 2.4rem;
text-align: center;
justify-content: space-between;
background-repeat: no-repeat;
background-position: top center;
background-size: 7.2rem 7.2rem;
padding-top: 7.2rem;
}
footer {
display: flex;
flex-direction: column;
gap: 1rem;
}
footer div, .campsite_type_features, .campsite_type_detail {
padding: 5rem 0;
border-top: 3px solid black;
}
footer div {
display: flex;
justify-content: space-between;
padding: 5rem 0;
margin: 0 2.5rem;
border-top: 3px solid black;
border-bottom: 3px solid black;
}

View File

@ -0,0 +1,75 @@
<!--
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.featureForm*/ -}}
{{ if .ID}}
{{( pgettext "Edit Campsite Type Feature" "title" )}}
{{ else }}
{{( pgettext "New Campsite Type Feature" "title" )}}
{{ end }}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite/types.featureForm*/ -}}
<form
{{ if .ID }}
data-hx-put="/admin/campsites/types/{{ .TypeSlug }}/features/{{ .ID }}"
{{ else }}
action="/admin/campsites/types/{{ .TypeSlug }}/features" method="post"
{{ end }}
>
<h2>
{{ if .ID }}
{{( pgettext "Edit Campsite Type Feature" "title" )}}
{{ else }}
{{( pgettext "New Campsite Type Feature" "title" )}}
{{ end }}
</h2>
{{ CSRFInput }}
<fieldset>
{{ with $field := .Icon -}}
<fieldset class="icon-input">
<legend>{{( pgettext "Icon" "input")}}</legend>
<input type="hidden" name="{{ .Name }}"
{{- range .Options }}
{{ if $field.IsSelected .Value }} value="{{ .Value }}"{{ end }}
{{- end }}
>
<ul>
{{- range .Options }}
<li>
<button type="button" data-icon-name="{{ .Value }}" class="icon_{{ .Value }}"></button>
</li>
{{- end }}
</ul>
{{ template "error-message" . }}
</fieldset>
{{- end }}
{{ with .Name -}}
<label>
{{( pgettext "Name" "input")}}<br>
<input type="text" name="{{ .Name }}" value="{{ .Val }}"
required {{ template "error-attrs" . }}><br>
</label>
{{ template "error-message" . }}
{{- end }}
</fieldset>
<footer>
<button type="submit">
{{ if .ID }}
{{( pgettext "Update" "action" )}}
{{ else }}
{{( pgettext "Add" "action" )}}
{{ end }}
</button>
</footer>
</form>
<script type="module">
import {setupIconInput} from "/static/camper.js";
setupIconInput(document.querySelector('.icon-input'));
</script>
{{- 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 Features" "title" )}}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite/types.featureIndex*/ -}}
<a href="/admin/campsites/types/{{ .TypeSlug }}/features/new">{{( pgettext "Add Feature" "action" )}}</a>
<h2>{{( pgettext "Campsite Type Features" "title" )}}</h2>
{{ if .Features -}}
<table class="services">
<thead>
<tr>
<th scope="col">{{( pgettext "Name" "header" )}}</th>
<th scope="col">{{( pgettext "Translations" "header" )}}</th>
</tr>
</thead>
<tbody>
{{ range .Features -}}
<tr>
<td class="icon_{{ .Icon }}"><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 features 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.featureL10nForm*/ -}}
{{printf (pgettext "Translate Campsite Type Feature to %s" "title") .Locale.Endonym }}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite/types.featureL10nForm*/ -}}
<form data-hx-put="/admin/campsites/types/{{ .TypeSlug }}/features/{{ .ID }}/{{ .Locale.Language }}">
<h2>
{{printf (pgettext "Translate Campsite Type Feature 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

@ -16,6 +16,7 @@
<tr>
<th scope="col">{{( pgettext "Name" "header" )}}</th>
<th scope="col">{{( pgettext "Translations" "header" )}}</th>
<th scope="col">{{( pgettext "Features" "header" )}}</th>
<th scope="col">{{( pgettext "Options" "header" )}}</th>
<th scope="col">{{( pgettext "Carousel" "header" )}}</th>
<th scope="col">{{( pgettext "Active" "campsite type" )}}</th>
@ -34,8 +35,15 @@
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><a href="/admin/campsites/types/{{ .Slug }}/slides">{{( pgettext "Edit Carousel" "action" )}}</a></td>
<td>
<a href="/admin/campsites/types/{{ .Slug }}/features">{{( pgettext "Edit Features" "action" )}}</a>
</td>
<td>
<a href="/admin/campsites/types/{{ .Slug }}/options">{{( pgettext "Edit Options" "action" )}}</a>
</td>
<td>
<a href="/admin/campsites/types/{{ .Slug }}/slides">{{( pgettext "Edit Carousel" "action" )}}</a>
</td>
<td>{{ if .Active }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }}</td>
</tr>
{{- end }}

View File

@ -56,7 +56,18 @@
</article>
{{- end }}
<article>
{{ with .Features -}}
<article class="campsite_type_features">
<h3>{{( pgettext "Features" "title" )}}</h3>
<ul>
{{ range . -}}
<li class="icon_{{ .Icon }}">{{ .Name }}</li>
{{- end }}
</ul>
</article>
{{- end }}
<article class="campsite_type_detail">
<section>
<h3>{{( pgettext "Info" "title" )}}</h3>
{{ .Info }}