411 lines
12 KiB
Go
411 lines
12 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"git.haelnorr.com/h/golib/hws"
|
|
"git.haelnorr.com/h/oslstats/internal/db"
|
|
"git.haelnorr.com/h/oslstats/internal/notify"
|
|
"git.haelnorr.com/h/oslstats/internal/respond"
|
|
"git.haelnorr.com/h/oslstats/internal/throw"
|
|
"git.haelnorr.com/h/oslstats/internal/validation"
|
|
"git.haelnorr.com/h/timefmt"
|
|
"github.com/pkg/errors"
|
|
"github.com/uptrace/bun"
|
|
)
|
|
|
|
// ProposeSeriesSchedule handles POST /series/{series_id}/schedule
|
|
func ProposeSeriesSchedule(
|
|
s *hws.Server,
|
|
conn *db.DB,
|
|
) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
seriesID, err := strconv.Atoi(r.PathValue("series_id"))
|
|
if err != nil {
|
|
throw.BadRequest(s, w, r, "Invalid series ID", err)
|
|
return
|
|
}
|
|
|
|
getter, ok := validation.ParseFormOrNotify(s, w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
format := timefmt.NewBuilder().Year4().Dash().MonthNumeric2().Dash().
|
|
DayNumeric2().T().Hour24().Colon().Minute().Build()
|
|
aest, _ := time.LoadLocation("Australia/Sydney")
|
|
scheduledTime := getter.TimeInLocation("scheduled_time", format, aest).Value
|
|
|
|
if !getter.ValidateAndNotify(s, w, r) {
|
|
return
|
|
}
|
|
|
|
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
|
series, err := db.GetPlayoffSeriesByID(ctx, tx, seriesID)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "db.GetPlayoffSeriesByID")
|
|
}
|
|
if series == nil {
|
|
respond.NotFound(w, errors.New("series not found"))
|
|
return false, nil
|
|
}
|
|
|
|
user := db.CurrentUser(ctx)
|
|
canSchedule, userTeamID, err := db.CanScheduleSeries(ctx, tx, series, user)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "db.CanScheduleSeries")
|
|
}
|
|
if !canSchedule {
|
|
throw.Forbidden(s, w, r, "You must be a team manager to propose a schedule", nil)
|
|
return false, nil
|
|
}
|
|
|
|
_, err = db.ProposeSeriesSchedule(ctx, tx, seriesID, userTeamID, scheduledTime, db.NewAuditFromRequest(r))
|
|
if err != nil {
|
|
if db.IsBadRequest(err) {
|
|
notify.Warn(s, w, r, "Cannot Propose", err.Error(), nil)
|
|
return false, nil
|
|
}
|
|
return false, errors.Wrap(err, "db.ProposeSeriesSchedule")
|
|
}
|
|
return true, nil
|
|
}); !ok {
|
|
return
|
|
}
|
|
|
|
notify.SuccessWithDelay(s, w, r, "Time Proposed", "Your proposed time has been submitted.", nil)
|
|
respond.HXRedirect(w, "/series/%d", seriesID)
|
|
})
|
|
}
|
|
|
|
// AcceptSeriesSchedule handles POST /series/{series_id}/schedule/{schedule_id}/accept
|
|
func AcceptSeriesSchedule(
|
|
s *hws.Server,
|
|
conn *db.DB,
|
|
) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
seriesID, err := strconv.Atoi(r.PathValue("series_id"))
|
|
if err != nil {
|
|
throw.BadRequest(s, w, r, "Invalid series ID", err)
|
|
return
|
|
}
|
|
scheduleID, err := strconv.Atoi(r.PathValue("schedule_id"))
|
|
if err != nil {
|
|
throw.BadRequest(s, w, r, "Invalid schedule ID", err)
|
|
return
|
|
}
|
|
|
|
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
|
series, err := db.GetPlayoffSeriesByID(ctx, tx, seriesID)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "db.GetPlayoffSeriesByID")
|
|
}
|
|
if series == nil {
|
|
respond.NotFound(w, errors.New("series not found"))
|
|
return false, nil
|
|
}
|
|
|
|
user := db.CurrentUser(ctx)
|
|
canSchedule, userTeamID, err := db.CanScheduleSeries(ctx, tx, series, user)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "db.CanScheduleSeries")
|
|
}
|
|
if !canSchedule {
|
|
throw.Forbidden(s, w, r, "You must be a team manager to accept a schedule", nil)
|
|
return false, nil
|
|
}
|
|
|
|
err = db.AcceptSeriesSchedule(ctx, tx, scheduleID, userTeamID, db.NewAuditFromRequest(r))
|
|
if err != nil {
|
|
if db.IsBadRequest(err) {
|
|
notify.Warn(s, w, r, "Cannot Accept", err.Error(), nil)
|
|
return false, nil
|
|
}
|
|
return false, errors.Wrap(err, "db.AcceptSeriesSchedule")
|
|
}
|
|
return true, nil
|
|
}); !ok {
|
|
return
|
|
}
|
|
|
|
notify.Success(s, w, r, "Schedule Accepted", "The series time has been confirmed.", nil)
|
|
respond.HXRedirect(w, "/series/%d", seriesID)
|
|
})
|
|
}
|
|
|
|
// RejectSeriesSchedule handles POST /series/{series_id}/schedule/{schedule_id}/reject
|
|
func RejectSeriesSchedule(
|
|
s *hws.Server,
|
|
conn *db.DB,
|
|
) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
seriesID, err := strconv.Atoi(r.PathValue("series_id"))
|
|
if err != nil {
|
|
throw.BadRequest(s, w, r, "Invalid series ID", err)
|
|
return
|
|
}
|
|
scheduleID, err := strconv.Atoi(r.PathValue("schedule_id"))
|
|
if err != nil {
|
|
throw.BadRequest(s, w, r, "Invalid schedule ID", err)
|
|
return
|
|
}
|
|
|
|
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
|
series, err := db.GetPlayoffSeriesByID(ctx, tx, seriesID)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "db.GetPlayoffSeriesByID")
|
|
}
|
|
if series == nil {
|
|
respond.NotFound(w, errors.New("series not found"))
|
|
return false, nil
|
|
}
|
|
|
|
user := db.CurrentUser(ctx)
|
|
canSchedule, _, err := db.CanScheduleSeries(ctx, tx, series, user)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "db.CanScheduleSeries")
|
|
}
|
|
if !canSchedule {
|
|
throw.Forbidden(s, w, r, "You must be a team manager to reject a schedule", nil)
|
|
return false, nil
|
|
}
|
|
|
|
err = db.RejectSeriesSchedule(ctx, tx, scheduleID, db.NewAuditFromRequest(r))
|
|
if err != nil {
|
|
if db.IsBadRequest(err) {
|
|
notify.Warn(s, w, r, "Cannot Reject", err.Error(), nil)
|
|
return false, nil
|
|
}
|
|
return false, errors.Wrap(err, "db.RejectSeriesSchedule")
|
|
}
|
|
return true, nil
|
|
}); !ok {
|
|
return
|
|
}
|
|
|
|
notify.Success(s, w, r, "Schedule Rejected", "The proposed time has been rejected.", nil)
|
|
respond.HXRedirect(w, "/series/%d", seriesID)
|
|
})
|
|
}
|
|
|
|
// PostponeSeriesSchedule handles POST /series/{series_id}/schedule/postpone
|
|
func PostponeSeriesSchedule(
|
|
s *hws.Server,
|
|
conn *db.DB,
|
|
) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
seriesID, err := strconv.Atoi(r.PathValue("series_id"))
|
|
if err != nil {
|
|
throw.BadRequest(s, w, r, "Invalid series ID", err)
|
|
return
|
|
}
|
|
|
|
getter, ok := validation.ParseFormOrNotify(s, w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
reason := getter.String("reschedule_reason").TrimSpace().Required().Value
|
|
if !getter.ValidateAndNotify(s, w, r) {
|
|
return
|
|
}
|
|
|
|
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
|
series, err := db.GetPlayoffSeriesByID(ctx, tx, seriesID)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "db.GetPlayoffSeriesByID")
|
|
}
|
|
if series == nil {
|
|
respond.NotFound(w, errors.New("series not found"))
|
|
return false, nil
|
|
}
|
|
|
|
user := db.CurrentUser(ctx)
|
|
canSchedule, _, err := db.CanScheduleSeries(ctx, tx, series, user)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "db.CanScheduleSeries")
|
|
}
|
|
if !canSchedule {
|
|
throw.Forbidden(s, w, r, "You must be a team manager to postpone a series", nil)
|
|
return false, nil
|
|
}
|
|
|
|
err = db.PostponeSeriesSchedule(ctx, tx, seriesID, reason, db.NewAuditFromRequest(r))
|
|
if err != nil {
|
|
if db.IsBadRequest(err) {
|
|
notify.Warn(s, w, r, "Cannot Postpone", err.Error(), nil)
|
|
return false, nil
|
|
}
|
|
return false, errors.Wrap(err, "db.PostponeSeriesSchedule")
|
|
}
|
|
return true, nil
|
|
}); !ok {
|
|
return
|
|
}
|
|
|
|
notify.Success(s, w, r, "Series Postponed", "The series has been postponed.", nil)
|
|
respond.HXRedirect(w, "/series/%d", seriesID)
|
|
})
|
|
}
|
|
|
|
// RescheduleSeriesHandler handles POST /series/{series_id}/schedule/reschedule
|
|
func RescheduleSeriesHandler(
|
|
s *hws.Server,
|
|
conn *db.DB,
|
|
) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
seriesID, err := strconv.Atoi(r.PathValue("series_id"))
|
|
if err != nil {
|
|
throw.BadRequest(s, w, r, "Invalid series ID", err)
|
|
return
|
|
}
|
|
|
|
getter, ok := validation.ParseFormOrNotify(s, w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
format := timefmt.NewBuilder().Year4().Dash().MonthNumeric2().Dash().
|
|
DayNumeric2().T().Hour24().Colon().Minute().Build()
|
|
aest, _ := time.LoadLocation("Australia/Sydney")
|
|
scheduledTime := getter.TimeInLocation("scheduled_time", format, aest).After(time.Now()).Value
|
|
reason := getter.String("reschedule_reason").TrimSpace().Required().Value
|
|
|
|
if !getter.ValidateAndNotify(s, w, r) {
|
|
return
|
|
}
|
|
|
|
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
|
series, err := db.GetPlayoffSeriesByID(ctx, tx, seriesID)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "db.GetPlayoffSeriesByID")
|
|
}
|
|
if series == nil {
|
|
respond.NotFound(w, errors.New("series not found"))
|
|
return false, nil
|
|
}
|
|
|
|
user := db.CurrentUser(ctx)
|
|
canSchedule, userTeamID, err := db.CanScheduleSeries(ctx, tx, series, user)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "db.CanScheduleSeries")
|
|
}
|
|
if !canSchedule {
|
|
throw.Forbidden(s, w, r, "You must be a team manager to reschedule a series", nil)
|
|
return false, nil
|
|
}
|
|
|
|
_, err = db.RescheduleSeriesSchedule(ctx, tx, seriesID, userTeamID, scheduledTime, reason, db.NewAuditFromRequest(r))
|
|
if err != nil {
|
|
if db.IsBadRequest(err) {
|
|
notify.Warn(s, w, r, "Cannot Reschedule", err.Error(), nil)
|
|
return false, nil
|
|
}
|
|
return false, errors.Wrap(err, "db.RescheduleSeriesSchedule")
|
|
}
|
|
return true, nil
|
|
}); !ok {
|
|
return
|
|
}
|
|
|
|
notify.Success(s, w, r, "Series Rescheduled", "The new proposed time has been submitted.", nil)
|
|
respond.HXRedirect(w, "/series/%d", seriesID)
|
|
})
|
|
}
|
|
|
|
// WithdrawSeriesScheduleHandler handles POST /series/{series_id}/schedule/{schedule_id}/withdraw
|
|
func WithdrawSeriesScheduleHandler(
|
|
s *hws.Server,
|
|
conn *db.DB,
|
|
) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
seriesID, err := strconv.Atoi(r.PathValue("series_id"))
|
|
if err != nil {
|
|
throw.BadRequest(s, w, r, "Invalid series ID", err)
|
|
return
|
|
}
|
|
scheduleID, err := strconv.Atoi(r.PathValue("schedule_id"))
|
|
if err != nil {
|
|
throw.BadRequest(s, w, r, "Invalid schedule ID", err)
|
|
return
|
|
}
|
|
|
|
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
|
series, err := db.GetPlayoffSeriesByID(ctx, tx, seriesID)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "db.GetPlayoffSeriesByID")
|
|
}
|
|
if series == nil {
|
|
respond.NotFound(w, errors.New("series not found"))
|
|
return false, nil
|
|
}
|
|
|
|
user := db.CurrentUser(ctx)
|
|
canSchedule, userTeamID, err := db.CanScheduleSeries(ctx, tx, series, user)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "db.CanScheduleSeries")
|
|
}
|
|
if !canSchedule {
|
|
throw.Forbidden(s, w, r, "You must be a team manager to withdraw a proposal", nil)
|
|
return false, nil
|
|
}
|
|
|
|
err = db.WithdrawSeriesSchedule(ctx, tx, scheduleID, userTeamID, db.NewAuditFromRequest(r))
|
|
if err != nil {
|
|
if db.IsBadRequest(err) {
|
|
notify.Warn(s, w, r, "Cannot Withdraw", err.Error(), nil)
|
|
return false, nil
|
|
}
|
|
return false, errors.Wrap(err, "db.WithdrawSeriesSchedule")
|
|
}
|
|
return true, nil
|
|
}); !ok {
|
|
return
|
|
}
|
|
|
|
notify.Success(s, w, r, "Proposal Withdrawn", "Your proposed time has been withdrawn.", nil)
|
|
respond.HXRedirect(w, "/series/%d", seriesID)
|
|
})
|
|
}
|
|
|
|
// CancelSeriesScheduleHandler handles POST /series/{series_id}/schedule/cancel
|
|
// This is a moderator-only action that requires playoffs.manage permission.
|
|
func CancelSeriesScheduleHandler(
|
|
s *hws.Server,
|
|
conn *db.DB,
|
|
) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
seriesID, err := strconv.Atoi(r.PathValue("series_id"))
|
|
if err != nil {
|
|
throw.BadRequest(s, w, r, "Invalid series ID", err)
|
|
return
|
|
}
|
|
|
|
getter, ok := validation.ParseFormOrNotify(s, w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
reason := getter.String("reschedule_reason").TrimSpace().Required().Value
|
|
if !getter.ValidateAndNotify(s, w, r) {
|
|
return
|
|
}
|
|
|
|
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
|
err := db.CancelSeriesSchedule(ctx, tx, seriesID, reason, db.NewAuditFromRequest(r))
|
|
if err != nil {
|
|
if db.IsBadRequest(err) {
|
|
notify.Warn(s, w, r, "Cannot Cancel", err.Error(), nil)
|
|
return false, nil
|
|
}
|
|
return false, errors.Wrap(err, "db.CancelSeriesSchedule")
|
|
}
|
|
return true, nil
|
|
}); !ok {
|
|
return
|
|
}
|
|
|
|
notify.Success(s, w, r, "Schedule Cancelled", "The series schedule has been cancelled.", nil)
|
|
respond.HXRedirect(w, "/series/%d", seriesID)
|
|
})
|
|
}
|