aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/admin/handler.go
diff options
context:
space:
mode:
Diffstat (limited to 'backend/admin/handler.go')
-rw-r--r--backend/admin/handler.go287
1 files changed, 287 insertions, 0 deletions
diff --git a/backend/admin/handler.go b/backend/admin/handler.go
index 6ca0a3f..6e981bc 100644
--- a/backend/admin/handler.go
+++ b/backend/admin/handler.go
@@ -79,6 +79,12 @@ func (h *Handler) RegisterHandlers(g *echo.Group) {
g.GET("/problems/:problemID/testcases/:testcaseID", h.getTestcaseEdit)
g.POST("/problems/:problemID/testcases/:testcaseID", h.postTestcaseEdit)
g.POST("/problems/:problemID/testcases/:testcaseID/delete", h.postTestcaseDelete)
+
+ g.GET("/tournaments", h.getTournaments)
+ g.GET("/tournaments/new", h.getTournamentNew)
+ g.POST("/tournaments/new", h.postTournamentNew)
+ g.GET("/tournaments/:tournamentID", h.getTournamentEdit)
+ g.POST("/tournaments/:tournamentID", h.postTournamentEdit)
}
func (h *Handler) getDashboard(c echo.Context) error {
@@ -951,3 +957,284 @@ func (h *Handler) postTestcaseDelete(c echo.Context) error {
return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/problems/"+strconv.Itoa(problemID)+"/testcases")
}
+
+func (h *Handler) getTournaments(c echo.Context) error {
+ rows, err := h.q.ListTournaments(c.Request().Context())
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+ tournaments := make([]echo.Map, len(rows))
+ for i, t := range rows {
+ tournaments[i] = echo.Map{
+ "TournamentID": t.TournamentID,
+ "DisplayName": t.DisplayName,
+ "BracketSize": t.BracketSize,
+ }
+ }
+
+ return c.Render(http.StatusOK, "tournaments", echo.Map{
+ "BasePath": h.conf.BasePath,
+ "Title": "Tournaments",
+ "Tournaments": tournaments,
+ })
+}
+
+func (h *Handler) getTournamentNew(c echo.Context) error {
+ return c.Render(http.StatusOK, "tournament_new", echo.Map{
+ "BasePath": h.conf.BasePath,
+ "Title": "New Tournament",
+ })
+}
+
+func nextPowerOf2(n int) int {
+ p := 1
+ for p < n {
+ p *= 2
+ }
+ return p
+}
+
+func log2Int(n int) int {
+ r := 0
+ for n > 1 {
+ n /= 2
+ r++
+ }
+ return r
+}
+
+func (h *Handler) postTournamentNew(c echo.Context) error {
+ displayName := c.FormValue("display_name")
+ numParticipants, err := strconv.Atoi(c.FormValue("num_participants"))
+ if err != nil || numParticipants < 2 {
+ return echo.NewHTTPError(http.StatusBadRequest, "Invalid num_participants")
+ }
+
+ bracketSize := nextPowerOf2(numParticipants)
+ numRounds := log2Int(bracketSize)
+
+ ctx := c.Request().Context()
+ err = h.txm.RunInTx(ctx, func(qtx db.Querier) error {
+ tournamentID, err := qtx.CreateTournament(ctx, db.CreateTournamentParams{
+ DisplayName: displayName,
+ BracketSize: int32(bracketSize),
+ NumRounds: int32(numRounds),
+ })
+ if err != nil {
+ return err
+ }
+ // Create match slots for all rounds
+ for round := 0; round < numRounds; round++ {
+ numPositions := bracketSize / (1 << (round + 1))
+ for pos := 0; pos < numPositions; pos++ {
+ if err := qtx.CreateTournamentMatch(ctx, db.CreateTournamentMatchParams{
+ TournamentID: tournamentID,
+ Round: int32(round),
+ Position: int32(pos),
+ }); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+
+ return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/tournaments")
+}
+
+func (h *Handler) getTournamentEdit(c echo.Context) error {
+ tournamentID, err := strconv.Atoi(c.Param("tournamentID"))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, "Invalid tournament id")
+ }
+ ctx := c.Request().Context()
+
+ tournament, err := h.q.GetTournamentByID(ctx, int32(tournamentID))
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return echo.NewHTTPError(http.StatusNotFound)
+ }
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+
+ entryRows, err := h.q.ListTournamentEntries(ctx, int32(tournamentID))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+ seedUserMap := make(map[int]int)
+ for _, e := range entryRows {
+ seedUserMap[int(e.Seed)] = int(e.UserID)
+ }
+
+ matchRows, err := h.q.ListTournamentMatches(ctx, int32(tournamentID))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+ matchGameMap := make(map[int]int)
+ var matches []echo.Map
+ for _, m := range matchRows {
+ gameID := 0
+ if m.GameID != nil {
+ gameID = int(*m.GameID)
+ }
+ matchGameMap[int(m.TournamentMatchID)] = gameID
+ matches = append(matches, echo.Map{
+ "MatchID": int(m.TournamentMatchID),
+ "Round": int(m.Round),
+ "Position": int(m.Position),
+ "Description": "R" + strconv.Itoa(int(m.Round)) + "P" + strconv.Itoa(int(m.Position)),
+ "IsBye": false,
+ })
+ }
+
+ userRows, err := h.q.ListUsers(ctx)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+ var users []echo.Map
+ for _, r := range userRows {
+ users = append(users, echo.Map{
+ "UserID": int(r.UserID),
+ "Username": r.Username,
+ })
+ }
+
+ gameRows, err := h.q.ListAllGames(ctx)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+ var games []echo.Map
+ for _, g := range gameRows {
+ games = append(games, echo.Map{
+ "GameID": int(g.GameID),
+ "DisplayName": g.DisplayName,
+ })
+ }
+
+ seeds := make([]int, tournament.BracketSize)
+ for i := range seeds {
+ seeds[i] = i + 1
+ }
+
+ return c.Render(http.StatusOK, "tournament_edit", echo.Map{
+ "BasePath": h.conf.BasePath,
+ "Title": "Tournament Edit",
+ "Tournament": echo.Map{
+ "TournamentID": tournament.TournamentID,
+ "DisplayName": tournament.DisplayName,
+ "BracketSize": tournament.BracketSize,
+ "NumRounds": tournament.NumRounds,
+ },
+ "Seeds": seeds,
+ "SeedUserMap": seedUserMap,
+ "Matches": matches,
+ "MatchGameMap": matchGameMap,
+ "Users": users,
+ "Games": games,
+ })
+}
+
+func (h *Handler) postTournamentEdit(c echo.Context) error {
+ tournamentID, err := strconv.Atoi(c.Param("tournamentID"))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, "Invalid tournament id")
+ }
+ ctx := c.Request().Context()
+
+ tournament, err := h.q.GetTournamentByID(ctx, int32(tournamentID))
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return echo.NewHTTPError(http.StatusNotFound)
+ }
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+
+ displayName := c.FormValue("display_name")
+
+ // Parse seed → user assignments
+ type seedEntry struct {
+ seed int
+ userID int
+ }
+ var seedEntries []seedEntry
+ for seed := 1; seed <= int(tournament.BracketSize); seed++ {
+ raw := c.FormValue("seed_" + strconv.Itoa(seed))
+ if raw == "" || raw == "0" {
+ continue
+ }
+ userID, err := strconv.Atoi(raw)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, "Invalid seed_"+strconv.Itoa(seed))
+ }
+ seedEntries = append(seedEntries, seedEntry{seed: seed, userID: userID})
+ }
+
+ // Parse match → game assignments
+ matchRows, err := h.q.ListTournamentMatches(ctx, int32(tournamentID))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+ type matchGame struct {
+ matchID int
+ gameID *int32
+ }
+ var matchGames []matchGame
+ for _, m := range matchRows {
+ raw := c.FormValue("match_" + strconv.Itoa(int(m.TournamentMatchID)))
+ var gameID *int32
+ if raw != "" && raw != "0" {
+ gid, err := strconv.Atoi(raw)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, "Invalid match game")
+ }
+ gid32 := int32(gid)
+ gameID = &gid32
+ }
+ matchGames = append(matchGames, matchGame{matchID: int(m.TournamentMatchID), gameID: gameID})
+ }
+
+ err = h.txm.RunInTx(ctx, func(qtx db.Querier) error {
+ if err := qtx.UpdateTournament(ctx, db.UpdateTournamentParams{
+ TournamentID: int32(tournamentID),
+ DisplayName: displayName,
+ BracketSize: tournament.BracketSize,
+ NumRounds: tournament.NumRounds,
+ }); err != nil {
+ return err
+ }
+
+ // Replace entries
+ if err := qtx.DeleteTournamentEntries(ctx, int32(tournamentID)); err != nil {
+ return err
+ }
+ for _, se := range seedEntries {
+ if err := qtx.CreateTournamentEntry(ctx, db.CreateTournamentEntryParams{
+ TournamentID: int32(tournamentID),
+ UserID: int32(se.userID),
+ Seed: int32(se.seed),
+ }); err != nil {
+ return err
+ }
+ }
+
+ // Update match game assignments
+ for _, mg := range matchGames {
+ if err := qtx.UpdateTournamentMatchGame(ctx, db.UpdateTournamentMatchGameParams{
+ TournamentMatchID: int32(mg.matchID),
+ GameID: mg.gameID,
+ }); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ })
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+
+ return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/tournaments")
+}