• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

classconnect-grupo3 / courses-service / 15891193208

26 Jun 2025 01:58AM UTC coverage: 76.8% (+0.1%) from 76.688%
15891193208

push

github

tinchomorilla
test: add E2E tests for forum participants and implement mock service method

3946 of 5138 relevant lines covered (76.8%)

0.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.37
/src/controller/forum_controller.go
1
package controller
2

3
import (
4
        "log/slog"
5
        "net/http"
6

7
        "courses-service/src/model"
8
        "courses-service/src/schemas"
9
        "courses-service/src/service"
10

11
        "github.com/gin-gonic/gin"
12
)
13

14
type ForumController struct {
15
        service service.ForumServiceInterface
16
}
17

18
func NewForumController(service service.ForumServiceInterface) *ForumController {
1✔
19
        return &ForumController{service: service}
1✔
20
}
1✔
21

22
// Question endpoints
23

24
// @Summary Create a new question
25
// @Description Create a new question in the forum for a specific course
26
// @Tags forum
27
// @Accept json
28
// @Produce json
29
// @Param question body schemas.CreateQuestionRequest true "Question to create"
30
// @Success 201 {object} schemas.QuestionDetailResponse
31
// @Failure 400 {object} schemas.ErrorResponse
32
// @Failure 500 {object} schemas.ErrorResponse
33
// @Router /forum/questions [post]
34
func (c *ForumController) CreateQuestion(ctx *gin.Context) {
1✔
35
        slog.Debug("Creating forum question")
1✔
36

1✔
37
        var request schemas.CreateQuestionRequest
1✔
38
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
39
                slog.Error("Error binding JSON", "error", err)
1✔
40
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
41
                return
1✔
42
        }
1✔
43

44
        question, err := c.service.CreateQuestion(
1✔
45
                request.CourseID,
1✔
46
                request.AuthorID,
1✔
47
                request.Title,
1✔
48
                request.Description,
1✔
49
                request.Tags,
1✔
50
        )
1✔
51
        if err != nil {
2✔
52
                slog.Error("Error creating question", "error", err)
1✔
53
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
54
                return
1✔
55
        }
1✔
56

57
        response := c.mapQuestionToDetailResponse(question)
1✔
58
        slog.Debug("Question created", "question_id", question.ID.Hex())
1✔
59
        ctx.JSON(http.StatusCreated, response)
1✔
60
}
61

62
// @Summary Get question by ID
63
// @Description Get a specific question by its ID with all answers
64
// @Tags forum
65
// @Accept json
66
// @Produce json
67
// @Param questionId path string true "Question ID"
68
// @Success 200 {object} schemas.QuestionDetailResponse
69
// @Failure 404 {object} schemas.ErrorResponse
70
// @Failure 500 {object} schemas.ErrorResponse
71
// @Router /forum/questions/{questionId} [get]
72
func (c *ForumController) GetQuestionById(ctx *gin.Context) {
1✔
73
        slog.Debug("Getting question by ID")
1✔
74

1✔
75
        id := ctx.Param("questionId")
1✔
76
        question, err := c.service.GetQuestionById(id)
1✔
77
        if err != nil {
2✔
78
                slog.Error("Error getting question by ID", "error", err)
1✔
79
                ctx.JSON(http.StatusNotFound, schemas.ErrorResponse{Error: err.Error()})
1✔
80
                return
1✔
81
        }
1✔
82

83
        response := c.mapQuestionToDetailResponse(question)
1✔
84
        slog.Debug("Question retrieved", "question_id", id)
1✔
85
        ctx.JSON(http.StatusOK, response)
1✔
86
}
87

