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
6 changes: 6 additions & 0 deletions backend/lms-main-system/app/gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
fb "github.com/multi-tenants-cms-golang/lms-sys/protogen/files"
lspb "github.com/multi-tenants-cms-golang/lms-sys/protogen/lesson"
mpb "github.com/multi-tenants-cms-golang/lms-sys/protogen/modules"
qspb "github.com/multi-tenants-cms-golang/lms-sys/protogen/quiz"
"github.com/rakyll/statik/fs"
"github.com/rs/cors"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -99,6 +100,11 @@ func (g *Gateway) Start() error {
if err != nil {
return fmt.Errorf("failed to register module handler: %w", err)
}
// Quiz
err = qspb.RegisterQuizServiceHandlerFromEndpoint(ctx, gwMux, g.grpcAddr, opts)
if err != nil {
return fmt.Errorf("failed to register quiz handler: %w", err)
}

err = fb.RegisterFileServiceHandlerFromEndpoint(
ctx,
Expand Down
119 changes: 119 additions & 0 deletions backend/lms-main-system/app/rpc/quiz/rpc_create_quiz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package quiz

import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/multi-tenants-cms-golang/lms-sys/internal/convert/global"
"github.com/multi-tenants-cms-golang/lms-sys/internal/repo"
db "github.com/multi-tenants-cms-golang/lms-sys/internal/repo"
"github.com/multi-tenants-cms-golang/lms-sys/pkg/utils"
qpb "github.com/multi-tenants-cms-golang/lms-sys/protogen/quiz"
"github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)

func (qs *QuizService) CreateQuiz(ctx context.Context, req *qpb.CreateQuizRequest) (*qpb.QuizResponse, error) {
qs.logger.WithFields(logrus.Fields{
"method": "CreateQuiz",
"params": req,
}).Info("Creating quiz")

md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, utils.ErrMissingOrganization().ToGRPCStatus()
}
organization := md.Get("x-organisation")
if len(organization) <= 0 {
return nil, utils.ErrMissingOrganization().ToGRPCStatus()
}
organizationName := organization[0]

// Validate ModuleID
if req.ModuleId == "" {
return nil, status.Error(codes.InvalidArgument, "Module ID is required")
}
pgModuleID, err := global.ConvertStringToUUID(req.ModuleId)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

fmt.Println("Org Name:", organizationName)
fmt.Println("Module ID:", pgModuleID)
// Check module exists
args := repo.GetModuleByIDWithTenantParams{
ModuleID: uuid.MustParse(pgModuleID.String()),
Namespace: organizationName,
}
module, err := qs.store.GetModuleByIDWithTenant(ctx, args)
if err != nil {
qs.logger.WithError(err).Error("Failed to fetch module")
return nil, status.Errorf(codes.NotFound, "Module not found")
}

// Create Quiz
quizResult, err := qs.store.CreateQuiz(ctx, db.CreateQuizParams{
ModuleID: module.ModuleID,
Title: "Quiz for " + module.ModuleName,
})
if err != nil {
qs.logger.WithError(err).Error("Failed to create quiz")
return nil, status.Errorf(codes.Internal, "Unable to create quiz")
}

questionIDToQuestion := make(map[uuid.UUID]*qpb.Question)
var allQuestions []*qpb.Question

// Insert each question and its answers
for _, q := range req.Quiz {
questionResult, err := qs.store.CreateQuestion(ctx, db.CreateQuestionParams{
Question: q.Question,
QuizID: quizResult.QuizID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "Failed to create question")
}

questionPB := &qpb.Question{
QuestionId: questionResult.QuestionID.String(),
QuizId: quizResult.QuizID.String(),
Question: questionResult.Question,
Answers: []*qpb.Answer{},
}
allQuestions = append(allQuestions, questionPB)
questionIDToQuestion[questionResult.QuestionID] = questionPB

for _, a := range q.AnswerOptions {
answerResult, err := qs.store.CreateAnswer(ctx, db.CreateAnswerParams{
Answer: a.Answer,
IsCorrect: pgtype.Bool{Bool: a.IsCorrect, Valid: true},
QuestionID: questionResult.QuestionID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "Failed to create answer")
}

questionPB.Answers = append(questionPB.Answers, &qpb.Answer{
AnswerId: answerResult.AnswerID.String(),
QuestionId: answerResult.QuestionID.String(),
Answer: answerResult.Answer,
IsCorrect: answerResult.IsCorrect.Bool,
})
}
}

return &qpb.QuizResponse{
Quiz: &qpb.Quiz{
QuizId: quizResult.QuizID.String(),
ModuleId: quizResult.ModuleID.String(),
Title: quizResult.Title,
CreatedAt: timestamppb.New(quizResult.CreatedAt.Time),
UpdatedAt: timestamppb.New(quizResult.UpdatedAt.Time),
},
Questions: allQuestions,
}, nil
}
56 changes: 56 additions & 0 deletions backend/lms-main-system/app/rpc/quiz/rpc_delete_quiz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package quiz

import (
"context"
"github.com/google/uuid"
qpb "github.com/multi-tenants-cms-golang/lms-sys/protogen/quiz"
"github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)

func (qs *QuizService) DeleteQuiz(ctx context.Context, req *qpb.DeleteQuizRequest) (*emptypb.Empty, error) {
qs.logger.WithFields(logrus.Fields{
"method": "DeleteQuiz",
"req": req,
}).Info("Deleting quizzes")

for _, id := range req.Ids {
quizID, err := uuid.Parse(id)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid quiz ID: %v", err)
}

// Check for existing questions
questions, err := qs.store.GetQuestionsByQuizID(ctx, quizID)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to fetch quiz questions: %v", err)
}

if len(questions) > 0 && !req.ForceDelete {
return nil, status.Errorf(codes.FailedPrecondition, "Quiz %s has associated questions. Use force_delete: true to force delete.", id)
}

if req.ForceDelete {
// Delete answers
for _, q := range questions {
if err := qs.store.DeleteAnswersByQuestionId(ctx, uuid.MustParse(q.QuestionID.String())); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete answers: %v", err)
}
}

// Delete questions
if err := qs.store.DeleteQuestionsByQuizId(ctx, quizID); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete questions: %v", err)
}
}

