Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions internal/controllers/contest-controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,60 @@ func (cc *ContestController) GetContestRegistrations(ctx echo.Context) error {

return ctx.JSON(http.StatusOK, registrations)
}

func (cc *ContestController) GetProblemTestcases(ctx echo.Context) error {
contestID := ctx.Param("contestid")
problemID := ctx.Param("problemid")
userID := ctx.Get(common.AUTH_USER_ID).(string)

err := cc.contestService.GetProblemVisibility(ctx.Request().Context(), contestID, userID)
if err != nil {
if err == common.ContestNotFoundError {
return ctx.JSON(http.StatusNotFound, map[string]string{
"error": common.ContestNotFoundError.Error(),
})
} else if err == common.UserNotRegisteredError ||
err == common.ContestNotRunningError {
return ctx.JSON(http.StatusForbidden, map[string]string{
"error": err.Error(),
})
}

return ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": common.FetchContestFailedError.Error(),
})
}

testcases, err := cc.contestService.GetProblemTestcases(ctx.Request().Context(), contestID, problemID)
if err != nil {
if err == common.ContestNotFoundError {
return ctx.JSON(http.StatusNotFound, map[string]string{
"error": common.ContestNotFoundError.Error(),
})
}
return ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": "failed to load problem testcases",
})
}

return ctx.JSON(http.StatusOK, testcases)
}

func (cc *ContestController) GetProblemAnswers(ctx echo.Context) error {
contestID := ctx.Param("contestid")
problemID := ctx.Param("problemid")

answers, err := cc.contestService.GetProblemAnswers(ctx.Request().Context(), contestID, problemID)
if err != nil {
if err == common.ContestNotFoundError {
return ctx.JSON(http.StatusNotFound, map[string]string{
"error": common.ContestNotFoundError.Error(),
})
}
return ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": "failed to get problem answers",
})
}

return ctx.JSON(http.StatusOK, answers)
}
1 change: 1 addition & 0 deletions internal/migrations/000018_problem_add_testcases.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE problems DROP COLUMN testcases;
1 change: 1 addition & 0 deletions internal/migrations/000018_problem_add_testcases.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE problems ADD COLUMN testcases TEXT;
36 changes: 25 additions & 11 deletions internal/models/dto/problem-dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,32 @@ type ProblemOverview struct {
}

type GetProblemStatementResponse struct {
ProblemID string `json:"problem_id"`
ContestID string `json:"contest_id"`
Name string `json:"name"`
Description string `json:"description"`
Score int `json:"score"`
Type models.SubmissionType `json:"type"`
ProblemID string `json:"problem_id"`
ContestID string `json:"contest_id"`
Name string `json:"name"`
Description string `json:"description"`
Score int `json:"score"`
Type models.SubmissionType `json:"type"`
Testcases []TestCaseResponse `json:"testcases,omitempty"`
TestcasesKey string `json:"-"`
}

type CreateProblemRequest struct {
Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"`
Score int `json:"score" validate:"required,gt=0"`
Type models.SubmissionType `json:"type" validate:"required,oneof=mcq code"`
Answer []int `json:"answer"` // required only for MCQ
Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"`
Score int `json:"score" validate:"required,gt=0"`
Type models.SubmissionType `json:"type" validate:"required,oneof=mcq code"`
Answer []int `json:"answer"` // required only for MCQ
Testcases []CreateTestCaseRequest `json:"testcases,omitempty"`
}

type CreateTestCaseRequest struct {
Input string `json:"input" validate:"required"`
ExpectedOutput string `json:"expected_output" validate:"required"`
}

