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

classconnect-grupo3 / courses-service / 15908085741

26 Jun 2025 05:07PM UTC coverage: 81.817% (+7.1%) from 74.755%
15908085741

Pull #48

github

rovifran
formatting
Pull Request #48: Notification messages and testing

83 of 144 new or added lines in 4 files covered. (57.64%)

44 existing lines in 2 files now uncovered.

4540 of 5549 relevant lines covered (81.82%)

0.9 hits per line

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

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

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

9
        "courses-service/src/model"
10
        "courses-service/src/queues"
11
        "courses-service/src/schemas"
12
        "courses-service/src/service"
13

14
        "github.com/gin-gonic/gin"
15
)
16

17
type ForumController struct {
18
        service            service.ForumServiceInterface
19
        activityService    service.TeacherActivityServiceInterface
20
        notificationsQueue queues.NotificationsQueueInterface
21
}
22

23
func NewForumController(service service.ForumServiceInterface, activityService service.TeacherActivityServiceInterface, notificationsQueue queues.NotificationsQueueInterface) *ForumController {
1✔
24
        return &ForumController{
1✔
25
                service:            service,
1✔
26
                activityService:    activityService,
1✔
27
                notificationsQueue: notificationsQueue,
1✔
28
        }
1✔
29
}
1✔
30

31
// Question endpoints
32

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

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

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

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

77
        response := c.mapQuestionToDetailResponse(question)
1✔
78
        slog.Debug("Question created", "question_id", question.ID.Hex())
1✔
79

1✔
80
        message := queues.NewForumActivityMessage(request.CourseID, request.AuthorID, question.ID.Hex(), question.Title, response.CreatedAt)
1✔
81
        slog.Info("Publishing message", "message", message)
1✔
82
        err = c.notificationsQueue.Publish(message)
1✔
83
        if err != nil {
1✔
NEW
84
                slog.Error("Error publishing message", "error", err)
×
NEW
85
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
×
NEW
86
                return
×
NEW
87
        }
×
88
        ctx.JSON(http.StatusCreated, response)
1✔
89
}
90

91
// @Summary Get question by ID
92
// @Description Get a specific question by its ID with all answers
93
// @Tags forum
94
// @Accept json
95
// @Produce json
96
// @Param questionId path string true "Question ID"
97
// @Success 200 {object} schemas.QuestionDetailResponse
98
// @Failure 404 {object} schemas.ErrorResponse
99
// @Failure 500 {object} schemas.ErrorResponse
100
// @Router /forum/questions/{questionId} [get]
101
func (c *ForumController) GetQuestionById(ctx *gin.Context) {
1✔
102
        slog.Debug("Getting question by ID")
1✔
103

1✔
104
        id := ctx.Param("questionId")
1✔
105
        question, err := c.service.GetQuestionById(id)
1✔
106
        if err != nil {
2✔
107
                slog.Error("Error getting question by ID", "error", err)
1✔
108
                ctx.JSON(http.StatusNotFound, schemas.ErrorResponse{Error: err.Error()})
1✔
109
                return
1✔
110
        }
1✔
111

112
        response := c.mapQuestionToDetailResponse(question)
1✔
113
        slog.Debug("Question retrieved", "question_id", id)
1✔
114
        ctx.JSON(http.StatusOK, response)
1✔
115
}
116

117
// @Summary Get questions by course ID
118
// @Description Get all questions for a specific course
119
// @Tags forum
120
// @Accept json
121
// @Produce json
122
// @Param courseId path string true "Course ID"
123
// @Success 200 {array} schemas.QuestionResponse
124
// @Failure 404 {object} schemas.ErrorResponse
125
// @Failure 500 {object} schemas.ErrorResponse
126
// @Router /forum/courses/{courseId}/questions [get]
127
func (c *ForumController) GetQuestionsByCourseId(ctx *gin.Context) {
1✔
128
        slog.Debug("Getting questions by course ID")
1✔
129

1✔
130
        courseID := ctx.Param("courseId")
1✔
131
        questions, err := c.service.GetQuestionsByCourseId(courseID)
1✔
132
        if err != nil {
2✔
133
                slog.Error("Error getting questions by course ID", "error", err)
1✔
134
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
135
                return
1✔
136
        }
1✔
137

138
        var responses []schemas.QuestionResponse
1✔
139
        for _, question := range questions {
2✔
140
                responses = append(responses, c.mapQuestionToResponse(&question))
1✔
141
        }
1✔
142

143
        slog.Debug("Questions retrieved", "course_id", courseID, "count", len(responses))
1✔
144
        ctx.JSON(http.StatusOK, responses)
1✔
145
}
146

