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