-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add pagination, filters, and CSV download in participants & question #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -275,6 +275,9 @@ const ( | |
| const ( | ||
| SingleAnswerString = "single answer" | ||
| SurveyString = "survey" | ||
| SingleString = "single" | ||
| AllString = "all" | ||
| SingleAnsField = "single answer" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| SingleAnswer = 1 | ||
| Survey = 2 | ||
|
|
@@ -296,3 +299,23 @@ const ( | |
| ChannelUserDisconnect = "user_disconnect" | ||
| ChannelSetAnswer = "set_answer" | ||
| ) | ||
|
|
||
| // Analysis Download Queries | ||
| const ( | ||
| DownloadFileName = "quiz_analysis" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i don't able to find the use of |
||
| QuestionType = "question_type" | ||
| ContentType = "Content-Type" | ||
| CsvAcceptHeader = "text/csv" | ||
| ) | ||
|
|
||
| const ( | ||
| PageLimit = "user_limit" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why its value is |
||
| PageOffset = "starting_at" | ||
| SortOrder = "order_by" | ||
| AscOrder = "asc" | ||
| DescOrder = "desc" | ||
| DefaultUserLimit = "10" | ||
| DefaultUserOffset = "0" | ||
| IsTop10Users = "top_10" | ||
| Top10UserTrueValue = "true" | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| package v1 | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "strings" | ||
|
|
||
| "github.com/Improwised/jovvix/api/config" | ||
| "github.com/Improwised/jovvix/api/constants" | ||
| "github.com/Improwised/jovvix/api/models" | ||
| "github.com/Improwised/jovvix/api/services" | ||
| "github.com/Improwised/jovvix/api/utils" | ||
| "github.com/doug-martin/goqu/v9" | ||
| fiber "github.com/gofiber/fiber/v2" | ||
| "go.uber.org/zap" | ||
| ) | ||
|
|
||
| type DownloadParticipantsController struct { | ||
| QuizModel *models.QuizModel | ||
| FinalScoreBoardAdminModel models.FinalScoreBoardAdminModel | ||
| Config *config.AppConfig | ||
| } | ||
|
|
||
| func InitDownloadParticipantsController(db *goqu.Database, logger *zap.Logger, appConfig *config.AppConfig) (*DownloadParticipantsController, error) { | ||
| quizModel := models.InitQuizModel(db) | ||
|
|
||
| finalScoreBoardAdminModel, err := models.InitFinalScoreBoardAdminModel(db) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return &DownloadParticipantsController{ | ||
| QuizModel: quizModel, | ||
| FinalScoreBoardAdminModel: finalScoreBoardAdminModel, | ||
| Config: appConfig, | ||
| }, nil | ||
| } | ||
|
|
||
| func (dcp *DownloadParticipantsController) DownloadParticipantsReport(c *fiber.Ctx) error { | ||
| activeQuizId := c.Params(constants.ActiveQuizId) | ||
| isTop10Users := strings.ToLower(c.Query(constants.IsTop10Users)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. convert this var into bool. |
||
| userOrder := strings.ToLower(c.Query(constants.SortOrder)) | ||
|
|
||
| order := constants.DescOrder | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use only one var for |
||
|
|
||
| quizAnalysis, err := dcp.QuizModel.GetQuizAnalysis(activeQuizId) | ||
| if err != nil { | ||
| return utils.JSONError(c, http.StatusInternalServerError, err.Error()) | ||
| } | ||
|
|
||
| totalUsers, err := dcp.FinalScoreBoardAdminModel.TotoalUsers(activeQuizId) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| limit := totalUsers | ||
|
|
||
| if isTop10Users == constants.Top10UserTrueValue { | ||
| limit = 10 | ||
| } | ||
|
|
||
| if userOrder == constants.AscOrder { | ||
| order = constants.AscOrder | ||
| } | ||
|
|
||
| rankData, err := dcp.FinalScoreBoardAdminModel.GetScoreForAdmin(activeQuizId, limit, 0, order) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| input := utils.InputParticipants{ | ||
| QuizAnalysis: quizAnalysis, | ||
| FinalScoreBoardAdmin: rankData, | ||
| } | ||
|
|
||
| filePath, err := services.GenerateCsv(dcp.Config.FilePath, activeQuizId, input, func(ip utils.InputParticipants) [][]string { | ||
| return utils.GetParticipantsData(ip) | ||
| }) | ||
|
|
||
| if err != nil { | ||
| return err | ||
| } | ||
| return utils.CsvFileResponse(c, filePath, activeQuizId+".csv") | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,8 @@ package v1 | |
| import ( | ||
| "errors" | ||
| "net/http" | ||
| "strconv" | ||
| "strings" | ||
|
|
||
| "github.com/Improwised/jovvix/api/constants" | ||
| "github.com/Improwised/jovvix/api/models" | ||
|
|
@@ -45,19 +47,51 @@ func NewFinalScoreBoardAdminController(goqu *goqu.Database, logger *zap.Logger) | |
| // 400: GenericResFailNotFound | ||
| // 500: GenericResError | ||
| func (fc *FinalScoreBoardAdminController) GetScoreForAdmin(ctx *fiber.Ctx) error { | ||
|
|
||
| var limit, offset int | ||
| var err error | ||
| var activeQuizId = ctx.Query(constants.ActiveQuizId) | ||
|
|
||
| userLimit := ctx.Query(constants.PageLimit) | ||
| userOffset := ctx.Query(constants.PageOffset) | ||
| userSortOrder := strings.ToLower(ctx.Query(constants.SortOrder)) | ||
|
|
||
| if userLimit == "" { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use one var for all query params. |
||
| userLimit = constants.DefaultUserLimit | ||
| } | ||
|
|
||
| if userOffset == "" { | ||
| userOffset = constants.DefaultUserOffset | ||
| } | ||
|
|
||
| limit, err = strconv.Atoi(userLimit) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| offset, err = strconv.Atoi(userOffset) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if userSortOrder != constants.AscOrder && userSortOrder != constants.DescOrder { | ||
| userSortOrder = constants.DescOrder | ||
| } | ||
|
|
||
| if !(activeQuizId != "" && len(activeQuizId) == 36) { | ||
| fc.logger.Debug("active quiz id is not valid - either empty string or it is not 36 characters long") | ||
| return utils.JSONFail(ctx, http.StatusBadRequest, errors.New("user play quiz should be valid string").Error()) | ||
| } | ||
|
|
||
| finalScoreBoardData, err := fc.finalScoreBoardAdminModel.GetScoreForAdmin(activeQuizId) | ||
| finalScoreBoardData, err := fc.finalScoreBoardAdminModel.GetScoreForAdmin(activeQuizId, limit, offset, userSortOrder) | ||
| if err != nil { | ||
| fc.logger.Error("Error while getting final scoreboard for admin", zap.Error(err)) | ||
| return utils.JSONFail(ctx, http.StatusInternalServerError, errors.New("internal server error").Error()) | ||
| } | ||
| totalUsers, err := fc.finalScoreBoardAdminModel.TotoalUsers(activeQuizId) | ||
|
|
||
| return utils.JSONSuccess(ctx, http.StatusOK, finalScoreBoardData) | ||
| if err != nil { | ||
| fc.logger.Error("Error while getting total user", zap.Error(err)) | ||
| return utils.JSONFail(ctx, http.StatusInternalServerError, errors.New("internal server error").Error()) | ||
| } | ||
| return utils.JSONSuccessWithPagination(ctx, http.StatusOK, finalScoreBoardData, totalUsers) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,12 +26,18 @@ func InitFinalScoreBoardAdminModel(goqu *goqu.Database) (FinalScoreBoardAdminMod | |
|
|
||
| // GetScoreForAdmin to send final score after quiz over | ||
|
|
||
| func (model *FinalScoreBoardAdminModel) GetScoreForAdmin(activeQuizId string) ([]FinalScoreBoardAdmin, error) { | ||
| func (model *FinalScoreBoardAdminModel) GetScoreForAdmin(activeQuizId string, limit, offset int, sortOrder string) ([]FinalScoreBoardAdmin, error) { | ||
| var finalScoreBoardData []FinalScoreBoardAdmin | ||
|
|
||
| UserQuizResponseTable := "user_quiz_responses" | ||
| UserPlayedQuizTable := "user_played_quizzes" | ||
|
|
||
| order := goqu.I("score").Desc() | ||
|
|
||
| if sortOrder == "asc" { | ||
| order = goqu.I("score").Asc() | ||
| } | ||
|
|
||
| err := model.db. | ||
| From(goqu.T("users")). | ||
| Select( | ||
|
|
@@ -53,6 +59,9 @@ func (model *FinalScoreBoardAdminModel) GetScoreForAdmin(activeQuizId string) ([ | |
| UserPlayedQuizTable + ".active_quiz_id": activeQuizId, | ||
| }). | ||
| GroupBy(goqu.I("users.id")). | ||
| Order(order). | ||
| Limit(uint(limit)). | ||
| Offset(uint(offset)). | ||
| ScanStructs(&finalScoreBoardData) | ||
|
|
||
| if err != nil { | ||
|
|
@@ -61,3 +70,22 @@ func (model *FinalScoreBoardAdminModel) GetScoreForAdmin(activeQuizId string) ([ | |
|
|
||
| return finalScoreBoardData, nil | ||
| } | ||
|
|
||
| func (model *FinalScoreBoardAdminModel) TotoalUsers(activeQuizId string) (int, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo in function name. |
||
| var totalCount int | ||
| ds := model.db.From("users"). | ||
| Join( | ||
| goqu.T("user_played_quizzes"), | ||
| goqu.On(goqu.I("user_played_quizzes.user_id").Eq(goqu.I("users.id"))), | ||
| ). | ||
| Select(goqu.COUNT("*")). | ||
| Where( | ||
| goqu.I("user_played_quizzes.active_quiz_id").Eq(activeQuizId), | ||
| ) | ||
|
|
||
| _, err := ds.ScanVal(&totalCount) | ||
| if err != nil { | ||
| return 0, err | ||
| } | ||
| return totalCount, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package structs | ||
|
|
||
| type OptionsWithSelectedCount struct { | ||
| Option string `csv:"option"` | ||
| SelectedCount int `csv:"selected_count"` | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package structs | ||
|
|
||
| type Participants struct { | ||
| UserName string | ||
| Rank int | ||
| Score int | ||
| Accuracy int | ||
| CorrectAnswers int | ||
| SurveyAnswers int | ||
| Attempted int | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -127,6 +127,11 @@ func Setup(app *fiber.App, goqu *goqu.Database, logger *zap.Logger, config confi | |
| return err | ||
| } | ||
|
|
||
| err = setupDownloadPaticipantsController(v1, goqu, logger, middleware, config) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
|
|
@@ -252,6 +257,7 @@ func setupQuizController(v1 fiber.Router, db *goqu.Database, logger *zap.Logger, | |
| report := admin.Group("/reports") | ||
| report.Get("/list", quizController.ListQuizzesAnalysis) | ||
| report.Get(fmt.Sprintf("/:%s/analysis", constants.ActiveQuizId), middleware.KratosAuthenticated, quizController.GetQuizAnalysis) | ||
| report.Get(fmt.Sprintf("/:%s/download/analysis", constants.ActiveQuizId), middleware.KratosAuthenticated, quizController.DownloadReport) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. route is like |
||
| return nil | ||
| } | ||
|
|
||
|
|
@@ -349,3 +355,14 @@ func setupSharedQuizzesController(v1 fiber.Router, goqu *goqu.Database, logger * | |
| sharedQuizzesRouter.Delete(fmt.Sprintf("/:%s", constants.QuizId), middlewares.QuizPermission, middlewares.VerifyQuizShareAccess, sharedQuizzesController.DeleteUserPermissionOfQuiz) | ||
| return nil | ||
| } | ||
|
|
||
| func setupDownloadPaticipantsController(v1 fiber.Router, goqu *goqu.Database, logger *zap.Logger, middleware middlewares.Middleware, config config.AppConfig) error { | ||
| downloadParticipantsController, err := controller.InitDownloadParticipantsController(goqu, logger, &config) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| reports := v1.Group("/admin/reports") | ||
| reports.Get(fmt.Sprintf("/:%s/download/participants", constants.ActiveQuizId), middleware.KratosAuthenticated, downloadParticipantsController.DownloadParticipantsReport) | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package services | ||
|
|
||
| import ( | ||
| "encoding/csv" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| ) | ||
|
|
||
| func GenerateCsv[T any](filePath, id string, analysis T, getData func(T) [][]string) (string, error) { | ||
| fullPath := filepath.Join(filePath, id+".csv") | ||
|
|
||
| csvFile, err := os.Create(fullPath) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| defer csvFile.Close() | ||
|
|
||
| csvwriter := csv.NewWriter(csvFile) | ||
| defer csvwriter.Flush() | ||
|
|
||
| data := getData(analysis) | ||
| if data == nil { | ||
| return "", fmt.Errorf("no data found") | ||
| } | ||
|
|
||
| for _, row := range data { | ||
| if err := csvwriter.Write(row); err != nil { | ||
| return "", err | ||
| } | ||
| } | ||
|
|
||
| if err = csvwriter.Error(); err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| return fullPath, nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mention which FilePath. means give proper var name