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

classconnect-grupo3 / courses-service / 15892339239

26 Jun 2025 03:34AM UTC coverage: 74.755% (-2.0%) from 76.8%
15892339239

Pull #47

github

rovifran
fixed tests
Pull Request #47: Features/log aux teachers activity

133 of 299 new or added lines in 10 files covered. (44.48%)

10 existing lines in 3 files now uncovered.

4051 of 5419 relevant lines covered (74.76%)

0.83 hits per line

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

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

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

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

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

15
type ForumController struct {
16
        service         service.ForumServiceInterface
17
        activityService service.TeacherActivityServiceInterface
18
}
19

20
func NewForumController(service service.ForumServiceInterface, activityService service.TeacherActivityServiceInterface) *ForumController {
1✔
21
        return &ForumController{
1✔
22
                service:         service,
1✔
23
                activityService: activityService,
1✔
24
        }
1✔
25
}
1✔
26

27
// Question endpoints
28

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

1✔
42
        var request schemas.CreateQuestionRequest
1✔
43
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
44
                slog.Error("Error binding JSON", "error", err)
1✔
45
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
46
                return
1✔
47
        }
1✔
48

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

62
        // Log activity if teacher is auxiliary
63
        teacherUUID := ctx.GetHeader("X-Teacher-UUID")
1✔
64
        if teacherUUID != "" && teacherUUID == request.AuthorID {
1✔
NEW
65
                c.activityService.LogActivityIfAuxTeacher(
×
NEW
66
                        request.CourseID,
×
NEW
67
                        teacherUUID,
×
NEW
68
                        "CREATE_FORUM_QUESTION",
×
NEW
69
                        fmt.Sprintf("Created forum question: %s", request.Title),
×
NEW
70
                )
×
NEW
71
        }
×
72

73
        response := c.mapQuestionToDetailResponse(question)
1✔
74
        slog.Debug("Question created", "question_id", question.ID.Hex())
1✔
75
        ctx.JSON(http.StatusCreated, response)
1✔
76
}
77

78
// @Summary Get question by ID
79
// @Description Get a specific question by its ID with all answers
80
// @Tags forum
81
// @Accept json
82
// @Produce json
83
// @Param questionId path string true "Question ID"
84
// @Success 200 {object} schemas.QuestionDetailResponse
85
// @Failure 404 {object} schemas.ErrorResponse
86
// @Failure 500 {object} schemas.ErrorResponse
87
// @Router /forum/questions/{questionId} [get]
88
func (c *ForumController) GetQuestionById(ctx *gin.Context) {
1✔
89
        slog.Debug("Getting question by ID")
1✔
90

1✔
91
        id := ctx.Param("questionId")
1✔
92
        question, err := c.service.GetQuestionById(id)
1✔
93
        if err != nil {
2✔
94
                slog.Error("Error getting question by ID", "error", err)
1✔
95
                ctx.JSON(http.StatusNotFound, schemas.ErrorResponse{Error: err.Error()})
1✔
96
                return
1✔
97
        }
1✔
98

99
        response := c.mapQuestionToDetailResponse(question)
1✔
100
        slog.Debug("Question retrieved", "question_id", id)
1✔
101
        ctx.JSON(http.StatusOK, response)
1✔
102
}
103

104
// @Summary Get questions by course ID
105
// @Description Get all questions for a specific course
106
// @Tags forum
107
// @Accept json
108
// @Produce json
109
// @Param courseId path string true "Course ID"
110
// @Success 200 {array} schemas.QuestionResponse
111
// @Failure 404 {object} schemas.ErrorResponse
112
// @Failure 500 {object} schemas.ErrorResponse
113
// @Router /forum/courses/{courseId}/questions [get]
114
func (c *ForumController) GetQuestionsByCourseId(ctx *gin.Context) {
1✔
115
        slog.Debug("Getting questions by course ID")
1✔
116

1✔
117
        courseID := ctx.Param("courseId")
1✔
118
        questions, err := c.service.GetQuestionsByCourseId(courseID)
1✔
119
        if err != nil {
2✔
120
                slog.Error("Error getting questions by course ID", "error", err)
1✔
121
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
122
                return
1✔
123
        }
1✔
124

125
        var responses []schemas.QuestionResponse
1✔
126
        for _, question := range questions {
2✔
127
                responses = append(responses, c.mapQuestionToResponse(&question))
1✔
128
        }
1✔
129

130
        slog.Debug("Questions retrieved", "course_id", courseID, "count", len(responses))
1✔
131
        ctx.JSON(http.StatusOK, responses)
1✔
132
}
133

