feat: merge auth&tester

This commit is contained in:
Vyacheslav1557 2025-04-22 20:44:52 +05:00
parent 0a2dea6c23
commit 441af4c6a2
72 changed files with 4910 additions and 2378 deletions

View file

@ -0,0 +1,25 @@
package contests
import (
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"github.com/gofiber/fiber/v2"
)
type ContestsHandlers interface {
ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error
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
CreateParticipant(c *fiber.Ctx, params testerv1.CreateParticipantParams) 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, id int32) error
CreateTask(c *fiber.Ctx, params testerv1.CreateTaskParams) error
GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error
GetTask(c *fiber.Ctx, id int32) error
}

View file

@ -0,0 +1,202 @@
package rest
import (
"context"
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/contests"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/internal/problems"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/gofiber/fiber/v2"
)
type Handlers struct {
problemsUC problems.UseCase
contestsUC contests.UseCase
jwtSecret string
}
func NewHandlers(problemsUC problems.UseCase, contestsUC contests.UseCase, jwtSecret string) *Handlers {
return &Handlers{
problemsUC: problemsUC,
contestsUC: contestsUC,
jwtSecret: jwtSecret,
}
}
const (
sessionKey = "session"
)
func sessionFromCtx(ctx context.Context) (*models.Session, error) {
const op = "sessionFromCtx"
session, ok := ctx.Value(sessionKey).(*models.Session)
if !ok {
return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "")
}
return session, nil
}
func (h *Handlers) CreateContest(c *fiber.Ctx) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
id, err := h.contestsUC.CreateContest(ctx, "Название контеста")
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(&testerv1.CreateContestResponse{
Id: id,
})
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) GetContest(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
contest, err := h.contestsUC.GetContest(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
tasks, err := h.contestsUC.GetTasks(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
solutions := make([]*models.SolutionsListItem, 0)
participantId, err := h.contestsUC.GetParticipantId(ctx, contest.Id, session.UserId)
if err == nil { // Admin or Teacher may not participate in contest
solutions, err = h.contestsUC.GetBestSolutions(ctx, id, participantId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
}
return c.JSON(GetContestResponseDTO(contest, tasks, solutions))
case models.RoleStudent:
contest, err := h.contestsUC.GetContest(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
tasks, err := h.contestsUC.GetTasks(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
participantId, err := h.contestsUC.GetParticipantId(ctx, contest.Id, session.UserId)
solutions, err := h.contestsUC.GetBestSolutions(c.Context(), id, participantId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(GetContestResponseDTO(contest, tasks, solutions))
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) UpdateContest(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
var req testerv1.UpdateContestRequest
err := c.BodyParser(&req)
if err != nil {
return err
}
err = h.contestsUC.UpdateContest(ctx, id, models.ContestUpdate{
Title: req.Title,
})
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) DeleteContest(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
err := h.contestsUC.DeleteContest(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
filter := models.ContestsFilter{
Page: params.Page,
PageSize: params.PageSize,
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
contestsList, err := h.contestsUC.ListContests(ctx, filter)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(ListContestsResponseDTO(contestsList))
case models.RoleStudent:
filter.UserId = &session.UserId
contestsList, err := h.contestsUC.ListContests(ctx, filter)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(ListContestsResponseDTO(contestsList))
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}

View file

@ -0,0 +1,202 @@
package rest
import (
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
func GetContestResponseDTO(contest *models.Contest,
tasks []*models.TasksListItem,
solutions []*models.SolutionsListItem) *testerv1.GetContestResponse {
m := make(map[int32]*models.SolutionsListItem)
for i := 0; i < len(solutions); i++ {
m[solutions[i].TaskPosition] = solutions[i]
}
resp := testerv1.GetContestResponse{
Contest: ContestDTO(*contest),
Tasks: make([]struct {
Solution testerv1.SolutionsListItem `json:"solution"`
Task testerv1.TasksListItem `json:"task"`
}, len(tasks)),
}
for i, task := range tasks {
solution := testerv1.SolutionsListItem{}
if sol, ok := m[task.Position]; ok {
solution = SolutionsListItemDTO(*sol)
}
resp.Tasks[i] = struct {
Solution testerv1.SolutionsListItem `json:"solution"`
Task testerv1.TasksListItem `json:"task"`
}{
Solution: solution,
Task: TasksListItemDTO(*task),
}
}
return &resp
}
func ListContestsResponseDTO(contestsList *models.ContestsList) *testerv1.ListContestsResponse {
resp := testerv1.ListContestsResponse{
Contests: make([]testerv1.ContestsListItem, len(contestsList.Contests)),
Pagination: PaginationDTO(contestsList.Pagination),
}
for i, contest := range contestsList.Contests {
resp.Contests[i] = ContestsListItemDTO(*contest)
}
return &resp
}
func ListSolutionsResponseDTO(solutionsList *models.SolutionsList) *testerv1.ListSolutionsResponse {
resp := testerv1.ListSolutionsResponse{
Solutions: make([]testerv1.SolutionsListItem, len(solutionsList.Solutions)),
Pagination: PaginationDTO(solutionsList.Pagination),
}
for i, solution := range solutionsList.Solutions {
resp.Solutions[i] = SolutionsListItemDTO(*solution)
}
return &resp
}
func GetTaskResponseDTO(contest *models.Contest, tasks []*models.TasksListItem, task *models.Task) *testerv1.GetTaskResponse {
resp := testerv1.GetTaskResponse{
Contest: ContestDTO(*contest),
Tasks: make([]testerv1.TasksListItem, len(tasks)),
Task: *TaskDTO(task),
}
for i, t := range tasks {
resp.Tasks[i] = TasksListItemDTO(*t)
}
return &resp
}
func PaginationDTO(p models.Pagination) testerv1.Pagination {
return testerv1.Pagination{
Page: p.Page,
Total: p.Total,
}
}
func ContestDTO(c models.Contest) testerv1.Contest {
return testerv1.Contest{
Id: c.Id,
Title: c.Title,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}
func ContestsListItemDTO(c models.ContestsListItem) testerv1.ContestsListItem {
return testerv1.ContestsListItem{
Id: c.Id,
Title: c.Title,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}
func TasksListItemDTO(t models.TasksListItem) testerv1.TasksListItem {
return testerv1.TasksListItem{
Id: t.Id,
Position: t.Position,
Title: t.Title,
MemoryLimit: t.MemoryLimit,
ProblemId: t.ProblemId,
TimeLimit: t.TimeLimit,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
}
}
func TaskDTO(t *models.Task) *testerv1.Task {
return &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,
}
}
func ParticipantsListItemDTO(p models.ParticipantsListItem) testerv1.ParticipantsListItem {
return testerv1.ParticipantsListItem{
Id: p.Id,
UserId: p.UserId,
Name: p.Name,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}
}
func SolutionsListItemDTO(s models.SolutionsListItem) testerv1.SolutionsListItem {
return testerv1.SolutionsListItem{
Id: s.Id,
ParticipantId: s.ParticipantId,
ParticipantName: s.ParticipantName,
State: s.State,
Score: s.Score,
Penalty: s.Penalty,
TimeStat: s.TimeStat,
MemoryStat: s.MemoryStat,
Language: s.Language,
TaskId: s.TaskId,
TaskPosition: s.TaskPosition,
TaskTitle: s.TaskTitle,
ContestId: s.ContestId,
ContestTitle: s.ContestTitle,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
}
func SolutionDTO(s models.Solution) testerv1.Solution {
return testerv1.Solution{
Id: s.Id,
ParticipantId: s.ParticipantId,
ParticipantName: s.ParticipantName,
Solution: s.Solution,
State: s.State,
Score: s.Score,
Penalty: s.Penalty,
TimeStat: s.TimeStat,
MemoryStat: s.MemoryStat,
Language: s.Language,
TaskId: s.TaskId,
TaskPosition: s.TaskPosition,
TaskTitle: s.TaskTitle,
ContestId: s.ContestId,
ContestTitle: s.ContestTitle,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
}

View file

@ -0,0 +1,71 @@
package rest
import (
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/gofiber/fiber/v2"
)
func (h *Handlers) GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher, models.RoleStudent:
contest, err := h.contestsUC.GetContest(ctx, params.ContestId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
monitor, err := h.contestsUC.GetMonitor(ctx, params.ContestId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
tasks, err := h.contestsUC.GetTasks(ctx, params.ContestId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
resp := testerv1.GetMonitorResponse{
Contest: ContestDTO(*contest),
Tasks: make([]testerv1.TasksListItem, len(tasks)),
Participants: make([]testerv1.ParticipantsStat, len(monitor.Participants)),
SummaryPerProblem: make([]testerv1.ProblemStatSummary, len(monitor.Summary)),
}
for i, participant := range monitor.Participants {
resp.Participants[i] = testerv1.ParticipantsStat{
Id: participant.Id,
Name: participant.Name,
PenaltyInTotal: participant.PenaltyInTotal,
Solutions: make([]testerv1.SolutionsListItem, len(participant.Solutions)),
SolvedInTotal: participant.SolvedInTotal,
}
for j, solution := range participant.Solutions {
resp.Participants[i].Solutions[j] = SolutionsListItemDTO(*solution)
}
}
for i, problem := range monitor.Summary {
resp.SummaryPerProblem[i] = testerv1.ProblemStatSummary{
Id: problem.Id,
Success: problem.Success,
Total: problem.Total,
}
}
for i, task := range tasks {
resp.Tasks[i] = TasksListItemDTO(*task)
}
return c.JSON(resp)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}

View file

@ -0,0 +1,116 @@
package rest
import (
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/gofiber/fiber/v2"
)
func (h *Handlers) CreateParticipant(c *fiber.Ctx, params testerv1.CreateParticipantParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
id, err := h.contestsUC.CreateParticipant(ctx, params.ContestId, params.UserId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.CreateParticipantResponse{
Id: id,
})
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
var req testerv1.UpdateParticipantRequest
err := c.BodyParser(&req)
if err != nil {
return err
}
err = h.contestsUC.UpdateParticipant(ctx, params.ParticipantId, models.ParticipantUpdate{
Name: req.Name,
})
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
err := h.contestsUC.DeleteParticipant(c.Context(), params.ParticipantId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
participantsList, err := h.contestsUC.ListParticipants(c.Context(), models.ParticipantsFilter{
Page: params.Page,
PageSize: params.PageSize,
ContestId: params.ContestId,
})
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
resp := testerv1.ListParticipantsResponse{
Participants: make([]testerv1.ParticipantsListItem, len(participantsList.Participants)),
Pagination: PaginationDTO(participantsList.Pagination),
}
for i, participant := range participantsList.Participants {
resp.Participants[i] = ParticipantsListItemDTO(*participant)
}
return c.JSON(resp)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}

View file

@ -0,0 +1,148 @@
package rest
import (
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/gofiber/fiber/v2"
"io"
)
const (
maxSolutionSize int64 = 10 * 1024 * 1024
)
func (h *Handlers) CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher, models.RoleStudent:
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(ctx, &models.SolutionCreation{
UserId: session.UserId,
TaskId: params.TaskId,
Language: params.Language,
Solution: string(b),
})
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.CreateSolutionResponse{
Id: id,
})
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) GetSolution(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
solution, err := h.contestsUC.GetSolution(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.GetSolutionResponse{Solution: SolutionDTO(*solution)})
case models.RoleStudent:
_, err := h.contestsUC.GetParticipantId3(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
solution, err := h.contestsUC.GetSolution(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.GetSolutionResponse{Solution: SolutionDTO(*solution)})
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
filter := 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,
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
solutionsList, err := h.contestsUC.ListSolutions(ctx, filter)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(ListSolutionsResponseDTO(solutionsList))
case models.RoleStudent:
if params.ContestId == nil {
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
participantId, err := h.contestsUC.GetParticipantId(ctx, *params.ContestId, session.UserId)
if err != nil {
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
// Student cannot view other users' solutions
if params.ParticipantId != nil && *params.ParticipantId != participantId {
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
filter.ParticipantId = &participantId
solutionsList, err := h.contestsUC.ListSolutions(ctx, filter)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(ListSolutionsResponseDTO(solutionsList))
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}

View file

@ -0,0 +1,105 @@
package rest
import (
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/gofiber/fiber/v2"
)
func (h *Handlers) CreateTask(c *fiber.Ctx, params testerv1.CreateTaskParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
id, err := h.contestsUC.CreateTask(ctx, params.ContestId, params.ProblemId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.CreateTaskResponse{
Id: id,
})
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) GetTask(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
contest, err := h.contestsUC.GetContest(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
tasks, err := h.contestsUC.GetTasks(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
t, err := h.contestsUC.GetTask(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(GetTaskResponseDTO(contest, tasks, t))
case models.RoleStudent:
_, err = h.contestsUC.GetParticipantId2(ctx, id, session.UserId)
if err != nil {
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
contest, err := h.contestsUC.GetContest(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
tasks, err := h.contestsUC.GetTasks(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
t, err := h.contestsUC.GetTask(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(GetTaskResponseDTO(contest, tasks, t))
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) DeleteTask(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
err := h.contestsUC.DeleteTask(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}

View file

@ -0,0 +1,34 @@
package contests
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
type Repository interface {
CreateContest(ctx context.Context, title string) (int32, error)
GetContest(ctx context.Context, id int32) (*models.Contest, error)
DeleteContest(ctx context.Context, id int32) error
UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error
ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error)
CreateTask(ctx context.Context, contestId int32, taskId int32) (int32, error)
GetTask(ctx context.Context, id int32) (*models.Task, error)
DeleteTask(ctx context.Context, taskId int32) error
GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error)
GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error)
GetParticipantId2(ctx context.Context, taskId int32, userId int32) (int32, error)
GetParticipantId3(ctx context.Context, solutionId int32) (int32, error)
CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error)
DeleteParticipant(ctx context.Context, participantId int32) error
UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error
ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error)
GetSolution(ctx context.Context, id int32) (*models.Solution, error)
CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error)
ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error)
GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error)
GetMonitor(ctx context.Context, id int32, penalty int32) (*models.Monitor, error)
}

View file

@ -0,0 +1,145 @@
package repository
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
)
type Repository struct {
db *sqlx.DB
}
func NewRepository(db *sqlx.DB) *Repository {
return &Repository{
db: db,
}
}
const CreateContestQuery = "INSERT INTO contests (title) VALUES ($1) RETURNING id"
func (r *Repository) CreateContest(ctx context.Context, title string) (int32, error) {
const op = "Repository.CreateContest"
rows, err := r.db.QueryxContext(ctx, CreateContestQuery, title)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
return id, nil
}
const GetContestQuery = "SELECT * from contests WHERE id=$1 LIMIT 1"
func (r *Repository) GetContest(ctx context.Context, id int32) (*models.Contest, error) {
const op = "Repository.GetContest"
var contest models.Contest
err := r.db.GetContext(ctx, &contest, GetContestQuery, id)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &contest, nil
}
const (
UpdateContestQuery = "UPDATE contests SET title = COALESCE($1, title) WHERE id = $2"
)
func (r *Repository) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error {
const op = "Repository.UpdateContest"
_, err := r.db.ExecContext(ctx, UpdateContestQuery, contestUpdate.Title, id)
if err != nil {
return pkg.HandlePgErr(err, op)
}
return nil
}
const DeleteContestQuery = "DELETE FROM contests WHERE id=$1"
func (r *Repository) DeleteContest(ctx context.Context, id int32) error {
const op = "Repository.DeleteContest"
_, err := r.db.ExecContext(ctx, DeleteContestQuery, id)
if err != nil {
return pkg.HandlePgErr(err, op)
}
return nil
}
func buildListContestsQueries(filter models.ContestsFilter) (sq.SelectBuilder, sq.SelectBuilder) {
columns := []string{
"c.id",
"c.title",
"c.created_at",
"c.updated_at",
}
qb := sq.StatementBuilder.PlaceholderFormat(sq.Dollar).Select(columns...).From("contests c")
if filter.UserId != nil {
qb = qb.Join("participants p ON c.id = p.contest_id")
qb = qb.Where(sq.Eq{"p.user_id": *filter.UserId})
}
countQb := sq.Select("COUNT(*)").FromSelect(qb, "sub")
if filter.Order != nil && *filter.Order < 0 {
qb = qb.OrderBy("c.created_at DESC")
} else {
qb = qb.OrderBy("c.created_at ASC")
}
qb = qb.Limit(uint64(filter.PageSize)).Offset(uint64(filter.Offset()))
return qb, countQb
}
func (r *Repository) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) {
const op = "Repository.ListContests"
baseQb, countQb := buildListContestsQueries(filter)
query, args, err := baseQb.ToSql()
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
var contests []*models.ContestsListItem
err = r.db.SelectContext(ctx, &contests, query, args...)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
query, args, err = countQb.ToSql()
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
var count int32
err = r.db.GetContext(ctx, &count, query, args...)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &models.ContestsList{
Contests: contests,
Pagination: models.Pagination{
Total: models.Total(count, filter.PageSize),
Page: filter.Page,
},
}, nil
}

View file

@ -0,0 +1,116 @@
package repository_test
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/contests/repository"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"github.com/DATA-DOG/go-sqlmock"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
// setupTestDB creates a mocked sqlx.DB and sqlmock instance for testing.
func setupTestDB(t *testing.T) (*sqlx.DB, sqlmock.Sqlmock) {
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
assert.NoError(t, err)
sqlxDB := sqlx.NewDb(db, "sqlmock")
return sqlxDB, mock
}
func TestRepository_CreateContest(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
contest := models.Contest{
Id: 1,
Title: "Test Contest",
}
mock.ExpectQuery(repository.CreateContestQuery).
WithArgs(contest.Title).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(contest.Id))
id, err := repo.CreateContest(ctx, contest.Title)
assert.NoError(t, err)
assert.Equal(t, contest.Id, id)
})
}
func TestRepository_GetContest(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
contest := models.Contest{
Id: 1,
Title: "Test Contest",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
mock.ExpectQuery(repository.GetContestQuery).
WithArgs(contest.Id).
WillReturnRows(sqlmock.NewRows([]string{"id", "title", "created_at", "updated_at"}).
AddRow(contest.Id, contest.Title, contest.CreatedAt, contest.UpdatedAt))
result, err := repo.GetContest(ctx, contest.Id)
assert.NoError(t, err)
assert.EqualExportedValues(t, &contest, result)
})
}
func TestRepository_UpdateContest(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
var contestId int32 = 1
update := models.ContestUpdate{
Title: sp("Updated Contest"),
}
mock.ExpectExec(repository.UpdateContestQuery).
WithArgs(update.Title, contestId).
WillReturnResult(sqlmock.NewResult(0, 1))
err := repo.UpdateContest(ctx, contestId, update)
assert.NoError(t, err)
})
}
func TestRepository_DeleteContest(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
mock.ExpectExec(repository.DeleteContestQuery).
WithArgs(1).
WillReturnResult(sqlmock.NewResult(0, 1))
err := repo.DeleteContest(ctx, 1)
assert.NoError(t, err)
})
}
func sp(s string) *string {
return &s
}

View file

@ -0,0 +1,161 @@
package repository
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
)
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 = $1
GROUP BY t.id, t.position
ORDER BY t.position;
`
SolutionsQuery = `
WITH RankedSolutions AS (
SELECT
s.id,
s.participant_id,
p2.name as participant_name,
s.state,
s.score,
s.penalty,
s.time_stat,
s.memory_stat,
s.language,
s.task_id,
t.position as task_position,
p.title as task_title,
t.contest_id,
c.title as contest_title,
s.updated_at,
s.created_at,
ROW_NUMBER() OVER (
PARTITION BY s.task_id, s.participant_id
ORDER BY s.score DESC, s.created_at
) as rn
FROM solutions s
LEFT JOIN tasks t ON s.task_id = t.id
LEFT JOIN problems p ON t.problem_id = p.id
LEFT JOIN contests c ON t.contest_id = c.id
LEFT JOIN participants p2 on s.participant_id = p2.id
WHERE t.contest_id = $1
)
SELECT
rs.id,
rs.participant_id,
rs.participant_name,
rs.state,
rs.score,
rs.penalty,
rs.time_stat,
rs.memory_stat,
rs.language,
rs.task_id,
rs.task_position,
rs.task_title,
rs.contest_id,
rs.contest_title,
rs.updated_at,
rs.created_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 = $1
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(a.failed_attempts), 0) * $2 + COALESCE(SUM(a.success_penalty), 0) as penalty_in_total
FROM participants p LEFT JOIN Attempts a ON a.participant_id = p.id
WHERE p.contest_id = $1
GROUP BY p.id, p.name
`
)
func (r *Repository) GetMonitor(ctx context.Context, contestId int32, penalty int32) (*models.Monitor, error) {
const op = "Repository.GetMonitor"
rows, err := r.db.QueryxContext(ctx, ReadStatisticsQuery, contestId)
if err != nil {
return nil, pkg.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, pkg.HandlePgErr(err, op)
}
monitor.Summary = append(monitor.Summary, &stat)
}
var solutions []*models.SolutionsListItem
err = r.db.SelectContext(ctx, &solutions, SolutionsQuery, contestId)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
rows3, err := r.db.QueryxContext(ctx, ParticipantsQuery, contestId, penalty)
if err != nil {
return nil, pkg.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, pkg.HandlePgErr(err, op)
}
if sols, ok := solutionsMap[stat.Id]; ok {
stat.Solutions = sols
}
monitor.Participants = append(monitor.Participants, &stat)
}
return &monitor, nil
}

View file

@ -0,0 +1 @@
package repository

View file

@ -0,0 +1,126 @@
package repository
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
)
const GetParticipantIdQuery = "SELECT id FROM participants WHERE user_id=$1 AND contest_id=$2 LIMIT 1"
func (r *Repository) GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error) {
const op = "Repository.GetParticipantId"
var participantId int32
err := r.db.GetContext(ctx, &participantId, GetParticipantIdQuery, userId, contestId)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
return participantId, nil
}
const GetParticipantId2Query = "SELECT p.id FROM participants p JOIN tasks t ON p.contest_id=t.contest_id WHERE user_id=$1 AND t.id=$2 LIMIT 1"
func (r *Repository) GetParticipantId2(ctx context.Context, taskId int32, userId int32) (int32, error) {
const op = "Repository.GetParticipantId2"
var participantId int32
err := r.db.GetContext(ctx, &participantId, GetParticipantId2Query, userId, taskId)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
return participantId, nil
}
const GetParticipantId3Query = "SELECT participant_id FROM solutions WHERE id=$1 LIMIT 1"
func (r *Repository) GetParticipantId3(ctx context.Context, solutionId int32) (int32, error) {
const op = "Repository.GetParticipantId3"
var participantId int32
err := r.db.GetContext(ctx, &participantId, GetParticipantId3Query, solutionId)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
return participantId, nil
}
const CreateParticipantQuery = "INSERT INTO participants (user_id, contest_id, name) VALUES ($1, $2, $3) RETURNING id"
func (r *Repository) CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error) {
const op = "Repository.CreateParticipant"
name := ""
rows, err := r.db.QueryxContext(ctx, CreateParticipantQuery, userId, contestId, name)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, err
}
return id, nil
}
const DeleteParticipantQuery = "DELETE FROM participants WHERE id=$1"
const (
UpdateParticipantQuery = "UPDATE participants SET name = COALESCE($1, name) WHERE id = $2"
)
func (r *Repository) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error {
const op = "Repository.UpdateParticipant"
_, err := r.db.ExecContext(ctx, UpdateParticipantQuery, participantUpdate.Name, id)
if err != nil {
return pkg.HandlePgErr(err, op)
}
return nil
}
func (r *Repository) DeleteParticipant(ctx context.Context, participantId int32) error {
const op = "Repository.DeleteParticipant"
_, err := r.db.ExecContext(ctx, DeleteParticipantQuery, participantId)
if err != nil {
return pkg.HandlePgErr(err, op)
}
return nil
}
const (
ReadParticipantsListQuery = `SELECT id, user_id, name, created_at, updated_at FROM participants WHERE contest_id = $1 LIMIT $2 OFFSET $3`
CountParticipantsQuery = "SELECT COUNT(*) FROM participants WHERE contest_id = $1"
)
func (r *Repository) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) {
const op = "Repository.ReadParticipants"
var participants []*models.ParticipantsListItem
err := r.db.SelectContext(ctx, &participants,
ReadParticipantsListQuery, filter.ContestId, filter.PageSize, filter.Offset())
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
var count int32
err = r.db.GetContext(ctx, &count, CountParticipantsQuery, filter.ContestId)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &models.ParticipantsList{
Participants: participants,
Pagination: models.Pagination{
Total: models.Total(count, filter.PageSize),
Page: filter.Page,
},
}, nil
}

View file

@ -0,0 +1,51 @@
package repository_test
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/contests/repository"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"testing"
)
func TestRepository_CreateParticipant(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
var (
expectedId int32 = 1
userId int32 = 2
contestId int32 = 3
)
ctx := context.Background()
mock.ExpectQuery(repository.CreateParticipantQuery).
WithArgs(userId, contestId).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedId))
id, err := repo.CreateParticipant(ctx, contestId, userId)
assert.NoError(t, err)
assert.Equal(t, expectedId, id)
})
}
func TestRepository_DeleteParticipant(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
var participantId int32 = 1
mock.ExpectExec(repository.DeleteParticipantQuery).
WithArgs(participantId).WillReturnResult(sqlmock.NewResult(0, 1))
err := repo.DeleteParticipant(ctx, participantId)
assert.NoError(t, err)
})
}

View file

@ -0,0 +1,222 @@
package repository
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
sq "github.com/Masterminds/squirrel"
)
const (
GetSolutionQuery = "SELECT * FROM solutions WHERE id = $1"
)
func (r *Repository) GetSolution(ctx context.Context, id int32) (*models.Solution, error) {
const op = "Repository.GetSolution"
var solution models.Solution
err := r.db.GetContext(ctx, &solution, GetSolutionQuery, id)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &solution, nil
}
const (
CreateSolutionQuery = `INSERT INTO solutions (task_id, participant_id, language, penalty, solution)
VALUES ($1, $2, $3, $4, $5)
RETURNING id`
)
func (r *Repository) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
const op = "Repository.CreateSolution"
rows, err := r.db.QueryxContext(ctx,
CreateSolutionQuery,
creation.TaskId,
creation.ParticipantId,
creation.Language,
creation.Penalty,
creation.Solution,
)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
return id, nil
}
func buildListSolutionsQueries(filter models.SolutionsFilter) (sq.SelectBuilder, sq.SelectBuilder) {
columns := []string{
"s.id",
"s.participant_id",
"p2.name AS participant_name",
"s.state",
"s.score",
"s.penalty",
"s.time_stat",
"s.memory_stat",
"s.language",
"s.task_id",
"t.position AS task_position",
"p.title AS task_title",
"t.contest_id",
"c.title",
"s.updated_at",
"s.created_at",
}
qb := sq.StatementBuilder.PlaceholderFormat(sq.Dollar).Select(columns...).
From("solutions s").
LeftJoin("tasks t ON s.task_id = t.id").
LeftJoin("problems p ON t.problem_id = p.id").
LeftJoin("contests c ON t.contest_id = c.id").
LeftJoin("participants p2 ON s.participant_id = p2.id")
if filter.ContestId != nil {
qb = qb.Where(sq.Eq{"s.contest_id": *filter.ContestId})
}
if filter.ParticipantId != nil {
qb = qb.Where(sq.Eq{"s.participant_id": *filter.ParticipantId})
}
if filter.TaskId != nil {
qb = qb.Where(sq.Eq{"s.task_id": *filter.TaskId})
}
if filter.Language != nil {
qb = qb.Where(sq.Eq{"s.language": *filter.Language})
}
if filter.State != nil {
qb = qb.Where(sq.Eq{"s.state": *filter.State})
}
countQb := sq.Select("COUNT(*)").FromSelect(qb, "sub")
if filter.Order != nil && *filter.Order < 0 {
qb = qb.OrderBy("s.id DESC")
} else {
qb = qb.OrderBy("s.id ASC")
}
qb = qb.Limit(uint64(filter.PageSize)).Offset(uint64(filter.Offset()))
return qb, countQb
}
func (r *Repository) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) {
const op = "ContestRepository.ListSolutions"
baseQb, countQb := buildListSolutionsQueries(filter)
query, args, err := countQb.ToSql()
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
var totalCount int32
err = r.db.GetContext(ctx, &totalCount, query, args...)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
query, args, err = baseQb.ToSql()
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
rows, err := r.db.QueryxContext(ctx, query, args...)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
defer rows.Close()
solutions := make([]*models.SolutionsListItem, 0)
for rows.Next() {
var solution models.SolutionsListItem
err = rows.StructScan(&solution)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
solutions = append(solutions, &solution)
}
if err = rows.Err(); err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &models.SolutionsList{
Solutions: solutions,
Pagination: models.Pagination{
Total: models.Total(totalCount, filter.PageSize),
Page: filter.Page,
},
}, nil
}
const (
// state=5 - AC
GetBestSolutions = `
WITH contest_tasks AS (
SELECT t.id AS task_id,
t.position AS task_position,
t.contest_id,
t.problem_id,
t.created_at,
t.updated_at,
p.title AS task_title,
c.title AS contest_title
FROM tasks t
LEFT JOIN problems p ON p.id = t.problem_id
LEFT JOIN contests c ON c.id = t.contest_id
WHERE t.contest_id = ?
),
best_solutions AS (
SELECT DISTINCT ON (s.task_id)
*
FROM solutions s
WHERE s.participant_id = ?
ORDER BY s.task_id, s.score DESC, s.created_at DESC
)
SELECT
s.id,
s.participant_id,
p.name AS participant_name,
s.state,
s.score,
s.penalty,
s.time_stat,
s.memory_stat,
s.language,
ct.task_id,
ct.task_position,
ct.task_title,
ct.contest_id,
ct.contest_title,
s.updated_at,
s.created_at
FROM contest_tasks ct
LEFT JOIN best_solutions s ON s.task_id = ct.task_id
LEFT JOIN participants p ON p.id = s.participant_id WHERE s.id IS NOT NULL
ORDER BY ct.task_position
`
)
func (r *Repository) GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error) {
const op = "Repository.GetBestSolutions"
var solutions []*models.SolutionsListItem
query := r.db.Rebind(GetBestSolutions)
err := r.db.SelectContext(ctx, &solutions, query, contestId, participantId)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return solutions, nil
}

View file

@ -0,0 +1 @@
package repository

View file

@ -0,0 +1,101 @@
package repository
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
)
const CreateTaskQuery = `INSERT INTO tasks (problem_id, contest_id, position)
VALUES ($1, $2, COALESCE((SELECT MAX(position) FROM tasks WHERE contest_id = $2), 0) + 1)
RETURNING id
`
func (r *Repository) CreateTask(ctx context.Context, contestId int32, problemId int32) (int32, error) {
const op = "Repository.AddTask"
rows, err := r.db.QueryxContext(ctx, CreateTaskQuery, problemId, contestId)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
return id, nil
}
const DeleteTaskQuery = "DELETE FROM tasks WHERE id=$1"
func (r *Repository) DeleteTask(ctx context.Context, taskId int32) error {
const op = "Repository.DeleteTask"
_, err := r.db.ExecContext(ctx, DeleteTaskQuery, taskId)
if err != nil {
return pkg.HandlePgErr(err, op)
}
return nil
}
const GetTasksQuery = `SELECT tasks.id,
problem_id,
contest_id,
position,
title,
memory_limit,
time_limit,
tasks.created_at,
tasks.updated_at
FROM tasks
INNER JOIN problems ON tasks.problem_id = problems.id
WHERE contest_id = $1 ORDER BY position`
func (r *Repository) GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) {
const op = "Repository.ReadTasks"
var tasks []*models.TasksListItem
err := r.db.SelectContext(ctx, &tasks, GetTasksQuery, contestId)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return tasks, nil
}
const (
GetTaskQuery = `
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 *Repository) GetTask(ctx context.Context, id int32) (*models.Task, error) {
const op = "Repository.ReadTask"
query := r.db.Rebind(GetTaskQuery)
var task models.Task
err := r.db.GetContext(ctx, &task, query, id)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &task, nil
}

View file

@ -0,0 +1,51 @@
package repository_test
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/contests/repository"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"testing"
)
func TestRepository_CreateTask(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
var (
expectedId int32 = 1
problemId int32 = 2
contestId int32 = 3
)
ctx := context.Background()
mock.ExpectQuery(repository.CreateTaskQuery).
WithArgs(problemId, contestId).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedId))
id, err := repo.CreateTask(ctx, contestId, problemId)
assert.NoError(t, err)
assert.Equal(t, expectedId, id)
})
}
func TestRepository_DeleteTask(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
mock.ExpectExec(repository.DeleteTaskQuery).
WithArgs(1).
WillReturnResult(sqlmock.NewResult(0, 1))
err := repo.DeleteTask(ctx, 1)
assert.NoError(t, err)
})
}