147
// @Summary Update a question
148
// @Description Update a question's title, description, or tags
149
// @Tags forum
150
// @Accept json
151
// @Produce json
152
// @Param questionId path string true "Question ID"
153
// @Param question body schemas.UpdateQuestionRequest true "Question update data"
154
// @Success 200 {object} schemas.QuestionDetailResponse
155
// @Failure 400 {object} schemas.ErrorResponse
156
// @Failure 403 {object} schemas.ErrorResponse
157
// @Failure 404 {object} schemas.ErrorResponse
158
// @Failure 500 {object} schemas.ErrorResponse
159
// @Router /forum/questions/{questionId} [put]
160
func (c *ForumController) UpdateQuestion(ctx *gin.Context) {
1✔
161
        slog.Debug("Updating question")
1✔
162

1✔
163
        id := ctx.Param("questionId")
1✔
164
        var request schemas.UpdateQuestionRequest
1✔
165
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
166
                slog.Error("Error binding JSON", "error", err)
1✔
167
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
168
                return
1✔
169
        }
1✔
170

171
        question, err := c.service.UpdateQuestion(id, request.Title, request.Description, request.Tags)
1✔
172
        if err != nil {
2✔
173
                slog.Error("Error updating question", "error", err)
1✔
174
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
175
                return
1✔
176
        }
1✔
177

178
        // Log activity if teacher is auxiliary
179
        teacherUUID := ctx.GetHeader("X-Teacher-UUID")
1✔
180
        if teacherUUID != "" && question != nil {
1✔
181
                c.activityService.LogActivityIfAuxTeacher(
×
182
                        question.CourseID,
×
183
                        teacherUUID,
×
184
                        "UPDATE_FORUM_QUESTION",
×
185
                        fmt.Sprintf("Updated forum question: %s", request.Title),
×
186
                )
×
187
        }
×
188

189
        response := c.mapQuestionToDetailResponse(question)
1✔
190
        slog.Debug("Question updated", "question_id", id)
1✔
191

1✔
192
        message := queues.NewForumActivityMessage(question.CourseID, question.AuthorID, id, question.Title, response.UpdatedAt)
1✔
193
        slog.Info("Publishing message", "message", message)
1✔
194
        err = c.notificationsQueue.Publish(message)
1✔
195
        if err != nil {
1✔
NEW
196
                slog.Error("Error publishing message", "error", err)
×
NEW
197
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
×
NEW
198
                return
×
NEW
199
        }
×
200
        ctx.JSON(http.StatusOK, response)
1✔
201
}
202

203
// @Summary Delete a question
204
// @Description Delete a question (only by the author)
205
// @Tags forum
206
// @Accept json
207
// @Produce json
208
// @Param questionId path string true "Question ID"
209
// @Param authorId query string true "Author ID"
210
// @Success 200 {object} schemas.MessageResponse
211
// @Failure 403 {object} schemas.ErrorResponse
212
// @Failure 404 {object} schemas.ErrorResponse
213
// @Failure 500 {object} schemas.ErrorResponse
214
// @Router /forum/questions/{questionId} [delete]
215
func (c *ForumController) DeleteQuestion(ctx *gin.Context) {
1✔
216
        slog.Debug("Deleting question")
1✔
217

1✔
218
        id := ctx.Param("questionId")
1✔
219
        authorID := ctx.Query("authorId")
1✔
220

1✔
221
        if authorID == "" {
2✔
222
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "authorId query parameter is required"})
1✔
223
                return
1✔
224
        }