88
// @Summary Get questions by course ID
89
// @Description Get all questions for a specific course
90
// @Tags forum
91
// @Accept json
92
// @Produce json
93
// @Param courseId path string true "Course ID"
94
// @Success 200 {array} schemas.QuestionResponse
95
// @Failure 404 {object} schemas.ErrorResponse
96
// @Failure 500 {object} schemas.ErrorResponse
97
// @Router /forum/courses/{courseId}/questions [get]
98
func (c *ForumController) GetQuestionsByCourseId(ctx *gin.Context) {
1✔
99
        slog.Debug("Getting questions by course ID")
1✔
100

1✔
101
        courseID := ctx.Param("courseId")
1✔
102
        questions, err := c.service.GetQuestionsByCourseId(courseID)
1✔
103
        if err != nil {
2✔
104
                slog.Error("Error getting questions by course ID", "error", err)
1✔
105
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
106
                return
1✔
107
        }
1✔
108

109
        var responses []schemas.QuestionResponse
1✔
110
        for _, question := range questions {
2✔
111
                responses = append(responses, c.mapQuestionToResponse(&question))
1✔
112
        }
1✔
113

114
        slog.Debug("Questions retrieved", "course_id", courseID, "count", len(responses))
1✔
115
        ctx.JSON(http.StatusOK, responses)
1✔
116
}
117

118
// @Summary Update a question
119
// @Description Update a question's title, description, or tags
120
// @Tags forum
121
// @Accept json
122
// @Produce json
123
// @Param questionId path string true "Question ID"
124
// @Param question body schemas.UpdateQuestionRequest true "Question update data"
125
// @Success 200 {object} schemas.QuestionDetailResponse
126
// @Failure 400 {object} schemas.ErrorResponse
127
// @Failure 403 {object} schemas.ErrorResponse
128
// @Failure 404 {object} schemas.ErrorResponse
129
// @Failure 500 {object} schemas.ErrorResponse
130
// @Router /forum/questions/{questionId} [put]
131
func (c *ForumController) UpdateQuestion(ctx *gin.Context) {
1✔
132
        slog.Debug("Updating question")
1✔
133

1✔
134
        id := ctx.Param("questionId")
1✔
135
        var request schemas.UpdateQuestionRequest
1✔
136
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
137
                slog.Error("Error binding JSON", "error", err)
1✔
138
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
139
                return
1✔
140
        }
1✔
141

142
        question, err := c.service.UpdateQuestion(id, request.Title, request.Description, request.Tags)
1✔
143
        if err != nil {
2✔
144
                slog.Error("Error updating question", "error", err)
1✔
145
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
146
                return
1✔
147
        }
1✔
148

149
        response := c.mapQuestionToDetailResponse(question)
1✔
150
        slog.Debug("Question updated", "question_id", id)
1✔
151
        ctx.JSON(http.StatusOK, response)
1✔
152
}
153

154
// @Summary Delete a question
155
// @Description Delete a question (only by the author)
156
// @Tags forum
157
// @Accept json
158
// @Produce json
159
// @Param questionId path string true "Question ID"
160
// @Param authorId query string true "Author ID"
161
// @Success 200 {object} schemas.MessageResponse
162
// @Failure 403 {object} schemas.ErrorResponse
163
// @Failure 404 {object} schemas.ErrorResponse
164
// @Failure 500 {object} schemas.ErrorResponse
165
// @Router /forum/questions/{questionId} [delete]
166
func (c *ForumController) DeleteQuestion(ctx *gin.Context) {
1✔
167
        slog.Debug("Deleting question")
1✔
168

1✔
169
        id := ctx.Param("questionId")
1✔
170
        authorID := ctx.Query("authorId")
1✔
171

1✔
172
        if authorID == "" {
2✔
173
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "authorId query parameter is required"})
1✔
174
                return
1✔
175
        }
1✔
176

177
        err := c.service.DeleteQuestion(id, authorID)
1✔
178
        if err != nil {
2✔
179
                slog.Error("Error deleting question", "error", err)
1✔
180
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
181
                return
1✔
182
        }
1✔
183

184
        slog.Debug("Question deleted", "question_id", id)
1✔
185
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Question deleted successfully"})
1✔
186
}
187

188
// Answer endpoints
189

