Skip to content

Commit 0553e5d

Browse files
Merge branch 'dev' into feature/add-integration-test-reproducing-the-race-in-competition
2 parents e8be2f8 + 298260b commit 0553e5d

132 files changed

Lines changed: 5733 additions & 809 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/main/java/com/itasocialacademy/oitassist/chat/controller/ParticipantForumController.java

Lines changed: 96 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -21,127 +21,159 @@
2121
import org.springframework.web.bind.annotation.*;
2222

2323
@RestController
24-
@RequestMapping("/api/v1/tasks/{taskId}/questions")
24+
@RequestMapping("/api/v1/task-assignments/{taskAssignmentId}/questions")
2525
@RequiredArgsConstructor
2626
@PreAuthorize("isAuthenticated()")
2727
@Tag(
2828
name = "Participant Forum V1",
29-
description = "Participant operations for temporary TaskBody-scoped question forums")
29+
description = "Participant operations for TaskAssignment-scoped question forums")
3030
public class ParticipantForumController {
3131
private final ParticipantForumService participantForumService;
3232

3333
@Operation(
34-
summary = "Get participant forum questions",
34+
summary = "Get task assignment forum questions",
3535
description = """
36-
Returns a paginated list of questions for the specified task.
36+
Returns a paginated list of questions for the specified task assignment.
3737
38-
The response contains public questions and private questions created by
39-
the authenticated participant. Questions are ordered by creation time
40-
in descending order, with the question ID used as a deterministic
41-
tie-breaker.
38+
For an authenticated participant, the response contains public questions
39+
and private questions created by that participant. Participant access
40+
requires the task assignment to be visible and requires a matching
41+
Participation for the assignment's competition and stage.
4242
43-
This is a temporary TaskBody-scoped API that will be migrated to
44-
TaskAssignment-scoped access.
43+
A global administrator may access an existing task assignment forum
44+
without a participant record.
45+
46+
Questions are ordered by creation time in descending order, with the
47+
question identifier used as a deterministic tie-breaker.
4548
""")
4649
@ApiResponses(value = {
4750
@ApiResponse(
4851
responseCode = "200",
49-
description = "Forum questions retrieved successfully",
52+
description = "Task assignment forum page retrieved successfully",
5053
content = @Content(
5154
mediaType = "application/json",
52-
schema = @Schema(implementation = PageResponse.class))),
55+
schema = @Schema(
56+
implementation = PageResponse.class))),
5357
@ApiResponse(
5458
responseCode = "400",
55-
description = "Task identifier or pagination parameters are invalid",
59+
description = "Task assignment identifier or pagination parameters are invalid",
5660
content = @Content(
5761
mediaType = "application/json",
58-
schema = @Schema(implementation = ErrorResponse.class))),
62+
schema = @Schema(
63+
implementation = ErrorResponse.class))),
5964
@ApiResponse(
6065
responseCode = "401",
6166
description = "Authentication is required",
6267
content = @Content(
6368
mediaType = "application/json",
64-
schema = @Schema(implementation = ErrorResponse.class))),
69+
schema = @Schema(
70+
implementation = ErrorResponse.class))),
71+
@ApiResponse(
72+
responseCode = "403",
73+
description = "The task assignment is hidden or matching participation is missing",
74+
content = @Content(
75+
mediaType = "application/json",
76+
schema = @Schema(
77+
implementation = ErrorResponse.class))),
6578
@ApiResponse(
6679
responseCode = "404",
67-
description = "Task was not found",
80+
description = "The task assignment, related tour, or related stage was not found",
6881
content = @Content(
6982
mediaType = "application/json",
70-
schema = @Schema(implementation = ErrorResponse.class)))
83+
schema = @Schema(
84+
implementation = ErrorResponse.class)))
7185
})
7286
@GetMapping
7387
public ResponseEntity<PageResponse<QuestionThreadSummaryResponseDTO>> getParticipantForum(
7488
@Parameter(
75-
description = "Positive identifier of the task whose forum is requested",
89+
description = "Positive identifier of the task assignment whose forum is requested",
7690
example = "42",
77-
required = true) @PathVariable Long taskId,
78-
91+
required = true) @PathVariable Long taskAssignmentId,
7992
@Parameter(
8093
description = "Zero-based page number",
8194
example = "0") @RequestParam(defaultValue = "0") int page,
82-
8395
@Parameter(
8496
description = "Number of questions per page. Must be between 1 and 100",
8597
example = "20") @RequestParam(defaultValue = "20") int size) {
8698
return ResponseEntity.ok(
87-
PageResponse.from(
88-
participantForumService.getForumQuestions(
89-
taskId,
90-
page,
91-
size)));
99+
PageResponse.from(participantForumService.getForumQuestions(taskAssignmentId, page, size)));
92100
}
93101