134
// @Summary Update a question
135
// @Description Update a question's title, description, or tags
136
// @Tags forum
137
// @Accept json
138
// @Produce json
139
// @Param questionId path string true "Question ID"
140
// @Param question body schemas.UpdateQuestionRequest true "Question update data"
141
// @Success 200 {object} schemas.QuestionDetailResponse
142
// @Failure 400 {object} schemas.ErrorResponse
143
// @Failure 403 {object} schemas.ErrorResponse
144
// @Failure 404 {object} schemas.ErrorResponse
145
// @Failure 500 {object} schemas.ErrorResponse
146
// @Router /forum/questions/{questionId} [put]
147
func (c *ForumController) UpdateQuestion(ctx *gin.Context) {
1✔
148
        slog.Debug("Updating question")
1✔
149

1✔
150
        id := ctx.Param("questionId")
1✔
151
        var request schemas.UpdateQuestionRequest
1✔
152
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
153
                slog.Error("Error binding JSON", "error", err)
1✔
154
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
155
                return
1✔
156
        }
1✔
157

158
        question, err := c.service.UpdateQuestion(id, request.Title, request.Description, request.Tags)
1✔
159
        if err != nil {
2✔
160
                slog.Error("Error updating question", "error", err)
1✔
161
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
162
                return
1✔
163
        }
1✔
164

165
        // Log activity if teacher is auxiliary
166
        teacherUUID := ctx.GetHeader("X-Teacher-UUID")
1✔
167
        if teacherUUID != "" && question != nil {
1✔
NEW
168
                c.activityService.LogActivityIfAuxTeacher(
×
NEW
169
                        question.CourseID,
×
NEW
170
                        teacherUUID,
×
NEW
171
                        "UPDATE_FORUM_QUESTION",
×
NEW
172
                        fmt.Sprintf("Updated forum question: %s", request.Title),
×
NEW
173
                )
×
NEW
174
        }
×
175

176
        response := c.mapQuestionToDetailResponse(question)
1✔
177
        slog.Debug("Question updated", "question_id", id)
1✔
178
        ctx.JSON(http.StatusOK, response)
1✔
179
}
180

181
// @Summary Delete a question
182
// @Description Delete a question (only by the author)
183
// @Tags forum
184
// @Accept json
185
// @Produce json
186
// @Param questionId path string true "Question ID"
187
// @Param authorId query string true "Author ID"
188
// @Success 200 {object} schemas.MessageResponse
189
// @Failure 403 {object} schemas.ErrorResponse
190
// @Failure 404 {object} schemas.ErrorResponse
191
// @Failure 500 {object} schemas.ErrorResponse
192
// @Router /forum/questions/{questionId} [delete]
193
func (c *ForumController) DeleteQuestion(ctx *gin.Context) {
1✔
194
        slog.Debug("Deleting question")
1✔
195

1✔
196
        id := ctx.Param("questionId")
1✔
197
        authorID := ctx.Query("authorId")
1✔
198

1✔
199
        if authorID == "" {
2✔
200
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "authorId query parameter is required"})
1✔
201
                return
1✔
202
        }
1✔
203

204
        // Get question before deleting for logging
205
        question, qErr := c.service.GetQuestionById(id)
1✔
206

1✔
207
        err := c.service.DeleteQuestion(id, authorID)
1✔
208
        if err != nil {
2✔
209
                slog.Error("Error deleting question", "error", err)
1✔
210
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
211
                return
1✔
212
        }
1✔
213

214
        // Log activity if teacher is auxiliary
215
        teacherUUID := ctx.GetHeader("X-Teacher-UUID")
1✔
216
        if teacherUUID != "" && teacherUUID == authorID && qErr == nil && question != nil {
1✔
NEW
217
                c.activityService.LogActivityIfAuxTeacher(
×
NEW
218
                        question.CourseID,
×
NEW
219
                        teacherUUID,
×
NEW
220
                        "DELETE_FORUM_QUESTION",
×
NEW
221
                        fmt.Sprintf("Deleted forum question: %s", question.Title),
×
NEW
222
                )
×
NEW
223
        }
×
224

225
        slog.Debug("Question deleted", "question_id", id)