190
// @Summary Add an answer to a question
191
// @Description Add a new answer to a specific question
192
// @Tags forum
193
// @Accept json
194
// @Produce json
195
// @Param questionId path string true "Question ID"
196
// @Param answer body schemas.CreateAnswerRequest true "Answer to create"
197
// @Success 201 {object} schemas.AnswerResponse
198
// @Failure 400 {object} schemas.ErrorResponse
199
// @Failure 404 {object} schemas.ErrorResponse
200
// @Failure 500 {object} schemas.ErrorResponse
201
// @Router /forum/questions/{questionId}/answers [post]
202
func (c *ForumController) AddAnswer(ctx *gin.Context) {
1✔
203
        slog.Debug("Adding answer to question")
1✔
204

1✔
205
        questionID := ctx.Param("questionId")
1✔
206
        var request schemas.CreateAnswerRequest
1✔
207
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
208
                slog.Error("Error binding JSON", "error", err)
1✔
209
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
210
                return
1✔
211
        }
1✔
212

213
        answer, err := c.service.AddAnswer(questionID, request.AuthorID, request.Content)
1✔
214
        if err != nil {
2✔
215
                slog.Error("Error adding answer", "error", err)
1✔
216
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
217
                return
1✔
218
        }
1✔
219

220
        response := c.mapAnswerToResponse(answer)
1✔
221
        slog.Debug("Answer added", "question_id", questionID, "answer_id", answer.ID)
1✔
222
        ctx.JSON(http.StatusCreated, response)
1✔
223
}
224

225
// @Summary Update an answer
226
// @Description Update an answer's content (only by the author)
227
// @Tags forum
228
// @Accept json
229
// @Produce json
230
// @Param questionId path string true "Question ID"
231
// @Param answerId path string true "Answer ID"
232
// @Param answer body schemas.UpdateAnswerRequest true "Answer update data"
233
// @Param authorId query string true "Author ID"
234
// @Success 200 {object} schemas.AnswerResponse
235
// @Failure 400 {object} schemas.ErrorResponse
236
// @Failure 403 {object} schemas.ErrorResponse
237
// @Failure 404 {object} schemas.ErrorResponse
238
// @Failure 500 {object} schemas.ErrorResponse
239
// @Router /forum/questions/{questionId}/answers/{answerId} [put]
240
func (c *ForumController) UpdateAnswer(ctx *gin.Context) {
1✔
241
        slog.Debug("Updating answer")
1✔
242

1✔
243
        questionID := ctx.Param("questionId")
1✔
244
        answerID := ctx.Param("answerId")
1✔
245
        authorID := ctx.Query("authorId")
1✔
246

1✔
247
        if authorID == "" {
2✔
248
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "authorId query parameter is required"})
1✔
249
                return
1✔
250
        }
1✔
251

252
        var request schemas.UpdateAnswerRequest
1✔
253
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
254
                slog.Error("Error binding JSON", "error", err)
1✔
255
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
256
                return
1✔
257
        }
1✔
258

259
        answer, err := c.service.UpdateAnswer(questionID, answerID, authorID, request.Content)
1✔
260
        if err != nil {
2✔
261
                slog.Error("Error updating answer", "error", err)
1✔
262
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
263
                return
1✔
264
        }
1✔
265

266
        response := c.mapAnswerToResponse(answer)
1✔
267
        slog.Debug("Answer updated", "question_id", questionID, "answer_id", answerID)
1✔
268
        ctx.JSON(http.StatusOK, response)
1✔
269
}
270

271
// @Summary Delete an answer
272
// @Description Delete an answer (only by the author)
273
// @Tags forum
274
// @Accept json
275
// @Produce json
276
// @Param questionId path string true "Question ID"
277
// @Param answerId path string true "Answer ID"
278
// @Param authorId query string true "Author ID"
279
// @Success 200 {object} schemas.MessageResponse
280
// @Failure 403 {object} schemas.ErrorResponse
281
// @Failure 404 {object} schemas.ErrorResponse
282
// @Failure 500 {object} schemas.ErrorResponse
283
// @Router /forum/questions/{questionId}/answers/{answerId} [delete]
284
func (c *ForumController) DeleteAnswer(ctx *gin.Context) {
1✔
285
        slog.Debug("Deleting answer")
1✔
286

1✔
287
        questionID := ctx.Param("questionId")
1✔
288
        answerID := ctx.Param("answerId")
1✔
289
        authorID := ctx.Query("authorId")
1✔
290

1✔
291
        if authorID == "" {
2✔
292
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "authorId query parameter is required"})
1✔
293
                return
