Add seasons’ relation, functions, and admin section

Seasons have a color to show on the calendar. I need them in HTML format
(e.g., #123abc) in order to set as value to `<input type="color">`, but
i did not want to save them as text in the database, as integers are
better representations of colors—in fact, that’s what the HTML syntax
also is: an integer.

I think the best would be to create an extension that adds an HTML color
type, with functions to convert from many representations (e.g., CSS’
rgb or even color names) to integer and back.  However, that’s a lot of
work and i can satisfy Camper’s needs with just a couple of functions
and a domain.

To show the color on the index, at first tried to use a read-only
`<input type="color">`, but seems that this type of input can not be
read-only and must be disabled instead.  However, i do not know whether
it makes sense to have a disabled input outside a form “just” to show
a color; i suspect it does not.  Thus, at the end i use SVG with a
single circle, which is better that a 50%-rounded div with a background
color, even if the result is the same—SVG **is** intended for showing
pictures, which is this case.
This commit is contained in:
jordi fita mas 2023-08-16 20:15:57 +02:00
parent 1837b7a113
commit b4919db6c4
33 changed files with 1283 additions and 124 deletions

24
deploy/add_season.sql Normal file
View File

@ -0,0 +1,24 @@
-- Deploy camper:add_season to pg
-- requires: roles
-- requires: schema_camper
-- requires: season
-- requires: color
-- requires: to_integer
begin;
set search_path to camper, public;
create or replace function add_season(company integer, name text, color color) returns uuid as $$
insert into season (company_id, name, color)
values (company, name, to_integer(color))
returning slug
;
$$
language sql
;
revoke execute on function add_season(integer, text, color) from public;
grant execute on function add_season(integer, text, color) to admin;
commit;

14
deploy/color.sql Normal file
View File

@ -0,0 +1,14 @@
-- Deploy camper:color to pg
-- requires: schema_camper
-- requires: extension_citext
begin;
set search_path to camper, public;
create domain color as citext
check ( value ~ '^#[a-fA-F0-9]{6}$' );
comment on domain color is 'seven-character string specifying an RGB color in hexadecimal format starting with #, like HTML.';
commit;

27
deploy/edit_season.sql Normal file
View File

@ -0,0 +1,27 @@
-- Deploy camper:edit_season to pg
-- requires: roles
-- requires: schema_camper
-- requires: season
-- requires: color
-- requires: to_integer
begin;
set search_path to camper, public;
create or replace function edit_season(slug uuid, name text, color color, active boolean) returns uuid as $$
update season
set name = edit_season.name
, color = to_integer(edit_season.color)
, active = edit_season.active
where slug = edit_season.slug
returning slug
;
$$
language sql
;
revoke execute on function edit_season(uuid, text, color, boolean) from public;
grant execute on function edit_season(uuid, text, color, boolean) to admin;
commit;

58
deploy/season.sql Normal file
View File

@ -0,0 +1,58 @@
-- Deploy camper:season to pg
-- requires: roles
-- requires: schema_camper
-- requires: company
-- requires: user_profile
begin;
set search_path to camper, public;
create table season (
season_id serial primary key,
company_id integer not null references company,
slug uuid not null unique default gen_random_uuid(),
name text not null constraint name_not_empty check(length(trim(name)) > 0),
color integer not null default 0,
active boolean not null default true
);
grant select on table season to guest;
grant select on table season to employee;
grant select, insert, delete, update on table season to admin;
grant usage on sequence season_season_id_seq to admin;
alter table season enable row level security;
create policy guest_ok
on season
for select
using (true)
;
create policy insert_to_company
on season
for insert
with check (
company_id in (select company_id from user_profile)
)
;
create policy update_company
on season
for update
using (
company_id in (select company_id from user_profile)
)
;
create policy delete_from_company
on season
for delete
using (
company_id in (select company_id from user_profile)
)
;
commit;

18
deploy/to_color.sql Normal file
View File

@ -0,0 +1,18 @@
-- Deploy camper:to_color to pg
-- requires: roles
-- requires: schema_camper
-- requires: color
begin;
set search_path to camper, public;
create or replace function to_color(color integer) returns color as
$$
select '#' || lpad(to_hex(color), 6, '0');
$$
language sql
immutable
;
commit;

18
deploy/to_integer.sql Normal file
View File

@ -0,0 +1,18 @@
-- Deploy camper:to_integer to pg
-- requires: roles
-- requires: schema_camper
-- requires: color
begin;
set search_path to camper, public;
create or replace function to_integer(color color) returns integer as
$$
select ('x00' || substr(color, 2))::bit(32)::integer;
$$
language sql
immutable
;
commit;

View File

@ -13,18 +13,21 @@ import (
"dev.tandem.ws/tandem/camper/pkg/company" "dev.tandem.ws/tandem/camper/pkg/company"
"dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/database"
httplib "dev.tandem.ws/tandem/camper/pkg/http" httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/season"
"dev.tandem.ws/tandem/camper/pkg/template" "dev.tandem.ws/tandem/camper/pkg/template"
) )
type adminHandler struct { type adminHandler struct {
campsite *campsite.AdminHandler campsite *campsite.AdminHandler
company *company.AdminHandler company *company.AdminHandler
season *season.AdminHandler
} }
func newAdminHandler() *adminHandler { func newAdminHandler() *adminHandler {
return &adminHandler{ return &adminHandler{
campsite: campsite.NewAdminHandler(), campsite: campsite.NewAdminHandler(),
company: company.NewAdminHandler(), company: company.NewAdminHandler(),
season: season.NewAdminHandler(),
} }
} }
@ -48,6 +51,8 @@ func (h *adminHandler) Handle(user *auth.User, company *auth.Company, conn *data
h.campsite.Handler(user, company, conn).ServeHTTP(w, r) h.campsite.Handler(user, company, conn).ServeHTTP(w, r)
case "company": case "company":
h.company.Handler(user, company, conn).ServeHTTP(w, r) h.company.Handler(user, company, conn).ServeHTTP(w, r)
case "seasons":
h.season.Handler(user, company, conn).ServeHTTP(w, r)
case "": case "":
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:

View File

@ -62,6 +62,14 @@ func (v *Validator) CheckValidPhone(ctx context.Context, conn *database.Conn, in
return v.check(input, b, message), nil return v.check(input, b, message), nil
} }
func (v *Validator) CheckValidColor(ctx context.Context, conn *database.Conn, input *Input, message string) (bool, error) {
b, err := conn.GetBool(ctx, "select input_is_valid($1, 'color')", input.Val)
if err != nil {
return false, err
}
return v.check(input, b, message), nil
}
func (v *Validator) CheckValidPostalCode(ctx context.Context, conn *database.Conn, input *Input, country string, message string) (bool, error) { func (v *Validator) CheckValidPostalCode(ctx context.Context, conn *database.Conn, input *Input, country string, message string) (bool, error) {
pattern, err := conn.GetText(ctx, "select '^' || postal_code_regex || '$' from country where country_code = $1", country) pattern, err := conn.GetText(ctx, "select '^' || postal_code_regex || '$' from country where country_code = $1", country)
if err != nil { if err != nil {

214
pkg/season/admin.go Normal file
View File

@ -0,0 +1,214 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package season
import (
"context"
"net/http"
"dev.tandem.ws/tandem/camper/pkg/auth"
"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 {
}
func NewAdminHandler() *AdminHandler {
return &AdminHandler{}
}
func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
case "new":
switch r.Method {
case http.MethodGet:
f := newSeasonForm()
f.MustRender(w, r, user, company)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
case "":
switch r.Method {
case http.MethodGet:
serveSeasonIndex(w, r, user, company, conn)
case http.MethodPost:
addSeason(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 := newSeasonForm()
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:
editSeason(w, r, user, company, conn, f)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
}
}
}
}
func serveSeasonIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
campsites, err := collectSeasonEntries(r.Context(), company, conn)
if err != nil {
panic(err)
}
page := &seasonIndex{
Seasons: campsites,
}
page.MustRender(w, r, user, company)
}
func collectSeasonEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*seasonEntry, error) {
rows, err := conn.Query(ctx, `
select slug
, name
, to_color(color)::text
, active
from season
where company_id = $1
order by name`, company.ID)
if err != nil {
return nil, err
}
defer rows.Close()
var seasons []*seasonEntry
for rows.Next() {
entry := &seasonEntry{}
if err = rows.Scan(&entry.Slug, &entry.Name, &entry.Color, &entry.Active); err != nil {
return nil, err
}
seasons = append(seasons, entry)
}
return seasons, nil
}
type seasonEntry struct {
Slug string
Name string
Color string
Active bool
}
type seasonIndex struct {
Seasons []*seasonEntry
}
func (page *seasonIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "season/index.gohtml", page)
}
func processSeasonForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *seasonForm, act func()) {
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 ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil {
panic(err)
} else if !ok {
if !httplib.IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
f.MustRender(w, r, user, company)
return
}
act()
httplib.Redirect(w, r, "/admin/seasons", http.StatusSeeOther)
}
func addSeason(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
f := newSeasonForm()
processSeasonForm(w, r, user, company, conn, f, func() {
conn.MustExec(r.Context(), "select add_season($1, $2, $3)", company.ID, f.Name, f.Color)
})
}
func editSeason(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *seasonForm) {
processSeasonForm(w, r, user, company, conn, f, func() {
conn.MustExec(r.Context(), "select edit_season($1, $2, $3, $4)", f.Slug, f.Name, f.Color, f.Active)
})
}
type seasonForm struct {
Slug string
Active *form.Checkbox
Name *form.Input
Color *form.Input
}
func newSeasonForm() *seasonForm {
return &seasonForm{
Active: &form.Checkbox{
Name: "active",
Checked: true,
},
Name: &form.Input{
Name: "label",
},
Color: &form.Input{
Name: "season",
},
}
}
func (f *seasonForm) FillFromDatabase(ctx context.Context, conn *database.Conn, slug string) error {
f.Slug = slug
row := conn.QueryRow(ctx, "select name, to_color(color)::text, active from season where slug = $1", slug)
return row.Scan(&f.Name.Val, &f.Color.Val, &f.Active.Checked)
}
func (f *seasonForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
f.Active.FillValue(r)
f.Name.FillValue(r)
f.Color.FillValue(r)
return nil
}
func (f *seasonForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
v := form.NewValidator(l)
v.CheckRequired(f.Name, l.GettextNoop("Name can not be empty."))
if v.CheckRequired(f.Color, l.GettextNoop("Color can not be empty.")) {
if _, err := v.CheckValidColor(ctx, conn, f.Color, l.Gettext("This color is not valid. It must be like #123abc.")); err != nil {
return false, err
}
}
return v.AllOK, nil
}
func (f *seasonForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "season/form.gohtml", f)
}

