diff --git a/apperror/error.go b/apperror/error.go index fd37cd8..e09a3d7 100644 --- a/apperror/error.go +++ b/apperror/error.go @@ -30,5 +30,6 @@ var ( NonRegisterableEvent = &AppError{"non-registerable-event", http.StatusBadRequest} Forbidden = &AppError{"forbidden", http.StatusForbidden} AlreadyCheckin = &AppError{"already-checkin", http.StatusConflict} + AlreadySubmitted = &AppError{"already-submitted", http.StatusConflict} NotFound = &AppError{"not-found", http.StatusNotFound} ) diff --git a/cmd/main.go b/cmd/main.go index 20b653c..13480b4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -34,6 +34,7 @@ func main() { r.GET("/events/:eventId", container.EventHandler.GetEventById) r.POST("/schedules/:scheduleId/register", container.EvtregHandler.RegisterEvent) r.POST("/staff/checkin/:userId", container.StaffHandler.AttendeeStaffCheckin) + r.POST("/feedback", container.FeedbackHandler.SubmitFeedback) r.POST("/auth/register", container.AuthHandler.Register) r.GET("/auth/me", container.AuthHandler.GetProfile) r.GET("/auth/login", container.AuthHandler.GoogleLogin) diff --git a/di/wire.go b/di/wire.go index 89105b5..51f8384 100644 --- a/di/wire.go +++ b/di/wire.go @@ -12,6 +12,7 @@ import ( event "github.com/isd-sgcu/oph66-backend/internal/event" "github.com/isd-sgcu/oph66-backend/internal/evtreg" featureflag "github.com/isd-sgcu/oph66-backend/internal/feature_flag" + "github.com/isd-sgcu/oph66-backend/internal/feedback" healthcheck "github.com/isd-sgcu/oph66-backend/internal/health_check" "github.com/isd-sgcu/oph66-backend/internal/middleware" "github.com/isd-sgcu/oph66-backend/internal/router" @@ -27,6 +28,7 @@ type Container struct { AuthHandler auth.Handler EvtregHandler evtreg.Handler StaffHandler staff.Handler + FeedbackHandler feedback.Handler Config *cfgldr.Config Logger *zap.Logger CorsHandler cfgldr.CorsHandler @@ -40,6 +42,7 @@ func newContainer( authHandler auth.Handler, evtregHandler evtreg.Handler, staffHandler staff.Handler, + feedbackHandler feedback.Handler, config *cfgldr.Config, logger *zap.Logger, corsHandler cfgldr.CorsHandler, @@ -52,6 +55,7 @@ func newContainer( authHandler, evtregHandler, staffHandler, + feedbackHandler, config, logger, corsHandler, @@ -81,6 +85,9 @@ func Init() (Container, error) { staff.NewRepository, staff.NewService, staff.NewHandler, + feedback.NewRepository, + feedback.NewService, + feedback.NewHandler, auth.NewHandler, auth.NewService, auth.NewRepository, diff --git a/di/wire_gen.go b/di/wire_gen.go index d4f27aa..e52e4e6 100644 --- a/di/wire_gen.go +++ b/di/wire_gen.go @@ -14,6 +14,7 @@ import ( "github.com/isd-sgcu/oph66-backend/internal/event" "github.com/isd-sgcu/oph66-backend/internal/evtreg" "github.com/isd-sgcu/oph66-backend/internal/feature_flag" + "github.com/isd-sgcu/oph66-backend/internal/feedback" "github.com/isd-sgcu/oph66-backend/internal/health_check" "github.com/isd-sgcu/oph66-backend/internal/middleware" "github.com/isd-sgcu/oph66-backend/internal/router" @@ -56,10 +57,13 @@ func Init() (Container, error) { staffRepository := staff.NewRepository(db) staffService := staff.NewService(staffRepository, authRepository, zapLogger) staffHandler := staff.NewHandler(staffService, zapLogger) + feedbackRepository := feedback.NewRepository(db) + feedbackService := feedback.NewService(feedbackRepository, authRepository, zapLogger) + feedbackHandler := feedback.NewHandler(feedbackService, zapLogger) corsHandler := cfgldr.MakeCorsConfig(config) authMiddleware := middleware.NewAuthMiddleware(authRepository, config) routerRouter := router.NewRouter(config, corsHandler, authMiddleware) - container := newContainer(handler, healthcheckHandler, featureflagHandler, authHandler, evtregHandler, staffHandler, config, zapLogger, corsHandler, routerRouter) + container := newContainer(handler, healthcheckHandler, featureflagHandler, authHandler, evtregHandler, staffHandler, feedbackHandler, config, zapLogger, corsHandler, routerRouter) return container, nil } @@ -72,6 +76,7 @@ type Container struct { AuthHandler auth.Handler EvtregHandler evtreg.Handler StaffHandler staff.Handler + FeedbackHandler feedback.Handler Config *cfgldr.Config Logger *zap.Logger CorsHandler cfgldr.CorsHandler @@ -85,6 +90,7 @@ func newContainer( authHandler auth.Handler, evtregHandler evtreg.Handler, staffHandler staff.Handler, + feedbackHandler feedback.Handler, config *cfgldr.Config, logger2 *zap.Logger, corsHandler cfgldr.CorsHandler, router2 *router.Router, ) Container { @@ -95,6 +101,7 @@ func newContainer( authHandler, evtregHandler, staffHandler, + feedbackHandler, config, logger2, corsHandler, router2, } } diff --git a/docs/docs.go b/docs/docs.go index 8d085df..d256bb7 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -278,6 +278,40 @@ const docTemplate = `{ } } }, + "/feedback": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Submit feedback form", + "produces": [ + "application/json" + ], + "tags": [ + "feedback" + ], + "summary": "Submit feedback form", + "operationId": "SubmitFeedback", + "parameters": [ + { + "description": "Feedback dto", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SubmitFeedbackDTO" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/live": { "get": { "description": "Get livestream flag", @@ -933,6 +967,91 @@ const docTemplate = `{ } } }, + "dto.SubmitFeedbackDTO": { + "type": "object", + "properties": { + "comment": { + "type": "string", + "example": "very good" + }, + "q1": { + "type": "string", + "example": "1" + }, + "q10": { + "type": "string", + "example": "1" + }, + "q11": { + "type": "string", + "example": "1" + }, + "q12": { + "type": "string", + "example": "1" + }, + "q13": { + "type": "string", + "example": "1" + }, + "q14": { + "type": "string", + "example": "1" + }, + "q15": { + "type": "string", + "example": "1" + }, + "q16": { + "type": "string", + "example": "1" + }, + "q17": { + "type": "string", + "example": "1" + }, + "q18": { + "type": "string", + "example": "1" + }, + "q19": { + "type": "string", + "example": "1" + }, + "q2": { + "type": "string", + "example": "1" + }, + "q3": { + "type": "string", + "example": "1" + }, + "q4": { + "type": "string", + "example": "1" + }, + "q5": { + "type": "string", + "example": "1" + }, + "q6": { + "type": "string", + "example": "1" + }, + "q7": { + "type": "string", + "example": "1" + }, + "q8": { + "type": "string", + "example": "1" + }, + "q9": { + "type": "string", + "example": "1" + } + } + }, "dto.User": { "type": "object", "properties": { @@ -955,6 +1074,10 @@ const docTemplate = `{ "type": "string", "example": "Ph.D." }, + "feedback_submitted": { + "type": "boolean", + "example": true + }, "first_name": { "type": "string", "example": "John" diff --git a/docs/swagger.json b/docs/swagger.json index ec6855d..767737f 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -273,6 +273,40 @@ } } }, + "/feedback": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Submit feedback form", + "produces": [ + "application/json" + ], + "tags": [ + "feedback" + ], + "summary": "Submit feedback form", + "operationId": "SubmitFeedback", + "parameters": [ + { + "description": "Feedback dto", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SubmitFeedbackDTO" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/live": { "get": { "description": "Get livestream flag", @@ -928,6 +962,91 @@ } } }, + "dto.SubmitFeedbackDTO": { + "type": "object", + "properties": { + "comment": { + "type": "string", + "example": "very good" + }, + "q1": { + "type": "string", + "example": "1" + }, + "q10": { + "type": "string", + "example": "1" + }, + "q11": { + "type": "string", + "example": "1" + }, + "q12": { + "type": "string", + "example": "1" + }, + "q13": { + "type": "string", + "example": "1" + }, + "q14": { + "type": "string", + "example": "1" + }, + "q15": { + "type": "string", + "example": "1" + }, + "q16": { + "type": "string", + "example": "1" + }, + "q17": { + "type": "string", + "example": "1" + }, + "q18": { + "type": "string", + "example": "1" + }, + "q19": { + "type": "string", + "example": "1" + }, + "q2": { + "type": "string", + "example": "1" + }, + "q3": { + "type": "string", + "example": "1" + }, + "q4": { + "type": "string", + "example": "1" + }, + "q5": { + "type": "string", + "example": "1" + }, + "q6": { + "type": "string", + "example": "1" + }, + "q7": { + "type": "string", + "example": "1" + }, + "q8": { + "type": "string", + "example": "1" + }, + "q9": { + "type": "string", + "example": "1" + } + } + }, "dto.User": { "type": "object", "properties": { @@ -950,6 +1069,10 @@ "type": "string", "example": "Ph.D." }, + "feedback_submitted": { + "type": "boolean", + "example": true + }, "first_name": { "type": "string", "example": "John" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4002f5b..db3f878 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -366,6 +366,69 @@ definitions: example: "2021-08-01T00:00:00+07:00" type: string type: object + dto.SubmitFeedbackDTO: + properties: + comment: + example: very good + type: string + q1: + example: "1" + type: string + q2: + example: "1" + type: string + q3: + example: "1" + type: string + q4: + example: "1" + type: string + q5: + example: "1" + type: string + q6: + example: "1" + type: string + q7: + example: "1" + type: string + q8: + example: "1" + type: string + q9: + example: "1" + type: string + q10: + example: "1" + type: string + q11: + example: "1" + type: string + q12: + example: "1" + type: string + q13: + example: "1" + type: string + q14: + example: "1" + type: string + q15: + example: "1" + type: string + q16: + example: "1" + type: string + q17: + example: "1" + type: string + q18: + example: "1" + type: string + q19: + example: "1" + type: string + type: object dto.User: properties: allergies: @@ -382,6 +445,9 @@ definitions: educational_level: example: Ph.D. type: string + feedback_submitted: + example: true + type: boolean first_name: example: John type: string @@ -603,6 +669,27 @@ paths: summary: get event by id tags: - event + /feedback: + post: + description: Submit feedback form + operationId: SubmitFeedback + parameters: + - description: Feedback dto + in: body + name: user + required: true + schema: + $ref: '#/definitions/dto.SubmitFeedbackDTO' + produces: + - application/json + responses: + "204": + description: No Content + security: + - Bearer: [] + summary: Submit feedback form + tags: + - feedback /live: get: description: Get livestream flag diff --git a/internal/auth/auth.repository.go b/internal/auth/auth.repository.go index 693fba6..aec3bc5 100644 --- a/internal/auth/auth.repository.go +++ b/internal/auth/auth.repository.go @@ -9,6 +9,7 @@ type Repository interface { CreateUser(user *model.User) error GetUserByEmail(user *model.User, email string) error GetUserById(model *model.User, id int) error + GetFeedbackByUserId(feedback *model.Feedback, userId int) error } type repositoryImpl struct { @@ -46,3 +47,7 @@ func (r *repositoryImpl) GetUserByEmail(user *model.User, email string) error { func (r *repositoryImpl) GetUserById(user *model.User, id int) error { return r.db.Model(user).First(&user, "id = ?", id).Error } + +func (r *repositoryImpl) GetFeedbackByUserId(feedback *model.Feedback, userId int) error { + return r.db.Model(feedback).Omit("comment").First(feedback, "user_id = ?", userId).Error +} diff --git a/internal/auth/auth.service.go b/internal/auth/auth.service.go index dd1456e..8afb692 100644 --- a/internal/auth/auth.service.go +++ b/internal/auth/auth.service.go @@ -95,5 +95,16 @@ func (s *serviceImpl) GetUserFromJWTToken(email string) (*dto.User, *apperror.Ap user := UserModelToDTO(&mUser) + formSubbmited := true + + if err = s.repo.GetFeedbackByUserId(&model.Feedback{}, user.Id); errors.Is(err, gorm.ErrRecordNotFound) { + formSubbmited = false + } else if err != nil { + s.logger.Error("failed to find user feedback", zap.Error(err), zap.String("email", email)) + return nil, apperror.InternalError + } + + user.FeedbackSubmitted = formSubbmited + return &user, nil } diff --git a/internal/dto/auth.dto.go b/internal/dto/auth.dto.go index b4d79f2..91be89a 100644 --- a/internal/dto/auth.dto.go +++ b/internal/dto/auth.dto.go @@ -53,6 +53,7 @@ type User struct { InterestedFaculties []FacultyInfo `json:"interested_faculties"` RegisteredEvents []Schedule `json:"registered_events"` VisitingFaculties []FacultyInfo `json:"visiting_faculties"` + FeedbackSubmitted bool `example:"true" json:"feedback_submitted"` } type NewsSource string diff --git a/internal/dto/feedback.dto.go b/internal/dto/feedback.dto.go new file mode 100644 index 0000000..eb86695 --- /dev/null +++ b/internal/dto/feedback.dto.go @@ -0,0 +1,24 @@ +package dto + +type SubmitFeedbackDTO struct { + Q1 string `example:"1" json:"q1"` + Q2 string `example:"1" json:"q2"` + Q3 string `example:"1" json:"q3"` + Q4 string `example:"1" json:"q4"` + Q5 string `example:"1" json:"q5"` + Q6 string `example:"1" json:"q6"` + Q7 string `example:"1" json:"q7"` + Q8 string `example:"1" json:"q8"` + Q9 string `example:"1" json:"q9"` + Q10 string `example:"1" json:"q10"` + Q11 string `example:"1" json:"q11"` + Q12 string `example:"1" json:"q12"` + Q13 string `example:"1" json:"q13"` + Q14 string `example:"1" json:"q14"` + Q15 string `example:"1" json:"q15"` + Q16 string `example:"1" json:"q16"` + Q17 string `example:"1" json:"q17"` + Q18 string `example:"1" json:"q18"` + Q19 string `example:"1" json:"q19"` + Comment string `example:"very good" json:"comment"` +} diff --git a/internal/feedback/feedback.handler.go b/internal/feedback/feedback.handler.go new file mode 100644 index 0000000..3f5ba0c --- /dev/null +++ b/internal/feedback/feedback.handler.go @@ -0,0 +1,58 @@ +package feedback + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/isd-sgcu/oph66-backend/apperror" + "github.com/isd-sgcu/oph66-backend/internal/dto" + "github.com/isd-sgcu/oph66-backend/utils" + "go.uber.org/zap" +) + +type Handler interface { + SubmitFeedback(c *gin.Context) +} + +type handlerImpl struct { + svc Service + logger *zap.Logger +} + +func NewHandler(svc Service, logger *zap.Logger) Handler { + return &handlerImpl{ + svc, + logger, + } +} + +// GoogleLogin godoc +// @summary Submit feedback form +// @description Submit feedback form +// @id SubmitFeedback +// @produce json +// @Security Bearer +// @tags feedback +// @router /feedback [post] +// @param user body dto.SubmitFeedbackDTO true "Feedback dto" +// @success 204 +func (h *handlerImpl) SubmitFeedback(c *gin.Context) { + email := c.GetString("email") + if email == "" { + utils.ReturnError(c, apperror.Unauthorized) + return + } + + var body dto.SubmitFeedbackDTO + if err := c.ShouldBindJSON(&body); err != nil { + utils.ReturnError(c, apperror.BadRequest) + return + } + + if apperr := h.svc.SubmitFeedback(&body, email); apperr != nil { + utils.ReturnError(c, apperr) + return + } + + c.AbortWithStatus(http.StatusNoContent) +} diff --git a/internal/feedback/feedback.repository.go b/internal/feedback/feedback.repository.go new file mode 100644 index 0000000..b91b85f --- /dev/null +++ b/internal/feedback/feedback.repository.go @@ -0,0 +1,23 @@ +package feedback + +import ( + "github.com/isd-sgcu/oph66-backend/internal/model" + "gorm.io/gorm" +) + +type Repository interface { + CreateFeedback(feedback *model.Feedback) error +} + +type repositoryImpl struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) Repository { + return &repositoryImpl{ + db, + } +} +func (r *repositoryImpl) CreateFeedback(feedback *model.Feedback) error { + return r.db.Model(feedback).Create(feedback).Error +} diff --git a/internal/feedback/feedback.service.go b/internal/feedback/feedback.service.go new file mode 100644 index 0000000..d1ce5c4 --- /dev/null +++ b/internal/feedback/feedback.service.go @@ -0,0 +1,53 @@ +package feedback + +import ( + "errors" + + "github.com/isd-sgcu/oph66-backend/apperror" + "github.com/isd-sgcu/oph66-backend/internal/auth" + "github.com/isd-sgcu/oph66-backend/internal/dto" + "github.com/isd-sgcu/oph66-backend/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type Service interface { + SubmitFeedback(dto *dto.SubmitFeedbackDTO, userEmail string) *apperror.AppError +} + +func NewService(repo Repository, userRepo auth.Repository, logger *zap.Logger) Service { + return &serviceImpl{ + repo, + userRepo, + logger, + } +} + +type serviceImpl struct { + repo Repository + userRepo auth.Repository + logger *zap.Logger +} + +func (s *serviceImpl) SubmitFeedback(dto *dto.SubmitFeedbackDTO, userEmail string) *apperror.AppError { + var user model.User + + if err := s.userRepo.GetUserByEmail(&user, userEmail); errors.Is(err, gorm.ErrRecordNotFound) { + return apperror.NotFound + } else if err != nil { + s.logger.Error("unable to get user by email", zap.String("email", userEmail), zap.Any("submitFeedbackDto", dto)) + return apperror.InternalError + } + + feedback := FeedbackDTOToModel(dto) + feedback.UserId = user.Id + + if err := s.repo.CreateFeedback(&feedback); errors.Is(err, gorm.ErrDuplicatedKey) { + return apperror.AlreadySubmitted + } else if err != nil { + s.logger.Error("unable to submit feedback form", zap.String("email", userEmail), zap.Any("submitFeedbackDto", dto), zap.Any("feedback", feedback)) + return apperror.InternalError + } + + return nil +} diff --git a/internal/feedback/feedback.utils.go b/internal/feedback/feedback.utils.go new file mode 100644 index 0000000..4ec9da9 --- /dev/null +++ b/internal/feedback/feedback.utils.go @@ -0,0 +1,35 @@ +package feedback + +import ( + "github.com/isd-sgcu/oph66-backend/internal/dto" + "github.com/isd-sgcu/oph66-backend/internal/model" +) + +func FeedbackDTOToModel(dto *dto.SubmitFeedbackDTO) model.Feedback { + var m model.Feedback + if dto == nil { + return m + } + m.Q1 = model.FirstpartAnswer(dto.Q1) + m.Q2 = model.FirstpartAnswer(dto.Q2) + m.Q3 = model.FirstpartAnswer(dto.Q3) + m.Q4 = model.FirstpartAnswer(dto.Q4) + m.Q5 = model.FirstpartAnswer(dto.Q5) + m.Q6 = model.FirstpartAnswer(dto.Q6) + m.Q7 = model.SecondPartAnswer(dto.Q7) + m.Q8 = model.SecondPartAnswer(dto.Q8) + m.Q9 = model.SecondPartAnswer(dto.Q9) + m.Q10 = model.SecondPartAnswer(dto.Q10) + m.Q11 = model.SecondPartAnswer(dto.Q11) + m.Q12 = model.SecondPartAnswer(dto.Q12) + m.Q13 = model.SecondPartAnswer(dto.Q13) + m.Q14 = model.SecondPartAnswer(dto.Q14) + m.Q15 = model.SecondPartAnswer(dto.Q15) + m.Q16 = model.SecondPartAnswer(dto.Q16) + m.Q17 = model.SecondPartAnswer(dto.Q17) + m.Q18 = model.SecondPartAnswer(dto.Q18) + m.Q19 = model.SecondPartAnswer(dto.Q19) + m.Comment = dto.Comment + + return m +} diff --git a/internal/model/feedback.model.go b/internal/model/feedback.model.go new file mode 100644 index 0000000..68d40a7 --- /dev/null +++ b/internal/model/feedback.model.go @@ -0,0 +1,30 @@ +package model + +type FirstpartAnswer string + +type SecondPartAnswer string + +type Feedback struct { + User User `gorm:"foreignKey:UserId;references:Id"` + UserId int `gorm:""` + Q1 FirstpartAnswer `gorm:""` + Q2 FirstpartAnswer `gorm:""` + Q3 FirstpartAnswer `gorm:""` + Q4 FirstpartAnswer `gorm:""` + Q5 FirstpartAnswer `gorm:""` + Q6 FirstpartAnswer `gorm:""` + Q7 SecondPartAnswer `gorm:""` + Q8 SecondPartAnswer `gorm:""` + Q9 SecondPartAnswer `gorm:""` + Q10 SecondPartAnswer `gorm:""` + Q11 SecondPartAnswer `gorm:""` + Q12 SecondPartAnswer `gorm:""` + Q13 SecondPartAnswer `gorm:""` + Q14 SecondPartAnswer `gorm:""` + Q15 SecondPartAnswer `gorm:""` + Q16 SecondPartAnswer `gorm:""` + Q17 SecondPartAnswer `gorm:""` + Q18 SecondPartAnswer `gorm:""` + Q19 SecondPartAnswer `gorm:""` + Comment string `gorm:""` +} diff --git a/migrations/04-feedback.sql b/migrations/04-feedback.sql new file mode 100644 index 0000000..bb610f7 --- /dev/null +++ b/migrations/04-feedback.sql @@ -0,0 +1,42 @@ +CREATE TYPE feedback_first_part_answer AS ENUM ( + '1', + '2', + '3', + '4', + '5' +); + +CREATE TYPE feedback_second_part_answer AS ENUM ( + '1', + '2', + '3', + '4', + '5', + 'did-not-attend' +); + +CREATE TABLE feedbacks ( + "user_id" INT NOT NULL, + "q1" feedback_first_part_answer NOT NULL, + "q2" feedback_first_part_answer NOT NULL, + "q3" feedback_first_part_answer NOT NULL, + "q4" feedback_first_part_answer NOT NULL, + "q5" feedback_first_part_answer NOT NULL, + "q6" feedback_first_part_answer NOT NULL, + "q7" feedback_second_part_answer NOT NULL, + "q8" feedback_second_part_answer NOT NULL, + "q9" feedback_second_part_answer NOT NULL, + "q10" feedback_second_part_answer NOT NULL, + "q11" feedback_second_part_answer NOT NULL, + "q12" feedback_second_part_answer NOT NULL, + "q13" feedback_second_part_answer NOT NULL, + "q14" feedback_second_part_answer NOT NULL, + "q15" feedback_second_part_answer NOT NULL, + "q16" feedback_second_part_answer NOT NULL, + "q17" feedback_second_part_answer NOT NULL, + "q18" feedback_second_part_answer NOT NULL, + "q19" feedback_second_part_answer NOT NULL, + "comment" VARCHAR(600), + PRIMARY KEY ("user_id"), + CONSTRAINT "fk_feedbacks_users" FOREIGN KEY ("user_id") REFERENCES "users" ("id") +);