diff --git a/deploy/add_season.sql b/deploy/add_season.sql new file mode 100644 index 0000000..9969588 --- /dev/null +++ b/deploy/add_season.sql @@ -0,0 +1,24 @@ +-- Deploy camper:add_season to pg +-- requires: roles +-- requires: schema_camper +-- requires: season +-- requires: color +-- requires: to_integer + +begin; + +set search_path to camper, public; + +create or replace function add_season(company integer, name text, color color) returns uuid as $$ + insert into season (company_id, name, color) + values (company, name, to_integer(color)) + returning slug + ; +$$ + language sql +; + +revoke execute on function add_season(integer, text, color) from public; +grant execute on function add_season(integer, text, color) to admin; + +commit; diff --git a/deploy/color.sql b/deploy/color.sql new file mode 100644 index 0000000..92a186c --- /dev/null +++ b/deploy/color.sql @@ -0,0 +1,14 @@ +-- Deploy camper:color to pg +-- requires: schema_camper +-- requires: extension_citext + +begin; + +set search_path to camper, public; + +create domain color as citext +check ( value ~ '^#[a-fA-F0-9]{6}$' ); + +comment on domain color is 'seven-character string specifying an RGB color in hexadecimal format starting with #, like HTML.'; + +commit; diff --git a/deploy/edit_season.sql b/deploy/edit_season.sql new file mode 100644 index 0000000..dcd85a5 --- /dev/null +++ b/deploy/edit_season.sql @@ -0,0 +1,27 @@ +-- Deploy camper:edit_season to pg +-- requires: roles +-- requires: schema_camper +-- requires: season +-- requires: color +-- requires: to_integer + +begin; + +set search_path to camper, public; + +create or replace function edit_season(slug uuid, name text, color color, active boolean) returns uuid as $$ + update season + set name = edit_season.name + , color = to_integer(edit_season.color) + , active = edit_season.active + where slug = edit_season.slug + returning slug + ; +$$ + language sql +; + +revoke execute on function edit_season(uuid, text, color, boolean) from public; +grant execute on function edit_season(uuid, text, color, boolean) to admin; + +commit; diff --git a/deploy/season.sql b/deploy/season.sql new file mode 100644 index 0000000..17ede1f --- /dev/null +++ b/deploy/season.sql @@ -0,0 +1,58 @@ +-- Deploy camper:season to pg +-- requires: roles +-- requires: schema_camper +-- requires: company +-- requires: user_profile + +begin; + +set search_path to camper, public; + +create table season ( + season_id serial primary key, + company_id integer not null references company, + slug uuid not null unique default gen_random_uuid(), + name text not null constraint name_not_empty check(length(trim(name)) > 0), + color integer not null default 0, + active boolean not null default true +); + +grant select on table season to guest; +grant select on table season to employee; +grant select, insert, delete, update on table season to admin; + +grant usage on sequence season_season_id_seq to admin; + +alter table season enable row level security; + +create policy guest_ok +on season +for select +using (true) +; + +create policy insert_to_company +on season +for insert +with check ( + company_id in (select company_id from user_profile) +) +; + +create policy update_company +on season +for update +using ( + company_id in (select company_id from user_profile) +) +; + +create policy delete_from_company +on season +for delete +using ( + company_id in (select company_id from user_profile) +) +; + +commit; diff --git a/deploy/to_color.sql b/deploy/to_color.sql new file mode 100644 index 0000000..21b99cb --- /dev/null +++ b/deploy/to_color.sql @@ -0,0 +1,18 @@ +-- Deploy camper:to_color to pg +-- requires: roles +-- requires: schema_camper +-- requires: color + +begin; + +set search_path to camper, public; + +create or replace function to_color(color integer) returns color as +$$ + select '#' || lpad(to_hex(color), 6, '0'); +$$ + language sql + immutable +; + +commit; diff --git a/deploy/to_integer.sql b/deploy/to_integer.sql new file mode 100644 index 0000000..456bd5c --- /dev/null +++ b/deploy/to_integer.sql @@ -0,0 +1,18 @@ +-- Deploy camper:to_integer to pg +-- requires: roles +-- requires: schema_camper +-- requires: color + +begin; + +set search_path to camper, public; + +create or replace function to_integer(color color) returns integer as +$$ + select ('x00' || substr(color, 2))::bit(32)::integer; +$$ + language sql + immutable +; + +commit; diff --git a/pkg/app/admin.go b/pkg/app/admin.go index ee2edbe..20f641b 100644 --- a/pkg/app/admin.go +++ b/pkg/app/admin.go @@ -13,18 +13,21 @@ import ( "dev.tandem.ws/tandem/camper/pkg/company" "dev.tandem.ws/tandem/camper/pkg/database" httplib "dev.tandem.ws/tandem/camper/pkg/http" + "dev.tandem.ws/tandem/camper/pkg/season" "dev.tandem.ws/tandem/camper/pkg/template" ) type adminHandler struct { campsite *campsite.AdminHandler company *company.AdminHandler + season *season.AdminHandler } func newAdminHandler() *adminHandler { return &adminHandler{ campsite: campsite.NewAdminHandler(), company: company.NewAdminHandler(), + season: season.NewAdminHandler(), } } @@ -48,6 +51,8 @@ func (h *adminHandler) Handle(user *auth.User, company *auth.Company, conn *data h.campsite.Handler(user, company, conn).ServeHTTP(w, r) case "company": h.company.Handler(user, company, conn).ServeHTTP(w, r) + case "seasons": + h.season.Handler(user, company, conn).ServeHTTP(w, r) case "": switch r.Method { case http.MethodGet: diff --git a/pkg/form/validator.go b/pkg/form/validator.go index db9ceaa..0fa46db 100644 --- a/pkg/form/validator.go +++ b/pkg/form/validator.go @@ -62,6 +62,14 @@ func (v *Validator) CheckValidPhone(ctx context.Context, conn *database.Conn, in return v.check(input, b, message), nil } +func (v *Validator) CheckValidColor(ctx context.Context, conn *database.Conn, input *Input, message string) (bool, error) { + b, err := conn.GetBool(ctx, "select input_is_valid($1, 'color')", input.Val) + if err != nil { + return false, err + } + return v.check(input, b, message), nil +} + func (v *Validator) CheckValidPostalCode(ctx context.Context, conn *database.Conn, input *Input, country string, message string) (bool, error) { pattern, err := conn.GetText(ctx, "select '^' || postal_code_regex || '$' from country where country_code = $1", country) if err != nil { diff --git a/pkg/season/admin.go b/pkg/season/admin.go new file mode 100644 index 0000000..6862054 --- /dev/null +++ b/pkg/season/admin.go @@ -0,0 +1,214 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package season + +import ( + "context" + "net/http" + + "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" + "dev.tandem.ws/tandem/camper/pkg/uuid" +) + +type AdminHandler struct { +} + +func NewAdminHandler() *AdminHandler { + return &AdminHandler{} +} + +func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var head string + head, r.URL.Path = httplib.ShiftPath(r.URL.Path) + + switch head { + case "new": + switch r.Method { + case http.MethodGet: + f := newSeasonForm() + f.MustRender(w, r, user, company) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet) + } + case "": + switch r.Method { + case http.MethodGet: + serveSeasonIndex(w, r, user, company, conn) + case http.MethodPost: + addSeason(w, r, user, company, conn) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) + } + default: + if !uuid.Valid(head) { + http.NotFound(w, r) + return + } + f := newSeasonForm() + if err := f.FillFromDatabase(r.Context(), conn, head); err != nil { + if database.ErrorIsNotFound(err) { + http.NotFound(w, r) + return + } + panic(err) + } + switch r.Method { + case http.MethodGet: + f.MustRender(w, r, user, company) + case http.MethodPut: + editSeason(w, r, user, company, conn, f) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) + } + } + } +} + +func serveSeasonIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { + campsites, err := collectSeasonEntries(r.Context(), company, conn) + if err != nil { + panic(err) + } + page := &seasonIndex{ + Seasons: campsites, + } + page.MustRender(w, r, user, company) +} + +func collectSeasonEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*seasonEntry, error) { + rows, err := conn.Query(ctx, ` + select slug + , name + , to_color(color)::text + , active + from season + where company_id = $1 + order by name`, company.ID) + if err != nil { + return nil, err + } + defer rows.Close() + + var seasons []*seasonEntry + for rows.Next() { + entry := &seasonEntry{} + if err = rows.Scan(&entry.Slug, &entry.Name, &entry.Color, &entry.Active); err != nil { + return nil, err + } + seasons = append(seasons, entry) + } + + return seasons, nil +} + +type seasonEntry struct { + Slug string + Name string + Color string + Active bool +} + +type seasonIndex struct { + Seasons []*seasonEntry +} + +func (page *seasonIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "season/index.gohtml", page) +} + +func processSeasonForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *seasonForm, act func()) { + if err := f.Parse(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := user.VerifyCSRFToken(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil { + panic(err) + } else if !ok { + if !httplib.IsHTMxRequest(r) { + w.WriteHeader(http.StatusUnprocessableEntity) + } + f.MustRender(w, r, user, company) + return + } + act() + httplib.Redirect(w, r, "/admin/seasons", http.StatusSeeOther) +} + +func addSeason(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { + f := newSeasonForm() + processSeasonForm(w, r, user, company, conn, f, func() { + conn.MustExec(r.Context(), "select add_season($1, $2, $3)", company.ID, f.Name, f.Color) + }) +} + +func editSeason(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *seasonForm) { + processSeasonForm(w, r, user, company, conn, f, func() { + conn.MustExec(r.Context(), "select edit_season($1, $2, $3, $4)", f.Slug, f.Name, f.Color, f.Active) + }) +} + +type seasonForm struct { + Slug string + Active *form.Checkbox + Name *form.Input + Color *form.Input +} + +func newSeasonForm() *seasonForm { + return &seasonForm{ + Active: &form.Checkbox{ + Name: "active", + Checked: true, + }, + Name: &form.Input{ + Name: "label", + }, + Color: &form.Input{ + Name: "season", + }, + } +} + +func (f *seasonForm) FillFromDatabase(ctx context.Context, conn *database.Conn, slug string) error { + f.Slug = slug + row := conn.QueryRow(ctx, "select name, to_color(color)::text, active from season where slug = $1", slug) + return row.Scan(&f.Name.Val, &f.Color.Val, &f.Active.Checked) +} + +func (f *seasonForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.Active.FillValue(r) + f.Name.FillValue(r) + f.Color.FillValue(r) + return nil +} + +func (f *seasonForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) { + v := form.NewValidator(l) + v.CheckRequired(f.Name, l.GettextNoop("Name can not be empty.")) + if v.CheckRequired(f.Color, l.GettextNoop("Color can not be empty.")) { + if _, err := v.CheckValidColor(ctx, conn, f.Color, l.Gettext("This color is not valid. It must be like #123abc.")); err != nil { + return false, err + } + } + return v.AllOK, nil +} + +func (f *seasonForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "season/form.gohtml", f) +} diff --git a/po/ca.po b/po/ca.po index 53ae64f..7bb1e92 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-08-15 22:22+0200\n" +"POT-Creation-Date: 2023-08-16 20:03+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -36,133 +36,185 @@ msgid "Singular Lodges" msgstr "Allotjaments singulars" #: web/templates/admin/campsite/form.gohtml:8 -#: web/templates/admin/campsite/form.gohtml:25 +#: web/templates/admin/campsite/form.gohtml:26 msgctxt "title" msgid "Edit Campsite" msgstr "Edició de l’allotjament" #: web/templates/admin/campsite/form.gohtml:10 -#: web/templates/admin/campsite/form.gohtml:27 +#: web/templates/admin/campsite/form.gohtml:28 msgctxt "title" msgid "New Campsite" msgstr "Nou allotjament" -#: web/templates/admin/campsite/form.gohtml:37 -#: web/templates/admin/campsite/type/form.gohtml:37 -#: web/templates/admin/campsite/type/index.gohtml:18 -msgctxt "campsite type" +#: web/templates/admin/campsite/form.gohtml:38 +#: web/templates/admin/campsite/index.gohtml:20 +msgctxt "campsite" msgid "Active" msgstr "Actiu" -#: web/templates/admin/campsite/form.gohtml:46 +#: web/templates/admin/campsite/form.gohtml:47 msgctxt "input" msgid "Campsite Type" msgstr "Tipus d’allotjament" -#: web/templates/admin/campsite/form.gohtml:51 +#: web/templates/admin/campsite/form.gohtml:52 msgid "Select campsite type" msgstr "Escolliu un tipus d’allotjament" -#: web/templates/admin/campsite/form.gohtml:60 +#: web/templates/admin/campsite/form.gohtml:61 msgctxt "input" msgid "Label" msgstr "Etiqueta" -#: web/templates/admin/campsite/form.gohtml:70 -#: web/templates/admin/campsite/type/form.gohtml:63 +#: web/templates/admin/campsite/form.gohtml:71 +#: web/templates/admin/campsite/type/form.gohtml:64 +#: web/templates/admin/season/form.gohtml:65 msgctxt "action" msgid "Update" msgstr "Actualitza" -#: web/templates/admin/campsite/form.gohtml:72 -#: web/templates/admin/campsite/type/form.gohtml:65 +#: web/templates/admin/campsite/form.gohtml:73 +#: web/templates/admin/campsite/type/form.gohtml:66 +#: web/templates/admin/season/form.gohtml:67 msgctxt "action" msgid "Add" msgstr "Afegeix" #: web/templates/admin/campsite/index.gohtml:6 -#: web/templates/admin/campsite/index.gohtml:12 +#: web/templates/admin/campsite/index.gohtml:13 +#: web/templates/admin/layout.gohtml:70 msgctxt "title" msgid "Campsites" msgstr "Allotjaments" -#: web/templates/admin/campsite/index.gohtml:11 +#: web/templates/admin/campsite/index.gohtml:12 msgctxt "action" msgid "Add Campsite" msgstr "Afegeix allotjament" -#: web/templates/admin/campsite/index.gohtml:17 +#: web/templates/admin/campsite/index.gohtml:18 msgctxt "header" msgid "Label" msgstr "Etiqueta" -#: web/templates/admin/campsite/index.gohtml:18 +#: web/templates/admin/campsite/index.gohtml:19 msgctxt "header" msgid "Type" msgstr "Tipus" -#: web/templates/admin/campsite/index.gohtml:19 -msgctxt "campsite" -msgid "Active" -msgstr "Actiu" - -#: web/templates/admin/campsite/index.gohtml:27 -#: web/templates/admin/campsite/type/index.gohtml:25 +#: web/templates/admin/campsite/index.gohtml:28 +#: web/templates/admin/campsite/type/index.gohtml:26 +#: web/templates/admin/season/index.gohtml:32 msgid "Yes" msgstr "Sí" -#: web/templates/admin/campsite/index.gohtml:27 -#: web/templates/admin/campsite/type/index.gohtml:25 +#: web/templates/admin/campsite/index.gohtml:28 +#: web/templates/admin/campsite/type/index.gohtml:26 +#: web/templates/admin/season/index.gohtml:32 msgid "No" msgstr "No" -#: web/templates/admin/campsite/index.gohtml:33 +#: web/templates/admin/campsite/index.gohtml:34 msgid "No campsites added yet." msgstr "No s’ha afegit cap allotjament encara." #: web/templates/admin/campsite/type/form.gohtml:8 -#: web/templates/admin/campsite/type/form.gohtml:25 +#: web/templates/admin/campsite/type/form.gohtml:26 msgctxt "title" msgid "Edit Campsite Type" msgstr "Edició del tipus d’allotjament" #: web/templates/admin/campsite/type/form.gohtml:10 -#: web/templates/admin/campsite/type/form.gohtml:27 +#: web/templates/admin/campsite/type/form.gohtml:28 msgctxt "title" msgid "New Campsite Type" msgstr "Nou tipus d’allotjament" -#: web/templates/admin/campsite/type/form.gohtml:46 +#: web/templates/admin/campsite/type/form.gohtml:38 +#: web/templates/admin/campsite/type/index.gohtml:19 +msgctxt "campsite type" +msgid "Active" +msgstr "Actiu" + +#: web/templates/admin/campsite/type/form.gohtml:47 +#: web/templates/admin/season/form.gohtml:47 #: web/templates/admin/profile.gohtml:26 msgctxt "input" msgid "Name" msgstr "Nom" -#: web/templates/admin/campsite/type/form.gohtml:54 +#: web/templates/admin/campsite/type/form.gohtml:55 msgctxt "input" msgid "Description" msgstr "Descripció" #: web/templates/admin/campsite/type/index.gohtml:6 -#: web/templates/admin/campsite/type/index.gohtml:12 +#: web/templates/admin/campsite/type/index.gohtml:13 +#: web/templates/admin/layout.gohtml:67 msgctxt "title" msgid "Campsite Types" msgstr "Tipus d’allotjaments" -#: web/templates/admin/campsite/type/index.gohtml:11 +#: web/templates/admin/campsite/type/index.gohtml:12 msgctxt "action" msgid "Add Type" msgstr "Afegeix tipus" -#: web/templates/admin/campsite/type/index.gohtml:17 +#: web/templates/admin/campsite/type/index.gohtml:18 +#: web/templates/admin/season/index.gohtml:18 msgctxt "header" msgid "Name" msgstr "Nom" -#: web/templates/admin/campsite/type/index.gohtml:31 +#: web/templates/admin/campsite/type/index.gohtml:32 msgid "No campsite types added yet." msgstr "No s’ha afegit cap tipus d’allotjament encara." +#: web/templates/admin/season/form.gohtml:8 +#: web/templates/admin/season/form.gohtml:26 +msgctxt "title" +msgid "Edit Season" +msgstr "Edició de la temporada" + +#: web/templates/admin/season/form.gohtml:10 +#: web/templates/admin/season/form.gohtml:28 +msgctxt "title" +msgid "New Season" +msgstr "Nova temporada" + +#: web/templates/admin/season/form.gohtml:38 +#: web/templates/admin/season/index.gohtml:20 +msgctxt "season" +msgid "Active" +msgstr "Activa" + +#: web/templates/admin/season/form.gohtml:55 +msgctxt "input" +msgid "Color" +msgstr "Color" + +#: web/templates/admin/season/index.gohtml:6 +#: web/templates/admin/season/index.gohtml:13 +#: web/templates/admin/layout.gohtml:73 +msgctxt "title" +msgid "Seasons" +msgstr "Temporades" + +#: web/templates/admin/season/index.gohtml:12 +msgctxt "action" +msgid "Add Season" +msgstr "Afegeix temporada" + +#: web/templates/admin/season/index.gohtml:19 +msgctxt "header" +msgid "Color" +msgstr "Color" + +#: web/templates/admin/season/index.gohtml:38 +msgid "No seasons added yet." +msgstr "No s’ha afegit cap temporada encara." + #: web/templates/admin/dashboard.gohtml:6 #: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:49 msgctxt "title" @@ -175,7 +227,7 @@ msgid "Login" msgstr "Entrada" #: web/templates/admin/login.gohtml:22 web/templates/admin/profile.gohtml:35 -#: web/templates/admin/taxDetails.gohtml:50 +#: web/templates/admin/taxDetails.gohtml:51 msgctxt "input" msgid "Email" msgstr "Correu-e" @@ -217,79 +269,80 @@ msgid "Language" msgstr "Idioma" #: web/templates/admin/profile.gohtml:75 -#: web/templates/admin/taxDetails.gohtml:144 +#: web/templates/admin/taxDetails.gohtml:145 msgctxt "action" msgid "Save changes" msgstr "Desa els canvis" #: web/templates/admin/taxDetails.gohtml:6 -#: web/templates/admin/taxDetails.gohtml:12 +#: web/templates/admin/taxDetails.gohtml:13 +#: web/templates/admin/layout.gohtml:64 msgctxt "title" msgid "Tax Details" msgstr "Configuració fiscal" -#: web/templates/admin/taxDetails.gohtml:17 -#: web/templates/admin/taxDetails.gohtml:58 +#: web/templates/admin/taxDetails.gohtml:18 +#: web/templates/admin/taxDetails.gohtml:59 msgctxt "input" msgid "Business Name" msgstr "Nom de l’empresa" -#: web/templates/admin/taxDetails.gohtml:26 +#: web/templates/admin/taxDetails.gohtml:27 msgctxt "input" msgid "VAT Number" msgstr "NIF" -#: web/templates/admin/taxDetails.gohtml:34 +#: web/templates/admin/taxDetails.gohtml:35 msgctxt "input" msgid "Trade Name" msgstr "Nom comercial" -#: web/templates/admin/taxDetails.gohtml:42 +#: web/templates/admin/taxDetails.gohtml:43 msgctxt "input" msgid "Phone" msgstr "Telèfon" -#: web/templates/admin/taxDetails.gohtml:66 +#: web/templates/admin/taxDetails.gohtml:67 msgctxt "input" msgid "Address" msgstr "Adreça" -#: web/templates/admin/taxDetails.gohtml:74 +#: web/templates/admin/taxDetails.gohtml:75 msgctxt "input" msgid "City" msgstr "Població" -#: web/templates/admin/taxDetails.gohtml:82 +#: web/templates/admin/taxDetails.gohtml:83 msgctxt "input" msgid "Province" msgstr "Província" -#: web/templates/admin/taxDetails.gohtml:90 +#: web/templates/admin/taxDetails.gohtml:91 msgctxt "input" msgid "Postal Code" msgstr "Codi postal" -#: web/templates/admin/taxDetails.gohtml:98 +#: web/templates/admin/taxDetails.gohtml:99 msgctxt "input" msgid "Country" msgstr "País" -#: web/templates/admin/taxDetails.gohtml:108 +#: web/templates/admin/taxDetails.gohtml:109 msgctxt "input" msgid "Currency" msgstr "Moneda" -#: web/templates/admin/taxDetails.gohtml:118 +#: web/templates/admin/taxDetails.gohtml:119 msgctxt "input" msgid "Default Language" msgstr "Idioma per defecte" -#: web/templates/admin/taxDetails.gohtml:128 +#: web/templates/admin/taxDetails.gohtml:129 msgctxt "input" msgid "Invoice Number Format" msgstr "Format del número de factura" -#: web/templates/admin/taxDetails.gohtml:136 +#: web/templates/admin/taxDetails.gohtml:137 msgctxt "input" msgid "Legal Disclaimer" msgstr "Nota legal" @@ -330,7 +383,7 @@ msgctxt "language option" msgid "Automatic" msgstr "Automàtic" -#: pkg/app/user.go:249 pkg/campsite/types/admin.go:197 +#: pkg/app/user.go:249 pkg/campsite/types/admin.go:197 pkg/season/admin.go:203 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." @@ -346,7 +399,7 @@ msgstr "L’idioma escollit no és vàlid." msgid "File must be a valid PNG or JPEG image." msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida." -#: pkg/app/admin.go:40 +#: pkg/app/admin.go:43 msgid "Access forbidden" msgstr "Accés prohibit" @@ -358,6 +411,14 @@ msgstr "El tipus d’allotjament escollit no és vàlid." msgid "Label can not be empty." msgstr "No podeu deixar l’etiqueta en blanc." +#: pkg/season/admin.go:204 +msgid "Color can not be empty." +msgstr "No podeu deixar el color en blanc." + +#: pkg/season/admin.go:205 +msgid "This color is not valid. It must be like #123abc." +msgstr "Aquest color no és vàlid. Hauria de ser similar a #123abc." + #: pkg/company/admin.go:186 msgid "Selected country is not valid." msgstr "El país escollit no és vàlid." @@ -422,10 +483,6 @@ msgstr "No podeu deixar el format del número de factura en blanc." msgid "Cross-site request forgery detected." msgstr "S’ha detectat un intent de falsificació de petició a llocs creuats." -#~ msgctxt "title" -#~ msgid "Edit Page" -#~ msgstr "Edició de pàgina" - #~ msgctxt "title" #~ msgid "New Page" #~ msgstr "Nova pàgina" @@ -449,6 +506,3 @@ msgstr "S’ha detectat un intent de falsificació de petició a llocs creuats." #~ msgctxt "header" #~ msgid "Title" #~ msgstr "Títol" - -#~ msgid "No pages added yet." -#~ msgstr "No s’ha afegit cap pàgina encara." diff --git a/po/es.po b/po/es.po index 5f9e274..828a28c 100644 --- a/po/es.po +++ b/po/es.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-08-15 22:23+0200\n" +"POT-Creation-Date: 2023-08-16 20:03+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -36,133 +36,185 @@ msgid "Singular Lodges" msgstr "Alojamientos singulares" #: web/templates/admin/campsite/form.gohtml:8 -#: web/templates/admin/campsite/form.gohtml:25 +#: web/templates/admin/campsite/form.gohtml:26 msgctxt "title" msgid "Edit Campsite" msgstr "Edición del alojamientos" #: web/templates/admin/campsite/form.gohtml:10 -#: web/templates/admin/campsite/form.gohtml:27 +#: web/templates/admin/campsite/form.gohtml:28 msgctxt "title" msgid "New Campsite" msgstr "Nuevo alojamiento" -#: web/templates/admin/campsite/form.gohtml:37 -#: web/templates/admin/campsite/type/form.gohtml:37 -#: web/templates/admin/campsite/type/index.gohtml:18 -msgctxt "campsite type" +#: web/templates/admin/campsite/form.gohtml:38 +#: web/templates/admin/campsite/index.gohtml:20 +msgctxt "campsite" msgid "Active" msgstr "Activo" -#: web/templates/admin/campsite/form.gohtml:46 +#: web/templates/admin/campsite/form.gohtml:47 msgctxt "input" msgid "Campsite Type" msgstr "Tipo de alojamiento" -#: web/templates/admin/campsite/form.gohtml:51 +#: web/templates/admin/campsite/form.gohtml:52 msgid "Select campsite type" msgstr "Escoged un tipo de alojamiento" -#: web/templates/admin/campsite/form.gohtml:60 +#: web/templates/admin/campsite/form.gohtml:61 msgctxt "input" msgid "Label" msgstr "Etiqueta" -#: web/templates/admin/campsite/form.gohtml:70 -#: web/templates/admin/campsite/type/form.gohtml:63 +#: web/templates/admin/campsite/form.gohtml:71 +#: web/templates/admin/campsite/type/form.gohtml:64 +#: web/templates/admin/season/form.gohtml:65 msgctxt "action" msgid "Update" msgstr "Actualizar" -#: web/templates/admin/campsite/form.gohtml:72 -#: web/templates/admin/campsite/type/form.gohtml:65 +#: web/templates/admin/campsite/form.gohtml:73 +#: web/templates/admin/campsite/type/form.gohtml:66 +#: web/templates/admin/season/form.gohtml:67 msgctxt "action" msgid "Add" msgstr "Añadir" #: web/templates/admin/campsite/index.gohtml:6 -#: web/templates/admin/campsite/index.gohtml:12 +#: web/templates/admin/campsite/index.gohtml:13 +#: web/templates/admin/layout.gohtml:70 msgctxt "title" msgid "Campsites" msgstr "Alojamientos" -#: web/templates/admin/campsite/index.gohtml:11 +#: web/templates/admin/campsite/index.gohtml:12 msgctxt "action" msgid "Add Campsite" msgstr "Añadir alojamiento" -#: web/templates/admin/campsite/index.gohtml:17 +#: web/templates/admin/campsite/index.gohtml:18 msgctxt "header" msgid "Label" msgstr "Etiqueta" -#: web/templates/admin/campsite/index.gohtml:18 +#: web/templates/admin/campsite/index.gohtml:19 msgctxt "header" msgid "Type" msgstr "Tipo" -#: web/templates/admin/campsite/index.gohtml:19 -msgctxt "campsite" -msgid "Active" -msgstr "Activo" - -#: web/templates/admin/campsite/index.gohtml:27 -#: web/templates/admin/campsite/type/index.gohtml:25 +#: web/templates/admin/campsite/index.gohtml:28 +#: web/templates/admin/campsite/type/index.gohtml:26 +#: web/templates/admin/season/index.gohtml:32 msgid "Yes" msgstr "Sí" -#: web/templates/admin/campsite/index.gohtml:27 -#: web/templates/admin/campsite/type/index.gohtml:25 +#: web/templates/admin/campsite/index.gohtml:28 +#: web/templates/admin/campsite/type/index.gohtml:26 +#: web/templates/admin/season/index.gohtml:32 msgid "No" msgstr "No" -#: web/templates/admin/campsite/index.gohtml:33 +#: web/templates/admin/campsite/index.gohtml:34 msgid "No campsites added yet." msgstr "No se ha añadido ningún alojamiento todavía." #: web/templates/admin/campsite/type/form.gohtml:8 -#: web/templates/admin/campsite/type/form.gohtml:25 +#: web/templates/admin/campsite/type/form.gohtml:26 msgctxt "title" msgid "Edit Campsite Type" msgstr "Edición del tipo de alojamientos" #: web/templates/admin/campsite/type/form.gohtml:10 -#: web/templates/admin/campsite/type/form.gohtml:27 +#: web/templates/admin/campsite/type/form.gohtml:28 msgctxt "title" msgid "New Campsite Type" msgstr "Nuevo tipo de alojamiento" -#: web/templates/admin/campsite/type/form.gohtml:46 +#: web/templates/admin/campsite/type/form.gohtml:38 +#: web/templates/admin/campsite/type/index.gohtml:19 +msgctxt "campsite type" +msgid "Active" +msgstr "Activo" + +#: web/templates/admin/campsite/type/form.gohtml:47 +#: web/templates/admin/season/form.gohtml:47 #: web/templates/admin/profile.gohtml:26 msgctxt "input" msgid "Name" msgstr "Nombre" -#: web/templates/admin/campsite/type/form.gohtml:54 +#: web/templates/admin/campsite/type/form.gohtml:55 msgctxt "input" msgid "Description" msgstr "Descripción" #: web/templates/admin/campsite/type/index.gohtml:6 -#: web/templates/admin/campsite/type/index.gohtml:12 +#: web/templates/admin/campsite/type/index.gohtml:13 +#: web/templates/admin/layout.gohtml:67 msgctxt "title" msgid "Campsite Types" msgstr "Tipos de alojamientos" -#: web/templates/admin/campsite/type/index.gohtml:11 +#: web/templates/admin/campsite/type/index.gohtml:12 msgctxt "action" msgid "Add Type" msgstr "Añadir tipo" -#: web/templates/admin/campsite/type/index.gohtml:17 +#: web/templates/admin/campsite/type/index.gohtml:18 +#: web/templates/admin/season/index.gohtml:18 msgctxt "header" msgid "Name" msgstr "Nombre" -#: web/templates/admin/campsite/type/index.gohtml:31 +#: web/templates/admin/campsite/type/index.gohtml:32 msgid "No campsite types added yet." msgstr "No se ha añadido ningún tipo de alojamiento todavía." +#: web/templates/admin/season/form.gohtml:8 +#: web/templates/admin/season/form.gohtml:26 +msgctxt "title" +msgid "Edit Season" +msgstr "Edición de temporada" + +#: web/templates/admin/season/form.gohtml:10 +#: web/templates/admin/season/form.gohtml:28 +msgctxt "title" +msgid "New Season" +msgstr "Nueva temporada" + +#: web/templates/admin/season/form.gohtml:38 +#: web/templates/admin/season/index.gohtml:20 +msgctxt "season" +msgid "Active" +msgstr "Activa" + +#: web/templates/admin/season/form.gohtml:55 +msgctxt "input" +msgid "Color" +msgstr "Color" + +#: web/templates/admin/season/index.gohtml:6 +#: web/templates/admin/season/index.gohtml:13 +#: web/templates/admin/layout.gohtml:73 +msgctxt "title" +msgid "Seasons" +msgstr "Temporadas" + +#: web/templates/admin/season/index.gohtml:12 +msgctxt "action" +msgid "Add Season" +msgstr "Añadir temporada" + +#: web/templates/admin/season/index.gohtml:19 +msgctxt "header" +msgid "Color" +msgstr "Color" + +#: web/templates/admin/season/index.gohtml:38 +msgid "No seasons added yet." +msgstr "No se ha añadido ninguna temporada todavía." + #: web/templates/admin/dashboard.gohtml:6 #: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:49 msgctxt "title" @@ -175,7 +227,7 @@ msgid "Login" msgstr "Entrada" #: web/templates/admin/login.gohtml:22 web/templates/admin/profile.gohtml:35 -#: web/templates/admin/taxDetails.gohtml:50 +#: web/templates/admin/taxDetails.gohtml:51 msgctxt "input" msgid "Email" msgstr "Correo-e" @@ -217,79 +269,80 @@ msgid "Language" msgstr "Idioma" #: web/templates/admin/profile.gohtml:75 -#: web/templates/admin/taxDetails.gohtml:144 +#: web/templates/admin/taxDetails.gohtml:145 msgctxt "action" msgid "Save changes" msgstr "Guardar los cambios" #: web/templates/admin/taxDetails.gohtml:6 -#: web/templates/admin/taxDetails.gohtml:12 +#: web/templates/admin/taxDetails.gohtml:13 +#: web/templates/admin/layout.gohtml:64 msgctxt "title" msgid "Tax Details" msgstr "Configuración fiscal" -#: web/templates/admin/taxDetails.gohtml:17 -#: web/templates/admin/taxDetails.gohtml:58 +#: web/templates/admin/taxDetails.gohtml:18 +#: web/templates/admin/taxDetails.gohtml:59 msgctxt "input" msgid "Business Name" msgstr "Nombre de empresa" -#: web/templates/admin/taxDetails.gohtml:26 +#: web/templates/admin/taxDetails.gohtml:27 msgctxt "input" msgid "VAT Number" msgstr "NIF" -#: web/templates/admin/taxDetails.gohtml:34 +#: web/templates/admin/taxDetails.gohtml:35 msgctxt "input" msgid "Trade Name" msgstr "Nombre comercial" -#: web/templates/admin/taxDetails.gohtml:42 +#: web/templates/admin/taxDetails.gohtml:43 msgctxt "input" msgid "Phone" msgstr "Teléfono" -#: web/templates/admin/taxDetails.gohtml:66 +#: web/templates/admin/taxDetails.gohtml:67 msgctxt "input" msgid "Address" msgstr "Dirección" -#: web/templates/admin/taxDetails.gohtml:74 +#: web/templates/admin/taxDetails.gohtml:75 msgctxt "input" msgid "City" msgstr "Población" -#: web/templates/admin/taxDetails.gohtml:82 +#: web/templates/admin/taxDetails.gohtml:83 msgctxt "input" msgid "Province" msgstr "Provincia" -#: web/templates/admin/taxDetails.gohtml:90 +#: web/templates/admin/taxDetails.gohtml:91 msgctxt "input" msgid "Postal Code" msgstr "Código postal" -#: web/templates/admin/taxDetails.gohtml:98 +#: web/templates/admin/taxDetails.gohtml:99 msgctxt "input" msgid "Country" msgstr "País" -#: web/templates/admin/taxDetails.gohtml:108 +#: web/templates/admin/taxDetails.gohtml:109 msgctxt "input" msgid "Currency" msgstr "Moneda" -#: web/templates/admin/taxDetails.gohtml:118 +#: web/templates/admin/taxDetails.gohtml:119 msgctxt "input" msgid "Default Language" msgstr "Idioma por defecto" -#: web/templates/admin/taxDetails.gohtml:128 +#: web/templates/admin/taxDetails.gohtml:129 msgctxt "input" msgid "Invoice Number Format" msgstr "Formato de número de factura" -#: web/templates/admin/taxDetails.gohtml:136 +#: web/templates/admin/taxDetails.gohtml:137 msgctxt "input" msgid "Legal Disclaimer" msgstr "Nota legal" @@ -330,7 +383,7 @@ msgctxt "language option" msgid "Automatic" msgstr "Automático" -#: pkg/app/user.go:249 pkg/campsite/types/admin.go:197 +#: pkg/app/user.go:249 pkg/campsite/types/admin.go:197 pkg/season/admin.go:203 msgid "Name can not be empty." msgstr "No podéis dejar el nombre en blanco." @@ -346,7 +399,7 @@ msgstr "El idioma escogido no es válido." msgid "File must be a valid PNG or JPEG image." msgstr "El archivo tiene que ser una imagen PNG o JPEG válida." -#: pkg/app/admin.go:40 +#: pkg/app/admin.go:43 msgid "Access forbidden" msgstr "Acceso prohibido" @@ -358,6 +411,14 @@ msgstr "El tipo de alojamiento escogido no es válido." msgid "Label can not be empty." msgstr "No podéis dejar la etiqueta en blanco." +#: pkg/season/admin.go:204 +msgid "Color can not be empty." +msgstr "No podéis dejar el color en blanco." + +#: pkg/season/admin.go:205 +msgid "This color is not valid. It must be like #123abc." +msgstr "Este color no es válido. Tiene que ser parecido a #123abc." + #: pkg/company/admin.go:186 msgid "Selected country is not valid." msgstr "El país escogido no es válido." @@ -422,10 +483,6 @@ msgstr "No podéis dejar el formato de número de factura en blanco." msgid "Cross-site request forgery detected." msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados." -#~ msgctxt "title" -#~ msgid "Edit Page" -#~ msgstr "Edición de página" - #~ msgctxt "title" #~ msgid "New Page" #~ msgstr "Nueva página" @@ -449,6 +506,3 @@ msgstr "Se ha detectado un intento de falsificación de petición en sitios cruz #~ msgctxt "header" #~ msgid "Title" #~ msgstr "Título" - -#~ msgid "No pages added yet." -#~ msgstr "No se ha añadido ninguna página todavía." diff --git a/revert/add_season.sql b/revert/add_season.sql new file mode 100644 index 0000000..69a9e1f --- /dev/null +++ b/revert/add_season.sql @@ -0,0 +1,7 @@ +-- Revert camper:add_season from pg + +begin; + +drop function if exists camper.add_season(integer, text, camper.color); + +commit; diff --git a/revert/color.sql b/revert/color.sql new file mode 100644 index 0000000..98f1cf8 --- /dev/null +++ b/revert/color.sql @@ -0,0 +1,7 @@ +-- Revert camper:color from pg + +begin; + +drop domain if exists camper.color; + +commit; diff --git a/revert/edit_season.sql b/revert/edit_season.sql new file mode 100644 index 0000000..d87ce98 --- /dev/null +++ b/revert/edit_season.sql @@ -0,0 +1,7 @@ +-- Revert camper:edit_season from pg + +begin; + +drop function if exists camper.edit_season(uuid, text, camper.color, boolean); + +commit; diff --git a/revert/season.sql b/revert/season.sql new file mode 100644 index 0000000..afce013 --- /dev/null +++ b/revert/season.sql @@ -0,0 +1,7 @@ +-- Revert camper:season from pg + +begin; + +drop table if exists camper.season; + +commit; diff --git a/revert/to_color.sql b/revert/to_color.sql new file mode 100644 index 0000000..a39be3e --- /dev/null +++ b/revert/to_color.sql @@ -0,0 +1,7 @@ +-- Revert camper:to_color from pg + +begin; + +drop function if exists camper.to_color(integer); + +commit; diff --git a/revert/to_integer.sql b/revert/to_integer.sql new file mode 100644 index 0000000..4ad5e56 --- /dev/null +++ b/revert/to_integer.sql @@ -0,0 +1,7 @@ +-- Revert camper:to_integer from pg + +begin; + +drop function if exists camper.to_integer(camper.color); + +commit; diff --git a/sqitch.plan b/sqitch.plan index 46d5597..fe1681c 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -47,3 +47,9 @@ add_campsite [roles schema_camper campsite campsite_type] 2023-08-14T17:03:23Z j edit_campsite [roles schema_camper campsite] 2023-08-14T17:28:16Z jordi fita mas # Add function to update campsites input_is_valid [roles schema_public] 2023-08-15T20:10:59Z jordi fita mas # Add function to check if an input string is valid for a domain input_is_valid_phone [roles schema_public extension_pg_libphonenumber] 2023-08-15T20:15:01Z jordi fita mas # Add function to check if an input string is valid for the phone number domain +color [schema_camper extension_citext] 2023-08-16T12:46:43Z jordi fita mas # Add domain for HTML colors +to_integer [roles schema_camper color] 2023-08-16T13:02:08Z jordi fita mas # Add function to convert color to integer +to_color [roles schema_camper color] 2023-08-16T13:11:32Z jordi fita mas # Add function to convert integer to color +season [roles schema_camper company user_profile] 2023-08-16T13:21:28Z jordi fita mas # Add relation of (tourist) season +add_season [roles schema_camper season color to_integer] 2023-08-16T16:59:17Z jordi fita mas # Add function to create seasons +edit_season [roles schema_camper season color to_integer] 2023-08-16T17:09:02Z jordi fita mas # Add function to update seasons diff --git a/test/add_season.sql b/test/add_season.sql new file mode 100644 index 0000000..45a318c --- /dev/null +++ b/test/add_season.sql @@ -0,0 +1,60 @@ +-- Test add_season +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +set search_path to camper, public; + +select plan(13); + +select has_function('camper', 'add_season', array ['integer', 'text', 'color']); +select function_lang_is('camper', 'add_season', array ['integer', 'text', 'color'], 'sql'); +select function_returns('camper', 'add_season', array ['integer', 'text', 'color'], 'uuid'); +select isnt_definer('camper', 'add_season', array ['integer', 'text', 'color']); +select volatility_is('camper', 'add_season', array ['integer', 'text', 'color'], 'volatile'); +select function_privs_are('camper', 'add_season', array ['integer', 'text', 'color'], 'guest', array[]::text[]); +select function_privs_are('camper', 'add_season', array ['integer', 'text', 'color'], 'employee', array[]::text[]); +select function_privs_are('camper', 'add_season', array ['integer', 'text', 'color'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'add_season', array ['integer', 'text', 'color'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate season 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') + , (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca') +; + +select lives_ok( + $$ select add_season(1, 'Low', '#232323') $$, + 'Should be able to add a season to the first company' +); + +select lives_ok( + $$ select add_season(1, 'Mid', '#555555') $$, + 'Should be able to add another season to the same company' +); + +select lives_ok( + $$ select add_season(2, 'High', '#a0a0a0') $$, + 'Should be able to add a season to the second company' +); + +select bag_eq( + $$ select company_id, name, to_color(color)::text, active from season $$, + $$ values (1, 'Low', '#232323', true) + , (1, 'Mid', '#555555', true) + , (2, 'High', '#a0a0a0', true) + $$, + 'Should have added all seasons' +); + +select * +from finish(); + +rollback; diff --git a/test/color.sql b/test/color.sql new file mode 100644 index 0000000..9586694 --- /dev/null +++ b/test/color.sql @@ -0,0 +1,46 @@ +-- Test color +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(8); + +set search_path to camper, public; + +select has_domain('color'); +select domain_type_is('color', 'citext'); + +select lives_ok($$ select '#775544'::color $$, 'Should be able to cast strings to color'); +select lives_ok($$ select '#aABbCc'::color $$, 'Should be able to cast hex-strings to color'); + +select throws_ok( + $$ select '0775544'::color $$, + 23514, null, + 'Should reject colors without the initial hash character' +); + +select throws_ok( + $$ select '#red'::color $$, + 23514, null, + 'Should reject named colors' +); + +select throws_ok( + $$ select '#0011ag'::color $$, + 23514, null, + 'Should reject colors with invalid hex digits' +); + +select throws_ok( + $$ select '#00112233'::color $$, + 23514, null, + 'Should reject colors with more than three pairs (i.e., no alpha)' +); + + +select * +from finish(); + +rollback; diff --git a/test/edit_season.sql b/test/edit_season.sql new file mode 100644 index 0000000..4a68109 --- /dev/null +++ b/test/edit_season.sql @@ -0,0 +1,58 @@ +-- Test edit_season +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +set search_path to camper, public; + +select plan(12); + +select has_function('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean']); +select function_lang_is('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean'], 'sql'); +select function_returns('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean'], 'uuid'); +select isnt_definer('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean']); +select volatility_is('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean'], 'volatile'); +select function_privs_are('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean'], 'guest', array[]::text[]); +select function_privs_are('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean'], 'employee', array[]::text[]); +select function_privs_are('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean'], 'authenticator', array[]::text[]); + +set client_min_messages to warning; +truncate season 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 season (company_id, slug, name, color, active) +values (1, '87452b88-b48f-48d3-bb6c-0296de64164e', 'Low', to_integer('#232323'), true) + , (1, '9b6370f7-f941-46f2-bc6e-de455675bd0a', 'High', to_integer('#323232'), false) +; + +select lives_ok( + $$ select edit_season('87452b88-b48f-48d3-bb6c-0296de64164e', 'Very Low', '#1e1e1e', false) $$, + 'Should be able to edit the first season' +); + +select lives_ok( + $$ select edit_season('9b6370f7-f941-46f2-bc6e-de455675bd0a', 'Very High', '#9f9f9f', true) $$, + 'Should be able to edit the second season' +); + +select bag_eq( + $$ select slug::text, name, to_color(color)::text, active from season $$, + $$ values ('87452b88-b48f-48d3-bb6c-0296de64164e', 'Very Low', '#1e1e1e', false) + , ('9b6370f7-f941-46f2-bc6e-de455675bd0a', 'Very High', '#9f9f9f', true) + $$, + 'Should have updated all seasons.' +); + +select * +from finish(); + +rollback; diff --git a/test/season.sql b/test/season.sql new file mode 100644 index 0000000..7275cde --- /dev/null +++ b/test/season.sql @@ -0,0 +1,206 @@ +-- Test season +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(57); + +set search_path to camper, public; + +select has_table('season'); +select has_pk('season' ); +select table_privs_are('season', 'guest', array['SELECT']); +select table_privs_are('season', 'employee', array['SELECT']); +select table_privs_are('season', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('season', 'authenticator', array[]::text[]); + +select has_sequence('season_season_id_seq'); +select sequence_privs_are('season_season_id_seq', 'guest', array[]::text[]); +select sequence_privs_are('season_season_id_seq', 'employee', array[]::text[]); +select sequence_privs_are('season_season_id_seq', 'admin', array['USAGE']); +select sequence_privs_are('season_season_id_seq', 'authenticator', array[]::text[]); + +select has_column('season', 'season_id'); +select col_is_pk('season', 'season_id'); +select col_type_is('season', 'season_id', 'integer'); +select col_not_null('season', 'season_id'); +select col_has_default('season', 'season_id'); +select col_default_is('season', 'season_id', 'nextval(''season_season_id_seq''::regclass)'); + +select has_column('season', 'company_id'); +select col_is_fk('season', 'company_id'); +select fk_ok('season', 'company_id', 'company', 'company_id'); +select col_type_is('season', 'company_id', 'integer'); +select col_not_null('season', 'company_id'); +select col_hasnt_default('season', 'company_id'); + +select has_column('season', 'slug'); +select col_is_unique('season', 'slug'); +select col_type_is('season', 'slug', 'uuid'); +select col_not_null('season', 'slug'); +select col_has_default('season', 'slug'); +select col_default_is('season', 'slug', 'gen_random_uuid()'); + +select has_column('season', 'name'); +select col_type_is('season', 'name', 'text'); +select col_not_null('season', 'name'); +select col_hasnt_default('season', 'name'); + +select has_column('season', 'color'); +select col_type_is('season', 'color', 'integer'); +select col_not_null('season', 'color'); +select col_has_default('season', 'color'); +select col_default_is('season', 'color', '0'); + +select has_column('season', 'active'); +select col_type_is('season', 'active', 'boolean'); +select col_not_null('season', 'active'); +select col_has_default('season', 'active'); +select col_default_is('season', 'active', 'true'); + + +set client_min_messages to warning; +truncate season 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 season (company_id, name) +values (2, 'Low') + , (4, 'High') +; + +prepare season_data as +select company_id, name +from season +order by company_id, name; + +set role guest; +select bag_eq( + 'season_data', + $$ values (2, 'Low') + , (4, 'High') + $$, + 'Everyone should be able to list all seasons across all companies' +); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2'); + +select lives_ok( + $$ insert into season(company_id, name) values (2, 'Another type' ) $$, + 'Admin from company 2 should be able to insert a new season to that company.' +); + +select bag_eq( + 'season_data', + $$ values (2, 'Low') + , (2, 'Another type') + , (4, 'High') + $$, + 'The new row should have been added' +); + +select lives_ok( + $$ update season set name = 'Another' where company_id = 2 and name = 'Another type' $$, + 'Admin from company 2 should be able to update season of that company.' +); + +select bag_eq( + 'season_data', + $$ values (2, 'Low') + , (2, 'Another') + , (4, 'High') + $$, + 'The row should have been updated.' +); + +select lives_ok( + $$ delete from season where company_id = 2 and name = 'Another' $$, + 'Admin from company 2 should be able to delete season from that company.' +); + +select bag_eq( + 'season_data', + $$ values (2, 'Low') + , (4, 'High') + $$, + 'The row should have been deleted.' +); + +select throws_ok( + $$ insert into season (company_id, name) values (4, 'Another type' ) $$, + '42501', 'new row violates row-level security policy for table "season"', + 'Admin from company 2 should NOT be able to insert new seasons to company 4.' +); + +select lives_ok( + $$ update season set name = 'Nope' where company_id = 4 $$, + 'Admin from company 2 should not be able to update new seasons of company 4, but no error if company_id is not changed.' +); + +select bag_eq( + 'season_data', + $$ values (2, 'Low') + , (4, 'High') + $$, + 'No row should have been changed.' +); + +select throws_ok( + $$ update season set company_id = 4 where company_id = 2 $$, + '42501', 'new row violates row-level security policy for table "season"', + 'Admin from company 2 should NOT be able to move seasons to company 4' +); + +select lives_ok( + $$ delete from season where company_id = 4 $$, + 'Admin from company 2 should NOT be able to delete seasons from company 4, but not error is thrown' +); + +select bag_eq( + 'season_data', + $$ values (2, 'Low') + , (4, 'High') + $$, + 'No row should have been changed' +); + +select throws_ok( + $$ insert into season (company_id, name) values (2, ' ' ) $$, + '23514', 'new row for relation "season" violates check constraint "name_not_empty"', + 'Should not be able to insert seasons with a blank name.' +); + +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/to_color.sql b/test/to_color.sql new file mode 100644 index 0000000..298e801 --- /dev/null +++ b/test/to_color.sql @@ -0,0 +1,37 @@ +-- Test to_color +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +set search_path to camper, public; + +select plan(20); + +select has_function('camper', 'to_color', array ['integer']); +select function_lang_is('camper', 'to_color', array ['integer'], 'sql'); +select function_returns('camper', 'to_color', array ['integer'], 'color'); +select isnt_definer('camper', 'to_color', array ['integer']); +select volatility_is('camper', 'to_color', array ['integer'], 'immutable'); +select function_privs_are('camper', 'to_color', array ['integer'], 'guest', array['EXECUTE']); +select function_privs_are('camper', 'to_color', array ['integer'], 'employee', array['EXECUTE']); +select function_privs_are('camper', 'to_color', array ['integer'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'to_color', array ['integer'], 'authenticator', array['EXECUTE']); + +select is( to_color(1122867), '#112233'); +select is( to_color(0), '#000000'); +select is( to_color(15), '#00000f'); +select is( to_color(255), '#0000ff'); +select is( to_color(4095), '#000fff'); +select is( to_color(65535), '#00ffff'); +select is( to_color(1048575), '#0fffff'); +select is( to_color(16777215), '#ffffff'); +select is( to_color(-1), '#ffffff'); +select is( to_color(-559038737), '#deadbe'); +select is( to_color(-2147483648), '#800000'); + +select * +from finish(); + +rollback; diff --git a/test/to_integer.sql b/test/to_integer.sql new file mode 100644 index 0000000..24941a9 --- /dev/null +++ b/test/to_integer.sql @@ -0,0 +1,40 @@ +-- Test to_integer +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +set search_path to camper, public; + +select plan(23); + +select has_function('camper', 'to_integer', array ['color']); +select function_lang_is('camper', 'to_integer', array ['color'], 'sql'); +select function_returns('camper', 'to_integer', array ['color'], 'integer'); +select isnt_definer('camper', 'to_integer', array ['color']); +select volatility_is('camper', 'to_integer', array ['color'], 'immutable'); +select function_privs_are('camper', 'to_integer', array ['color'], 'guest', array['EXECUTE']); +select function_privs_are('camper', 'to_integer', array ['color'], 'employee', array['EXECUTE']); +select function_privs_are('camper', 'to_integer', array ['color'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'to_integer', array ['color'], 'authenticator', array['EXECUTE']); + +select is( to_integer('#112233'), 1122867 ); +select is( to_integer('#000000'), 0 ); +select is( to_integer('#00000f'), 15 ); +select is( to_integer('#00000F'), 15 ); +select is( to_integer('#0000ff'), 255 ); +select is( to_integer('#0000FF'), 255 ); +select is( to_integer('#000fff'), 4095 ); +select is( to_integer('#000FFF'), 4095 ); +select is( to_integer('#00ffff'), 65535 ); +select is( to_integer('#00FFFF'), 65535 ); +select is( to_integer('#0fffff'), 1048575 ); +select is( to_integer('#0FFFFF'), 1048575 ); +select is( to_integer('#ffffff'), 16777215 ); +select is( to_integer('#FFFFFF'), 16777215 ); + +select * +from finish(); + +rollback; diff --git a/verify/add_season.sql b/verify/add_season.sql new file mode 100644 index 0000000..72c87cd --- /dev/null +++ b/verify/add_season.sql @@ -0,0 +1,7 @@ +-- Verify camper:add_season on pg + +begin; + +select has_function_privilege('camper.add_season(integer, text, camper.color)', 'execute'); + +rollback; diff --git a/verify/color.sql b/verify/color.sql new file mode 100644 index 0000000..d01d5d9 --- /dev/null +++ b/verify/color.sql @@ -0,0 +1,7 @@ +-- Verify camper:color on pg + +begin; + +select pg_catalog.has_type_privilege('camper.color', 'usage'); + +rollback; diff --git a/verify/edit_season.sql b/verify/edit_season.sql new file mode 100644 index 0000000..b0c44f0 --- /dev/null +++ b/verify/edit_season.sql @@ -0,0 +1,7 @@ +-- Verify camper:edit_season on pg + +begin; + +select has_function_privilege('camper.edit_season(uuid, text, camper.color, boolean)', 'execute'); + +rollback; diff --git a/verify/season.sql b/verify/season.sql new file mode 100644 index 0000000..b492e2e --- /dev/null +++ b/verify/season.sql @@ -0,0 +1,20 @@ +-- Verify camper:season on pg + +begin; + +select season_id + , company_id + , slug + , name + , color + , active +from camper.season +where false; + +select 1 / count(*) from pg_class where oid = 'camper.season'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.season'::regclass; +select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.season'::regclass; +select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.season'::regclass; +select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.season'::regclass; + +rollback; diff --git a/verify/to_color.sql b/verify/to_color.sql new file mode 100644 index 0000000..6f6f195 --- /dev/null +++ b/verify/to_color.sql @@ -0,0 +1,7 @@ +-- Verify camper:to_color on pg + +begin; + +select has_function_privilege('camper.to_color(integer)', 'execute'); + +rollback; diff --git a/verify/to_integer.sql b/verify/to_integer.sql new file mode 100644 index 0000000..ffc9fc9 --- /dev/null +++ b/verify/to_integer.sql @@ -0,0 +1,7 @@ +-- Verify camper:to_integer on pg + +begin; + +select has_function_privilege('camper.to_integer(camper.color)', 'execute'); + +rollback; diff --git a/web/templates/admin/layout.gohtml b/web/templates/admin/layout.gohtml index 6d30610..6fb46f4 100644 --- a/web/templates/admin/layout.gohtml +++ b/web/templates/admin/layout.gohtml @@ -69,6 +69,9 @@
  • {{( pgettext "Campsites" "title" )}}
  • +
  • + {{( pgettext "Seasons" "title" )}} +
  • {{- end }} diff --git a/web/templates/admin/season/form.gohtml b/web/templates/admin/season/form.gohtml new file mode 100644 index 0000000..434dd5f --- /dev/null +++ b/web/templates/admin/season/form.gohtml @@ -0,0 +1,72 @@ + +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.seasonForm*/ -}} + {{ if .Slug}} + {{( pgettext "Edit Season" "title" )}} + {{ else }} + {{( pgettext "New Season" "title" )}} + {{ end }} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.seasonForm*/ -}} + {{ template "settings-tabs" "seasons" }} +
    +

    + {{ if .Slug }} + {{( pgettext "Edit Season" "title" )}} + {{ else }} + {{( pgettext "New Season" "title" )}} + {{ end }} +

    + {{ CSRFInput }} +
    + {{ if .Slug }} + {{ with .Active -}} + + {{ template "error-message" . }} + {{- end }} + {{ else }} + + {{ end }} + {{ with .Name -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Color -}} + + {{ template "error-message" . }} + {{- end }} +
    +
    + +
    +
    +{{- end }} diff --git a/web/templates/admin/season/index.gohtml b/web/templates/admin/season/index.gohtml new file mode 100644 index 0000000..15a3b1a --- /dev/null +++ b/web/templates/admin/season/index.gohtml @@ -0,0 +1,40 @@ + +{{ define "title" -}} + {{( pgettext "Seasons" "title" )}} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.seasonIndex*/ -}} + {{ template "settings-tabs" "seasons" }} + {{( pgettext "Add Season" "action" )}} +

    {{( pgettext "Seasons" "title" )}}

    + {{ if .Seasons -}} + + + + + + + + + + {{ range .Seasons -}} + + + + + + {{- end }} + +
    {{( pgettext "Name" "header" )}}{{( pgettext "Color" "header" )}}{{( pgettext "Active" "season" )}}
    {{ .Name }} + + + + {{ if .Active }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }}
    + {{ else -}} +

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

    + {{- end }} +{{- end }}