From 216ae20638e9ad819a73b58ed63c5bc6c8f6f707 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Mon, 14 Aug 2023 20:18:26 +0200 Subject: [PATCH] Add the campsite relation, HTTP handlers, and form For now, there is only the label, type, and active fields. We will need some field to hold the area on the map, but this requires #4, and possibly #6, to be finished. Part of #27. --- deploy/add_campsite.sql | 34 ++++ deploy/campsite.sql | 59 ++++++ deploy/edit_campsite.sql | 25 +++ pkg/campsite/admin.go | 188 ++++++++++++++++++- po/ca.po | 119 +++++++++--- po/es.po | 119 +++++++++--- revert/add_campsite.sql | 7 + revert/campsite.sql | 7 + revert/edit_campsite.sql | 7 + sqitch.plan | 3 + test/add_campsite.sql | 74 ++++++++ test/campsite.sql | 213 ++++++++++++++++++++++ test/edit_campsite.sql | 66 +++++++ verify/add_campsite.sql | 7 + verify/campsite.sql | 20 ++ verify/edit_campsite.sql | 7 + web/templates/admin/campsite/form.gohtml | 77 ++++++++ web/templates/admin/campsite/index.gohtml | 35 ++++ 18 files changed, 1016 insertions(+), 51 deletions(-) create mode 100644 deploy/add_campsite.sql create mode 100644 deploy/campsite.sql create mode 100644 deploy/edit_campsite.sql create mode 100644 revert/add_campsite.sql create mode 100644 revert/campsite.sql create mode 100644 revert/edit_campsite.sql create mode 100644 test/add_campsite.sql create mode 100644 test/campsite.sql create mode 100644 test/edit_campsite.sql create mode 100644 verify/add_campsite.sql create mode 100644 verify/campsite.sql create mode 100644 verify/edit_campsite.sql create mode 100644 web/templates/admin/campsite/form.gohtml create mode 100644 web/templates/admin/campsite/index.gohtml diff --git a/deploy/add_campsite.sql b/deploy/add_campsite.sql new file mode 100644 index 0000000..8cd23d5 --- /dev/null +++ b/deploy/add_campsite.sql @@ -0,0 +1,34 @@ +-- Deploy camper:add_campsite to pg +-- requires: roles +-- requires: schema_camper +-- requires: campsite +-- requires: campsite_type + +begin; + +set search_path to camper, public; + +create or replace function add_campsite(campsite_type integer, label text) returns uuid as +$$ +declare + campsite_slug uuid; +begin + insert into campsite (company_id, campsite_type_id, label) + select company_id, campsite_type_id, label + from campsite_type + where campsite_type_id = add_campsite.campsite_type + returning slug into campsite_slug + ; + if campsite_slug is null then + raise foreign_key_violation using message = 'insert or update on table "campsite" violates foreign key constraint "campsite_campsite_type_id_fkey"'; + end if; + return campsite_slug; +end +$$ + language plpgsql +; + +revoke execute on function add_campsite(integer, text) from public; +grant execute on function add_campsite(integer, text) to admin; + +commit; diff --git a/deploy/campsite.sql b/deploy/campsite.sql new file mode 100644 index 0000000..d3c5f0a --- /dev/null +++ b/deploy/campsite.sql @@ -0,0 +1,59 @@ +-- Deploy camper:campsite to pg +-- requires: roles +-- requires: schema_camper +-- requires: company +-- requires: campsite_type +-- requires: user_profile + +begin; + +set search_path to camper, public; + +create table campsite ( + campsite_id serial primary key, + company_id integer not null references company, + slug uuid unique not null default gen_random_uuid(), + campsite_type_id integer not null references campsite_type, + label text not null constraint label_not_empty check(length(trim(label)) > 0), + active boolean not null default true +); + +grant select on table campsite to guest; +grant select on table campsite to employee; +grant select, insert, update, delete on table campsite to admin; + +grant usage on sequence campsite_campsite_id_seq to admin; + +alter table campsite enable row level security; + +create policy guest_ok +on campsite +for select +using (true) +; + +create policy insert_to_company +on campsite +for insert +with check ( + company_id in (select company_id from user_profile) +) +; + +create policy update_company +on campsite +for update +using ( + company_id in (select company_id from user_profile) +) +; + +create policy delete_from_company +on campsite +for delete +using ( + company_id in (select company_id from user_profile) +) +; + +commit; diff --git a/deploy/edit_campsite.sql b/deploy/edit_campsite.sql new file mode 100644 index 0000000..90fe3b3 --- /dev/null +++ b/deploy/edit_campsite.sql @@ -0,0 +1,25 @@ +-- Deploy camper:edit_campsite to pg +-- requires: roles +-- requires: schema_camper +-- requires: campsite + +begin; + +set search_path to camper, public; + +create or replace function edit_campsite(slug uuid, campsite_type integer, label text, active boolean) returns uuid as +$$ + update campsite + set label = edit_campsite.label + , campsite_type_id = edit_campsite.campsite_type + , active = edit_campsite.active + where slug = edit_campsite.slug + returning slug; +$$ + language sql +; + +revoke execute on function edit_campsite(uuid, integer, text, boolean) from public; +grant execute on function edit_campsite(uuid, integer, text, boolean) to admin; + +commit; diff --git a/pkg/campsite/admin.go b/pkg/campsite/admin.go index 151e50c..902597a 100644 --- a/pkg/campsite/admin.go +++ b/pkg/campsite/admin.go @@ -6,12 +6,17 @@ package campsite import ( + "context" "net/http" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/campsite/types" "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 { @@ -30,10 +35,191 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { + case "new": + switch r.Method { + case http.MethodGet: + f := newCampsiteForm(r.Context(), conn) + f.MustRender(w, r, user, company) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet) + } case "types": h.types.Handler(user, company, conn).ServeHTTP(w, r) + case "": + switch r.Method { + case http.MethodGet: + serveCampsiteIndex(w, r, user, company, conn) + case http.MethodPost: + addCampsite(w, r, user, company, conn) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) + } default: - http.NotFound(w, r) + if !uuid.Valid(head) { + http.NotFound(w, r) + return + } + f := newCampsiteForm(r.Context(), conn) + 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: + editCampsite(w, r, user, company, conn, f) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) + } } } } + +func serveCampsiteIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { + campsites, err := collectCampsiteEntries(r.Context(), company, conn) + if err != nil { + panic(err) + } + page := &campsiteIndex{ + Campsites: campsites, + } + page.MustRender(w, r, user, company) +} + +func collectCampsiteEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*campsiteEntry, error) { + rows, err := conn.Query(ctx, ` + select campsite.slug + , campsite.label + , campsite_type.name + , campsite.active + from campsite + join campsite_type using (campsite_type_id) + where campsite.company_id = $1 + order by label`, company.ID) + if err != nil { + return nil, err + } + defer rows.Close() + + var campsites []*campsiteEntry + for rows.Next() { + entry := &campsiteEntry{} + if err = rows.Scan(&entry.Slug, &entry.Label, &entry.Type, &entry.Active); err != nil { + return nil, err + } + campsites = append(campsites, entry) + } + + return campsites, nil +} + +type campsiteEntry struct { + Slug string + Label string + Type string + Active bool +} + +type campsiteIndex struct { + Campsites []*campsiteEntry +} + +func (page *campsiteIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "campsite/index.gohtml", page) +} + +func addCampsite(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { + f := newCampsiteForm(r.Context(), conn) + 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 !f.Valid(user.Locale) { + if !httplib.IsHTMxRequest(r) { + w.WriteHeader(http.StatusUnprocessableEntity) + } + f.MustRender(w, r, user, company) + return + } + conn.MustExec(r.Context(), "select add_campsite($1, $2)", f.CampsiteType, f.Label) + httplib.Redirect(w, r, "/admin/campsites", http.StatusSeeOther) +} + +func editCampsite(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *campsiteForm) { + 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 !f.Valid(user.Locale) { + if !httplib.IsHTMxRequest(r) { + w.WriteHeader(http.StatusUnprocessableEntity) + } + f.MustRender(w, r, user, company) + return + } + conn.MustExec(r.Context(), "select edit_campsite($1, $2, $3, $4)", f.Slug, f.CampsiteType, f.Label, f.Active) + httplib.Redirect(w, r, "/admin/campsites", http.StatusSeeOther) +} + +type campsiteForm struct { + Slug string + Active *form.Checkbox + CampsiteType *form.Select + Label *form.Input +} + +func newCampsiteForm(ctx context.Context, conn *database.Conn) *campsiteForm { + campsiteTypes := form.MustGetOptions(ctx, conn, "select campsite_type_id::text, name from campsite_type where active") + return &campsiteForm{ + Active: &form.Checkbox{ + Name: "active", + Checked: true, + }, + CampsiteType: &form.Select{ + Name: "description", + Options: campsiteTypes, + }, + Label: &form.Input{ + Name: "label", + }, + } +} + +func (f *campsiteForm) FillFromDatabase(ctx context.Context, conn *database.Conn, slug string) error { + f.Slug = slug + row := conn.QueryRow(ctx, "select array[campsite_type_id::text], label, active from campsite where slug = $1", slug) + return row.Scan(&f.CampsiteType.Selected, &f.Label.Val, &f.Active.Checked) +} + +func (f *campsiteForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.Active.FillValue(r) + f.CampsiteType.FillValue(r) + f.Label.FillValue(r) + return nil +} + +func (f *campsiteForm) Valid(l *locale.Locale) bool { + v := form.NewValidator(l) + v.CheckSelectedOptions(f.CampsiteType, l.GettextNoop("Selected campsite type is not valid.")) + v.CheckRequired(f.Label, l.GettextNoop("Label can not be empty.")) + return v.AllOK +} + +func (f *campsiteForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "campsite/form.gohtml", f) +} diff --git a/po/ca.po b/po/ca.po index 50124ca..540936a 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-14 11:39+0200\n" +"POT-Creation-Date: 2023-08-14 20:09+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -35,6 +35,91 @@ msgstr "Salta al contingut principal" msgid "Singular Lodges" msgstr "Allotjaments singulars" +#: web/templates/admin/campsite/form.gohtml:8 +#: web/templates/admin/campsite/form.gohtml:25 +msgctxt "title" +msgid "Edit Campsite" +msgstr "Edició de l’allotjament" + +#: web/templates/admin/campsite/form.gohtml:10 +#: web/templates/admin/campsite/form.gohtml:27 +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" +msgid "Active" +msgstr "Actiu" + +#: web/templates/admin/campsite/form.gohtml:46 +msgctxt "input" +msgid "Campsite Type" +msgstr "Tipus d’allotjament" + +#: web/templates/admin/campsite/form.gohtml:51 +msgid "Select campsite type" +msgstr "Escolliu un tipus d’allotjament" + +#: web/templates/admin/campsite/form.gohtml:60 +msgctxt "input" +msgid "Label" +msgstr "Etiqueta" + +#: web/templates/admin/campsite/form.gohtml:70 +#: web/templates/admin/campsite/type/form.gohtml:63 +msgctxt "action" +msgid "Update" +msgstr "Actualitza" + +#: web/templates/admin/campsite/form.gohtml:72 +#: web/templates/admin/campsite/type/form.gohtml:65 +msgctxt "action" +msgid "Add" +msgstr "Afegeix" + +#: web/templates/admin/campsite/index.gohtml:6 +#: web/templates/admin/campsite/index.gohtml:12 +msgctxt "title" +msgid "Campsites" +msgstr "Allotjaments" + +#: web/templates/admin/campsite/index.gohtml:11 +msgctxt "action" +msgid "Add Campsite" +msgstr "Afegeix allotjament" + +#: web/templates/admin/campsite/index.gohtml:17 +msgctxt "header" +msgid "Label" +msgstr "Etiqueta" + +#: web/templates/admin/campsite/index.gohtml:18 +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 +msgid "Yes" +msgstr "Sí" + +#: web/templates/admin/campsite/index.gohtml:27 +#: web/templates/admin/campsite/type/index.gohtml:25 +msgid "No" +msgstr "No" + +#: web/templates/admin/campsite/index.gohtml:33 +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 msgctxt "title" @@ -47,12 +132,6 @@ msgctxt "title" msgid "New Campsite Type" msgstr "Nou tipus d’allotjament" -#: web/templates/admin/campsite/type/form.gohtml:37 -#: web/templates/admin/campsite/type/index.gohtml:18 -msgctxt "campsite type" -msgid "Active" -msgstr "Actiu" - #: web/templates/admin/campsite/type/form.gohtml:46 #: web/templates/admin/profile.gohtml:26 msgctxt "input" @@ -64,16 +143,6 @@ msgctxt "input" msgid "Description" msgstr "Descripció" -#: web/templates/admin/campsite/type/form.gohtml:63 -msgctxt "action" -msgid "Update" -msgstr "Actualitza" - -#: web/templates/admin/campsite/type/form.gohtml:65 -msgctxt "action" -msgid "Add" -msgstr "Afegeix" - #: web/templates/admin/campsite/type/index.gohtml:6 #: web/templates/admin/campsite/type/index.gohtml:12 msgctxt "title" @@ -90,14 +159,6 @@ msgctxt "header" msgid "Name" msgstr "Nom" -#: web/templates/admin/campsite/type/index.gohtml:25 -msgid "Yes" -msgstr "Sí" - -#: web/templates/admin/campsite/type/index.gohtml:25 -msgid "No" -msgstr "No" - #: web/templates/admin/campsite/type/index.gohtml:31 msgid "No campsite types added yet." msgstr "No s’ha afegit cap tipus d’allotjament encara." @@ -210,6 +271,14 @@ msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida." msgid "Access forbidden" msgstr "Accés prohibit" +#: pkg/campsite/admin.go:218 +msgid "Selected campsite type is not valid." +msgstr "El tipus d’allotjament escollit no és vàlid." + +#: pkg/campsite/admin.go:219 +msgid "Label can not be empty." +msgstr "No podeu deixar l’etiqueta en blanc." + #: pkg/auth/user.go:40 msgid "Cross-site request forgery detected." msgstr "S’ha detectat un intent de falsificació de petició a llocs creuats." diff --git a/po/es.po b/po/es.po index a35bb89..f319b30 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-14 11:39+0200\n" +"POT-Creation-Date: 2023-08-14 20:09+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -35,6 +35,91 @@ msgstr "Saltar al contenido principal" msgid "Singular Lodges" msgstr "Alojamientos singulares" +#: web/templates/admin/campsite/form.gohtml:8 +#: web/templates/admin/campsite/form.gohtml:25 +msgctxt "title" +msgid "Edit Campsite" +msgstr "Edición del alojamientos" + +#: web/templates/admin/campsite/form.gohtml:10 +#: web/templates/admin/campsite/form.gohtml:27 +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" +msgid "Active" +msgstr "Activo" + +#: web/templates/admin/campsite/form.gohtml:46 +msgctxt "input" +msgid "Campsite Type" +msgstr "Tipo de alojamiento" + +#: web/templates/admin/campsite/form.gohtml:51 +msgid "Select campsite type" +msgstr "Escoged un tipo de alojamiento" + +#: web/templates/admin/campsite/form.gohtml:60 +msgctxt "input" +msgid "Label" +msgstr "Etiqueta" + +#: web/templates/admin/campsite/form.gohtml:70 +#: web/templates/admin/campsite/type/form.gohtml:63 +msgctxt "action" +msgid "Update" +msgstr "Actualizar" + +#: web/templates/admin/campsite/form.gohtml:72 +#: web/templates/admin/campsite/type/form.gohtml:65 +msgctxt "action" +msgid "Add" +msgstr "Añadir" + +#: web/templates/admin/campsite/index.gohtml:6 +#: web/templates/admin/campsite/index.gohtml:12 +msgctxt "title" +msgid "Campsites" +msgstr "Alojamientos" + +#: web/templates/admin/campsite/index.gohtml:11 +msgctxt "action" +msgid "Add Campsite" +msgstr "Añadir alojamiento" + +#: web/templates/admin/campsite/index.gohtml:17 +msgctxt "header" +msgid "Label" +msgstr "Etiqueta" + +#: web/templates/admin/campsite/index.gohtml:18 +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 +msgid "Yes" +msgstr "Sí" + +#: web/templates/admin/campsite/index.gohtml:27 +#: web/templates/admin/campsite/type/index.gohtml:25 +msgid "No" +msgstr "No" + +#: web/templates/admin/campsite/index.gohtml:33 +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 msgctxt "title" @@ -47,12 +132,6 @@ msgctxt "title" msgid "New Campsite Type" msgstr "Nuevo tipo de alojamiento" -#: web/templates/admin/campsite/type/form.gohtml:37 -#: web/templates/admin/campsite/type/index.gohtml:18 -msgctxt "campsite type" -msgid "Active" -msgstr "Activo" - #: web/templates/admin/campsite/type/form.gohtml:46 #: web/templates/admin/profile.gohtml:26 msgctxt "input" @@ -64,16 +143,6 @@ msgctxt "input" msgid "Description" msgstr "Descripción" -#: web/templates/admin/campsite/type/form.gohtml:63 -msgctxt "action" -msgid "Update" -msgstr "Actualitzar" - -#: web/templates/admin/campsite/type/form.gohtml:65 -msgctxt "action" -msgid "Add" -msgstr "Añadir" - #: web/templates/admin/campsite/type/index.gohtml:6 #: web/templates/admin/campsite/type/index.gohtml:12 msgctxt "title" @@ -90,14 +159,6 @@ msgctxt "header" msgid "Name" msgstr "Nombre" -#: web/templates/admin/campsite/type/index.gohtml:25 -msgid "Yes" -msgstr "Sí" - -#: web/templates/admin/campsite/type/index.gohtml:25 -msgid "No" -msgstr "No" - #: web/templates/admin/campsite/type/index.gohtml:31 msgid "No campsite types added yet." msgstr "No se ha añadido ningún tipo de alojamiento todavía." @@ -210,6 +271,14 @@ msgstr "El archivo tiene que ser una imagen PNG o JPEG válida." msgid "Access forbidden" msgstr "Acceso prohibido" +#: pkg/campsite/admin.go:218 +msgid "Selected campsite type is not valid." +msgstr "El tipo de alojamiento escogido no es válido." + +#: pkg/campsite/admin.go:219 +msgid "Label can not be empty." +msgstr "No podéis dejar la etiqueta en blanco." + #: pkg/auth/user.go:40 msgid "Cross-site request forgery detected." msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados." diff --git a/revert/add_campsite.sql b/revert/add_campsite.sql new file mode 100644 index 0000000..b349d62 --- /dev/null +++ b/revert/add_campsite.sql @@ -0,0 +1,7 @@ +-- Revert camper:add_campsite from pg + +begin; + +drop function if exists camper.add_campsite(integer, text); + +commit; diff --git a/revert/campsite.sql b/revert/campsite.sql new file mode 100644 index 0000000..68ed95f --- /dev/null +++ b/revert/campsite.sql @@ -0,0 +1,7 @@ +-- Revert camper:campsite from pg + +begin; + +drop table if exists camper.campsite; + +commit; diff --git a/revert/edit_campsite.sql b/revert/edit_campsite.sql new file mode 100644 index 0000000..70db7ce --- /dev/null +++ b/revert/edit_campsite.sql @@ -0,0 +1,7 @@ +-- Revert camper:edit_campsite from pg + +begin; + +drop function if exists camper.edit_campsite(uuid, integer, text, boolean); + +commit; diff --git a/sqitch.plan b/sqitch.plan index c932fb2..c75ae07 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -42,3 +42,6 @@ change_password [roles schema_auth schema_camper user] 2023-07-21T23:54:52Z jord campsite_type [roles schema_camper company user_profile] 2023-07-31T11:20:29Z jordi fita mas # Add relation of campsite type add_campsite_type [roles schema_camper campsite_type company] 2023-08-04T16:14:48Z jordi fita mas # Add function to create campsite types edit_campsite_type [roles schema_camper campsite_type company] 2023-08-07T22:21:34Z jordi fita mas # Add function to edit campsite types +campsite [roles schema_camper company campsite_type user_profile] 2023-08-14T10:11:51Z jordi fita mas # Add campsite relation +add_campsite [roles schema_camper campsite campsite_type] 2023-08-14T17:03:23Z jordi fita mas # Add function to create campsites +edit_campsite [roles schema_camper campsite] 2023-08-14T17:28:16Z jordi fita mas # Add function to update campsites diff --git a/test/add_campsite.sql b/test/add_campsite.sql new file mode 100644 index 0000000..3225c7c --- /dev/null +++ b/test/add_campsite.sql @@ -0,0 +1,74 @@ +-- Test add_campsite +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(14); + +select has_function('camper', 'add_campsite', array ['integer', 'text']); +select function_lang_is('camper', 'add_campsite', array ['integer', 'text'], 'plpgsql'); +select function_returns('camper', 'add_campsite', array ['integer', 'text'], 'uuid'); +select isnt_definer('camper', 'add_campsite', array ['integer', 'text']); +select volatility_is('camper', 'add_campsite', array ['integer', 'text'], 'volatile'); +select function_privs_are('camper', 'add_campsite', array ['integer', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'add_campsite', array ['integer', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'add_campsite', array ['integer', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'add_campsite', array ['integer', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate campsite cascade; +truncate campsite_type 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') +; + +insert into campsite_type (campsite_type_id, company_id, name) +values (11, 1, 'A') + , (12, 1, 'B') + , (21, 2, 'C') +; + +select lives_ok( + $$ select add_campsite(11, 'A1') $$, + 'Should be able to add a campsite to the first company' +); + +select lives_ok( + $$ select add_campsite(12, 'B1') $$, + 'Should be able to add a campsite to the same company, but of a different type' +); + +select lives_ok( + $$ select add_campsite(21, 'C1') $$, + 'Should be able to add a campsite to the second company' +); + +select throws_ok( + $$ select add_campsite(22, 'C1') $$, + '23503', 'insert or update on table "campsite" violates foreign key constraint "campsite_campsite_type_id_fkey"', + 'Should raise an error if the campsite type is not valid.' +); + +select bag_eq( + $$ select company_id, campsite_type_id, label, active from campsite $$, + $$ values (1, 11, 'A1', true) + , (1, 12, 'B1', true) + , (2, 21, 'C1', true) + $$, + 'Should have added all two campsite type' +); + +select * +from finish(); + +rollback; diff --git a/test/campsite.sql b/test/campsite.sql new file mode 100644 index 0000000..2dd4232 --- /dev/null +++ b/test/campsite.sql @@ -0,0 +1,213 @@ +-- Test campsite +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(58); + +set search_path to camper, public; + +select has_table('campsite'); +select has_pk('campsite'); +select table_privs_are('campsite', 'guest', array['SELECT']); +select table_privs_are('campsite', 'employee', array['SELECT']); +select table_privs_are('campsite', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('campsite', 'authenticator', array[]::text[]); + +select has_sequence('campsite_campsite_id_seq'); +select sequence_privs_are('campsite_campsite_id_seq', 'guest', array[]::text[]); +select sequence_privs_are('campsite_campsite_id_seq', 'employee', array[]::text[]); +select sequence_privs_are('campsite_campsite_id_seq', 'admin', array['USAGE']); +select sequence_privs_are('campsite_campsite_id_seq', 'authenticator', array[]::text[]); + +select has_column('campsite', 'campsite_id'); +select col_is_pk('campsite', 'campsite_id'); +select col_type_is('campsite', 'campsite_id', 'integer'); +select col_not_null('campsite', 'campsite_id'); +select col_has_default('campsite', 'campsite_id'); +select col_default_is('campsite', 'campsite_id', 'nextval(''campsite_campsite_id_seq''::regclass)'); + +select has_column('campsite', 'company_id'); +select col_is_fk('campsite', 'company_id'); +select fk_ok('campsite', 'company_id', 'company', 'company_id'); +select col_type_is('campsite', 'company_id', 'integer'); +select col_not_null('campsite', 'company_id'); +select col_hasnt_default('campsite', 'company_id'); + +select has_column('campsite', 'slug'); +select col_is_unique('campsite', 'slug'); +select col_type_is('campsite', 'slug', 'uuid'); +select col_not_null('campsite', 'slug'); +select col_has_default('campsite', 'slug'); +select col_default_is('campsite', 'slug', 'gen_random_uuid()'); + +select has_column('campsite', 'campsite_type_id'); +select col_is_fk('campsite', 'campsite_type_id'); +select fk_ok('campsite', 'campsite_type_id', 'campsite_type', 'campsite_type_id'); +select col_type_is('campsite', 'campsite_type_id', 'integer'); +select col_not_null('campsite', 'campsite_type_id'); +select col_hasnt_default('campsite', 'campsite_type_id'); + +select has_column('campsite', 'label'); +select col_type_is('campsite', 'label', 'text'); +select col_not_null('campsite', 'label'); +select col_hasnt_default('campsite', 'label'); + +select has_column('campsite', 'active'); +select col_type_is('campsite', 'active', 'boolean'); +select col_not_null('campsite', 'active'); +select col_has_default('campsite', 'active'); +select col_default_is('campsite', 'active', 'true'); + + +set client_min_messages to warning; +truncate campsite cascade; +truncate campsite_type 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 campsite_type (campsite_type_id, company_id, name) +values (22, 2, 'Wooden lodge') + , (44, 4, 'Bungalow') +; + +insert into campsite (company_id, campsite_type_id, label) +values (2, 22, 'W1') + , (4, 44, 'B1') +; + +prepare campsite_data as +select company_id, label +from campsite +order by company_id, label; + +set role guest; +select bag_eq( + 'campsite_data', + $$ values (2, 'W1') + , (4, 'B1') + $$, + 'Everyone should be able to list all campsites across all companies' +); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2'); + +select lives_ok( + $$ insert into campsite(company_id, campsite_type_id, label) values (2, 22, 'w2' ) $$, + 'Admin from company 2 should be able to insert a new campsite to that company.' +); + +select bag_eq( + 'campsite_data', + $$ values (2, 'W1') + , (2, 'w2') + , (4, 'B1') + $$, + 'The new row should have been added' +); + +select lives_ok( + $$ update campsite set label = 'W2' where company_id = 2 and label = 'w2' $$, + 'Admin from company 2 should be able to update campsites of that company.' +); + +select bag_eq( + 'campsite_data', + $$ values (2, 'W1') + , (2, 'W2') + , (4, 'B1') + $$, + 'The row should have been updated.' +); + +select lives_ok( + $$ delete from campsite where company_id = 2 and label = 'W2' $$, + 'Admin from company 2 should be able to delete campsites from that company.' +); + +select bag_eq( + 'campsite_data', + $$ values (2, 'W1') + , (4, 'B1') + $$, + 'The row should have been deleted.' +); + +select throws_ok( + $$ insert into campsite (company_id, campsite_type_id, label) values (4, 44, 'W3' ) $$, + '42501', 'new row violates row-level security policy for table "campsite"', + 'Admin from company 2 should NOT be able to insert new campsites to company 4.' +); + +select lives_ok( + $$ update campsite set label = 'Nope' where company_id = 4 $$, + 'Admin from company 2 should NOT be able to update campsite types of company 4, but no error if company_id is not changed.' +); + +select bag_eq( + 'campsite_data', + $$ values (2, 'W1') + , (4, 'B1') + $$, + 'No row should have been changed.' +); + +select throws_ok( + $$ update campsite set company_id = 4 where company_id = 2 $$, + '42501', 'new row violates row-level security policy for table "campsite"', + 'Admin from company 2 should NOT be able to move campsites to company 4' +); + +select lives_ok( + $$ delete from campsite where company_id = 4 $$, + 'Admin from company 2 should NOT be able to delete campsite types from company 4, but not error is thrown' +); + +select bag_eq( + 'campsite_data', + $$ values (2, 'W1') + , (4, 'B1') + $$, + 'No row should have been changed' +); + +select throws_ok( + $$ insert into campsite (company_id, campsite_type_id, label) values (2, 22, ' ' ) $$, + '23514', 'new row for relation "campsite" violates check constraint "label_not_empty"', + 'Should not be able to insert campsites with a blank label.' +); + +reset role; + +select * +from finish(); + +rollback; + diff --git a/test/edit_campsite.sql b/test/edit_campsite.sql new file mode 100644 index 0000000..29427e0 --- /dev/null +++ b/test/edit_campsite.sql @@ -0,0 +1,66 @@ +-- Test edit_campsite +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_campsite', array ['uuid', 'integer', 'text', 'boolean']); +select function_lang_is('camper', 'edit_campsite', array ['uuid', 'integer', 'text', 'boolean'], 'sql'); +select function_returns('camper', 'edit_campsite', array ['uuid', 'integer', 'text', 'boolean'], 'uuid'); +select isnt_definer('camper', 'edit_campsite', array ['uuid', 'integer', 'text', 'boolean']); +select volatility_is('camper', 'edit_campsite', array ['uuid', 'integer', 'text', 'boolean'], 'volatile'); +select function_privs_are('camper', 'edit_campsite', array ['uuid', 'integer', 'text', 'boolean'], 'guest', array[]::text[]); +select function_privs_are('camper', 'edit_campsite', array ['uuid', 'integer', 'text', 'boolean'], 'employee', array[]::text[]); +select function_privs_are('camper', 'edit_campsite', array ['uuid', 'integer', 'text', 'boolean'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'edit_campsite', array ['uuid', 'integer', 'text', 'boolean'], 'authenticator', array[]::text[]); + +set client_min_messages to warning; +truncate campsite cascade; +truncate campsite_type 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 campsite_type (campsite_type_id, company_id, name) +values (11, 1, 'Type A') + , (12, 1, 'Type B') + , (13, 1, 'Type C') +; + +insert into campsite (company_id, campsite_type_id, slug, label, active) +values (1, 11, '87452b88-b48f-48d3-bb6c-0296de64164e', 'A1', true) + , (1, 12, '9b6370f7-f941-46f2-bc6e-de455675bd0a', 'B1', false) +; + +select lives_ok( + $$ select edit_campsite('87452b88-b48f-48d3-bb6c-0296de64164e', 13, 'C1', false) $$, + 'Should be able to edit the first campsite.' +); + +select lives_ok( + $$ select edit_campsite('9b6370f7-f941-46f2-bc6e-de455675bd0a', 12, 'B2', true) $$, + 'Should be able to edit the second campsite.' +); + +select bag_eq( + $$ select slug::text, campsite_type_id, label, active from campsite $$, + $$ values ('87452b88-b48f-48d3-bb6c-0296de64164e', 13, 'C1', false) + , ('9b6370f7-f941-46f2-bc6e-de455675bd0a', 12, 'B2', true) + $$, + 'Should have updated all campsites.' +); + + +select * +from finish(); + +rollback; diff --git a/verify/add_campsite.sql b/verify/add_campsite.sql new file mode 100644 index 0000000..11b04b2 --- /dev/null +++ b/verify/add_campsite.sql @@ -0,0 +1,7 @@ +-- Verify camper:add_campsite on pg + +begin; + +select has_function_privilege('camper.add_campsite(integer, text)', 'execute'); + +rollback; diff --git a/verify/campsite.sql b/verify/campsite.sql new file mode 100644 index 0000000..a87d8be --- /dev/null +++ b/verify/campsite.sql @@ -0,0 +1,20 @@ +-- Verify camper:campsite on pg + +begin; + +select campsite_id + , company_id + , slug + , campsite_type_id + , label + , active +from camper.campsite +where false; + +select 1 / count(*) from pg_class where oid = 'camper.campsite'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.campsite'::regclass; +select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.campsite'::regclass; +select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.campsite'::regclass; +select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.campsite'::regclass; + +rollback; diff --git a/verify/edit_campsite.sql b/verify/edit_campsite.sql new file mode 100644 index 0000000..d3eb22d --- /dev/null +++ b/verify/edit_campsite.sql @@ -0,0 +1,7 @@ +-- Verify camper:edit_campsite on pg + +begin; + +select has_function_privilege('camper.edit_campsite(uuid, integer, text, boolean)', 'execute'); + +rollback; diff --git a/web/templates/admin/campsite/form.gohtml b/web/templates/admin/campsite/form.gohtml new file mode 100644 index 0000000..19ffde6 --- /dev/null +++ b/web/templates/admin/campsite/form.gohtml @@ -0,0 +1,77 @@ + +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite.campsiteForm*/ -}} + {{ if .Slug}} + {{( pgettext "Edit Campsite" "title" )}} + {{ else }} + {{( pgettext "New Campsite" "title" )}} + {{ end }} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite.campsiteForm*/ -}} +
+

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

+ {{ CSRFInput }} +
+ {{ if .Slug }} + {{ with .Active -}} + + {{ template "error-message" . }} + {{- end }} + {{ else }} + + {{ end }} + {{ with .CampsiteType -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Label -}} + + {{ template "error-message" . }} + {{- end }} +
+
+ +
+
+{{- end }} diff --git a/web/templates/admin/campsite/index.gohtml b/web/templates/admin/campsite/index.gohtml new file mode 100644 index 0000000..bd4d723 --- /dev/null +++ b/web/templates/admin/campsite/index.gohtml @@ -0,0 +1,35 @@ + +{{ define "title" -}} + {{( pgettext "Campsites" "title" )}} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite.campsiteIndex*/ -}} + {{( pgettext "Add Campsite" "action" )}} +

{{( pgettext "Campsites" "title" )}}

+ {{ if .Campsites -}} + + + + + + + + + + {{ range .Campsites -}} + + + + + + {{- end }} + +
{{( pgettext "Label" "header" )}}{{( pgettext "Type" "header" )}}{{( pgettext "Active" "campsite" )}}
{{ .Label }}{{ .Type }}{{ if .Active }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }}
+ {{ else -}} +

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

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