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
162 changes: 146 additions & 16 deletions internal/api/http/routes/contests_management.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,34 @@ import (
)

type ContestsManagementRoute interface {
AddGroupToContest(w http.ResponseWriter, r *http.Request)
AddParticipantsToContest(w http.ResponseWriter, r *http.Request)
AddTaskToContest(w http.ResponseWriter, r *http.Request)
ApproveRegistrationRequest(w http.ResponseWriter, r *http.Request)
CreateContest(w http.ResponseWriter, r *http.Request)
EditContest(w http.ResponseWriter, r *http.Request)
DeleteContest(w http.ResponseWriter, r *http.Request)
GetContestTasks(w http.ResponseWriter, r *http.Request)
EditContest(w http.ResponseWriter, r *http.Request)
EditContestTask(w http.ResponseWriter, r *http.Request)
GetAllContests(w http.ResponseWriter, r *http.Request)
GetAssignableGroups(w http.ResponseWriter, r *http.Request)
GetAssignableParticipants(w http.ResponseWriter, r *http.Request)
GetAssignableTasks(w http.ResponseWriter, r *http.Request)
AddTaskToContest(w http.ResponseWriter, r *http.Request)
RemoveTaskFromContest(w http.ResponseWriter, r *http.Request)
GetRegistrationRequests(w http.ResponseWriter, r *http.Request)
ApproveRegistrationRequest(w http.ResponseWriter, r *http.Request)
RejectRegistrationRequest(w http.ResponseWriter, r *http.Request)
GetContestGroups(w http.ResponseWriter, r *http.Request)
GetContestParticipants(w http.ResponseWriter, r *http.Request)
GetContestSubmissions(w http.ResponseWriter, r *http.Request)
GetCreatedContests(w http.ResponseWriter, r *http.Request)
GetManageableContests(w http.ResponseWriter, r *http.Request)
GetAllContests(w http.ResponseWriter, r *http.Request)
GetContestTask(w http.ResponseWriter, r *http.Request)
GetContestTaskStats(w http.ResponseWriter, r *http.Request)
GetContestTaskUserStats(w http.ResponseWriter, r *http.Request)
GetContestTaskUserSubmissions(w http.ResponseWriter, r *http.Request)
GetContestTasks(w http.ResponseWriter, r *http.Request)
GetContestUserStats(w http.ResponseWriter, r *http.Request)
AddGroupToContest(w http.ResponseWriter, r *http.Request)
GetCreatedContests(w http.ResponseWriter, r *http.Request)
GetManageableContests(w http.ResponseWriter, r *http.Request)
GetRegistrationRequests(w http.ResponseWriter, r *http.Request)
RejectRegistrationRequest(w http.ResponseWriter, r *http.Request)
RemoveGroupFromContest(w http.ResponseWriter, r *http.Request)
GetContestGroups(w http.ResponseWriter, r *http.Request)
GetAssignableGroups(w http.ResponseWriter, r *http.Request)
GetContestParticipants(w http.ResponseWriter, r *http.Request)
GetAssignableParticipants(w http.ResponseWriter, r *http.Request)
AddParticipantsToContest(w http.ResponseWriter, r *http.Request)
RemoveParticipantsFromContest(w http.ResponseWriter, r *http.Request)
RemoveTaskFromContest(w http.ResponseWriter, r *http.Request)
}

type contestsManagementRouteImpl struct {
Expand Down Expand Up @@ -1304,6 +1306,123 @@ func (cr *contestsManagementRouteImpl) GetAllContests(w http.ResponseWriter, r *
httputils.ReturnSuccess(w, http.StatusOK, response)
}

// EditContestTask godoc
//
// @Tags contests-management
// @Summary Edit a specific task in a contest
// @Description Edit the settings/details for a specific task assigned to a contest
// @Accept json
// @Produce json
// @Param id path int true "Contest ID"
// @Param taskID path int true "Task ID"
// @Param body body schemas.ContestTaskSettings true "Contest Task Settings"
// @Success 200 {object} httputils.APIResponse[schemas.ContestTaskSettings]
// @Failure 400 {object} httputils.APIError
// @Failure 403 {object} httputils.APIError
// @Failure 404 {object} httputils.APIError
// @Failure 405 {object} httputils.APIError
// @Failure 500 {object} httputils.APIError
// @Router /contests-management/contests/{id}/tasks/{taskID} [put]
func (cr *contestsManagementRouteImpl) EditContestTask(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
httputils.ReturnError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}

contestStr := httputils.GetPathValue(r, "id")
if contestStr == "" {
httputils.ReturnError(w, http.StatusBadRequest, "Contest ID cannot be empty")
return
}
contestID, err := strconv.ParseInt(contestStr, 10, 64)
if err != nil {
httputils.ReturnError(w, http.StatusBadRequest, "Invalid contest ID")
return
}

taskIDStr := httputils.GetPathValue(r, "taskID")
if taskIDStr == "" {
httputils.ReturnError(w, http.StatusBadRequest, "Task ID cannot be empty")
return
}
taskID, err := strconv.ParseInt(taskIDStr, 10, 64)
if err != nil {
httputils.ReturnError(w, http.StatusBadRequest, "Invalid task ID")
return
}

var request schemas.ContestTaskSettings
err = httputils.ShouldBindJSON(r.Body, &request)
if err != nil {
httputils.HandleValidationError(w, err)
return
}

currentUser := httputils.GetCurrentUser(r)
db := httputils.GetDatabase(r)

response, err := cr.contestService.EditContestTask(db, currentUser, contestID, taskID, &request)
if err != nil {
httputils.HandleServiceError(w, err, db, cr.logger)
return
}
httputils.ReturnSuccess(w, http.StatusOK, response)
}

