diff --git a/internal/api/http/routes/contests_management.go b/internal/api/http/routes/contests_management.go index af90a59..ed74818 100644 --- a/internal/api/http/routes/contests_management.go +++ b/internal/api/http/routes/contests_management.go @@ -25,6 +25,7 @@ type ContestsManagementRoute interface { GetRegistrationRequests(w http.ResponseWriter, r *http.Request) ApproveRegistrationRequest(w http.ResponseWriter, r *http.Request) RejectRegistrationRequest(w http.ResponseWriter, r *http.Request) + UpdateTaskInContest(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) @@ -187,16 +188,19 @@ func (cr *contestsManagementRouteImpl) DeleteContest(w http.ResponseWriter, r *h // // @Tags contests-management // @Summary Get available tasks for a contest -// @Description Get all tasks that are NOT yet assigned to the specified contest (admin/teacher only) -// +// @Description Get all tasks that are NOT yet assigned to the specified contest with pagination (admin/teacher only) // @Produce json -// @Param id path int true "Contest ID" -// @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 -// @Success 200 {object} httputils.APIResponse[[]schemas.Task] +// @Param id path int true "Contest ID" +// @Param limit query int false "Limit" +// @Param offset query int false "Offset" +// @Param sort query string false "Sort" +// @Param search query string false "Search tasks by title" +// @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 +// @Success 200 {object} httputils.APIResponse[schemas.PaginatedResult[[]schemas.Task]] // @Router /contests-management/contests/{id}/tasks/assignable-tasks [get] func (cr *contestsManagementRouteImpl) GetAssignableTasks(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { @@ -217,8 +221,11 @@ func (cr *contestsManagementRouteImpl) GetAssignableTasks(w http.ResponseWriter, db := httputils.GetDatabase(r) currentUser := httputils.GetCurrentUser(r) + queryParams := r.Context().Value(httputils.QueryParamsKey).(map[string]any) + paginationParams := httputils.ExtractPaginationParams(queryParams) + search, _ := queryParams["search"].(string) - tasks, err := cr.contestService.GetAssignableTasks(db, currentUser, contestID) + tasks, err := cr.contestService.GetAssignableTasks(db, currentUser, contestID, paginationParams, search) if err != nil { httputils.HandleServiceError(w, err, db, cr.logger) return @@ -338,6 +345,61 @@ func (cr *contestsManagementRouteImpl) RemoveTaskFromContest(w http.ResponseWrit httputils.ReturnSuccess(w, http.StatusOK, httputils.NewMessageResponse("Tasks removed from contest successfully")) } +// UpdateTaskInContest godoc +// +// @Tags contests-management +// @Summary Update a task's schedule in a contest +// @Description Update the start and end time of a task in a contest (only accessible by contest collaborators with edit permission) +// @Accept json +// @Produce json +// @Param id path int true "Contest ID" +// @Param task_id path int true "Task ID" +// @Param body body schemas.UpdateTaskInContest true "Update Task Schedule" +// @Failure 400 {object} httputils.ValidationErrorResponse +// @Failure 403 {object} httputils.APIError +// @Failure 404 {object} httputils.APIError +// @Failure 405 {object} httputils.APIError +// @Failure 500 {object} httputils.APIError +// @Success 200 {object} httputils.APIResponse[httputils.MessageResponse] +// @Router /contests-management/contests/{id}/tasks/{task_id} [put] +func (cr *contestsManagementRouteImpl) UpdateTaskInContest(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") + contestID, err := strconv.ParseInt(contestStr, 10, 64) + if err != nil { + httputils.ReturnError(w, http.StatusBadRequest, "Invalid contest ID") + return + } + + taskStr := httputils.GetPathValue(r, "task_id") + taskID, err := strconv.ParseInt(taskStr, 10, 64) + if err != nil { + httputils.ReturnError(w, http.StatusBadRequest, "Invalid task ID") + return + } + + var request schemas.UpdateTaskInContest + if err := httputils.ShouldBindJSON(r.Body, &request); err != nil { + httputils.HandleValidationError(w, err) + return + } + + db := httputils.GetDatabase(r) + currentUser := httputils.GetCurrentUser(r) + + err = cr.contestService.UpdateTaskInContest(db, currentUser, contestID, taskID, &request) + if err != nil { + httputils.HandleServiceError(w, err, db, cr.logger) + return + } + + httputils.ReturnSuccess(w, http.StatusOK, httputils.NewMessageResponse("Task schedule updated successfully")) +} + // GetRegistrationRequests godoc // // @Tags contests-management @@ -478,16 +540,20 @@ func (cr *contestsManagementRouteImpl) RejectRegistrationRequest(w http.Response // // @Tags contests-management // @Summary Get tasks for a contest -// @Description Get all tasks associated with a specific contest +// @Description Get all tasks associated with a specific contest with pagination // // @Produce json -// @Param id path int true "Contest ID" +// @Param id path int true "Contest ID" +// @Param limit query int false "Limit" +// @Param offset query int false "Offset" +// @Param sort query string false "Sort" +// @Param search query string false "Search tasks by title" // @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 -// @Success 200 {object} httputils.APIResponse[[]schemas.ContestTask] +// @Success 200 {object} httputils.APIResponse[schemas.PaginatedResult[[]schemas.ContestTask]] // @Router /contests-management/contests/{id}/tasks [get] func (cr *contestsManagementRouteImpl) GetContestTasks(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { @@ -508,8 +574,11 @@ func (cr *contestsManagementRouteImpl) GetContestTasks(w http.ResponseWriter, r db := httputils.GetDatabase(r) currentUser := httputils.GetCurrentUser(r) + queryParams := r.Context().Value(httputils.QueryParamsKey).(map[string]any) + paginationParams := httputils.ExtractPaginationParams(queryParams) + search, _ := queryParams["search"].(string) - tasks, err := cr.contestService.GetTasksForContest(db, currentUser, contestID) + tasks, err := cr.contestService.GetTasksForContest(db, currentUser, contestID, paginationParams, search) if err != nil { httputils.HandleServiceError(w, err, db, cr.logger) return @@ -1340,6 +1409,7 @@ func RegisterContestsManagementRoute(mux *mux.Router, route ContestsManagementRo }) mux.HandleFunc("/contests/{id}/tasks/assignable-tasks", route.GetAssignableTasks) + mux.HandleFunc("/contests/{id}/tasks/{task_id}", route.UpdateTaskInContest) mux.HandleFunc("/contests/{id}/tasks", func(w http.ResponseWriter, r *http.Request) { switch r.Method { diff --git a/package/domain/schemas/contest.go b/package/domain/schemas/contest.go index c630f96..8a998d0 100644 --- a/package/domain/schemas/contest.go +++ b/package/domain/schemas/contest.go @@ -90,6 +90,11 @@ type AddTaskToContest struct { EndAt OptionalTime `json:"endAt"` } +type UpdateTaskInContest struct { + StartAt OptionalTime `json:"startAt"` + EndAt OptionalTime `json:"endAt"` +} + type RegistrationRequest struct { ID int64 `json:"id"` ContestID int64 `json:"contestId"` diff --git a/package/domain/schemas/task.go b/package/domain/schemas/task.go index 373d19c..b2cb792 100644 --- a/package/domain/schemas/task.go +++ b/package/domain/schemas/task.go @@ -8,12 +8,13 @@ type EditTask struct { } type Task struct { - ID int64 `json:"id"` - Title string `json:"title"` - CreatedBy int64 `json:"createdBy"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - IsVisible bool `json:"isVisible"` + ID int64 `json:"id"` + Title string `json:"title"` + CreatedBy int64 `json:"createdBy"` + CreatorName string `json:"creatorName"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + IsVisible bool `json:"isVisible"` } // Struct to embed basic task info diff --git a/package/repository/contest.go b/package/repository/contest.go index 4ac3634..8ad6351 100644 --- a/package/repository/contest.go +++ b/package/repository/contest.go @@ -2,6 +2,7 @@ package repository import ( "fmt" + "strings" "github.com/mini-maxit/backend/internal/database" "github.com/mini-maxit/backend/package/domain/models" @@ -54,18 +55,22 @@ type ContestRepository interface { IsUserParticipant(db database.Database, contestID int64, userID int64) (bool, error) // GetTasksForContest retrieves all tasks assigned to a contest GetTasksForContest(db database.Database, contestID int64) ([]models.Task, error) - // GetContestTasksWithSettings retrieves contest-task relations with timing flags and associated task - GetContestTasksWithSettings(db database.Database, contestID int64) ([]models.ContestTask, error) + // GetContestTasksWithSettings retrieves contest-task relations with timing flags and associated task (paginated) + GetContestTasksWithSettings(db database.Database, contestID int64, limit, offset int, sort, search string) ([]models.ContestTask, int64, error) // GetVisibleContestTasksWithSettings retrieves visible contest-task relations with timing flags and associated task GetVisibleContestTasksWithSettings(db database.Database, contestID int64) ([]models.ContestTask, error) // GetTasksForContestWithStats retrieves all tasks assigned to a contest with submission statistics for a user GetTasksForContestWithStats(db database.Database, contestID, userID int64) ([]models.Task, error) - // GetAssignableTasks retrieves all tasks NOT assigned to a contest - GetAssignableTasks(db database.Database, contestID int64) ([]models.Task, error) + // GetAssignableTasks retrieves all tasks NOT assigned to a contest (paginated) + GetAssignableTasks(db database.Database, contestID int64, limit, offset int, sort, search string) ([]models.Task, int64, 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 + // UpdateTaskInContest updates a task's schedule in a contest + UpdateTaskInContest(db database.Database, taskContest models.ContestTask) error + // IsTaskInContest checks if a task is assigned to a contest + IsTaskInContest(db database.Database, contestID, taskID int64) (bool, error) // RemoveTaskFromContest removes a task from a contest RemoveTaskFromContest(db database.Database, contestID, taskID int64) error // GetRegistrationRequests retrieves 'status' registration requests for a contest @@ -634,18 +639,65 @@ func (cr *contestRepository) GetUpcomingContestsWithStats(db database.Database, } // GetContestTasksWithSettings retrieves contest-task relations (with timing flags) and preloads the associated Task -func (cr *contestRepository) GetContestTasksWithSettings(db database.Database, contestID int64) ([]models.ContestTask, error) { +func (cr *contestRepository) GetContestTasksWithSettings(db database.Database, contestID int64, limit, offset int, sort, search string) ([]models.ContestTask, int64, error) { tx := db.GetInstance() var relations []models.ContestTask - err := tx.Unscoped().Model(&models.ContestTask{}). - Where("contest_id = ?", contestID). + var totalCount int64 + + taskTable := database.ResolveTableName(tx, &models.Task{}) + contestTaskTable := database.ResolveTableName(tx, &models.ContestTask{}) + + base := tx.Unscoped().Model(&models.ContestTask{}). + Joins(fmt.Sprintf("JOIN %s ON %s.id = %s.task_id", taskTable, taskTable, contestTaskTable)). + Where(fmt.Sprintf("%s.contest_id = ?", contestTaskTable), contestID) + + if search != "" { + base = base.Where(fmt.Sprintf("%s.title ILIKE ?", taskTable), "%"+search+"%") + } + + if err := base.Count(&totalCount).Error; err != nil { + return nil, 0, err + } + + query := base + if sort != "" { + query = query.Order(qualifyContestTaskSort(sort, contestTaskTable, taskTable)) + } + + err := query. + Limit(limit). + Offset(offset). Preload("Task"). Preload("Task.Author"). Find(&relations).Error if err != nil { - return nil, err + return nil, 0, err } - return relations, nil + return relations, totalCount, nil +} + +// qualifyContestTaskSort qualifies sort fields with their table prefix for a joined contest_tasks/tasks query +func qualifyContestTaskSort(sortBy, contestTaskTable, taskTable string) string { + fieldMap := map[string]string{ + "id": contestTaskTable + ".task_id", + "task_id": contestTaskTable + ".task_id", + "start_at": contestTaskTable + ".start_at", + "end_at": contestTaskTable + ".end_at", + "title": taskTable + ".title", + } + parts := strings.Split(sortBy, ":") + if len(parts) != 2 { + return sortBy + } + field, ok := fieldMap[parts[0]] + if !ok { + return sortBy + } + dir := parts[1] + if dir != "asc" && dir != "desc" { + dir = "asc" + } + return field + " " + dir } // GetVisibleContestTasksWithSettings retrieves visible contest-task relations (with timing flags) and preloads the associated Task @@ -689,20 +741,35 @@ func (cr *contestRepository) GetTasksForContestWithStats(db database.Database, c return tasks, nil } -func (cr *contestRepository) GetAssignableTasks(db database.Database, contestID int64) ([]models.Task, error) { +func (cr *contestRepository) GetAssignableTasks(db database.Database, contestID int64, limit, offset int, sort, search string) ([]models.Task, int64, error) { tx := db.GetInstance() var tasks []models.Task - err := tx.Model(&models.Task{}). + var totalCount int64 + + base := tx.Model(&models.Task{}). Where("id NOT IN (?)", tx.Table(database.ResolveTableName(tx, &models.ContestTask{})). Select("task_id"). - Where("contest_id = ?", contestID), - ). - Find(&tasks).Error + Where("contest_id = ?", contestID)) + + if search != "" { + base = base.Where("title ILIKE ?", "%"+search+"%") + } + + if err := base.Count(&totalCount).Error; err != nil { + return nil, 0, err + } + + query, err := utils.ApplyPaginationAndSort(base, limit, offset, sort) if err != nil { - return nil, err + return nil, 0, err } - return tasks, nil + + err = query.Preload("Author").Find(&tasks).Error + if err != nil { + return nil, 0, err + } + return tasks, totalCount, nil } func (cr *contestRepository) GetContestsForUserWithStats(db database.Database, userID int64) ([]models.ParticipantContestStats, error) { @@ -840,6 +907,29 @@ func (cr *contestRepository) RemoveTaskFromContest(db database.Database, contest return err } +func (cr *contestRepository) UpdateTaskInContest(db database.Database, taskContest models.ContestTask) error { + tx := db.GetInstance() + err := tx.Model(&models.ContestTask{}). + Where("contest_id = ? AND task_id = ?", taskContest.ContestID, taskContest.TaskID). + Updates(map[string]any{ + "start_at": taskContest.StartAt, + "end_at": taskContest.EndAt, + }).Error + return err +} + +func (cr *contestRepository) IsTaskInContest(db database.Database, contestID, taskID int64) (bool, error) { + tx := db.GetInstance() + var count int64 + err := tx.Model(&models.ContestTask{}). + Where("contest_id = ? AND task_id = ?", contestID, taskID). + Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} + func (cr *contestRepository) GetRegistrationRequests(db database.Database, contestID int64, status types.RegistrationRequestStatus) ([]models.ContestRegistrationRequests, error) { tx := db.GetInstance() var requests []models.ContestRegistrationRequests diff --git a/package/repository/mocks/mockgen.go b/package/repository/mocks/mockgen.go index e536dfa..8ba1ccb 100644 --- a/package/repository/mocks/mockgen.go +++ b/package/repository/mocks/mockgen.go @@ -1739,18 +1739,19 @@ func (mr *MockContestRepositoryMockRecorder) GetAssignableParticipants(db, conte } // GetAssignableTasks mocks base method. -func (m *MockContestRepository) GetAssignableTasks(db database.Database, contestID int64) ([]models.Task, error) { +func (m *MockContestRepository) GetAssignableTasks(db database.Database, contestID int64, limit, offset int, sort, search string) ([]models.Task, int64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAssignableTasks", db, contestID) + ret := m.ctrl.Call(m, "GetAssignableTasks", db, contestID, limit, offset, sort, search) ret0, _ := ret[0].([]models.Task) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(int64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // GetAssignableTasks indicates an expected call of GetAssignableTasks. -func (mr *MockContestRepositoryMockRecorder) GetAssignableTasks(db, contestID any) *gomock.Call { +func (mr *MockContestRepositoryMockRecorder) GetAssignableTasks(db, contestID, limit, offset, sort, search any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAssignableTasks", reflect.TypeOf((*MockContestRepository)(nil).GetAssignableTasks), db, contestID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAssignableTasks", reflect.TypeOf((*MockContestRepository)(nil).GetAssignableTasks), db, contestID, limit, offset, sort, search) } // GetContestGroups mocks base method. @@ -1800,18 +1801,19 @@ func (mr *MockContestRepositoryMockRecorder) GetContestTask(db, contestID, taskI } // GetContestTasksWithSettings mocks base method. -func (m *MockContestRepository) GetContestTasksWithSettings(db database.Database, contestID int64) ([]models.ContestTask, error) { +func (m *MockContestRepository) GetContestTasksWithSettings(db database.Database, contestID int64, limit, offset int, sort, search string) ([]models.ContestTask, int64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetContestTasksWithSettings", db, contestID) + ret := m.ctrl.Call(m, "GetContestTasksWithSettings", db, contestID, limit, offset, sort, search) ret0, _ := ret[0].([]models.ContestTask) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(int64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // GetContestTasksWithSettings indicates an expected call of GetContestTasksWithSettings. -func (mr *MockContestRepositoryMockRecorder) GetContestTasksWithSettings(db, contestID any) *gomock.Call { +func (mr *MockContestRepositoryMockRecorder) GetContestTasksWithSettings(db, contestID, limit, offset, sort, search any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContestTasksWithSettings", reflect.TypeOf((*MockContestRepository)(nil).GetContestTasksWithSettings), db, contestID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContestTasksWithSettings", reflect.TypeOf((*MockContestRepository)(nil).GetContestTasksWithSettings), db, contestID, limit, offset, sort, search) } // GetContestsForUserWithStats mocks base method. @@ -2028,6 +2030,21 @@ func (mr *MockContestRepositoryMockRecorder) IsPendingRegistrationExists(db, con return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsPendingRegistrationExists", reflect.TypeOf((*MockContestRepository)(nil).IsPendingRegistrationExists), db, contestID, userID) } +// IsTaskInContest mocks base method. +func (m *MockContestRepository) IsTaskInContest(db database.Database, contestID, taskID int64) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsTaskInContest", db, contestID, taskID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsTaskInContest indicates an expected call of IsTaskInContest. +func (mr *MockContestRepositoryMockRecorder) IsTaskInContest(db, contestID, taskID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTaskInContest", reflect.TypeOf((*MockContestRepository)(nil).IsTaskInContest), db, contestID, taskID) +} + // IsUserParticipant mocks base method. func (m *MockContestRepository) IsUserParticipant(db database.Database, contestID, userID int64) (bool, error) { m.ctrl.T.Helper() @@ -2098,3 +2115,17 @@ func (mr *MockContestRepositoryMockRecorder) UpdateRegistrationRequestStatus(db, mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRegistrationRequestStatus", reflect.TypeOf((*MockContestRepository)(nil).UpdateRegistrationRequestStatus), db, requestID, status) } + +// UpdateTaskInContest mocks base method. +func (m *MockContestRepository) UpdateTaskInContest(db database.Database, taskContest models.ContestTask) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateTaskInContest", db, taskContest) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateTaskInContest indicates an expected call of UpdateTaskInContest. +func (mr *MockContestRepositoryMockRecorder) UpdateTaskInContest(db, taskContest any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTaskInContest", reflect.TypeOf((*MockContestRepository)(nil).UpdateTaskInContest), db, taskContest) +} diff --git a/package/service/contest_service.go b/package/service/contest_service.go index 5a6a806..3e86d82 100644 --- a/package/service/contest_service.go +++ b/package/service/contest_service.go @@ -35,17 +35,19 @@ type ContestService interface { // RegisterForContest creates a pending registration for a contest RegisterForContest(db database.Database, currentUser *schemas.User, contestID int64) error // GetTasksForContest retrieves all contest task relations (with timing/submission flags) for a contest (for authorized users) - GetTasksForContest(db database.Database, currentUser *schemas.User, contestID int64) ([]schemas.ContestTask, error) + GetTasksForContest(db database.Database, currentUser *schemas.User, contestID int64, paginationParams schemas.PaginationParams, search string) (schemas.PaginatedResult[[]schemas.ContestTask], error) // GetVisibleTasksForContest retrieves visible contest tasks filtered by status (for participants and users with access_policy) GetVisibleTasksForContest(db database.Database, currentUser *schemas.User, contestID int64, status types.ContestStatus) ([]schemas.ContestTask, error) // GetTaskProgressForContest retrieves all tasks associated with a contest with submission stats for the requesting user GetTaskProgressForContest(db database.Database, currentUser *schemas.User, contestID int64) ([]schemas.TaskWithContestStats, error) // GetAssignableTasks retrieves all tasks NOT assigned to a contest (for authorized users) - GetAssignableTasks(db database.Database, currentUser *schemas.User, contestID int64) ([]schemas.Task, error) + GetAssignableTasks(db database.Database, currentUser *schemas.User, contestID int64, paginationParams schemas.PaginationParams, search string) (schemas.PaginatedResult[[]schemas.Task], error) // GetUserContests retrieves all contests a user is participating in GetUserContests(db database.Database, userID int64) (*schemas.UserContestsWithStats, error) // AddTaskToContest adds a task to a contest AddTaskToContest(db database.Database, currentUser *schemas.User, contestID int64, request *schemas.AddTaskToContest) error + // UpdateTaskInContest updates a task's schedule in a contest + UpdateTaskInContest(db database.Database, currentUser *schemas.User, contestID, taskID int64, request *schemas.UpdateTaskInContest) error // RemoveTaskFromContest removes a task from a contest RemoveTaskFromContest(db database.Database, currentUser *schemas.User, contestID, taskID int64) error // GetRegistrationRequests retrieves pending registration requests for a contest @@ -484,22 +486,26 @@ func (cs *contestService) updateModel(model *models.Contest, editInfo *schemas.E } } -func (cs *contestService) GetTasksForContest(db database.Database, currentUser *schemas.User, contestID int64) ([]schemas.ContestTask, error) { +func (cs *contestService) GetTasksForContest(db database.Database, currentUser *schemas.User, contestID int64, paginationParams schemas.PaginationParams, search string) (schemas.PaginatedResult[[]schemas.ContestTask], error) { contest, err := cs.contestRepository.Get(db, contestID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.ErrNotFound + return schemas.PaginatedResult[[]schemas.ContestTask]{}, errors.ErrNotFound } - return nil, err + return schemas.PaginatedResult[[]schemas.ContestTask]{}, err } if !cs.isContestVisibleToUser(db, contest, currentUser) { - return nil, errors.ErrForbidden + return schemas.PaginatedResult[[]schemas.ContestTask]{}, errors.ErrForbidden } - relations, err := cs.contestRepository.GetContestTasksWithSettings(db, contestID) + if paginationParams.Sort == "" { + paginationParams.Sort = "start_at:asc" + } + + relations, totalCount, err := cs.contestRepository.GetContestTasksWithSettings(db, contestID, paginationParams.Limit, paginationParams.Offset, paginationParams.Sort, search) if err != nil { - return nil, err + return schemas.PaginatedResult[[]schemas.ContestTask]{}, err } result := make([]schemas.ContestTask, len(relations)) @@ -512,7 +518,7 @@ func (cs *contestService) GetTasksForContest(db database.Database, currentUser * IsSubmissionOpen: rel.IsSubmissionOpen, } } - return result, nil + return schemas.NewPaginatedResult(result, paginationParams.Offset, paginationParams.Limit, totalCount), nil } func (cs *contestService) GetVisibleTasksForContest(db database.Database, currentUser *schemas.User, contestID int64, status types.ContestStatus) ([]schemas.ContestTask, error) { @@ -617,28 +623,73 @@ func (cs *contestService) GetTaskProgressForContest(db database.Database, curren return result, nil } -func (cs *contestService) GetAssignableTasks(db database.Database, currentUser *schemas.User, contestID int64) ([]schemas.Task, error) { +func (cs *contestService) GetAssignableTasks(db database.Database, currentUser *schemas.User, contestID int64, paginationParams schemas.PaginationParams, search string) (schemas.PaginatedResult[[]schemas.Task], error) { err := cs.hasContestPermission(db, contestID, currentUser, types.PermissionEdit) if err != nil { - return nil, err + return schemas.PaginatedResult[[]schemas.Task]{}, err } - tasks, err := cs.contestRepository.GetAssignableTasks(db, contestID) + if paginationParams.Sort == "" { + paginationParams.Sort = "title:asc" + } + + tasks, totalCount, err := cs.contestRepository.GetAssignableTasks(db, contestID, paginationParams.Limit, paginationParams.Offset, paginationParams.Sort, search) if err != nil { - return nil, err + return schemas.PaginatedResult[[]schemas.Task]{}, err } result := make([]schemas.Task, len(tasks)) for i, task := range tasks { result[i] = schemas.Task{ - ID: task.ID, - Title: task.Title, - CreatedBy: task.CreatedBy, - CreatedAt: task.CreatedAt, - UpdatedAt: task.UpdatedAt, + ID: task.ID, + Title: task.Title, + CreatedBy: task.CreatedBy, + CreatorName: task.Author.Name, + CreatedAt: task.CreatedAt, + UpdatedAt: task.UpdatedAt, + IsVisible: task.IsVisible, } } - return result, nil + return schemas.NewPaginatedResult(result, paginationParams.Offset, paginationParams.Limit, totalCount), nil +} + +func (cs *contestService) UpdateTaskInContest(db database.Database, currentUser *schemas.User, contestID, taskID int64, request *schemas.UpdateTaskInContest) error { + err := cs.hasContestPermission(db, contestID, currentUser, types.PermissionEdit) + if err != nil { + return err + } + contest, err := cs.contestRepository.Get(db, contestID) + if err != nil { + return err + } + + isTask, err := cs.contestRepository.IsTaskInContest(db, contestID, taskID) + if err != nil { + return err + } + if !isTask { + return errors.ErrNotFound + } + + startAt := time.Now() + if request.StartAt.Set && request.StartAt.Value != nil { + startAt = *request.StartAt.Value + } + endAt := contest.EndAt + if request.EndAt.Set { + endAt = request.EndAt.Value + } + if endAt != nil && startAt.After(*endAt) { + return errors.ErrEndBeforeStart + } + + taskContest := models.ContestTask{ + ContestID: contestID, + TaskID: taskID, + StartAt: startAt, + EndAt: endAt, + } + return cs.contestRepository.UpdateTaskInContest(db, taskContest) } func (cs *contestService) GetUserContests(db database.Database, userID int64) (*schemas.UserContestsWithStats, error) { diff --git a/package/service/mocks/mockgen.go b/package/service/mocks/mockgen.go index 6e021b2..67545f6 100644 --- a/package/service/mocks/mockgen.go +++ b/package/service/mocks/mockgen.go @@ -328,18 +328,18 @@ func (mr *MockContestServiceMockRecorder) GetAssignableParticipants(db, currentU } // GetAssignableTasks mocks base method. -func (m *MockContestService) GetAssignableTasks(db database.Database, currentUser *schemas.User, contestID int64) ([]schemas.Task, error) { +func (m *MockContestService) GetAssignableTasks(db database.Database, currentUser *schemas.User, contestID int64, paginationParams schemas.PaginationParams, search string) (schemas.PaginatedResult[[]schemas.Task], error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAssignableTasks", db, currentUser, contestID) - ret0, _ := ret[0].([]schemas.Task) + ret := m.ctrl.Call(m, "GetAssignableTasks", db, currentUser, contestID, paginationParams, search) + ret0, _ := ret[0].(schemas.PaginatedResult[[]schemas.Task]) ret1, _ := ret[1].(error) return ret0, ret1 } // GetAssignableTasks indicates an expected call of GetAssignableTasks. -func (mr *MockContestServiceMockRecorder) GetAssignableTasks(db, currentUser, contestID any) *gomock.Call { +func (mr *MockContestServiceMockRecorder) GetAssignableTasks(db, currentUser, contestID, paginationParams, search any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAssignableTasks", reflect.TypeOf((*MockContestService)(nil).GetAssignableTasks), db, currentUser, contestID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAssignableTasks", reflect.TypeOf((*MockContestService)(nil).GetAssignableTasks), db, currentUser, contestID, paginationParams, search) } // GetContestGroups mocks base method. @@ -523,18 +523,18 @@ func (mr *MockContestServiceMockRecorder) GetTaskProgressForContest(db, currentU } // GetTasksForContest mocks base method. -func (m *MockContestService) GetTasksForContest(db database.Database, currentUser *schemas.User, contestID int64) ([]schemas.ContestTask, error) { +func (m *MockContestService) GetTasksForContest(db database.Database, currentUser *schemas.User, contestID int64, paginationParams schemas.PaginationParams, search string) (schemas.PaginatedResult[[]schemas.ContestTask], error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTasksForContest", db, currentUser, contestID) - ret0, _ := ret[0].([]schemas.ContestTask) + ret := m.ctrl.Call(m, "GetTasksForContest", db, currentUser, contestID, paginationParams, search) + ret0, _ := ret[0].(schemas.PaginatedResult[[]schemas.ContestTask]) ret1, _ := ret[1].(error) return ret0, ret1 } // GetTasksForContest indicates an expected call of GetTasksForContest. -func (mr *MockContestServiceMockRecorder) GetTasksForContest(db, currentUser, contestID any) *gomock.Call { +func (mr *MockContestServiceMockRecorder) GetTasksForContest(db, currentUser, contestID, paginationParams, search any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTasksForContest", reflect.TypeOf((*MockContestService)(nil).GetTasksForContest), db, currentUser, contestID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTasksForContest", reflect.TypeOf((*MockContestService)(nil).GetTasksForContest), db, currentUser, contestID, paginationParams, search) } // GetUpcomingContests mocks base method. @@ -682,6 +682,20 @@ func (mr *MockContestServiceMockRecorder) RemoveTaskFromContest(db, currentUser, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveTaskFromContest", reflect.TypeOf((*MockContestService)(nil).RemoveTaskFromContest), db, currentUser, contestID, taskID) } +// UpdateTaskInContest mocks base method. +func (m *MockContestService) UpdateTaskInContest(db database.Database, currentUser *schemas.User, contestID, taskID int64, request *schemas.UpdateTaskInContest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateTaskInContest", db, currentUser, contestID, taskID, request) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateTaskInContest indicates an expected call of UpdateTaskInContest. +func (mr *MockContestServiceMockRecorder) UpdateTaskInContest(db, currentUser, contestID, taskID, request any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTaskInContest", reflect.TypeOf((*MockContestService)(nil).UpdateTaskInContest), db, currentUser, contestID, taskID, request) +} + // ValidateContestSubmission mocks base method. func (m *MockContestService) ValidateContestSubmission(db database.Database, contestID, taskID, userID int64) error { m.ctrl.T.Helper() diff --git a/package/service/task_service.go b/package/service/task_service.go index b4eee5a..d2cf2c1 100644 --- a/package/service/task_service.go +++ b/package/service/task_service.go @@ -477,12 +477,13 @@ func (ts *taskService) updateModel(currentModel *models.Task, updateInfo *schema func TaskToSchema(model *models.Task) *schemas.Task { return &schemas.Task{ - ID: model.ID, - Title: model.Title, - CreatedBy: model.CreatedBy, - CreatedAt: model.CreatedAt, - UpdatedAt: model.UpdatedAt, - IsVisible: model.IsVisible, + ID: model.ID, + Title: model.Title, + CreatedBy: model.CreatedBy, + CreatorName: model.Author.Name, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + IsVisible: model.IsVisible, } } @@ -566,11 +567,12 @@ func (ts *taskService) enrichTaskWithAttempts( return &schemas.TaskWithAttempts{ Task: schemas.Task{ - ID: task.ID, - Title: task.Title, - CreatedBy: task.CreatedBy, - CreatedAt: task.CreatedAt, - UpdatedAt: task.UpdatedAt, + ID: task.ID, + Title: task.Title, + CreatedBy: task.CreatedBy, + CreatorName: task.Author.Name, + CreatedAt: task.CreatedAt, + UpdatedAt: task.UpdatedAt, }, AttemptsSummary: schemas.AttemptsSummary{ BestScore: bestScore,