diff options
| -rw-r--r-- | backend/admin/handler.go | 122 | ||||
| -rw-r--r-- | backend/admin/templates/dashboard.html | 3 | ||||
| -rw-r--r-- | backend/admin/templates/problem_edit.html | 36 | ||||
| -rw-r--r-- | backend/admin/templates/problem_new.html | 32 | ||||
| -rw-r--r-- | backend/admin/templates/problems.html | 20 | ||||
| -rw-r--r-- | backend/api/handler.go | 6 | ||||
| -rw-r--r-- | backend/db/models.go | 2 | ||||
| -rw-r--r-- | backend/db/query.sql.go | 108 | ||||
| -rw-r--r-- | backend/query.sql | 23 |
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; |
