feat(tester): add endpoints
add GetMonitor&GetTask endpoints
This commit is contained in:
parent
ef696d2836
commit
b960a923d2
9 changed files with 400 additions and 16 deletions
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
2
proto
2
proto
|
@ -1 +1 @@
|
|||
Subproject commit 6a83a9832d5fed659f72e72c90b3954a9518e0ba
|
||||
Subproject commit bf3233f7b8862f34a8c25b5946acfe9ee0afc8e5
|
Loading…
Add table
Reference in a new issue