From b960a923d26052d8949755696174d72478d8f950 Mon Sep 17 00:00:00 2001 From: Vyacheslav1557 Date: Fri, 28 Mar 2025 01:17:53 +0500 Subject: [PATCH] feat(tester): add endpoints add GetMonitor&GetTask endpoints --- internal/models/contest.go | 28 ++- internal/models/task.go | 19 +- internal/tester/delivery.go | 4 +- internal/tester/delivery/rest/handlers.go | 170 +++++++++++++++- internal/tester/pg_repository.go | 2 + .../repository/pg_contests_repository.go | 181 ++++++++++++++++++ internal/tester/usecase.go | 2 + internal/tester/usecase/contests_usecase.go | 8 + proto | 2 +- 9 files changed, 400 insertions(+), 16 deletions(-) diff --git a/internal/models/contest.go b/internal/models/contest.go index 62d4d30..d665977 100644 --- a/internal/models/contest.go +++ b/internal/models/contest.go @@ -3,10 +3,10 @@ package models import "time" type Contest struct { - Id *int32 `db:"id"` - Title *string `db:"title"` - CreatedAt *time.Time `db:"created_at"` - UpdatedAt *time.Time `db:"updated_at"` + Id int32 `db:"id"` + Title string `db:"title"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } type ContestsListItem struct { @@ -19,3 +19,23 @@ type ContestsListItem struct { type ContestUpdate struct { Title *string `json:"title"` } + +type Monitor struct { + Participants []*ParticipantsStat + Summary []*ProblemStatSummary +} + +type ParticipantsStat struct { + Id int32 `db:"id"` + Name string `db:"name"` + SolvedInTotal int32 `db:"solved_in_total"` + PenaltyInTotal int32 `db:"penalty_in_total"` + Solutions []*SolutionsListItem `db:"solutions"` +} + +type ProblemStatSummary struct { + Id int32 `db:"task_id"` + Position int32 `db:"position"` + Success int32 `db:"success"` + Total int32 `db:"total"` +} diff --git a/internal/models/task.go b/internal/models/task.go index aef2fe7..2f40ed4 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -3,10 +3,21 @@ package models import "time" type Task struct { - Id int32 `db:"id"` - ProblemId int32 `db:"problem_id"` - ContestId int32 `db:"contest_id"` - Position int32 `db:"position"` + Id int32 `db:"id"` + Position int32 `db:"position"` + Title string `db:"title"` + TimeLimit int32 `db:"time_limit"` + MemoryLimit int32 `db:"memory_limit"` + + ProblemId int32 `db:"problem_id"` + ContestId int32 `db:"contest_id"` + + LegendHtml string `db:"legend_html"` + InputFormatHtml string `db:"input_format_html"` + OutputFormatHtml string `db:"output_format_html"` + NotesHtml string `db:"notes_html"` + ScoringHtml string `db:"scoring_html"` + CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } diff --git a/internal/tester/delivery.go b/internal/tester/delivery.go index 2fe17bd..d1045a9 100644 --- a/internal/tester/delivery.go +++ b/internal/tester/delivery.go @@ -23,6 +23,8 @@ type Handlers interface { 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 + DeleteTask(c *fiber.Ctx, id int32) error AddTask(c *fiber.Ctx, params testerv1.AddTaskParams) error + GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error + GetTask(c *fiber.Ctx, id int32) error } diff --git a/internal/tester/delivery/rest/handlers.go b/internal/tester/delivery/rest/handlers.go index 92620f5..29c9cea 100644 --- a/internal/tester/delivery/rest/handlers.go +++ b/internal/tester/delivery/rest/handlers.go @@ -14,7 +14,7 @@ type TesterHandlers struct { contestsUC tester.ContestUseCase } -func NewTesterHandlers(problemsUC tester.ProblemUseCase, contestsUC tester.ContestRepository) *TesterHandlers { +func NewTesterHandlers(problemsUC tester.ProblemUseCase, contestsUC tester.ContestUseCase) *TesterHandlers { return &TesterHandlers{ problemsUC: problemsUC, contestsUC: contestsUC, @@ -120,9 +120,9 @@ func (h *TesterHandlers) GetContest(c *fiber.Ctx, id int32) error { resp := testerv1.GetContestResponse{ Contest: testerv1.Contest{ Id: id, - Title: *contest.Title, - CreatedAt: *contest.CreatedAt, - UpdatedAt: *contest.UpdatedAt, + Title: contest.Title, + CreatedAt: contest.CreatedAt, + UpdatedAt: contest.UpdatedAt, }, Tasks: make([]struct { BestSolution testerv1.BestSolution `json:"best_solution"` @@ -172,8 +172,8 @@ func (h *TesterHandlers) AddParticipant(c *fiber.Ctx, params testerv1.AddPartici }) } -func (h *TesterHandlers) DeleteTask(c *fiber.Ctx, params testerv1.DeleteTaskParams) error { - err := h.contestsUC.DeleteTask(c.Context(), params.TaskId) +func (h *TesterHandlers) DeleteTask(c *fiber.Ctx, id int32) error { + err := h.contestsUC.DeleteTask(c.Context(), id) if err != nil { return c.SendStatus(pkg.ToREST(err)) } @@ -442,3 +442,161 @@ func (h *TesterHandlers) GetSolution(c *fiber.Ctx, id int32) error { }}, ) } + +func (h *TesterHandlers) GetTask(c *fiber.Ctx, id int32) error { + contest, err := h.contestsUC.ReadContestById(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + tasks, err := h.contestsUC.ReadRichTasks(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + t, err := h.contestsUC.ReadTask(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + resp := testerv1.GetTaskResponse{ + Contest: struct { + Id int32 `json:"id"` + Tasks []testerv1.RichTask `json:"tasks"` + Title string `json:"title"` + }{ + Id: contest.Id, + Title: contest.Title, + Tasks: make([]testerv1.RichTask, len(tasks)), + }, + Task: testerv1.Task{ + Id: t.Id, + Title: t.Title, + MemoryLimit: t.MemoryLimit, + TimeLimit: t.TimeLimit, + + InputFormatHtml: t.InputFormatHtml, + LegendHtml: t.LegendHtml, + NotesHtml: t.NotesHtml, + OutputFormatHtml: t.OutputFormatHtml, + Position: t.Position, + ScoringHtml: t.ScoringHtml, + + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, + }, + } + + for i, task := range tasks { + resp.Contest.Tasks[i] = testerv1.RichTask{ + Id: task.Id, + Position: task.Position, + Title: task.Title, + MemoryLimit: task.MemoryLimit, + ProblemId: task.ProblemId, + TimeLimit: task.TimeLimit, + CreatedAt: task.CreatedAt, + UpdatedAt: task.UpdatedAt} + } + + return c.JSON(resp) +} + +func (h *TesterHandlers) GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error { + contest, err := h.contestsUC.ReadContestById(c.Context(), params.ContestId) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + monitor, err := h.contestsUC.ReadMonitor(c.Context(), params.ContestId) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + tasks, err := h.contestsUC.ReadRichTasks(c.Context(), params.ContestId) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + resp := testerv1.GetMonitorResponse{ + Contest: struct { + Id int32 `json:"id"` + Tasks []testerv1.RichTask `json:"tasks"` + Title string `json:"title"` + }{ + Id: contest.Id, + Title: contest.Title, + Tasks: make([]testerv1.RichTask, len(tasks)), + }, + Participants: make([]struct { + Id int32 `json:"id"` + Name string `json:"name"` + PenaltyInTotal int32 `json:"penalty_in_total"` + Solutions []testerv1.SolutionListItem `json:"solutions"` + SolvedInTotal int32 `json:"solved_in_total"` + }, len(monitor.Participants)), + SummaryPerProblem: make([]struct { + Id int32 `json:"id"` + Success int32 `json:"success"` + Total int32 `json:"total"` + }, len(monitor.Summary)), + } + + for i, participant := range monitor.Participants { + resp.Participants[i] = struct { + Id int32 `json:"id"` + Name string `json:"name"` + PenaltyInTotal int32 `json:"penalty_in_total"` + Solutions []testerv1.SolutionListItem `json:"solutions"` + SolvedInTotal int32 `json:"solved_in_total"` + }{ + Id: participant.Id, + Name: participant.Name, + PenaltyInTotal: participant.PenaltyInTotal, + Solutions: make([]testerv1.SolutionListItem, len(participant.Solutions)), + SolvedInTotal: participant.SolvedInTotal, + } + + for j, solution := range participant.Solutions { + resp.Participants[i].Solutions[j] = testerv1.SolutionListItem{ + ContestId: solution.ContestId, + CreatedAt: solution.CreatedAt, + Id: solution.Id, + Language: solution.Language, + ParticipantId: solution.ParticipantId, + Penalty: solution.Penalty, + Score: solution.Score, + State: solution.State, + TaskId: solution.TaskId, + TotalScore: solution.TotalScore, + UpdatedAt: solution.UpdatedAt, + } + } + } + + for i, problem := range monitor.Summary { + resp.SummaryPerProblem[i] = struct { + Id int32 `json:"id"` + Success int32 `json:"success"` + Total int32 `json:"total"` + }{ + Id: problem.Id, + Success: problem.Success, + Total: problem.Total, + } + } + + for i, task := range tasks { + resp.Contest.Tasks[i] = testerv1.RichTask{ + Id: task.Id, + Position: task.Position, + Title: task.Title, + MemoryLimit: task.MemoryLimit, + ProblemId: task.ProblemId, + TimeLimit: task.TimeLimit, + CreatedAt: task.CreatedAt, + UpdatedAt: task.UpdatedAt} + } + + return c.JSON(resp) +} diff --git a/internal/tester/pg_repository.go b/internal/tester/pg_repository.go index d61e20b..b559614 100644 --- a/internal/tester/pg_repository.go +++ b/internal/tester/pg_repository.go @@ -47,4 +47,6 @@ type ContestRepository interface { 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) + ReadTask(ctx context.Context, id int32) (*models.Task, error) + ReadMonitor(ctx context.Context, id int32) (*models.Monitor, error) } diff --git a/internal/tester/repository/pg_contests_repository.go b/internal/tester/repository/pg_contests_repository.go index c870de6..9e8e739 100644 --- a/internal/tester/repository/pg_contests_repository.go +++ b/internal/tester/repository/pg_contests_repository.go @@ -391,3 +391,184 @@ func (r *ContestRepository) ListSolutions(ctx context.Context, filters models.So return solutions, totalCount, nil } + +const ( + readTaskQuery = ` + SELECT + t.id, + t.position, + p.title, + p.time_limit, + p.memory_limit, + t.problem_id, + t.contest_id, + p.legend_html, + p.input_format_html, + p.output_format_html, + p.notes_html, + p.scoring_html, + t.created_at, + t.updated_at + FROM tasks t + LEFT JOIN problems p ON t.problem_id = p.id + WHERE t.id = ? + ` +) + +func (r *ContestRepository) ReadTask(ctx context.Context, id int32) (*models.Task, error) { + const op = "ContestRepository.ReadTask" + + query := r.db.Rebind(readTaskQuery) + var task models.Task + err := r.db.GetContext(ctx, &task, query, id) + if err != nil { + return nil, handlePgErr(err, op) + } + + return &task, nil +} + +const ( + // state=5 - AC + readStatisticsQuery = ` +SELECT t.id as task_id, + t.position, + COUNT(*) as total, + COUNT(CASE WHEN s.state = 5 THEN 1 END) as success +FROM tasks t + LEFT JOIN solutions s ON t.id = s.task_id +WHERE t.contest_id = ? +GROUP BY t.id, t.position +ORDER BY t.position; +` + + solutionsQuery = ` +WITH RankedSolutions AS ( + SELECT + s.id, + s.task_id, + s.participant_id, + s.state, + s.score, + s.penalty, + s.total_score, + s.language, + s.created_at, + s.updated_at, + t.contest_id, + ROW_NUMBER() OVER ( + PARTITION BY s.participant_id, s.task_id + ORDER BY + CASE WHEN s.state = 5 THEN 0 ELSE 1 END, + s.created_at + ) as rn + FROM solutions s + JOIN tasks t ON s.task_id = t.id + WHERE t.contest_id = ? +) +SELECT + rs.id, + rs.task_id, + rs.contest_id, + rs.participant_id, + rs.state, + rs.score, + rs.penalty, + rs.total_score, + rs.language, + rs.created_at, + rs.updated_at +FROM RankedSolutions rs +WHERE rs.rn = 1 +` + + participantsQuery = ` +WITH Attempts AS ( + SELECT + s.participant_id, + s.task_id, + COUNT(*) FILTER (WHERE s.state != 5 AND s.created_at < ( + SELECT MIN(s2.created_at) + FROM solutions s2 + WHERE s2.participant_id = s.participant_id + AND s2.task_id = s.task_id + AND s2.state = 5 + )) as failed_attempts, + MIN(CASE WHEN s.state = 5 THEN s.penalty END) as success_penalty + FROM solutions s + JOIN tasks t ON t.id = s.task_id + WHERE t.contest_id = :contest_id + GROUP BY s.participant_id, s.task_id +) +SELECT + p.id, + p.name, + COUNT(DISTINCT CASE WHEN a.success_penalty IS NOT NULL THEN a.task_id END) as solved_in_total, + COALESCE(SUM(CASE WHEN a.success_penalty IS NOT NULL + THEN a.failed_attempts * :penalty + a.success_penalty + ELSE 0 END), 0) as penalty_in_total +FROM participants p + LEFT JOIN Attempts a ON a.participant_id = p.id +WHERE p.contest_id = :contest_id +GROUP BY p.id, p.name +` +) + +func (r *ContestRepository) ReadMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) { + const op = "ContestRepository.ReadMonitor" + + query := r.db.Rebind(readStatisticsQuery) + rows, err := r.db.QueryxContext(ctx, query, contestId) + if err != nil { + return nil, handlePgErr(err, op) + } + defer rows.Close() + + var monitor models.Monitor + for rows.Next() { + var stat models.ProblemStatSummary + err = rows.StructScan(&stat) + if err != nil { + return nil, handlePgErr(err, op) + } + monitor.Summary = append(monitor.Summary, &stat) + } + + var solutions []*models.SolutionsListItem + err = r.db.SelectContext(ctx, &solutions, r.db.Rebind(solutionsQuery), contestId) + if err != nil { + return nil, handlePgErr(err, op) + } + + penalty := int32(20) // FIXME + namedQuery := r.db.Rebind(participantsQuery) + rows3, err := r.db.NamedQueryContext(ctx, namedQuery, map[string]interface{}{ + "contest_id": contestId, + "penalty": penalty, + }) + if err != nil { + return nil, handlePgErr(err, op) + } + defer rows3.Close() + + solutionsMap := make(map[int32][]*models.SolutionsListItem) + for _, solution := range solutions { + solutionsMap[solution.ParticipantId] = append(solutionsMap[solution.ParticipantId], solution) + } + + for rows3.Next() { + var stat models.ParticipantsStat + err = rows3.StructScan(&stat) + if err != nil { + return nil, handlePgErr(err, op) + } + + if sols, ok := solutionsMap[stat.Id]; ok { + stat.Solutions = sols + } + + monitor.Participants = append(monitor.Participants, &stat) + } + + return &monitor, nil +} diff --git a/internal/tester/usecase.go b/internal/tester/usecase.go index 4e11c18..dede5b2 100644 --- a/internal/tester/usecase.go +++ b/internal/tester/usecase.go @@ -29,4 +29,6 @@ type ContestUseCase interface { 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) + ReadTask(ctx context.Context, id int32) (*models.Task, error) + ReadMonitor(ctx context.Context, id int32) (*models.Monitor, error) } diff --git a/internal/tester/usecase/contests_usecase.go b/internal/tester/usecase/contests_usecase.go index 9d30463..5c23189 100644 --- a/internal/tester/usecase/contests_usecase.go +++ b/internal/tester/usecase/contests_usecase.go @@ -77,3 +77,11 @@ func (uc *ContestUseCase) CreateSolution(ctx context.Context, creation *models.S func (uc *ContestUseCase) ListSolutions(ctx context.Context, filters models.SolutionsFilter) ([]*models.SolutionsListItem, int32, error) { return uc.contestRepo.ListSolutions(ctx, filters) } + +func (uc *ContestUseCase) ReadTask(ctx context.Context, id int32) (*models.Task, error) { + return uc.contestRepo.ReadTask(ctx, id) +} + +func (uc *ContestUseCase) ReadMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) { + return uc.contestRepo.ReadMonitor(ctx, contestId) +} diff --git a/proto b/proto index 6a83a98..bf3233f 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 6a83a9832d5fed659f72e72c90b3954a9518e0ba +Subproject commit bf3233f7b8862f34a8c25b5946acfe9ee0afc8e5