From ea2fe8848b1ac10c544a98359a9df1c20f6e0539 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Wed, 27 Sep 2023 02:23:09 +0200 Subject: [PATCH] Add the season_calendar relation and table on the admin section This calendar is supposed to be edited by admin users, but do not yet have the complete JavaScript code to do so, thus for now i have made it read-only. --- demo/demo.sql | 23 +++ deploy/extension_btree_gist.sql | 8 ++ deploy/season_calendar.sql | 55 ++++++++ deploy/set_season_range.sql | 38 +++++ deploy/unset_season_range.sql | 49 +++++++ pkg/season/admin.go | 104 +++++++++++++- po/ca.po | 108 +++++++++++++- po/es.po | 108 +++++++++++++- revert/extension_btree_gist.sql | 7 + revert/season_calendar.sql | 7 + revert/set_season_range.sql | 7 + revert/unset_season_range.sql | 7 + sqitch.plan | 4 + test/extensions.sql | 3 +- test/season_calendar.sql | 179 ++++++++++++++++++++++++ test/set_season_range.sql | 83 +++++++++++ test/unset_season_range.sql | 75 ++++++++++ verify/extension_btree_gist.sql | 10 ++ verify/season_calendar.sql | 16 +++ verify/set_season_range.sql | 7 + verify/unset_season_range.sql | 7 + web/static/camper.css | 11 ++ web/templates/admin/season/index.gohtml | 35 +++++ 23 files changed, 939 insertions(+), 12 deletions(-) create mode 100644 deploy/extension_btree_gist.sql create mode 100644 deploy/season_calendar.sql create mode 100644 deploy/set_season_range.sql create mode 100644 deploy/unset_season_range.sql create mode 100644 revert/extension_btree_gist.sql create mode 100644 revert/season_calendar.sql create mode 100644 revert/set_season_range.sql create mode 100644 revert/unset_season_range.sql create mode 100644 test/season_calendar.sql create mode 100644 test/set_season_range.sql create mode 100644 test/unset_season_range.sql create mode 100644 verify/extension_btree_gist.sql create mode 100644 verify/season_calendar.sql create mode 100644 verify/set_season_range.sql create mode 100644 verify/unset_season_range.sql diff --git a/demo/demo.sql b/demo/demo.sql index 5733bfb..5be2c8b 100644 --- a/demo/demo.sql +++ b/demo/demo.sql @@ -243,5 +243,28 @@ select add_season(52, 'Temporada alta', '#ff926c'); select add_season(52, 'Temporada mitjana', '#ffe37f'); select add_season(52, 'Temporada baixa', '#00aa7d'); +select set_season_range(92, '[2023-04-06, 2023-04-10]'); +select set_season_range(94, '[2023-04-11, 2023-04-27]'); +select set_season_range(93, '[2023-04-28, 2023-04-30]'); +select set_season_range(94, '[2023-05-01, 2023-06-01]'); +select set_season_range(93, '[2023-06-02, 2023-06-03]'); +select set_season_range(94, '[2023-06-04, 2023-06-08]'); +select set_season_range(93, '[2023-06-09, 2023-06-10]'); +select set_season_range(94, '[2023-06-11, 2023-06-15]'); +select set_season_range(93, '[2023-06-16, 2023-06-22]'); +select set_season_range(92, '[2023-06-23, 2023-06-25]'); +select set_season_range(93, '[2023-06-26, 2023-06-30]'); +select set_season_range(92, '[2023-07-01, 2023-08-27]'); +select set_season_range(93, '[2023-08-28, 2023-09-02]'); +select set_season_range(94, '[2023-09-03, 2023-09-07]'); +select set_season_range(93, '[2023-09-08, 2023-09-10]'); +select set_season_range(94, '[2023-09-11, 2023-09-14]'); +select set_season_range(93, '[2023-09-15, 2023-09-16]'); +select set_season_range(94, '[2023-09-17, 2023-09-21]'); +select set_season_range(93, '[2023-09-22, 2023-09-23]'); +select set_season_range(94, '[2023-09-24, 2023-09-28]'); +select set_season_range(93, '[2023-09-29, 2023-09-30]'); +select set_season_range(94, '[2023-10-01, 2023-10-12]'); + commit; diff --git a/deploy/extension_btree_gist.sql b/deploy/extension_btree_gist.sql new file mode 100644 index 0000000..122afbb --- /dev/null +++ b/deploy/extension_btree_gist.sql @@ -0,0 +1,8 @@ +-- Deploy camper:extension_btree_gist to pg +-- requires: schema_public + +begin; + +create extension if not exists btree_gist; + +commit; diff --git a/deploy/season_calendar.sql b/deploy/season_calendar.sql new file mode 100644 index 0000000..b46a49c --- /dev/null +++ b/deploy/season_calendar.sql @@ -0,0 +1,55 @@ +-- Deploy camper:season_calendar to pg +-- requires: roles +-- requires: schema_camper +-- requires: season +-- requires: extension_btree_gist +-- requires: user_profile + +begin; + +set search_path to camper, public; + +create table season_calendar ( + season_id integer not null, + season_range daterange not null, + primary key (season_id, season_range), + constraint disallow_overlap exclude using gist (season_id with =, season_range with &&) +); + +grant select on table season_calendar to guest; +grant select on table season_calendar to employee; +grant select, insert, update, delete on table season_calendar to admin; + +alter table season_calendar enable row level security; + +create policy guest_ok +on season_calendar +for select +using (true) +; + +create policy insert_to_company +on season_calendar +for insert +with check ( + exists (select 1 from season join user_profile using (company_id) where season.season_id = season_calendar.season_id) +) +; + +create policy update_company +on season_calendar +for update +using ( + exists (select 1 from season join user_profile using (company_id) where season.season_id = season_calendar.season_id) +) +; + +create policy delete_from_company +on season_calendar +for delete +using ( + exists (select 1 from season join user_profile using (company_id) where season.season_id = season_calendar.season_id) +) +; + +commit; diff --git a/deploy/set_season_range.sql b/deploy/set_season_range.sql new file mode 100644 index 0000000..b34686a --- /dev/null +++ b/deploy/set_season_range.sql @@ -0,0 +1,38 @@ +-- Deploy camper:set_season_range to pg +-- requires: roles +-- requires: schema_camper +-- requires: season_calendar +-- requires: unset_season_range + +begin; + +set search_path to camper, public; + +create or replace function set_season_range(szn_id integer, range daterange) returns void as +$$ +declare + tmp_range daterange; +begin + select daterange(min(lower(season_range)), max(upper(season_range))) + into tmp_range + from season_calendar + where season_id = szn_id + and (season_range && range or season_range -|- range) + ; + if not lower_inf(tmp_range) and not upper_inf(tmp_range) then + range := range + tmp_range; + end if; + + perform unset_season_range(range); + + insert into season_calendar (season_id, season_range) + values (szn_id, range); +end +$$ + language plpgsql +; + +revoke execute on function set_season_range(integer, daterange) from public; +grant execute on function set_season_range(integer, daterange) to admin; + +commit; diff --git a/deploy/unset_season_range.sql b/deploy/unset_season_range.sql new file mode 100644 index 0000000..28b2ee3 --- /dev/null +++ b/deploy/unset_season_range.sql @@ -0,0 +1,49 @@ +-- Deploy camper:unset_season_range to pg +-- requires: roles +-- requires: schema_camper +-- requires: season_calendar + +begin; + +set search_path to camper, public; + +create or replace function unset_season_range(range daterange) returns void as +$$ +declare + tmp_range daterange; + tmp_id integer; +begin + for tmp_id, tmp_range in + select season_id, season_range + from season_calendar + where season_range @> range + loop + delete from season_calendar where season_id = tmp_id and season_range = tmp_range; + insert into season_calendar (season_id, season_range) + values (tmp_id, daterange(lower(tmp_range), lower(range))) + , (tmp_id, daterange(upper(range), upper(tmp_range))) + ; + end loop; + + delete from season_calendar where range @> season_range; + + update season_calendar + set season_range = season_range - range + where season_range && range + and season_range < range + ; + + update season_calendar + set season_range = season_range - (season_range * range) + where season_range && range + and season_range > range + ; +end +$$ + language plpgsql +; + +revoke execute on function unset_season_range(daterange) from public; +grant execute on function unset_season_range(daterange) to admin; + +commit; diff --git a/pkg/season/admin.go b/pkg/season/admin.go index 6862054..45bb253 100644 --- a/pkg/season/admin.go +++ b/pkg/season/admin.go @@ -8,6 +8,7 @@ package season import ( "context" "net/http" + "time" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/database" @@ -74,12 +75,17 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat } func serveSeasonIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { - campsites, err := collectSeasonEntries(r.Context(), company, conn) + seasons, err := collectSeasonEntries(r.Context(), company, conn) + if err != nil { + panic(err) + } + calendar, err := collectSeasonCalendar(r.Context(), company, conn, 2023) if err != nil { panic(err) } page := &seasonIndex{ - Seasons: campsites, + Seasons: seasons, + Calendar: calendar, } page.MustRender(w, r, user, company) } @@ -117,8 +123,100 @@ type seasonEntry struct { Active bool } +var longMonthNames = []string{ + locale.PgettextNoop("January", "month"), + locale.PgettextNoop("February", "month"), + locale.PgettextNoop("March", "month"), + locale.PgettextNoop("April", "month"), + locale.PgettextNoop("May", "month"), + locale.PgettextNoop("June", "month"), + locale.PgettextNoop("July", "month"), + locale.PgettextNoop("August", "month"), + locale.PgettextNoop("September", "month"), + locale.PgettextNoop("October", "month"), + locale.PgettextNoop("November", "month"), + locale.PgettextNoop("December", "month"), +} + +func collectSeasonCalendar(ctx context.Context, company *auth.Company, conn *database.Conn, year int) (seasonCalendar, error) { + firstDay := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC) + lastDay := time.Date(year, time.December, 31, 23, 59, 59, 0, time.UTC) + rows, err := conn.Query(ctx, ` + select t.day::date + , to_color(coalesce(color, 13750495)) as color + from generate_series($1, $2, interval '1 day') as t(day) + left join season_calendar on season_range @> t.day::date + left join season on season.season_id = season_calendar.season_id and company_id = $3 + `, firstDay, lastDay, company.ID) + if err != nil { + return nil, err + } + + var month *seasonMonth + var week seasonWeek + var calendar seasonCalendar + weekday := int(time.Monday) + for rows.Next() { + day := &seasonDay{} + if err = rows.Scan(&day.Date, &day.Color); err != nil { + return nil, err + } + dayMonth := day.Date.Month() + if month == nil || month.Month != dayMonth { + if month != nil { + for ; weekday != int(time.Sunday); weekday = (weekday + 1) % 7 { + week = append(week, &seasonDay{}) + } + month.Weeks = append(month.Weeks, week) + calendar = append(calendar, month) + } + month = &seasonMonth{ + Month: dayMonth, + Name: longMonthNames[dayMonth-1], + } + week = seasonWeek{} + weekday = int(time.Monday) + dayWeekday := int(day.Date.Weekday()) + for ; weekday != dayWeekday; weekday = (weekday + 1) % 7 { + week = append(week, &seasonDay{}) + } + } + week = append(week, day) + weekday = (weekday + 1) % 7 + if weekday == int(time.Monday) { + month.Weeks = append(month.Weeks, week) + week = seasonWeek{} + } + } + if month != nil { + for ; weekday != int(time.Sunday); weekday = (weekday + 1) % 7 { + week = append(week, &seasonDay{}) + } + month.Weeks = append(month.Weeks, week) + calendar = append(calendar, month) + } + + return calendar, nil +} + +type seasonCalendar []*seasonMonth + +type seasonMonth struct { + Month time.Month + Name string + Weeks []seasonWeek +} + +type seasonWeek []*seasonDay + +type seasonDay struct { + Date time.Time + Color string +} + type seasonIndex struct { - Seasons []*seasonEntry + Seasons []*seasonEntry + Calendar seasonCalendar } func (page *seasonIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { diff --git a/po/ca.po b/po/ca.po index 5e7cbb2..c038146 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-09-26 17:28+0200\n" +"POT-Creation-Date: 2023-09-27 02:15+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -398,6 +398,46 @@ msgstr "Color" msgid "No seasons added yet." msgstr "No s’ha afegit cap temporada encara." +#: web/templates/admin/season/index.gohtml:40 +msgctxt "title" +msgid "Calendar" +msgstr "Calendari" + +#: web/templates/admin/season/index.gohtml:47 +msgctxt "day" +msgid "Mon" +msgstr "dl" + +#: web/templates/admin/season/index.gohtml:48 +msgctxt "day" +msgid "Tue" +msgstr "dt" + +#: web/templates/admin/season/index.gohtml:49 +msgctxt "day" +msgid "Wed" +msgstr "dc" + +#: web/templates/admin/season/index.gohtml:50 +msgctxt "day" +msgid "Thu" +msgstr "dj" + +#: web/templates/admin/season/index.gohtml:51 +msgctxt "day" +msgid "Fri" +msgstr "dv" + +#: web/templates/admin/season/index.gohtml:52 +msgctxt "day" +msgid "Sat" +msgstr "ds" + +#: web/templates/admin/season/index.gohtml:53 +msgctxt "day" +msgid "Sun" +msgstr "dg" + #: web/templates/admin/dashboard.gohtml:6 #: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:68 msgctxt "title" @@ -761,7 +801,7 @@ msgid "Automatic" msgstr "Automàtic" #: pkg/app/user.go:249 pkg/campsite/types/l10n.go:82 -#: pkg/campsite/types/admin.go:274 pkg/season/admin.go:203 +#: pkg/campsite/types/admin.go:274 pkg/season/admin.go:301 #: pkg/services/l10n.go:73 pkg/services/admin.go:266 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." @@ -808,11 +848,71 @@ msgstr "El tipus d’allotjament escollit no és vàlid." msgid "Label can not be empty." msgstr "No podeu deixar l’etiqueta en blanc." -#: pkg/season/admin.go:204 +#: pkg/season/admin.go:127 +msgctxt "month" +msgid "January" +msgstr "gener" + +#: pkg/season/admin.go:128 +msgctxt "month" +msgid "February" +msgstr "febrer" + +#: pkg/season/admin.go:129 +msgctxt "month" +msgid "March" +msgstr "març" + +#: pkg/season/admin.go:130 +msgctxt "month" +msgid "April" +msgstr "abril" + +#: pkg/season/admin.go:131 +msgctxt "month" +msgid "May" +msgstr "maig" + +#: pkg/season/admin.go:132 +msgctxt "month" +msgid "June" +msgstr "juny" + +#: pkg/season/admin.go:133 +msgctxt "month" +msgid "July" +msgstr "juliol" + +#: pkg/season/admin.go:134 +msgctxt "month" +msgid "August" +msgstr "agost" + +#: pkg/season/admin.go:135 +msgctxt "month" +msgid "September" +msgstr "setembre" + +#: pkg/season/admin.go:136 +msgctxt "month" +msgid "October" +msgstr "octubre" + +#: pkg/season/admin.go:137 +msgctxt "month" +msgid "November" +msgstr "novembre" + +#: pkg/season/admin.go:138 +msgctxt "month" +msgid "December" +msgstr "desembre" + +#: pkg/season/admin.go:302 msgid "Color can not be empty." msgstr "No podeu deixar el color en blanc." -#: pkg/season/admin.go:205 +#: pkg/season/admin.go:303 msgid "This color is not valid. It must be like #123abc." msgstr "Aquest color no és vàlid. Hauria de ser similar a #123abc." diff --git a/po/es.po b/po/es.po index 435cd1b..4bc7490 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-09-26 17:28+0200\n" +"POT-Creation-Date: 2023-09-27 02:15+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -398,6 +398,46 @@ msgstr "Color" msgid "No seasons added yet." msgstr "No se ha añadido ninguna temporada todavía." +#: web/templates/admin/season/index.gohtml:40 +msgctxt "title" +msgid "Calendar" +msgstr "Calendario" + +#: web/templates/admin/season/index.gohtml:47 +msgctxt "day" +msgid "Mon" +msgstr "lu" + +#: web/templates/admin/season/index.gohtml:48 +msgctxt "day" +msgid "Tue" +msgstr "ma" + +#: web/templates/admin/season/index.gohtml:49 +msgctxt "day" +msgid "Wed" +msgstr "mi" + +#: web/templates/admin/season/index.gohtml:50 +msgctxt "day" +msgid "Thu" +msgstr "ju" + +#: web/templates/admin/season/index.gohtml:51 +msgctxt "day" +msgid "Fri" +msgstr "vi" + +#: web/templates/admin/season/index.gohtml:52 +msgctxt "day" +msgid "Sat" +msgstr "sá" + +#: web/templates/admin/season/index.gohtml:53 +msgctxt "day" +msgid "Sun" +msgstr "do" + #: web/templates/admin/dashboard.gohtml:6 #: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:68 msgctxt "title" @@ -761,7 +801,7 @@ msgid "Automatic" msgstr "Automático" #: pkg/app/user.go:249 pkg/campsite/types/l10n.go:82 -#: pkg/campsite/types/admin.go:274 pkg/season/admin.go:203 +#: pkg/campsite/types/admin.go:274 pkg/season/admin.go:301 #: pkg/services/l10n.go:73 pkg/services/admin.go:266 msgid "Name can not be empty." msgstr "No podéis dejar el nombre en blanco." @@ -808,11 +848,71 @@ msgstr "El tipo de alojamiento escogido no es válido." msgid "Label can not be empty." msgstr "No podéis dejar la etiqueta en blanco." -#: pkg/season/admin.go:204 +#: pkg/season/admin.go:127 +msgctxt "month" +msgid "January" +msgstr "enero" + +#: pkg/season/admin.go:128 +msgctxt "month" +msgid "February" +msgstr "febrero" + +#: pkg/season/admin.go:129 +msgctxt "month" +msgid "March" +msgstr "marzo" + +#: pkg/season/admin.go:130 +msgctxt "month" +msgid "April" +msgstr "abril" + +#: pkg/season/admin.go:131 +msgctxt "month" +msgid "May" +msgstr "mayo" + +#: pkg/season/admin.go:132 +msgctxt "month" +msgid "June" +msgstr "junio" + +#: pkg/season/admin.go:133 +msgctxt "month" +msgid "July" +msgstr "julio" + +#: pkg/season/admin.go:134 +msgctxt "month" +msgid "August" +msgstr "agosto" + +#: pkg/season/admin.go:135 +msgctxt "month" +msgid "September" +msgstr "septiembre" + +#: pkg/season/admin.go:136 +msgctxt "month" +msgid "October" +msgstr "octubre" + +#: pkg/season/admin.go:137 +msgctxt "month" +msgid "November" +msgstr "noviembre" + +#: pkg/season/admin.go:138 +msgctxt "month" +msgid "December" +msgstr "diciembre" + +#: pkg/season/admin.go:302 msgid "Color can not be empty." msgstr "No podéis dejar el color en blanco." -#: pkg/season/admin.go:205 +#: pkg/season/admin.go:303 msgid "This color is not valid. It must be like #123abc." msgstr "Este color no es válido. Tiene que ser parecido a #123abc." diff --git a/revert/extension_btree_gist.sql b/revert/extension_btree_gist.sql new file mode 100644 index 0000000..91c1975 --- /dev/null +++ b/revert/extension_btree_gist.sql @@ -0,0 +1,7 @@ +-- Revert camper:extension_btree_gist from pg + +begin; + +drop extension if exists btree_gist; + +commit; diff --git a/revert/season_calendar.sql b/revert/season_calendar.sql new file mode 100644 index 0000000..998f582 --- /dev/null +++ b/revert/season_calendar.sql @@ -0,0 +1,7 @@ +-- Revert camper:season_calendar from pg + +begin; + +drop table if exists camper.season_calendar; + +commit; diff --git a/revert/set_season_range.sql b/revert/set_season_range.sql new file mode 100644 index 0000000..4df1929 --- /dev/null +++ b/revert/set_season_range.sql @@ -0,0 +1,7 @@ +-- Revert camper:set_season_range from pg + +begin; + +drop function if exists camper.set_season_range(integer, daterange); + +commit; diff --git a/revert/unset_season_range.sql b/revert/unset_season_range.sql new file mode 100644 index 0000000..9643652 --- /dev/null +++ b/revert/unset_season_range.sql @@ -0,0 +1,7 @@ +-- Revert camper:unset_season_range from pg + +begin; + +drop function if exists camper.unset_season_range(daterange); + +commit; diff --git a/sqitch.plan b/sqitch.plan index 3096bd2..2a5e8ed 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -80,3 +80,7 @@ service_i18n [roles schema_camper service language] 2023-09-17T00:13:42Z jordi f translate_service [roles schema_camper service_i18n] 2023-09-17T00:17:00Z jordi fita mas # Add function to translate a service remove_service [roles schema_camper service service_i18n] 2023-09-26T15:21:00Z jordi fita mas # Add function to remove service translation [schema_camper] 2023-09-25T16:27:50Z jordi fita mas # Add the type for a translation +extension_btree_gist [schema_public] 2023-09-26T17:49:30Z jordi fita mas # Add btree_gist extension +season_calendar [roles schema_camper season extension_btree_gist user_profile] 2023-09-26T18:07:21Z jordi fita mas # Add the relation of date ranges for seasons +unset_season_range [roles schema_camper season_calendar] 2023-09-26T21:56:38Z jordi fita mas # Add function to unset a date range from the seasons’ calendar +set_season_range [roles schema_camper season_calendar unset_season_range] 2023-09-26T18:37:29Z jordi fita mas # Add function to set a season’s date range diff --git a/test/extensions.sql b/test/extensions.sql index aebb976..e0d7904 100644 --- a/test/extensions.sql +++ b/test/extensions.sql @@ -8,7 +8,8 @@ begin; select plan(1); select extensions_are(array [ - 'citext' + 'btree_gist' + , 'citext' , 'pgtap' , 'pgcrypto' , 'pg_libphonenumber' diff --git a/test/season_calendar.sql b/test/season_calendar.sql new file mode 100644 index 0000000..bdbec37 --- /dev/null +++ b/test/season_calendar.sql @@ -0,0 +1,179 @@ +-- Test season_calendar +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(29); + +set search_path to camper, public; + +select has_table('season_calendar'); +select has_pk('season_calendar'); +select col_is_pk('season_calendar', array['season_id', 'season_range']); +select table_privs_are('season_calendar', 'guest', array['SELECT']); +select table_privs_are('season_calendar', 'employee', array['SELECT']); +select table_privs_are('season_calendar', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('season_calendar', 'authenticator', array[]::text[]); + +select has_column('season_calendar', 'season_id'); +select col_type_is('season_calendar', 'season_id', 'integer'); +select col_not_null('season_calendar', 'season_id'); +select col_hasnt_default('season_calendar', 'season_id'); + +select has_column('season_calendar', 'season_range'); +select col_type_is('season_calendar', 'season_range', 'daterange'); +select col_not_null('season_calendar', 'season_range'); +select col_hasnt_default('season_calendar', 'season_range'); + + +set client_min_messages to warning; +truncate season_calendar cascade; +truncate season cascade; +truncate company_host cascade; +truncate company_user cascade; +truncate company cascade; +truncate auth."user" cascade; +reset client_min_messages; + + +insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at) +values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month') + , (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month') +; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca') +; + +insert into company_user (company_id, user_id, role) +values (2, 1, 'admin') + , (4, 5, 'admin') +; + +insert into company_host (company_id, host) +values (2, 'co2') + , (4, 'co4') +; + +insert into season (season_id, company_id, name) +values (7, 2, 'Peak') + , (8, 4, 'Low') +; + +insert into season_calendar (season_id, season_range) +values (7, '[2023-01-01, 2023-02-01)') + , (8, '[2023-02-01, 2023-03-01)') +; + +prepare season_data as +select season_id, lower(season_range)::text +from season_calendar +; + +set role guest; +select bag_eq( + 'season_data', + $$ values (7, '2023-01-01') + , (8, '2023-02-01') + $$, + 'Everyone should be able to list all seasons across all companies' +); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2'); + +select lives_ok( + $$ insert into season_calendar(season_id, season_range) values (7, '[2023-03-01, 2023-04-01)' ) $$, + 'Admin from company 2 should be able to insert a new season to that company.' +); + +select bag_eq( + 'season_data', + $$ values (7, '2023-01-01') + , (7, '2023-03-01') + , (8, '2023-02-01') + $$, + 'The new row should have been added' +); + +select lives_ok( + $$ update season_calendar set season_range = '[2023-04-01, 2023-05-01)' where season_id = 7 and season_range = '[2023-03-01, 2023-04-01)' $$, + 'Admin from company 2 should be able to update season of that company.' +); + +select bag_eq( + 'season_data', + $$ values (7, '2023-01-01') + , (7, '2023-04-01') + , (8, '2023-02-01') + $$, + 'The row should have been updated.' +); + +select lives_ok( + $$ delete from season_calendar where season_id = 7 and season_range = '[2023-04-01, 2023-05-01)' $$, + 'Admin from company 2 should be able to delete season from that company.' +); + +select bag_eq( + 'season_data', + $$ values (7, '2023-01-01') + , (8, '2023-02-01') + $$, + 'The row should have been deleted.' +); + +select throws_ok( + $$ insert into season_calendar (season_id, season_range) values (8, '[2023-05-01, 2023-06-01)' ) $$, + '42501', 'new row violates row-level security policy for table "season_calendar"', + 'Admin from company 2 should NOT be able to insert new seasons to company 4.' +); + +select lives_ok( + $$ update season_calendar set season_range = '[2023-09-01, 2023-10-01)' where season_id = 8 $$, + 'Admin from company 2 should not be able to update new seasons of company 4, but no error if season_id is not changed.' +); + +select bag_eq( + 'season_data', + $$ values (7, '2023-01-01') + , (8, '2023-02-01') + $$, + 'No row should have been changed.' +); + +select throws_ok( + $$ update season_calendar set season_id = 8 where season_id = 7 $$, + '42501', 'new row violates row-level security policy for table "season_calendar"', + 'Admin from company 2 should NOT be able to move seasons to company 4' +); + +select lives_ok( + $$ delete from season_calendar where season_id = 8 $$, + 'Admin from company 2 should NOT be able to delete seasons from company 4, but not error is thrown' +); + +select bag_eq( + 'season_data', + $$ values (7, '2023-01-01') + , (8, '2023-02-01') + $$, + 'No row should have been changed' +); + +select throws_ok( + $$ insert into season_calendar (season_id, season_range) values (7, '[2023-01-30, 2023-02-02)' ) $$, + '23P01', 'conflicting key value violates exclusion constraint "disallow_overlap"', + 'Should not be able to insert seasons with overlapping range.' +); + +reset role; + +select * +from finish(); + +rollback; + diff --git a/test/set_season_range.sql b/test/set_season_range.sql new file mode 100644 index 0000000..c4c62b1 --- /dev/null +++ b/test/set_season_range.sql @@ -0,0 +1,83 @@ +-- Test set_season_range +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(19); + +set search_path to camper, public; + +select has_function('camper', 'set_season_range', array['integer', 'daterange']); +select function_lang_is('camper', 'set_season_range', array['integer', 'daterange'], 'plpgsql'); +select function_returns('camper', 'set_season_range', array['integer', 'daterange'], 'void'); +select isnt_definer('camper', 'set_season_range', array['integer', 'daterange']); +select volatility_is('camper', 'set_season_range', array['integer', 'daterange'], 'volatile'); +select function_privs_are('camper', 'set_season_range', array ['integer', 'daterange'], 'guest', array[]::text[]); +select function_privs_are('camper', 'set_season_range', array ['integer', 'daterange'], 'employee', array[]::text[]); +select function_privs_are('camper', 'set_season_range', array ['integer', 'daterange'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'set_season_range', array ['integer', 'daterange'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate season_calendar cascade; +truncate season cascade; +truncate company cascade; +reset client_min_messages; + + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca') +; + +insert into season (season_id, company_id, name) +values ( 7, 2, 'Peak') + , ( 8, 2, 'Shoulder') + , ( 9, 2, 'Off-Season') + , (10, 2, 'Saint John’s Eve') +; + +insert into season_calendar (season_id, season_range) +values (7, '[2023-04-07, 2023-04-09]') + , (9, '[2023-04-12, 2023-04-16]') + , (8, '[2023-04-18, 2023-04-22]') + , (9, '[2023-04-24, 2023-05-14]') + , (9, '[2023-05-22, 2023-05-28]') + , (9, '[2023-06-05, 2023-06-15]') + , (8, '[2023-06-16, 2023-06-30]') +; + +select lives_ok($$ select set_season_range( 7, '[2023-04-06, 2023-04-07]') $$); +select lives_ok($$ select set_season_range( 7, '[2023-04-08, 2023-04-10]') $$); +select lives_ok($$ select set_season_range( 9, '[2023-04-11, 2023-06-15]') $$); +select lives_ok($$ select set_season_range( 8, '[2023-04-28, 2023-04-30]') $$); +select lives_ok($$ select set_season_range( 8, '[2023-06-02, 2023-06-03]') $$); +select lives_ok($$ select set_season_range( 8, '[2023-06-09, 2023-06-10]') $$); +select lives_ok($$ select set_season_range(10, '[2023-06-23, 2023-06-25]') $$); +select lives_ok($$ select set_season_range( 7, '[2023-07-01, 2023-08-27]') $$); +select lives_ok($$ select set_season_range( 8, '[2023-08-28, 2023-08-31]') $$); + +select bag_eq( + $$ select season_id, season_range from season_calendar $$, + $$ values ( 7, '[2023-04-06, 2023-04-10]'::daterange) + , ( 9, '[2023-04-11, 2023-04-27]'::daterange) + , ( 8, '[2023-04-28, 2023-04-30]'::daterange) + , ( 9, '[2023-05-01, 2023-06-01]'::daterange) + , ( 8, '[2023-06-02, 2023-06-03]'::daterange) + , ( 9, '[2023-06-04, 2023-06-08]'::daterange) + , ( 8, '[2023-06-09, 2023-06-10]'::daterange) + , ( 9, '[2023-06-11, 2023-06-15]'::daterange) + , ( 8, '[2023-06-16, 2023-06-22]'::daterange) + , (10, '[2023-06-23, 2023-06-25]'::daterange) + , ( 8, '[2023-06-26, 2023-06-30]'::daterange) + , ( 7, '[2023-07-01, 2023-08-27]'::daterange) + , ( 8, '[2023-08-28, 2023-08-31]'::daterange) + $$, + 'Should have updated the calendar' +); + +select * +from finish(); + +rollback; diff --git a/test/unset_season_range.sql b/test/unset_season_range.sql new file mode 100644 index 0000000..76ded84 --- /dev/null +++ b/test/unset_season_range.sql @@ -0,0 +1,75 @@ +-- Test unset_season_range +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(14); + +set search_path to camper, public; + +select has_function('camper', 'unset_season_range', array['daterange']); +select function_lang_is('camper', 'unset_season_range', array['daterange'], 'plpgsql'); +select function_returns('camper', 'unset_season_range', array['daterange'], 'void'); +select isnt_definer('camper', 'unset_season_range', array['daterange']); +select volatility_is('camper', 'unset_season_range', array['daterange'], 'volatile'); +select function_privs_are('camper', 'unset_season_range', array ['daterange'], 'guest', array[]::text[]); +select function_privs_are('camper', 'unset_season_range', array ['daterange'], 'employee', array[]::text[]); +select function_privs_are('camper', 'unset_season_range', array ['daterange'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'unset_season_range', array ['daterange'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate season_calendar cascade; +truncate season cascade; +truncate company cascade; +reset client_min_messages; + + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca') +; + +insert into season (season_id, company_id, name) +values ( 7, 2, 'Peak') + , ( 8, 2, 'Shoulder') + , ( 9, 2, 'Off-Season') + , (10, 2, 'Saint John’s Eve') +; + +insert into season_calendar (season_id, season_range) +values ( 9, '[2023-04-01, 2023-04-05]') + , ( 7, '[2023-04-06, 2023-04-10]') + , ( 9, '[2023-04-11, 2023-04-12]') + , ( 8, '[2023-04-13, 2023-04-16]') + , ( 9, '[2023-04-17, 2023-04-19]') + , (10, '[2023-04-20, 2023-04-22]') + , ( 9, '[2023-04-23, 2023-04-27]') + , ( 8, '[2023-04-28, 2023-04-30]') +; + +select lives_ok($$ select unset_season_range('[2023-04-08, 2023-04-11]') $$); +select lives_ok($$ select unset_season_range('[2023-04-15, 2023-04-15]') $$); +select lives_ok($$ select unset_season_range('[2023-04-20, 2023-04-23]') $$); +select lives_ok($$ select unset_season_range('[2023-04-26, 2023-04-29]') $$); + +select bag_eq( + $$ select season_id, season_range from season_calendar $$, + $$ values ( 9, '[2023-04-01, 2023-04-05]'::daterange) + , ( 7, '[2023-04-06, 2023-04-07]'::daterange) + , ( 9, '[2023-04-12, 2023-04-12]'::daterange) + , ( 8, '[2023-04-13, 2023-04-14]'::daterange) + , ( 8, '[2023-04-16, 2023-04-16]'::daterange) + , ( 9, '[2023-04-17, 2023-04-19]'::daterange) + , ( 9, '[2023-04-24, 2023-04-25]'::daterange) + , ( 8, '[2023-04-30, 2023-04-30]'::daterange) + $$, + 'Should have updated the calendar' +); + + +select * +from finish(); + +rollback; diff --git a/verify/extension_btree_gist.sql b/verify/extension_btree_gist.sql new file mode 100644 index 0000000..64757c1 --- /dev/null +++ b/verify/extension_btree_gist.sql @@ -0,0 +1,10 @@ +-- Verify camper:extension_btree_gist on pg + +begin; + +select 1 / count(*) +from pg_extension +where extname = 'btree_gist' +; + +rollback; diff --git a/verify/season_calendar.sql b/verify/season_calendar.sql new file mode 100644 index 0000000..6f6a12f --- /dev/null +++ b/verify/season_calendar.sql @@ -0,0 +1,16 @@ +-- Verify camper:season_calendar on pg + +begin; + +select season_id + , season_range +from camper.season_calendar +where false; + +select 1 / count(*) from pg_class where oid = 'camper.season_calendar'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.season_calendar'::regclass; +select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.season_calendar'::regclass; +select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.season_calendar'::regclass; +select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.season_calendar'::regclass; + +rollback; diff --git a/verify/set_season_range.sql b/verify/set_season_range.sql new file mode 100644 index 0000000..1d59353 --- /dev/null +++ b/verify/set_season_range.sql @@ -0,0 +1,7 @@ +-- Verify camper:set_season_range on pg + +begin; + +select has_function_privilege('camper.set_season_range(integer, daterange)', 'execute'); + +rollback; diff --git a/verify/unset_season_range.sql b/verify/unset_season_range.sql new file mode 100644 index 0000000..9e60907 --- /dev/null +++ b/verify/unset_season_range.sql @@ -0,0 +1,7 @@ +-- Verify camper:unset_season_range on pg + +begin; + +select has_function_privilege('camper.unset_season_range(daterange)', 'execute'); + +rollback; diff --git a/web/static/camper.css b/web/static/camper.css index ef51999..3481790 100644 --- a/web/static/camper.css +++ b/web/static/camper.css @@ -123,3 +123,14 @@ a.missing-translation { .icon-input button[aria-pressed="true"] { background-color: #ffeeaa; } + +.season-calendar { + display: grid; + grid-template-columns: repeat(3, auto); + justify-content: center; + gap: 2em; +} + +.season-calendar svg { + max-width: 5rem; +} diff --git a/web/templates/admin/season/index.gohtml b/web/templates/admin/season/index.gohtml index bfb4a14..b9cc836 100644 --- a/web/templates/admin/season/index.gohtml +++ b/web/templates/admin/season/index.gohtml @@ -36,4 +36,39 @@ {{ else -}}

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

{{- end }} + +

{{( pgettext "Calendar" "title" )}}

+
+ {{ range .Calendar -}} + + + + + + + + + + + + + + + {{ range .Weeks }} + + {{ range . }} + + {{- end }} + + {{- end }} + +
{{ pgettext .Name "month" }}
{{(pgettext "Mon" "day" )}}{{(pgettext "Tue" "day" )}}{{(pgettext "Wed" "day" )}}{{(pgettext "Thu" "day" )}}{{(pgettext "Fri" "day" )}}{{(pgettext "Sat" "day" )}}{{(pgettext "Sun" "day" )}}
+ {{- if .Color -}} + + + + {{- end -}} +
+ {{- end }} +
{{- end }}