94102
@Operation(
95-
summary = "Create a participant question",
103+
summary = "Create a question in a task assignment forum",
96104
description = """
97-
Creates a private question in the forum of the specified task.
105+
Creates a private question in the forum of the specified task assignment.
106+
107+
For a participant, the task assignment must be visible and a matching
108+
Participation must exist for the assignment's competition and stage.
109+
A global administrator may access an existing task assignment forum
110+
without a participant record.
111+
112+
The related tour must have the IN_PROGRESS execution status.
98113
99114
The authenticated user becomes the question author. The backend assigns
100-
the initial status NEW, state OPEN, visibility PRIVATE, an empty reviewer,
101-
and version zero.
115+
status NEW, state OPEN, visibility PRIVATE, no reviewer, and version zero.
102116
103-
The request may provide only the question title and content. This is a
104-
temporary TaskBody-scoped API that will be migrated to
105-
TaskAssignment-scoped access.
117+
The request may provide only the question title and content. The task
118+
assignment identifier and all workflow fields are controlled by the
119+
backend.
106120
""")
107-
@ApiResponses(value = {
108-
@ApiResponse(
109-
responseCode = "201",
110-
description = "Question created successfully",
111-
content = @Content(
112-
mediaType = "application/json",
113-
schema = @Schema(implementation = QuestionThreadResponseDTO.class))),
114-
@ApiResponse(
115-
responseCode = "400",
116-
description = "Task identifier or request body is invalid",
117-
content = @Content(
118-
mediaType = "application/json",
119-
schema = @Schema(implementation = ErrorResponse.class))),
120-
@ApiResponse(
121-
responseCode = "401",
122-
description = "Authentication is required",
123-
content = @Content(
124-
mediaType = "application/json",
125-
schema = @Schema(implementation = ErrorResponse.class))),
126-
@ApiResponse(
127-
responseCode = "404",
128-
description = "Task was not found",
129-
content = @Content(
130-
mediaType = "application/json",
131-
schema = @Schema(implementation = ErrorResponse.class)))
132-
})
121+
@ApiResponses(
122+
value = {
123+
@ApiResponse(
124+
responseCode = "201",
125+
description = "Question created successfully",
126+
content = @Content(
127+
mediaType = "application/json",
128+
schema = @Schema(
129+
implementation = QuestionThreadResponseDTO.class))),
130+
@ApiResponse(
131+
responseCode = "400",
132+
description = "Task assignment identifier or request body is invalid",
133+
content = @Content(
134+
mediaType = "application/json",
135+
schema = @Schema(
136+
implementation = ErrorResponse.class))),
137+
@ApiResponse(
138+
responseCode = "401",
139+
description = "Authentication is required",
140+
content = @Content(
141+
mediaType = "application/json",
142+
schema = @Schema(
143+
implementation = ErrorResponse.class))),
144+
@ApiResponse(
145+
responseCode = "403",
146+
description = "The task assignment is hidden or matching participation is missing",
147+
content = @Content(
148+
mediaType = "application/json",
149+
schema = @Schema(
150+
implementation = ErrorResponse.class))),
151+
@ApiResponse(
152+
responseCode = "404",
153+
description = "The task assignment, related tour, or related stage was not found",
154+
content = @Content(
155+
mediaType = "application/json",
156+
schema = @Schema(
157+
implementation = ErrorResponse.class))),
158+
@ApiResponse(
159+
responseCode = "409",
160+
description = "Question creation is not allowed because the related tour is not in progress",
161+
content = @Content(
162+
mediaType = "application/json",
163+
schema = @Schema(
164+
implementation = ErrorResponse.class)))
165+
})
133166
@PostMapping
134167
public ResponseEntity<QuestionThreadResponseDTO> createQuestion(
135168
@Parameter(
136-
description = "Positive identifier of the task in which the question is created",
169+
description = "Positive identifier of the task assignment in which the question is created",
137170
example = "42",
138-
required = true) @PathVariable Long taskId,
171+
required = true) @PathVariable Long taskAssignmentId,
139172
@Valid @RequestBody CreateQuestionRequestDTO request) {
140-
QuestionThreadResponseDTO response =
141-
participantForumService.createQuestion(taskId, request);
173+
QuestionThreadResponseDTO response = participantForumService.createQuestion(taskAssignmentId, request);
142174

143175
return ResponseEntity
144176
.status(HttpStatus.CREATED)
145177
.body(response);
146178
}
147-
}
179+
}