1✔
226
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Question deleted successfully"})
1✔
227
}
228

229
// Answer endpoints
230

231
// @Summary Add an answer to a question
232
// @Description Add a new answer to a specific question
233
// @Tags forum
234
// @Accept json
235
// @Produce json
236
// @Param questionId path string true "Question ID"
237
// @Param answer body schemas.CreateAnswerRequest true "Answer to create"
238
// @Success 201 {object} schemas.AnswerResponse
239
// @Failure 400 {object} schemas.ErrorResponse
240
// @Failure 404 {object} schemas.ErrorResponse
241
// @Failure 500 {object} schemas.ErrorResponse
242
// @Router /forum/questions/{questionId}/answers [post]
243
func (c *ForumController) AddAnswer(ctx *gin.Context) {
1✔
244
        slog.Debug("Adding answer to question")
1✔
245

1✔
246
        questionID := ctx.Param("questionId")
1✔
247
        var request schemas.CreateAnswerRequest
1✔
248
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
249
                slog.Error("Error binding JSON", "error", err)
1✔
250
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
251
                return
1✔
252
        }
1✔
253

254
        answer, err := c.service.AddAnswer(questionID, request.AuthorID, request.Content)
1✔
255
        if err != nil {
2✔
256
                slog.Error("Error adding answer", "error", err)
1✔
257
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
258
                return
1✔
259
        }
1✔
260

261
        // Log activity if teacher is auxiliary - we need to get the question to know the course
262
        teacherUUID := ctx.GetHeader("X-Teacher-UUID")
1✔
263
        if teacherUUID != "" && teacherUUID == request.AuthorID {
1✔
NEW
264
                // Get the question to find the course ID
×
NEW
265
                question, qErr := c.service.GetQuestionById(questionID)
×
NEW
266
                if qErr == nil && question != nil {
×
NEW
267
                        c.activityService.LogActivityIfAuxTeacher(
×
NEW
268
                                question.CourseID,
×
NEW
269
                                teacherUUID,
×
NEW
270
                                "CREATE_FORUM_ANSWER",
×
NEW
271
                                "Created forum answer",
×
NEW
272
                        )
×
NEW
273
                }
×
274
        }
275

276
        response := c.mapAnswerToResponse(answer)
1✔
277
        slog.Debug("Answer added", "question_id", questionID, "answer_id", answer.ID)
1✔
278
        ctx.JSON(http.StatusCreated, response)
1✔
279
}
280

281
// @Summary Update an answer
282
// @Description Update an answer's content (only by the author)
283
// @Tags forum
284
// @Accept json
285
// @Produce json
286
// @Param questionId path string true "Question ID"
287
// @Param answerId path string true "Answer ID"
288
// @Param answer body schemas.UpdateAnswerRequest true "Answer update data"
289
// @Param authorId query string true "Author ID"
290
// @Success 200 {object} schemas.AnswerResponse
291
// @Failure 400 {object} schemas.ErrorResponse
292
// @Failure 403 {object} schemas.ErrorResponse
293
// @Failure 404 {object} schemas.ErrorResponse
294
// @Failure 500 {object} schemas.ErrorResponse
295
// @Router /forum/questions/{questionId}/answers/{answerId} [put]
296
func (c *ForumController) UpdateAnswer(ctx *gin.Context) {
1✔
297
        slog.Debug("Updating answer")
1✔
298

1✔
299
        questionID := ctx.Param("questionId")
1✔
300
        answerID := ctx.Param("answerId")
1✔
301
        authorID := ctx.Query("authorId")
1✔
302

1✔
303
        if authorID == "" {
2✔
304
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "authorId query parameter is required"})
1✔
305
                return
1✔
306
        }
1✔
307

308
        var request schemas.UpdateAnswerRequest
1✔
309
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
310
                slog.Error("Error binding JSON", "error", err)
1✔
311
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
312
                return
1✔
313
        }
1✔
314

315
        answer, err := c.service.UpdateAnswer(questionID, answerID, authorID, request.Content)
1✔
316
        if err != nil {
2✔
317
                slog.Error("Error updating answer", "error", err)
1✔
318
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
319
                return
1✔
320
        }
1✔
321

322
        response := c.mapAnswerToResponse(answer)
1✔
323
        slog.Debug("Answer updated", "question_id", questionID, "answer_id", answerID)