View file

@ -0,0 +1,34 @@
package contests
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
type UseCase interface {
CreateContest(ctx context.Context, title string) (int32, error)
GetContest(ctx context.Context, id int32) (*models.Contest, error)
DeleteContest(ctx context.Context, id int32) error
ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error)
UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error
CreateTask(ctx context.Context, contestId int32, taskId int32) (int32, error)
DeleteTask(ctx context.Context, taskId int32) error
GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error)
GetTask(ctx context.Context, id int32) (*models.Task, error)
CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error)
GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error)
GetParticipantId2(ctx context.Context, taskId, userId int32) (int32, error)
GetParticipantId3(ctx context.Context, solutionId int32) (int32, error)
UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error
DeleteParticipant(ctx context.Context, participantId int32) error
ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error)
GetSolution(ctx context.Context, id int32) (*models.Solution, error)
CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error)
ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error)
GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error)
GetMonitor(ctx context.Context, id int32) (*models.Monitor, error)
}

View file

@ -0,0 +1,39 @@
package usecase
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/contests"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
type ContestUseCase struct {
contestRepo contests.Repository
}
func NewContestUseCase(
contestRepo contests.Repository,
) *ContestUseCase {
return &ContestUseCase{
contestRepo: contestRepo,
}
}
func (uc *ContestUseCase) CreateContest(ctx context.Context, title string) (int32, error) {
return uc.contestRepo.CreateContest(ctx, title)
}
func (uc *ContestUseCase) GetContest(ctx context.Context, id int32) (*models.Contest, error) {
return uc.contestRepo.GetContest(ctx, id)
}
func (uc *ContestUseCase) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error {
return uc.contestRepo.UpdateContest(ctx, id, contestUpdate)
}
func (uc *ContestUseCase) DeleteContest(ctx context.Context, id int32) error {
return uc.contestRepo.DeleteContest(ctx, id)
}
func (uc *ContestUseCase) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) {
return uc.contestRepo.ListContests(ctx, filter)
}