1✔
225

226
        // Get question before deleting for logging
227
        question, qErr := c.service.GetQuestionById(id)
1✔
228

1✔
229
        err := c.service.DeleteQuestion(id, authorID)
1✔
230
        if err != nil {
2✔
231
                slog.Error("Error deleting question", "error", err)
1✔
232
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
233
                return
1✔
234
        }
1✔
235

236
        // Log activity if teacher is auxiliary
237
        teacherUUID := ctx.GetHeader("X-Teacher-UUID")
1✔
238
        if teacherUUID != "" && teacherUUID == authorID && qErr == nil && question != nil {
1✔
239
                c.activityService.LogActivityIfAuxTeacher(
×
240
                        question.CourseID,
×
241
                        teacherUUID,
×
242
                        "DELETE_FORUM_QUESTION",
×
243
                        fmt.Sprintf("Deleted forum question: %s", question.Title),
×
244
                )
×
245
        }
×
246

247
        slog.Debug("Question deleted", "question_id", id)
1✔
248
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Question deleted successfully"})
1✔
249
}
250

251
// Answer endpoints
252

253
// @Summary Add an answer to a question
254
// @Description Add a new answer to a specific question
255
// @Tags forum
256
// @Accept json
257
// @Produce json
258
// @Param questionId path string true "Question ID"
259
// @Param answer body schemas.CreateAnswerRequest true "Answer to create"
260
// @Success 201 {object} schemas.AnswerResponse
261
// @Failure 400 {object} schemas.ErrorResponse
262
// @Failure 404 {object} schemas.ErrorResponse
263
// @Failure 500 {object} schemas.ErrorResponse
264
// @Router /forum/questions/{questionId}/answers [post]
265
func (c *ForumController) AddAnswer(ctx *gin.Context) {
1✔
266
        slog.Debug("Adding answer to question")
1✔
267

1✔
268
        questionID := ctx.Param("questionId")
1✔
269
        var request schemas.CreateAnswerRequest
1✔
270
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
271
                slog.Error("Error binding JSON", "error", err)
1✔
272
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
273
                return
1✔
274
        }
1✔
275

276
        answer, err := c.service.AddAnswer(questionID, request.AuthorID, request.Content)
1✔
277
        if err != nil {
2✔
278
                slog.Error("Error adding answer", "error", err)
1✔
279
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
280
                return
1✔
281
        }
1✔
282

283
        var question *model.ForumQuestion
1✔
284
        var qErr error
1✔
285

1✔
286
        // Log activity if teacher is auxiliary - we need to get the question to know the course
1✔
287
        teacherUUID := ctx.GetHeader("X-Teacher-UUID")
1✔
288
        if teacherUUID != "" && teacherUUID == request.AuthorID {
1✔
289
                // Get the question to find the course ID
×
NEW
290
                question, qErr = c.service.GetQuestionById(questionID)
×
291
                if qErr == nil && question != nil {
×
292
                        c.activityService.LogActivityIfAuxTeacher(
×
293
                                question.CourseID,
×
294
                                teacherUUID,
×
295
                                "CREATE_FORUM_ANSWER",
×
296
                                "Created forum answer",
×
297
                        )
×
298
                }
×
299
        }
300

301
        question, qErr = c.service.GetQuestionById(questionID)
1✔
302
        if qErr != nil {
1✔
NEW
303
                slog.Error("Error getting question", "error", qErr)
×
NEW
304
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: qErr.Error()})
×
NEW
305
                return
×
NEW
306
        }
×
307

308
        response := c.mapAnswerToResponse(answer)
1✔
309
        slog.Debug("Answer added", "question_id", questionID, "answer_id", answer.ID)
1✔
310

1✔
311
        message := queues.NewForumActivityMessage(question.CourseID, question.AuthorID, question.ID.Hex(), question.Title, response.CreatedAt)
1✔
312
        slog.Info("Publishing message", "message", message)
1✔
313
        err = c.notificationsQueue.Publish(message)
1✔
314
        if err != nil {
1✔
NEW
315
                slog.Error("Error publishing message", "error", err)
×
NEW
316
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
×
NEW
317
                return
×
NEW
318
        }