178
po/ca.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: camper\n" "Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-08-15 22:22+0200\n" "POT-Creation-Date: 2023-08-16 20:03+0200\n"
"PO-Revision-Date: 2023-07-22 23:45+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n" "Language-Team: Catalan <ca@dodds.net>\n"
@ -36,133 +36,185 @@ msgid "Singular Lodges"
msgstr "Allotjaments singulars" msgstr "Allotjaments singulars"
#: web/templates/admin/campsite/form.gohtml:8 #: web/templates/admin/campsite/form.gohtml:8
#: web/templates/admin/campsite/form.gohtml:25 #: web/templates/admin/campsite/form.gohtml:26
msgctxt "title" msgctxt "title"
msgid "Edit Campsite" msgid "Edit Campsite"
msgstr "Edició de lallotjament" msgstr "Edició de lallotjament"
#: web/templates/admin/campsite/form.gohtml:10 #: web/templates/admin/campsite/form.gohtml:10
#: web/templates/admin/campsite/form.gohtml:27 #: web/templates/admin/campsite/form.gohtml:28
msgctxt "title" msgctxt "title"
msgid "New Campsite" msgid "New Campsite"
msgstr "Nou allotjament" msgstr "Nou allotjament"
#: web/templates/admin/campsite/form.gohtml:37 #: web/templates/admin/campsite/form.gohtml:38
#: web/templates/admin/campsite/type/form.gohtml:37 #: web/templates/admin/campsite/index.gohtml:20
#: web/templates/admin/campsite/type/index.gohtml:18 msgctxt "campsite"
msgctxt "campsite type"
msgid "Active" msgid "Active"
msgstr "Actiu" msgstr "Actiu"
#: web/templates/admin/campsite/form.gohtml:46 #: web/templates/admin/campsite/form.gohtml:47
msgctxt "input" msgctxt "input"
msgid "Campsite Type" msgid "Campsite Type"
msgstr "Tipus dallotjament" msgstr "Tipus dallotjament"
#: web/templates/admin/campsite/form.gohtml:51 #: web/templates/admin/campsite/form.gohtml:52
msgid "Select campsite type" msgid "Select campsite type"
msgstr "Escolliu un tipus dallotjament" msgstr "Escolliu un tipus dallotjament"
#: web/templates/admin/campsite/form.gohtml:60 #: web/templates/admin/campsite/form.gohtml:61
msgctxt "input" msgctxt "input"
msgid "Label" msgid "Label"
msgstr "Etiqueta" msgstr "Etiqueta"
#: web/templates/admin/campsite/form.gohtml:70 #: web/templates/admin/campsite/form.gohtml:71
#: web/templates/admin/campsite/type/form.gohtml:63 #: web/templates/admin/campsite/type/form.gohtml:64
#: web/templates/admin/season/form.gohtml:65
msgctxt "action" msgctxt "action"
msgid "Update" msgid "Update"
msgstr "Actualitza" msgstr "Actualitza"
#: web/templates/admin/campsite/form.gohtml:72 #: web/templates/admin/campsite/form.gohtml:73
#: web/templates/admin/campsite/type/form.gohtml:65 #: web/templates/admin/campsite/type/form.gohtml:66
#: web/templates/admin/season/form.gohtml:67
msgctxt "action" msgctxt "action"
msgid "Add" msgid "Add"
msgstr "Afegeix" msgstr "Afegeix"
#: web/templates/admin/campsite/index.gohtml:6 #: web/templates/admin/campsite/index.gohtml:6
#: web/templates/admin/campsite/index.gohtml:12 #: web/templates/admin/campsite/index.gohtml:13
#: web/templates/admin/layout.gohtml:70
msgctxt "title" msgctxt "title"
msgid "Campsites" msgid "Campsites"
msgstr "Allotjaments" msgstr "Allotjaments"
#: web/templates/admin/campsite/index.gohtml:11 #: web/templates/admin/campsite/index.gohtml:12
msgctxt "action" msgctxt "action"
msgid "Add Campsite" msgid "Add Campsite"
msgstr "Afegeix allotjament" msgstr "Afegeix allotjament"
#: web/templates/admin/campsite/index.gohtml:17 #: web/templates/admin/campsite/index.gohtml:18
msgctxt "header" msgctxt "header"
msgid "Label" msgid "Label"
msgstr "Etiqueta" msgstr "Etiqueta"
#: web/templates/admin/campsite/index.gohtml:18 #: web/templates/admin/campsite/index.gohtml:19
msgctxt "header" msgctxt "header"
msgid "Type" msgid "Type"
msgstr "Tipus" msgstr "Tipus"
#: web/templates/admin/campsite/index.gohtml:19 #: web/templates/admin/campsite/index.gohtml:28
msgctxt "campsite" #: web/templates/admin/campsite/type/index.gohtml:26
msgid "Active" #: web/templates/admin/season/index.gohtml:32
msgstr "Actiu"
#: web/templates/admin/campsite/index.gohtml:27
#: web/templates/admin/campsite/type/index.gohtml:25
msgid "Yes" msgid "Yes"
msgstr "Sí" msgstr "Sí"
#: web/templates/admin/campsite/index.gohtml:27 #: web/templates/admin/campsite/index.gohtml:28
#: web/templates/admin/campsite/type/index.gohtml:25 #: web/templates/admin/campsite/type/index.gohtml:26
#: web/templates/admin/season/index.gohtml:32
msgid "No" msgid "No"
msgstr "No" msgstr "No"
#: web/templates/admin/campsite/index.gohtml:33 #: web/templates/admin/campsite/index.gohtml:34
msgid "No campsites added yet." msgid "No campsites added yet."
msgstr "No sha afegit cap allotjament encara." msgstr "No sha afegit cap allotjament encara."
#: web/templates/admin/campsite/type/form.gohtml:8 #: web/templates/admin/campsite/type/form.gohtml:8
#: web/templates/admin/campsite/type/form.gohtml:25 #: web/templates/admin/campsite/type/form.gohtml:26
msgctxt "title" msgctxt "title"
msgid "Edit Campsite Type" msgid "Edit Campsite Type"
msgstr "Edició del tipus dallotjament" msgstr "Edició del tipus dallotjament"
#: web/templates/admin/campsite/type/form.gohtml:10 #: web/templates/admin/campsite/type/form.gohtml:10
#: web/templates/admin/campsite/type/form.gohtml:27 #: web/templates/admin/campsite/type/form.gohtml:28
msgctxt "title" msgctxt "title"
msgid "New Campsite Type" msgid "New Campsite Type"
msgstr "Nou tipus dallotjament" msgstr "Nou tipus dallotjament"
#: web/templates/admin/campsite/type/form.gohtml:46 #: web/templates/admin/campsite/type/form.gohtml:38
#: web/templates/admin/campsite/type/index.gohtml:19
msgctxt "campsite type"
msgid "Active"
msgstr "Actiu"
#: web/templates/admin/campsite/type/form.gohtml:47
#: web/templates/admin/season/form.gohtml:47
#: web/templates/admin/profile.gohtml:26 #: web/templates/admin/profile.gohtml:26
msgctxt "input" msgctxt "input"
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
#: web/templates/admin/campsite/type/form.gohtml:54 #: web/templates/admin/campsite/type/form.gohtml:55
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripció" msgstr "Descripció"
#: web/templates/admin/campsite/type/index.gohtml:6 #: web/templates/admin/campsite/type/index.gohtml:6
#: web/templates/admin/campsite/type/index.gohtml:12 #: web/templates/admin/campsite/type/index.gohtml:13
#: web/templates/admin/layout.gohtml:67
msgctxt "title" msgctxt "title"
msgid "Campsite Types" msgid "Campsite Types"
msgstr "Tipus dallotjaments" msgstr "Tipus dallotjaments"
#: web/templates/admin/campsite/type/index.gohtml:11 #: web/templates/admin/campsite/type/index.gohtml:12
msgctxt "action" msgctxt "action"
msgid "Add Type" msgid "Add Type"
msgstr "Afegeix tipus" msgstr "Afegeix tipus"
#: web/templates/admin/campsite/type/index.gohtml:17 #: web/templates/admin/campsite/type/index.gohtml:18
#: web/templates/admin/season/index.gohtml:18
msgctxt "header" msgctxt "header"
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
#: web/templates/admin/campsite/type/index.gohtml:31 #: web/templates/admin/campsite/type/index.gohtml:32
msgid "No campsite types added yet." msgid "No campsite types added yet."
msgstr "No sha afegit cap tipus dallotjament encara." msgstr "No sha afegit cap tipus dallotjament encara."
#: web/templates/admin/season/form.gohtml:8
#: web/templates/admin/season/form.gohtml:26
msgctxt "title"
msgid "Edit Season"
msgstr "Edició de la temporada"
#: web/templates/admin/season/form.gohtml:10
#: web/templates/admin/season/form.gohtml:28
msgctxt "title"
msgid "New Season"
msgstr "Nova temporada"
#: web/templates/admin/season/form.gohtml:38
#: web/templates/admin/season/index.gohtml:20
msgctxt "season"
msgid "Active"
msgstr "Activa"
#: web/templates/admin/season/form.gohtml:55
msgctxt "input"
msgid "Color"
msgstr "Color"
#: web/templates/admin/season/index.gohtml:6
#: web/templates/admin/season/index.gohtml:13
#: web/templates/admin/layout.gohtml:73
msgctxt "title"
msgid "Seasons"
msgstr "Temporades"
#: web/templates/admin/season/index.gohtml:12
msgctxt "action"
msgid "Add Season"
msgstr "Afegeix temporada"
#: web/templates/admin/season/index.gohtml:19
msgctxt "header"
msgid "Color"
msgstr "Color"
#: web/templates/admin/season/index.gohtml:38
msgid "No seasons added yet."
msgstr "No sha afegit cap temporada encara."
#: web/templates/admin/dashboard.gohtml:6 #: web/templates/admin/dashboard.gohtml:6
#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:49 #: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:49
msgctxt "title" msgctxt "title"
@ -175,7 +227,7 @@ msgid "Login"
msgstr "Entrada" msgstr "Entrada"
#: web/templates/admin/login.gohtml:22 web/templates/admin/profile.gohtml:35 #: web/templates/admin/login.gohtml:22 web/templates/admin/profile.gohtml:35
#: web/templates/admin/taxDetails.gohtml:50 #: web/templates/admin/taxDetails.gohtml:51
msgctxt "input" msgctxt "input"
msgid "Email" msgid "Email"
msgstr "Correu-e" msgstr "Correu-e"
@ -217,79 +269,80 @@ msgid "Language"
msgstr "Idioma" msgstr "Idioma"
#: web/templates/admin/profile.gohtml:75 #: web/templates/admin/profile.gohtml:75
#: web/templates/admin/taxDetails.gohtml:144 #: web/templates/admin/taxDetails.gohtml:145
msgctxt "action" msgctxt "action"
msgid "Save changes" msgid "Save changes"
msgstr "Desa els canvis" msgstr "Desa els canvis"
#: web/templates/admin/taxDetails.gohtml:6 #: web/templates/admin/taxDetails.gohtml:6
#: web/templates/admin/taxDetails.gohtml:12 #: web/templates/admin/taxDetails.gohtml:13
#: web/templates/admin/layout.gohtml:64
msgctxt "title" msgctxt "title"
msgid "Tax Details" msgid "Tax Details"
msgstr "Configuració fiscal" msgstr "Configuració fiscal"
#: web/templates/admin/taxDetails.gohtml:17 #: web/templates/admin/taxDetails.gohtml:18
#: web/templates/admin/taxDetails.gohtml:58 #: web/templates/admin/taxDetails.gohtml:59
msgctxt "input" msgctxt "input"
msgid "Business Name" msgid "Business Name"
msgstr "Nom de lempresa" msgstr "Nom de lempresa"
#: web/templates/admin/taxDetails.gohtml:26 #: web/templates/admin/taxDetails.gohtml:27
msgctxt "input" msgctxt "input"
msgid "VAT Number" msgid "VAT Number"
msgstr "NIF" msgstr "NIF"
#: web/templates/admin/taxDetails.gohtml:34 #: web/templates/admin/taxDetails.gohtml:35
msgctxt "input" msgctxt "input"
msgid "Trade Name" msgid "Trade Name"
msgstr "Nom comercial" msgstr "Nom comercial"
#: web/templates/admin/taxDetails.gohtml:42 #: web/templates/admin/taxDetails.gohtml:43
msgctxt "input" msgctxt "input"
msgid "Phone" msgid "Phone"
msgstr "Telèfon" msgstr "Telèfon"
#: web/templates/admin/taxDetails.gohtml:66 #: web/templates/admin/taxDetails.gohtml:67
msgctxt "input" msgctxt "input"
msgid "Address" msgid "Address"
msgstr "Adreça" msgstr "Adreça"
#: web/templates/admin/taxDetails.gohtml:74 #: web/templates/admin/taxDetails.gohtml:75
msgctxt "input" msgctxt "input"
msgid "City" msgid "City"
msgstr "Població" msgstr "Població"
#: web/templates/admin/taxDetails.gohtml:82 #: web/templates/admin/taxDetails.gohtml:83
msgctxt "input" msgctxt "input"
msgid "Province" msgid "Province"
msgstr "Província" msgstr "Província"
#: web/templates/admin/taxDetails.gohtml:90 #: web/templates/admin/taxDetails.gohtml:91
msgctxt "input" msgctxt "input"
msgid "Postal Code" msgid "Postal Code"
msgstr "Codi postal" msgstr "Codi postal"
#: web/templates/admin/taxDetails.gohtml:98 #: web/templates/admin/taxDetails.gohtml:99
msgctxt "input" msgctxt "input"
msgid "Country" msgid "Country"
msgstr "País" msgstr "País"
#: web/templates/admin/taxDetails.gohtml:108 #: web/templates/admin/taxDetails.gohtml:109
msgctxt "input" msgctxt "input"
msgid "Currency" msgid "Currency"
msgstr "Moneda" msgstr "Moneda"
#: web/templates/admin/taxDetails.gohtml:118 #: web/templates/admin/taxDetails.gohtml:119
msgctxt "input" msgctxt "input"
msgid "Default Language" msgid "Default Language"
msgstr "Idioma per defecte" msgstr "Idioma per defecte"
#: web/templates/admin/taxDetails.gohtml:128 #: web/templates/admin/taxDetails.gohtml:129
msgctxt "input" msgctxt "input"
msgid "Invoice Number Format" msgid "Invoice Number Format"
msgstr "Format del número de factura" msgstr "Format del número de factura"
#: web/templates/admin/taxDetails.gohtml:136 #: web/templates/admin/taxDetails.gohtml:137
msgctxt "input" msgctxt "input"
msgid "Legal Disclaimer" msgid "Legal Disclaimer"
msgstr "Nota legal" msgstr "Nota legal"
@ -330,7 +383,7 @@ msgctxt "language option"
msgid "Automatic" msgid "Automatic"
msgstr "Automàtic" msgstr "Automàtic"
#: pkg/app/user.go:249 pkg/campsite/types/admin.go:197 #: pkg/app/user.go:249 pkg/campsite/types/admin.go:197 pkg/season/admin.go:203
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc." msgstr "No podeu deixar el nom en blanc."
@ -346,7 +399,7 @@ msgstr "Lidioma escollit no és vàlid."
msgid "File must be a valid PNG or JPEG image." msgid "File must be a valid PNG or JPEG image."
msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida." msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida."
#: pkg/app/admin.go:40 #: pkg/app/admin.go:43
msgid "Access forbidden" msgid "Access forbidden"
msgstr "Accés prohibit" msgstr "Accés prohibit"
@ -358,6 +411,14 @@ msgstr "El tipus dallotjament escollit no és vàlid."
msgid "Label can not be empty." msgid "Label can not be empty."
msgstr "No podeu deixar letiqueta en blanc." msgstr "No podeu deixar letiqueta en blanc."
#: pkg/season/admin.go:204
msgid "Color can not be empty."
msgstr "No podeu deixar el color en blanc."
#: pkg/season/admin.go:205
msgid "This color is not valid. It must be like #123abc."
msgstr "Aquest color no és vàlid. Hauria de ser similar a #123abc."
#: pkg/company/admin.go:186 #: pkg/company/admin.go:186
msgid "Selected country is not valid." msgid "Selected country is not valid."
msgstr "El país escollit no és vàlid." msgstr "El país escollit no és vàlid."
@ -422,10 +483,6 @@ msgstr "No podeu deixar el format del número de factura en blanc."
msgid "Cross-site request forgery detected." msgid "Cross-site request forgery detected."
msgstr "Sha detectat un intent de falsificació de petició a llocs creuats." msgstr "Sha detectat un intent de falsificació de petició a llocs creuats."
#~ msgctxt "title"
#~ msgid "Edit Page"
#~ msgstr "Edició de pàgina"
#~ msgctxt "title" #~ msgctxt "title"
#~ msgid "New Page" #~ msgid "New Page"
#~ msgstr "Nova pàgina" #~ msgstr "Nova pàgina"
@ -449,6 +506,3 @@ msgstr "Sha detectat un intent de falsificació de petició a llocs creuats."
#~ msgctxt "header" #~ msgctxt "header"
#~ msgid "Title" #~ msgid "Title"
#~ msgstr "Títol" #~ msgstr "Títol"
#~ msgid "No pages added yet."
#~ msgstr "No sha afegit cap pàgina encara."