View file

@ -0,0 +1,10 @@
package usecase
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
func (uc *ContestUseCase) GetMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) {
return uc.contestRepo.GetMonitor(ctx, contestId, 20)
}

View file

@ -0,0 +1,34 @@
package usecase
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
func (uc *ContestUseCase) GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error) {
return uc.contestRepo.GetParticipantId(ctx, contestId, userId)
}
func (uc *ContestUseCase) GetParticipantId2(ctx context.Context, taskId, userId int32) (int32, error) {
return uc.contestRepo.GetParticipantId2(ctx, taskId, userId)
}
func (uc *ContestUseCase) GetParticipantId3(ctx context.Context, solutionId int32) (int32, error) {
return uc.contestRepo.GetParticipantId3(ctx, solutionId)
}
func (uc *ContestUseCase) CreateParticipant(ctx context.Context, contestId int32, userId int32) (id int32, err error) {
return uc.contestRepo.CreateParticipant(ctx, contestId, userId)
}
func (uc *ContestUseCase) DeleteParticipant(ctx context.Context, participantId int32) error {
return uc.contestRepo.DeleteParticipant(ctx, participantId)
}
func (uc *ContestUseCase) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) {
return uc.contestRepo.ListParticipants(ctx, filter)
}
func (uc *ContestUseCase) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error {
return uc.contestRepo.UpdateParticipant(ctx, id, participantUpdate)
}