×
319
        ctx.JSON(http.StatusCreated, response)
1✔
320
}
321

322
// @Summary Update an answer
323
// @Description Update an answer's content (only by the author)
324
// @Tags forum
325
// @Accept json
326
// @Produce json
327
// @Param questionId path string true "Question ID"
328
// @Param answerId path string true "Answer ID"
329
// @Param answer body schemas.UpdateAnswerRequest true "Answer update data"
330
// @Param authorId query string true "Author ID"
331
// @Success 200 {object} schemas.AnswerResponse
332
// @Failure 400 {object} schemas.ErrorResponse
333
// @Failure 403 {object} schemas.ErrorResponse
334
// @Failure 404 {object} schemas.ErrorResponse
335
// @Failure 500 {object} schemas.ErrorResponse
336
// @Router /forum/questions/{questionId}/answers/{answerId} [put]
337
func (c *ForumController) UpdateAnswer(ctx *gin.Context) {
1✔
338
        slog.Debug("Updating answer")
1✔
339

1✔
340
        questionID := ctx.Param("questionId")
1✔
341
        answerID := ctx.Param("answerId")
1✔
342
        authorID := ctx.Query("authorId")
1✔
343

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

349
        var request schemas.UpdateAnswerRequest
1✔
350
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
351
                slog.Error("Error binding JSON", "error", err)
1✔
352
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
353
                return
1✔
354
        }
1✔
355

356
        answer, err := c.service.UpdateAnswer(questionID, answerID, authorID, request.Content)
1✔
357
        if err != nil {
2✔
358
                slog.Error("Error updating answer", "error", err)
1✔
359
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
360
                return
1✔
361
        }
1✔
362

363
        response := c.mapAnswerToResponse(answer)
1✔
364
        slog.Debug("Answer updated", "question_id", questionID, "answer_id", answerID)
1✔
365
        ctx.JSON(http.StatusOK, response)
1✔
366
}
367

368
// @Summary Delete an answer
369
// @Description Delete an answer (only by the author)
370
// @Tags forum
371
// @Accept json
372
// @Produce json
373
// @Param questionId path string true "Question ID"
374
// @Param answerId path string true "Answer ID"
375
// @Param authorId query string true "Author ID"
376
// @Success 200 {object} schemas.MessageResponse
377
// @Failure 403 {object} schemas.ErrorResponse
378
// @Failure 404 {object} schemas.ErrorResponse
379
// @Failure 500 {object} schemas.ErrorResponse
380
// @Router /forum/questions/{questionId}/answers/{answerId} [delete]
381
func (c *ForumController) DeleteAnswer(ctx *gin.Context) {
1✔
382
        slog.Debug("Deleting answer")
1✔
383

1✔
384
        questionID := ctx.Param("questionId")
1✔
385
        answerID := ctx.Param("answerId")
1✔
386
        authorID := ctx.Query("authorId")
1✔
387

1✔
388
        if authorID == "" {
2✔
389
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "authorId query parameter is required"})
1✔
390
                return
1✔
391
        }
1✔
392

393
        err := c.service.DeleteAnswer(questionID, answerID, authorID)
1✔
394
        if err != nil {
2✔
395
                slog.Error("Error deleting answer", "error", err)
1✔
396
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
397
                return
1✔
398
        }
1✔
399

400
        slog.Debug("Answer deleted", "question_id", questionID, "answer_id", answerID)
1✔
401
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Answer deleted successfully"})
1✔
402
}
403