type TestCaseResponse struct {
Index int `json:"index"`
Input string `json:"input"`
ExpectedOutput string `json:"expected_output,omitempty"`
}
1 change: 1 addition & 0 deletions internal/models/problem.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ type Problem struct {
Type SubmissionType `json:"type"` // "code" or "mcq"
HasMultipleAnswers bool `json:"has_multiple_answers"`
Answer []int `json:"answer"`
Testcases string `json:"testcases"`
}
2 changes: 2 additions & 0 deletions internal/routes/admin-routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ func AddAdminRoutes(
adminGroup.POST("/:contestid/problem", contestController.HandleCreateProblem)
adminGroup.PUT("/:contestid/:problemid", contestController.HandleUpdateProblem)
adminGroup.DELETE("/:contestid/:problemid", contestController.HandleDeleteProblem)
adminGroup.GET("/:contestid/:problemid/testcases", contestController.GetProblemTestcases)
adminGroup.GET("/:contestid/:problemid/answers", contestController.GetProblemAnswers)

//Leaderboard/User Management
adminGroup.PUT("/:contestid/leaderboard/:userid", contestController.HandleUpdateLeaderboardUser)
Expand Down
45 changes: 45 additions & 0 deletions internal/s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,48 @@ func (s *S3) DeleteObject(ctx context.Context, key string) error {
}
return nil
}

func (s *S3) ListObjects(ctx context.Context, prefix string) ([]string, error) {
var keys []string

paginator := s3.NewListObjectsV2Paginator(s.client, &s3.ListObjectsV2Input{
Bucket: aws.String(s.Bucket),
Prefix: aws.String(prefix),
})

for paginator.HasMorePages() {
out, err := paginator.NextPage(ctx)
if err != nil {
log.Errorf("s3: list objects failed: %v", err)
return nil, err
}
for _, obj := range out.Contents {
if obj.Key != nil {
keys = append(keys, *obj.Key)
}
}
}
return keys, nil
}

func (s *S3) DeletePrefix(ctx context.Context, prefix string) error {
out, err := s.client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
Bucket: aws.String(s.Bucket),
Prefix: aws.String(prefix),
})
if err != nil {
return err
}

for _, obj := range out.Contents {
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(s.Bucket),
Key: obj.Key,
})
if err != nil {
return err
}
}

return nil
}
143 changes: 135 additions & 8 deletions internal/services/contest-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"context"
"encoding/json"
"slices"
"strings"

"fmt"

Expand Down Expand Up @@ -104,7 +105,7 @@ func (cs *ContestService) CreateProblem(ctx context.Context, contestID string, r
HasMultipleAnswers: req.Type == "mcq" && len(req.Answer) > 1,
}

s3Key := fmt.Sprintf("problems/%s/%s.json", problem.ContestID, problem.ID)
s3Key := fmt.Sprintf("problems/%s/%s/description.json", contestID, problem.ID)

