diff --git a/internal/models/solution.go b/internal/models/solution.go index 5e10a0c..46e1c8d 100644 --- a/internal/models/solution.go +++ b/internal/models/solution.go @@ -15,3 +15,36 @@ type Solution struct { UpdatedAt time.Time `db:"updated_at"` CreatedAt time.Time `db:"created_at"` } + +type SolutionCreation struct { + Solution string + TaskId int32 + ParticipantId int32 + Language int32 + Penalty int32 +} + +type SolutionsListItem struct { + Id int32 `db:"id"` + TaskId int32 `db:"task_id"` + ContestId int32 `db:"contest_id"` + ParticipantId int32 `db:"participant_id"` + State int32 `db:"state"` + Score int32 `db:"score"` + Penalty int32 `db:"penalty"` + TotalScore int32 `db:"total_score"` + Language int32 `db:"language"` + UpdatedAt time.Time `db:"updated_at"` + CreatedAt time.Time `db:"created_at"` +} + +type SolutionsFilter struct { + Page int32 + PageSize int32 + ContestId *int32 + ParticipantId *int32 + TaskId *int32 + Language *int32 + State *int32 + Order *int32 +} diff --git a/internal/tester/delivery.go b/internal/tester/delivery.go index f16d4cc..2fe17bd 100644 --- a/internal/tester/delivery.go +++ b/internal/tester/delivery.go @@ -10,16 +10,19 @@ type Handlers interface { CreateContest(c *fiber.Ctx) error DeleteContest(c *fiber.Ctx, id int32) error GetContest(c *fiber.Ctx, id int32) error + UpdateContest(c *fiber.Ctx, id int32) error DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) error + ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error + UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error AddParticipant(c *fiber.Ctx, params testerv1.AddParticipantParams) error - DeleteTask(c *fiber.Ctx, params testerv1.DeleteTaskParams) error - AddTask(c *fiber.Ctx, params testerv1.AddTaskParams) error ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error CreateProblem(c *fiber.Ctx) error DeleteProblem(c *fiber.Ctx, id int32) error GetProblem(c *fiber.Ctx, id int32) error - ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error UpdateProblem(c *fiber.Ctx, id int32) error - UpdateContest(c *fiber.Ctx, id int32) error - UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error + ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error + CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error + GetSolution(c *fiber.Ctx, id int32) error + DeleteTask(c *fiber.Ctx, params testerv1.DeleteTaskParams) error + AddTask(c *fiber.Ctx, params testerv1.AddTaskParams) error } diff --git a/internal/tester/delivery/rest/handlers.go b/internal/tester/delivery/rest/handlers.go index f9a8487..92620f5 100644 --- a/internal/tester/delivery/rest/handlers.go +++ b/internal/tester/delivery/rest/handlers.go @@ -6,6 +6,7 @@ import ( "git.sch9.ru/new_gate/ms-tester/pkg" testerv1 "git.sch9.ru/new_gate/ms-tester/proto/tester/v1" "github.com/gofiber/fiber/v2" + "io" ) type TesterHandlers struct { @@ -332,3 +333,112 @@ func (h *TesterHandlers) UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateP return c.SendStatus(fiber.StatusOK) } + +func (h *TesterHandlers) ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error { + list, total, err := h.contestsUC.ListSolutions(c.Context(), models.SolutionsFilter{ + ContestId: params.ContestId, + Page: params.Page, + PageSize: params.PageSize, + ParticipantId: params.ParticipantId, + TaskId: params.TaskId, + Language: params.Language, + Order: params.Order, + State: params.State, + }) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + resp := testerv1.ListSolutionsResponse{ + Solutions: make([]testerv1.SolutionListItem, len(list)), + Page: params.Page, + MaxPage: func() int32 { + if total%params.PageSize == 0 { + return total / params.PageSize + } + return total/params.PageSize + 1 + }(), + } + + for i, solution := range list { + resp.Solutions[i] = testerv1.SolutionListItem{ + Id: solution.Id, + TaskId: solution.TaskId, + ContestId: solution.ContestId, + ParticipantId: solution.ParticipantId, + Language: solution.Language, + Penalty: solution.Penalty, + Score: solution.Score, + State: solution.State, + TotalScore: solution.TotalScore, + CreatedAt: solution.CreatedAt, + UpdatedAt: solution.UpdatedAt, + } + } + + return c.JSON(resp) +} + +const ( + maxSolutionSize int64 = 10 * 1024 * 1024 +) + +func (h *TesterHandlers) CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error { + s, err := c.FormFile("solution") + if err != nil { + return err + } + + if s.Size == 0 || s.Size > maxSolutionSize { + return c.SendStatus(fiber.StatusBadRequest) + } + + f, err := s.Open() + if err != nil { + return err + } + defer f.Close() + + b, err := io.ReadAll(f) + if err != nil { + return err + } + + id, err := h.contestsUC.CreateSolution(c.Context(), &models.SolutionCreation{ + TaskId: params.TaskId, + ParticipantId: 1, + Language: params.Language, + Penalty: 0, + Solution: string(b), + }) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.JSON(testerv1.CreateSolutionResponse{ + Id: id, + }) +} + +func (h *TesterHandlers) GetSolution(c *fiber.Ctx, id int32) error { + solution, err := h.contestsUC.ReadSolution(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.JSON( + testerv1.GetSolutionResponse{Solution: testerv1.Solution{ + Id: solution.Id, + TaskId: solution.TaskId, + ParticipantId: solution.ParticipantId, + Solution: solution.Solution, + State: solution.State, + Score: solution.Score, + Penalty: solution.Penalty, + TotalScore: solution.TotalScore, + Language: solution.Language, + CreatedAt: solution.CreatedAt, + UpdatedAt: solution.UpdatedAt, + }}, + ) +} diff --git a/internal/tester/pg_repository.go b/internal/tester/pg_repository.go index 806185e..d61e20b 100644 --- a/internal/tester/pg_repository.go +++ b/internal/tester/pg_repository.go @@ -44,4 +44,7 @@ type ContestRepository interface { ListParticipants(ctx context.Context, contestId int32, page int32, pageSize int32) ([]*models.ParticipantsListItem, int32, error) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error + ReadSolution(ctx context.Context, id int32) (*models.Solution, error) + CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) + ListSolutions(ctx context.Context, filters models.SolutionsFilter) ([]*models.SolutionsListItem, int32, error) } diff --git a/internal/tester/repository/pg_contests_repository.go b/internal/tester/repository/pg_contests_repository.go index d00074c..c870de6 100644 --- a/internal/tester/repository/pg_contests_repository.go +++ b/internal/tester/repository/pg_contests_repository.go @@ -2,8 +2,10 @@ package repository import ( "context" + "fmt" "git.sch9.ru/new_gate/ms-tester/internal/models" "github.com/jmoiron/sqlx" + "strings" ) type ContestRepository struct { @@ -247,3 +249,145 @@ func (r *ContestRepository) UpdateParticipant(ctx context.Context, id int32, par return nil } + +const ( + readSolutionQuery = "SELECT * FROM solutions WHERE id = ?" +) + +func (r *ContestRepository) ReadSolution(ctx context.Context, id int32) (*models.Solution, error) { + const op = "ContestRepository.ReadSolution" + + query := r.db.Rebind(readSolutionQuery) + var solution models.Solution + err := r.db.GetContext(ctx, &solution, query, id) + if err != nil { + return nil, handlePgErr(err, op) + } + + return &solution, nil +} + +const ( + createSolutionQuery = `INSERT INTO solutions (task_id, participant_id, language, penalty, solution) +VALUES (?, ?, ?, ?, ?) +RETURNING id` +) + +func (r *ContestRepository) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) { + const op = "ContestRepository.CreateSolution" + + query := r.db.Rebind(createSolutionQuery) + + rows, err := r.db.QueryxContext(ctx, + query, + creation.TaskId, + creation.ParticipantId, + creation.Language, + creation.Penalty, + creation.Solution, + ) + if err != nil { + return 0, handlePgErr(err, op) + } + + defer rows.Close() + var id int32 + rows.Next() + err = rows.Scan(&id) + if err != nil { + return 0, handlePgErr(err, op) + } + + return id, nil +} + +func (r *ContestRepository) ListSolutions(ctx context.Context, filters models.SolutionsFilter) ([]*models.SolutionsListItem, int32, error) { + const op = "ContestRepository.ListSolutions" + + baseQuery := ` + SELECT + s.id, + s.task_id, + t.contest_id, + s.participant_id, + s.state, + s.score, + s.penalty, + s.total_score, + s.language, + s.updated_at, + s.created_at + FROM solutions s + LEFT JOIN tasks t ON s.task_id = t.id + WHERE 1=1 + ` + + var conditions []string + var args []interface{} + + if filters.ContestId != nil { + conditions = append(conditions, "contest_id = ?") + args = append(args, *filters.ContestId) + } + if filters.ParticipantId != nil { + conditions = append(conditions, "participant_id = ?") + args = append(args, *filters.ParticipantId) + } + if filters.TaskId != nil { + conditions = append(conditions, "task_id = ?") + args = append(args, *filters.TaskId) + } + if filters.Language != nil { + conditions = append(conditions, "language = ?") + args = append(args, *filters.Language) + } + if filters.State != nil { + conditions = append(conditions, "state = ?") + args = append(args, *filters.State) + } + + if len(conditions) > 0 { + baseQuery += " AND " + strings.Join(conditions, " AND ") + } + + if filters.Order != nil { + orderDirection := "ASC" + if *filters.Order < 0 { + orderDirection = "DESC" + } + baseQuery += fmt.Sprintf(" ORDER BY s.id %s", orderDirection) + } + + countQuery := "SELECT COUNT(*) FROM (" + baseQuery + ") as count_table" + var totalCount int32 + err := r.db.QueryRowxContext(ctx, r.db.Rebind(countQuery), args...).Scan(&totalCount) + if err != nil { + return nil, 0, handlePgErr(err, op) + } + + offset := (filters.Page - 1) * filters.PageSize + baseQuery += " LIMIT ? OFFSET ?" + args = append(args, filters.PageSize, offset) + + rows, err := r.db.QueryxContext(ctx, r.db.Rebind(baseQuery), args...) + if err != nil { + return nil, 0, handlePgErr(err, op) + } + defer rows.Close() + + var solutions []*models.SolutionsListItem + for rows.Next() { + var solution models.SolutionsListItem + err = rows.StructScan(&solution) + if err != nil { + return nil, 0, handlePgErr(err, op) + } + solutions = append(solutions, &solution) + } + + if err = rows.Err(); err != nil { + return nil, 0, handlePgErr(err, op) + } + + return solutions, totalCount, nil +} diff --git a/internal/tester/usecase.go b/internal/tester/usecase.go index 1c615fe..4e11c18 100644 --- a/internal/tester/usecase.go +++ b/internal/tester/usecase.go @@ -26,4 +26,7 @@ type ContestUseCase interface { ListParticipants(ctx context.Context, contestId int32, page int32, pageSize int32) ([]*models.ParticipantsListItem, int32, error) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error + ReadSolution(ctx context.Context, id int32) (*models.Solution, error) + CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) + ListSolutions(ctx context.Context, filters models.SolutionsFilter) ([]*models.SolutionsListItem, int32, error) } diff --git a/internal/tester/usecase/contests_usecase.go b/internal/tester/usecase/contests_usecase.go index 73afd78..9d30463 100644 --- a/internal/tester/usecase/contests_usecase.go +++ b/internal/tester/usecase/contests_usecase.go @@ -65,3 +65,15 @@ func (uc *ContestUseCase) UpdateContest(ctx context.Context, id int32, contestUp func (uc *ContestUseCase) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error { return uc.contestRepo.UpdateParticipant(ctx, id, participantUpdate) } + +func (uc *ContestUseCase) ReadSolution(ctx context.Context, id int32) (*models.Solution, error) { + return uc.contestRepo.ReadSolution(ctx, id) +} + +func (uc *ContestUseCase) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) { + return uc.contestRepo.CreateSolution(ctx, creation) +} + +func (uc *ContestUseCase) ListSolutions(ctx context.Context, filters models.SolutionsFilter) ([]*models.SolutionsListItem, int32, error) { + return uc.contestRepo.ListSolutions(ctx, filters) +} diff --git a/migrations/20240727123308_initial.sql b/migrations/20240727123308_initial.sql index e5723b7..c0f3188 100644 --- a/migrations/20240727123308_initial.sql +++ b/migrations/20240727123308_initial.sql @@ -105,9 +105,9 @@ CREATE TABLE IF NOT EXISTS solutions participant_id integer REFERENCES participants (id) ON DELETE SET NULL, solution varchar(1048576) NOT NULL, state integer NOT NULL DEFAULT 1, - score integer NOT NULL, + score integer NOT NULL DEFAULT 0, penalty integer NOT NULL, - total_score integer NOT NULL, + total_score integer NOT NULL DEFAULT 0, language integer NOT NULL, updated_at timestamptz NOT NULL DEFAULT now(), created_at timestamptz NOT NULL DEFAULT now(), diff --git a/proto b/proto index fb65ba8..6a83a98 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit fb65ba8ce2220e7e47990f0f52214126839b3d78 +Subproject commit 6a83a9832d5fed659f72e72c90b3954a9518e0ba