src/main/java/com/itasocialacademy/oitassist/chat/dao/dto/response/QuestionThreadResponseDTO.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
@Schema(description = "Foundational question thread response")
1212
public record QuestionThreadResponseDTO(
1313
Long id,
14-
Long taskId,
14+
Long taskAssignmentId,
1515
Long authorId,
1616
Long assignedReviewerId,
1717
String title,

src/main/java/com/itasocialacademy/oitassist/chat/dao/dto/response/QuestionThreadSummaryResponseDTO.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
@Schema(description = "Question thread summary displayed in the participant forum")
1212
public record QuestionThreadSummaryResponseDTO(
1313
Long id,
14-
Long taskId,
14+
Long taskAssignmentId,
1515
Long authorId,
1616
String title,
1717
QuestionStatus status,

src/main/java/com/itasocialacademy/oitassist/chat/dao/model/QuestionThread.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,8 @@ public class QuestionThread {
3838
@GeneratedValue(strategy = GenerationType.IDENTITY)
3939
private Long id;
4040

41-
/**
42-
* TODO change after TaskAssignment is implemented. Temporary TaskBody
43-
* reference. This field should be replaced with taskAssignmentId after
44-
* TaskAssignment is implemented.
45-
*/
46-
@Column(name = "task_id", nullable = false)
47-
private Long taskId;
41+
@Column(name = "task_assignment_id", nullable = false)
42+
private Long taskAssignmentId;
4843

4944
@CreatedBy
5045
@Column(name = "author_id", nullable = false, updatable = false)

src/main/java/com/itasocialacademy/oitassist/chat/dao/repository/QuestionThreadRepository.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public interface QuestionThreadRepository extends JpaRepository<QuestionThread,
1313
@Query("""
1414
SELECT question
1515
FROM QuestionThread question
16-
WHERE question.taskId = :taskId
16+
WHERE question.taskAssignmentId = :taskAssignmentId
1717
AND (
1818
question.visibility = PUBLIC
1919
OR (
@@ -23,7 +23,7 @@ public interface QuestionThreadRepository extends JpaRepository<QuestionThread,
2323
)
2424
""")
2525
Page<QuestionThread> findParticipantVisibleQuestions(
26-
@Param("taskId") Long taskId,
26+
@Param("taskAssignmentId") Long taskAssignmentId,
2727
@Param("participantId") Long participantId,
2828
Pageable pageable);
2929
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.itasocialacademy.oitassist.chat.exceptions;
2+
3+
import com.itasocialacademy.oitassist.competition.dao.enums.ExecutionStatus;
4+
import com.itasocialacademy.oitassist.core.enums.ErrorCode;
5+
import com.itasocialacademy.oitassist.core.exceptions.BusinessException;
6+
7+
public class QuestionCreationNotAllowedException extends BusinessException {
8+
public QuestionCreationNotAllowedException(Long taskAssignmentId, ExecutionStatus executionStatus) {
9+
super(
10+
"Question creation is not allowed for task assignment with id %s while the tour status is %s"
11+
.formatted(taskAssignmentId, executionStatus),
12+
ErrorCode.QUESTION_INVALID_STATE);
13+
}
14+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.itasocialacademy.oitassist.chat.exceptions;
2+
3+
import com.itasocialacademy.oitassist.core.enums.ErrorCode;
4+
import com.itasocialacademy.oitassist.core.exceptions.AuthorizationException;
5+
6+
public class QuestionForumAccessRestrictedException extends AuthorizationException {
7+
public QuestionForumAccessRestrictedException(Long taskAssignmentId) {
8+
super(
9+
"Access to the question forum for task assignment with id %s is restricted"
10+
.formatted(taskAssignmentId),
11+
ErrorCode.QUESTION_ACCESS_RESTRICTED);
12+
}
13+
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
@ApplicationModule(
22
displayName = "Chat",
33
allowedDependencies = {"core", "core :: AuthenticationException", "security :: SecurityFacade",
4-
"task :: api", "task :: dto", "task :: exceptions"})
4+
"taskassignment :: api", "taskassignment :: dto",
5+
"taskassignment :: enums", "taskassignment :: exceptions",
6+
"competition :: api", "competition :: dto",
7+
"competition :: enums", "competition :: exceptions",
8+
"participation :: api"})
59
package com.itasocialacademy.oitassist.chat;
610

711
import org.springframework.modulith.ApplicationModule;

0 commit comments

Comments
 (0)