1✔
324
        ctx.JSON(http.StatusOK, response)
1✔
325
}
326

327
// @Summary Delete an answer
328
// @Description Delete an answer (only by the author)
329
// @Tags forum
330
// @Accept json
331
// @Produce json
332
// @Param questionId path string true "Question ID"
333
// @Param answerId path string true "Answer ID"
334
// @Param authorId query string true "Author ID"
335
// @Success 200 {object} schemas.MessageResponse
336
// @Failure 403 {object} schemas.ErrorResponse
337
// @Failure 404 {object} schemas.ErrorResponse
338
// @Failure 500 {object} schemas.ErrorResponse
339
// @Router /forum/questions/{questionId}/answers/{answerId} [delete]
340
func (c *ForumController) DeleteAnswer(ctx *gin.Context) {
1✔
341
        slog.Debug("Deleting answer")
1✔
342

1✔
343
        questionID := ctx.Param("questionId")
1✔
344
        answerID := ctx.Param("answerId")
1✔
345
        authorID := ctx.Query("authorId")
1✔
346

1✔
347
        if authorID == "" {
2✔
348
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "authorId query parameter is required"})
1✔
349
                return
1✔
350
        }
1✔
351

352
        err := c.service.DeleteAnswer(questionID, answerID, authorID)
1✔
353
        if err != nil {
2✔
354
                slog.Error("Error deleting answer", "error", err)
1✔
355
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
356
                return
1✔
357
        }
1✔
358

359
        slog.Debug("Answer deleted", "question_id", questionID, "answer_id", answerID)
1✔
360
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Answer deleted successfully"})
1✔
361
}
362

363
// @Summary Accept an answer
364
// @Description Accept an answer as the solution (only by the question author)
365
// @Tags forum
366
// @Accept json
367
// @Produce json
368
// @Param questionId path string true "Question ID"
369
// @Param answerId path string true "Answer ID"
370
// @Param authorId query string true "Question Author ID"
371
// @Success 200 {object} schemas.MessageResponse
372
// @Failure 403 {object} schemas.ErrorResponse
373
// @Failure 404 {object} schemas.ErrorResponse
374
// @Failure 500 {object} schemas.ErrorResponse
375
// @Router /forum/questions/{questionId}/answers/{answerId}/accept [post]
376
func (c *ForumController) AcceptAnswer(ctx *gin.Context) {
1✔
377
        slog.Debug("Accepting answer")
1✔
378

1✔
379
        questionID := ctx.Param("questionId")
1✔
380
        answerID := ctx.Param("answerId")
1✔
381
        authorID := ctx.Query("authorId")
1✔
382

1✔
383
        if authorID == "" {
2✔
384
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "authorId query parameter is required"})
1✔
385
                return
1✔
386
        }
1✔
387

388
        err := c.service.AcceptAnswer(questionID, answerID, authorID)
1✔
389
        if err != nil {
2✔
390
                slog.Error("Error accepting answer", "error", err)
1✔
391
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
392
                return
1✔
393
        }
1✔
394

395
        // Log activity if teacher is auxiliary
396
        teacherUUID := ctx.GetHeader("X-Teacher-UUID")
1✔
397
        if teacherUUID != "" && teacherUUID == authorID {
1✔
NEW
398
                // Get the question to find the course ID
×
NEW
399
                question, qErr := c.service.GetQuestionById(questionID)
×
NEW
400
                if qErr == nil && question != nil {
×
NEW
401
                        c.activityService.LogActivityIfAuxTeacher(
×
NEW
402
                                question.CourseID,
×
NEW
403
                                teacherUUID,
×
NEW
404
                                "ACCEPT_FORUM_ANSWER",
×
NEW
405
                                "Accepted forum answer",
×
NEW
406
                        )
×
NEW
407
                }
×
408
        }
409

410
        slog.Debug("Answer accepted", "question_id", questionID, "answer_id", answerID)
1✔
411
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Answer accepted successfully"})
1✔
412
}
413

414
// Vote endpoints
415

