diff --git a/docs/docs.go b/docs/docs.go index 9e72b0b029a24d96014014b30d84b4ff9b06e6ad..49b75f3556829fa1845dddc0737e82c9ab85bc01 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1523,10 +1523,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/material.Material" - } + "$ref": "#/definitions/material.Material" } } } @@ -1738,6 +1735,119 @@ const docTemplate = `{ } } }, + "/quiz/{id}/solution": { + "get": { + "description": "Take a quiz", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "quiz" + ], + "summary": "Get Quiz Solution", + "parameters": [ + { + "type": "string", + "description": "Authenticate User (any role)", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "Quiz id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.BaseResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/quiz.QuizDetail" + } + } + } + ] + } + } + } + } + }, + "/quiz/{id}/take": { + "post": { + "description": "Finish quiz session and get the score", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "quiz" + ], + "summary": "Finish Quiz", + "parameters": [ + { + "type": "string", + "description": "Authenticate User (any role)", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Quiz Finish payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/quiz.FinishQuizPayload" + } + }, + { + "type": "string", + "format": "uuid", + "description": "Quiz id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.BaseResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/quiz.QuizDetail" + } + } + } + ] + } + } + } + } + }, "/reset/confirm": { "put": { "description": "Do confirmation to reset password", @@ -2249,6 +2359,37 @@ const docTemplate = `{ } } }, + "quiz.AnswerOption": { + "type": "object", + "properties": { + "answer": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_solution": { + "type": "boolean" + }, + "media_id": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "quiz.FinishQuizPayload": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/quiz.Response" + } + } + } + }, "quiz.Quiz": { "type": "object", "properties": { @@ -2261,8 +2402,88 @@ const docTemplate = `{ "id": { "type": "string" }, + "nama": { + "type": "string" + } + } + }, + "quiz.QuizDetail": { + "type": "object", + "properties": { + "course_id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "help": { + "type": "string" + }, + "id": { + "type": "string" + }, + "media": { + "type": "array", + "items": { + "$ref": "#/definitions/quiz.QuizMedia" + } + }, "name": { "type": "string" + }, + "problems": { + "type": "array", + "items": { + "$ref": "#/definitions/quiz.QuizProblem" + } + } + } + }, + "quiz.QuizMedia": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "quiz.QuizProblem": { + "type": "object", + "properties": { + "answers": { + "type": "array", + "items": { + "$ref": "#/definitions/quiz.AnswerOption" + } + }, + "id": { + "type": "string" + }, + "media_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "question": { + "type": "string" + } + } + }, + "quiz.Response": { + "type": "object", + "properties": { + "answer_id": { + "type": "string" + }, + "problem_id": { + "type": "string" } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 07c6617ab34c4011a16323b020510a7289432e12..883d3d799d994fab91eb3dbf9e1aee4d8483c36f 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1515,10 +1515,7 @@ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/material.Material" - } + "$ref": "#/definitions/material.Material" } } } @@ -1730,6 +1727,119 @@ } } }, + "/quiz/{id}/solution": { + "get": { + "description": "Take a quiz", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "quiz" + ], + "summary": "Get Quiz Solution", + "parameters": [ + { + "type": "string", + "description": "Authenticate User (any role)", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "Quiz id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.BaseResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/quiz.QuizDetail" + } + } + } + ] + } + } + } + } + }, + "/quiz/{id}/take": { + "post": { + "description": "Finish quiz session and get the score", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "quiz" + ], + "summary": "Finish Quiz", + "parameters": [ + { + "type": "string", + "description": "Authenticate User (any role)", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Quiz Finish payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/quiz.FinishQuizPayload" + } + }, + { + "type": "string", + "format": "uuid", + "description": "Quiz id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/web.BaseResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/quiz.QuizDetail" + } + } + } + ] + } + } + } + } + }, "/reset/confirm": { "put": { "description": "Do confirmation to reset password", @@ -2241,6 +2351,37 @@ } } }, + "quiz.AnswerOption": { + "type": "object", + "properties": { + "answer": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_solution": { + "type": "boolean" + }, + "media_id": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "quiz.FinishQuizPayload": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/quiz.Response" + } + } + } + }, "quiz.Quiz": { "type": "object", "properties": { @@ -2253,8 +2394,88 @@ "id": { "type": "string" }, + "nama": { + "type": "string" + } + } + }, + "quiz.QuizDetail": { + "type": "object", + "properties": { + "course_id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "help": { + "type": "string" + }, + "id": { + "type": "string" + }, + "media": { + "type": "array", + "items": { + "$ref": "#/definitions/quiz.QuizMedia" + } + }, "name": { "type": "string" + }, + "problems": { + "type": "array", + "items": { + "$ref": "#/definitions/quiz.QuizProblem" + } + } + } + }, + "quiz.QuizMedia": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "quiz.QuizProblem": { + "type": "object", + "properties": { + "answers": { + "type": "array", + "items": { + "$ref": "#/definitions/quiz.AnswerOption" + } + }, + "id": { + "type": "string" + }, + "media_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "question": { + "type": "string" + } + } + }, + "quiz.Response": { + "type": "object", + "properties": { + "answer_id": { + "type": "string" + }, + "problem_id": { + "type": "string" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ee7cc0f2bd3089513cd4d4688a70efda551cca03..58ea9348fb473426c5890cff161bd98360f62bf0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -298,6 +298,26 @@ definitions: upload_link: type: string type: object + quiz.AnswerOption: + properties: + answer: + type: string + id: + type: string + is_solution: + type: boolean + media_id: + items: + type: string + type: array + type: object + quiz.FinishQuizPayload: + properties: + data: + items: + $ref: '#/definitions/quiz.Response' + type: array + type: object quiz.Quiz: properties: course_id: @@ -306,8 +326,60 @@ definitions: type: string id: type: string + nama: + type: string + type: object + quiz.QuizDetail: + properties: + course_id: + type: string + description: + type: string + help: + type: string + id: + type: string + media: + items: + $ref: '#/definitions/quiz.QuizMedia' + type: array name: type: string + problems: + items: + $ref: '#/definitions/quiz.QuizProblem' + type: array + type: object + quiz.QuizMedia: + properties: + id: + type: string + type: + type: string + url: + type: string + type: object + quiz.QuizProblem: + properties: + answers: + items: + $ref: '#/definitions/quiz.AnswerOption' + type: array + id: + type: string + media_id: + items: + type: string + type: array + question: + type: string + type: object + quiz.Response: + properties: + answer_id: + type: string + problem_id: + type: string type: object refresh.RefreshResponsePayload: description: Refresh endpoint response when process success @@ -1393,9 +1465,7 @@ paths: - $ref: '#/definitions/web.BaseResponse' - properties: data: - items: - $ref: '#/definitions/material.Material' - type: array + $ref: '#/definitions/material.Material' type: object summary: Get material detail tags: @@ -1505,6 +1575,76 @@ paths: summary: Get Quiz Detail tags: - quiz + /quiz/{id}/solution: + get: + consumes: + - application/json + description: Take a quiz + parameters: + - description: Authenticate User (any role) + in: header + name: Authorization + required: true + type: string + - description: Quiz id + format: uuid + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/web.BaseResponse' + - properties: + data: + $ref: '#/definitions/quiz.QuizDetail' + type: object + summary: Get Quiz Solution + tags: + - quiz + /quiz/{id}/take: + post: + consumes: + - application/json + description: Finish quiz session and get the score + parameters: + - description: Authenticate User (any role) + in: header + name: Authorization + required: true + type: string + - description: Quiz Finish payload + in: body + name: data + required: true + schema: + $ref: '#/definitions/quiz.FinishQuizPayload' + - description: Quiz id + format: uuid + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/web.BaseResponse' + - properties: + data: + $ref: '#/definitions/quiz.QuizDetail' + type: object + summary: Finish Quiz + tags: + - quiz /reset/confirm: put: description: Do confirmation to reset password diff --git a/handler/quiz/take.go b/handler/quiz/take.go new file mode 100644 index 0000000000000000000000000000000000000000..530f4f77823001b1a5027b76d610b8c9ccafcaad --- /dev/null +++ b/handler/quiz/take.go @@ -0,0 +1,219 @@ +package quiz + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" + "gitlab.informatika.org/ocw/ocw-backend/middleware/guard" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + authToken "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token" + "gitlab.informatika.org/ocw/ocw-backend/model/web/quiz" +) + +// Index godoc +// +// @Tags quiz +// @Summary Take Quiz +// @Description Take a quiz +// @Produce json +// @Accept json +// @Param Authorization header string true "Authenticate User (any role)" +// @Param id path string true "Quiz id" Format(uuid) +// @Success 200 {object} web.BaseResponse{data=quiz.QuizDetail} +// @Router /quiz/{id}/take [post] +func (m QuizHandlerImpl) TakeQuiz(w http.ResponseWriter, r *http.Request) { + rawQuizId := chi.URLParam(r, "id") + + if rawQuizId == "" { + payload := m.WrapperUtil.ErrorResponseWrap("quiz id is required", nil) + m.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + quizId, err := uuid.Parse(rawQuizId) + + if err != nil { + payload := m.WrapperUtil.ErrorResponseWrap("quiz id is not valid", nil) + m.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + user, ok := r.Context().Value(guard.UserContext).(authToken.UserClaim) + + if !ok { + m.Logger.Error("Context is not found") + payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil) + m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + detail, err := m.DoTakeQuiz(r.Context(), quizId, user.Email) + + if err != nil { + respErr, ok := err.(web.ResponseError) + if ok { + payload := m.WrapperUtil.ErrorResponseWrap(respErr.Error(), respErr) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } else { + payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil) + m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } + return + } + + responsePayload := m.WrapperUtil.SuccessResponseWrap(detail) + m.HttpUtil.WriteSuccessJson(w, responsePayload) +} + +// Index godoc +// +// @Tags quiz +// @Summary Get Quiz Solution +// @Description Take a quiz +// @Produce json +// @Accept json +// @Param Authorization header string true "Authenticate User (any role)" +// @Param id path string true "Quiz id" Format(uuid) +// @Success 200 {object} web.BaseResponse{data=quiz.QuizDetail} +// @Router /quiz/{id}/solution [get] +func (m QuizHandlerImpl) GetQuizSolution(w http.ResponseWriter, r *http.Request) { + rawQuizId := chi.URLParam(r, "id") + + if rawQuizId == "" { + payload := m.WrapperUtil.ErrorResponseWrap("quiz id is required", nil) + m.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + quizId, err := uuid.Parse(rawQuizId) + + if err != nil { + payload := m.WrapperUtil.ErrorResponseWrap("quiz id is not valid", nil) + m.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + user, ok := r.Context().Value(guard.UserContext).(authToken.UserClaim) + + if !ok { + m.Logger.Error("Context is not found") + payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil) + m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + + detail, err := m.GetSolutionQuiz(r.Context(), quizId, user.Email) + + if err != nil { + respErr, ok := err.(web.ResponseError) + if ok { + if respErr.Code == "ERR_NOT_ALLOWED" { + payload := m.WrapperUtil.ErrorResponseWrap(respErr.Error(), respErr) + m.HttpUtil.WriteJson(w, http.StatusForbidden, payload) + } else { + payload := m.WrapperUtil.ErrorResponseWrap(respErr.Error(), respErr) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } + } else { + payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil) + m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } + return + } + + responsePayload := m.WrapperUtil.SuccessResponseWrap(detail) + m.HttpUtil.WriteSuccessJson(w, responsePayload) +} + +// Index godoc +// +// @Tags quiz +// @Summary Finish Quiz +// @Description Finish quiz session and get the score +// @Produce json +// @Accept json +// @Param Authorization header string true "Authenticate User (any role)" +// @Param data body quiz.FinishQuizPayload true "Quiz Finish payload" +// @Param id path string true "Quiz id" Format(uuid) +// @Success 200 {object} web.BaseResponse{data=quiz.QuizDetail} +// @Router /quiz/{id}/take [post] +func (m QuizHandlerImpl) FinishQuiz(w http.ResponseWriter, r *http.Request) { + payload := quiz.FinishQuizPayload{} + + /* Get user */ + user, ok := r.Context().Value(guard.UserContext).(authToken.UserClaim) + + if !ok { + m.Logger.Error("Context is not found") + payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil) + m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + return + } + /* Get user */ + + /* Validate input */ + validate := validator.New() + + if r.Header.Get("Content-Type") != "application/json" { + payload := m.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil) + m.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + if err := m.HttpUtil.ParseJson(r, &payload); err != nil { + payload := m.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error()) + m.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload) + return + } + + if err := validate.Struct(payload); err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + payload := m.WrapperUtil.ErrorResponseWrap(err.Error(), nil) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + + errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors)) + payload := m.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + return + } + /* End of validate */ + + /* Get quiz id */ + rawQuizId := chi.URLParam(r, "id") + + if rawQuizId == "" { + payload := m.WrapperUtil.ErrorResponseWrap("quiz id is required", nil) + m.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + + quizId, err := uuid.Parse(rawQuizId) + + if err != nil { + payload := m.WrapperUtil.ErrorResponseWrap("quiz id is not valid", nil) + m.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload) + return + } + /* end of get quiz id */ + + res, err := m.DoFinishQuiz(r.Context(), quizId, user.Email, payload.Data) + + if err != nil { + respErr, ok := err.(web.ResponseError) + if ok { + payload := m.WrapperUtil.ErrorResponseWrap(respErr.Error(), respErr) + m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload) + } else { + payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil) + m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload) + } + return + } + + responsePayload := m.WrapperUtil.SuccessResponseWrap(res) + m.HttpUtil.WriteSuccessJson(w, responsePayload) +} diff --git a/handler/quiz/type.go b/handler/quiz/type.go index 2c9d3e03d76b678496942f6101f39a27b074d2a3..8c3bf6f5567cf7f04be9e42b67558279a0f6159d 100644 --- a/handler/quiz/type.go +++ b/handler/quiz/type.go @@ -5,4 +5,8 @@ import "net/http" type QuizHandler interface { GetAllQuizes(w http.ResponseWriter, r *http.Request) GetQuizDetail(w http.ResponseWriter, r *http.Request) + + TakeQuiz(w http.ResponseWriter, r *http.Request) + GetQuizSolution(w http.ResponseWriter, r *http.Request) + FinishQuiz(w http.ResponseWriter, r *http.Request) } diff --git a/middleware/guard/builder.go b/middleware/guard/builder.go index 75850ebf45a8ef08662c76311156cd2a067a4ff3..c0f06a9e6c2324734c1db6f64e5b5eb8e34f46d8 100644 --- a/middleware/guard/builder.go +++ b/middleware/guard/builder.go @@ -10,7 +10,9 @@ import ( ) type GuardBuilder struct { - GuardMiddleware + token.TokenUtil + logger.Logger + wrapper.WrapperUtil } func NewBuilder( @@ -19,25 +21,19 @@ func NewBuilder( wrapper wrapper.WrapperUtil, ) *GuardBuilder { return &GuardBuilder{ - GuardMiddleware{ - Token: token, - Role: []user.UserRole{}, - Logger: logger, - WrapperUtil: wrapper, - }, + token, + logger, + wrapper, } } -func (g *GuardBuilder) AddRole(role ...user.UserRole) *GuardBuilder { - g.GuardMiddleware.Role = role - return g -} - -func (g *GuardBuilder) Build() func(http.Handler) http.Handler { - return g.GuardMiddleware.Handle -} +func (g *GuardBuilder) Build(role ...user.UserRole) func(http.Handler) http.Handler { + handler := &GuardMiddleware{ + Token: g.TokenUtil, + Role: role, + Logger: g.Logger, + WrapperUtil: g.WrapperUtil, + } -func (g *GuardBuilder) BuildSimple(role user.UserRole) func(http.Handler) http.Handler { - g.AddRole(role) - return g.Build() + return handler.Handle } diff --git a/model/domain/quiz/answer.go b/model/domain/quiz/answer.go deleted file mode 100644 index 477b2039d253b51934420202f7e1c34adbb23dac..0000000000000000000000000000000000000000 --- a/model/domain/quiz/answer.go +++ /dev/null @@ -1,8 +0,0 @@ -package quiz - -import "github.com/google/uuid" - -type Answer struct { - QuestionId uuid.UUID - OptionId uuid.UUID -} diff --git a/model/domain/quiz/option.go b/model/domain/quiz/option.go deleted file mode 100644 index 6f3af1fb082860285b9d141abb98245fe0d723f8..0000000000000000000000000000000000000000 --- a/model/domain/quiz/option.go +++ /dev/null @@ -1,9 +0,0 @@ -package quiz - -import "github.com/google/uuid" - -type Option struct { - Id uuid.UUID `json:"id"` - Text string `json:"text"` - IsAnswer bool `json:"is_answer"` -} diff --git a/model/domain/quiz/problem.go b/model/domain/quiz/problem.go new file mode 100644 index 0000000000000000000000000000000000000000..7794b7f4355177f0ce5a948d11c6ff3f62aa89de --- /dev/null +++ b/model/domain/quiz/problem.go @@ -0,0 +1,38 @@ +package quiz + +import "github.com/google/uuid" + +type QuizMedia struct { + Id uuid.UUID `json:"id"` + Url string `json:"url"` + Type string `json:"type"` +} + +type AnswerOption struct { + Id uuid.UUID `json:"id"` + MediaId []uuid.UUID `json:"media_id"` + Answer string `json:"answer"` + IsSolution *bool `json:"is_solution"` +} + +type QuizProblem struct { + Id uuid.UUID `json:"id"` + MediaId []uuid.UUID `json:"media_id"` + Question string `json:"question"` + Answer []AnswerOption `json:"answers"` +} + +type QuizDetail struct { + Id uuid.UUID `json:"id"` + Name string `json:"name"` + CourseId string `json:"course_id"` + Description string `json:"description"` + Help string `json:"help"` + Media []QuizMedia `json:"media"` + Problems []QuizProblem `json:"problems"` +} + +type Response struct { + ProblemId uuid.UUID `json:"problem_id"` + AnswerId uuid.UUID `json:"answer_id"` +} diff --git a/model/domain/quiz/question.go b/model/domain/quiz/question.go deleted file mode 100644 index d8b5fdfea2ca012266489b05e2ced985cbdb75d2..0000000000000000000000000000000000000000 --- a/model/domain/quiz/question.go +++ /dev/null @@ -1,9 +0,0 @@ -package quiz - -import "github.com/google/uuid" - -type Question struct { - Id uuid.UUID `json:"id"` - Description string `json:"string"` - Options []Option `json:"options"` -} diff --git a/model/domain/quiz/quiz.go b/model/domain/quiz/quiz.go index 1300dbc89fddcec83976bbaa726125d1e7a13e86..896536cc66c3ffbca9840836c446019b139e8074 100644 --- a/model/domain/quiz/quiz.go +++ b/model/domain/quiz/quiz.go @@ -6,7 +6,7 @@ import ( type Quiz struct { Id uuid.UUID `gorm:"primaryKey" json:"id"` - Name string `json:"name"` + Name string `json:"nama"` CourseId string `json:"course_id"` CreatorEmail string `json:"creator_email"` QuizPath string `json:"-"` diff --git a/model/web/quiz/finish.go b/model/web/quiz/finish.go new file mode 100644 index 0000000000000000000000000000000000000000..80c7335963d5977527460cf5ada30201865723e4 --- /dev/null +++ b/model/web/quiz/finish.go @@ -0,0 +1,7 @@ +package quiz + +import "gitlab.informatika.org/ocw/ocw-backend/model/domain/quiz" + +type FinishQuizPayload struct { + Data []quiz.Response `json:"data"` +} diff --git a/provider/storage/manager.go b/provider/storage/manager.go index cc87edd73acda90734470345e5fd3ea1230a1280..cfeae57144fe4d23729a2382984360253e5d4659 100644 --- a/provider/storage/manager.go +++ b/provider/storage/manager.go @@ -9,3 +9,16 @@ import ( func (s S3Storage) Delete(ctx context.Context, path string) error { return s.minio.RemoveObject(ctx, s.env.BucketName, path, minio.RemoveObjectOptions{}) } + +func (s S3Storage) Get(ctx context.Context, path string) ([]byte, error) { + result := []byte{} + obj, err := s.minio.GetObject(ctx, s.env.BucketName, path, minio.GetObjectOptions{}) + + if err != nil { + return result, err + } + + _, err = obj.Read(result) + + return result, err +} diff --git a/provider/storage/type.go b/provider/storage/type.go index dc7cdd2f97597d8760d26aa2b75791989a12ff9f..19f2a46fb4d9044839660ba4234f47f5f661f0d2 100644 --- a/provider/storage/type.go +++ b/provider/storage/type.go @@ -9,4 +9,5 @@ type Storage interface { CreatePutSignedLink(ctx context.Context, path string) (string, error) CreateGetSignedLink(ctx context.Context, path string, reqParam url.Values) (string, error) Delete(ctx context.Context, path string) error + Get(ctx context.Context, path string) ([]byte, error) } diff --git a/repository/quiz/impl.go b/repository/quiz/impl.go index 03665df4d4915b3c188da6e737bed27e7dc1582d..e51495422f1a10911dd47bf00b1764eb541446ab 100644 --- a/repository/quiz/impl.go +++ b/repository/quiz/impl.go @@ -86,3 +86,12 @@ func (q *QuizRepositoryImpl) GetAllTake(quizId uuid.UUID, userEmail string) ([]q return result, err } + +func (q *QuizRepositoryImpl) GetLastTake(quizId uuid.UUID, userEmail string) (*quiz.QuizTake, error) { + result := &quiz.QuizTake{} + err := q.db. + Where("quiz_id = ? AND email = ?", quizId, userEmail). + Last(result).Error + + return result, err +} diff --git a/repository/quiz/type.go b/repository/quiz/type.go index 0fc33d4d153ff2f8eabaea5cc9b44be16be724e7..1b4e0cbeca391ec136dfc8c696d1c058d49e55e4 100644 --- a/repository/quiz/type.go +++ b/repository/quiz/type.go @@ -12,4 +12,5 @@ type QuizRepository interface { NewTake(quizId uuid.UUID, userEmail string) (uuid.UUID, error) IsActiveTake(quizId uuid.UUID, userEmail string) (bool, error) GetAllTake(quizId uuid.UUID, userEmail string) ([]quiz.QuizTake, error) + GetLastTake(quizId uuid.UUID, userEmail string) (*quiz.QuizTake, error) } diff --git a/routes/admin/route.go b/routes/admin/route.go index d1ff589b86faa181e6e671bf7111ab3efed0b239..d1fe8e2213ad86d7b7bdd1ecf4fcac90ab8f60a5 100644 --- a/routes/admin/route.go +++ b/routes/admin/route.go @@ -14,7 +14,7 @@ type AdminRoutes struct { func (adr AdminRoutes) Register(r chi.Router) { r.Route("/admin", func(r chi.Router) { - r.Use(adr.GuardBuilder.BuildSimple(user.Admin)) + r.Use(adr.GuardBuilder.Build(user.Admin)) r.Get("/user", adr.AdminHandler.GetAllUser) r.Get("/user/{email}", adr.AdminHandler.GetUserByEmail) diff --git a/routes/course/route.go b/routes/course/route.go index 23cb9f533b686bbf87655fcf1186ff319f1a0f6a..43a8305f4187ad99c9e7baadea375a59f373ad37 100644 --- a/routes/course/route.go +++ b/routes/course/route.go @@ -36,7 +36,7 @@ func (c CourseRoutes) Register(r chi.Router) { }) r.Route("/course/{id}/material", func(r chi.Router) { - r.Use(c.BuildSimple(user.Contributor)) + r.Use(c.Build(user.Contributor)) r.Post("/", c.MaterialHandler.CreateMaterial) }) } diff --git a/routes/material/route.go b/routes/material/route.go index 39a9408ee968113a716969bf0482b71e5073ccd9..41126ecf7b9b60c1f56f9bd77208e9486db51ef0 100644 --- a/routes/material/route.go +++ b/routes/material/route.go @@ -17,7 +17,7 @@ func (c MaterialRoutes) Register(r chi.Router) { r.Get("/", c.DetailMaterial) r.Route("/", func(r chi.Router) { - r.Use(c.GuardBuilder.BuildSimple(user.Contributor)) + r.Use(c.GuardBuilder.Build(user.Contributor)) // Add r.Post("/content", c.AddContent) diff --git a/routes/quiz/route.go b/routes/quiz/route.go index 58dbb0deb323c33a1138db79b8692b79aad0c5c0..b89269412c9add8ccc39a190a965f187a13c15b9 100644 --- a/routes/quiz/route.go +++ b/routes/quiz/route.go @@ -3,13 +3,37 @@ package quiz import ( "github.com/go-chi/chi/v5" "gitlab.informatika.org/ocw/ocw-backend/handler/quiz" + "gitlab.informatika.org/ocw/ocw-backend/middleware/guard" + "gitlab.informatika.org/ocw/ocw-backend/model/domain/user" ) type QuizRoutes struct { quiz.QuizHandler + *guard.GuardBuilder } func (q QuizRoutes) Register(r chi.Router) { r.Get("/course/{id}/quiz", q.QuizHandler.GetAllQuizes) r.Get("/quiz/{id}", q.QuizHandler.GetQuizDetail) + + guard := q.GuardBuilder.Build( + user.Student, + user.Contributor, + user.Admin, + ) + + r.Route("/quiz/{id}/take", func(r chi.Router) { + r.Use(guard) + r.Post("/", q.QuizHandler.TakeQuiz) + }) + + r.Route("/quiz/{id}/finish", func(r chi.Router) { + r.Use(guard) + r.Post("/", q.QuizHandler.FinishQuiz) + }) + + r.Route("/quiz/{id}/solution", func(r chi.Router) { + r.Use(guard) + r.Get("/", q.QuizHandler.GetQuizSolution) + }) } diff --git a/service/course/add.go b/service/course/add.go index dc42d5698abbb725b15e52d533ffd7ef9c719226..a94a59b6e56c8d465adf46559d3877dff50b34e0 100644 --- a/service/course/add.go +++ b/service/course/add.go @@ -132,7 +132,7 @@ func (c CourseServiceImpl) AddFaculty(payload faculty.AddFacultyRequestPayload) // Unauthorized Role if claim.Role != user.Admin { - return web.NewResponseErrorFromError(err, web.UnauthorizedAccess) + return web.NewResponseError("user is not allowed to access this resources", web.UnauthorizedAccess) } id, err := uuid.NewUUID() diff --git a/service/quiz/impl.go b/service/quiz/impl.go index 5bf6171e4d6fd4535810463b454f7bae90b93c9c..3898cd448978c6604f71f68e7561077c9c396a00 100644 --- a/service/quiz/impl.go +++ b/service/quiz/impl.go @@ -1,13 +1,20 @@ package quiz import ( + "bytes" + "context" + "encoding/json" + "github.com/google/uuid" "gitlab.informatika.org/ocw/ocw-backend/model/domain/quiz" + "gitlab.informatika.org/ocw/ocw-backend/model/web" + "gitlab.informatika.org/ocw/ocw-backend/provider/storage" quizRepo "gitlab.informatika.org/ocw/ocw-backend/repository/quiz" ) type QuizServiceImpl struct { quizRepo.QuizRepository + storage.Storage } func (q QuizServiceImpl) ListAllQuiz(courseId string) ([]quiz.Quiz, error) { @@ -17,3 +24,149 @@ func (q QuizServiceImpl) ListAllQuiz(courseId string) ([]quiz.Quiz, error) { func (q QuizServiceImpl) GetQuizDetail(quizId uuid.UUID) (*quiz.Quiz, error) { return q.QuizRepository.GetQuizDetail(quizId) } + +func (q QuizServiceImpl) getQuizDetail(ctx context.Context, quizId uuid.UUID) (*quiz.QuizDetail, error) { + detail, err := q.QuizRepository.GetQuizDetail(quizId) + + if err != nil { + return nil, err + } + + payload, err := q.Storage.Get(ctx, detail.QuizPath) + + if err != nil { + return nil, err + } + + result := &quiz.QuizDetail{} + + decoder := json.NewDecoder(bytes.NewReader(payload)) + err = decoder.Decode(result) + + return result, err +} + +func (q QuizServiceImpl) DoTakeQuiz(ctx context.Context, quizId uuid.UUID, email string) (*quiz.QuizDetail, error) { + result, err := q.getQuizDetail(ctx, quizId) + + if err != nil { + return nil, err + } + + taken, err := q.IsActiveTake(quizId, email) + + if err != nil { + return nil, err + } + + if !taken { + _, err = q.NewTake(quizId, email) + + if err != nil { + return nil, err + } + } + + for i := range result.Problems { + for j := range result.Problems[i].Answer { + result.Problems[i].Answer[j].IsSolution = nil + } + } + + return result, nil +} + +func (q QuizServiceImpl) GetSolutionQuiz(ctx context.Context, quizId uuid.UUID, email string) (*quiz.QuizDetail, error) { + result, err := q.getQuizDetail(ctx, quizId) + + if err != nil { + return nil, err + } + + _, err = q.GetLastTake(quizId, email) + + if err != nil { + return nil, err + } + + taken, err := q.IsActiveTake(quizId, email) + + if err != nil { + return nil, err + } + + if taken { + return nil, web.NewResponseError("user is not allow to access this data", "ERR_NOT_ALLOWED") + } + + return result, nil +} + +func (q QuizServiceImpl) checkAnswer(detail *quiz.QuizDetail, studentAnswer []quiz.Response) float64 { + answerDict := map[uuid.UUID][]uuid.UUID{} + totalProblem := len(detail.Problems) + + for _, problem := range detail.Problems { + correctAnswerId := []uuid.UUID{} + for _, answer := range problem.Answer { + if *answer.IsSolution { + correctAnswerId = append(correctAnswerId, answer.Id) + } + } + + answerDict[problem.Id] = correctAnswerId + } + + correctAnswer := 0 + for _, responseItem := range studentAnswer { + numCorrect := 0 + + for _, correctId := range answerDict[responseItem.ProblemId] { + if responseItem.AnswerId == correctId { + numCorrect++ + } + } + + if numCorrect == len(answerDict[responseItem.ProblemId]) { + correctAnswer++ + } + } + + return float64(correctAnswer) / float64(totalProblem) * 100 +} + +func (q QuizServiceImpl) DoFinishQuiz(ctx context.Context, quizId uuid.UUID, email string, studentAnswer []quiz.Response) (*quiz.QuizTake, error) { + taken, err := q.IsActiveTake(quizId, email) + + if err != nil { + return nil, err + } + + if !taken { + return nil, web.NewResponseError("user not yet do take the quiz", "NOT_TAKEN_QUIZ_YET") + } + + result, err := q.getQuizDetail(ctx, quizId) + + if err != nil { + return nil, err + } + + score := q.checkAnswer(result, studentAnswer) + + data, err := q.QuizRepository.GetLastTake(quizId, email) + data.IsFinished = true + data.Score = int(score) + + if err != nil { + return nil, err + } + + err = q.QuizRepository.UpdateScore(data.Id, int(score)) + + if err != nil { + return nil, err + } + + return data, nil +} diff --git a/service/quiz/type.go b/service/quiz/type.go index f7d0f72eb3a7a148c1e324a5ff800f99d38f5304..7c97e4965500fb25f75c19a4117c536559fcf9a8 100644 --- a/service/quiz/type.go +++ b/service/quiz/type.go @@ -1,6 +1,8 @@ package quiz import ( + "context" + "github.com/google/uuid" "gitlab.informatika.org/ocw/ocw-backend/model/domain/quiz" ) @@ -8,4 +10,8 @@ import ( type QuizService interface { ListAllQuiz(courseId string) ([]quiz.Quiz, error) GetQuizDetail(quizId uuid.UUID) (*quiz.Quiz, error) + + DoTakeQuiz(ctx context.Context, quizId uuid.UUID, email string) (*quiz.QuizDetail, error) + DoFinishQuiz(ctx context.Context, quizId uuid.UUID, email string, studentAnswer []quiz.Response) (*quiz.QuizTake, error) + GetSolutionQuiz(ctx context.Context, quizId uuid.UUID, email string) (*quiz.QuizDetail, error) }