// GetContestTask godoc
//
// @Tags contests-management
// @Summary Get a specific task in a contest
// @Description Get the settings/details for a specific task assigned to a contest
// @Produce json
// @Param id path int true "Contest ID"
// @Param taskID path int true "Task ID"
// @Success 200 {object} httputils.APIResponse[schemas.ContestTaskSettings]
// @Failure 400 {object} httputils.APIError
// @Failure 403 {object} httputils.APIError
// @Failure 404 {object} httputils.APIError
// @Failure 500 {object} httputils.APIError
// @Router /contests-management/contests/{id}/tasks/{taskID} [get]
func (cr *contestsManagementRouteImpl) GetContestTask(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
httputils.ReturnError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}

contestStr := httputils.GetPathValue(r, "id")
if contestStr == "" {
httputils.ReturnError(w, http.StatusBadRequest, "Contest ID cannot be empty")
return
}
contestID, err := strconv.ParseInt(contestStr, 10, 64)
if err != nil {
httputils.ReturnError(w, http.StatusBadRequest, "Invalid contest ID")
return
}

taskIDStr := httputils.GetPathValue(r, "taskID")
if taskIDStr == "" {
httputils.ReturnError(w, http.StatusBadRequest, "Task ID cannot be empty")
return
}
taskID, err := strconv.ParseInt(taskIDStr, 10, 64)
if err != nil {
httputils.ReturnError(w, http.StatusBadRequest, "Invalid task ID")
return
}

currentUser := httputils.GetCurrentUser(r)
db := httputils.GetDatabase(r)

settings, err := cr.contestService.GetContestTaskSettings(db, currentUser, contestID, taskID)
if err != nil {
httputils.HandleServiceError(w, err, db, cr.logger)
return
}

httputils.ReturnSuccess(w, http.StatusOK, settings)
}

func RegisterContestsManagementRoute(mux *mux.Router, route ContestsManagementRoute) {
mux.HandleFunc("/contests", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
Expand Down Expand Up @@ -1354,6 +1473,17 @@ func RegisterContestsManagementRoute(mux *mux.Router, route ContestsManagementRo
}
})

mux.HandleFunc("/contests/{id}/tasks/{taskID}", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
route.GetContestTask(w, r)
case http.MethodPut:
route.EditContestTask(w, r)
default:
httputils.ReturnError(w, http.StatusMethodNotAllowed, "Method not allowed")
}
})