404
// @Summary Accept an answer
405
// @Description Accept an answer as the solution (only by the question author)
406
// @Tags forum
407
// @Accept json
408
// @Produce json
409
// @Param questionId path string true "Question ID"
410
// @Param answerId path string true "Answer ID"
411
// @Param authorId query string true "Question Author ID"
412
// @Success 200 {object} schemas.MessageResponse
413
// @Failure 403 {object} schemas.ErrorResponse
414
// @Failure 404 {object} schemas.ErrorResponse
415
// @Failure 500 {object} schemas.ErrorResponse
416
// @Router /forum/questions/{questionId}/answers/{answerId}/accept [post]
417
func (c *ForumController) AcceptAnswer(ctx *gin.Context) {
1✔
418
        slog.Debug("Accepting answer")
1✔
419

1✔
420
        questionID := ctx.Param("questionId")
1✔
421
        answerID := ctx.Param("answerId")
1✔
422
        authorID := ctx.Query("authorId")
1✔
423

1✔
424
        if authorID == "" {
2✔
425
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "authorId query parameter is required"})
1✔
426
                return
1✔
427
        }
1✔
428

429
        err := c.service.AcceptAnswer(questionID, answerID, authorID)
1✔
430
        if err != nil {
2✔
431
                slog.Error("Error accepting answer", "error", err)
1✔
432
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
433
                return
1✔
434
        }
1✔
435

436
        // Log activity if teacher is auxiliary
437
        teacherUUID := ctx.GetHeader("X-Teacher-UUID")
1✔
438
        if teacherUUID != "" && teacherUUID == authorID {
1✔
439
                // Get the question to find the course ID
×
440
                question, qErr := c.service.GetQuestionById(questionID)
×
441
                if qErr == nil && question != nil {
×
442
                        c.activityService.LogActivityIfAuxTeacher(
×
443
                                question.CourseID,
×
444
                                teacherUUID,
×
445
                                "ACCEPT_FORUM_ANSWER",
×
446
                                "Accepted forum answer",
×
447
                        )
×
448
                }
×
449
        }
450

451
        slog.Debug("Answer accepted", "question_id", questionID, "answer_id", answerID)
1✔
452
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Answer accepted successfully"})
1✔
453
}
454

455
// Vote endpoints
456

457
// @Summary Vote on a question
458
// @Description Vote up or down on a question
459
// @Tags forum
460
// @Accept json
461
// @Produce json
462
// @Param questionId path string true "Question 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}/vote [post]
470
func (c *ForumController) VoteQuestion(ctx *gin.Context) {
1✔
471
        slog.Debug("Voting on question")
1✔
472

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

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

488
        // get the question to find the course ID
489
        question, qErr := c.service.GetQuestionById(questionID)
1✔
490
        if qErr != nil {
1✔
NEW
491
                slog.Error("Error getting question", "error", qErr)
×
NEW
492
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: qErr.Error()})
×
NEW
493
                return
×
NEW
494
        }
×
495

496
        voteTypeStr := "up"
1✔
497
        if request.VoteType == model.VoteTypeDown {
2✔
498
                voteTypeStr = "down"
1✔
499
        }
1✔
500

501
        message := queues.NewForumActivityMessage(question.CourseID, request.UserID, question.ID.Hex(), question.Title, time.Now())
1✔
502
        slog.Info("Publishing message", "message", message)
1✔
503
        err = c.notificationsQueue.Publish(message)
1✔
504
        if err != nil {
1✔
NEW
505
                slog.Error("Error publishing message", "error", err)
×
NEW
506
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
×
NEW
507
                return
×
NEW
508
        }
×
509

510
        slog.Debug("Vote registered", "question_id", questionID, "vote_type", voteTypeStr)
1✔
511
        ctx.JSON(http.StatusOK, schemas.VoteResponse{Message: "Vote registered successfully"})
1✔
512
}
513

