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.
This commit is contained in:
jordi fita mas 2023-08-14 20:18:26 +02:00
parent 8b5a45299e
commit 216ae20638
18 changed files with 1016 additions and 51 deletions

34
deploy/add_campsite.sql Normal file
View File

@ -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;

59
deploy/campsite.sql Normal file
View File

@ -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;

25
deploy/edit_campsite.sql Normal file
View File

@ -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;

View File

@ -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:
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)
}

119
po/ca.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-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 <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\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 lallotjament"
#: 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 dallotjament"
#: web/templates/admin/campsite/form.gohtml:51
msgid "Select campsite type"
msgstr "Escolliu un tipus dallotjament"
#: 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 sha 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 dallotjament"
#: 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 sha afegit cap tipus dallotjament 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 dallotjament escollit no és vàlid."
#: pkg/campsite/admin.go:219
msgid "Label can not be empty."
msgstr "No podeu deixar letiqueta en blanc."
#: pkg/auth/user.go:40
msgid "Cross-site request forgery detected."
msgstr "Sha detectat un intent de falsificació de petició a llocs creuats."

119
po/es.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-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 <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\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."

7
revert/add_campsite.sql Normal file
View File

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

7
revert/campsite.sql Normal file
View File

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

7
revert/edit_campsite.sql Normal file
View File

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

View File

@ -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 <jordi@tandem.blog> # Add relation of campsite type
add_campsite_type [roles schema_camper campsite_type company] 2023-08-04T16:14:48Z jordi fita mas <jordi@tandem.blog> # Add function to create campsite types
edit_campsite_type [roles schema_camper campsite_type company] 2023-08-07T22:21:34Z jordi fita mas <jordi@tandem.blog> # Add function to edit campsite types
campsite [roles schema_camper company campsite_type user_profile] 2023-08-14T10:11:51Z jordi fita mas <jordi@tandem.blog> # Add campsite relation
add_campsite [roles schema_camper campsite campsite_type] 2023-08-14T17:03:23Z jordi fita mas <jordi@tandem.blog> # Add function to create campsites
edit_campsite [roles schema_camper campsite] 2023-08-14T17:28:16Z jordi fita mas <jordi@tandem.blog> # Add function to update campsites

74
test/add_campsite.sql Normal file
View File

@ -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;

213
test/campsite.sql Normal file
View File

@ -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;

66
test/edit_campsite.sql Normal file
View File

@ -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;

7
verify/add_campsite.sql Normal file
View File

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

20
verify/campsite.sql Normal file
View File

@ -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;

7
verify/edit_campsite.sql Normal file
View File

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

View File

@ -0,0 +1,77 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite.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*/ -}}
<form
{{ if .Slug }}
data-hx-put="/admin/campsites/{{ .Slug }}"
{{ else }}
action="/admin/campsites" method="post"
{{ end }}
>
<h2>
{{ if .Slug }}
{{( pgettext "Edit Campsite" "title" )}}
{{ else }}
{{( pgettext "New Campsite" "title" )}}
{{ end }}
</h2>
{{ CSRFInput }}
<fieldset>
{{ if .Slug }}
{{ with .Active -}}
<label>
<input type="checkbox" name="{{ .Name }}" {{ if .Checked}}checked{{ end }}
{{ template "error-attrs" . }}>
{{( pgettext "Active" "campsite type" )}}<br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ else }}
<input type="hidden" name="{{ .Active.Name }}" value="true">
{{ end }}
{{ with .CampsiteType -}}
<label>
{{( pgettext "Campsite Type" "input")}}<br>
<select name="{{ .Name }}"
required
{{ template "error-attrs" . }}>
{{ if not $.Slug }}
<option value="">{{( gettext "Select campsite type" )}}</option>
{{ end }}
{{ template "list-options" . }}
</select><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .Label -}}
<label>
{{( pgettext "Label" "input")}}<br>
<input type="text" name="{{ .Name }}" value="{{ .Val }}"
required {{ template "error-attrs" . }}><br>
</label>
{{ template "error-message" . }}
{{- end }}
</fieldset>
<footer>
<button type="submit">
{{ if .Slug }}
{{( pgettext "Update" "action" )}}
{{ else }}
{{( pgettext "Add" "action" )}}
{{ end }}
</button>
</footer>
</form>
{{- end }}

View File

@ -0,0 +1,35 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Campsites" "title" )}}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite.campsiteIndex*/ -}}
<a href="/admin/campsites/new">{{( pgettext "Add Campsite" "action" )}}</a>
<h2>{{( pgettext "Campsites" "title" )}}</h2>
{{ if .Campsites -}}
<table>
<thead>
<tr>
<th scope="col">{{( pgettext "Label" "header" )}}</th>
<th scope="col">{{( pgettext "Type" "header" )}}</th>
<th scope="col">{{( pgettext "Active" "campsite" )}}</th>
</tr>
</thead>
<tbody>
{{ range .Campsites -}}
<tr>
<td><a href="/admin/campsites/{{ .Slug }}">{{ .Label }}</a></td>
<td>{{ .Type }}</td>
<td>{{ if .Active }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }}</td>
</tr>
{{- end }}
</tbody>
</table>
{{ else -}}
<p>{{( gettext "No campsites added yet." )}}</p>
{{- end }}
{{- end }}