View file

@ -0,0 +1,29 @@
package usecase
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
func (uc *ContestUseCase) GetSolution(ctx context.Context, id int32) (*models.Solution, error) {
return uc.contestRepo.GetSolution(ctx, id)
}
func (uc *ContestUseCase) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
participantId, err := uc.contestRepo.GetParticipantId2(ctx, creation.TaskId, creation.UserId)
if err != nil {
return 0, err
}
creation.ParticipantId = participantId
return uc.contestRepo.CreateSolution(ctx, creation)
}
func (uc *ContestUseCase) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) {
return uc.contestRepo.ListSolutions(ctx, filter)
}
func (uc *ContestUseCase) GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error) {
return uc.contestRepo.GetBestSolutions(ctx, contestId, participantId)
}

View file

@ -0,0 +1,22 @@
package usecase
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
func (uc *ContestUseCase) CreateTask(ctx context.Context, contestId int32, taskId int32) (id int32, err error) {
return uc.contestRepo.CreateTask(ctx, contestId, taskId)
}
func (uc *ContestUseCase) GetTask(ctx context.Context, id int32) (*models.Task, error) {
return uc.contestRepo.GetTask(ctx, id)
}
func (uc *ContestUseCase) GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) {
return uc.contestRepo.GetTasks(ctx, contestId)
}
func (uc *ContestUseCase) DeleteTask(ctx context.Context, taskId int32) error {
return uc.contestRepo.DeleteTask(ctx, taskId)
}