1✔
294
        }
1✔
295

296
        err := c.service.DeleteAnswer(questionID, answerID, authorID)
1✔
297
        if err != nil {
2✔
298
                slog.Error("Error deleting answer", "error", err)
1✔
299
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
300
                return
1✔
301
        }
1✔
302

303
        slog.Debug("Answer deleted", "question_id", questionID, "answer_id", answerID)
1✔
304
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Answer deleted successfully"})
1✔
305
}
306

307
// @Summary Accept an answer
308
// @Description Accept an answer as the solution (only by the question author)
309
// @Tags forum
310
// @Accept json
311
// @Produce json
312
// @Param questionId path string true "Question ID"
313
// @Param answerId path string true "Answer ID"
314
// @Param authorId query string true "Question Author ID"
315
// @Success 200 {object} schemas.MessageResponse
316
// @Failure 403 {object} schemas.ErrorResponse
317
// @Failure 404 {object} schemas.ErrorResponse
318
// @Failure 500 {object} schemas.ErrorResponse
319
// @Router /forum/questions/{questionId}/answers/{answerId}/accept [post]
320
func (c *ForumController) AcceptAnswer(ctx *gin.Context) {
1✔
321
        slog.Debug("Accepting answer")
1✔
322

1✔
323
        questionID := ctx.Param("questionId")
1✔
324
        answerID := ctx.Param("answerId")
1✔
325
        authorID := ctx.Query("authorId")
1✔
326

1✔
327
        if authorID == "" {
2✔
328
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "authorId query parameter is required"})
1✔
329
                return
1✔
330
        }
1✔
331

332
        err := c.service.AcceptAnswer(questionID, answerID, authorID)
1✔
333
        if err != nil {
2✔
334
                slog.Error("Error accepting answer", "error", err)
1✔
335
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
336
                return
1✔
337
        }
1✔
338

339
        slog.Debug("Answer accepted", "question_id", questionID, "answer_id", answerID)
1✔
340
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Answer accepted successfully"})
1✔
341
}
342

343
// Vote endpoints
344

345
// @Summary Vote on a question
346
// @Description Vote up or down on a question
347
// @Tags forum
348
// @Accept json
349
// @Produce json
350
// @Param questionId path string true "Question ID"
351
// @Param vote body schemas.VoteRequest true "Vote data"
352
// @Success 200 {object} schemas.VoteResponse
353
// @Failure 400 {object} schemas.ErrorResponse
354
// @Failure 403 {object} schemas.ErrorResponse
355
// @Failure 404 {object} schemas.ErrorResponse
356
// @Failure 500 {object} schemas.ErrorResponse
357
// @Router /forum/questions/{questionId}/vote [post]
358
func (c *ForumController) VoteQuestion(ctx *gin.Context) {
1✔
359
        slog.Debug("Voting on question")
1✔
360

1✔
361
        questionID := ctx.Param("questionId")
1✔
362
        var request schemas.VoteRequest
1✔
363
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
364
                slog.Error("Error binding JSON", "error", err)
1✔
365
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
366
                return
1✔
367
        }
1✔
368

369
        err := c.service.VoteQuestion(questionID, request.UserID, request.VoteType)
1✔
370
        if err != nil {
2✔
371
                slog.Error("Error voting on question", "error", err)
1✔
372
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
373
                return
1✔
374
        }
1✔
375

376
        voteTypeStr := "up"
1✔
377
        if request.VoteType == model.VoteTypeDown {
2✔
378
                voteTypeStr = "down"
1✔
379
        }
1✔
380