416
// @Summary Vote on a question
417
// @Description Vote up or down on a question
418
// @Tags forum
419
// @Accept json
420
// @Produce json
421
// @Param questionId path string true "Question ID"
422
// @Param vote body schemas.VoteRequest true "Vote data"
423
// @Success 200 {object} schemas.VoteResponse
424
// @Failure 400 {object} schemas.ErrorResponse
425
// @Failure 403 {object} schemas.ErrorResponse
426
// @Failure 404 {object} schemas.ErrorResponse
427
// @Failure 500 {object} schemas.ErrorResponse
428
// @Router /forum/questions/{questionId}/vote [post]
429
func (c *ForumController) VoteQuestion(ctx *gin.Context) {
1✔
430
        slog.Debug("Voting on question")
1✔
431

1✔
432
        questionID := ctx.Param("questionId")
1✔
433
        var request schemas.VoteRequest
1✔
434
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
435
                slog.Error("Error binding JSON", "error", err)
1✔
436
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
437
                return
1✔
438
        }
1✔
439

440
        err := c.service.VoteQuestion(questionID, request.UserID, request.VoteType)
1✔
441
        if err != nil {
2✔
442
                slog.Error("Error voting on question", "error", err)
1✔
443
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
444
                return
1✔
445
        }
1✔
446

447
        voteTypeStr := "up"
1✔
448
        if request.VoteType == model.VoteTypeDown {
2✔
449
                voteTypeStr = "down"
1✔
450
        }
1✔
451

452
        slog.Debug("Vote registered", "question_id", questionID, "vote_type", voteTypeStr)
1✔
453
        ctx.JSON(http.StatusOK, schemas.VoteResponse{Message: "Vote registered successfully"})
1✔
454
}
455

456
// @Summary Vote on an answer
457
// @Description Vote up or down on an answer
458
// @Tags forum
459
// @Accept json
460
// @Produce json
461
// @Param questionId path string true "Question ID"
462
// @Param answerId path string true "Answer ID"
463
// @Param vote body schemas.VoteRequest true "Vote data"
464
// @Success 200 {object} schemas.VoteResponse
465
// @Failure 400 {object} schemas.ErrorResponse
466
// @Failure 403 {object} schemas.ErrorResponse
467
// @Failure 404 {object} schemas.ErrorResponse
468
// @Failure 500 {object} schemas.ErrorResponse
469
// @Router /forum/questions/{questionId}/answers/{answerId}/vote [post]
470
func (c *ForumController) VoteAnswer(ctx *gin.Context) {
1✔
471
        slog.Debug("Voting on answer")
1✔
472

1✔
473
        questionID := ctx.Param("questionId")
1✔
474
        answerID := ctx.Param("answerId")
1✔
475
        var request schemas.VoteRequest
1✔
476
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
477
                slog.Error("Error binding JSON", "error", err)
1✔
478
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
479
                return
1✔
480
        }
1✔
481

482
        err := c.service.VoteAnswer(questionID, answerID, request.UserID, request.VoteType)
1✔
483
        if err != nil {
2✔
484
                slog.Error("Error voting on answer", "error", err)
1✔
485
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
486
                return
1✔
487
        }
1✔
488

489
        voteTypeStr := "up"
1✔
490
        if request.VoteType == model.VoteTypeDown {
2✔
491
                voteTypeStr = "down"
1✔
492
        }
1✔
493

494
        slog.Debug("Vote registered", "question_id", questionID, "answer_id", answerID, "vote_type", voteTypeStr)
1✔
495
        ctx.JSON(http.StatusOK, schemas.VoteResponse{Message: "Vote registered successfully"})
1✔
496
}
497

498
// @Summary Remove vote from question
499
// @Description Remove a user's vote from a question
500
// @Tags forum
501
// @Accept json
502
// @Produce json
503
// @Param questionId path string true "Question ID"
504
// @Param userId query string true "User ID"
505
// @Success 200 {object} schemas.MessageResponse
506
// @Failure 404 {object} schemas.ErrorResponse
507
// @Failure 500 {object} schemas.ErrorResponse
508
// @Router /forum/questions/{questionId}/vote [delete]
509
func (c *ForumController) RemoveVoteFromQuestion(ctx *gin.Context) {
1✔
510
        slog.Debug("Removing vote from question")
1✔
511

1✔
512
        questionID := ctx.Param("questionId")
1✔
513
        userID := ctx.Query("userId")
1✔
514

1✔
515
        if userID == "" {
2✔
516
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "userId query parameter is required"})
1✔
517
                return
1✔
518
        }
1✔
519

520
        err := c.service.RemoveVoteFromQuestion(questionID, userID)
1✔
521
        if err != nil {
2✔
522
                slog.Error("Error removing vote from question", "error", err)
1✔
523
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
524
                return
1✔
525
        }