514
// @Summary Vote on an answer
515
// @Description Vote up or down on an answer
516
// @Tags forum
517
// @Accept json
518
// @Produce json
519
// @Param questionId path string true "Question ID"
520
// @Param answerId path string true "Answer ID"
521
// @Param vote body schemas.VoteRequest true "Vote data"
522
// @Success 200 {object} schemas.VoteResponse
523
// @Failure 400 {object} schemas.ErrorResponse
524
// @Failure 403 {object} schemas.ErrorResponse
525
// @Failure 404 {object} schemas.ErrorResponse
526
// @Failure 500 {object} schemas.ErrorResponse
527
// @Router /forum/questions/{questionId}/answers/{answerId}/vote [post]
528
func (c *ForumController) VoteAnswer(ctx *gin.Context) {
1✔
529
        slog.Debug("Voting on answer")
1✔
530

1✔
531
        questionID := ctx.Param("questionId")
1✔
532
        answerID := ctx.Param("answerId")
1✔
533
        var request schemas.VoteRequest
1✔
534
        if err := ctx.ShouldBindJSON(&request); err != nil {
2✔
535
                slog.Error("Error binding JSON", "error", err)
1✔
536
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
1✔
537
                return
1✔
538
        }
1✔
539

540
        err := c.service.VoteAnswer(questionID, answerID, request.UserID, request.VoteType)
1✔
541
        if err != nil {
2✔
542
                slog.Error("Error voting on answer", "error", err)
1✔
543
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
544
                return
1✔
545
        }
1✔
546

547
        voteTypeStr := "up"
1✔
548
        if request.VoteType == model.VoteTypeDown {
2✔
549
                voteTypeStr = "down"
1✔
550
        }
1✔
551

552
        // get the question to find the course ID
553
        question, qErr := c.service.GetQuestionById(questionID)
1✔
554
        if qErr != nil {
1✔
NEW
555
                slog.Error("Error getting question", "error", qErr)
×
NEW
556
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: qErr.Error()})
×
NEW
557
                return
×
NEW
558
        }
×
559
        slog.Debug("Vote registered", "question_id", questionID, "answer_id", answerID, "vote_type", voteTypeStr)
1✔
560

1✔
561
        message := queues.NewForumActivityMessage(question.CourseID, request.UserID, question.ID.Hex(), question.Title, time.Now())
1✔
562
        slog.Info("Publishing message", "message", message)
1✔
563
        err = c.notificationsQueue.Publish(message)
1✔
564
        if err != nil {
1✔
NEW
565
                slog.Error("Error publishing message", "error", err)
×
NEW
566
                ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
×
NEW
567
                return
×
NEW
568
        }
×
569

570
        ctx.JSON(http.StatusOK, schemas.VoteResponse{Message: "Vote registered successfully"})
1✔
571
}
572

573
// @Summary Remove vote from question
574
// @Description Remove a user's vote from a question
575
// @Tags forum
576
// @Accept json
577
// @Produce json
578
// @Param questionId path string true "Question ID"
579
// @Param userId query string true "User ID"
580
// @Success 200 {object} schemas.MessageResponse
581
// @Failure 404 {object} schemas.ErrorResponse
582
// @Failure 500 {object} schemas.ErrorResponse
583
// @Router /forum/questions/{questionId}/vote [delete]
584
func (c *ForumController) RemoveVoteFromQuestion(ctx *gin.Context) {
1✔
585
        slog.Debug("Removing vote from question")
1✔
586

1✔
587
        questionID := ctx.Param("questionId")
1✔
588
        userID := ctx.Query("userId")
1✔
589

1✔
590
        if userID == "" {
2✔
591
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "userId query parameter is required"})
1✔
592
                return
1✔
593
        }
1✔
594

595
        err := c.service.RemoveVoteFromQuestion(questionID, userID)
1✔
596
        if err != nil {
2✔
597
                slog.Error("Error removing vote from question", "error", err)
1✔
598
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
599
                return
1✔
600
        }
1✔
601

602
        slog.Debug("Vote removed", "question_id", questionID, "user_id", userID)
1✔
603
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Vote removed successfully"})
1✔
604
}
605

606
// @Summary Remove vote from answer
607
// @Description Remove a user's vote from an answer
608
// @Tags forum
609
// @Accept json
610
// @Produce json
611
// @Param questionId path string true "Question ID"
612
// @Param answerId path string true "Answer ID"
613
// @Param userId query string true "User ID"
614
// @Success 200 {object} schemas.MessageResponse
615
// @Failure 404 {object} schemas.ErrorResponse
616
// @Failure 500 {object} schemas.ErrorResponse
617
// @Router /forum/questions/{questionId}/answers/{answerId}/vote [delete]
618
func (c *ForumController) RemoveVoteFromAnswer(ctx *gin.Context) {
1✔
619
        slog.Debug("Removing vote from answer")
1✔
620

1✔
621
        questionID := ctx.Param("questionId")
1✔
622
        answerID := ctx.Param("answerId")
1✔
623
        userID := ctx.Query("userId")
1✔
624

1✔
625
        if userID == "" {
2✔
626
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: "userId query parameter is required"})
1✔
627
                return