mux.HandleFunc("/contests/{id}/registration-requests", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
Expand Down
12 changes: 6 additions & 6 deletions internal/api/http/routes/tasks_management.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ type tasksManagementRoute struct {
// @Description Uploads a task to the FileStorage service
// @Accept multipart/form-data
// @Produce json
// @Param title formData string true "Name of the task"
// @Param title formData string true "Name of the task"
// @Param isVisible formData boolean false "Task visibility (default: false)"
// @Param archive formData file true "Task archive"
// @Failure 405 {object} httputils.APIError
// @Failure 400 {object} httputils.APIError
// @Failure 500 {object} httputils.APIError
// @Success 200 {object} httputils.APIResponse[schemas.TaskCreateResponse]
// @Param archive formData file true "Task archive"
// @Failure 405 {object} httputils.APIError
// @Failure 400 {object} httputils.APIError
// @Failure 500 {object} httputils.APIError
// @Success 200 {object} httputils.APIResponse[schemas.TaskCreateResponse]
// @Router /tasks-management/tasks/ [post]
func (tr *tasksManagementRoute) UploadTask(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
Expand Down
6 changes: 6 additions & 0 deletions package/domain/schemas/contest.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,9 @@ type ContestResults struct {
Contest BaseContest `json:"contest"`
TaskResults []TaskResult `json:"taskResults"`
}

type ContestTaskSettings struct {
StartAt time.Time `json:"startAt"`
EndAt *time.Time `json:"endAt,omitempty"`
IsSubmissionOpen bool `json:"isSubmissionOpen"`
}
8 changes: 4 additions & 4 deletions package/repository/contest.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ type ContestRepository interface {
GetAssignableTasks(db database.Database, contestID int64) ([]models.Task, error)
// GetContestsForUserWithStats retrieves contests with stats a user is participating in
GetContestsForUserWithStats(db database.Database, userID int64) ([]models.ParticipantContestStats, error)
// AddTasksToContest assigns tasks to a contest
AddTaskToContest(db database.Database, taskContest models.ContestTask) error
// SaveContestTask creates or updates a contest-task assignment
SaveContestTask(db database.Database, taskContest models.ContestTask) error
// RemoveTaskFromContest removes a task from a contest
RemoveTaskFromContest(db database.Database, contestID, taskID int64) error
// GetRegistrationRequests retrieves 'status' registration requests for a contest
Expand Down Expand Up @@ -825,9 +825,9 @@ func (cr *contestRepository) GetContestsForUserWithStats(db database.Database, u
return contests, nil
}

func (cr *contestRepository) AddTaskToContest(db database.Database, taskContest models.ContestTask) error {
func (cr *contestRepository) SaveContestTask(db database.Database, taskContest models.ContestTask) error {
tx := db.GetInstance()
err := tx.Create(&taskContest).Error
err := tx.Save(&taskContest).Error
if err != nil {
return err
}
Expand Down
28 changes: 14 additions & 14 deletions package/repository/mocks/mockgen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 57 additions & 1 deletion package/service/contest_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ type ContestService interface {
AddParticipantsToContest(db database.Database, currentUser *schemas.User, contestID int64, userIDs []int64) error
// RemoveParticipantsFromContest removes multiple users from contest participants (only accessible by contest collaborators with edit permission)
RemoveParticipantsFromContest(db database.Database, currentUser *schemas.User, contestID int64, userIDs []int64) error

// GetContestTaskSettings retrieves settings for a specific task within a contest (only accessible by contest collaborators)
GetContestTaskSettings(db database.Database, currentUser *schemas.User, contestID, taskID int64) (*schemas.ContestTaskSettings, error)
// EditContestTask updates settings for a specific task within a contest (only accessible by contest collaborators with edit permission)
EditContestTask(db database.Database, currentUser *schemas.User, contestID, taskID int64, settings *schemas.ContestTaskSettings) (*schemas.ContestTaskSettings, error)
}

const defaultContestSort = "created_at:desc"
Expand Down Expand Up @@ -726,7 +731,7 @@ func (cs *contestService) AddTaskToContest(db database.Database, currentUser *sc
IsSubmissionOpen: true,
}

return cs.contestRepository.AddTaskToContest(db, taskContest)
return cs.contestRepository.SaveContestTask(db, taskContest)
}

func (cs *contestService) RemoveTaskFromContest(db database.Database, currentUser *schemas.User, contestID, taskID int64) error {
Expand Down Expand Up @@ -1491,6 +1496,57 @@ func (cs *contestService) RemoveParticipantsFromContest(db database.Database, cu
return cs.contestRepository.RemoveParticipantsFromContest(db, contestID, userIDs)
}

func (cs *contestService) GetContestTaskSettings(db database.Database, currentUser *schemas.User, contestID, taskID int64) (*schemas.ContestTaskSettings, error) {
err := cs.hasContestPermission(db, contestID, currentUser, types.PermissionEdit)
if err != nil {
return nil, err
}

settingsModel, err := cs.contestRepository.GetContestTask(db, contestID, taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.ErrTaskNotInContest
}
return nil, err
}
settings := &schemas.ContestTaskSettings{
StartAt: settingsModel.StartAt,
EndAt: settingsModel.EndAt,
IsSubmissionOpen: settingsModel.IsSubmissionOpen,
}
return settings, nil
}

func (cs *contestService) EditContestTask(db database.Database, currentUser *schemas.User, contestID, taskID int64, settings *schemas.ContestTaskSettings) (*schemas.ContestTaskSettings, error) {
err := cs.hasContestPermission(db, contestID, currentUser, types.PermissionEdit)
if err != nil {
return nil, err
}

settingsModel, err := cs.contestRepository.GetContestTask(db, contestID, taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.ErrTaskNotInContest
}
return nil, err
}
settingsModel.StartAt = settings.StartAt
settingsModel.EndAt = settings.EndAt
settingsModel.IsSubmissionOpen = settings.IsSubmissionOpen

err = cs.contestRepository.SaveContestTask(db, *settingsModel)
if err != nil {
return nil, err
}

response := &schemas.ContestTaskSettings{
StartAt: settingsModel.StartAt,
EndAt: settingsModel.EndAt,
IsSubmissionOpen: settingsModel.IsSubmissionOpen,
}
return response, nil
}

func NewContestService(contestRepository repository.ContestRepository, userRepository repository.UserRepository, submissionRepository repository.SubmissionRepository, taskRepository repository.TaskRepository, groupRepository repository.GroupRepository, accessControlService AccessControlService, taskService TaskService) ContestService {
return &contestService{
contestRepository: contestRepository,
Expand Down
Loading
Loading