From b4919db6c43018770b663f5e4fbf57fd4671e9b2 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Wed, 16 Aug 2023 20:15:57 +0200 Subject: [PATCH] =?UTF-8?q?Add=20seasons=E2=80=99=20relation,=20functions,?= =?UTF-8?q?=20and=20admin=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seasons have a color to show on the calendar. I need them in HTML format (e.g., #123abc) in order to set as value to ``, but i did not want to save them as text in the database, as integers are better representations of colors—in fact, that’s what the HTML syntax also is: an integer. I think the best would be to create an extension that adds an HTML color type, with functions to convert from many representations (e.g., CSS’ rgb or even color names) to integer and back. However, that’s a lot of work and i can satisfy Camper’s needs with just a couple of functions and a domain. To show the color on the index, at first tried to use a read-only ``, but seems that this type of input can not be read-only and must be disabled instead. However, i do not know whether it makes sense to have a disabled input outside a form “just” to show a color; i suspect it does not. Thus, at the end i use SVG with a single circle, which is better that a 50%-rounded div with a background color, even if the result is the same—SVG **is** intended for showing pictures, which is this case. --- deploy/add_season.sql | 24 +++ deploy/color.sql | 14 ++ deploy/edit_season.sql | 27 +++ deploy/season.sql | 58 +++++++ deploy/to_color.sql | 18 ++ deploy/to_integer.sql | 18 ++ pkg/app/admin.go | 5 + pkg/form/validator.go | 8 + pkg/season/admin.go | 214 ++++++++++++++++++++++++ po/ca.po | 178 +++++++++++++------- po/es.po | 178 +++++++++++++------- revert/add_season.sql | 7 + revert/color.sql | 7 + revert/edit_season.sql | 7 + revert/season.sql | 7 + revert/to_color.sql | 7 + revert/to_integer.sql | 7 + sqitch.plan | 6 + test/add_season.sql | 60 +++++++ test/color.sql | 46 +++++ test/edit_season.sql | 58 +++++++ test/season.sql | 206 +++++++++++++++++++++++ test/to_color.sql | 37 ++++ test/to_integer.sql | 40 +++++ verify/add_season.sql | 7 + verify/color.sql | 7 + verify/edit_season.sql | 7 + verify/season.sql | 20 +++ verify/to_color.sql | 7 + verify/to_integer.sql | 7 + web/templates/admin/layout.gohtml | 3 + web/templates/admin/season/form.gohtml | 72 ++++++++ web/templates/admin/season/index.gohtml | 40 +++++ 33 files changed, 1283 insertions(+), 124 deletions(-) create mode 100644 deploy/add_season.sql create mode 100644 deploy/color.sql create mode 100644 deploy/edit_season.sql create mode 100644 deploy/season.sql create mode 100644 deploy/to_color.sql create mode 100644 deploy/to_integer.sql create mode 100644 pkg/season/admin.go create mode 100644 revert/add_season.sql create mode 100644 revert/color.sql create mode 100644 revert/edit_season.sql create mode 100644 revert/season.sql create mode 100644 revert/to_color.sql create mode 100644 revert/to_integer.sql create mode 100644 test/add_season.sql create mode 100644 test/color.sql create mode 100644 test/edit_season.sql create mode 100644 test/season.sql create mode 100644 test/to_color.sql create mode 100644 test/to_integer.sql create mode 100644 verify/add_season.sql create mode 100644 verify/color.sql create mode 100644 verify/edit_season.sql create mode 100644 verify/season.sql create mode 100644 verify/to_color.sql create mode 100644 verify/to_integer.sql create mode 100644 web/templates/admin/season/form.gohtml create mode 100644 web/templates/admin/season/index.gohtml 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 }}