aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-20 23:32:22 +0900
committernsfisis <nsfisis@gmail.com>2026-02-20 23:32:22 +0900
commit8e73d12a703e90ad908962143951178c13d0d6fe (patch)
tree8bed43aa4b115f8bc50ed258aa192a94b6d2903e /backend
parentaa07ba2e0a40b0097a4f9aee3c06dcbd9a749105 (diff)
downloadphperkaigi-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.go151
-rw-r--r--backend/api/handler.go38
-rw-r--r--backend/api/handler_test.go166
-rw-r--r--backend/api/handler_wrapper.go10
-rw-r--r--backend/db/querier.go1
-rw-r--r--backend/db/query.sql.go39
-rw-r--r--backend/query.sql5
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