payload := map[string]string{
"description": req.Description,
Expand All @@ -118,6 +119,37 @@ func (cs *ContestService) CreateProblem(ctx context.Context, contestID string, r

problem.Description = s3Key

if req.Type == "code" {

testcasesKey := fmt.Sprintf("problems/%s/%s/testcases.json", contestID, problem.ID)
answersKey := fmt.Sprintf("problems/%s/%s/answers.json", contestID, problem.ID)

var tcArray []map[string]interface{}
var ansArray []string

for i, tc := range req.Testcases {
tcArray = append(tcArray, map[string]interface{}{
"index": i,
"input": tc.Input,
})

ansArray = append(ansArray, tc.ExpectedOutput)
}

tcBytes, _ := json.Marshal(tcArray)
ansBytes, _ := json.Marshal(ansArray)

if err := cs.s3.PutObject(ctx, testcasesKey, string(tcBytes)); err != nil {
return nil, err
}

if err := cs.s3.PutObject(ctx, answersKey, string(ansBytes)); err != nil {
return nil, err
}

problem.Testcases = testcasesKey
}

if err := cs.stores.Problems.CreateProblem(ctx, problem); err != nil {
return nil, err
}
Expand Down Expand Up @@ -146,6 +178,36 @@ func (cs *ContestService) UpdateProblem(ctx context.Context, contestID string, p
return nil, err
}

var testcasesKey string = meta.TestcasesKey

if req.Type == "code" && len(req.Testcases) > 0 {

testcasesKey = fmt.Sprintf("problems/%s/%s/testcases.json", contestID, problemID)
answersKey := fmt.Sprintf("problems/%s/%s/answers.json", contestID, problemID)

var tcArray []map[string]interface{}
var ansArray []string

for i, tc := range req.Testcases {
tcArray = append(tcArray, map[string]interface{}{
"index": i,
"input": tc.Input,
})

ansArray = append(ansArray, tc.ExpectedOutput)
}

tcBytes, _ := json.Marshal(tcArray)
if err := cs.s3.PutObjectOverwrite(ctx, testcasesKey, string(tcBytes)); err != nil {
return nil, err
}

ansBytes, _ := json.Marshal(ansArray)
if err := cs.s3.PutObjectOverwrite(ctx, answersKey, string(ansBytes)); err != nil {
return nil, err
}
}

hasMultiple := req.Type == "mcq" && len(req.Answer) > 1

problem := &models.Problem{
Expand All @@ -157,6 +219,7 @@ func (cs *ContestService) UpdateProblem(ctx context.Context, contestID string, p
Type: req.Type,
Answer: req.Answer,
HasMultipleAnswers: hasMultiple,
Testcases: testcasesKey,
}

if err := cs.stores.Problems.UpdateProblem(ctx, problem); err != nil {
Expand All @@ -167,19 +230,19 @@ func (cs *ContestService) UpdateProblem(ctx context.Context, contestID string, p

func (cs *ContestService) DeleteProblem(ctx context.Context, contestID string, problemID string) error {

meta, err := cs.stores.Problems.GetProblem(ctx, problemID, contestID)
_, err := cs.stores.Problems.GetProblem(ctx, problemID, contestID)
if err != nil {
return err
}

s3Key := meta.Description

if err := cs.stores.Problems.DeleteProblem(ctx, contestID, problemID); err != nil {
return err
}

if err := cs.s3.DeleteObject(ctx, s3Key); err != nil {
log.Errorf("failed to delete S3 file for problem %s: %v", problemID, err)
prefix := fmt.Sprintf("problems/%s/%s/", contestID, problemID)

if err := cs.s3.DeleteObject(ctx, prefix); err != nil {
log.Errorf("failed to delete S3 folder for problem %s: %v", problemID, err)
}

return nil
Expand Down Expand Up @@ -220,15 +283,29 @@ func (cs *ContestService) GetContestProblem(ctx context.Context, contestID strin
return nil, err
}

s3Key := meta.Description
descKey := meta.Description

desc, err := cs.s3.GetObject(ctx, s3Key)
desc, err := cs.s3.GetObject(ctx, descKey)
if err != nil {
return nil, err
}

meta.Description = desc

testcaseKey := meta.TestcasesKey

testcase, err := cs.s3.GetObject(ctx, testcaseKey)
if err != nil {
return meta, nil
}

var tcArr []dto.TestCaseResponse
if err := json.Unmarshal([]byte(testcase), &tcArr); err != nil {
return nil, err
}

meta.Testcases = tcArr

return meta, nil
}

Expand All @@ -254,3 +331,53 @@ func (cs *ContestService) GetContest(ctx context.Context, contestID string, user
func (cs *ContestService) GetContestRegistrations(ctx context.Context, contestID string) ([]dto.ContestRegistration, error) {
return cs.stores.Contests.GetContestRegistrations(ctx, contestID)
}

func (cs *ContestService) GetProblemTestcases(ctx context.Context, contestID, problemID string) ([]dto.TestCaseResponse, error) {

meta, err := cs.stores.Problems.GetProblem(ctx, problemID, contestID)
if err != nil {
return nil, err
}

if meta.TestcasesKey == "" {
return []dto.TestCaseResponse{}, nil
}

raw, err := cs.s3.GetObject(ctx, meta.TestcasesKey)
if err != nil {
return nil, err
}

var arr []dto.TestCaseResponse
if err := json.Unmarshal([]byte(raw), &arr); err != nil {
return nil, err
}

return arr, nil
}

func (cs *ContestService) GetProblemAnswers(ctx context.Context, contestID, problemID string) ([]string, error) {

meta, err := cs.stores.Problems.GetProblem(ctx, problemID, contestID)
if err != nil {
return nil, err
}

if meta.TestcasesKey == "" {
return []string{}, nil
}

answersKey := strings.Replace(meta.TestcasesKey, "testcases.json", "answers.json", 1)

raw, err := cs.s3.GetObject(ctx, answersKey)
if err != nil {
return nil, err
}

var arr []string
if err := json.Unmarshal([]byte(raw), &arr); err != nil {
return nil, err
}

return arr, nil
}
Loading