Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type AppConfig struct {
Secret string `envconfig:"JWT_SECRET"`
WebUrl string `envconfig:"WEB_URL"`
JWTIssuer string `envconfig:"ISSUER"`
FilePath string `envconfig:"FILE_PATH"`
Copy link
Contributor

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

RedisClient RedisClientConfig
DB DBConfig
Kratos KratosConfig
Expand Down
23 changes: 23 additions & 0 deletions api/constants/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@ const (
const (
SingleAnswerString = "single answer"
SurveyString = "survey"
SingleString = "single"
AllString = "all"
SingleAnsField = "single answer"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

single answer is already there.


SingleAnswer = 1
Survey = 2
Expand All @@ -296,3 +299,23 @@ const (
ChannelUserDisconnect = "user_disconnect"
ChannelSetAnswer = "set_answer"
)

// Analysis Download Queries
const (
DownloadFileName = "quiz_analysis"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't able to find the use of DownloadFileName use. in this PR.

QuestionType = "question_type"
ContentType = "Content-Type"
CsvAcceptHeader = "text/csv"
)

const (
PageLimit = "user_limit"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why its value is user_limit,

PageOffset = "starting_at"
SortOrder = "order_by"
AscOrder = "asc"
DescOrder = "desc"
DefaultUserLimit = "10"
DefaultUserOffset = "0"
IsTop10Users = "top_10"
Top10UserTrueValue = "true"
)
83 changes: 83 additions & 0 deletions api/controllers/api/v1/download_participants_controller.go
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))
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use only one var for order.


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")
}
40 changes: 37 additions & 3 deletions api/controllers/api/v1/final_scoreboard_admin_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package v1
import (
"errors"
"net/http"
"strconv"
"strings"

"github.com/Improwised/jovvix/api/constants"
"github.com/Improwised/jovvix/api/models"
Expand Down Expand Up @@ -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 == "" {
Copy link
Contributor

Choose a reason for hiding this comment

The 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)
}
27 changes: 27 additions & 0 deletions api/controllers/api/v1/quiz_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,30 @@ func (ctrl *QuizController) DeleteQuizById(c *fiber.Ctx) error {
ctrl.logger.Debug("QuizController.DeleteQuizById success", zap.Any(constants.QuizId, quizId))
return utils.JSONSuccess(c, http.StatusOK, "success")
}

func (qc *QuizController) DownloadReport(c *fiber.Ctx) error {
questionType := c.Query(constants.QuestionType)
activeQuizId := c.Params(constants.ActiveQuizId)
contentType := c.Get(constants.ContentType)

quizAnalysis, err := qc.quizModel.GetQuizAnalysis(activeQuizId)

if err != nil {
qc.logger.Error("error while get quiz analysis", zap.Error(err))
return utils.JSONError(c, http.StatusInternalServerError, err.Error())
}

if contentType == constants.CsvAcceptHeader {

filePath, err := services.GenerateCsv(qc.appConfig.FilePath, activeQuizId, quizAnalysis, func(a []models.QuizAnalysis) [][]string {
return utils.GetQuestionsData(a, questionType)
})

if err != nil {
return err
}
return utils.CsvFileResponse(c, filePath, activeQuizId+".csv")
}

return utils.JSONSuccess(c, http.StatusOK, quizAnalysis)
}
30 changes: 29 additions & 1 deletion api/models/final_scoreboard_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 {
Expand All @@ -61,3 +70,22 @@ func (model *FinalScoreBoardAdminModel) GetScoreForAdmin(activeQuizId string) ([

return finalScoreBoardData, nil
}

func (model *FinalScoreBoardAdminModel) TotoalUsers(activeQuizId string) (int, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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
}
6 changes: 6 additions & 0 deletions api/pkg/structs/csv.go
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"`
}
11 changes: 11 additions & 0 deletions api/pkg/structs/participants.go
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
}
17 changes: 17 additions & 0 deletions api/routes/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

route is like analysis/download

return nil
}

Expand Down Expand Up @@ -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
}
38 changes: 38 additions & 0 deletions api/services/csv.go
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
}
Loading