381
        slog.Debug("Vote registered", "question_id", questionID, "vote_type", voteTypeStr)
1✔
382
        ctx.JSON(http.StatusOK, schemas.VoteResponse{Message: "Vote registered successfully"})
1✔
383
}
384

385
// @Summary Vote on an answer
386
// @Description Vote up or down on an answer
387
// @Tags forum
388
// @Accept json
389
// @Produce json
390
// @Param questionId path string true "Question ID"
391
// @Param answerId path string true "Answer ID"
392
// @Param vote body schemas.VoteRequest true "Vote data"
393
// @Success 200 {object} schemas.VoteResponse
394
// @Failure 400 {object} schemas.ErrorResponse
395
// @Failure 403 {object} schemas.ErrorResponse
396
// @Failure 404 {object} schemas.ErrorResponse
397
// @Failure 500 {object} schemas.ErrorResponse
398
// @Router /forum/questions/{questionId}/answers/{answerId}/vote [post]
399
func (c *ForumController) VoteAnswer(ctx *gin.Context) {
1✔
400
        slog.Debug("Voting on answer")
1✔
401

1✔
402
        questionID := ctx.Param("questionId")
1✔
403
        answerID := ctx.Param("answerId")
1✔
404
        var request schemas.VoteRequest
1✔
405
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
406
                slog.Error("Error binding JSON", "error", err)
1✔
407
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
408
                return
1✔
409
        }
1✔
410

411
        err := c.service.VoteAnswer(questionID, answerID, request.UserID, request.VoteType)
1✔
412
        if err != nil {
2✔
413
                slog.Error("Error voting on answer", "error", err)
1✔
414
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
415
                return
1✔
416
        }
1✔
417

418
        voteTypeStr := "up"
1✔
419
        if request.VoteType == model.VoteTypeDown {
2✔
420
                voteTypeStr = "down"
1✔
421
        }
1✔
422

423
        slog.Debug("Vote registered", "question_id", questionID, "answer_id", answerID, "vote_type", voteTypeStr)
1✔
424
        ctx.JSON(http.StatusOK, schemas.VoteResponse{Message: "Vote registered successfully"})
1✔
425
}
426

427
// @Summary Remove vote from question
428
// @Description Remove a user's vote from a question
429
// @Tags forum
430
// @Accept json
431
// @Produce json
432
// @Param questionId path string true "Question ID"
433
// @Param userId query string true "User ID"
434
// @Success 200 {object} schemas.MessageResponse
435
// @Failure 404 {object} schemas.ErrorResponse
436
// @Failure 500 {object} schemas.ErrorResponse
437
// @Router /forum/questions/{questionId}/vote [delete]
438
func (c *ForumController) RemoveVoteFromQuestion(ctx *gin.Context) {
1✔
439
        slog.Debug("Removing vote from question")
1✔
440

1✔
441
        questionID := ctx.Param("questionId")
1✔
442
        userID := ctx.Query("userId")
1✔
443

1✔
444
        if userID == "" {
2✔
445
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "userId query parameter is required"})
1✔
446
                return
1✔
447
        }
1✔
448

449
        err := c.service.RemoveVoteFromQuestion(questionID, userID)
1✔
450
        if err != nil {
2✔
451
                slog.Error("Error removing vote from question", "error", err)
1✔
452
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
453
                return
1✔
454
        }
1✔
455

456
        slog.Debug("Vote removed", "question_id", questionID, "user_id", userID)
1✔
457
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Vote removed successfully"})
1✔
458
}
459