1✔
628
        }
1✔
629

630
        err := c.service.RemoveVoteFromAnswer(questionID, answerID, userID)
1✔
631
        if err != nil {
2✔
632
                slog.Error("Error removing vote from answer", "error", err)
1✔
633
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
634
                return
1✔
635
        }
1✔
636

637
        slog.Debug("Vote removed", "question_id", questionID, "answer_id", answerID, "user_id", userID)
1✔
638
        ctx.JSON(http.StatusOK, schemas.MessageResponse{Message: "Vote removed successfully"})
1✔
639
}
640

641
// Search endpoints
642

643
// @Summary Search questions
644
// @Description Search questions in a course with optional filters
645
// @Tags forum
646
// @Accept json
647
// @Produce json
648
// @Param courseId path string true "Course ID"
649
// @Param query query string false "Search query"
650
// @Param tags query []string false "Filter by tags"
651
// @Param status query string false "Filter by status"
652
// @Success 200 {object} schemas.SearchQuestionsResponse
653
// @Failure 400 {object} schemas.ErrorResponse
654
// @Failure 500 {object} schemas.ErrorResponse
655
// @Router /forum/courses/{courseId}/search [get]
656
func (c *ForumController) SearchQuestions(ctx *gin.Context) {
1✔
657
        slog.Debug("Searching questions")
1✔
658

1✔
659
        courseID := ctx.Param("courseId")
1✔
660

1✔
661
        var request schemas.SearchQuestionsRequest
1✔
662
        if err := ctx.ShouldBindQuery(&request); err != nil {
1✔
663
                slog.Error("Error binding query parameters", "error", err)
×
664
                ctx.JSON(http.StatusBadRequest, schemas.ErrorResponse{Error: err.Error()})
×
665
                return
×
666
        }
×
667

668
        questions, err := c.service.SearchQuestions(courseID, request.Query, request.Tags, request.Status)
1✔
669
        if err != nil {
2✔
670
                slog.Error("Error searching questions", "error", err)
1✔
671
                ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
1✔
672
                return
1✔
673
        }
1✔
674

675
        var questionResponses []schemas.QuestionResponse
1✔
676
        for _, question := range questions {
2✔
677
                questionResponses = append(questionResponses, c.mapQuestionToResponse(&question))
1✔
678
        }
1✔
679

680
        response := schemas.SearchQuestionsResponse{
1✔
681
                Questions: questionResponses,
1✔
682
                Total:     len(questionResponses),
1✔
683
        }
1✔
684

1✔
685
        slog.Debug("Questions searched", "course_id", courseID, "total", response.Total)
1✔
686
        ctx.JSON(http.StatusOK, response)
1✔
687
}
688

689
// @Summary Get forum participants
690
// @Description Get all unique participants (authors, answerers, voters) for a specific course forum
691
// @Tags forum
692
// @Accept json
693
// @Produce json
694
// @Param courseId path string true "Course ID"
695
// @Success 200 {object} schemas.ForumParticipantsResponse
696
// @Failure 400 {object} schemas.ErrorResponse
697
// @Failure 404 {object} schemas.ErrorResponse
698
// @Failure 500 {object} schemas.ErrorResponse
699
// @Router /forum/courses/{courseId}/participants [get]
700
func (c *ForumController) GetForumParticipants(ctx *gin.Context) {
1✔
701
        slog.Debug("Getting forum participants")
1✔
702

1✔
703
        courseID := ctx.Param("courseId")
1✔
704
        participants, err := c.service.GetForumParticipants(courseID)
1✔
705
        if err != nil {
2✔
706
                slog.Error("Error getting forum participants", "error", err)
1✔
707
                if err.Error() == "course not found" {
2✔
708
                        ctx.JSON(http.StatusNotFound, schemas.ErrorResponse{Error: err.Error()})
1✔
709
                } else {
1✔
710
                        ctx.JSON(http.StatusInternalServerError, schemas.ErrorResponse{Error: err.Error()})
×
711
                }
×
712
                return
1✔
713
        }
714

715
        response := schemas.ForumParticipantsResponse{
1✔
716
                Participants: participants,
1✔
717
        }
1✔
718

1✔
719
        slog.Debug("Forum participants retrieved", "course_id", courseID, "total", len(participants))
1✔
720
        ctx.JSON(http.StatusOK, response)
1✔
721
}
722