178
po/es.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: camper\n" "Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-08-15 22:23+0200\n" "POT-Creation-Date: 2023-08-16 20:03+0200\n"
"PO-Revision-Date: 2023-07-22 23:46+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n" "Language-Team: Spanish <es@tp.org.es>\n"
@ -36,133 +36,185 @@ msgid "Singular Lodges"
msgstr "Alojamientos singulares" msgstr "Alojamientos singulares"
#: web/templates/admin/campsite/form.gohtml:8 #: web/templates/admin/campsite/form.gohtml:8
#: web/templates/admin/campsite/form.gohtml:25 #: web/templates/admin/campsite/form.gohtml:26
msgctxt "title" msgctxt "title"
msgid "Edit Campsite" msgid "Edit Campsite"
msgstr "Edición del alojamientos" msgstr "Edición del alojamientos"
#: web/templates/admin/campsite/form.gohtml:10 #: web/templates/admin/campsite/form.gohtml:10
#: web/templates/admin/campsite/form.gohtml:27 #: web/templates/admin/campsite/form.gohtml:28
msgctxt "title" msgctxt "title"
msgid "New Campsite" msgid "New Campsite"
msgstr "Nuevo alojamiento" msgstr "Nuevo alojamiento"
#: web/templates/admin/campsite/form.gohtml:37 #: web/templates/admin/campsite/form.gohtml:38
#: web/templates/admin/campsite/type/form.gohtml:37 #: web/templates/admin/campsite/index.gohtml:20
#: web/templates/admin/campsite/type/index.gohtml:18 msgctxt "campsite"
msgctxt "campsite type"
msgid "Active" msgid "Active"
msgstr "Activo" msgstr "Activo"
#: web/templates/admin/campsite/form.gohtml:46 #: web/templates/admin/campsite/form.gohtml:47
msgctxt "input" msgctxt "input"
msgid "Campsite Type" msgid "Campsite Type"
msgstr "Tipo de alojamiento" msgstr "Tipo de alojamiento"
#: web/templates/admin/campsite/form.gohtml:51 #: web/templates/admin/campsite/form.gohtml:52
msgid "Select campsite type" msgid "Select campsite type"
msgstr "Escoged un tipo de alojamiento" msgstr "Escoged un tipo de alojamiento"
#: web/templates/admin/campsite/form.gohtml:60 #: web/templates/admin/campsite/form.gohtml:61
msgctxt "input" msgctxt "input"
msgid "Label" msgid "Label"
msgstr "Etiqueta" msgstr "Etiqueta"
#: web/templates/admin/campsite/form.gohtml:70 #: web/templates/admin/campsite/form.gohtml:71
#: web/templates/admin/campsite/type/form.gohtml:63 #: web/templates/admin/campsite/type/form.gohtml:64
#: web/templates/admin/season/form.gohtml:65
msgctxt "action" msgctxt "action"
msgid "Update" msgid "Update"
msgstr "Actualizar" msgstr "Actualizar"
#: web/templates/admin/campsite/form.gohtml:72 #: web/templates/admin/campsite/form.gohtml:73
#: web/templates/admin/campsite/type/form.gohtml:65 #: web/templates/admin/campsite/type/form.gohtml:66
#: web/templates/admin/season/form.gohtml:67
msgctxt "action" msgctxt "action"
msgid "Add" msgid "Add"
msgstr "Añadir" msgstr "Añadir"
#: web/templates/admin/campsite/index.gohtml:6 #: web/templates/admin/campsite/index.gohtml:6
#: web/templates/admin/campsite/index.gohtml:12 #: web/templates/admin/campsite/index.gohtml:13
#: web/templates/admin/layout.gohtml:70
msgctxt "title" msgctxt "title"
msgid "Campsites" msgid "Campsites"
msgstr "Alojamientos" msgstr "Alojamientos"
#: web/templates/admin/campsite/index.gohtml:11 #: web/templates/admin/campsite/index.gohtml:12
msgctxt "action" msgctxt "action"
msgid "Add Campsite" msgid "Add Campsite"
msgstr "Añadir alojamiento" msgstr "Añadir alojamiento"
#: web/templates/admin/campsite/index.gohtml:17 #: web/templates/admin/campsite/index.gohtml:18
msgctxt "header" msgctxt "header"
msgid "Label" msgid "Label"
msgstr "Etiqueta" msgstr "Etiqueta"
#: web/templates/admin/campsite/index.gohtml:18 #: web/templates/admin/campsite/index.gohtml:19
msgctxt "header" msgctxt "header"
msgid "Type" msgid "Type"
msgstr "Tipo" msgstr "Tipo"
#: web/templates/admin/campsite/index.gohtml:19 #: web/templates/admin/campsite/index.gohtml:28
msgctxt "campsite" #: web/templates/admin/campsite/type/index.gohtml:26
msgid "Active" #: web/templates/admin/season/index.gohtml:32
msgstr "Activo"
#: web/templates/admin/campsite/index.gohtml:27
#: web/templates/admin/campsite/type/index.gohtml:25
msgid "Yes" msgid "Yes"
msgstr "Sí" msgstr "Sí"
#: web/templates/admin/campsite/index.gohtml:27 #: web/templates/admin/campsite/index.gohtml:28
#: web/templates/admin/campsite/type/index.gohtml:25 #: web/templates/admin/campsite/type/index.gohtml:26
#: web/templates/admin/season/index.gohtml:32
msgid "No" msgid "No"
msgstr "No" msgstr "No"
#: web/templates/admin/campsite/index.gohtml:33 #: web/templates/admin/campsite/index.gohtml:34
msgid "No campsites added yet." msgid "No campsites added yet."
msgstr "No se ha añadido ningún alojamiento todavía." 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:8
#: web/templates/admin/campsite/type/form.gohtml:25 #: web/templates/admin/campsite/type/form.gohtml:26
msgctxt "title" msgctxt "title"
msgid "Edit Campsite Type" msgid "Edit Campsite Type"
msgstr "Edición del tipo de alojamientos" msgstr "Edición del tipo de alojamientos"
#: web/templates/admin/campsite/type/form.gohtml:10 #: web/templates/admin/campsite/type/form.gohtml:10
#: web/templates/admin/campsite/type/form.gohtml:27 #: web/templates/admin/campsite/type/form.gohtml:28
msgctxt "title" msgctxt "title"
msgid "New Campsite Type" msgid "New Campsite Type"
msgstr "Nuevo tipo de alojamiento" msgstr "Nuevo tipo de alojamiento"
#: web/templates/admin/campsite/type/form.gohtml:46 #: web/templates/admin/campsite/type/form.gohtml:38
#: web/templates/admin/campsite/type/index.gohtml:19
msgctxt "campsite type"
msgid "Active"
msgstr "Activo"
#: web/templates/admin/campsite/type/form.gohtml:47
#: web/templates/admin/season/form.gohtml:47
#: web/templates/admin/profile.gohtml:26 #: web/templates/admin/profile.gohtml:26
msgctxt "input" msgctxt "input"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: web/templates/admin/campsite/type/form.gohtml:54 #: web/templates/admin/campsite/type/form.gohtml:55
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripción" msgstr "Descripción"
#: web/templates/admin/campsite/type/index.gohtml:6 #: web/templates/admin/campsite/type/index.gohtml:6
#: web/templates/admin/campsite/type/index.gohtml:12 #: web/templates/admin/campsite/type/index.gohtml:13
#: web/templates/admin/layout.gohtml:67
msgctxt "title" msgctxt "title"
msgid "Campsite Types" msgid "Campsite Types"
msgstr "Tipos de alojamientos" msgstr "Tipos de alojamientos"
#: web/templates/admin/campsite/type/index.gohtml:11 #: web/templates/admin/campsite/type/index.gohtml:12
msgctxt "action" msgctxt "action"
msgid "Add Type" msgid "Add Type"
msgstr "Añadir tipo" msgstr "Añadir tipo"
#: web/templates/admin/campsite/type/index.gohtml:17 #: web/templates/admin/campsite/type/index.gohtml:18
#: web/templates/admin/season/index.gohtml:18
msgctxt "header" msgctxt "header"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: web/templates/admin/campsite/type/index.gohtml:31 #: web/templates/admin/campsite/type/index.gohtml:32
msgid "No campsite types added yet." msgid "No campsite types added yet."
msgstr "No se ha añadido ningún tipo de alojamiento todavía." msgstr "No se ha añadido ningún tipo de alojamiento todavía."
#: web/templates/admin/season/form.gohtml:8
#: web/templates/admin/season/form.gohtml:26
msgctxt "title"
msgid "Edit Season"
msgstr "Edición de temporada"
#: web/templates/admin/season/form.gohtml:10
#: web/templates/admin/season/form.gohtml:28
msgctxt "title"
msgid "New Season"
msgstr "Nueva temporada"
#: web/templates/admin/season/form.gohtml:38
#: web/templates/admin/season/index.gohtml:20
msgctxt "season"
msgid "Active"
msgstr "Activa"
#: web/templates/admin/season/form.gohtml:55
msgctxt "input"
msgid "Color"
msgstr "Color"
#: web/templates/admin/season/index.gohtml:6
#: web/templates/admin/season/index.gohtml:13
#: web/templates/admin/layout.gohtml:73
msgctxt "title"
msgid "Seasons"
msgstr "Temporadas"
#: web/templates/admin/season/index.gohtml:12
msgctxt "action"
msgid "Add Season"
msgstr "Añadir temporada"
#: web/templates/admin/season/index.gohtml:19
msgctxt "header"
msgid "Color"
msgstr "Color"
#: web/templates/admin/season/index.gohtml:38
msgid "No seasons added yet."
msgstr "No se ha añadido ninguna temporada todavía."
#: web/templates/admin/dashboard.gohtml:6 #: web/templates/admin/dashboard.gohtml:6
#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:49 #: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:49
msgctxt "title" msgctxt "title"
@ -175,7 +227,7 @@ msgid "Login"
msgstr "Entrada" msgstr "Entrada"
#: web/templates/admin/login.gohtml:22 web/templates/admin/profile.gohtml:35 #: web/templates/admin/login.gohtml:22 web/templates/admin/profile.gohtml:35
#: web/templates/admin/taxDetails.gohtml:50 #: web/templates/admin/taxDetails.gohtml:51
msgctxt "input" msgctxt "input"
msgid "Email" msgid "Email"
msgstr "Correo-e" msgstr "Correo-e"
@ -217,79 +269,80 @@ msgid "Language"
msgstr "Idioma" msgstr "Idioma"
#: web/templates/admin/profile.gohtml:75 #: web/templates/admin/profile.gohtml:75
#: web/templates/admin/taxDetails.gohtml:144 #: web/templates/admin/taxDetails.gohtml:145
msgctxt "action" msgctxt "action"
msgid "Save changes" msgid "Save changes"
msgstr "Guardar los cambios" msgstr "Guardar los cambios"
#: web/templates/admin/taxDetails.gohtml:6 #: web/templates/admin/taxDetails.gohtml:6
#: web/templates/admin/taxDetails.gohtml:12 #: web/templates/admin/taxDetails.gohtml:13
#: web/templates/admin/layout.gohtml:64
msgctxt "title" msgctxt "title"
msgid "Tax Details" msgid "Tax Details"
msgstr "Configuración fiscal" msgstr "Configuración fiscal"
#: web/templates/admin/taxDetails.gohtml:17 #: web/templates/admin/taxDetails.gohtml:18
#: web/templates/admin/taxDetails.gohtml:58 #: web/templates/admin/taxDetails.gohtml:59
msgctxt "input" msgctxt "input"
msgid "Business Name" msgid "Business Name"
msgstr "Nombre de empresa" msgstr "Nombre de empresa"
#: web/templates/admin/taxDetails.gohtml:26 #: web/templates/admin/taxDetails.gohtml:27
msgctxt "input" msgctxt "input"
msgid "VAT Number" msgid "VAT Number"
msgstr "NIF" msgstr "NIF"
#: web/templates/admin/taxDetails.gohtml:34 #: web/templates/admin/taxDetails.gohtml:35
msgctxt "input" msgctxt "input"
msgid "Trade Name" msgid "Trade Name"
msgstr "Nombre comercial" msgstr "Nombre comercial"
#: web/templates/admin/taxDetails.gohtml:42 #: web/templates/admin/taxDetails.gohtml:43
msgctxt "input" msgctxt "input"
msgid "Phone" msgid "Phone"
msgstr "Teléfono" msgstr "Teléfono"
#: web/templates/admin/taxDetails.gohtml:66 #: web/templates/admin/taxDetails.gohtml:67
msgctxt "input" msgctxt "input"
msgid "Address" msgid "Address"
msgstr "Dirección" msgstr "Dirección"
#: web/templates/admin/taxDetails.gohtml:74 #: web/templates/admin/taxDetails.gohtml:75
msgctxt "input" msgctxt "input"
msgid "City" msgid "City"
msgstr "Población" msgstr "Población"
#: web/templates/admin/taxDetails.gohtml:82 #: web/templates/admin/taxDetails.gohtml:83
msgctxt "input" msgctxt "input"
msgid "Province" msgid "Province"
msgstr "Provincia" msgstr "Provincia"
#: web/templates/admin/taxDetails.gohtml:90 #: web/templates/admin/taxDetails.gohtml:91
msgctxt "input" msgctxt "input"
msgid "Postal Code" msgid "Postal Code"
msgstr "Código postal" msgstr "Código postal"
#: web/templates/admin/taxDetails.gohtml:98 #: web/templates/admin/taxDetails.gohtml:99
msgctxt "input" msgctxt "input"
msgid "Country" msgid "Country"
msgstr "País" msgstr "País"
#: web/templates/admin/taxDetails.gohtml:108 #: web/templates/admin/taxDetails.gohtml:109
msgctxt "input" msgctxt "input"
msgid "Currency" msgid "Currency"
msgstr "Moneda" msgstr "Moneda"
#: web/templates/admin/taxDetails.gohtml:118 #: web/templates/admin/taxDetails.gohtml:119
msgctxt "input" msgctxt "input"
msgid "Default Language" msgid "Default Language"
msgstr "Idioma por defecto" msgstr "Idioma por defecto"
#: web/templates/admin/taxDetails.gohtml:128 #: web/templates/admin/taxDetails.gohtml:129
msgctxt "input" msgctxt "input"
msgid "Invoice Number Format" msgid "Invoice Number Format"
msgstr "Formato de número de factura" msgstr "Formato de número de factura"
#: web/templates/admin/taxDetails.gohtml:136 #: web/templates/admin/taxDetails.gohtml:137
msgctxt "input" msgctxt "input"
msgid "Legal Disclaimer" msgid "Legal Disclaimer"
msgstr "Nota legal" msgstr "Nota legal"
@ -330,7 +383,7 @@ msgctxt "language option"
msgid "Automatic" msgid "Automatic"
msgstr "Automático" msgstr "Automático"
#: pkg/app/user.go:249 pkg/campsite/types/admin.go:197 #: pkg/app/user.go:249 pkg/campsite/types/admin.go:197 pkg/season/admin.go:203
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco." msgstr "No podéis dejar el nombre en blanco."
@ -346,7 +399,7 @@ msgstr "El idioma escogido no es válido."
msgid "File must be a valid PNG or JPEG image." msgid "File must be a valid PNG or JPEG image."
msgstr "El archivo tiene que ser una imagen PNG o JPEG válida." msgstr "El archivo tiene que ser una imagen PNG o JPEG válida."
#: pkg/app/admin.go:40 #: pkg/app/admin.go:43
msgid "Access forbidden" msgid "Access forbidden"
msgstr "Acceso prohibido" msgstr "Acceso prohibido"
@ -358,6 +411,14 @@ msgstr "El tipo de alojamiento escogido no es válido."
msgid "Label can not be empty." msgid "Label can not be empty."
msgstr "No podéis dejar la etiqueta en blanco." msgstr "No podéis dejar la etiqueta en blanco."
#: pkg/season/admin.go:204
msgid "Color can not be empty."
msgstr "No podéis dejar el color en blanco."
#: pkg/season/admin.go:205
msgid "This color is not valid. It must be like #123abc."
msgstr "Este color no es válido. Tiene que ser parecido a #123abc."
#: pkg/company/admin.go:186 #: pkg/company/admin.go:186
msgid "Selected country is not valid." msgid "Selected country is not valid."
msgstr "El país escogido no es válido." msgstr "El país escogido no es válido."
@ -422,10 +483,6 @@ msgstr "No podéis dejar el formato de número de factura en blanco."
msgid "Cross-site request forgery detected." msgid "Cross-site request forgery detected."
msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados." msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados."
#~ msgctxt "title"
#~ msgid "Edit Page"
#~ msgstr "Edición de página"
#~ msgctxt "title" #~ msgctxt "title"
#~ msgid "New Page" #~ msgid "New Page"
#~ msgstr "Nueva página" #~ msgstr "Nueva página"
@ -449,6 +506,3 @@ msgstr "Se ha detectado un intento de falsificación de petición en sitios cruz
#~ msgctxt "header" #~ msgctxt "header"
#~ msgid "Title" #~ msgid "Title"
#~ msgstr "Título" #~ msgstr "Título"
#~ msgid "No pages added yet."
#~ msgstr "No se ha añadido ninguna página todavía."

7
revert/add_season.sql Normal file
View File

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

7
revert/color.sql Normal file
View File

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

7
revert/edit_season.sql Normal file
View File

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

7
revert/season.sql Normal file
View File

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

7
revert/to_color.sql Normal file
View File

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

7
revert/to_integer.sql Normal file
View File

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

View File

@ -47,3 +47,9 @@ add_campsite [roles schema_camper campsite campsite_type] 2023-08-14T17:03:23Z j
edit_campsite [roles schema_camper campsite] 2023-08-14T17:28:16Z jordi fita mas <jordi@tandem.blog> # Add function to update campsites edit_campsite [roles schema_camper campsite] 2023-08-14T17:28:16Z jordi fita mas <jordi@tandem.blog> # Add function to update campsites
input_is_valid [roles schema_public] 2023-08-15T20:10:59Z jordi fita mas <jordi@tandem.blog> # Add function to check if an input string is valid for a domain input_is_valid [roles schema_public] 2023-08-15T20:10:59Z jordi fita mas <jordi@tandem.blog> # Add function to check if an input string is valid for a domain
input_is_valid_phone [roles schema_public extension_pg_libphonenumber] 2023-08-15T20:15:01Z jordi fita mas <jordi@tandem.blog> # Add function to check if an input string is valid for the phone number domain input_is_valid_phone [roles schema_public extension_pg_libphonenumber] 2023-08-15T20:15:01Z jordi fita mas <jordi@tandem.blog> # Add function to check if an input string is valid for the phone number domain
color [schema_camper extension_citext] 2023-08-16T12:46:43Z jordi fita mas <jordi@tandem.blog> # Add domain for HTML colors
to_integer [roles schema_camper color] 2023-08-16T13:02:08Z jordi fita mas <jordi@tandem.blog> # Add function to convert color to integer
to_color [roles schema_camper color] 2023-08-16T13:11:32Z jordi fita mas <jordi@tandem.blog> # Add function to convert integer to color
season [roles schema_camper company user_profile] 2023-08-16T13:21:28Z jordi fita mas <jordi@tandem.blog> # Add relation of (tourist) season
add_season [roles schema_camper season color to_integer] 2023-08-16T16:59:17Z jordi fita mas <jordi@tandem.blog> # Add function to create seasons
edit_season [roles schema_camper season color to_integer] 2023-08-16T17:09:02Z jordi fita mas <jordi@tandem.blog> # Add function to update seasons