460
// @Summary Remove vote from answer
461
// @Description Remove a user's vote from an answer
462
// @Tags forum
463
// @Accept json
464
// @Produce json
465
// @Param questionId path string true "Question ID"
466
// @Param answerId path string true "Answer ID"
467
// @Param userId query string true "User ID"
468
// @Success 200 {object} schemas.MessageResponse
469
// @Failure 404 {object} schemas.ErrorResponse
470
// @Failure 500 {object} schemas.ErrorResponse
471
// @Router /forum/questions/{questionId}/answers/{answerId}/vote [delete]
472
func (c *ForumController) RemoveVoteFromAnswer(ctx *gin.Context) {
1✔
473
        slog.Debug("Removing vote from answer")
1✔
474

1✔
475
        questionID := ctx.Param("questionId")
1✔
476
        answerID := ctx.Param("answerId")
1✔
477
        userID := ctx.Query("userId")
1✔
478

1✔
479
        if userID == "" {
2✔
480
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "userId query parameter is required"})
1✔
481
                return
1✔
482
        }
1✔
483

484
        err := c.service.RemoveVoteFromAnswer(questionID, answerID, userID)
1✔
485
        if err != nil {
2✔
486
                slog.Error("Error removing vote from answer", "error", err)
1✔
487
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
488
                return
1✔
489
        }
1✔
490

491
        slog.Debug("Vote removed", "question_id", questionID, "answer_id", answerID, "user_id", userID)
1✔
492
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Vote removed successfully"})
1✔
493
}
494

495
// Search endpoints
496

497
// @Summary Search questions
498
// @Description Search questions in a course with optional filters
499
// @Tags forum
500
// @Accept json
501
// @Produce json
502
// @Param courseId path string true "Course ID"
503
// @Param query query string false "Search query"
504
// @Param tags query []string false "Filter by tags"
505
// @Param status query string false "Filter by status"
506
// @Success 200 {object} schemas.SearchQuestionsResponse
507
// @Failure 400 {object} schemas.ErrorResponse
508
// @Failure 500 {object} schemas.ErrorResponse
509
// @Router /forum/courses/{courseId}/search [get]
510
func (c *ForumController) SearchQuestions(ctx *gin.Context) {
1✔
511
        slog.Debug("Searching questions")
1✔
512

1✔
513
        courseID := ctx.Param("courseId")
1✔
514

1✔
515
        var request schemas.SearchQuestionsRequest
1✔
516
        if err := ctx.ShouldBindQuery(&request); err != nil {
1✔
517
                slog.Error("Error binding query parameters", "error", err)
×
518
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
×
519
                return
×
520
        }
×
521

522
        questions, err := c.service.SearchQuestions(courseID, request.Query, request.Tags, request.Status)
1✔
523
        if err != nil {
2✔
524
                slog.Error("Error searching questions", "error", err)
1✔
525
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
526
                return
1✔
527
        }
1✔
528

529
        var questionResponses []schemas.QuestionResponse
1✔
530
        for _, question := range questions {
2✔
531
                questionResponses = append(questionResponses, c.mapQuestionToResponse(&question))
1✔
532
        }
1✔
533

534
        response := schemas.SearchQuestionsResponse{
1✔
535
                Questions: questionResponses,
1✔
536
                Total:     len(questionResponses),
1✔
537
        }
1✔
538

1✔
539
        slog.Debug("Questions searched", "course_id", courseID, "total", response.Total)
1✔
540
        ctx.JSON(http.StatusOK, response)
1✔
541
}
542

543
// @Summary Get forum participants
544
// @Description Get all unique participants (authors, answerers, voters) for a specific course forum
545
// @Tags forum
546
// @Accept json
547
// @Produce json
548
// @Param courseId path string true "Course ID"
549
// @Success 200 {object} schemas.ForumParticipantsResponse
550
// @Failure 400 {object} schemas.ErrorResponse
551
// @Failure 404 {object} schemas.ErrorResponse
552
// @Failure 500 {object} schemas.ErrorResponse
553
// @Router /forum/courses/{courseId}/participants [get]
554
func (c *ForumController) GetForumParticipants(ctx *gin.Context) {
1✔
555
        slog.Debug("Getting forum participants")
1✔
556

1✔
557
        courseID := ctx.Param("courseId")
1✔
558
        participants, err := c.service.GetForumParticipants(courseID)
1✔
559
        if err != nil {
2✔
560
                slog.Error("Error getting forum participants", "error", err)
1✔
561
                if err.Error() == "course not found" {
2✔
562
                        ctx.JSON(http.StatusNotFound, schemas.ErrorResponse{Error: err.Error()})
1✔
563
                } else {
1✔
564
                        ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
×
565
                }
×
566
                return
1✔
567
        }
568

569
        response := schemas.ForumParticipantsResponse{
1✔
570
                Participants: participants,
1✔
571
        }
1✔
572

1✔
573
        slog.Debug("Forum participants retrieved", "course_id", courseID, "total", len(participants))
1✔
574
        ctx.JSON(http.StatusOK, response)
1✔
575
}
576

