diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-20 23:32:22 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-20 23:32:22 +0900 |
| commit | 8e73d12a703e90ad908962143951178c13d0d6fe (patch) | |
| tree | 8bed43aa4b115f8bc50ed258aa192a94b6d2903e /backend | |
| parent | aa07ba2e0a40b0097a4f9aee3c06dcbd9a749105 (diff) | |
| download | phperkaigi-2026-albatross-8e73d12a703e90ad908962143951178c13d0d6fe.tar.gz phperkaigi-2026-albatross-8e73d12a703e90ad908962143951178c13d0d6fe.tar.zst phperkaigi-2026-albatross-8e73d12a703e90ad908962143951178c13d0d6fe.zip | |
feat: add user submission history page
Allow users to view their own past submissions (code, size, status,
timestamp) for each game. Adds API endpoint, backend handler, SQL query,
and frontend page with expandable code display.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'backend')
| -rw-r--r-- | backend/api/generated.go | 151 | ||||
| -rw-r--r-- | backend/api/handler.go | 38 | ||||
| -rw-r--r-- | backend/api/handler_test.go | 166 | ||||
| -rw-r--r-- | backend/api/handler_wrapper.go | 10 | ||||
| -rw-r--r-- | backend/db/querier.go | 1 | ||||
| -rw-r--r-- | backend/db/query.sql.go | 39 | ||||
| -rw-r--r-- | backend/query.sql | 5 |
7 files changed, 376 insertions, 34 deletions
diff --git a/backend/api/generated.go b/backend/api/generated.go index a419123..aee6f3e 100644 --- a/backend/api/generated.go +++ b/backend/api/generated.go @@ -97,6 +97,16 @@ type RankingEntry struct { SubmittedAt int64 `json:"submitted_at"` } +// Submission defines model for Submission. +type Submission struct { + Code string `json:"code"` + CodeSize int `json:"code_size"` + CreatedAt int64 `json:"created_at"` + GameID int `json:"game_id"` + Status ExecutionStatus `json:"status"` + SubmissionID int `json:"submission_id"` +} + // Tournament defines model for Tournament. type Tournament struct { BracketSize int `json:"bracket_size"` @@ -177,6 +187,9 @@ type ServerInterface interface { // (GET /games/{game_id}/play/latest_state) GetGamePlayLatestState(ctx echo.Context, gameID int) error + // (GET /games/{game_id}/play/submissions) + GetGamePlaySubmissions(ctx echo.Context, gameID int) error + // (POST /games/{game_id}/play/submit) PostGamePlaySubmit(ctx echo.Context, gameID int) error @@ -261,6 +274,22 @@ func (w *ServerInterfaceWrapper) GetGamePlayLatestState(ctx echo.Context) error return err } +// GetGamePlaySubmissions converts echo context to params. +func (w *ServerInterfaceWrapper) GetGamePlaySubmissions(ctx echo.Context) error { + var err error + // ------------- Path parameter "game_id" ------------- + var gameID int + + err = runtime.BindStyledParameterWithOptions("simple", "game_id", ctx.Param("game_id"), &gameID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter game_id: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetGamePlaySubmissions(ctx, gameID) + return err +} + // PostGamePlaySubmit converts echo context to params. func (w *ServerInterfaceWrapper) PostGamePlaySubmit(ctx echo.Context) error { var err error @@ -384,6 +413,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/games/:game_id", wrapper.GetGame) router.POST(baseURL+"/games/:game_id/play/code", wrapper.PostGamePlayCode) router.GET(baseURL+"/games/:game_id/play/latest_state", wrapper.GetGamePlayLatestState) + router.GET(baseURL+"/games/:game_id/play/submissions", wrapper.GetGamePlaySubmissions) router.POST(baseURL+"/games/:game_id/play/submit", wrapper.PostGamePlaySubmit) router.GET(baseURL+"/games/:game_id/watch/latest_states", wrapper.GetGameWatchLatestStates) router.GET(baseURL+"/games/:game_id/watch/ranking", wrapper.GetGameWatchRanking) @@ -566,6 +596,52 @@ func (response GetGamePlayLatestState404JSONResponse) VisitGetGamePlayLatestStat return json.NewEncoder(w).Encode(response) } +type GetGamePlaySubmissionsRequestObject struct { + GameID int `json:"game_id"` +} + +type GetGamePlaySubmissionsResponseObject interface { + VisitGetGamePlaySubmissionsResponse(w http.ResponseWriter) error +} + +type GetGamePlaySubmissions200JSONResponse struct { + Submissions []Submission `json:"submissions"` +} + +func (response GetGamePlaySubmissions200JSONResponse) VisitGetGamePlaySubmissionsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetGamePlaySubmissions401JSONResponse Error + +func (response GetGamePlaySubmissions401JSONResponse) VisitGetGamePlaySubmissionsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetGamePlaySubmissions403JSONResponse Error + +func (response GetGamePlaySubmissions403JSONResponse) VisitGetGamePlaySubmissionsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + +type GetGamePlaySubmissions404JSONResponse Error + +func (response GetGamePlaySubmissions404JSONResponse) VisitGetGamePlaySubmissionsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + type PostGamePlaySubmitRequestObject struct { GameID int `json:"game_id"` Body *PostGamePlaySubmitJSONRequestBody @@ -842,6 +918,9 @@ type StrictServerInterface interface { // (GET /games/{game_id}/play/latest_state) GetGamePlayLatestState(ctx context.Context, request GetGamePlayLatestStateRequestObject) (GetGamePlayLatestStateResponseObject, error) + // (GET /games/{game_id}/play/submissions) + GetGamePlaySubmissions(ctx context.Context, request GetGamePlaySubmissionsRequestObject) (GetGamePlaySubmissionsResponseObject, error) + // (POST /games/{game_id}/play/submit) PostGamePlaySubmit(ctx context.Context, request PostGamePlaySubmitRequestObject) (PostGamePlaySubmitResponseObject, error) @@ -980,6 +1059,31 @@ func (sh *strictHandler) GetGamePlayLatestState(ctx echo.Context, gameID int) er return nil } +// GetGamePlaySubmissions operation middleware +func (sh *strictHandler) GetGamePlaySubmissions(ctx echo.Context, gameID int) error { + var request GetGamePlaySubmissionsRequestObject + + request.GameID = gameID + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetGamePlaySubmissions(ctx.Request().Context(), request.(GetGamePlaySubmissionsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetGamePlaySubmissions") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(GetGamePlaySubmissionsResponseObject); ok { + return validResponse.VisitGetGamePlaySubmissionsResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // PostGamePlaySubmit operation middleware func (sh *strictHandler) PostGamePlaySubmit(ctx echo.Context, gameID int) error { var request PostGamePlaySubmitRequestObject @@ -1164,29 +1268,30 @@ func (sh *strictHandler) GetTournament(ctx echo.Context, tournamentID int) error // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xaTW/bOBP+KwLf96jaThPswbfsIigKpICxTbGHohAoaWKzlUgth4rrGv7vC5L6FiXL", - "SbqLpL454nA4H88zw4/sSSTSTHDgCslyTzDaQErNzxsphdQ/MikykIqB+ZwCIl2D/ql2GZAlQSUZX5PD", - "wScS/s6ZhJgsP1eCX/xSUIRfIVLk4JOb7xDlign+UVGVG73A81RP44ID8YnMOddafYJ5FAEi8clWCr4O", - "KMctSOITxVIQuSK+8YElEIAx2UzWg9XfjCuQnCbFh9qi0nSfvKMp9J2NGWYJ3QW8GO1Ni3NJtR8BQiR4", - "jA0hvegapJZa0xQCFo8M2s978n8J92RJ/jev0zIvcjLXJt5puYNPGAZZHiYsaugMhUiAcj2cUsYDbTlI", - "YxJTkOIx/Z/QGlSoo1LSnf47kyJMID02fVWIHXyCikoFcUCVw2WffH+zFm/qr79d9bBTBqwZnabTfjsz", - "jjzUZnei4YJjFdkGDi8eLvTUPFHMTnXC5pYqQKXnayQ7EBQCqgAjISHAPEyZquPC8yShYQJkqWQO/qQ4", - "aazHbiiaRSbotfkpWDeW0C5Ju0kylpTr+oOeVsu5Ir+qsdVhHmAkWaaXd3qbUL7Oizo0AZW3pXiN50FC", - "Ik2zBILBQCumkgn1r7FMOcdvudVwor3oSKBuG26XSM02mVawZffKidE/Kf/G+PqGK7nrB7p0cwA1tZqC", - "BROLSIVGR3w7PHhEfShsqcHXQdxgGO9ELnXJ4MpBVUmjb6ACZD8GLD/aDoArWWibVHJrc2x2HNU3pSra", - "PErlBz3TpZLnaSBFPtiuVKVigCOdbLTle7W5FdfW6nXAaj/HszYAYgQYIHOOUzHbccpM9K3mcZtsnHs2", - "jTZ9hkG4A3fztuC+mMq0QjwYYZwVeXuaxrejGgWyTm1ujJr0HsWWyfhghLaMc5CBTsPJKKw0l6Y0DK5i", - "70rqpwItJ+4CWSR4kFG1cY9iQOOUcXe2ExpCMqkAj4TCDg7Y5wC2jU01p0fZyuTSvn6wtFrG74VZ0HZE", - "cp2EVEmB6JU7bm8LoXe9ek988gASDWLIYnY5W2ijRQacZowsyeVsMVvoNFG1MTGfa/pYHoGp1TohZov3", - "PiZL8g7MpksXDgmYCY5W+O1iYZsaV0WNp1mWsMjMnH9Fi1gLeDdlp1dac2TolVfHThYHwtfa45C7DXh6", - "JqDyNhQ9c+6BGOKZXuRqcXGSY6MbO3MKcphwbU5aHkMv5zRXGyHZj2r9y39z/XshQxbHwGda7uAXeJjv", - "i6p6OIYMgyVJU1DmCPR5TzQBDb50DzJMaRwz6pRZ8tWO9KrOl2eH3DSgOYB1xtVTcKUXv/r5i+v4I8gH", - "kF5EORfKu2c89lSdFog9CShyGcEQ3Oe6Os/LvXom0IH8lbAn0VVCd3/Ys9nPpIAx/XcR756A/oEzluug", - "6YZ62+iDm5pnPrxSPiTm9iXA8uplrCFoVtjbGntT80L6Q+XbWKy7t1BdAlkl52bxS5HDXolMaxcfrey5", - "YZwbxmvlxJaqaNPqGEdPl3/pKY2egS+qaZhfNI7NtQdNVi2Jk7pJn0iO9nI+5P5qXJL2XWESi4o3iJdC", - "oIZrk66CWk8sx66ESuVnvrxWviRiba97h3det0bkubZFGUXcChk7759Puxrm5eVZofEJ26hHOvO0J5uX", - "zqoKQSJXRyFk//3lZW9hrcPp6AH+A5AzxJ454vV7Gc73rSfc0cv1xgv6lGbefRv+z1q6ar38T3tBH3lj", - "PHfv19m9D4d/AgAA///ukgFcEykAAA==", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/backend/api/handler.go b/backend/api/handler.go index 8d0d9be..57ae973 100644 --- a/backend/api/handler.go +++ b/backend/api/handler.go @@ -376,6 +376,44 @@ func (h *Handler) GetGameWatchRanking(ctx context.Context, request GetGameWatchR }, nil } +func (h *Handler) GetGamePlaySubmissions(ctx context.Context, request GetGamePlaySubmissionsRequestObject, user *db.User) (GetGamePlaySubmissionsResponseObject, error) { + gameID := request.GameID + + _, err := h.q.GetGameByID(ctx, int32(gameID)) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return GetGamePlaySubmissions404JSONResponse{ + Message: "Game not found", + }, nil + } + return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + rows, err := h.q.GetSubmissionsByGameIDAndUserID(ctx, db.GetSubmissionsByGameIDAndUserIDParams{ + GameID: int32(gameID), + UserID: user.UserID, + }) + if err != nil { + return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + submissions := make([]Submission, len(rows)) + for i, row := range rows { + submissions[i] = Submission{ + SubmissionID: int(row.SubmissionID), + GameID: int(row.GameID), + Code: row.Code, + CodeSize: int(row.CodeSize), + Status: ExecutionStatus(row.Status), + CreatedAt: row.CreatedAt.Time.Unix(), + } + } + + return GetGamePlaySubmissions200JSONResponse{ + Submissions: submissions, + }, nil +} + func (h *Handler) PostGamePlayCode(ctx context.Context, request PostGamePlayCodeRequestObject, user *db.User) (PostGamePlayCodeResponseObject, error) { gameID := request.GameID diff --git a/backend/api/handler_test.go b/backend/api/handler_test.go index 2415710..a28d605 100644 --- a/backend/api/handler_test.go +++ b/backend/api/handler_test.go @@ -16,17 +16,18 @@ import ( // mockQuerier implements db.Querier for testing. type mockQuerier struct { db.Querier - getGameByIDFunc func(ctx context.Context, gameID int32) (db.GetGameByIDRow, error) - listMainPlayersFunc func(ctx context.Context, gameIDs []int32) ([]db.ListMainPlayersRow, error) - listPublicGamesFunc func(ctx context.Context) ([]db.ListPublicGamesRow, error) - deleteSessionFunc func(ctx context.Context, sessionID string) error - getLatestStateFunc func(ctx context.Context, arg db.GetLatestStateParams) (db.GetLatestStateRow, error) - updateCodeFunc func(ctx context.Context, arg db.UpdateCodeParams) error - getRankingFunc func(ctx context.Context, gameID int32) ([]db.GetRankingRow, error) - getLatestStatesFunc func(ctx context.Context, gameID int32) ([]db.GetLatestStatesOfMainPlayersRow, error) - getTournamentByIDFunc func(ctx context.Context, tournamentID int32) (db.Tournament, error) - listTournamentEntriesFunc func(ctx context.Context, tournamentID int32) ([]db.ListTournamentEntriesRow, error) - listTournamentMatchesFunc func(ctx context.Context, tournamentID int32) ([]db.TournamentMatch, error) + getGameByIDFunc func(ctx context.Context, gameID int32) (db.GetGameByIDRow, error) + listMainPlayersFunc func(ctx context.Context, gameIDs []int32) ([]db.ListMainPlayersRow, error) + listPublicGamesFunc func(ctx context.Context) ([]db.ListPublicGamesRow, error) + deleteSessionFunc func(ctx context.Context, sessionID string) error + getLatestStateFunc func(ctx context.Context, arg db.GetLatestStateParams) (db.GetLatestStateRow, error) + updateCodeFunc func(ctx context.Context, arg db.UpdateCodeParams) error + getRankingFunc func(ctx context.Context, gameID int32) ([]db.GetRankingRow, error) + getLatestStatesFunc func(ctx context.Context, gameID int32) ([]db.GetLatestStatesOfMainPlayersRow, error) + getTournamentByIDFunc func(ctx context.Context, tournamentID int32) (db.Tournament, error) + listTournamentEntriesFunc func(ctx context.Context, tournamentID int32) ([]db.ListTournamentEntriesRow, error) + listTournamentMatchesFunc func(ctx context.Context, tournamentID int32) ([]db.TournamentMatch, error) + getSubmissionsByGameIDAndUserIDFunc func(ctx context.Context, arg db.GetSubmissionsByGameIDAndUserIDParams) ([]db.Submission, error) } func (m *mockQuerier) GetGameByID(ctx context.Context, gameID int32) (db.GetGameByIDRow, error) { @@ -85,6 +86,13 @@ func (m *mockQuerier) GetLatestStatesOfMainPlayers(ctx context.Context, gameID i return nil, nil } +func (m *mockQuerier) GetSubmissionsByGameIDAndUserID(ctx context.Context, arg db.GetSubmissionsByGameIDAndUserIDParams) ([]db.Submission, error) { + if m.getSubmissionsByGameIDAndUserIDFunc != nil { + return m.getSubmissionsByGameIDAndUserIDFunc(ctx, arg) + } + return nil, nil +} + func (m *mockQuerier) GetTournamentByID(ctx context.Context, tournamentID int32) (db.Tournament, error) { if m.getTournamentByIDFunc != nil { return m.getTournamentByIDFunc(ctx, tournamentID) @@ -137,6 +145,142 @@ func (m *mockAuthenticator) Login(_ context.Context, _, _ string) (int, error) { return m.loginResult, m.loginErr } +func TestGetGamePlaySubmissions_GameNotFound(t *testing.T) { + h := Handler{ + q: &mockQuerier{}, + txm: &mockTxManager{}, + hub: &mockGameHub{}, + auth: &mockAuthenticator{}, + conf: &config.Config{}, + } + user := &db.User{UserID: 1} + resp, err := h.GetGamePlaySubmissions(context.Background(), GetGamePlaySubmissionsRequestObject{ + GameID: 999, + }, user) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if _, ok := resp.(GetGamePlaySubmissions404JSONResponse); !ok { + t.Errorf("expected 404 response, got %T", resp) + } +} + +func TestGetGamePlaySubmissions_Empty(t *testing.T) { + h := Handler{ + q: &mockQuerier{ + getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { + return db.GetGameByIDRow{ + GameID: 1, + Language: "php", + }, nil + }, + }, + txm: &mockTxManager{}, + hub: &mockGameHub{}, + auth: &mockAuthenticator{}, + conf: &config.Config{}, + } + user := &db.User{UserID: 1} + resp, err := h.GetGamePlaySubmissions(context.Background(), GetGamePlaySubmissionsRequestObject{ + GameID: 1, + }, user) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + okResp, ok := resp.(GetGamePlaySubmissions200JSONResponse) + if !ok { + t.Fatalf("expected 200 response, got %T", resp) + } + if len(okResp.Submissions) != 0 { + t.Errorf("expected 0 submissions, got %d", len(okResp.Submissions)) + } +} + +func TestGetGamePlaySubmissions_WithSubmissions(t *testing.T) { + now := time.Now() + h := Handler{ + q: &mockQuerier{ + getGameByIDFunc: func(_ context.Context, _ int32) (db.GetGameByIDRow, error) { + return db.GetGameByIDRow{ + GameID: 1, + Language: "php", + }, nil + }, + getSubmissionsByGameIDAndUserIDFunc: func(_ context.Context, arg db.GetSubmissionsByGameIDAndUserIDParams) ([]db.Submission, error) { + if arg.GameID != 1 || arg.UserID != 42 { + t.Errorf("unexpected query params: game_id=%d, user_id=%d", arg.GameID, arg.UserID) + } + return []db.Submission{ + { + SubmissionID: 10, + GameID: 1, + UserID: 42, + Code: "<?php echo 1;", + CodeSize: 14, + Status: "success", + CreatedAt: pgtype.Timestamp{Time: now, Valid: true}, + }, + { + SubmissionID: 9, + GameID: 1, + UserID: 42, + Code: "<?php echo 'hello';", + CodeSize: 20, + Status: "wrong_answer", + CreatedAt: pgtype.Timestamp{Time: now.Add(-5 * time.Minute), Valid: true}, + }, + }, nil + }, + }, + txm: &mockTxManager{}, + hub: &mockGameHub{}, + auth: &mockAuthenticator{}, + conf: &config.Config{}, + } + user := &db.User{UserID: 42} + resp, err := h.GetGamePlaySubmissions(context.Background(), GetGamePlaySubmissionsRequestObject{ + GameID: 1, + }, user) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + okResp, ok := resp.(GetGamePlaySubmissions200JSONResponse) + if !ok { + t.Fatalf("expected 200 response, got %T", resp) + } + if len(okResp.Submissions) != 2 { + t.Fatalf("expected 2 submissions, got %d", len(okResp.Submissions)) + } + + s0 := okResp.Submissions[0] + if s0.SubmissionID != 10 { + t.Errorf("expected submission_id 10, got %d", s0.SubmissionID) + } + if s0.GameID != 1 { + t.Errorf("expected game_id 1, got %d", s0.GameID) + } + if s0.Code != "<?php echo 1;" { + t.Errorf("expected code '<?php echo 1;', got %q", s0.Code) + } + if s0.CodeSize != 14 { + t.Errorf("expected code_size 14, got %d", s0.CodeSize) + } + if s0.Status != Success { + t.Errorf("expected status 'success', got %q", s0.Status) + } + if s0.CreatedAt != now.Unix() { + t.Errorf("expected created_at %d, got %d", now.Unix(), s0.CreatedAt) + } + + s1 := okResp.Submissions[1] + if s1.SubmissionID != 9 { + t.Errorf("expected submission_id 9, got %d", s1.SubmissionID) + } + if s1.Status != WrongAnswer { + t.Errorf("expected status 'wrong_answer', got %q", s1.Status) + } +} + func TestPostGamePlaySubmit_GameNotFound(t *testing.T) { h := Handler{ q: &mockQuerier{}, diff --git a/backend/api/handler_wrapper.go b/backend/api/handler_wrapper.go index 28b89e4..48a0eef 100644 --- a/backend/api/handler_wrapper.go +++ b/backend/api/handler_wrapper.go @@ -47,6 +47,16 @@ func (h *HandlerWrapper) GetGamePlayLatestState(ctx context.Context, request Get return h.impl.GetGamePlayLatestState(ctx, request, user) } +func (h *HandlerWrapper) GetGamePlaySubmissions(ctx context.Context, request GetGamePlaySubmissionsRequestObject) (GetGamePlaySubmissionsResponseObject, error) { + user, ok := GetUserFromContext(ctx) + if !ok { + return GetGamePlaySubmissions401JSONResponse{ + Message: "Unauthorized", + }, nil + } + return h.impl.GetGamePlaySubmissions(ctx, request, user) +} + func (h *HandlerWrapper) GetGameWatchLatestStates(ctx context.Context, request GetGameWatchLatestStatesRequestObject) (GetGameWatchLatestStatesResponseObject, error) { user, ok := GetUserFromContext(ctx) if !ok { diff --git a/backend/db/querier.go b/backend/db/querier.go index 220a86c..692ea0d 100644 --- a/backend/db/querier.go +++ b/backend/db/querier.go @@ -37,6 +37,7 @@ type Querier interface { GetRanking(ctx context.Context, gameID int32) ([]GetRankingRow, error) GetSubmissionByID(ctx context.Context, submissionID int32) (Submission, error) GetSubmissionsByGameID(ctx context.Context, gameID int32) ([]Submission, error) + GetSubmissionsByGameIDAndUserID(ctx context.Context, arg GetSubmissionsByGameIDAndUserIDParams) ([]Submission, error) GetTestcaseByID(ctx context.Context, testcaseID int32) (Testcase, error) GetTestcaseResultsBySubmissionID(ctx context.Context, submissionID int32) ([]TestcaseResult, error) GetTournamentByID(ctx context.Context, tournamentID int32) (Tournament, error) diff --git a/backend/db/query.sql.go b/backend/db/query.sql.go index 02f1abf..80cbc16 100644 --- a/backend/db/query.sql.go +++ b/backend/db/query.sql.go @@ -705,6 +705,45 @@ func (q *Queries) GetSubmissionsByGameID(ctx context.Context, gameID int32) ([]S return items, nil } +const getSubmissionsByGameIDAndUserID = `-- name: GetSubmissionsByGameIDAndUserID :many +SELECT submission_id, game_id, user_id, code, code_size, status, created_at FROM submissions +WHERE game_id = $1 AND user_id = $2 +ORDER BY created_at DESC +` + +type GetSubmissionsByGameIDAndUserIDParams struct { + GameID int32 + UserID int32 +} + +func (q *Queries) GetSubmissionsByGameIDAndUserID(ctx context.Context, arg GetSubmissionsByGameIDAndUserIDParams) ([]Submission, error) { + rows, err := q.db.Query(ctx, getSubmissionsByGameIDAndUserID, arg.GameID, arg.UserID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Submission + for rows.Next() { + var i Submission + if err := rows.Scan( + &i.SubmissionID, + &i.GameID, + &i.UserID, + &i.Code, + &i.CodeSize, + &i.Status, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getTestcaseByID = `-- name: GetTestcaseByID :one SELECT testcase_id, problem_id, stdin, stdout FROM testcases WHERE testcase_id = $1 diff --git a/backend/query.sql b/backend/query.sql index 45ac46f..c791c27 100644 --- a/backend/query.sql +++ b/backend/query.sql @@ -259,6 +259,11 @@ WHERE testcase_id = $1; DELETE FROM testcases WHERE testcase_id = $1; +-- name: GetSubmissionsByGameIDAndUserID :many +SELECT * FROM submissions +WHERE game_id = $1 AND user_id = $2 +ORDER BY created_at DESC; + -- name: GetSubmissionsByGameID :many SELECT * FROM submissions |
