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