1✔
526

527
        slog.Debug("Vote removed", "question_id", questionID, "user_id", userID)
1✔
528
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Vote removed successfully"})
1✔
529
}
530

531
// @Summary Remove vote from answer
532
// @Description Remove a user's vote from an answer
533
// @Tags forum
534
// @Accept json
535
// @Produce json
536
// @Param questionId path string true "Question ID"
537
// @Param answerId path string true "Answer ID"
538
// @Param userId query string true "User ID"
539
// @Success 200 {object} schemas.MessageResponse
540
// @Failure 404 {object} schemas.ErrorResponse
541
// @Failure 500 {object} schemas.ErrorResponse
542
// @Router /forum/questions/{questionId}/answers/{answerId}/vote [delete]
543
func (c *ForumController) RemoveVoteFromAnswer(ctx *gin.Context) {
1✔
544
        slog.Debug("Removing vote from answer")
1✔
545

1✔
546
        questionID := ctx.Param("questionId")
1✔
547
        answerID := ctx.Param("answerId")
1✔
548
        userID := ctx.Query("userId")
1✔
549

1✔
550
        if userID == "" {
2✔
551
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "userId query parameter is required"})
1✔
552
                return
1✔
553
        }
1✔
554

555
        err := c.service.RemoveVoteFromAnswer(questionID, answerID, userID)
1✔
556
        if err != nil {
2✔
557
                slog.Error("Error removing vote from answer", "error", err)
1✔
558
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
559
                return
1✔
560
        }
1✔
561

562
        slog.Debug("Vote removed", "question_id", questionID, "answer_id", answerID, "user_id", userID)
1✔
563
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Vote removed successfully"})
1✔
564
}
565

566
// Search endpoints
567

568
// @Summary Search questions
569
// @Description Search questions in a course with optional filters
570
// @Tags forum
571
// @Accept json
572
// @Produce json
573
// @Param courseId path string true "Course ID"
574
// @Param query query string false "Search query"
575
// @Param tags query []string false "Filter by tags"
576
// @Param status query string false "Filter by status"
577
// @Success 200 {object} schemas.SearchQuestionsResponse
578
// @Failure 400 {object} schemas.ErrorResponse
579
// @Failure 500 {object} schemas.ErrorResponse
580
// @Router /forum/courses/{courseId}/search [get]
581
func (c *ForumController) SearchQuestions(ctx *gin.Context) {
1✔
582
        slog.Debug("Searching questions")
1✔
583

1✔
584
        courseID := ctx.Param("courseId")
1✔
585

1✔
586
        var request schemas.SearchQuestionsRequest
1✔
587
        if err := ctx.ShouldBindQuery(&request); err != nil {
1✔
588
                slog.Error("Error binding query parameters", "error", err)
×
589
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
×
590
                return
×
591
        }
×
592

593
        questions, err := c.service.SearchQuestions(courseID, request.Query, request.Tags, request.Status)
1✔
594
        if err != nil {
2✔
595
                slog.Error("Error searching questions", "error", err)
1✔
596
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
597
                return
1✔
598
        }
1✔
599

600
        var questionResponses []schemas.QuestionResponse
1✔
601
        for _, question := range questions {
2✔
602
                questionResponses = append(questionResponses, c.mapQuestionToResponse(&question))
1✔
603
        }
1✔
604

605
        response := schemas.SearchQuestionsResponse{
1✔
606
                Questions: questionResponses,
1✔
607
                Total:     len(questionResponses),
1✔
608
        }
1✔
609

1✔
610
        slog.Debug("Questions searched", "course_id", courseID, "total", response.Total)
1✔
611
        ctx.JSON(http.StatusOK, response)
1✔
612
}
613