723
// Helper methods for mapping models to responses
724

725
func (c *ForumController) mapQuestionToResponse(question *model.ForumQuestion) schemas.QuestionResponse {
1✔
726
        voteCount := c.calculateVoteCount(question.Votes)
1✔
727
        answerCount := len(question.Answers)
1✔
728

1✔
729
        return schemas.QuestionResponse{
1✔
730
                ID:               question.ID.Hex(),
1✔
731
                CourseID:         question.CourseID,
1✔
732
                AuthorID:         question.AuthorID,
1✔
733
                Title:            question.Title,
1✔
734
                Description:      question.Description,
1✔
735
                Tags:             question.Tags,
1✔
736
                Votes:            question.Votes,
1✔
737
                VoteCount:        voteCount,
1✔
738
                AnswerCount:      answerCount,
1✔
739
                Status:           question.Status,
1✔
740
                AcceptedAnswerID: question.AcceptedAnswerID,
1✔
741
                CreatedAt:        question.CreatedAt,
1✔
742
                UpdatedAt:        question.UpdatedAt,
1✔
743
        }
1✔
744
}
1✔
745

746
func (c *ForumController) mapQuestionToDetailResponse(question *model.ForumQuestion) schemas.QuestionDetailResponse {
1✔
747
        voteCount := c.calculateVoteCount(question.Votes)
1✔
748

1✔
749
        var answers []schemas.AnswerResponse
1✔
750
        for _, answer := range question.Answers {
2✔
751
                answers = append(answers, c.mapAnswerToResponse(&answer))
1✔
752
        }
1✔
753

754
        return schemas.QuestionDetailResponse{
1✔
755
                ID:               question.ID.Hex(),
1✔
756
                CourseID:         question.CourseID,
1✔
757
                AuthorID:         question.AuthorID,
1✔
758
                Title:            question.Title,
1✔
759
                Description:      question.Description,
1✔
760
                Tags:             question.Tags,
1✔
761
                Votes:            question.Votes,
1✔
762
                VoteCount:        voteCount,
1✔
763
                Answers:          answers,
1✔
764
                Status:           question.Status,
1✔
765
                AcceptedAnswerID: question.AcceptedAnswerID,
1✔
766
                CreatedAt:        question.CreatedAt,
1✔
767
                UpdatedAt:        question.UpdatedAt,
1✔
768
        }
1✔
769
}
770

771
func (c *ForumController) mapAnswerToResponse(answer *model.ForumAnswer) schemas.AnswerResponse {
1✔
772
        voteCount := c.calculateVoteCount(answer.Votes)
1✔
773

1✔
774
        return schemas.AnswerResponse{
1✔
775
                ID:         answer.ID,
1✔
776
                AuthorID:   answer.AuthorID,
1✔
777
                Content:    answer.Content,
1✔
778
                Votes:      answer.Votes,
1✔
779
                VoteCount:  voteCount,
1✔
780
                IsAccepted: answer.IsAccepted,
1✔
781
                CreatedAt:  answer.CreatedAt,
1✔
782
                UpdatedAt:  answer.UpdatedAt,
1✔
783
        }
1✔
784
}
1✔
785

786
func (c *ForumController) calculateVoteCount(votes []model.Vote) int {
1✔
787
        count := 0
1✔
788
        for _, vote := range votes {
2✔
789
                count += vote.VoteType
1✔
790
        }
1✔
791
        return count
1✔
792
}
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