577
// Helper methods for mapping models to responses
578

579
func (c *ForumController) mapQuestionToResponse(question *model.ForumQuestion) schemas.QuestionResponse {
1✔
580
        voteCount := c.calculateVoteCount(question.Votes)
1✔
581
        answerCount := len(question.Answers)
1✔
582

1✔
583
        return schemas.QuestionResponse{
1✔
584
                ID:               question.ID.Hex(),
1✔
585
                CourseID:         question.CourseID,
1✔
586
                AuthorID:         question.AuthorID,
1✔
587
                Title:            question.Title,
1✔
588
                Description:      question.Description,
1✔
589
                Tags:             question.Tags,
1✔
590
                Votes:            question.Votes,
1✔
591
                VoteCount:        voteCount,
1✔
592
                AnswerCount:      answerCount,
1✔
593
                Status:           question.Status,
1✔
594
                AcceptedAnswerID: question.AcceptedAnswerID,
1✔
595
                CreatedAt:        question.CreatedAt,
1✔
596
                UpdatedAt:        question.UpdatedAt,
1✔
597
        }
1✔
598
}
1✔
599

600
func (c *ForumController) mapQuestionToDetailResponse(question *model.ForumQuestion) schemas.QuestionDetailResponse {
1✔
601
        voteCount := c.calculateVoteCount(question.Votes)
1✔
602

1✔
603
        var answers []schemas.AnswerResponse
1✔
604
        for _, answer := range question.Answers {
2✔
605
                answers = append(answers, c.mapAnswerToResponse(&answer))
1✔
606
        }
1✔
607

608
        return schemas.QuestionDetailResponse{
1✔
609
                ID:               question.ID.Hex(),
1✔
610
                CourseID:         question.CourseID,
1✔
611
                AuthorID:         question.AuthorID,
1✔
612
                Title:            question.Title,
1✔
613
                Description:      question.Description,
1✔
614
                Tags:             question.Tags,
1✔
615
                Votes:            question.Votes,
1✔
616
                VoteCount:        voteCount,
1✔
617
                Answers:          answers,
1✔
618
                Status:           question.Status,
1✔
619
                AcceptedAnswerID: question.AcceptedAnswerID,
1✔
620
                CreatedAt:        question.CreatedAt,
1✔
621
                UpdatedAt:        question.UpdatedAt,
1✔
622
        }
1✔
623
}
624

625
func (c *ForumController) mapAnswerToResponse(answer *model.ForumAnswer) schemas.AnswerResponse {
1✔
626
        voteCount := c.calculateVoteCount(answer.Votes)
1✔
627

1✔
628
        return schemas.AnswerResponse{
1✔
629
                ID:         answer.ID,
1✔
630
                AuthorID:   answer.AuthorID,
1✔
631
                Content:    answer.Content,
1✔
632
                Votes:      answer.Votes,
1✔
633
                VoteCount:  voteCount,
1✔
634
                IsAccepted: answer.IsAccepted,
1✔
635
                CreatedAt:  answer.CreatedAt,
1✔
636
                UpdatedAt:  answer.UpdatedAt,
1✔
637
        }
1✔
638
}
1✔
639

640
func (c *ForumController) calculateVoteCount(votes []model.Vote) int {
1✔
641
        count := 0
1✔
642
        for _, vote := range votes {
2✔
643
                count += vote.VoteType
1✔
644
        }
1✔
645
        return count
1✔
646
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc