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.
2023-08-16 18:15:57 +00:00
|
|
|
/*
|
|
|
|
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
|
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
*/
|
|
|
|
|
|
|
|
package season
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"net/http"
|
2023-09-27 00:23:09 +00:00
|
|
|
"time"
|
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.
2023-08-16 18:15:57 +00:00
|
|
|
|
|
|
|
"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) {
|
2023-09-27 00:23:09 +00:00
|
|
|
seasons, err := collectSeasonEntries(r.Context(), company, conn)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
calendar, err := collectSeasonCalendar(r.Context(), company, conn, 2023)
|
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.
2023-08-16 18:15:57 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
page := &seasonIndex{
|
2023-09-27 00:23:09 +00:00
|
|
|
Seasons: seasons,
|
|
|
|
Calendar: calendar,
|
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.
2023-08-16 18:15:57 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-09-27 00:23:09 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
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.
2023-08-16 18:15:57 +00:00
|
|
|
type seasonIndex struct {
|
2023-09-27 00:23:09 +00:00
|
|
|
Seasons []*seasonEntry
|
|
|
|
Calendar seasonCalendar
|
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.
2023-08-16 18:15:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|