// Delete quiz
if err := qs.store.DeleteQuizById(ctx, quizID); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete quiz: %v", err)
}
}

return &emptypb.Empty{}, nil
}
85 changes: 85 additions & 0 deletions backend/lms-main-system/app/rpc/quiz/rpc_get_quiz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package quiz

import (
"context"
"github.com/google/uuid"
"github.com/multi-tenants-cms-golang/lms-sys/internal/convert/global"
qpb "github.com/multi-tenants-cms-golang/lms-sys/protogen/quiz"
"github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)

func (qs *QuizService) GetQuizzesByModule(ctx context.Context, req *qpb.GetQuizByModuleRequest) (*qpb.GetQuizByModuleResponse, error) {
qs.logger.WithFields(logrus.Fields{
"method": "GetQuizzesByModule",
"params": req,
}).Info("Fetching quizzes by module")

// Validate input
if req.ModuleId == "" {
return nil, status.Error(codes.InvalidArgument, "Module ID is required")
}
pgModuleID, err := global.ConvertStringToUUID(req.ModuleId)
if err != nil {
return nil, status.Error(codes.InvalidArgument, "Invalid Module ID")
}
// Get quizzes by module ID
quizzes, err := qs.store.GetQuizzesByModule(ctx, uuid.MustParse(pgModuleID.String()))
if err != nil {
qs.logger.WithError(err).Error("Failed to fetch quizzes")
return nil, status.Errorf(codes.Internal, "Failed to fetch quizzes")
}
var quizResponses []*qpb.QuizResponse

for _, quiz := range quizzes {
questions, err := qs.store.GetQuestionsByQuizID(ctx, quiz.QuizID)
if err != nil {
qs.logger.WithError(err).Error("Failed to fetch questions")
return nil, status.Errorf(codes.Internal, "Failed to fetch questions for quiz %s", quiz.QuizID)
}

var questionResponses []*qpb.Question

for _, question := range questions {
answers, err := qs.store.GetAnswersByQuestionID(ctx, question.QuestionID)
if err != nil {
qs.logger.WithError(err).Error("Failed to fetch answers")
return nil, status.Errorf(codes.Internal, "Failed to fetch answers for question %s", question.QuestionID)
}

var answerResponses []*qpb.Answer
for _, ans := range answers {
answerResponses = append(answerResponses, &qpb.Answer{
AnswerId: ans.AnswerID.String(),
QuestionId: ans.QuestionID.String(),
Answer: ans.Answer,
IsCorrect: ans.IsCorrect.Bool,
})
}

questionResponses = append(questionResponses, &qpb.Question{
QuestionId: question.QuestionID.String(),
QuizId: question.QuizID.String(),
Question: question.Question,
Answers: answerResponses,
})
}

quizResponses = append(quizResponses, &qpb.QuizResponse{
Quiz: &qpb.Quiz{
QuizId: quiz.QuizID.String(),
ModuleId: quiz.ModuleID.String(),
Title: quiz.Title,
CreatedAt: timestamppb.New(quiz.CreatedAt.Time),
UpdatedAt: timestamppb.New(quiz.UpdatedAt.Time),
},
Questions: questionResponses,
})
}

return &qpb.GetQuizByModuleResponse{
Quizzes: quizResponses,
}, nil
}
89 changes: 89 additions & 0 deletions backend/lms-main-system/app/rpc/quiz/rpc_update_quiz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package quiz

import (
"context"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/multi-tenants-cms-golang/lms-sys/internal/convert/global"
"github.com/multi-tenants-cms-golang/lms-sys/internal/repo"
qpb "github.com/multi-tenants-cms-golang/lms-sys/protogen/quiz"
"github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)

func (qs *QuizService) UpdateQuiz(ctx context.Context, req *qpb.UpdateQuizRequest) (*qpb.Question, error) {
qs.logger.WithFields(logrus.Fields{
"method": "UpdateQuiz",
"params": req,
}).Info("Updating quiz")

// Validate input
if req.QuestionId == "" {
return nil, status.Error(codes.InvalidArgument, "Question Id is required")
}
if req.Question == "" {
return nil, status.Error(codes.InvalidArgument, "Question is required")
}

questionID, err := global.ConvertStringToUUID(req.QuestionId)
if err != nil {
return nil, status.Error(codes.InvalidArgument, "Invalid question ID")
}
qUUID := uuid.MustParse(questionID.String())
// Update the question
updateQ := repo.UpdateQuestionParams{
Question: req.Question,
QuestionID: qUUID,
}

_, err = qs.store.UpdateQuestion(ctx, updateQ)
if err != nil {
qs.logger.WithError(err).Error("Failed to update question")
return nil, status.Error(codes.Internal, "Failed to update question")
}

// Update each answer option
for _, a := range req.AnswerOptions {
ua, err := qs.store.UpdateAnswer(ctx, repo.UpdateAnswerParams{
Answer: a.Answer,
IsCorrect: pgtype.Bool{Bool: a.IsCorrect, Valid: true},
AnswerID: global.ConvertStringToGoogleUUID(a.AnswerId),
})
if err != nil {
qs.logger.WithError(err).Errorf("Failed to update answer %s", ua.AnswerID)
return nil, status.Errorf(codes.Internal, "Failed to update answer: %s", ua.AnswerID)
}
}

// Fetch updated data to return
question, err := qs.store.GetQuestionByID(ctx, qUUID)
if err != nil {
return nil, status.Errorf(codes.Internal, "Failed to retrieve updated question")
}

answers, err := qs.store.GetAnswersByQuestionID(ctx, qUUID)
if err != nil {
return nil, status.Errorf(codes.Internal, "Failed to retrieve updated answers")
}

responseAnswers := make([]*qpb.Answer, len(answers))
for _, a := range answers {
responseAnswers = append(responseAnswers, &qpb.Answer{
AnswerId: a.AnswerID.String(),
Answer: a.Answer,
QuestionId: a.QuestionID.String(),
IsCorrect: a.IsCorrect.Bool,
CreatedAt: timestamppb.New(a.CreatedAt.Time),
UpdatedAt: timestamppb.New(a.CreatedAt.Time),
})
}

return &qpb.Question{
QuestionId: question.QuestionID.String(),
QuizId: question.QuizID.String(),
Question: question.Question,
Answers: responseAnswers,
}, nil
}
20 changes: 20 additions & 0 deletions backend/lms-main-system/app/rpc/quiz/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package quiz

import (
db "github.com/multi-tenants-cms-golang/lms-sys/internal/repo"
qpb "github.com/multi-tenants-cms-golang/lms-sys/protogen/quiz"
"github.com/sirupsen/logrus"
)

type QuizService struct {
store db.Store
logger *logrus.Logger
qpb.UnimplementedQuizServiceServer
}

func NewQuizService(store db.Store, logger *logrus.Logger) *QuizService {
return &QuizService{
store: store,
logger: logger,
}
}
Loading