614
// @Summary Get forum participants
615
// @Description Get all unique participants (authors, answerers, voters) for a specific course forum
616
// @Tags forum
617
// @Accept json
618
// @Produce json
619
// @Param courseId path string true "Course ID"
620
// @Success 200 {object} schemas.ForumParticipantsResponse
621
// @Failure 400 {object} schemas.ErrorResponse
622
// @Failure 404 {object} schemas.ErrorResponse
623
// @Failure 500 {object} schemas.ErrorResponse
624
// @Router /forum/courses/{courseId}/participants [get]
625
func (c *ForumController) GetForumParticipants(ctx *gin.Context) {
1✔
626
        slog.Debug("Getting forum participants")
1✔
627

1✔
628
        courseID := ctx.Param("courseId")
1✔
629
        participants, err := c.service.GetForumParticipants(courseID)
1✔
630
        if err != nil {
2✔
631
                slog.Error("Error getting forum participants", "error", err)
1✔
632
                if err.Error() == "course not found" {
2✔
633
                        ctx.JSON(http.StatusNotFound, schemas.ErrorResponse{Error: err.Error()})
1✔
634
                } else {
1✔
635
                        ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
×
636
                }
×
637
                return
1✔
638
        }
639

640
        response := schemas.ForumParticipantsResponse{
1✔
641
                Participants: participants,
1✔
642
        }
1✔
643

1✔
644
        slog.Debug("Forum participants retrieved", "course_id", courseID, "total", len(participants))
1✔
645
        ctx.JSON(http.StatusOK, response)
1✔
646
}
647

648
// Helper methods for mapping models to responses
649

650
func (c *ForumController) mapQuestionToResponse(question *model.ForumQuestion) schemas.QuestionResponse {
1✔
651
        voteCount := c.calculateVoteCount(question.Votes)
1✔
652
        answerCount := len(question.Answers)
1✔
653

1✔
654
        return schemas.QuestionResponse{
1✔
655
                ID:               question.ID.Hex(),
1✔
656
                CourseID:         question.CourseID,
1✔
657
                AuthorID:         question.AuthorID,
1✔
658
                Title:            question.Title,
1✔
659
                Description:      question.Description,
1✔
660
                Tags:             question.Tags,
1✔
661
                Votes:            question.Votes,
1✔
662
                VoteCount:        voteCount,
1✔
663
                AnswerCount:      answerCount,
1✔
664
                Status:           question.Status,
1✔
665
                AcceptedAnswerID: question.AcceptedAnswerID,
1✔
666
                CreatedAt:        question.CreatedAt,
1✔
667
                UpdatedAt:        question.UpdatedAt,
1✔
668
        }
1✔
669
}
1✔
670

671
func (c *ForumController) mapQuestionToDetailResponse(question *model.ForumQuestion) schemas.QuestionDetailResponse {
1✔
672
        voteCount := c.calculateVoteCount(question.Votes)
1✔
673

1✔
674
        var answers []schemas.AnswerResponse
1✔
675
        for _, answer := range question.Answers {
2✔
676
                answers = append(answers, c.mapAnswerToResponse(&answer))
1✔
677
        }
1✔
678

679
        return schemas.QuestionDetailResponse{
1✔
680
                ID:               question.ID.Hex(),
1✔
681
                CourseID:         question.CourseID,
1✔
682
                AuthorID:         question.AuthorID,
1✔
683
                Title:            question.Title,
1✔
684
                Description:      question.Description,
1✔
685
                Tags:             question.Tags,
1✔
686
                Votes:            question.Votes,
1✔
687
                VoteCount:        voteCount,
1✔
688
                Answers:          answers,
1✔
689
                Status:           question.Status,
1✔
690
                AcceptedAnswerID: question.AcceptedAnswerID,
1✔
691
                CreatedAt:        question.CreatedAt,
1✔
692
                UpdatedAt:        question.UpdatedAt,
1✔
693
        }
1✔
694
}
695

696
func (c *ForumController) mapAnswerToResponse(answer *model.ForumAnswer) schemas.AnswerResponse {
1✔
697
        voteCount := c.calculateVoteCount(answer.Votes)
1✔
698

1✔
699
        return schemas.AnswerResponse{
1✔
700
                ID:         answer.ID,
1✔
701
                AuthorID:   answer.AuthorID,
1✔
702
                Content:    answer.Content,
1✔
703
                Votes:      answer.Votes,
1✔
704
                VoteCount:  voteCount,
1✔
705
                IsAccepted: answer.IsAccepted,
1✔
706
                CreatedAt:  answer.CreatedAt,
1✔
707
                UpdatedAt:  answer.UpdatedAt,
1✔
708
        }
1✔
709
}
1✔
710

711
func (c *ForumController) calculateVoteCount(votes []model.Vote) int {
1✔
712
        count := 0
1✔
713
        for _, vote := range votes {
2✔
714
                count += vote.VoteType
1✔
715
        }
1✔
716
        return count
1✔
717
}
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