60
test/add_season.sql Normal file
View File

@ -0,0 +1,60 @@
-- Test add_season
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(13);
select has_function('camper', 'add_season', array ['integer', 'text', 'color']);
select function_lang_is('camper', 'add_season', array ['integer', 'text', 'color'], 'sql');
select function_returns('camper', 'add_season', array ['integer', 'text', 'color'], 'uuid');
select isnt_definer('camper', 'add_season', array ['integer', 'text', 'color']);
select volatility_is('camper', 'add_season', array ['integer', 'text', 'color'], 'volatile');
select function_privs_are('camper', 'add_season', array ['integer', 'text', 'color'], 'guest', array[]::text[]);
select function_privs_are('camper', 'add_season', array ['integer', 'text', 'color'], 'employee', array[]::text[]);
select function_privs_are('camper', 'add_season', array ['integer', 'text', 'color'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'add_season', array ['integer', 'text', 'color'], 'authenticator', array[]::text[]);
set client_min_messages to warning;
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 (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca')
;
select lives_ok(
$$ select add_season(1, 'Low', '#232323') $$,
'Should be able to add a season to the first company'
);
select lives_ok(
$$ select add_season(1, 'Mid', '#555555') $$,
'Should be able to add another season to the same company'
);
select lives_ok(
$$ select add_season(2, 'High', '#a0a0a0') $$,
'Should be able to add a season to the second company'
);
select bag_eq(
$$ select company_id, name, to_color(color)::text, active from season $$,
$$ values (1, 'Low', '#232323', true)
, (1, 'Mid', '#555555', true)
, (2, 'High', '#a0a0a0', true)
$$,
'Should have added all seasons'
);
select *
from finish();
rollback;

46
test/color.sql Normal file
View File

@ -0,0 +1,46 @@
-- Test color
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(8);
set search_path to camper, public;
select has_domain('color');
select domain_type_is('color', 'citext');
select lives_ok($$ select '#775544'::color $$, 'Should be able to cast strings to color');
select lives_ok($$ select '#aABbCc'::color $$, 'Should be able to cast hex-strings to color');
select throws_ok(
$$ select '0775544'::color $$,
23514, null,
'Should reject colors without the initial hash character'
);
select throws_ok(
$$ select '#red'::color $$,
23514, null,
'Should reject named colors'
);
select throws_ok(
$$ select '#0011ag'::color $$,
23514, null,
'Should reject colors with invalid hex digits'
);
select throws_ok(
$$ select '#00112233'::color $$,
23514, null,
'Should reject colors with more than three pairs (i.e., no alpha)'
);
select *
from finish();
rollback;

58
test/edit_season.sql Normal file
View File

@ -0,0 +1,58 @@
-- Test edit_season
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_season', array ['uuid', 'text', 'color', 'boolean']);
select function_lang_is('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean'], 'sql');
select function_returns('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean'], 'uuid');
select isnt_definer('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean']);
select volatility_is('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean'], 'volatile');
select function_privs_are('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean'], 'guest', array[]::text[]);
select function_privs_are('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean'], 'employee', array[]::text[]);
select function_privs_are('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'edit_season', array ['uuid', 'text', 'color', 'boolean'], 'authenticator', array[]::text[]);
set client_min_messages to warning;
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 (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
;
insert into season (company_id, slug, name, color, active)
values (1, '87452b88-b48f-48d3-bb6c-0296de64164e', 'Low', to_integer('#232323'), true)
, (1, '9b6370f7-f941-46f2-bc6e-de455675bd0a', 'High', to_integer('#323232'), false)
;
select lives_ok(
$$ select edit_season('87452b88-b48f-48d3-bb6c-0296de64164e', 'Very Low', '#1e1e1e', false) $$,
'Should be able to edit the first season'
);
select lives_ok(
$$ select edit_season('9b6370f7-f941-46f2-bc6e-de455675bd0a', 'Very High', '#9f9f9f', true) $$,
'Should be able to edit the second season'
);
select bag_eq(
$$ select slug::text, name, to_color(color)::text, active from season $$,
$$ values ('87452b88-b48f-48d3-bb6c-0296de64164e', 'Very Low', '#1e1e1e', false)
, ('9b6370f7-f941-46f2-bc6e-de455675bd0a', 'Very High', '#9f9f9f', true)
$$,
'Should have updated all seasons.'
);
select *
from finish();
rollback;

206
test/season.sql Normal file
View File

@ -0,0 +1,206 @@
-- Test season
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(57);
set search_path to camper, public;
select has_table('season');
select has_pk('season' );
select table_privs_are('season', 'guest', array['SELECT']);
select table_privs_are('season', 'employee', array['SELECT']);
select table_privs_are('season', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('season', 'authenticator', array[]::text[]);
select has_sequence('season_season_id_seq');
select sequence_privs_are('season_season_id_seq', 'guest', array[]::text[]);
select sequence_privs_are('season_season_id_seq', 'employee', array[]::text[]);
select sequence_privs_are('season_season_id_seq', 'admin', array['USAGE']);
select sequence_privs_are('season_season_id_seq', 'authenticator', array[]::text[]);
select has_column('season', 'season_id');
select col_is_pk('season', 'season_id');
select col_type_is('season', 'season_id', 'integer');
select col_not_null('season', 'season_id');
select col_has_default('season', 'season_id');
select col_default_is('season', 'season_id', 'nextval(''season_season_id_seq''::regclass)');
select has_column('season', 'company_id');
select col_is_fk('season', 'company_id');
select fk_ok('season', 'company_id', 'company', 'company_id');
select col_type_is('season', 'company_id', 'integer');
select col_not_null('season', 'company_id');
select col_hasnt_default('season', 'company_id');
select has_column('season', 'slug');
select col_is_unique('season', 'slug');
select col_type_is('season', 'slug', 'uuid');
select col_not_null('season', 'slug');
select col_has_default('season', 'slug');
select col_default_is('season', 'slug', 'gen_random_uuid()');
select has_column('season', 'name');
select col_type_is('season', 'name', 'text');
select col_not_null('season', 'name');
select col_hasnt_default('season', 'name');
select has_column('season', 'color');
select col_type_is('season', 'color', 'integer');
select col_not_null('season', 'color');
select col_has_default('season', 'color');
select col_default_is('season', 'color', '0');
select has_column('season', 'active');
select col_type_is('season', 'active', 'boolean');
select col_not_null('season', 'active');
select col_has_default('season', 'active');
select col_default_is('season', 'active', 'true');
set client_min_messages to warning;
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 (company_id, name)
values (2, 'Low')
, (4, 'High')
;
prepare season_data as
select company_id, name
from season
order by company_id, name;
set role guest;
select bag_eq(
'season_data',
$$ values (2, 'Low')
, (4, 'High')
$$,
'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(company_id, name) values (2, 'Another type' ) $$,
'Admin from company 2 should be able to insert a new season to that company.'
);
select bag_eq(
'season_data',
$$ values (2, 'Low')
, (2, 'Another type')
, (4, 'High')
$$,
'The new row should have been added'
);
select lives_ok(
$$ update season set name = 'Another' where company_id = 2 and name = 'Another type' $$,
'Admin from company 2 should be able to update season of that company.'
);
select bag_eq(
'season_data',
$$ values (2, 'Low')
, (2, 'Another')
, (4, 'High')
$$,
'The row should have been updated.'
);
select lives_ok(
$$ delete from season where company_id = 2 and name = 'Another' $$,
'Admin from company 2 should be able to delete season from that company.'
);
select bag_eq(
'season_data',
$$ values (2, 'Low')
, (4, 'High')
$$,
'The row should have been deleted.'
);
select throws_ok(
$$ insert into season (company_id, name) values (4, 'Another type' ) $$,
'42501', 'new row violates row-level security policy for table "season"',
'Admin from company 2 should NOT be able to insert new seasons to company 4.'
);
select lives_ok(
$$ update season set name = 'Nope' where company_id = 4 $$,
'Admin from company 2 should not be able to update new seasons of company 4, but no error if company_id is not changed.'
);
select bag_eq(
'season_data',
$$ values (2, 'Low')
, (4, 'High')
$$,
'No row should have been changed.'
);
select throws_ok(
$$ update season set company_id = 4 where company_id = 2 $$,
'42501', 'new row violates row-level security policy for table "season"',
'Admin from company 2 should NOT be able to move seasons to company 4'
);
select lives_ok(
$$ delete from season where company_id = 4 $$,
'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 (2, 'Low')
, (4, 'High')
$$,
'No row should have been changed'
);
select throws_ok(
$$ insert into season (company_id, name) values (2, ' ' ) $$,
'23514', 'new row for relation "season" violates check constraint "name_not_empty"',
'Should not be able to insert seasons with a blank name.'
);
reset role;
select *
from finish();
rollback;

37
test/to_color.sql Normal file
View File

@ -0,0 +1,37 @@
-- Test to_color
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(20);
select has_function('camper', 'to_color', array ['integer']);
select function_lang_is('camper', 'to_color', array ['integer'], 'sql');
select function_returns('camper', 'to_color', array ['integer'], 'color');
select isnt_definer('camper', 'to_color', array ['integer']);
select volatility_is('camper', 'to_color', array ['integer'], 'immutable');
select function_privs_are('camper', 'to_color', array ['integer'], 'guest', array['EXECUTE']);
select function_privs_are('camper', 'to_color', array ['integer'], 'employee', array['EXECUTE']);
select function_privs_are('camper', 'to_color', array ['integer'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'to_color', array ['integer'], 'authenticator', array['EXECUTE']);
select is( to_color(1122867), '#112233');
select is( to_color(0), '#000000');
select is( to_color(15), '#00000f');
select is( to_color(255), '#0000ff');
select is( to_color(4095), '#000fff');
select is( to_color(65535), '#00ffff');
select is( to_color(1048575), '#0fffff');
select is( to_color(16777215), '#ffffff');
select is( to_color(-1), '#ffffff');
select is( to_color(-559038737), '#deadbe');
select is( to_color(-2147483648), '#800000');
select *
from finish();
rollback;

40
test/to_integer.sql Normal file
View File

@ -0,0 +1,40 @@
-- Test to_integer
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(23);
select has_function('camper', 'to_integer', array ['color']);
select function_lang_is('camper', 'to_integer', array ['color'], 'sql');
select function_returns('camper', 'to_integer', array ['color'], 'integer');
select isnt_definer('camper', 'to_integer', array ['color']);
select volatility_is('camper', 'to_integer', array ['color'], 'immutable');
select function_privs_are('camper', 'to_integer', array ['color'], 'guest', array['EXECUTE']);
select function_privs_are('camper', 'to_integer', array ['color'], 'employee', array['EXECUTE']);
select function_privs_are('camper', 'to_integer', array ['color'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'to_integer', array ['color'], 'authenticator', array['EXECUTE']);
select is( to_integer('#112233'), 1122867 );
select is( to_integer('#000000'), 0 );
select is( to_integer('#00000f'), 15 );
select is( to_integer('#00000F'), 15 );
select is( to_integer('#0000ff'), 255 );
select is( to_integer('#0000FF'), 255 );
select is( to_integer('#000fff'), 4095 );
select is( to_integer('#000FFF'), 4095 );
select is( to_integer('#00ffff'), 65535 );
select is( to_integer('#00FFFF'), 65535 );
select is( to_integer('#0fffff'), 1048575 );
select is( to_integer('#0FFFFF'), 1048575 );
select is( to_integer('#ffffff'), 16777215 );
select is( to_integer('#FFFFFF'), 16777215 );
select *
from finish();
rollback;

7
verify/add_season.sql Normal file
View File

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

7
verify/color.sql Normal file
View File

@ -0,0 +1,7 @@
-- Verify camper:color on pg
begin;
select pg_catalog.has_type_privilege('camper.color', 'usage');
rollback;

7
verify/edit_season.sql Normal file
View File

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

20
verify/season.sql Normal file
View File

@ -0,0 +1,20 @@
-- Verify camper:season on pg
begin;
select season_id
, company_id
, slug
, name
, color
, active
from camper.season
where false;
select 1 / count(*) from pg_class where oid = 'camper.season'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.season'::regclass;
select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.season'::regclass;
select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.season'::regclass;
select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.season'::regclass;
rollback;

7
verify/to_color.sql Normal file
View File

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

7
verify/to_integer.sql Normal file
View File

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

View File

@ -69,6 +69,9 @@
<li> <li>
<a {{ if ne . "campsites"}}href="/admin/campsites"{{ end }}>{{( pgettext "Campsites" "title" )}}</a> <a {{ if ne . "campsites"}}href="/admin/campsites"{{ end }}>{{( pgettext "Campsites" "title" )}}</a>
</li> </li>
<li>
<a {{ if ne . "seasons"}}href="/admin/seasons"{{ end }}>{{( pgettext "Seasons" "title" )}}</a>
</li>
</ul> </ul>
</nav> </nav>
{{- end }} {{- end }}

View File

@ -0,0 +1,72 @@
<!--
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/season.seasonForm*/ -}}
{{ if .Slug}}
{{( pgettext "Edit Season" "title" )}}
{{ else }}
{{( pgettext "New Season" "title" )}}
{{ end }}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.seasonForm*/ -}}
{{ template "settings-tabs" "seasons" }}
<form
{{ if .Slug }}
data-hx-put="/admin/seasons/{{ .Slug }}"
{{ else }}
action="/admin/seasons" method="post"
{{ end }}
>
<h2>
{{ if .Slug }}
{{( pgettext "Edit Season" "title" )}}
{{ else }}
{{( pgettext "New Season" "title" )}}
{{ end }}
</h2>
{{ CSRFInput }}
<fieldset>
{{ if .Slug }}
{{ with .Active -}}
<label>
<input type="checkbox" name="{{ .Name }}" {{ if .Checked}}checked{{ end }}
{{ template "error-attrs" . }}>
{{( pgettext "Active" "season" )}}<br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ else }}
<input type="hidden" name="{{ .Active.Name }}" value="true">
{{ end }}
{{ with .Name -}}
<label>
{{( pgettext "Name" "input")}}<br>
<input type="text" name="{{ .Name }}" value="{{ .Val }}"
required {{ template "error-attrs" . }}><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .Color -}}
<label>
{{( pgettext "Color" "input")}}<br>
<input type="color" 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,40 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Seasons" "title" )}}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.seasonIndex*/ -}}
{{ template "settings-tabs" "seasons" }}
<a href="/admin/seasons/new">{{( pgettext "Add Season" "action" )}}</a>
<h2>{{( pgettext "Seasons" "title" )}}</h2>
{{ if .Seasons -}}
<table>
<thead>
<tr>
<th scope="col">{{( pgettext "Name" "header" )}}</th>
<th scope="col">{{( pgettext "Color" "header" )}}</th>
<th scope="col">{{( pgettext "Active" "season" )}}</th>
</tr>
</thead>
<tbody>
{{ range .Seasons -}}
<tr>
<td><a href="/admin/seasons/{{ .Slug }}">{{ .Name }}</a></td>
<td>
<svg width="20px" height="20px">
<circle cx="50%" cy="50%" r="50%" fill="{{ .Color }}"/>
</svg>
</td>
<td>{{ if .Active }}{{( gettext "Yes" )}}{{ else }}{{( gettext "No" )}}{{ end }}</td>
</tr>
{{- end }}
</tbody>
</table>
{{ else -}}
<p>{{( gettext "No seasons added yet." )}}</p>
{{- end }}
{{- end }}