aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-09-06 00:32:03 +0900
committernsfisis <nsfisis@gmail.com>2025-09-06 00:32:03 +0900
commite33bfff4db95586a3140b5e71a7d3dba2c72f694 (patch)
treee469dee99215bb888c3dd3f30d0c3cfba0f669d0
parent94d03dc712aff58e7594e7af38e0b6881238c778 (diff)
downloadiosdc-japan-2025-albatross-e33bfff4db95586a3140b5e71a7d3dba2c72f694.tar.gz
iosdc-japan-2025-albatross-e33bfff4db95586a3140b5e71a7d3dba2c72f694.tar.zst
iosdc-japan-2025-albatross-e33bfff4db95586a3140b5e71a7d3dba2c72f694.zip
feat(backend): add admin page for problems
-rw-r--r--backend/admin/handler.go122
-rw-r--r--backend/admin/templates/dashboard.html3
-rw-r--r--backend/admin/templates/problem_edit.html36
-rw-r--r--backend/admin/templates/problem_new.html32
-rw-r--r--backend/admin/templates/problems.html20
-rw-r--r--backend/api/handler.go6
-rw-r--r--backend/db/models.go2
-rw-r--r--backend/db/query.sql.go108
-rw-r--r--backend/query.sql23
9 files changed, 320 insertions, 32 deletions
diff --git a/backend/admin/handler.go b/backend/admin/handler.go
index aec7b37..5a0f0a1 100644
--- a/backend/admin/handler.go
+++ b/backend/admin/handler.go
@@ -53,16 +53,24 @@ func (h *Handler) RegisterHandlers(g *echo.Group) {
g.Use(h.newAdminMiddleware())
g.GET("/dashboard", h.getDashboard)
+
+ g.GET("/online-qualifying-ranking", h.getOnlineQualifyingRanking)
+
g.GET("/users", h.getUsers)
g.GET("/users/:userID", h.getUserEdit)
g.POST("/users/:userID", h.postUserEdit)
g.POST("/users/:userID/fetch-icon", h.postUserFetchIcon)
+
g.GET("/games", h.getGames)
g.GET("/games/:gameID", h.getGameEdit)
g.POST("/games/:gameID", h.postGameEdit)
g.POST("/games/:gameID/start", h.postGameStart)
- g.GET("/online-qualifying-ranking", h.getOnlineQualifyingRanking)
- g.POST("/fix", h.postFix)
+
+ g.GET("/problems", h.getProblems)
+ g.GET("/problems/new", h.getProblemNew)
+ g.POST("/problems/new", h.postProblemNew)
+ g.GET("/problems/:problemID", h.getProblemEdit)
+ g.POST("/problems/:problemID", h.postProblemEdit)
}
func (h *Handler) getDashboard(c echo.Context) error {
@@ -428,39 +436,101 @@ func (h *Handler) getOnlineQualifyingRanking(c echo.Context) error {
})
}
-func (h *Handler) postFix(c echo.Context) error {
- rows, err := h.q.ListSubmissionIDs(c.Request().Context())
+func (h *Handler) getProblems(c echo.Context) error {
+ rows, err := h.q.ListProblems(c.Request().Context())
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
- for _, submissionID := range rows {
- as, err := h.q.AggregateTestcaseResults(c.Request().Context(), submissionID)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
- }
- err = h.q.UpdateSubmissionStatus(c.Request().Context(), db.UpdateSubmissionStatusParams{
- SubmissionID: submissionID,
- Status: as,
- })
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ problems := make([]echo.Map, len(rows))
+ for i, p := range rows {
+ problems[i] = echo.Map{
+ "ProblemID": p.ProblemID,
+ "Title": p.Title,
+ "Description": p.Description,
+ "Language": p.Language,
}
}
- rows2, err := h.q.ListGameStateIDs(c.Request().Context())
+ return c.Render(http.StatusOK, "problems", echo.Map{
+ "BasePath": h.conf.BasePath,
+ "Title": "Problems",
+ "Problems": problems,
+ })
+}
+
+func (h *Handler) getProblemNew(c echo.Context) error {
+ return c.Render(http.StatusOK, "problem_new", echo.Map{
+ "BasePath": h.conf.BasePath,
+ "Title": "New Problem",
+ })
+}
+
+func (h *Handler) postProblemNew(c echo.Context) error {
+ title := c.FormValue("title")
+ description := c.FormValue("description")
+ language := c.FormValue("language")
+ sampleCode := c.FormValue("sample_code")
+
+ _, err := h.q.CreateProblem(c.Request().Context(), db.CreateProblemParams{
+ Title: title,
+ Description: description,
+ Language: language,
+ SampleCode: sampleCode,
+ })
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
- for _, r := range rows2 {
- gameID := r.GameID
- userID := r.UserID
- err := h.q.SyncGameStateBestScoreSubmission(c.Request().Context(), db.SyncGameStateBestScoreSubmissionParams{
- GameID: gameID,
- UserID: userID,
- })
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+
+ return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/problems")
+}
+
+func (h *Handler) getProblemEdit(c echo.Context) error {
+ problemID, err := strconv.Atoi(c.Param("problemID"))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, "Invalid problem id")
+ }
+ row, err := h.q.GetProblemByID(c.Request().Context(), int32(problemID))
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return echo.NewHTTPError(http.StatusNotFound)
}
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+
+ return c.Render(http.StatusOK, "problem_edit", echo.Map{
+ "BasePath": h.conf.BasePath,
+ "Title": "Problem Edit",
+ "Problem": echo.Map{
+ "ProblemID": row.ProblemID,
+ "Title": row.Title,
+ "Description": row.Description,
+ "Language": row.Language,
+ "SampleCode": row.SampleCode,
+ },
+ })
+}
+
+func (h *Handler) postProblemEdit(c echo.Context) error {
+ problemID, err := strconv.Atoi(c.Param("problemID"))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, "Invalid problem_id")
+ }
+
+ title := c.FormValue("title")
+ description := c.FormValue("description")
+ language := c.FormValue("language")
+ sampleCode := c.FormValue("sample_code")
+
+ err = h.q.UpdateProblem(c.Request().Context(), db.UpdateProblemParams{
+ ProblemID: int32(problemID),
+ Title: title,
+ Description: description,
+ Language: language,
+ SampleCode: sampleCode,
+ })
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
- return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/dashboard")
+
+ return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/problems")
}
diff --git a/backend/admin/templates/dashboard.html b/backend/admin/templates/dashboard.html
index dcc71ba..1ea40c9 100644
--- a/backend/admin/templates/dashboard.html
+++ b/backend/admin/templates/dashboard.html
@@ -8,6 +8,9 @@
<a href="{{ .BasePath }}admin/games">Games</a>
</p>
<p>
+ <a href="{{ .BasePath }}admin/problems">Problems</a>
+</p>
+<p>
<a href="{{ .BasePath }}admin/online-qualifying-ranking?game_1=7&game_2=8">Online Qualifying Ranking</a>
</p>
<form method="post" action="{{ .BasePath }}admin/fix">
diff --git a/backend/admin/templates/problem_edit.html b/backend/admin/templates/problem_edit.html
new file mode 100644
index 0000000..cc700f4
--- /dev/null
+++ b/backend/admin/templates/problem_edit.html
@@ -0,0 +1,36 @@
+{{ template "base.html" . }}
+
+{{ define "breadcrumb" }}
+<a href="{{ .BasePath }}admin/dashboard">Dashboard</a> | <a href="{{ .BasePath }}admin/problems">Problems</a>
+{{ end }}
+
+{{ define "content" }}
+<form method="post">
+ <div>
+ <label>Problem ID</label>
+ <input type="text" name="problem_id" value="{{ .Problem.ProblemID }}" readonly required>
+ </div>
+ <div>
+ <label>Title</label>
+ <input type="text" name="title" value="{{ .Problem.Title }}" required>
+ </div>
+ <div>
+ <label>Description</label>
+ <textarea name="description" rows="10" required>{{ .Problem.Description }}</textarea>
+ </div>
+ <div>
+ <label>Language</label>
+ <select name="language" required>
+ <option value="php"{{ if eq .Problem.Language "php" }} selected{{ end }}>PHP</option>
+ <option value="swift"{{ if eq .Problem.Language "swift" }} selected{{ end }}>Swift</option>
+ </select>
+ </div>
+ <div>
+ <label>Sample Code</label>
+ <textarea name="sample_code" rows="15" required>{{ .Problem.SampleCode }}</textarea>
+ </div>
+ <div>
+ <button type="submit">Save</button>
+ </div>
+</form>
+{{ end }}
diff --git a/backend/admin/templates/problem_new.html b/backend/admin/templates/problem_new.html
new file mode 100644
index 0000000..ed8ad2a
--- /dev/null
+++ b/backend/admin/templates/problem_new.html
@@ -0,0 +1,32 @@
+{{ template "base.html" . }}
+
+{{ define "breadcrumb" }}
+<a href="{{ .BasePath }}admin/dashboard">Dashboard</a> | <a href="{{ .BasePath }}admin/problems">Problems</a>
+{{ end }}
+
+{{ define "content" }}
+<form method="post">
+ <div>
+ <label>Title</label>
+ <input type="text" name="title" required>
+ </div>
+ <div>
+ <label>Description</label>
+ <textarea name="description" rows="10" required></textarea>
+ </div>
+ <div>
+ <label>Language</label>
+ <select name="language" required>
+ <option value="php">PHP</option>
+ <option value="swift">Swift</option>
+ </select>
+ </div>
+ <div>
+ <label>Sample Code</label>
+ <textarea name="sample_code" rows="15" required></textarea>
+ </div>
+ <div>
+ <button type="submit">Create</button>
+ </div>
+</form>
+{{ end }}
diff --git a/backend/admin/templates/problems.html b/backend/admin/templates/problems.html
new file mode 100644
index 0000000..120789e
--- /dev/null
+++ b/backend/admin/templates/problems.html
@@ -0,0 +1,20 @@
+{{ template "base.html" . }}
+
+{{ define "breadcrumb" }}
+<a href="{{ .BasePath }}admin/dashboard">Dashboard</a>
+{{ end }}
+
+{{ define "content" }}
+<div>
+ <a href="{{ .BasePath }}admin/problems/new">Create New Problem</a>
+</div>
+<ul>
+ {{ range .Problems }}
+ <li>
+ <a href="{{ $.BasePath }}admin/problems/{{ .ProblemID }}">
+ {{ .Title }} (id={{ .ProblemID }} language={{ .Language }})
+ </a>
+ </li>
+ {{ end }}
+</ul>
+{{ end }}
diff --git a/backend/api/handler.go b/backend/api/handler.go
index 4321d15..60dab6f 100644
--- a/backend/api/handler.go
+++ b/backend/api/handler.go
@@ -88,7 +88,7 @@ func (h *Handler) GetGames(ctx context.Context, _ GetGamesRequestObject, _ *auth
ProblemID: int(row.ProblemID),
Title: row.Title,
Description: row.Description,
- Language: ProblemLanguage(*row.Language),
+ Language: ProblemLanguage(row.Language),
SampleCode: row.SampleCode,
},
}
@@ -167,7 +167,7 @@ func (h *Handler) GetGame(ctx context.Context, request GetGameRequestObject, _ *
ProblemID: int(row.ProblemID),
Title: row.Title,
Description: row.Description,
- Language: ProblemLanguage(*row.Language),
+ Language: ProblemLanguage(row.Language),
SampleCode: row.SampleCode,
},
MainPlayers: mainPlayers,
@@ -305,7 +305,7 @@ func (h *Handler) PostGamePlaySubmit(ctx context.Context, request PostGamePlaySu
return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
- language := *gameRow.Language
+ language := gameRow.Language
codeSize := h.hub.CalcCodeSize(code, language)
// TODO: check if the game is running
// TODO: transaction
diff --git a/backend/db/models.go b/backend/db/models.go
index c7d649c..cc12942 100644
--- a/backend/db/models.go
+++ b/backend/db/models.go
@@ -36,7 +36,7 @@ type Problem struct {
ProblemID int32
Title string
Description string
- Language *string
+ Language string
SampleCode string
}
diff --git a/backend/db/query.sql.go b/backend/db/query.sql.go
index f70cd29..3f85f46 100644
--- a/backend/db/query.sql.go
+++ b/backend/db/query.sql.go
@@ -50,6 +50,31 @@ func (q *Queries) AggregateTestcaseResults(ctx context.Context, submissionID int
return status, err
}
+const createProblem = `-- name: CreateProblem :one
+INSERT INTO problems (title, description, language, sample_code)
+VALUES ($1, $2, $3, $4)
+RETURNING problem_id
+`
+
+type CreateProblemParams struct {
+ Title string
+ Description string
+ Language string
+ SampleCode string
+}
+
+func (q *Queries) CreateProblem(ctx context.Context, arg CreateProblemParams) (int32, error) {
+ row := q.db.QueryRow(ctx, createProblem,
+ arg.Title,
+ arg.Description,
+ arg.Language,
+ arg.SampleCode,
+ )
+ var problem_id int32
+ err := row.Scan(&problem_id)
+ return problem_id, err
+}
+
const createSubmission = `-- name: CreateSubmission :one
INSERT INTO submissions (game_id, user_id, code, code_size, status)
VALUES ($1, $2, $3, $4, 'running')
@@ -146,7 +171,7 @@ type GetGameByIDRow struct {
ProblemID_2 int32
Title string
Description string
- Language *string
+ Language string
SampleCode string
}
@@ -277,6 +302,25 @@ func (q *Queries) GetLatestStatesOfMainPlayers(ctx context.Context, gameID int32
return items, nil
}
+const getProblemByID = `-- name: GetProblemByID :one
+SELECT problem_id, title, description, language, sample_code FROM problems
+WHERE problem_id = $1
+LIMIT 1
+`
+
+func (q *Queries) GetProblemByID(ctx context.Context, problemID int32) (Problem, error) {
+ row := q.db.QueryRow(ctx, getProblemByID, problemID)
+ var i Problem
+ err := row.Scan(
+ &i.ProblemID,
+ &i.Title,
+ &i.Description,
+ &i.Language,
+ &i.SampleCode,
+ )
+ return i, err
+}
+
const getQualifyingRanking = `-- name: GetQualifyingRanking :many
SELECT
u.username AS username,
@@ -576,6 +620,37 @@ func (q *Queries) ListMainPlayers(ctx context.Context, dollar_1 []int32) ([]List
return items, nil
}
+const listProblems = `-- name: ListProblems :many
+SELECT problem_id, title, description, language, sample_code FROM problems
+ORDER BY problem_id
+`
+
+func (q *Queries) ListProblems(ctx context.Context) ([]Problem, error) {
+ rows, err := q.db.Query(ctx, listProblems)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []Problem
+ for rows.Next() {
+ var i Problem
+ if err := rows.Scan(
+ &i.ProblemID,
+ &i.Title,
+ &i.Description,
+ &i.Language,
+ &i.SampleCode,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
const listPublicGames = `-- name: ListPublicGames :many
SELECT game_id, game_type, is_public, display_name, duration_seconds, created_at, started_at, games.problem_id, problems.problem_id, title, description, language, sample_code FROM games
JOIN problems ON games.problem_id = problems.problem_id
@@ -595,7 +670,7 @@ type ListPublicGamesRow struct {
ProblemID_2 int32
Title string
Description string
- Language *string
+ Language string
SampleCode string
}
@@ -868,6 +943,35 @@ func (q *Queries) UpdateGameStateStatus(ctx context.Context, arg UpdateGameState
return err
}
+const updateProblem = `-- name: UpdateProblem :exec
+UPDATE problems
+SET
+ title = $2,
+ description = $3,
+ language = $4,
+ sample_code = $5
+WHERE problem_id = $1
+`
+
+type UpdateProblemParams struct {
+ ProblemID int32
+ Title string
+ Description string
+ Language string
+ SampleCode string
+}
+
+func (q *Queries) UpdateProblem(ctx context.Context, arg UpdateProblemParams) error {
+ _, err := q.db.Exec(ctx, updateProblem,
+ arg.ProblemID,
+ arg.Title,
+ arg.Description,
+ arg.Language,
+ arg.SampleCode,
+ )
+ return err
+}
+
const updateSubmissionStatus = `-- name: UpdateSubmissionStatus :exec
UPDATE submissions
SET status = $2
diff --git a/backend/query.sql b/backend/query.sql
index 4a06ee7..9f272b1 100644
--- a/backend/query.sql
+++ b/backend/query.sql
@@ -194,3 +194,26 @@ SELECT submission_id FROM submissions;
-- name: ListGameStateIDs :many
SELECT game_id, user_id FROM game_states;
+
+-- name: ListProblems :many
+SELECT * FROM problems
+ORDER BY problem_id;
+
+-- name: GetProblemByID :one
+SELECT * FROM problems
+WHERE problem_id = $1
+LIMIT 1;
+
+-- name: CreateProblem :one
+INSERT INTO problems (title, description, language, sample_code)
+VALUES ($1, $2, $3, $4)
+RETURNING problem_id;
+
+-- name: UpdateProblem :exec
+UPDATE problems
+SET
+ title = $2,
+ description = $3,
+ language = $4,
+ sample_code = $5
+WHERE problem_id = $1;