feat: merge auth&tester
This commit is contained in:
parent
0a2dea6c23
commit
441af4c6a2
72 changed files with 4910 additions and 2378 deletions
25
internal/contests/delivery.go
Normal file
25
internal/contests/delivery.go
Normal 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
|
||||
}
|
202
internal/contests/delivery/rest/contests_handlers.go
Normal file
202
internal/contests/delivery/rest/contests_handlers.go
Normal 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))
|
||||
}
|
||||
}
|
202
internal/contests/delivery/rest/dto.go
Normal file
202
internal/contests/delivery/rest/dto.go
Normal 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,
|
||||
}
|
||||
}
|
71
internal/contests/delivery/rest/monitor_handlers.go
Normal file
71
internal/contests/delivery/rest/monitor_handlers.go
Normal 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))
|
||||
}
|
||||
}
|
116
internal/contests/delivery/rest/participants_handlers.go
Normal file
116
internal/contests/delivery/rest/participants_handlers.go
Normal 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))
|
||||
}
|
||||
}
|
148
internal/contests/delivery/rest/solutions_handlers.go
Normal file
148
internal/contests/delivery/rest/solutions_handlers.go
Normal 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))
|
||||
}
|
||||
}
|
105
internal/contests/delivery/rest/tasks_handlers.go
Normal file
105
internal/contests/delivery/rest/tasks_handlers.go
Normal 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))
|
||||
}
|
||||
}
|
34
internal/contests/pg_repository.go
Normal file
34
internal/contests/pg_repository.go
Normal 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)
|
||||
}
|
145
internal/contests/repository/contests_pg_repository.go
Normal file
145
internal/contests/repository/contests_pg_repository.go
Normal 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
|
||||
}
|
116
internal/contests/repository/contests_pg_repository_test.go
Normal file
116
internal/contests/repository/contests_pg_repository_test.go
Normal 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
|
||||
}
|
161
internal/contests/repository/monitor_pg_repository.go
Normal file
161
internal/contests/repository/monitor_pg_repository.go
Normal 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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package repository
|
126
internal/contests/repository/participants_pg_repository.go
Normal file
126
internal/contests/repository/participants_pg_repository.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
222
internal/contests/repository/solutions_pg_repository.go
Normal file
222
internal/contests/repository/solutions_pg_repository.go
Normal 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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package repository
|
101
internal/contests/repository/tasks_pg_repository.go
Normal file
101
internal/contests/repository/tasks_pg_repository.go
Normal 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
|
||||
}
|
51
internal/contests/repository/tasks_pg_repository_test.go
Normal file
51
internal/contests/repository/tasks_pg_repository_test.go
Normal 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)
|
||||
})
|
||||
}
|
34
internal/contests/usecase.go
Normal file
34
internal/contests/usecase.go
Normal 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)
|
||||
}
|
39
internal/contests/usecase/contests_usecase.go
Normal file
39
internal/contests/usecase/contests_usecase.go
Normal 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)
|
||||
}
|
10
internal/contests/usecase/monitor_usecase.go
Normal file
10
internal/contests/usecase/monitor_usecase.go
Normal 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)
|
||||
}
|
34
internal/contests/usecase/participants_usecase.go
Normal file
34
internal/contests/usecase/participants_usecase.go
Normal 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)
|
||||
}
|
29
internal/contests/usecase/solutions_usecase.go
Normal file
29
internal/contests/usecase/solutions_usecase.go
Normal 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)
|
||||
}
|
22
internal/contests/usecase/tasks_usecase.go
Normal file
22
internal/contests/usecase/tasks_usecase.go
Normal 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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue