Spaces:
Configuration error
Configuration error
Commit ·
decc167
1
Parent(s): 5a84566
Deploy files from GitHub repository
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- main.go +0 -4
- pkg/storage/local.go +43 -41
- response/api_response_v2.go +2 -1
- services/forgot_password_service.go +1 -1
- space/config/config.go +214 -44
- space/main.go +24 -23
- space/models/exception_model.go +1 -0
- space/repositories/academy_repository.go +16 -17
- space/repositories/quiz_repository.go +91 -71
- space/response/api_response_v2.go +13 -1
- space/services/email_service.go +1 -1
- space/services/forgot_password_service.go +5 -4
- space/services/jwt_service.go +5 -8
- space/services/quiz_service.go +196 -134
- space/space/controller/quiz/quiz_controller.go +24 -0
- space/space/models/request_model.go +12 -1
- space/space/router/quiz_route.go +1 -0
- space/space/services/quiz_service.go +203 -124
- space/space/space/repositories/quiz_repository.go +2 -1
- space/space/space/space/models/field_counter.go +1 -1
- space/space/space/space/pkg/validation/validation.go +2 -2
- space/space/space/space/services/cv_service.go +7 -2
- space/space/space/space/space/controller/quiz/quiz_controller.go +142 -4
- space/space/space/space/space/models/database_orm_model.go +19 -13
- space/space/space/space/space/models/request_model.go +85 -20
- space/space/space/space/space/repositories/quiz_repository.go +275 -33
- space/space/space/space/space/router/quiz_route.go +6 -3
- space/space/space/space/space/services/quiz_service.go +187 -68
- space/space/space/space/space/space/response/api_response_v2.go +9 -0
- space/space/space/space/space/space/space/config/database_connection_config.go +3 -1
- space/space/space/space/space/space/space/controller/quiz/quiz_controller.go +76 -0
- space/space/space/space/space/space/space/main.go +6 -0
- space/space/space/space/space/space/space/models/database_orm_model.go +127 -66
- space/space/space/space/space/space/space/models/exception_model.go +6 -0
- space/space/space/space/space/space/space/models/field_counter.go +40 -0
- space/space/space/space/space/space/space/models/request_model.go +30 -18
- space/space/space/space/space/space/space/models/response_model.go +0 -5
- space/space/space/space/space/space/space/repositories/academy_repository.go +48 -0
- space/space/space/space/space/space/space/repositories/quiz_repository.go +111 -118
- space/space/space/space/space/space/space/router/quiz_route.go +5 -13
- space/space/space/space/space/space/space/router/router.go +1 -1
- space/space/space/space/space/space/space/router/server.go +4 -0
- space/space/space/space/space/space/space/services/cv_service.go +22 -14
- space/space/space/space/space/space/space/services/quiz_service.go +198 -35
- space/space/space/space/space/space/space/space/space/controller/academy/academy_controller.go +11 -5
- space/space/space/space/space/space/space/space/space/models/database_orm_model.go +3 -1
- space/space/space/space/space/space/space/space/space/models/request_model.go +56 -4
- space/space/space/space/space/space/space/space/space/repositories/academy_repository.go +21 -13
- space/space/space/space/space/space/space/space/space/repositories/quiz_repository.go +19 -0
- space/space/space/space/space/space/space/space/space/router/quiz_route.go +1 -1
main.go
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
package main
|
| 2 |
|
| 3 |
import (
|
| 4 |
-
"fmt"
|
| 5 |
"log/slog"
|
| 6 |
"net"
|
| 7 |
"strconv"
|
|
@@ -32,9 +31,6 @@ func main() {
|
|
| 32 |
err := config.LoadConfig()
|
| 33 |
utils.FatalIfErr("failed to load config", err)
|
| 34 |
|
| 35 |
-
// print the all config loaded
|
| 36 |
-
fmt.Printf("config: %+v", config.GlobalConfig)
|
| 37 |
-
|
| 38 |
// setup validator for validation request
|
| 39 |
validator, err := validation.New(config.DB)
|
| 40 |
utils.FatalIfErr("failed to setup validator", err)
|
|
|
|
| 1 |
package main
|
| 2 |
|
| 3 |
import (
|
|
|
|
| 4 |
"log/slog"
|
| 5 |
"net"
|
| 6 |
"strconv"
|
|
|
|
| 31 |
err := config.LoadConfig()
|
| 32 |
utils.FatalIfErr("failed to load config", err)
|
| 33 |
|
|
|
|
|
|
|
|
|
|
| 34 |
// setup validator for validation request
|
| 35 |
validator, err := validation.New(config.DB)
|
| 36 |
utils.FatalIfErr("failed to setup validator", err)
|
pkg/storage/local.go
CHANGED
|
@@ -1,67 +1,69 @@
|
|
| 1 |
package storage
|
| 2 |
|
| 3 |
import (
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
"strings"
|
| 10 |
)
|
| 11 |
|
| 12 |
type LocalStorage struct {
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
}
|
| 17 |
|
| 18 |
func NewLocalStorage(basePath, baseURL string) *LocalStorage {
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
}
|
| 25 |
|
| 26 |
func (s *LocalStorage) Upload(ctx context.Context, file io.ReadSeeker, path string) error {
|
| 27 |
-
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
}
|
| 45 |
|
| 46 |
func (s *LocalStorage) GetURL(path string) string {
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
func (s *LocalStorage) Delete(ctx context.Context, path string) error {
|
| 51 |
-
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
|
| 60 |
-
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
fmt.Println(fullPath)
|
| 65 |
|
| 66 |
-
|
| 67 |
}
|
|
|
|
| 1 |
package storage
|
| 2 |
|
| 3 |
import (
|
| 4 |
+
"context"
|
| 5 |
+
"io"
|
| 6 |
+
"os"
|
| 7 |
+
"path/filepath"
|
| 8 |
+
"strings"
|
|
|
|
| 9 |
)
|
| 10 |
|
| 11 |
type LocalStorage struct {
|
| 12 |
+
*BaseStorage
|
| 13 |
+
basePath string
|
| 14 |
+
baseURL string
|
| 15 |
}
|
| 16 |
|
| 17 |
func NewLocalStorage(basePath, baseURL string) *LocalStorage {
|
| 18 |
+
return &LocalStorage{
|
| 19 |
+
BaseStorage: NewBaseStorage(),
|
| 20 |
+
basePath: basePath,
|
| 21 |
+
baseURL: baseURL,
|
| 22 |
+
}
|
| 23 |
}
|
| 24 |
|
| 25 |
func (s *LocalStorage) Upload(ctx context.Context, file io.ReadSeeker, path string) error {
|
| 26 |
+
fullPath := filepath.Join(s.basePath, path)
|
| 27 |
|
| 28 |
+
dir := filepath.Dir(fullPath)
|
| 29 |
+
if err := os.MkdirAll(dir, 0755); err != nil {
|
| 30 |
+
return err
|
| 31 |
+
}
|
| 32 |
|
| 33 |
+
dst, err := os.Create(fullPath)
|
| 34 |
+
if err != nil {
|
| 35 |
+
return err
|
| 36 |
+
}
|
| 37 |
+
defer dst.Close()
|
| 38 |
|
| 39 |
+
if _, err := io.Copy(dst, file); err != nil {
|
| 40 |
+
return err
|
| 41 |
+
}
|
| 42 |
+
return nil
|
| 43 |
}
|
| 44 |
|
| 45 |
func (s *LocalStorage) GetURL(path string) string {
|
| 46 |
+
urlBuilder := strings.Builder{}
|
| 47 |
+
urlBuilder.Write([]byte(s.baseURL))
|
| 48 |
+
urlBuilder.Write([]byte(path))
|
| 49 |
+
|
| 50 |
+
return urlBuilder.String()
|
| 51 |
}
|
| 52 |
|
| 53 |
func (s *LocalStorage) Delete(ctx context.Context, path string) error {
|
| 54 |
+
// Contoh input: "http://localhost:8080/storage/users/5/profile/filename.png"
|
| 55 |
|
| 56 |
+
// Ambil bagian setelah "/storage/"
|
| 57 |
+
const prefix = "/storage/"
|
| 58 |
+
idx := strings.Index(path, prefix)
|
| 59 |
+
if idx == -1 {
|
| 60 |
+
return os.ErrNotExist
|
| 61 |
+
}
|
| 62 |
|
| 63 |
+
relativePath := path[idx+len(prefix):]
|
| 64 |
|
| 65 |
+
// Gabungkan dengan basePath
|
| 66 |
+
fullPath := filepath.Join(s.basePath, relativePath)
|
|
|
|
| 67 |
|
| 68 |
+
return os.Remove(fullPath)
|
| 69 |
}
|
response/api_response_v2.go
CHANGED
|
@@ -50,7 +50,8 @@ func HandleError(c *gin.Context, err error) {
|
|
| 50 |
responseError(c, http.StatusBadRequest, exception)
|
| 51 |
case exception.AcademyNotFinished:
|
| 52 |
responseError(c, http.StatusBadRequest, exception)
|
| 53 |
-
|
|
|
|
| 54 |
case exception.ValidationError:
|
| 55 |
responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
|
| 56 |
default:
|
|
|
|
| 50 |
responseError(c, http.StatusBadRequest, exception)
|
| 51 |
case exception.AcademyNotFinished:
|
| 52 |
responseError(c, http.StatusBadRequest, exception)
|
| 53 |
+
case exception.QuizAlreadyPassed:
|
| 54 |
+
responseError(c, http.StatusBadRequest, exception)
|
| 55 |
case exception.ValidationError:
|
| 56 |
responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
|
| 57 |
default:
|
services/forgot_password_service.go
CHANGED
|
@@ -41,7 +41,7 @@ func (s *ForgotPasswordService) Create(email string) {
|
|
| 41 |
return
|
| 42 |
}
|
| 43 |
|
| 44 |
-
remainingTime := time.Duration(config.GlobalConfig.App.EmailVerificationDuration) * time.
|
| 45 |
dueTime := CalculateDueTime(remainingTime)
|
| 46 |
|
| 47 |
s.Constructor.UUID = uuid.NewV4()
|
|
|
|
| 41 |
return
|
| 42 |
}
|
| 43 |
|
| 44 |
+
remainingTime := time.Duration(config.GlobalConfig.App.EmailVerificationDuration) * time.Minute
|
| 45 |
dueTime := CalculateDueTime(remainingTime)
|
| 46 |
|
| 47 |
s.Constructor.UUID = uuid.NewV4()
|
space/config/config.go
CHANGED
|
@@ -1,66 +1,236 @@
|
|
| 1 |
package config
|
| 2 |
|
| 3 |
import (
|
|
|
|
|
|
|
| 4 |
"os"
|
| 5 |
"strconv"
|
| 6 |
"time"
|
| 7 |
|
|
|
|
| 8 |
"github.com/joho/godotenv"
|
|
|
|
|
|
|
| 9 |
)
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
var EMAIL_VERIFICATION_DURATION int
|
| 18 |
-
|
| 19 |
-
var SMTP_SENDER_EMAIL string
|
| 20 |
-
var SMTP_SENDER_PASSWORD string
|
| 21 |
-
var SMTP_HOST string
|
| 22 |
-
var SMTP_PORT string
|
| 23 |
-
|
| 24 |
-
var REDIS_HOST string
|
| 25 |
-
var REDIS_PORT int
|
| 26 |
-
var REDIS_PASSWORD string
|
| 27 |
-
var REDIS_DB int
|
| 28 |
-
var REDIS_MIN_IDLE_CONNS int
|
| 29 |
-
var REDIS_POOL_SIZE int
|
| 30 |
-
var REDIS_POOL_TIMEOUT time.Duration
|
| 31 |
-
|
| 32 |
-
var APP_URL string
|
| 33 |
-
|
| 34 |
-
func init() {
|
| 35 |
-
godotenv.Load()
|
| 36 |
-
ENV = os.Getenv("ENV")
|
| 37 |
-
HOST_ADDRESS = os.Getenv("HOST_ADDRESS")
|
| 38 |
-
HOST_PORT = os.Getenv("HOST_PORT")
|
| 39 |
-
TCP_ADDRESS = HOST_ADDRESS + ":" + HOST_PORT
|
| 40 |
-
LOG_PATH = os.Getenv("LOG_PATH")
|
| 41 |
-
EMAIL_VERIFICATION_DURATION, _ = strconv.Atoi(os.Getenv("EMAIL_VERIFICATION_DURATION"))
|
| 42 |
-
SMTP_SENDER_EMAIL = os.Getenv("SMTP_SENDER_EMAIL")
|
| 43 |
-
SMTP_SENDER_PASSWORD = os.Getenv("SMTP_SENDER_PASSWORD")
|
| 44 |
-
SMTP_HOST = os.Getenv("SMTP_HOST")
|
| 45 |
-
SMTP_PORT = os.Getenv("SMTP_PORT")
|
| 46 |
-
|
| 47 |
-
REDIS_HOST = getValue(os.Getenv("REDIS_HOST"), "redis", func(s string) (string, error) { return s, nil })
|
| 48 |
-
REDIS_PORT = getValue(os.Getenv("REDIS_PORT"), 6379, func(s string) (int, error) { return strconv.Atoi(s) })
|
| 49 |
-
REDIS_PASSWORD = getValue(os.Getenv("REDIS_PASSWORD"), "qobiltu", func(s string) (string, error) { return s, nil })
|
| 50 |
-
REDIS_DB = getValue(os.Getenv("REDIS_DB"), 0, func(s string) (int, error) { return strconv.Atoi(s) })
|
| 51 |
-
REDIS_POOL_SIZE = getValue(os.Getenv("REDIS_POOL_SIZE"), 10, func(s string) (int, error) { return strconv.Atoi(s) })
|
| 52 |
-
REDIS_MIN_IDLE_CONNS = getValue(os.Getenv("REDIS_MIN_IDLE_CONNS"), 10, func(s string) (int, error) { return strconv.Atoi(s) })
|
| 53 |
-
REDIS_POOL_TIMEOUT = getValue(os.Getenv("REDIS_POOL_TIMEOUT"), time.Second*30, func(s string) (time.Duration, error) { return time.ParseDuration(s) })
|
| 54 |
-
APP_URL = getValue(os.Getenv("APP_URL"), "http://localhost:3000", func(s string) (string, error) { return s, nil })
|
| 55 |
}
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
func getValue[T any](value string, defaultValue T, convert func(string) (T, error)) T {
|
| 58 |
if value == "" {
|
| 59 |
return defaultValue
|
| 60 |
}
|
| 61 |
convertedValue, err := convert(value)
|
| 62 |
if err != nil {
|
|
|
|
| 63 |
return defaultValue
|
| 64 |
}
|
| 65 |
return convertedValue
|
| 66 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
package config
|
| 2 |
|
| 3 |
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"log"
|
| 6 |
"os"
|
| 7 |
"strconv"
|
| 8 |
"time"
|
| 9 |
|
| 10 |
+
"api.qobiltu.id/models"
|
| 11 |
"github.com/joho/godotenv"
|
| 12 |
+
"gorm.io/driver/postgres"
|
| 13 |
+
"gorm.io/gorm"
|
| 14 |
)
|
| 15 |
|
| 16 |
+
// Config holds all configuration settings for the application.
|
| 17 |
+
type Config struct {
|
| 18 |
+
Database DatabaseConfig
|
| 19 |
+
SMTP SMTPConfig
|
| 20 |
+
Redis RedisConfig
|
| 21 |
+
App AppConfig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
|
| 24 |
+
// DatabaseConfig holds database connection settings.
|
| 25 |
+
type DatabaseConfig struct {
|
| 26 |
+
Host string
|
| 27 |
+
Port string
|
| 28 |
+
User string
|
| 29 |
+
Password string
|
| 30 |
+
Name string
|
| 31 |
+
DB *gorm.DB
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// SMTPConfig holds SMTP settings for email services.
|
| 35 |
+
type SMTPConfig struct {
|
| 36 |
+
SenderEmail string
|
| 37 |
+
Password string
|
| 38 |
+
Host string
|
| 39 |
+
Port string
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// RedisConfig holds Redis connection settings.
|
| 43 |
+
type RedisConfig struct {
|
| 44 |
+
Host string
|
| 45 |
+
Port int
|
| 46 |
+
Password string
|
| 47 |
+
DB int
|
| 48 |
+
MinIdleConns int
|
| 49 |
+
PoolSize int
|
| 50 |
+
PoolTimeout time.Duration
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// AppConfig holds general application settings.
|
| 54 |
+
type AppConfig struct {
|
| 55 |
+
Env string
|
| 56 |
+
HostAddress string
|
| 57 |
+
HostPort string
|
| 58 |
+
TCPAddress string
|
| 59 |
+
LogPath string
|
| 60 |
+
EmailVerificationDuration int
|
| 61 |
+
AppURL string
|
| 62 |
+
Salt string
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// GlobalConfig is the global configuration instance.
|
| 66 |
+
var GlobalConfig *Config
|
| 67 |
+
var DB *gorm.DB // Global DB instance for compatibility
|
| 68 |
+
|
| 69 |
+
// LoadConfig initializes the application configuration.
|
| 70 |
+
func LoadConfig() error {
|
| 71 |
+
// Load environment variables from .env file
|
| 72 |
+
if err := godotenv.Load(); err != nil {
|
| 73 |
+
log.Printf("Failed to load .env file: %v", err)
|
| 74 |
+
// Continue with defaults if .env file is not found
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Set timezone
|
| 78 |
+
if err := os.Setenv("TZ", "Asia/Jakarta"); err != nil {
|
| 79 |
+
return fmt.Errorf("failed to set timezone: %w", err)
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
cfg := &Config{
|
| 83 |
+
Database: DatabaseConfig{
|
| 84 |
+
Host: getValue(os.Getenv("DB_HOST"), "localhost", stringConverter),
|
| 85 |
+
Port: getValue(os.Getenv("DB_PORT"), "5432", stringConverter),
|
| 86 |
+
User: getValue(os.Getenv("DB_USER"), "postgres", stringConverter),
|
| 87 |
+
Password: getValue(os.Getenv("DB_PASSWORD"), "", stringConverter),
|
| 88 |
+
Name: getValue(os.Getenv("DB_NAME"), "qobiltu", stringConverter),
|
| 89 |
+
},
|
| 90 |
+
SMTP: SMTPConfig{
|
| 91 |
+
SenderEmail: getValue(os.Getenv("SMTP_SENDER_EMAIL"), "", stringConverter),
|
| 92 |
+
Password: getValue(os.Getenv("SMTP_SENDER_PASSWORD"), "", stringConverter),
|
| 93 |
+
Host: getValue(os.Getenv("SMTP_HOST"), "", stringConverter),
|
| 94 |
+
Port: getValue(os.Getenv("SMTP_PORT"), "", stringConverter),
|
| 95 |
+
},
|
| 96 |
+
Redis: RedisConfig{
|
| 97 |
+
Host: getValue(os.Getenv("REDIS_HOST"), "redis", stringConverter),
|
| 98 |
+
Port: getValue(os.Getenv("REDIS_PORT"), 6379, intConverter),
|
| 99 |
+
Password: getValue(os.Getenv("REDIS_PASSWORD"), "qobiltu", stringConverter),
|
| 100 |
+
DB: getValue(os.Getenv("REDIS_DB"), 0, intConverter),
|
| 101 |
+
MinIdleConns: getValue(os.Getenv("REDIS_MIN_IDLE_CONNS"), 10, intConverter),
|
| 102 |
+
PoolSize: getValue(os.Getenv("REDIS_POOL_SIZE"), 10, intConverter),
|
| 103 |
+
PoolTimeout: getValue(os.Getenv("REDIS_POOL_TIMEOUT"), 30*time.Second, durationConverter),
|
| 104 |
+
},
|
| 105 |
+
App: AppConfig{
|
| 106 |
+
Env: getValue(os.Getenv("ENV"), "development", stringConverter),
|
| 107 |
+
HostAddress: getValue(os.Getenv("HOST_ADDRESS"), "localhost", stringConverter),
|
| 108 |
+
HostPort: getValue(os.Getenv("HOST_PORT"), "8080", stringConverter),
|
| 109 |
+
LogPath: getValue(os.Getenv("LOG_PATH"), "./logs", stringConverter),
|
| 110 |
+
EmailVerificationDuration: getValue(os.Getenv("EMAIL_VERIFICATION_DURATION"), 24, intConverter),
|
| 111 |
+
AppURL: getValue(os.Getenv("APP_URL"), "http://localhost:3000", stringConverter),
|
| 112 |
+
Salt: getValue(os.Getenv("SALT"), "D3f4u|t", stringConverter),
|
| 113 |
+
},
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// Set TCP address
|
| 117 |
+
cfg.App.TCPAddress = fmt.Sprintf("%s:%s", cfg.App.HostAddress, cfg.App.HostPort)
|
| 118 |
+
|
| 119 |
+
// Initialize database connection
|
| 120 |
+
dsn := fmt.Sprintf(
|
| 121 |
+
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Jakarta",
|
| 122 |
+
cfg.Database.Host, cfg.Database.User, cfg.Database.Password, cfg.Database.Name, cfg.Database.Port,
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
| 126 |
+
TranslateError: true,
|
| 127 |
+
// Logger: logger.Default.LogMode(logger.Info),
|
| 128 |
+
})
|
| 129 |
+
if err != nil {
|
| 130 |
+
return fmt.Errorf("failed to connect to database: %w", err)
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
// Configure database connection pooling
|
| 134 |
+
sqlDB, err := db.DB()
|
| 135 |
+
if err != nil {
|
| 136 |
+
return fmt.Errorf("failed to get sql.DB: %w", err)
|
| 137 |
+
}
|
| 138 |
+
sqlDB.SetMaxIdleConns(10)
|
| 139 |
+
sqlDB.SetMaxOpenConns(100)
|
| 140 |
+
sqlDB.SetConnMaxLifetime(time.Hour)
|
| 141 |
+
sqlDB.SetConnMaxIdleTime(30 * time.Minute)
|
| 142 |
+
|
| 143 |
+
// Assign database connection
|
| 144 |
+
cfg.Database.DB = db
|
| 145 |
+
DB = db
|
| 146 |
+
|
| 147 |
+
// Perform auto-migration
|
| 148 |
+
if err := AutoMigrateAll(db); err != nil {
|
| 149 |
+
return fmt.Errorf("auto-migration failed: %w", err)
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
// Assign to global config
|
| 153 |
+
GlobalConfig = cfg
|
| 154 |
+
|
| 155 |
+
return nil
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// AutoMigrateAll performs auto-migration for all models and creates necessary sequences.
|
| 159 |
+
func AutoMigrateAll(db *gorm.DB) error {
|
| 160 |
+
// Auto-migrate all models
|
| 161 |
+
err := db.AutoMigrate(
|
| 162 |
+
&models.Account{},
|
| 163 |
+
&models.AccountDetails{},
|
| 164 |
+
&models.EmailVerification{},
|
| 165 |
+
&models.ExternalAuth{},
|
| 166 |
+
&models.FCM{},
|
| 167 |
+
&models.ForgotPassword{},
|
| 168 |
+
&models.Academy{},
|
| 169 |
+
&models.AcademyMaterial{},
|
| 170 |
+
&models.AcademyMaterialProgress{},
|
| 171 |
+
&models.RegionCity{},
|
| 172 |
+
&models.RegionProvince{},
|
| 173 |
+
&models.OptionCategory{},
|
| 174 |
+
&models.OptionValues{},
|
| 175 |
+
&models.Quiz{},
|
| 176 |
+
&models.Question{},
|
| 177 |
+
&models.Answer{},
|
| 178 |
+
&models.QuizAttempt{},
|
| 179 |
+
&models.UserAnswer{},
|
| 180 |
+
&models.PersonalityAndPreferenceCV{},
|
| 181 |
+
&models.FamilyMemberCV{},
|
| 182 |
+
&models.PhysicalAndHealthCV{},
|
| 183 |
+
&models.WorshipAndReligiousUnderstandingCV{},
|
| 184 |
+
&models.EducationCV{},
|
| 185 |
+
&models.JobCV{},
|
| 186 |
+
&models.AchievementCV{},
|
| 187 |
+
&models.MarriageReadinessProfile{},
|
| 188 |
+
&models.PartnerCriteria{},
|
| 189 |
+
)
|
| 190 |
+
if err != nil {
|
| 191 |
+
return fmt.Errorf("failed to auto-migrate models: %w", err)
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
// Create sequences
|
| 195 |
+
sequences := []string{
|
| 196 |
+
`CREATE SEQUENCE IF NOT EXISTS seq_ikh_counter START 1 INCREMENT 1 MINVALUE 1;`,
|
| 197 |
+
`CREATE SEQUENCE IF NOT EXISTS seq_akh_counter START 1 INCREMENT 1 MINVALUE 1;`,
|
| 198 |
+
}
|
| 199 |
+
for _, seq := range sequences {
|
| 200 |
+
if err := db.Exec(seq).Error; err != nil {
|
| 201 |
+
log.Printf("Failed to create sequence: %v", err)
|
| 202 |
+
return fmt.Errorf("failed to create sequence: %w", err)
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
log.Println("Auto-migration and sequence creation completed successfully")
|
| 207 |
+
return nil
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// getValue retrieves an environment variable with a default value and type conversion.
|
| 211 |
func getValue[T any](value string, defaultValue T, convert func(string) (T, error)) T {
|
| 212 |
if value == "" {
|
| 213 |
return defaultValue
|
| 214 |
}
|
| 215 |
convertedValue, err := convert(value)
|
| 216 |
if err != nil {
|
| 217 |
+
log.Printf("Failed to convert %v to type %T: %v, using default: %v", value, defaultValue, err, defaultValue)
|
| 218 |
return defaultValue
|
| 219 |
}
|
| 220 |
return convertedValue
|
| 221 |
}
|
| 222 |
+
|
| 223 |
+
// stringConverter converts a string to a string (identity function).
|
| 224 |
+
func stringConverter(s string) (string, error) {
|
| 225 |
+
return s, nil
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
// intConverter converts a string to an int.
|
| 229 |
+
func intConverter(s string) (int, error) {
|
| 230 |
+
return strconv.Atoi(s)
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
// durationConverter converts a string to a time.Duration.
|
| 234 |
+
func durationConverter(s string) (time.Duration, error) {
|
| 235 |
+
return time.ParseDuration(s)
|
| 236 |
+
}
|
space/main.go
CHANGED
|
@@ -27,31 +27,33 @@ import (
|
|
| 27 |
|
| 28 |
func main() {
|
| 29 |
|
| 30 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
validator, err := validation.New(config.DB)
|
| 32 |
utils.FatalIfErr("failed to setup validator", err)
|
| 33 |
|
| 34 |
-
// setup storage
|
| 35 |
-
localStorage := storage.NewLocalStorage("uploads", config.
|
| 36 |
-
|
| 37 |
-
// setup
|
| 38 |
-
|
| 39 |
-
Host: config.
|
| 40 |
-
Port: config.
|
| 41 |
-
From: config.
|
| 42 |
-
Username: config.
|
| 43 |
-
Password: config.
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
emailSender, err := mail.New(&emailConfig)
|
| 47 |
utils.FatalIfErr("failed to setup email sender", err)
|
| 48 |
mail.EmailSender = emailSender
|
| 49 |
|
| 50 |
-
// setup
|
| 51 |
asynqRedisOpt := asynq.RedisClientOpt{
|
| 52 |
-
Addr: net.JoinHostPort(config.
|
| 53 |
-
Password: config.
|
| 54 |
-
DB: config.
|
| 55 |
}
|
| 56 |
|
| 57 |
taskDistributor := worker.NewRedisTaskDistributor(asynqRedisOpt)
|
|
@@ -59,7 +61,6 @@ func main() {
|
|
| 59 |
worker.AsyncTaskDistributor = taskDistributor
|
| 60 |
|
| 61 |
// setup repo, service, and controller
|
| 62 |
-
|
| 63 |
regionRepository := repositories.NewRegionRepository(config.DB)
|
| 64 |
regionService := services.NewRegionService(regionRepository)
|
| 65 |
regionController := region_controller.NewRegionController(regionService)
|
|
@@ -76,8 +77,8 @@ func main() {
|
|
| 76 |
academyService := services.NewAcademyService(academyRepository, validator)
|
| 77 |
academyController := academy_controller.NewAcademyController(academyService)
|
| 78 |
|
| 79 |
-
quizRepository := repositories.NewQuizRepository(
|
| 80 |
-
quizService := services.NewQuizService(quizRepository, academyRepository, validator)
|
| 81 |
quizController := quiz_controller.NewQuizController(quizService)
|
| 82 |
|
| 83 |
cvRepository := repositories.NewCVRepository(config.DB)
|
|
@@ -111,7 +112,7 @@ func main() {
|
|
| 111 |
utils.FatalIfErr("failed to create server", err)
|
| 112 |
|
| 113 |
// run server
|
| 114 |
-
slog.Info("Starting server", "address", config.
|
| 115 |
-
err = s.Start(config.
|
| 116 |
utils.FatalIfErr("failed to start server", err)
|
| 117 |
}
|
|
|
|
| 27 |
|
| 28 |
func main() {
|
| 29 |
|
| 30 |
+
// load config
|
| 31 |
+
err := config.LoadConfig()
|
| 32 |
+
utils.FatalIfErr("failed to load config", err)
|
| 33 |
+
|
| 34 |
+
// setup validator for validation request
|
| 35 |
validator, err := validation.New(config.DB)
|
| 36 |
utils.FatalIfErr("failed to setup validator", err)
|
| 37 |
|
| 38 |
+
// setup storage for saving files
|
| 39 |
+
localStorage := storage.NewLocalStorage("uploads", config.GlobalConfig.App.AppURL+"/storage/")
|
| 40 |
+
|
| 41 |
+
// setup smtp for sending email
|
| 42 |
+
emailSender, err := mail.New(&mail.Config{
|
| 43 |
+
Host: config.GlobalConfig.SMTP.Host,
|
| 44 |
+
Port: config.GlobalConfig.SMTP.Port,
|
| 45 |
+
From: config.GlobalConfig.SMTP.SenderEmail,
|
| 46 |
+
Username: config.GlobalConfig.SMTP.SenderEmail,
|
| 47 |
+
Password: config.GlobalConfig.SMTP.Password,
|
| 48 |
+
})
|
|
|
|
|
|
|
| 49 |
utils.FatalIfErr("failed to setup email sender", err)
|
| 50 |
mail.EmailSender = emailSender
|
| 51 |
|
| 52 |
+
// setup task distributor and processor for async task
|
| 53 |
asynqRedisOpt := asynq.RedisClientOpt{
|
| 54 |
+
Addr: net.JoinHostPort(config.GlobalConfig.Redis.Host, strconv.Itoa(config.GlobalConfig.Redis.Port)),
|
| 55 |
+
Password: config.GlobalConfig.Redis.Password,
|
| 56 |
+
DB: config.GlobalConfig.Redis.DB,
|
| 57 |
}
|
| 58 |
|
| 59 |
taskDistributor := worker.NewRedisTaskDistributor(asynqRedisOpt)
|
|
|
|
| 61 |
worker.AsyncTaskDistributor = taskDistributor
|
| 62 |
|
| 63 |
// setup repo, service, and controller
|
|
|
|
| 64 |
regionRepository := repositories.NewRegionRepository(config.DB)
|
| 65 |
regionService := services.NewRegionService(regionRepository)
|
| 66 |
regionController := region_controller.NewRegionController(regionService)
|
|
|
|
| 77 |
academyService := services.NewAcademyService(academyRepository, validator)
|
| 78 |
academyController := academy_controller.NewAcademyController(academyService)
|
| 79 |
|
| 80 |
+
quizRepository := repositories.NewQuizRepository()
|
| 81 |
+
quizService := services.NewQuizService(config.DB, quizRepository, academyRepository, validator)
|
| 82 |
quizController := quiz_controller.NewQuizController(quizService)
|
| 83 |
|
| 84 |
cvRepository := repositories.NewCVRepository(config.DB)
|
|
|
|
| 112 |
utils.FatalIfErr("failed to create server", err)
|
| 113 |
|
| 114 |
// run server
|
| 115 |
+
slog.Info("Starting server", "address", config.GlobalConfig.App.HostAddress, "port", config.GlobalConfig.App.HostPort)
|
| 116 |
+
err = s.Start(config.GlobalConfig.App.TCPAddress)
|
| 117 |
utils.FatalIfErr("failed to start server", err)
|
| 118 |
}
|
space/models/exception_model.go
CHANGED
|
@@ -20,6 +20,7 @@ type Exception struct {
|
|
| 20 |
|
| 21 |
// quiz context
|
| 22 |
QuizTimeExpired bool `json:"quiz_time_expired,omitempty"`
|
|
|
|
| 23 |
QuizAttemptLimit bool `json:"quiz_attempt_limit,omitempty"`
|
| 24 |
QuizAlreadyFinished bool `json:"quiz_already_finished,omitempty"`
|
| 25 |
AcademyNotFinished bool `json:"academy_not_finished,omitempty"`
|
|
|
|
| 20 |
|
| 21 |
// quiz context
|
| 22 |
QuizTimeExpired bool `json:"quiz_time_expired,omitempty"`
|
| 23 |
+
QuizAlreadyPassed bool `json:"quiz_already_passed,omitempty"`
|
| 24 |
QuizAttemptLimit bool `json:"quiz_attempt_limit,omitempty"`
|
| 25 |
QuizAlreadyFinished bool `json:"quiz_already_finished,omitempty"`
|
| 26 |
AcademyNotFinished bool `json:"academy_not_finished,omitempty"`
|
space/repositories/academy_repository.go
CHANGED
|
@@ -26,7 +26,7 @@ type AcademyRepository interface {
|
|
| 26 |
|
| 27 |
// === USER ===
|
| 28 |
UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
|
| 29 |
-
UserGetPercentageProgressAcademyByID(ctx context.Context,
|
| 30 |
UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
|
| 31 |
UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
|
| 32 |
UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error)
|
|
@@ -288,34 +288,33 @@ func (r *academyRepository) UserListAcademy(ctx context.Context, req *models.Lis
|
|
| 288 |
return academies, pageInfo, nil
|
| 289 |
}
|
| 290 |
|
| 291 |
-
func (r *academyRepository) UserGetPercentageProgressAcademyByID(ctx context.Context,
|
| 292 |
-
var
|
| 293 |
-
var totalReadMaterial int64
|
| 294 |
|
| 295 |
-
//
|
| 296 |
-
err :=
|
| 297 |
Model(&models.AcademyMaterial{}).
|
| 298 |
Where("academy_id = ?", academyID).
|
| 299 |
-
Count(&
|
| 300 |
if err != nil {
|
| 301 |
return 0, err
|
| 302 |
}
|
| 303 |
|
| 304 |
-
|
| 305 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
Model(&models.AcademyMaterialProgress{}).
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
Count(&totalReadMaterial).Error
|
| 310 |
if err != nil {
|
| 311 |
return 0, err
|
| 312 |
}
|
| 313 |
|
| 314 |
-
//
|
| 315 |
-
|
| 316 |
-
return 0, nil
|
| 317 |
-
}
|
| 318 |
-
percentage := float64(totalReadMaterial) / float64(totalMaterial) * 100
|
| 319 |
return percentage, nil
|
| 320 |
}
|
| 321 |
|
|
|
|
| 26 |
|
| 27 |
// === USER ===
|
| 28 |
UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
|
| 29 |
+
UserGetPercentageProgressAcademyByID(ctx context.Context, db *gorm.DB, accountID, academyID int64) (float64, error)
|
| 30 |
UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
|
| 31 |
UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
|
| 32 |
UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error)
|
|
|
|
| 288 |
return academies, pageInfo, nil
|
| 289 |
}
|
| 290 |
|
| 291 |
+
func (r *academyRepository) UserGetPercentageProgressAcademyByID(ctx context.Context, db *gorm.DB, accountID, academyID int64) (float64, error) {
|
| 292 |
+
var totalMaterials, completedMaterials int64
|
|
|
|
| 293 |
|
| 294 |
+
// Count total materials for the academy
|
| 295 |
+
err := db.WithContext(ctx).
|
| 296 |
Model(&models.AcademyMaterial{}).
|
| 297 |
Where("academy_id = ?", academyID).
|
| 298 |
+
Count(&totalMaterials).Error
|
| 299 |
if err != nil {
|
| 300 |
return 0, err
|
| 301 |
}
|
| 302 |
|
| 303 |
+
if totalMaterials == 0 {
|
| 304 |
+
return 100.0, nil // If no materials, consider progress as 100%
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
// Count completed materials for the account
|
| 308 |
+
err = db.WithContext(ctx).
|
| 309 |
Model(&models.AcademyMaterialProgress{}).
|
| 310 |
+
Where("account_id = ? AND academy_material_id IN (SELECT id FROM academy_materials WHERE academy_id = ?)", accountID, academyID).
|
| 311 |
+
Count(&completedMaterials).Error
|
|
|
|
| 312 |
if err != nil {
|
| 313 |
return 0, err
|
| 314 |
}
|
| 315 |
|
| 316 |
+
// Calculate percentage
|
| 317 |
+
percentage := (float64(completedMaterials) / float64(totalMaterials)) * 100
|
|
|
|
|
|
|
|
|
|
| 318 |
return percentage, nil
|
| 319 |
}
|
| 320 |
|
space/repositories/quiz_repository.go
CHANGED
|
@@ -10,38 +10,37 @@ import (
|
|
| 10 |
)
|
| 11 |
|
| 12 |
type QuizRepository interface {
|
| 13 |
-
UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
-
type quizRepository struct
|
| 37 |
-
db *gorm.DB
|
| 38 |
-
}
|
| 39 |
|
| 40 |
-
func NewQuizRepository(
|
| 41 |
-
return &quizRepository{
|
| 42 |
}
|
| 43 |
|
| 44 |
-
func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
|
| 45 |
var quizResponse models.UserGetQuizResponse
|
| 46 |
|
| 47 |
rawQuery := `
|
|
@@ -73,7 +72,7 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
|
|
| 73 |
q.attempt_limit, q.time_limit, q.min_score, q.created_at, q.updated_at;
|
| 74 |
`
|
| 75 |
|
| 76 |
-
err :=
|
| 77 |
"accountID": req.AccountID,
|
| 78 |
"academyID": req.AcademyID,
|
| 79 |
}).Scan(&quizResponse).Error
|
|
@@ -88,10 +87,24 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
|
|
| 88 |
return &quizResponse, nil
|
| 89 |
}
|
| 90 |
|
| 91 |
-
func (r *quizRepository)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
var quizAttempt models.QuizAttempt
|
| 93 |
|
| 94 |
-
err :=
|
| 95 |
if err != nil {
|
| 96 |
return nil, err
|
| 97 |
}
|
|
@@ -99,10 +112,10 @@ func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID
|
|
| 99 |
return &quizAttempt, nil
|
| 100 |
}
|
| 101 |
|
| 102 |
-
func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error) {
|
| 103 |
var totalAttempts int64
|
| 104 |
|
| 105 |
-
err :=
|
| 106 |
if err != nil {
|
| 107 |
return 0, err
|
| 108 |
}
|
|
@@ -110,11 +123,11 @@ func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, accountID
|
|
| 110 |
return totalAttempts, nil
|
| 111 |
}
|
| 112 |
|
| 113 |
-
func (r *quizRepository) UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
|
| 114 |
-
return
|
| 115 |
}
|
| 116 |
|
| 117 |
-
func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error) {
|
| 118 |
questions := make([]models.UserAttemptQuizQuestionsResponse, 0)
|
| 119 |
|
| 120 |
rawQuery := `
|
|
@@ -144,7 +157,7 @@ func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context
|
|
| 144 |
q.id;
|
| 145 |
`
|
| 146 |
|
| 147 |
-
err :=
|
| 148 |
if err != nil {
|
| 149 |
return nil, err
|
| 150 |
}
|
|
@@ -152,10 +165,10 @@ func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context
|
|
| 152 |
return questions, nil
|
| 153 |
}
|
| 154 |
|
| 155 |
-
func (r *quizRepository) UserGetAttemptByID(ctx context.Context, attemptID int64) (*models.QuizAttempt, error) {
|
| 156 |
var quizAttempt models.QuizAttempt
|
| 157 |
|
| 158 |
-
err :=
|
| 159 |
Preload("Quiz").
|
| 160 |
First(&quizAttempt).Error
|
| 161 |
if err != nil {
|
|
@@ -165,14 +178,14 @@ func (r *quizRepository) UserGetAttemptByID(ctx context.Context, attemptID int64
|
|
| 165 |
return &quizAttempt, nil
|
| 166 |
}
|
| 167 |
|
| 168 |
-
func (r *quizRepository) UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
|
| 169 |
-
return
|
| 170 |
}
|
| 171 |
|
| 172 |
-
func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error) {
|
| 173 |
var totalQuestion int64
|
| 174 |
|
| 175 |
-
err :=
|
| 176 |
if err != nil {
|
| 177 |
return 0, err
|
| 178 |
}
|
|
@@ -180,10 +193,10 @@ func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, quizID in
|
|
| 180 |
return totalQuestion, nil
|
| 181 |
}
|
| 182 |
|
| 183 |
-
func (r *quizRepository) UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error) {
|
| 184 |
var totalCorrectAnswer int64
|
| 185 |
|
| 186 |
-
err :=
|
| 187 |
if err != nil {
|
| 188 |
return 0, err
|
| 189 |
}
|
|
@@ -191,11 +204,11 @@ func (r *quizRepository) UserGetTotalCorrectAnswerQuiz(ctx context.Context, quiz
|
|
| 191 |
return totalCorrectAnswer, nil
|
| 192 |
}
|
| 193 |
|
| 194 |
-
func (r *quizRepository) UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error {
|
| 195 |
-
return
|
| 196 |
}
|
| 197 |
|
| 198 |
-
func (r *quizRepository) UserGetRemainingAttempts(ctx context.Context, accountID int64, quizID int64) (int64, error) {
|
| 199 |
var remainingAttempts int64
|
| 200 |
|
| 201 |
rawQuery := `
|
|
@@ -214,7 +227,7 @@ func (r *quizRepository) UserGetRemainingAttempts(ctx context.Context, accountID
|
|
| 214 |
q.attempt_limit;
|
| 215 |
`
|
| 216 |
|
| 217 |
-
err :=
|
| 218 |
if err != nil {
|
| 219 |
return 0, err
|
| 220 |
}
|
|
@@ -222,7 +235,7 @@ func (r *quizRepository) UserGetRemainingAttempts(ctx context.Context, accountID
|
|
| 222 |
return remainingAttempts, nil
|
| 223 |
}
|
| 224 |
|
| 225 |
-
func (r *quizRepository) UserGetQuestionQuiz(ctx context.Context, attemptID, questionID int64) (*models.GetQuestionQuizResponse, error) {
|
| 226 |
var resultJSON string
|
| 227 |
var response models.GetQuestionQuizResponse
|
| 228 |
|
|
@@ -251,14 +264,14 @@ func (r *quizRepository) UserGetQuestionQuiz(ctx context.Context, attemptID, que
|
|
| 251 |
LEFT JOIN
|
| 252 |
user_answers ua
|
| 253 |
ON q.id = ua.question_id
|
| 254 |
-
AND ua.quiz_attempt_id =
|
| 255 |
WHERE
|
| 256 |
-
q.id =
|
| 257 |
GROUP BY
|
| 258 |
q.id, q.quiz_id, q.content, ua.selected_answer_id, ua.is_doubt;
|
| 259 |
`
|
| 260 |
|
| 261 |
-
err :=
|
| 262 |
if err != nil {
|
| 263 |
return nil, err
|
| 264 |
}
|
|
@@ -271,10 +284,10 @@ func (r *quizRepository) UserGetQuestionQuiz(ctx context.Context, attemptID, que
|
|
| 271 |
return &response, nil
|
| 272 |
}
|
| 273 |
|
| 274 |
-
func (r *quizRepository) UserGetQuestionByID(ctx context.Context, questionID int64) (*models.Question, error) {
|
| 275 |
var question models.Question
|
| 276 |
|
| 277 |
-
err :=
|
| 278 |
if err != nil {
|
| 279 |
return nil, err
|
| 280 |
}
|
|
@@ -282,10 +295,10 @@ func (r *quizRepository) UserGetQuestionByID(ctx context.Context, questionID int
|
|
| 282 |
return &question, nil
|
| 283 |
}
|
| 284 |
|
| 285 |
-
func (r *quizRepository) UserGetUserAnswer(ctx context.Context, attemptID, questionID int64) (*models.UserAnswer, error) {
|
| 286 |
var userAnswer models.UserAnswer
|
| 287 |
|
| 288 |
-
err :=
|
| 289 |
if err != nil {
|
| 290 |
return nil, err
|
| 291 |
}
|
|
@@ -293,11 +306,11 @@ func (r *quizRepository) UserGetUserAnswer(ctx context.Context, attemptID, quest
|
|
| 293 |
return &userAnswer, nil
|
| 294 |
}
|
| 295 |
|
| 296 |
-
func (r *quizRepository) UserSaveUserAnswer(ctx context.Context, userAnswer *models.UserAnswer) error {
|
| 297 |
-
return
|
| 298 |
}
|
| 299 |
|
| 300 |
-
func (r *quizRepository) UserGetReviewQuiz(ctx context.Context, attemptID, accountID, quizID int64) (*models.ReviewQuizResponse, error) {
|
| 301 |
var resultJSON string
|
| 302 |
var reviewQuizResponse models.ReviewQuizResponse
|
| 303 |
|
|
@@ -326,7 +339,7 @@ func (r *quizRepository) UserGetReviewQuiz(ctx context.Context, attemptID, accou
|
|
| 326 |
FROM answers a
|
| 327 |
WHERE a.question_id = q.id
|
| 328 |
),
|
| 329 |
-
|
| 330 |
) ORDER BY q.order
|
| 331 |
),
|
| 332 |
'[]'::json
|
|
@@ -341,7 +354,7 @@ func (r *quizRepository) UserGetReviewQuiz(ctx context.Context, attemptID, accou
|
|
| 341 |
q.quiz_id = ?
|
| 342 |
`
|
| 343 |
|
| 344 |
-
err :=
|
| 345 |
if err != nil {
|
| 346 |
return nil, fmt.Errorf("failed to query reviews: %w", err)
|
| 347 |
}
|
|
@@ -356,10 +369,10 @@ func (r *quizRepository) UserGetReviewQuiz(ctx context.Context, attemptID, accou
|
|
| 356 |
return &reviewQuizResponse, nil
|
| 357 |
}
|
| 358 |
|
| 359 |
-
func (r *quizRepository) UseGetLastAttemptQuiz(ctx context.Context, accountID int64, academyID int64) (*models.QuizAttempt, error) {
|
| 360 |
var attempt models.QuizAttempt
|
| 361 |
|
| 362 |
-
err :=
|
| 363 |
if err != nil {
|
| 364 |
return nil, err
|
| 365 |
}
|
|
@@ -367,7 +380,7 @@ func (r *quizRepository) UseGetLastAttemptQuiz(ctx context.Context, accountID in
|
|
| 367 |
return &attempt, nil
|
| 368 |
}
|
| 369 |
|
| 370 |
-
func (r *quizRepository) GetCorrectOptionID(ctx context.Context, questionID int64) (int64, error) {
|
| 371 |
var correctOptionID int64
|
| 372 |
|
| 373 |
rawQuery := `
|
|
@@ -378,7 +391,7 @@ func (r *quizRepository) GetCorrectOptionID(ctx context.Context, questionID int6
|
|
| 378 |
LIMIT 1;
|
| 379 |
`
|
| 380 |
|
| 381 |
-
err :=
|
| 382 |
if err != nil {
|
| 383 |
return 0, err
|
| 384 |
}
|
|
@@ -386,13 +399,20 @@ func (r *quizRepository) GetCorrectOptionID(ctx context.Context, questionID int6
|
|
| 386 |
return correctOptionID, nil
|
| 387 |
}
|
| 388 |
|
| 389 |
-
func (r *quizRepository) UserDeleteProgressAndAttempt(ctx context.Context, accountID int64, academyID int64) error {
|
| 390 |
-
|
| 391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
|
| 393 |
-
// Delete quiz attempts
|
| 394 |
-
|
| 395 |
-
Delete(&models.QuizAttempt{})
|
|
|
|
|
|
|
|
|
|
| 396 |
|
| 397 |
return nil
|
| 398 |
}
|
|
|
|
| 10 |
)
|
| 11 |
|
| 12 |
type QuizRepository interface {
|
| 13 |
+
UserGetQuiz(ctx context.Context, db *gorm.DB, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
|
| 14 |
+
UserIsPassedQuiz(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (bool, error)
|
| 15 |
+
UserGetActiveAttemptQuiz(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (*models.QuizAttempt, error)
|
| 16 |
+
UserGetTotalAttemptsQuiz(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (int64, error)
|
| 17 |
+
UserCreateAttemptQuiz(ctx context.Context, db *gorm.DB, attempt *models.QuizAttempt) error
|
| 18 |
+
UserGetAttemptQuizQuestionsResponse(ctx context.Context, db *gorm.DB, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error)
|
| 19 |
+
|
| 20 |
+
UserGetAttemptByID(ctx context.Context, db *gorm.DB, attemptID int64) (*models.QuizAttempt, error)
|
| 21 |
+
UserUpdateAttemptQuiz(ctx context.Context, db *gorm.DB, attempt *models.QuizAttempt) error
|
| 22 |
+
|
| 23 |
+
UserGetReviewQuiz(ctx context.Context, db *gorm.DB, attemptID, accountID, quizID int64) (*models.ReviewQuizResponse, error)
|
| 24 |
+
UseGetLastAttemptQuiz(ctx context.Context, db *gorm.DB, accountID int64, academyID int64) (*models.QuizAttempt, error)
|
| 25 |
+
|
| 26 |
+
UserGetTotalQuestionQuiz(ctx context.Context, db *gorm.DB, quizID int64) (int64, error)
|
| 27 |
+
UserGetTotalCorrectAnswerQuiz(ctx context.Context, db *gorm.DB, quizAttemptID int64) (int64, error)
|
| 28 |
+
UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) error
|
| 29 |
+
UserGetRemainingAttempts(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (int64, error)
|
| 30 |
+
UserGetQuestionQuiz(ctx context.Context, db *gorm.DB, attemptID, questionID int64) (*models.GetQuestionQuizResponse, error)
|
| 31 |
+
UserGetUserAnswer(ctx context.Context, db *gorm.DB, attemptID, questionID int64) (*models.UserAnswer, error)
|
| 32 |
+
UserSaveUserAnswer(ctx context.Context, db *gorm.DB, answer *models.UserAnswer) error
|
| 33 |
+
GetCorrectOptionID(ctx context.Context, db *gorm.DB, questionID int64) (int64, error)
|
| 34 |
+
UserDeleteProgressAndAttempt(ctx context.Context, db *gorm.DB, accountID int64, academyID int64) error
|
| 35 |
}
|
| 36 |
|
| 37 |
+
type quizRepository struct{}
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
func NewQuizRepository() QuizRepository {
|
| 40 |
+
return &quizRepository{}
|
| 41 |
}
|
| 42 |
|
| 43 |
+
func (r *quizRepository) UserGetQuiz(ctx context.Context, db *gorm.DB, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
|
| 44 |
var quizResponse models.UserGetQuizResponse
|
| 45 |
|
| 46 |
rawQuery := `
|
|
|
|
| 72 |
q.attempt_limit, q.time_limit, q.min_score, q.created_at, q.updated_at;
|
| 73 |
`
|
| 74 |
|
| 75 |
+
err := db.Debug().Raw(rawQuery, map[string]any{
|
| 76 |
"accountID": req.AccountID,
|
| 77 |
"academyID": req.AcademyID,
|
| 78 |
}).Scan(&quizResponse).Error
|
|
|
|
| 87 |
return &quizResponse, nil
|
| 88 |
}
|
| 89 |
|
| 90 |
+
func (r *quizRepository) UserIsPassedQuiz(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (bool, error) {
|
| 91 |
+
var count int64
|
| 92 |
+
|
| 93 |
+
err := db.WithContext(ctx).
|
| 94 |
+
Model(&models.QuizAttempt{}).
|
| 95 |
+
Where("account_id = ? AND quiz_id = ? AND is_passed = ?", accountID, quizID, true).
|
| 96 |
+
Count(&count).Error
|
| 97 |
+
if err != nil {
|
| 98 |
+
return false, fmt.Errorf("failed to check if quiz is passed: %w", err)
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
return count > 0, nil
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (*models.QuizAttempt, error) {
|
| 105 |
var quizAttempt models.QuizAttempt
|
| 106 |
|
| 107 |
+
err := db.Debug().Where("account_id = ? AND quiz_id = ? AND finished_at IS NULL", accountID, quizID).First(&quizAttempt).Error
|
| 108 |
if err != nil {
|
| 109 |
return nil, err
|
| 110 |
}
|
|
|
|
| 112 |
return &quizAttempt, nil
|
| 113 |
}
|
| 114 |
|
| 115 |
+
func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (int64, error) {
|
| 116 |
var totalAttempts int64
|
| 117 |
|
| 118 |
+
err := db.Debug().Model(&models.QuizAttempt{}).Where("account_id = ? AND quiz_id = ? AND finished_at IS NOT NULL", accountID, quizID).Count(&totalAttempts).Error
|
| 119 |
if err != nil {
|
| 120 |
return 0, err
|
| 121 |
}
|
|
|
|
| 123 |
return totalAttempts, nil
|
| 124 |
}
|
| 125 |
|
| 126 |
+
func (r *quizRepository) UserCreateAttemptQuiz(ctx context.Context, db *gorm.DB, attempt *models.QuizAttempt) error {
|
| 127 |
+
return db.Debug().Create(attempt).Error
|
| 128 |
}
|
| 129 |
|
| 130 |
+
func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context, db *gorm.DB, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error) {
|
| 131 |
questions := make([]models.UserAttemptQuizQuestionsResponse, 0)
|
| 132 |
|
| 133 |
rawQuery := `
|
|
|
|
| 157 |
q.id;
|
| 158 |
`
|
| 159 |
|
| 160 |
+
err := db.Debug().Raw(rawQuery, attemptID, accountID, quizID, attemptID, quizID).Scan(&questions).Error
|
| 161 |
if err != nil {
|
| 162 |
return nil, err
|
| 163 |
}
|
|
|
|
| 165 |
return questions, nil
|
| 166 |
}
|
| 167 |
|
| 168 |
+
func (r *quizRepository) UserGetAttemptByID(ctx context.Context, db *gorm.DB, attemptID int64) (*models.QuizAttempt, error) {
|
| 169 |
var quizAttempt models.QuizAttempt
|
| 170 |
|
| 171 |
+
err := db.Where("id = ?", attemptID).
|
| 172 |
Preload("Quiz").
|
| 173 |
First(&quizAttempt).Error
|
| 174 |
if err != nil {
|
|
|
|
| 178 |
return &quizAttempt, nil
|
| 179 |
}
|
| 180 |
|
| 181 |
+
func (r *quizRepository) UserUpdateAttemptQuiz(ctx context.Context, db *gorm.DB, attempt *models.QuizAttempt) error {
|
| 182 |
+
return db.Save(attempt).Error
|
| 183 |
}
|
| 184 |
|
| 185 |
+
func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, db *gorm.DB, quizID int64) (int64, error) {
|
| 186 |
var totalQuestion int64
|
| 187 |
|
| 188 |
+
err := db.Model(&models.Question{}).Where("quiz_id = ?", quizID).Count(&totalQuestion).Error
|
| 189 |
if err != nil {
|
| 190 |
return 0, err
|
| 191 |
}
|
|
|
|
| 193 |
return totalQuestion, nil
|
| 194 |
}
|
| 195 |
|
| 196 |
+
func (r *quizRepository) UserGetTotalCorrectAnswerQuiz(ctx context.Context, db *gorm.DB, quizAttemptID int64) (int64, error) {
|
| 197 |
var totalCorrectAnswer int64
|
| 198 |
|
| 199 |
+
err := db.Model(&models.UserAnswer{}).Where("quiz_attempt_id = ? AND is_correct = TRUE", quizAttemptID).Count(&totalCorrectAnswer).Error
|
| 200 |
if err != nil {
|
| 201 |
return 0, err
|
| 202 |
}
|
|
|
|
| 204 |
return totalCorrectAnswer, nil
|
| 205 |
}
|
| 206 |
|
| 207 |
+
func (r *quizRepository) UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) error {
|
| 208 |
+
return db.Where("account_id = ? AND quiz_id = ?", accountID, quizID).Delete(&models.QuizAttempt{}).Error
|
| 209 |
}
|
| 210 |
|
| 211 |
+
func (r *quizRepository) UserGetRemainingAttempts(ctx context.Context, db *gorm.DB, accountID int64, quizID int64) (int64, error) {
|
| 212 |
var remainingAttempts int64
|
| 213 |
|
| 214 |
rawQuery := `
|
|
|
|
| 227 |
q.attempt_limit;
|
| 228 |
`
|
| 229 |
|
| 230 |
+
err := db.Raw(rawQuery, accountID, quizID).Scan(&remainingAttempts).Error
|
| 231 |
if err != nil {
|
| 232 |
return 0, err
|
| 233 |
}
|
|
|
|
| 235 |
return remainingAttempts, nil
|
| 236 |
}
|
| 237 |
|
| 238 |
+
func (r *quizRepository) UserGetQuestionQuiz(ctx context.Context, db *gorm.DB, attemptID, questionID int64) (*models.GetQuestionQuizResponse, error) {
|
| 239 |
var resultJSON string
|
| 240 |
var response models.GetQuestionQuizResponse
|
| 241 |
|
|
|
|
| 264 |
LEFT JOIN
|
| 265 |
user_answers ua
|
| 266 |
ON q.id = ua.question_id
|
| 267 |
+
AND ua.quiz_attempt_id = ?
|
| 268 |
WHERE
|
| 269 |
+
q.id = ?
|
| 270 |
GROUP BY
|
| 271 |
q.id, q.quiz_id, q.content, ua.selected_answer_id, ua.is_doubt;
|
| 272 |
`
|
| 273 |
|
| 274 |
+
err := db.Raw(rawQuery, attemptID, questionID).Scan(&resultJSON).Error
|
| 275 |
if err != nil {
|
| 276 |
return nil, err
|
| 277 |
}
|
|
|
|
| 284 |
return &response, nil
|
| 285 |
}
|
| 286 |
|
| 287 |
+
func (r *quizRepository) UserGetQuestionByID(ctx context.Context, db *gorm.DB, questionID int64) (*models.Question, error) {
|
| 288 |
var question models.Question
|
| 289 |
|
| 290 |
+
err := db.Where("id = ?", questionID).First(&question).Error
|
| 291 |
if err != nil {
|
| 292 |
return nil, err
|
| 293 |
}
|
|
|
|
| 295 |
return &question, nil
|
| 296 |
}
|
| 297 |
|
| 298 |
+
func (r *quizRepository) UserGetUserAnswer(ctx context.Context, db *gorm.DB, attemptID, questionID int64) (*models.UserAnswer, error) {
|
| 299 |
var userAnswer models.UserAnswer
|
| 300 |
|
| 301 |
+
err := db.Where("quiz_attempt_id = ? AND question_id = ?", attemptID, questionID).First(&userAnswer).Error
|
| 302 |
if err != nil {
|
| 303 |
return nil, err
|
| 304 |
}
|
|
|
|
| 306 |
return &userAnswer, nil
|
| 307 |
}
|
| 308 |
|
| 309 |
+
func (r *quizRepository) UserSaveUserAnswer(ctx context.Context, db *gorm.DB, userAnswer *models.UserAnswer) error {
|
| 310 |
+
return db.Save(userAnswer).Error
|
| 311 |
}
|
| 312 |
|
| 313 |
+
func (r *quizRepository) UserGetReviewQuiz(ctx context.Context, db *gorm.DB, attemptID, accountID, quizID int64) (*models.ReviewQuizResponse, error) {
|
| 314 |
var resultJSON string
|
| 315 |
var reviewQuizResponse models.ReviewQuizResponse
|
| 316 |
|
|
|
|
| 339 |
FROM answers a
|
| 340 |
WHERE a.question_id = q.id
|
| 341 |
),
|
| 342 |
+
'answer_id', NULLIF(ua.selected_answer_id, 0)
|
| 343 |
) ORDER BY q.order
|
| 344 |
),
|
| 345 |
'[]'::json
|
|
|
|
| 354 |
q.quiz_id = ?
|
| 355 |
`
|
| 356 |
|
| 357 |
+
err := db.Raw(rawQuery, attemptID, quizID).Scan(&resultJSON).Error
|
| 358 |
if err != nil {
|
| 359 |
return nil, fmt.Errorf("failed to query reviews: %w", err)
|
| 360 |
}
|
|
|
|
| 369 |
return &reviewQuizResponse, nil
|
| 370 |
}
|
| 371 |
|
| 372 |
+
func (r *quizRepository) UseGetLastAttemptQuiz(ctx context.Context, db *gorm.DB, accountID int64, academyID int64) (*models.QuizAttempt, error) {
|
| 373 |
var attempt models.QuizAttempt
|
| 374 |
|
| 375 |
+
err := db.Where("account_id = ? AND academy_id = ?", accountID, academyID).Last(&attempt).Error
|
| 376 |
if err != nil {
|
| 377 |
return nil, err
|
| 378 |
}
|
|
|
|
| 380 |
return &attempt, nil
|
| 381 |
}
|
| 382 |
|
| 383 |
+
func (r *quizRepository) GetCorrectOptionID(ctx context.Context, db *gorm.DB, questionID int64) (int64, error) {
|
| 384 |
var correctOptionID int64
|
| 385 |
|
| 386 |
rawQuery := `
|
|
|
|
| 391 |
LIMIT 1;
|
| 392 |
`
|
| 393 |
|
| 394 |
+
err := db.Raw(rawQuery, questionID).Scan(&correctOptionID).Error
|
| 395 |
if err != nil {
|
| 396 |
return 0, err
|
| 397 |
}
|
|
|
|
| 399 |
return correctOptionID, nil
|
| 400 |
}
|
| 401 |
|
| 402 |
+
func (r *quizRepository) UserDeleteProgressAndAttempt(ctx context.Context, db *gorm.DB, accountID int64, academyID int64) error {
|
| 403 |
+
// Delete academy material progress
|
| 404 |
+
err := db.Where("account_id = ? AND academy_material_id IN (SELECT id FROM academy_materials WHERE academy_id = ?)", accountID, academyID).
|
| 405 |
+
Delete(&models.AcademyMaterialProgress{}).Error
|
| 406 |
+
if err != nil {
|
| 407 |
+
return err
|
| 408 |
+
}
|
| 409 |
|
| 410 |
+
// Delete quiz attempts
|
| 411 |
+
err = db.Where("account_id = ? AND quiz_id IN (SELECT id FROM quizzes WHERE academy_id = ?)", accountID, academyID).
|
| 412 |
+
Delete(&models.QuizAttempt{}).Error
|
| 413 |
+
if err != nil {
|
| 414 |
+
return err
|
| 415 |
+
}
|
| 416 |
|
| 417 |
return nil
|
| 418 |
}
|
space/response/api_response_v2.go
CHANGED
|
@@ -4,6 +4,7 @@ import (
|
|
| 4 |
"encoding/json"
|
| 5 |
"errors"
|
| 6 |
"net/http"
|
|
|
|
| 7 |
|
| 8 |
"api.qobiltu.id/models"
|
| 9 |
"api.qobiltu.id/pkg/validation"
|
|
@@ -49,7 +50,8 @@ func HandleError(c *gin.Context, err error) {
|
|
| 49 |
responseError(c, http.StatusBadRequest, exception)
|
| 50 |
case exception.AcademyNotFinished:
|
| 51 |
responseError(c, http.StatusBadRequest, exception)
|
| 52 |
-
|
|
|
|
| 53 |
case exception.ValidationError:
|
| 54 |
responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
|
| 55 |
default:
|
|
@@ -84,6 +86,16 @@ func HandleError(c *gin.Context, err error) {
|
|
| 84 |
return
|
| 85 |
}
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
utils.LogError(err)
|
| 88 |
responseError(c, http.StatusInternalServerError, models.Exception{
|
| 89 |
InternalServerError: true,
|
|
|
|
| 4 |
"encoding/json"
|
| 5 |
"errors"
|
| 6 |
"net/http"
|
| 7 |
+
"strconv"
|
| 8 |
|
| 9 |
"api.qobiltu.id/models"
|
| 10 |
"api.qobiltu.id/pkg/validation"
|
|
|
|
| 50 |
responseError(c, http.StatusBadRequest, exception)
|
| 51 |
case exception.AcademyNotFinished:
|
| 52 |
responseError(c, http.StatusBadRequest, exception)
|
| 53 |
+
case exception.QuizAlreadyPassed:
|
| 54 |
+
responseError(c, http.StatusBadRequest, exception)
|
| 55 |
case exception.ValidationError:
|
| 56 |
responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
|
| 57 |
default:
|
|
|
|
| 86 |
return
|
| 87 |
}
|
| 88 |
|
| 89 |
+
var numErr *strconv.NumError
|
| 90 |
+
if errors.As(err, &numErr) {
|
| 91 |
+
responseError(c, http.StatusBadRequest, models.Exception{
|
| 92 |
+
BadRequest: true,
|
| 93 |
+
Message: "Invalid number format",
|
| 94 |
+
Err: err,
|
| 95 |
+
})
|
| 96 |
+
return
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
utils.LogError(err)
|
| 100 |
responseError(c, http.StatusInternalServerError, models.Exception{
|
| 101 |
InternalServerError: true,
|
space/services/email_service.go
CHANGED
|
@@ -44,7 +44,7 @@ func (s *emailService) CreateEmailVerification(ctx context.Context, req *models.
|
|
| 44 |
}
|
| 45 |
}
|
| 46 |
|
| 47 |
-
remainingTime := time.Duration(config.
|
| 48 |
dueTime := time.Now().Add(remainingTime)
|
| 49 |
|
| 50 |
payload := models.EmailVerification{
|
|
|
|
| 44 |
}
|
| 45 |
}
|
| 46 |
|
| 47 |
+
remainingTime := time.Duration(config.GlobalConfig.App.EmailVerificationDuration) * time.Minute
|
| 48 |
dueTime := time.Now().Add(remainingTime)
|
| 49 |
|
| 50 |
payload := models.EmailVerification{
|
space/services/forgot_password_service.go
CHANGED
|
@@ -1,13 +1,14 @@
|
|
| 1 |
package services
|
| 2 |
|
| 3 |
import (
|
| 4 |
-
"api.qobiltu.id/pkg/worker"
|
| 5 |
-
"api.qobiltu.id/utils"
|
| 6 |
"context"
|
| 7 |
-
"github.com/hibiken/asynq"
|
| 8 |
"strconv"
|
| 9 |
"time"
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
"api.qobiltu.id/config"
|
| 12 |
"api.qobiltu.id/models"
|
| 13 |
"api.qobiltu.id/repositories"
|
|
@@ -40,7 +41,7 @@ func (s *ForgotPasswordService) Create(email string) {
|
|
| 40 |
return
|
| 41 |
}
|
| 42 |
|
| 43 |
-
remainingTime := time.Duration(config.
|
| 44 |
dueTime := CalculateDueTime(remainingTime)
|
| 45 |
|
| 46 |
s.Constructor.UUID = uuid.NewV4()
|
|
|
|
| 1 |
package services
|
| 2 |
|
| 3 |
import (
|
|
|
|
|
|
|
| 4 |
"context"
|
|
|
|
| 5 |
"strconv"
|
| 6 |
"time"
|
| 7 |
|
| 8 |
+
"api.qobiltu.id/pkg/worker"
|
| 9 |
+
"api.qobiltu.id/utils"
|
| 10 |
+
"github.com/hibiken/asynq"
|
| 11 |
+
|
| 12 |
"api.qobiltu.id/config"
|
| 13 |
"api.qobiltu.id/models"
|
| 14 |
"api.qobiltu.id/repositories"
|
|
|
|
| 41 |
return
|
| 42 |
}
|
| 43 |
|
| 44 |
+
remainingTime := time.Duration(config.GlobalConfig.App.EmailVerificationDuration) * time.Minute
|
| 45 |
dueTime := CalculateDueTime(remainingTime)
|
| 46 |
|
| 47 |
s.Constructor.UUID = uuid.NewV4()
|
space/services/jwt_service.go
CHANGED
|
@@ -11,16 +11,15 @@ import (
|
|
| 11 |
"golang.org/x/crypto/bcrypt"
|
| 12 |
)
|
| 13 |
|
| 14 |
-
var salt = config.Salt
|
| 15 |
-
var secretKey = []byte(salt)
|
| 16 |
-
|
| 17 |
func GenerateToken(user *models.Account) (string, error) {
|
|
|
|
|
|
|
| 18 |
claims := models.CustomClaims{
|
| 19 |
UserID: user.Id,
|
| 20 |
RegisteredClaims: jwt.RegisteredClaims{
|
| 21 |
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), // Token berlaku 24 jam
|
| 22 |
IssuedAt: jwt.NewNumericDate(time.Now()),
|
| 23 |
-
Issuer: "
|
| 24 |
},
|
| 25 |
}
|
| 26 |
|
|
@@ -38,13 +37,11 @@ func ExtractBearerToken(authHeader string) (string, error) {
|
|
| 38 |
}
|
| 39 |
|
| 40 |
func VerifyToken(bearerToken string) (uint, string, error) {
|
| 41 |
-
|
| 42 |
|
| 43 |
tokenData, err := ExtractBearerToken(bearerToken)
|
| 44 |
if err != nil {
|
| 45 |
return 0, "invalid-token", err
|
| 46 |
-
} else {
|
| 47 |
-
// fmt.Println("Extracted Token:", tokenData)
|
| 48 |
}
|
| 49 |
|
| 50 |
token, err := jwt.ParseWithClaims(tokenData, &models.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
|
@@ -55,7 +52,6 @@ func VerifyToken(bearerToken string) (uint, string, error) {
|
|
| 55 |
return 0, "invalid-token", err
|
| 56 |
}
|
| 57 |
|
| 58 |
-
// Extract the claims
|
| 59 |
claims, ok := token.Claims.(*models.CustomClaims)
|
| 60 |
if !ok || !token.Valid {
|
| 61 |
return 0, "invalid-token", err
|
|
@@ -74,6 +70,7 @@ func VerifyPassword(hashedPassword, password string) error {
|
|
| 74 |
}
|
| 75 |
return nil
|
| 76 |
}
|
|
|
|
| 77 |
func HashPassword(password string) (string, error) {
|
| 78 |
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
| 79 |
return string(bytes), err
|
|
|
|
| 11 |
"golang.org/x/crypto/bcrypt"
|
| 12 |
)
|
| 13 |
|
|
|
|
|
|
|
|
|
|
| 14 |
func GenerateToken(user *models.Account) (string, error) {
|
| 15 |
+
secretKey := []byte(config.GlobalConfig.App.Salt)
|
| 16 |
+
|
| 17 |
claims := models.CustomClaims{
|
| 18 |
UserID: user.Id,
|
| 19 |
RegisteredClaims: jwt.RegisteredClaims{
|
| 20 |
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), // Token berlaku 24 jam
|
| 21 |
IssuedAt: jwt.NewNumericDate(time.Now()),
|
| 22 |
+
Issuer: "api.qobiltu.id",
|
| 23 |
},
|
| 24 |
}
|
| 25 |
|
|
|
|
| 37 |
}
|
| 38 |
|
| 39 |
func VerifyToken(bearerToken string) (uint, string, error) {
|
| 40 |
+
secretKey := []byte(config.GlobalConfig.App.Salt)
|
| 41 |
|
| 42 |
tokenData, err := ExtractBearerToken(bearerToken)
|
| 43 |
if err != nil {
|
| 44 |
return 0, "invalid-token", err
|
|
|
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
token, err := jwt.ParseWithClaims(tokenData, &models.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
|
|
|
| 52 |
return 0, "invalid-token", err
|
| 53 |
}
|
| 54 |
|
|
|
|
| 55 |
claims, ok := token.Claims.(*models.CustomClaims)
|
| 56 |
if !ok || !token.Valid {
|
| 57 |
return 0, "invalid-token", err
|
|
|
|
| 70 |
}
|
| 71 |
return nil
|
| 72 |
}
|
| 73 |
+
|
| 74 |
func HashPassword(password string) (string, error) {
|
| 75 |
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
| 76 |
return string(bytes), err
|
space/services/quiz_service.go
CHANGED
|
@@ -5,6 +5,7 @@ import (
|
|
| 5 |
"errors"
|
| 6 |
"time"
|
| 7 |
|
|
|
|
| 8 |
"api.qobiltu.id/models"
|
| 9 |
"api.qobiltu.id/pkg/validation"
|
| 10 |
"api.qobiltu.id/repositories"
|
|
@@ -27,17 +28,23 @@ type QuizService interface {
|
|
| 27 |
}
|
| 28 |
|
| 29 |
type quizService struct {
|
|
|
|
| 30 |
quizRepository repositories.QuizRepository
|
| 31 |
academyRepository repositories.AcademyRepository
|
| 32 |
validator *validation.Validator
|
| 33 |
}
|
| 34 |
|
| 35 |
-
func NewQuizService(quizRepository repositories.QuizRepository, academyRepository repositories.AcademyRepository, validator *validation.Validator) QuizService {
|
| 36 |
-
return &quizService{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
}
|
| 38 |
|
| 39 |
func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
|
| 40 |
-
quiz, err := s.quizRepository.UserGetQuiz(ctx, req)
|
| 41 |
if err != nil {
|
| 42 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 43 |
}
|
|
@@ -46,8 +53,8 @@ func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRe
|
|
| 46 |
}
|
| 47 |
|
| 48 |
func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
|
| 49 |
-
//
|
| 50 |
-
quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, &models.UserGetQuizRequest{
|
| 51 |
AccountID: req.AccountID,
|
| 52 |
AcademyID: req.AcademyID,
|
| 53 |
})
|
|
@@ -57,31 +64,41 @@ func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttem
|
|
| 57 |
|
| 58 |
quiz := &quizAttempt.Quiz
|
| 59 |
|
| 60 |
-
//
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
| 63 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 64 |
}
|
| 65 |
|
| 66 |
-
//
|
| 67 |
if existingAttempt != nil {
|
| 68 |
return s.handleActiveAttempt(ctx, req, quiz, existingAttempt)
|
| 69 |
}
|
| 70 |
|
|
|
|
| 71 |
return s.handleNewAttempt(ctx, req, quiz)
|
| 72 |
}
|
| 73 |
|
| 74 |
func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
|
| 75 |
now := time.Now()
|
| 76 |
|
| 77 |
-
//
|
| 78 |
if attempt.DueAt.Before(now) && attempt.FinishedAt == nil {
|
| 79 |
-
// frontend nge-trigger submit
|
| 80 |
return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
|
| 81 |
}
|
| 82 |
|
| 83 |
-
//
|
| 84 |
-
questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
|
| 85 |
if err != nil {
|
| 86 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 87 |
}
|
|
@@ -101,64 +118,77 @@ func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserA
|
|
| 101 |
}
|
| 102 |
|
| 103 |
func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
|
|
|
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
-
// ambil question
|
| 136 |
-
questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
|
| 137 |
if err != nil {
|
| 138 |
-
return nil,
|
| 139 |
}
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
return &models.UserAttemptQuizResponse{
|
| 144 |
-
ID: attempt.ID,
|
| 145 |
-
AccountID: attempt.AccountID,
|
| 146 |
-
QuizID: attempt.QuizID,
|
| 147 |
-
StartedAt: attempt.StartedAt,
|
| 148 |
-
DueAt: attempt.DueAt,
|
| 149 |
-
FinishedAt: attempt.FinishedAt,
|
| 150 |
-
Score: attempt.Score,
|
| 151 |
-
Questions: questions,
|
| 152 |
-
}, nil
|
| 153 |
}
|
| 154 |
|
| 155 |
func (s *quizService) calculateQuizScore(
|
| 156 |
ctx context.Context,
|
|
|
|
| 157 |
quiz *models.Quiz,
|
| 158 |
attempt *models.QuizAttempt,
|
| 159 |
) error {
|
| 160 |
-
//
|
| 161 |
-
totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, attempt.QuizID)
|
| 162 |
if err != nil {
|
| 163 |
return err
|
| 164 |
}
|
|
@@ -172,14 +202,14 @@ func (s *quizService) calculateQuizScore(
|
|
| 172 |
return nil
|
| 173 |
}
|
| 174 |
|
| 175 |
-
//
|
| 176 |
-
correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, attempt.ID)
|
| 177 |
if err != nil {
|
| 178 |
return err
|
| 179 |
}
|
| 180 |
|
|
|
|
| 181 |
score := float64(correctAnswer) / float64(totalQuestion) * 100
|
| 182 |
-
|
| 183 |
attempt.Score = score
|
| 184 |
attempt.TotalQuestions = totalQuestion
|
| 185 |
attempt.TotalCorrectAnswer = correctAnswer
|
|
@@ -193,14 +223,14 @@ func (s *quizService) calculateQuizScore(
|
|
| 193 |
}
|
| 194 |
|
| 195 |
func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error) {
|
| 196 |
-
//
|
| 197 |
-
_, err := s.quizRepository.UserGetAttemptByID(ctx, req.AttemptID)
|
| 198 |
if err != nil {
|
| 199 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 200 |
}
|
| 201 |
|
| 202 |
-
//
|
| 203 |
-
question, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
|
| 204 |
if err != nil {
|
| 205 |
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 206 |
return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
|
|
@@ -212,105 +242,135 @@ func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQu
|
|
| 212 |
}
|
| 213 |
|
| 214 |
func (s *quizService) UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error) {
|
|
|
|
| 215 |
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
if err != nil {
|
| 223 |
-
|
| 224 |
-
// jika belum ada answer
|
| 225 |
-
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 226 |
-
userAnswer := models.UserAnswer{
|
| 227 |
-
QuizAttemptID: req.AttemptID,
|
| 228 |
-
QuestionID: req.QuestionID,
|
| 229 |
-
AnswerID: req.AnswerID,
|
| 230 |
-
IsDoubt: req.IsDoubt,
|
| 231 |
-
IsCorrect: false,
|
| 232 |
-
}
|
| 233 |
-
|
| 234 |
-
if req.AnswerID != nil {
|
| 235 |
-
userAnswer.IsCorrect = *req.AnswerID == correctOptionID
|
| 236 |
-
}
|
| 237 |
-
|
| 238 |
-
if err := s.quizRepository.UserSaveUserAnswer(ctx, &userAnswer); err != nil {
|
| 239 |
-
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 240 |
-
}
|
| 241 |
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
}
|
| 246 |
|
| 247 |
-
return
|
| 248 |
}
|
| 249 |
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
|
| 261 |
-
|
| 262 |
-
return nil
|
| 263 |
-
}
|
| 264 |
|
| 265 |
-
res, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
|
| 266 |
if err != nil {
|
| 267 |
-
|
| 268 |
-
return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
|
| 269 |
-
}
|
| 270 |
-
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 271 |
}
|
| 272 |
|
| 273 |
-
return
|
| 274 |
}
|
| 275 |
|
| 276 |
func (s *quizService) UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error) {
|
| 277 |
-
|
| 278 |
-
if err != nil {
|
| 279 |
-
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 280 |
-
}
|
| 281 |
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
|
|
|
| 289 |
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
if err
|
| 298 |
-
return
|
| 299 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
}
|
| 301 |
|
| 302 |
-
return
|
| 303 |
-
QuizAttempt: *attempt,
|
| 304 |
-
RemainingAttempts: remainingAttempts,
|
| 305 |
-
}, nil
|
| 306 |
}
|
| 307 |
|
| 308 |
func (s *quizService) UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error) {
|
| 309 |
-
|
|
|
|
| 310 |
if err != nil {
|
| 311 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 312 |
}
|
| 313 |
-
|
|
|
|
|
|
|
| 314 |
if err != nil {
|
| 315 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 316 |
}
|
|
@@ -322,12 +382,14 @@ func (s *quizService) UserResultQuiz(ctx context.Context, req *models.ResultQuiz
|
|
| 322 |
}
|
| 323 |
|
| 324 |
func (s *quizService) UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error) {
|
| 325 |
-
|
|
|
|
| 326 |
if err != nil {
|
| 327 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 328 |
}
|
| 329 |
|
| 330 |
-
|
|
|
|
| 331 |
if err != nil {
|
| 332 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 333 |
}
|
|
|
|
| 5 |
"errors"
|
| 6 |
"time"
|
| 7 |
|
| 8 |
+
"api.qobiltu.id/config"
|
| 9 |
"api.qobiltu.id/models"
|
| 10 |
"api.qobiltu.id/pkg/validation"
|
| 11 |
"api.qobiltu.id/repositories"
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
type quizService struct {
|
| 31 |
+
db *gorm.DB
|
| 32 |
quizRepository repositories.QuizRepository
|
| 33 |
academyRepository repositories.AcademyRepository
|
| 34 |
validator *validation.Validator
|
| 35 |
}
|
| 36 |
|
| 37 |
+
func NewQuizService(db *gorm.DB, quizRepository repositories.QuizRepository, academyRepository repositories.AcademyRepository, validator *validation.Validator) QuizService {
|
| 38 |
+
return &quizService{
|
| 39 |
+
db: db,
|
| 40 |
+
quizRepository: quizRepository,
|
| 41 |
+
academyRepository: academyRepository,
|
| 42 |
+
validator: validator,
|
| 43 |
+
}
|
| 44 |
}
|
| 45 |
|
| 46 |
func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
|
| 47 |
+
quiz, err := s.quizRepository.UserGetQuiz(ctx, s.db, req)
|
| 48 |
if err != nil {
|
| 49 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 50 |
}
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
|
| 56 |
+
// Fetch quiz data
|
| 57 |
+
quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, s.db, &models.UserGetQuizRequest{
|
| 58 |
AccountID: req.AccountID,
|
| 59 |
AcademyID: req.AcademyID,
|
| 60 |
})
|
|
|
|
| 64 |
|
| 65 |
quiz := &quizAttempt.Quiz
|
| 66 |
|
| 67 |
+
// check is already passed
|
| 68 |
+
isPassedQuiz, err := s.quizRepository.UserIsPassedQuiz(ctx, s.db, req.AccountID, quiz.ID)
|
| 69 |
+
if err != nil {
|
| 70 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
if isPassedQuiz {
|
| 74 |
+
return nil, models.Exception{QuizAlreadyPassed: true, Message: "You have already passed this quiz"}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Check for active attempt
|
| 78 |
+
existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, s.db, req.AccountID, quiz.ID)
|
| 79 |
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
| 80 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 81 |
}
|
| 82 |
|
| 83 |
+
// Handle existing active attempt
|
| 84 |
if existingAttempt != nil {
|
| 85 |
return s.handleActiveAttempt(ctx, req, quiz, existingAttempt)
|
| 86 |
}
|
| 87 |
|
| 88 |
+
// Handle new attempt
|
| 89 |
return s.handleNewAttempt(ctx, req, quiz)
|
| 90 |
}
|
| 91 |
|
| 92 |
func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
|
| 93 |
now := time.Now()
|
| 94 |
|
| 95 |
+
// Check if attempt has expired
|
| 96 |
if attempt.DueAt.Before(now) && attempt.FinishedAt == nil {
|
|
|
|
| 97 |
return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
|
| 98 |
}
|
| 99 |
|
| 100 |
+
// Fetch questions for active attempt
|
| 101 |
+
questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, s.db, req.AccountID, quiz.ID, attempt.ID)
|
| 102 |
if err != nil {
|
| 103 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 104 |
}
|
|
|
|
| 118 |
}
|
| 119 |
|
| 120 |
func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
|
| 121 |
+
var result *models.UserAttemptQuizResponse
|
| 122 |
|
| 123 |
+
err := config.RunTx(ctx, s.db, func(tx *gorm.DB) error {
|
| 124 |
+
// Check total attempts
|
| 125 |
+
totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, tx, req.AccountID, quiz.ID)
|
| 126 |
+
if err != nil {
|
| 127 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 128 |
+
}
|
| 129 |
+
if totalAttempts >= quiz.AttemptLimit {
|
| 130 |
+
return models.Exception{QuizAttemptLimit: true, Message: "Attempt limit reached"}
|
| 131 |
+
}
|
| 132 |
|
| 133 |
+
// Check academy progress
|
| 134 |
+
percentage, err := s.academyRepository.UserGetPercentageProgressAcademyByID(ctx, tx, req.AccountID, req.AcademyID)
|
| 135 |
+
if err != nil {
|
| 136 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 137 |
+
}
|
| 138 |
+
if percentage < 100 {
|
| 139 |
+
return models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
|
| 140 |
+
}
|
| 141 |
|
| 142 |
+
// Create new attempt
|
| 143 |
+
attempt := models.QuizAttempt{
|
| 144 |
+
AccountID: req.AccountID,
|
| 145 |
+
QuizID: quiz.ID,
|
| 146 |
+
AcademyID: quiz.AcademyID,
|
| 147 |
+
StartedAt: time.Now(),
|
| 148 |
+
DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
|
| 149 |
+
}
|
| 150 |
|
| 151 |
+
if err := s.quizRepository.UserCreateAttemptQuiz(ctx, tx, &attempt); err != nil {
|
| 152 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// Fetch questions
|
| 156 |
+
questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, tx, req.AccountID, quiz.ID, attempt.ID)
|
| 157 |
+
if err != nil {
|
| 158 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
questions = utils.ShuffleWithKey(questions, attempt.ID)
|
| 162 |
+
|
| 163 |
+
result = &models.UserAttemptQuizResponse{
|
| 164 |
+
ID: attempt.ID,
|
| 165 |
+
AccountID: attempt.AccountID,
|
| 166 |
+
QuizID: attempt.QuizID,
|
| 167 |
+
StartedAt: attempt.StartedAt,
|
| 168 |
+
DueAt: attempt.DueAt,
|
| 169 |
+
FinishedAt: attempt.FinishedAt,
|
| 170 |
+
Score: attempt.Score,
|
| 171 |
+
Questions: questions,
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
return nil
|
| 175 |
+
})
|
| 176 |
|
|
|
|
|
|
|
| 177 |
if err != nil {
|
| 178 |
+
return nil, models.Exception{InternalServerError: true, Message: "Internal Server Error"}
|
| 179 |
}
|
| 180 |
|
| 181 |
+
return result, nil
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
}
|
| 183 |
|
| 184 |
func (s *quizService) calculateQuizScore(
|
| 185 |
ctx context.Context,
|
| 186 |
+
tx *gorm.DB,
|
| 187 |
quiz *models.Quiz,
|
| 188 |
attempt *models.QuizAttempt,
|
| 189 |
) error {
|
| 190 |
+
// Fetch total questions
|
| 191 |
+
totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, tx, attempt.QuizID)
|
| 192 |
if err != nil {
|
| 193 |
return err
|
| 194 |
}
|
|
|
|
| 202 |
return nil
|
| 203 |
}
|
| 204 |
|
| 205 |
+
// Fetch total correct answers
|
| 206 |
+
correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, tx, attempt.ID)
|
| 207 |
if err != nil {
|
| 208 |
return err
|
| 209 |
}
|
| 210 |
|
| 211 |
+
// Calculate score
|
| 212 |
score := float64(correctAnswer) / float64(totalQuestion) * 100
|
|
|
|
| 213 |
attempt.Score = score
|
| 214 |
attempt.TotalQuestions = totalQuestion
|
| 215 |
attempt.TotalCorrectAnswer = correctAnswer
|
|
|
|
| 223 |
}
|
| 224 |
|
| 225 |
func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error) {
|
| 226 |
+
// Check if attempt exists
|
| 227 |
+
_, err := s.quizRepository.UserGetAttemptByID(ctx, s.db, req.AttemptID)
|
| 228 |
if err != nil {
|
| 229 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 230 |
}
|
| 231 |
|
| 232 |
+
// Fetch question and user answer
|
| 233 |
+
question, err := s.quizRepository.UserGetQuestionQuiz(ctx, s.db, req.AttemptID, req.QuestionID)
|
| 234 |
if err != nil {
|
| 235 |
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 236 |
return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
|
|
|
|
| 242 |
}
|
| 243 |
|
| 244 |
func (s *quizService) UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error) {
|
| 245 |
+
var result *models.GetQuestionQuizResponse
|
| 246 |
|
| 247 |
+
err := config.RunTx(ctx, s.db, func(tx *gorm.DB) error {
|
| 248 |
+
// Fetch correct option ID
|
| 249 |
+
correctOptionID, err := s.quizRepository.GetCorrectOptionID(ctx, tx, req.QuestionID)
|
| 250 |
+
if err != nil {
|
| 251 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 252 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
+
// Check if user answer exists
|
| 255 |
+
question, err := s.quizRepository.UserGetUserAnswer(ctx, tx, req.AttemptID, req.QuestionID)
|
| 256 |
+
if err != nil {
|
| 257 |
+
// If no answer exists, create a new one
|
| 258 |
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 259 |
+
userAnswer := models.UserAnswer{
|
| 260 |
+
QuizAttemptID: req.AttemptID,
|
| 261 |
+
QuestionID: req.QuestionID,
|
| 262 |
+
AnswerID: req.AnswerID,
|
| 263 |
+
IsDoubt: req.IsDoubt,
|
| 264 |
+
IsCorrect: false,
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
if req.AnswerID != nil {
|
| 268 |
+
userAnswer.IsCorrect = *req.AnswerID == correctOptionID
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
if err := s.quizRepository.UserSaveUserAnswer(ctx, tx, &userAnswer); err != nil {
|
| 272 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
res, err := s.quizRepository.UserGetQuestionQuiz(ctx, tx, req.AttemptID, req.QuestionID)
|
| 276 |
+
if err != nil {
|
| 277 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
result = res
|
| 281 |
+
return nil
|
| 282 |
}
|
| 283 |
|
| 284 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 285 |
}
|
| 286 |
|
| 287 |
+
// Update existing answer
|
| 288 |
+
question.AnswerID = req.AnswerID
|
| 289 |
+
question.IsDoubt = req.IsDoubt
|
| 290 |
+
if req.AnswerID != nil {
|
| 291 |
+
question.IsCorrect = *req.AnswerID == correctOptionID
|
| 292 |
+
}
|
| 293 |
|
| 294 |
+
if err := s.quizRepository.UserSaveUserAnswer(ctx, tx, question); err != nil {
|
| 295 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 296 |
+
}
|
| 297 |
|
| 298 |
+
res, err := s.quizRepository.UserGetQuestionQuiz(ctx, tx, req.AttemptID, req.QuestionID)
|
| 299 |
+
if err != nil {
|
| 300 |
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 301 |
+
return models.Exception{DataNotFound: true, Message: "Question not found"}
|
| 302 |
+
}
|
| 303 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 304 |
+
}
|
| 305 |
|
| 306 |
+
result = res
|
| 307 |
+
return nil
|
| 308 |
+
})
|
| 309 |
|
|
|
|
| 310 |
if err != nil {
|
| 311 |
+
return nil, err
|
|
|
|
|
|
|
|
|
|
| 312 |
}
|
| 313 |
|
| 314 |
+
return result, nil
|
| 315 |
}
|
| 316 |
|
| 317 |
func (s *quizService) UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error) {
|
| 318 |
+
var result *models.SubmitQuizResponse
|
|
|
|
|
|
|
|
|
|
| 319 |
|
| 320 |
+
err := config.RunTx(ctx, s.db, func(tx *gorm.DB) error {
|
| 321 |
+
// Fetch attempt
|
| 322 |
+
attempt, err := s.quizRepository.UserGetAttemptByID(ctx, tx, req.AttemptID)
|
| 323 |
+
if err != nil {
|
| 324 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 325 |
+
}
|
| 326 |
|
| 327 |
+
// Calculate score
|
| 328 |
+
if err := s.calculateQuizScore(ctx, tx, attempt.Quiz, attempt); err != nil {
|
| 329 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 330 |
+
}
|
| 331 |
|
| 332 |
+
// Update attempt
|
| 333 |
+
if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, tx, attempt); err != nil {
|
| 334 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 335 |
+
}
|
| 336 |
|
| 337 |
+
// Get remaining attempts
|
| 338 |
+
remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, tx, attempt.AccountID, attempt.AcademyID)
|
| 339 |
+
if err != nil {
|
| 340 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 341 |
}
|
| 342 |
+
|
| 343 |
+
// If no remaining attempts, delete progress and attempts
|
| 344 |
+
if remainingAttempts == 0 {
|
| 345 |
+
if err := s.quizRepository.UserDeleteProgressAndAttempt(ctx, tx, attempt.AccountID, attempt.AcademyID); err != nil {
|
| 346 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 347 |
+
}
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
result = &models.SubmitQuizResponse{
|
| 351 |
+
QuizAttempt: *attempt,
|
| 352 |
+
RemainingAttempts: remainingAttempts,
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
return nil
|
| 356 |
+
})
|
| 357 |
+
|
| 358 |
+
if err != nil {
|
| 359 |
+
return nil, err
|
| 360 |
}
|
| 361 |
|
| 362 |
+
return result, nil
|
|
|
|
|
|
|
|
|
|
| 363 |
}
|
| 364 |
|
| 365 |
func (s *quizService) UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error) {
|
| 366 |
+
// Fetch last attempt
|
| 367 |
+
attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, s.db, req.AccountID, req.AcademyID)
|
| 368 |
if err != nil {
|
| 369 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 370 |
}
|
| 371 |
+
|
| 372 |
+
// Get remaining attempts
|
| 373 |
+
remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, s.db, attempt.AccountID, attempt.AcademyID)
|
| 374 |
if err != nil {
|
| 375 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 376 |
}
|
|
|
|
| 382 |
}
|
| 383 |
|
| 384 |
func (s *quizService) UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error) {
|
| 385 |
+
// Fetch last attempt
|
| 386 |
+
attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, s.db, req.AccountID, req.AcademyID)
|
| 387 |
if err != nil {
|
| 388 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 389 |
}
|
| 390 |
|
| 391 |
+
// Fetch review
|
| 392 |
+
review, err := s.quizRepository.UserGetReviewQuiz(ctx, s.db, attempt.ID, attempt.AccountID, attempt.QuizID)
|
| 393 |
if err != nil {
|
| 394 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 395 |
}
|
space/space/controller/quiz/quiz_controller.go
CHANGED
|
@@ -20,6 +20,7 @@ type QuizController interface {
|
|
| 20 |
UserGetQuestionQuiz(ctx *gin.Context)
|
| 21 |
UserAnswerQuiz(ctx *gin.Context)
|
| 22 |
UserSubmitQuiz(ctx *gin.Context)
|
|
|
|
| 23 |
UserReviewQuiz(ctx *gin.Context)
|
| 24 |
}
|
| 25 |
|
|
@@ -189,6 +190,29 @@ func (c *quizController) UserSubmitQuiz(ctx *gin.Context) {
|
|
| 189 |
response.HandleSuccess(ctx, http.StatusOK, "Quiz submitted successfully", res, nil)
|
| 190 |
}
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
func (c *quizController) UserReviewQuiz(ctx *gin.Context) {
|
| 193 |
academyID := ctx.Query("academy_id")
|
| 194 |
academyIDInt, err := strconv.Atoi(academyID)
|
|
|
|
| 20 |
UserGetQuestionQuiz(ctx *gin.Context)
|
| 21 |
UserAnswerQuiz(ctx *gin.Context)
|
| 22 |
UserSubmitQuiz(ctx *gin.Context)
|
| 23 |
+
UserResultQuiz(ctx *gin.Context)
|
| 24 |
UserReviewQuiz(ctx *gin.Context)
|
| 25 |
}
|
| 26 |
|
|
|
|
| 190 |
response.HandleSuccess(ctx, http.StatusOK, "Quiz submitted successfully", res, nil)
|
| 191 |
}
|
| 192 |
|
| 193 |
+
func (c *quizController) UserResultQuiz(ctx *gin.Context) {
|
| 194 |
+
academyID := ctx.Query("academy_id")
|
| 195 |
+
academyIDInt, err := strconv.Atoi(academyID)
|
| 196 |
+
if err != nil {
|
| 197 |
+
response.HandleError(ctx, err)
|
| 198 |
+
return
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
accountData := middleware.GetAccountData(ctx)
|
| 202 |
+
req := models.ResultQuizRequest{
|
| 203 |
+
AccountID: int64(accountData.UserID),
|
| 204 |
+
AcademyID: int64(academyIDInt),
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
res, err := c.quizService.UserResultQuiz(ctx, &req)
|
| 208 |
+
if err != nil {
|
| 209 |
+
response.HandleError(ctx, err)
|
| 210 |
+
return
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
response.HandleSuccess(ctx, http.StatusOK, "Quiz submitted successfully", res, nil)
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
func (c *quizController) UserReviewQuiz(ctx *gin.Context) {
|
| 217 |
academyID := ctx.Query("academy_id")
|
| 218 |
academyIDInt, err := strconv.Atoi(academyID)
|
space/space/models/request_model.go
CHANGED
|
@@ -234,6 +234,17 @@ type (
|
|
| 234 |
RemainingAttempts int64 `json:"remaining_attempts" validate:"required"`
|
| 235 |
}
|
| 236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
// REVIEW QUIZ
|
| 238 |
ReviewQuizRequest struct {
|
| 239 |
AccountID int64 `json:"account_id" validate:"required"`
|
|
@@ -254,7 +265,7 @@ type (
|
|
| 254 |
Content string `json:"content"`
|
| 255 |
IsCorrect bool `json:"is_correct"`
|
| 256 |
} `json:"answer_options"`
|
| 257 |
-
AnswerID int64 `json:"answer_id"`
|
| 258 |
}
|
| 259 |
|
| 260 |
ReviewQuizResponse struct {
|
|
|
|
| 234 |
RemainingAttempts int64 `json:"remaining_attempts" validate:"required"`
|
| 235 |
}
|
| 236 |
|
| 237 |
+
// RESULT QUIZ
|
| 238 |
+
ResultQuizRequest struct {
|
| 239 |
+
AccountID int64 `json:"account_id" validate:"required"`
|
| 240 |
+
AcademyID int64 `json:"academy_id" validate:"required"`
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
ResultQuizResponse struct {
|
| 244 |
+
QuizAttempt
|
| 245 |
+
RemainingAttempts int64 `json:"remaining_attempts" validate:"required"`
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
// REVIEW QUIZ
|
| 249 |
ReviewQuizRequest struct {
|
| 250 |
AccountID int64 `json:"account_id" validate:"required"`
|
|
|
|
| 265 |
Content string `json:"content"`
|
| 266 |
IsCorrect bool `json:"is_correct"`
|
| 267 |
} `json:"answer_options"`
|
| 268 |
+
AnswerID *int64 `json:"answer_id"`
|
| 269 |
}
|
| 270 |
|
| 271 |
ReviewQuizResponse struct {
|
space/space/router/quiz_route.go
CHANGED
|
@@ -12,6 +12,7 @@ func (s *Server) QuizRoute() {
|
|
| 12 |
userRouterGroup.GET("/question", middleware.AuthUser, s.quizController.UserGetQuestionQuiz)
|
| 13 |
userRouterGroup.PUT("/answer", middleware.AuthUser, s.quizController.UserAnswerQuiz)
|
| 14 |
userRouterGroup.POST("/submit", middleware.AuthUser, s.quizController.UserSubmitQuiz)
|
|
|
|
| 15 |
userRouterGroup.GET("/review", middleware.AuthUser, s.quizController.UserReviewQuiz)
|
| 16 |
}
|
| 17 |
}
|
|
|
|
| 12 |
userRouterGroup.GET("/question", middleware.AuthUser, s.quizController.UserGetQuestionQuiz)
|
| 13 |
userRouterGroup.PUT("/answer", middleware.AuthUser, s.quizController.UserAnswerQuiz)
|
| 14 |
userRouterGroup.POST("/submit", middleware.AuthUser, s.quizController.UserSubmitQuiz)
|
| 15 |
+
userRouterGroup.GET("/result", middleware.AuthUser, s.quizController.UserResultQuiz)
|
| 16 |
userRouterGroup.GET("/review", middleware.AuthUser, s.quizController.UserReviewQuiz)
|
| 17 |
}
|
| 18 |
}
|
space/space/services/quiz_service.go
CHANGED
|
@@ -5,6 +5,7 @@ import (
|
|
| 5 |
"errors"
|
| 6 |
"time"
|
| 7 |
|
|
|
|
| 8 |
"api.qobiltu.id/models"
|
| 9 |
"api.qobiltu.id/pkg/validation"
|
| 10 |
"api.qobiltu.id/repositories"
|
|
@@ -22,21 +23,28 @@ type QuizService interface {
|
|
| 22 |
UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
|
| 23 |
UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error)
|
| 24 |
UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
|
|
|
|
| 25 |
UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
|
| 26 |
}
|
| 27 |
|
| 28 |
type quizService struct {
|
|
|
|
| 29 |
quizRepository repositories.QuizRepository
|
| 30 |
academyRepository repositories.AcademyRepository
|
| 31 |
validator *validation.Validator
|
| 32 |
}
|
| 33 |
|
| 34 |
-
func NewQuizService(quizRepository repositories.QuizRepository, academyRepository repositories.AcademyRepository, validator *validation.Validator) QuizService {
|
| 35 |
-
return &quizService{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
|
| 39 |
-
quiz, err := s.quizRepository.UserGetQuiz(ctx, req)
|
| 40 |
if err != nil {
|
| 41 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 42 |
}
|
|
@@ -45,8 +53,8 @@ func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRe
|
|
| 45 |
}
|
| 46 |
|
| 47 |
func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
|
| 48 |
-
//
|
| 49 |
-
quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, &models.UserGetQuizRequest{
|
| 50 |
AccountID: req.AccountID,
|
| 51 |
AcademyID: req.AcademyID,
|
| 52 |
})
|
|
@@ -56,31 +64,41 @@ func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttem
|
|
| 56 |
|
| 57 |
quiz := &quizAttempt.Quiz
|
| 58 |
|
| 59 |
-
//
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
| 62 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 63 |
}
|
| 64 |
|
| 65 |
-
//
|
| 66 |
if existingAttempt != nil {
|
| 67 |
return s.handleActiveAttempt(ctx, req, quiz, existingAttempt)
|
| 68 |
}
|
| 69 |
|
|
|
|
| 70 |
return s.handleNewAttempt(ctx, req, quiz)
|
| 71 |
}
|
| 72 |
|
| 73 |
func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
|
| 74 |
now := time.Now()
|
| 75 |
|
| 76 |
-
//
|
| 77 |
if attempt.DueAt.Before(now) && attempt.FinishedAt == nil {
|
| 78 |
-
// frontend nge-trigger submit
|
| 79 |
return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
|
| 80 |
}
|
| 81 |
|
| 82 |
-
//
|
| 83 |
-
questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
|
| 84 |
if err != nil {
|
| 85 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 86 |
}
|
|
@@ -100,64 +118,77 @@ func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserA
|
|
| 100 |
}
|
| 101 |
|
| 102 |
func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
|
|
|
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
| 112 |
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
-
// ambil question
|
| 135 |
-
questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
|
| 136 |
if err != nil {
|
| 137 |
-
return nil,
|
| 138 |
}
|
| 139 |
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
return &models.UserAttemptQuizResponse{
|
| 143 |
-
ID: attempt.ID,
|
| 144 |
-
AccountID: attempt.AccountID,
|
| 145 |
-
QuizID: attempt.QuizID,
|
| 146 |
-
StartedAt: attempt.StartedAt,
|
| 147 |
-
DueAt: attempt.DueAt,
|
| 148 |
-
FinishedAt: attempt.FinishedAt,
|
| 149 |
-
Score: attempt.Score,
|
| 150 |
-
Questions: questions,
|
| 151 |
-
}, nil
|
| 152 |
}
|
| 153 |
|
| 154 |
func (s *quizService) calculateQuizScore(
|
| 155 |
ctx context.Context,
|
|
|
|
| 156 |
quiz *models.Quiz,
|
| 157 |
attempt *models.QuizAttempt,
|
| 158 |
) error {
|
| 159 |
-
//
|
| 160 |
-
totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, attempt.QuizID)
|
| 161 |
if err != nil {
|
| 162 |
return err
|
| 163 |
}
|
|
@@ -171,14 +202,14 @@ func (s *quizService) calculateQuizScore(
|
|
| 171 |
return nil
|
| 172 |
}
|
| 173 |
|
| 174 |
-
//
|
| 175 |
-
correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, attempt.ID)
|
| 176 |
if err != nil {
|
| 177 |
return err
|
| 178 |
}
|
| 179 |
|
|
|
|
| 180 |
score := float64(correctAnswer) / float64(totalQuestion) * 100
|
| 181 |
-
|
| 182 |
attempt.Score = score
|
| 183 |
attempt.TotalQuestions = totalQuestion
|
| 184 |
attempt.TotalCorrectAnswer = correctAnswer
|
|
@@ -192,14 +223,14 @@ func (s *quizService) calculateQuizScore(
|
|
| 192 |
}
|
| 193 |
|
| 194 |
func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error) {
|
| 195 |
-
//
|
| 196 |
-
_, err := s.quizRepository.UserGetAttemptByID(ctx, req.AttemptID)
|
| 197 |
if err != nil {
|
| 198 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 199 |
}
|
| 200 |
|
| 201 |
-
//
|
| 202 |
-
question, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
|
| 203 |
if err != nil {
|
| 204 |
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 205 |
return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
|
|
@@ -211,106 +242,154 @@ func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQu
|
|
| 211 |
}
|
| 212 |
|
| 213 |
func (s *quizService) UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error) {
|
|
|
|
| 214 |
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
if err != nil {
|
| 222 |
|
| 223 |
-
//
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
}
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
}
|
| 236 |
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
}
|
| 245 |
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
}
|
| 248 |
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
}
|
| 251 |
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
question.IsDoubt = req.IsDoubt
|
| 255 |
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
}
|
| 259 |
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
| 263 |
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
|
| 268 |
}
|
| 269 |
-
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 270 |
-
}
|
| 271 |
|
| 272 |
-
|
| 273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
-
func (s *quizService) UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error) {
|
| 276 |
-
attempt, err := s.quizRepository.UserGetAttemptByID(ctx, req.AttemptID)
|
| 277 |
if err != nil {
|
| 278 |
-
return nil,
|
| 279 |
}
|
| 280 |
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
}
|
| 284 |
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
| 286 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 287 |
}
|
| 288 |
|
| 289 |
-
|
|
|
|
| 290 |
if err != nil {
|
| 291 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 292 |
}
|
| 293 |
|
| 294 |
-
|
| 295 |
-
// hapus progress dan semua attempt pada quiz
|
| 296 |
-
if err := s.quizRepository.UserDeleteProgressAndAttempt(ctx, attempt.AccountID, attempt.QuizID); err != nil {
|
| 297 |
-
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 298 |
-
}
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
-
return &models.SubmitQuizResponse{
|
| 302 |
QuizAttempt: *attempt,
|
| 303 |
RemainingAttempts: remainingAttempts,
|
| 304 |
}, nil
|
| 305 |
}
|
| 306 |
|
| 307 |
func (s *quizService) UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error) {
|
| 308 |
-
|
|
|
|
| 309 |
if err != nil {
|
| 310 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 311 |
}
|
| 312 |
|
| 313 |
-
|
|
|
|
| 314 |
if err != nil {
|
| 315 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 316 |
}
|
|
|
|
| 5 |
"errors"
|
| 6 |
"time"
|
| 7 |
|
| 8 |
+
"api.qobiltu.id/config"
|
| 9 |
"api.qobiltu.id/models"
|
| 10 |
"api.qobiltu.id/pkg/validation"
|
| 11 |
"api.qobiltu.id/repositories"
|
|
|
|
| 23 |
UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
|
| 24 |
UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error)
|
| 25 |
UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
|
| 26 |
+
UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error)
|
| 27 |
UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
|
| 28 |
}
|
| 29 |
|
| 30 |
type quizService struct {
|
| 31 |
+
db *gorm.DB
|
| 32 |
quizRepository repositories.QuizRepository
|
| 33 |
academyRepository repositories.AcademyRepository
|
| 34 |
validator *validation.Validator
|
| 35 |
}
|
| 36 |
|
| 37 |
+
func NewQuizService(db *gorm.DB, quizRepository repositories.QuizRepository, academyRepository repositories.AcademyRepository, validator *validation.Validator) QuizService {
|
| 38 |
+
return &quizService{
|
| 39 |
+
db: db,
|
| 40 |
+
quizRepository: quizRepository,
|
| 41 |
+
academyRepository: academyRepository,
|
| 42 |
+
validator: validator,
|
| 43 |
+
}
|
| 44 |
}
|
| 45 |
|
| 46 |
func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
|
| 47 |
+
quiz, err := s.quizRepository.UserGetQuiz(ctx, s.db, req)
|
| 48 |
if err != nil {
|
| 49 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 50 |
}
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
|
| 56 |
+
// Fetch quiz data
|
| 57 |
+
quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, s.db, &models.UserGetQuizRequest{
|
| 58 |
AccountID: req.AccountID,
|
| 59 |
AcademyID: req.AcademyID,
|
| 60 |
})
|
|
|
|
| 64 |
|
| 65 |
quiz := &quizAttempt.Quiz
|
| 66 |
|
| 67 |
+
// check is already passed
|
| 68 |
+
isPassedQuiz, err := s.quizRepository.UserIsPassedQuiz(ctx, s.db, req.AccountID, quiz.ID)
|
| 69 |
+
if err != nil {
|
| 70 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
if isPassedQuiz {
|
| 74 |
+
return nil, models.Exception{QuizAlreadyPassed: true, Message: "You have already passed this quiz"}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Check for active attempt
|
| 78 |
+
existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, s.db, req.AccountID, quiz.ID)
|
| 79 |
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
| 80 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 81 |
}
|
| 82 |
|
| 83 |
+
// Handle existing active attempt
|
| 84 |
if existingAttempt != nil {
|
| 85 |
return s.handleActiveAttempt(ctx, req, quiz, existingAttempt)
|
| 86 |
}
|
| 87 |
|
| 88 |
+
// Handle new attempt
|
| 89 |
return s.handleNewAttempt(ctx, req, quiz)
|
| 90 |
}
|
| 91 |
|
| 92 |
func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
|
| 93 |
now := time.Now()
|
| 94 |
|
| 95 |
+
// Check if attempt has expired
|
| 96 |
if attempt.DueAt.Before(now) && attempt.FinishedAt == nil {
|
|
|
|
| 97 |
return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
|
| 98 |
}
|
| 99 |
|
| 100 |
+
// Fetch questions for active attempt
|
| 101 |
+
questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, s.db, req.AccountID, quiz.ID, attempt.ID)
|
| 102 |
if err != nil {
|
| 103 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 104 |
}
|
|
|
|
| 118 |
}
|
| 119 |
|
| 120 |
func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
|
| 121 |
+
var result *models.UserAttemptQuizResponse
|
| 122 |
|
| 123 |
+
err := config.RunTx(ctx, s.db, func(tx *gorm.DB) error {
|
| 124 |
+
// Check total attempts
|
| 125 |
+
totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, tx, req.AccountID, quiz.ID)
|
| 126 |
+
if err != nil {
|
| 127 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 128 |
+
}
|
| 129 |
+
if totalAttempts >= quiz.AttemptLimit {
|
| 130 |
+
return models.Exception{QuizAttemptLimit: true, Message: "Attempt limit reached"}
|
| 131 |
+
}
|
| 132 |
|
| 133 |
+
// Check academy progress
|
| 134 |
+
percentage, err := s.academyRepository.UserGetPercentageProgressAcademyByID(ctx, tx, req.AccountID, req.AcademyID)
|
| 135 |
+
if err != nil {
|
| 136 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 137 |
+
}
|
| 138 |
+
if percentage < 100 {
|
| 139 |
+
return models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
|
| 140 |
+
}
|
| 141 |
|
| 142 |
+
// Create new attempt
|
| 143 |
+
attempt := models.QuizAttempt{
|
| 144 |
+
AccountID: req.AccountID,
|
| 145 |
+
QuizID: quiz.ID,
|
| 146 |
+
AcademyID: quiz.AcademyID,
|
| 147 |
+
StartedAt: time.Now(),
|
| 148 |
+
DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
|
| 149 |
+
}
|
| 150 |
|
| 151 |
+
if err := s.quizRepository.UserCreateAttemptQuiz(ctx, tx, &attempt); err != nil {
|
| 152 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// Fetch questions
|
| 156 |
+
questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, tx, req.AccountID, quiz.ID, attempt.ID)
|
| 157 |
+
if err != nil {
|
| 158 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
questions = utils.ShuffleWithKey(questions, attempt.ID)
|
| 162 |
+
|
| 163 |
+
result = &models.UserAttemptQuizResponse{
|
| 164 |
+
ID: attempt.ID,
|
| 165 |
+
AccountID: attempt.AccountID,
|
| 166 |
+
QuizID: attempt.QuizID,
|
| 167 |
+
StartedAt: attempt.StartedAt,
|
| 168 |
+
DueAt: attempt.DueAt,
|
| 169 |
+
FinishedAt: attempt.FinishedAt,
|
| 170 |
+
Score: attempt.Score,
|
| 171 |
+
Questions: questions,
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
return nil
|
| 175 |
+
})
|
| 176 |
|
|
|
|
|
|
|
| 177 |
if err != nil {
|
| 178 |
+
return nil, models.Exception{InternalServerError: true, Message: "Internal Server Error"}
|
| 179 |
}
|
| 180 |
|
| 181 |
+
return result, nil
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
}
|
| 183 |
|
| 184 |
func (s *quizService) calculateQuizScore(
|
| 185 |
ctx context.Context,
|
| 186 |
+
tx *gorm.DB,
|
| 187 |
quiz *models.Quiz,
|
| 188 |
attempt *models.QuizAttempt,
|
| 189 |
) error {
|
| 190 |
+
// Fetch total questions
|
| 191 |
+
totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, tx, attempt.QuizID)
|
| 192 |
if err != nil {
|
| 193 |
return err
|
| 194 |
}
|
|
|
|
| 202 |
return nil
|
| 203 |
}
|
| 204 |
|
| 205 |
+
// Fetch total correct answers
|
| 206 |
+
correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, tx, attempt.ID)
|
| 207 |
if err != nil {
|
| 208 |
return err
|
| 209 |
}
|
| 210 |
|
| 211 |
+
// Calculate score
|
| 212 |
score := float64(correctAnswer) / float64(totalQuestion) * 100
|
|
|
|
| 213 |
attempt.Score = score
|
| 214 |
attempt.TotalQuestions = totalQuestion
|
| 215 |
attempt.TotalCorrectAnswer = correctAnswer
|
|
|
|
| 223 |
}
|
| 224 |
|
| 225 |
func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error) {
|
| 226 |
+
// Check if attempt exists
|
| 227 |
+
_, err := s.quizRepository.UserGetAttemptByID(ctx, s.db, req.AttemptID)
|
| 228 |
if err != nil {
|
| 229 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 230 |
}
|
| 231 |
|
| 232 |
+
// Fetch question and user answer
|
| 233 |
+
question, err := s.quizRepository.UserGetQuestionQuiz(ctx, s.db, req.AttemptID, req.QuestionID)
|
| 234 |
if err != nil {
|
| 235 |
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 236 |
return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
|
|
|
|
| 242 |
}
|
| 243 |
|
| 244 |
func (s *quizService) UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error) {
|
| 245 |
+
var result *models.GetQuestionQuizResponse
|
| 246 |
|
| 247 |
+
err := config.RunTx(ctx, s.db, func(tx *gorm.DB) error {
|
| 248 |
+
// Fetch correct option ID
|
| 249 |
+
correctOptionID, err := s.quizRepository.GetCorrectOptionID(ctx, tx, req.QuestionID)
|
| 250 |
+
if err != nil {
|
| 251 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 252 |
+
}
|
|
|
|
| 253 |
|
| 254 |
+
// Check if user answer exists
|
| 255 |
+
question, err := s.quizRepository.UserGetUserAnswer(ctx, tx, req.AttemptID, req.QuestionID)
|
| 256 |
+
if err != nil {
|
| 257 |
+
// If no answer exists, create a new one
|
| 258 |
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 259 |
+
userAnswer := models.UserAnswer{
|
| 260 |
+
QuizAttemptID: req.AttemptID,
|
| 261 |
+
QuestionID: req.QuestionID,
|
| 262 |
+
AnswerID: req.AnswerID,
|
| 263 |
+
IsDoubt: req.IsDoubt,
|
| 264 |
+
IsCorrect: false,
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
if req.AnswerID != nil {
|
| 268 |
+
userAnswer.IsCorrect = *req.AnswerID == correctOptionID
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
if err := s.quizRepository.UserSaveUserAnswer(ctx, tx, &userAnswer); err != nil {
|
| 272 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
res, err := s.quizRepository.UserGetQuestionQuiz(ctx, tx, req.AttemptID, req.QuestionID)
|
| 276 |
+
if err != nil {
|
| 277 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
result = res
|
| 281 |
+
return nil
|
| 282 |
}
|
| 283 |
|
| 284 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 285 |
+
}
|
|
|
|
| 286 |
|
| 287 |
+
// Update existing answer
|
| 288 |
+
question.AnswerID = req.AnswerID
|
| 289 |
+
question.IsDoubt = req.IsDoubt
|
| 290 |
+
if req.AnswerID != nil {
|
| 291 |
+
question.IsCorrect = *req.AnswerID == correctOptionID
|
| 292 |
+
}
|
| 293 |
|
| 294 |
+
if err := s.quizRepository.UserSaveUserAnswer(ctx, tx, question); err != nil {
|
| 295 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 296 |
+
}
|
|
|
|
| 297 |
|
| 298 |
+
res, err := s.quizRepository.UserGetQuestionQuiz(ctx, tx, req.AttemptID, req.QuestionID)
|
| 299 |
+
if err != nil {
|
| 300 |
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 301 |
+
return models.Exception{DataNotFound: true, Message: "Question not found"}
|
| 302 |
+
}
|
| 303 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 304 |
}
|
| 305 |
|
| 306 |
+
result = res
|
| 307 |
+
return nil
|
| 308 |
+
})
|
| 309 |
+
|
| 310 |
+
if err != nil {
|
| 311 |
+
return nil, err
|
| 312 |
}
|
| 313 |
|
| 314 |
+
return result, nil
|
| 315 |
+
}
|
|
|
|
| 316 |
|
| 317 |
+
func (s *quizService) UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error) {
|
| 318 |
+
var result *models.SubmitQuizResponse
|
|
|
|
| 319 |
|
| 320 |
+
err := config.RunTx(ctx, s.db, func(tx *gorm.DB) error {
|
| 321 |
+
// Fetch attempt
|
| 322 |
+
attempt, err := s.quizRepository.UserGetAttemptByID(ctx, tx, req.AttemptID)
|
| 323 |
+
if err != nil {
|
| 324 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 325 |
+
}
|
| 326 |
|
| 327 |
+
// Calculate score
|
| 328 |
+
if err := s.calculateQuizScore(ctx, tx, attempt.Quiz, attempt); err != nil {
|
| 329 |
+
return response.HandleGormError(err, "Internal Server Error")
|
|
|
|
| 330 |
}
|
|
|
|
|
|
|
| 331 |
|
| 332 |
+
// Update attempt
|
| 333 |
+
if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, tx, attempt); err != nil {
|
| 334 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
// Get remaining attempts
|
| 338 |
+
remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, tx, attempt.AccountID, attempt.AcademyID)
|
| 339 |
+
if err != nil {
|
| 340 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// If no remaining attempts, delete progress and attempts
|
| 344 |
+
if remainingAttempts == 0 {
|
| 345 |
+
if err := s.quizRepository.UserDeleteProgressAndAttempt(ctx, tx, attempt.AccountID, attempt.AcademyID); err != nil {
|
| 346 |
+
return response.HandleGormError(err, "Internal Server Error")
|
| 347 |
+
}
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
result = &models.SubmitQuizResponse{
|
| 351 |
+
QuizAttempt: *attempt,
|
| 352 |
+
RemainingAttempts: remainingAttempts,
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
return nil
|
| 356 |
+
})
|
| 357 |
|
|
|
|
|
|
|
| 358 |
if err != nil {
|
| 359 |
+
return nil, err
|
| 360 |
}
|
| 361 |
|
| 362 |
+
return result, nil
|
| 363 |
+
}
|
|
|
|
| 364 |
|
| 365 |
+
func (s *quizService) UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error) {
|
| 366 |
+
// Fetch last attempt
|
| 367 |
+
attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, s.db, req.AccountID, req.AcademyID)
|
| 368 |
+
if err != nil {
|
| 369 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 370 |
}
|
| 371 |
|
| 372 |
+
// Get remaining attempts
|
| 373 |
+
remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, s.db, attempt.AccountID, attempt.AcademyID)
|
| 374 |
if err != nil {
|
| 375 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 376 |
}
|
| 377 |
|
| 378 |
+
return &models.ResultQuizResponse{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
QuizAttempt: *attempt,
|
| 380 |
RemainingAttempts: remainingAttempts,
|
| 381 |
}, nil
|
| 382 |
}
|
| 383 |
|
| 384 |
func (s *quizService) UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error) {
|
| 385 |
+
// Fetch last attempt
|
| 386 |
+
attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, s.db, req.AccountID, req.AcademyID)
|
| 387 |
if err != nil {
|
| 388 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 389 |
}
|
| 390 |
|
| 391 |
+
// Fetch review
|
| 392 |
+
review, err := s.quizRepository.UserGetReviewQuiz(ctx, s.db, attempt.ID, attempt.AccountID, attempt.QuizID)
|
| 393 |
if err != nil {
|
| 394 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 395 |
}
|
space/space/space/repositories/quiz_repository.go
CHANGED
|
@@ -65,6 +65,7 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
|
|
| 65 |
quiz_attempts qa
|
| 66 |
ON q.id = qa.quiz_id
|
| 67 |
AND qa.account_id = @accountID
|
|
|
|
| 68 |
WHERE
|
| 69 |
q.academy_id = @academyID
|
| 70 |
GROUP BY
|
|
@@ -77,7 +78,7 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
|
|
| 77 |
"academyID": req.AcademyID,
|
| 78 |
}).Scan(&quizResponse).Error
|
| 79 |
if err != nil {
|
| 80 |
-
return nil, err
|
| 81 |
}
|
| 82 |
|
| 83 |
if quizResponse.ID == 0 {
|
|
|
|
| 65 |
quiz_attempts qa
|
| 66 |
ON q.id = qa.quiz_id
|
| 67 |
AND qa.account_id = @accountID
|
| 68 |
+
AND qa.finished_at IS NOT NULL
|
| 69 |
WHERE
|
| 70 |
q.academy_id = @academyID
|
| 71 |
GROUP BY
|
|
|
|
| 78 |
"academyID": req.AcademyID,
|
| 79 |
}).Scan(&quizResponse).Error
|
| 80 |
if err != nil {
|
| 81 |
+
return nil, fmt.Errorf("failed to query quiz: %w", err)
|
| 82 |
}
|
| 83 |
|
| 84 |
if quizResponse.ID == 0 {
|
space/space/space/space/models/field_counter.go
CHANGED
|
@@ -164,7 +164,7 @@ func isFieldFilled(field reflect.Value) bool {
|
|
| 164 |
case reflect.Float32, reflect.Float64:
|
| 165 |
return field.Float() != 0
|
| 166 |
case reflect.Bool:
|
| 167 |
-
return
|
| 168 |
case reflect.Slice, reflect.Map, reflect.Array:
|
| 169 |
return !field.IsNil() && field.Len() > 0
|
| 170 |
case reflect.Struct:
|
|
|
|
| 164 |
case reflect.Float32, reflect.Float64:
|
| 165 |
return field.Float() != 0
|
| 166 |
case reflect.Bool:
|
| 167 |
+
return true
|
| 168 |
case reflect.Slice, reflect.Map, reflect.Array:
|
| 169 |
return !field.IsNil() && field.Len() > 0
|
| 170 |
case reflect.Struct:
|
space/space/space/space/pkg/validation/validation.go
CHANGED
|
@@ -21,9 +21,9 @@ func New(db *gorm.DB) (*Validator, error) {
|
|
| 21 |
return nil, err
|
| 22 |
}
|
| 23 |
|
| 24 |
-
// Start background refresh every
|
| 25 |
ctx := context.Background()
|
| 26 |
-
dbSource.StartAutoRefresh(ctx,
|
| 27 |
|
| 28 |
// create validator
|
| 29 |
validator := NewValidator(dbSource)
|
|
|
|
| 21 |
return nil, err
|
| 22 |
}
|
| 23 |
|
| 24 |
+
// Start background refresh every 5 minutes
|
| 25 |
ctx := context.Background()
|
| 26 |
+
dbSource.StartAutoRefresh(ctx, 5*time.Minute)
|
| 27 |
|
| 28 |
// create validator
|
| 29 |
validator := NewValidator(dbSource)
|
space/space/space/space/services/cv_service.go
CHANGED
|
@@ -192,6 +192,9 @@ func (s *cvService) SavePersonalityAndPreference(ctx context.Context, req *model
|
|
| 192 |
func (s *cvService) GetPersonalityAndPreference(ctx context.Context, req *models.GetPersonalityAndPreferenceRequest) (*models.PersonalityAndPreferenceCV, error) {
|
| 193 |
res, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, req.AccountID)
|
| 194 |
if err != nil {
|
|
|
|
|
|
|
|
|
|
| 195 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 196 |
}
|
| 197 |
|
|
@@ -312,7 +315,9 @@ func (s *cvService) SavePhysicalAndHealth(ctx context.Context, req *models.SaveP
|
|
| 312 |
func (s *cvService) GetPhysicalAndHealth(ctx context.Context, req *models.GetPhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error) {
|
| 313 |
res, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, req.AccountID)
|
| 314 |
if err != nil {
|
| 315 |
-
|
|
|
|
|
|
|
| 316 |
}
|
| 317 |
return res, nil
|
| 318 |
}
|
|
@@ -369,7 +374,7 @@ func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, re
|
|
| 369 |
func (s *cvService) GetWorshipAndReligiousUnderstanding(ctx context.Context, req *models.GetWorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
|
| 370 |
res, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, req.AccountID)
|
| 371 |
if err != nil {
|
| 372 |
-
return
|
| 373 |
}
|
| 374 |
return res, nil
|
| 375 |
}
|
|
|
|
| 192 |
func (s *cvService) GetPersonalityAndPreference(ctx context.Context, req *models.GetPersonalityAndPreferenceRequest) (*models.PersonalityAndPreferenceCV, error) {
|
| 193 |
res, err := s.cvRepository.GetPersonalityAndPreferenceByAccountID(ctx, req.AccountID)
|
| 194 |
if err != nil {
|
| 195 |
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 196 |
+
return &models.PersonalityAndPreferenceCV{}, nil
|
| 197 |
+
}
|
| 198 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 199 |
}
|
| 200 |
|
|
|
|
| 315 |
func (s *cvService) GetPhysicalAndHealth(ctx context.Context, req *models.GetPhysicalAndHealthRequest) (*models.PhysicalAndHealthCV, error) {
|
| 316 |
res, err := s.cvRepository.GetPhysicalAndHealthByAccountID(ctx, req.AccountID)
|
| 317 |
if err != nil {
|
| 318 |
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 319 |
+
return &models.PhysicalAndHealthCV{}, nil
|
| 320 |
+
}
|
| 321 |
}
|
| 322 |
return res, nil
|
| 323 |
}
|
|
|
|
| 374 |
func (s *cvService) GetWorshipAndReligiousUnderstanding(ctx context.Context, req *models.GetWorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
|
| 375 |
res, err := s.cvRepository.GetWorshipAndReligiousUnderstandingByAccountID(ctx, req.AccountID)
|
| 376 |
if err != nil {
|
| 377 |
+
return &models.WorshipAndReligiousUnderstandingCV{}, nil
|
| 378 |
}
|
| 379 |
return res, nil
|
| 380 |
}
|
space/space/space/space/space/controller/quiz/quiz_controller.go
CHANGED
|
@@ -17,6 +17,10 @@ type QuizController interface {
|
|
| 17 |
// === USER ===
|
| 18 |
UserGetQuiz(ctx *gin.Context)
|
| 19 |
UserAttemptQuiz(ctx *gin.Context)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
type quizController struct {
|
|
@@ -30,7 +34,7 @@ func NewQuizController(quizService services.QuizService) QuizController {
|
|
| 30 |
}
|
| 31 |
|
| 32 |
func (c *quizController) UserGetQuiz(ctx *gin.Context) {
|
| 33 |
-
academyID := ctx.
|
| 34 |
academyIDInt, err := strconv.Atoi(academyID)
|
| 35 |
if err != nil {
|
| 36 |
response.HandleError(ctx, err)
|
|
@@ -38,6 +42,7 @@ func (c *quizController) UserGetQuiz(ctx *gin.Context) {
|
|
| 38 |
}
|
| 39 |
|
| 40 |
accountData := middleware.GetAccountData(ctx)
|
|
|
|
| 41 |
req := models.UserGetQuizRequest{
|
| 42 |
AccountID: int64(accountData.UserID),
|
| 43 |
AcademyID: int64(academyIDInt),
|
|
@@ -49,11 +54,11 @@ func (c *quizController) UserGetQuiz(ctx *gin.Context) {
|
|
| 49 |
return
|
| 50 |
}
|
| 51 |
|
| 52 |
-
response.HandleSuccess(ctx, http.StatusOK, "Quiz retrieved successfully",
|
| 53 |
}
|
| 54 |
|
| 55 |
func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
|
| 56 |
-
academyID := ctx.
|
| 57 |
academyIDInt, err := strconv.Atoi(academyID)
|
| 58 |
if err != nil {
|
| 59 |
response.HandleError(ctx, err)
|
|
@@ -61,6 +66,7 @@ func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
|
|
| 61 |
}
|
| 62 |
|
| 63 |
accountData := middleware.GetAccountData(ctx)
|
|
|
|
| 64 |
req := models.UserAttemptQuizRequest{
|
| 65 |
AccountID: int64(accountData.UserID),
|
| 66 |
AcademyID: int64(academyIDInt),
|
|
@@ -72,5 +78,137 @@ func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
|
|
| 72 |
return
|
| 73 |
}
|
| 74 |
|
| 75 |
-
response.HandleSuccess(ctx, http.StatusOK, "Quiz attempt retrieved successfully",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
}
|
|
|
|
| 17 |
// === USER ===
|
| 18 |
UserGetQuiz(ctx *gin.Context)
|
| 19 |
UserAttemptQuiz(ctx *gin.Context)
|
| 20 |
+
UserGetQuestionQuiz(ctx *gin.Context)
|
| 21 |
+
UserAnswerQuiz(ctx *gin.Context)
|
| 22 |
+
UserSubmitQuiz(ctx *gin.Context)
|
| 23 |
+
UserReviewQuiz(ctx *gin.Context)
|
| 24 |
}
|
| 25 |
|
| 26 |
type quizController struct {
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
func (c *quizController) UserGetQuiz(ctx *gin.Context) {
|
| 37 |
+
academyID := ctx.Query("academy_id")
|
| 38 |
academyIDInt, err := strconv.Atoi(academyID)
|
| 39 |
if err != nil {
|
| 40 |
response.HandleError(ctx, err)
|
|
|
|
| 42 |
}
|
| 43 |
|
| 44 |
accountData := middleware.GetAccountData(ctx)
|
| 45 |
+
|
| 46 |
req := models.UserGetQuizRequest{
|
| 47 |
AccountID: int64(accountData.UserID),
|
| 48 |
AcademyID: int64(academyIDInt),
|
|
|
|
| 54 |
return
|
| 55 |
}
|
| 56 |
|
| 57 |
+
response.HandleSuccess(ctx, http.StatusOK, "Quiz retrieved successfully", res, nil)
|
| 58 |
}
|
| 59 |
|
| 60 |
func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
|
| 61 |
+
academyID := ctx.Query("academy_id")
|
| 62 |
academyIDInt, err := strconv.Atoi(academyID)
|
| 63 |
if err != nil {
|
| 64 |
response.HandleError(ctx, err)
|
|
|
|
| 66 |
}
|
| 67 |
|
| 68 |
accountData := middleware.GetAccountData(ctx)
|
| 69 |
+
|
| 70 |
req := models.UserAttemptQuizRequest{
|
| 71 |
AccountID: int64(accountData.UserID),
|
| 72 |
AcademyID: int64(academyIDInt),
|
|
|
|
| 78 |
return
|
| 79 |
}
|
| 80 |
|
| 81 |
+
response.HandleSuccess(ctx, http.StatusOK, "Quiz attempt retrieved successfully", res, nil)
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
func (c *quizController) UserGetQuestionQuiz(ctx *gin.Context) {
|
| 85 |
+
academyID := ctx.Query("academy_id")
|
| 86 |
+
academyIDInt, err := strconv.Atoi(academyID)
|
| 87 |
+
if err != nil {
|
| 88 |
+
response.HandleError(ctx, err)
|
| 89 |
+
return
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
questionID := ctx.Query("question_id")
|
| 93 |
+
questionIDInt, err := strconv.Atoi(questionID)
|
| 94 |
+
if err != nil {
|
| 95 |
+
response.HandleError(ctx, err)
|
| 96 |
+
return
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
attemptID := ctx.Query("attempt_id")
|
| 100 |
+
attemptIDInt, err := strconv.Atoi(attemptID)
|
| 101 |
+
if err != nil {
|
| 102 |
+
response.HandleError(ctx, err)
|
| 103 |
+
return
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
accountData := middleware.GetAccountData(ctx)
|
| 107 |
+
|
| 108 |
+
req := models.GetQuestionQuizRequest{
|
| 109 |
+
AccountID: int64(accountData.UserID),
|
| 110 |
+
AcademyID: int64(academyIDInt),
|
| 111 |
+
QuestionID: int64(questionIDInt),
|
| 112 |
+
AttemptID: int64(attemptIDInt),
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
res, err := c.quizService.UserGetQuestionQuiz(ctx, &req)
|
| 116 |
+
if err != nil {
|
| 117 |
+
response.HandleError(ctx, err)
|
| 118 |
+
return
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
response.HandleSuccess(ctx, http.StatusOK, "Question retrieved successfully", res, nil)
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
func (c *quizController) UserAnswerQuiz(ctx *gin.Context) {
|
| 125 |
+
var req models.AnswerQuizRequest
|
| 126 |
+
if err := ctx.ShouldBindJSON(&req); err != nil {
|
| 127 |
+
response.HandleError(ctx, err)
|
| 128 |
+
return
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
academyID := ctx.Query("academy_id")
|
| 132 |
+
academyIDInt, err := strconv.Atoi(academyID)
|
| 133 |
+
if err != nil {
|
| 134 |
+
response.HandleError(ctx, err)
|
| 135 |
+
return
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
attemptID := ctx.Query("attempt_id")
|
| 139 |
+
attemptIDInt, err := strconv.Atoi(attemptID)
|
| 140 |
+
if err != nil {
|
| 141 |
+
response.HandleError(ctx, err)
|
| 142 |
+
return
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
accountData := middleware.GetAccountData(ctx)
|
| 146 |
+
|
| 147 |
+
req.AccountID = int64(accountData.UserID)
|
| 148 |
+
req.AcademyID = int64(academyIDInt)
|
| 149 |
+
req.AttemptID = int64(attemptIDInt)
|
| 150 |
+
|
| 151 |
+
res, err := c.quizService.UserAnswerQuiz(ctx, &req)
|
| 152 |
+
if err != nil {
|
| 153 |
+
response.HandleError(ctx, err)
|
| 154 |
+
return
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
response.HandleSuccess(ctx, http.StatusOK, "Question retrieved successfully", res, nil)
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
func (c *quizController) UserSubmitQuiz(ctx *gin.Context) {
|
| 161 |
+
academyID := ctx.Query("academy_id")
|
| 162 |
+
academyIDInt, err := strconv.Atoi(academyID)
|
| 163 |
+
if err != nil {
|
| 164 |
+
response.HandleError(ctx, err)
|
| 165 |
+
return
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
attemptID := ctx.Query("attempt_id")
|
| 169 |
+
attemptIDInt, err := strconv.Atoi(attemptID)
|
| 170 |
+
if err != nil {
|
| 171 |
+
response.HandleError(ctx, err)
|
| 172 |
+
return
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
accountData := middleware.GetAccountData(ctx)
|
| 176 |
+
|
| 177 |
+
req := models.SubmitQuizRequest{
|
| 178 |
+
AccountID: int64(accountData.UserID),
|
| 179 |
+
AcademyID: int64(academyIDInt),
|
| 180 |
+
AttemptID: int64(attemptIDInt),
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
res, err := c.quizService.UserSubmitQuiz(ctx, &req)
|
| 184 |
+
if err != nil {
|
| 185 |
+
response.HandleError(ctx, err)
|
| 186 |
+
return
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
response.HandleSuccess(ctx, http.StatusOK, "Quiz submitted successfully", res, nil)
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
func (c *quizController) UserReviewQuiz(ctx *gin.Context) {
|
| 193 |
+
academyID := ctx.Query("academy_id")
|
| 194 |
+
academyIDInt, err := strconv.Atoi(academyID)
|
| 195 |
+
if err != nil {
|
| 196 |
+
response.HandleError(ctx, err)
|
| 197 |
+
return
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
accountData := middleware.GetAccountData(ctx)
|
| 201 |
+
|
| 202 |
+
req := models.ReviewQuizRequest{
|
| 203 |
+
AccountID: int64(accountData.UserID),
|
| 204 |
+
AcademyID: int64(academyIDInt),
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
res, err := c.quizService.UserReviewQuiz(ctx, &req)
|
| 208 |
+
if err != nil {
|
| 209 |
+
response.HandleError(ctx, err)
|
| 210 |
+
return
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
response.HandleSuccess(ctx, http.StatusOK, "Quiz review retrieved successfully", res.Reviews, nil)
|
| 214 |
}
|
space/space/space/space/space/models/database_orm_model.go
CHANGED
|
@@ -156,7 +156,7 @@ type (
|
|
| 156 |
Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"quiz,omitempty"`
|
| 157 |
Content string `gorm:"column:content" json:"content"`
|
| 158 |
Order int `gorm:"column:order" json:"order"`
|
| 159 |
-
Review string `gorm:"column:
|
| 160 |
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
| 161 |
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
| 162 |
}
|
|
@@ -172,17 +172,23 @@ type (
|
|
| 172 |
}
|
| 173 |
|
| 174 |
QuizAttempt struct {
|
| 175 |
-
ID
|
| 176 |
-
AccountID
|
| 177 |
-
Account
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
}
|
| 187 |
|
| 188 |
UserAnswer struct {
|
|
@@ -193,7 +199,7 @@ type (
|
|
| 193 |
Question *Question `gorm:"foreignKey:QuestionID;constraint:OnDelete:CASCADE" json:"question,omitempty"`
|
| 194 |
AnswerID *int64 `gorm:"column:selected_answer_id" json:"selected_answer"`
|
| 195 |
Answer *Answer `gorm:"foreignKey:AnswerID;constraint:OnDelete:CASCADE" json:"answer,omitempty"`
|
| 196 |
-
IsDoubt bool `gorm:"column:
|
| 197 |
IsCorrect bool `gorm:"column:is_correct" json:"is_correct"`
|
| 198 |
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
| 199 |
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
|
|
|
| 156 |
Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"quiz,omitempty"`
|
| 157 |
Content string `gorm:"column:content" json:"content"`
|
| 158 |
Order int `gorm:"column:order" json:"order"`
|
| 159 |
+
Review string `gorm:"column:review" json:"review"`
|
| 160 |
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
| 161 |
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
| 162 |
}
|
|
|
|
| 172 |
}
|
| 173 |
|
| 174 |
QuizAttempt struct {
|
| 175 |
+
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
| 176 |
+
AccountID int64 `gorm:"column:account_id;not null" json:"account_id"`
|
| 177 |
+
Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"`
|
| 178 |
+
AcademyID int64 `gorm:"column:academy_id;not null" json:"academy_id"`
|
| 179 |
+
Academy *Academy `gorm:"foreignKey:AcademyID;constraint:OnDelete:CASCADE" json:"academy,omitempty"`
|
| 180 |
+
QuizID int64 `gorm:"column:quiz_id;not null" json:"quiz_id"`
|
| 181 |
+
Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"-"`
|
| 182 |
+
StartedAt time.Time `gorm:"column:started_at;autoCreateTime" json:"started_at"`
|
| 183 |
+
DueAt time.Time `gorm:"column:due_at" json:"due_at"`
|
| 184 |
+
FinishedAt *time.Time `gorm:"column:finished_at" json:"finished_at"`
|
| 185 |
+
TotalQuestions int64 `gorm:"column:total_questions;not null" json:"total_questions"`
|
| 186 |
+
TotalCorrectAnswer int64 `gorm:"column:total_correct_answer;not null" json:"total_correct_answer"`
|
| 187 |
+
TotalWrongAnswer int64 `gorm:"column:total_wrong_answer;not null" json:"total_wrong_answer"`
|
| 188 |
+
Score float64 `gorm:"column:score" json:"score"`
|
| 189 |
+
IsPassed bool `gorm:"column:is_passed" json:"is_passed"`
|
| 190 |
+
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
| 191 |
+
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
| 192 |
}
|
| 193 |
|
| 194 |
UserAnswer struct {
|
|
|
|
| 199 |
Question *Question `gorm:"foreignKey:QuestionID;constraint:OnDelete:CASCADE" json:"question,omitempty"`
|
| 200 |
AnswerID *int64 `gorm:"column:selected_answer_id" json:"selected_answer"`
|
| 201 |
Answer *Answer `gorm:"foreignKey:AnswerID;constraint:OnDelete:CASCADE" json:"answer,omitempty"`
|
| 202 |
+
IsDoubt bool `gorm:"column:is_doubt" json:"is_doubt"`
|
| 203 |
IsCorrect bool `gorm:"column:is_correct" json:"is_correct"`
|
| 204 |
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
| 205 |
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
space/space/space/space/space/models/request_model.go
CHANGED
|
@@ -152,6 +152,7 @@ func NewListAcademyContentRequest() ListAcademyContentRequest {
|
|
| 152 |
}
|
| 153 |
|
| 154 |
type (
|
|
|
|
| 155 |
UserGetQuizRequest struct {
|
| 156 |
AccountID int64 `json:"account_id" validate:"required"`
|
| 157 |
AcademyID int64 `json:"academy_id" validate:"required"`
|
|
@@ -159,36 +160,105 @@ type (
|
|
| 159 |
|
| 160 |
UserGetQuizResponse struct {
|
| 161 |
Quiz
|
| 162 |
-
TotalQuestions
|
| 163 |
-
UserAttempts
|
|
|
|
| 164 |
}
|
| 165 |
|
|
|
|
| 166 |
UserAttemptQuizRequest struct {
|
| 167 |
-
AccountID int64 `json:"account_id"`
|
| 168 |
-
AcademyID int64 `json:"academy_id"`
|
| 169 |
}
|
| 170 |
|
| 171 |
UserAttemptQuizQuestionsResponse struct {
|
| 172 |
-
ID int64 `
|
| 173 |
-
IsDoubt bool `
|
| 174 |
-
IsAnswered bool `
|
| 175 |
}
|
| 176 |
|
| 177 |
UserAttemptQuizResponse struct {
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
}
|
| 181 |
|
|
|
|
| 182 |
GetQuestionQuizRequest struct {
|
| 183 |
-
AccountID
|
| 184 |
-
|
|
|
|
|
|
|
| 185 |
}
|
| 186 |
|
| 187 |
GetQuestionQuizResponse struct {
|
| 188 |
-
Question
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
}
|
| 193 |
)
|
| 194 |
|
|
@@ -239,11 +309,6 @@ type QuestionQuizRequest struct {
|
|
| 239 |
QuestionNo int `json:"question_no" binding:"required"`
|
| 240 |
}
|
| 241 |
|
| 242 |
-
type AnswerQuizRequest struct {
|
| 243 |
-
QuestionNo int `json:"question_no" binding:"required"`
|
| 244 |
-
Answer int `json:"answer" binding:"required"`
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
type (
|
| 248 |
ListCitiesByProvinceIdRequest struct {
|
| 249 |
ProvinceID int64 `json:"province_id" binding:"required"`
|
|
|
|
| 152 |
}
|
| 153 |
|
| 154 |
type (
|
| 155 |
+
// GET QUIZ
|
| 156 |
UserGetQuizRequest struct {
|
| 157 |
AccountID int64 `json:"account_id" validate:"required"`
|
| 158 |
AcademyID int64 `json:"academy_id" validate:"required"`
|
|
|
|
| 160 |
|
| 161 |
UserGetQuizResponse struct {
|
| 162 |
Quiz
|
| 163 |
+
TotalQuestions int64 `json:"total_questions"`
|
| 164 |
+
UserAttempts int64 `json:"user_attempts"`
|
| 165 |
+
HasActiveAttempt bool `json:"has_active_attempt"`
|
| 166 |
}
|
| 167 |
|
| 168 |
+
// ATTEMP QUIZ
|
| 169 |
UserAttemptQuizRequest struct {
|
| 170 |
+
AccountID int64 `json:"account_id" validate:"required"`
|
| 171 |
+
AcademyID int64 `json:"academy_id" validate:"required"`
|
| 172 |
}
|
| 173 |
|
| 174 |
UserAttemptQuizQuestionsResponse struct {
|
| 175 |
+
ID int64 `json:"id"`
|
| 176 |
+
IsDoubt bool `json:"is_doubt"`
|
| 177 |
+
IsAnswered bool `json:"is_answered"`
|
| 178 |
}
|
| 179 |
|
| 180 |
UserAttemptQuizResponse struct {
|
| 181 |
+
ID int64 `json:"id"`
|
| 182 |
+
AccountID int64 `json:"account_id"`
|
| 183 |
+
QuizID int64 `json:"quiz_id"`
|
| 184 |
+
StartedAt time.Time `json:"started_at"`
|
| 185 |
+
DueAt time.Time `json:"due_at"`
|
| 186 |
+
FinishedAt *time.Time `json:"finished_at"`
|
| 187 |
+
Score float64 `json:"score"`
|
| 188 |
+
Questions []UserAttemptQuizQuestionsResponse `json:"questions"`
|
| 189 |
}
|
| 190 |
|
| 191 |
+
// GET QUESTION
|
| 192 |
GetQuestionQuizRequest struct {
|
| 193 |
+
AccountID int64 `json:"account_id" validate:"required"`
|
| 194 |
+
AcademyID int64 `json:"academy_id" validate:"required"`
|
| 195 |
+
AttemptID int64 `json:"attempt_id" validate:"required"`
|
| 196 |
+
QuestionID int64 `json:"question_id" validate:"required"`
|
| 197 |
}
|
| 198 |
|
| 199 |
GetQuestionQuizResponse struct {
|
| 200 |
+
Question struct {
|
| 201 |
+
ID int64 `json:"id"`
|
| 202 |
+
QuizID int64 `json:"quiz_id"`
|
| 203 |
+
Content string `json:"content"`
|
| 204 |
+
} `json:"question"`
|
| 205 |
+
AnswerOptions []struct {
|
| 206 |
+
ID int64 `json:"id"`
|
| 207 |
+
Content string `json:"content"`
|
| 208 |
+
} `json:"answer_options"`
|
| 209 |
+
AnswerID *int64 `json:"answer_id"`
|
| 210 |
+
IsDoubt bool `json:"is_doubt"`
|
| 211 |
+
IsAnswered bool `json:"is_answered"`
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
// ANSWER QUESTION
|
| 215 |
+
AnswerQuizRequest struct {
|
| 216 |
+
AccountID int64 `json:"account_id" validate:"required"`
|
| 217 |
+
AcademyID int64 `json:"academy_id" validate:"required"`
|
| 218 |
+
AttemptID int64 `json:"attempt_id" validate:"required"`
|
| 219 |
+
|
| 220 |
+
QuestionID int64 `json:"question_id" validate:"required"`
|
| 221 |
+
AnswerID *int64 `json:"answer_id"`
|
| 222 |
+
IsDoubt bool `json:"is_doubt"`
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
// SUBMIT QUIZ
|
| 226 |
+
SubmitQuizRequest struct {
|
| 227 |
+
AccountID int64 `json:"account_id" validate:"required"`
|
| 228 |
+
AcademyID int64 `json:"academy_id" validate:"required"`
|
| 229 |
+
AttemptID int64 `json:"attempt_id" validate:"required"`
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
SubmitQuizResponse struct {
|
| 233 |
+
QuizAttempt
|
| 234 |
+
RemainingAttempts int64 `json:"remaining_attempts" validate:"required"`
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
// REVIEW QUIZ
|
| 238 |
+
ReviewQuizRequest struct {
|
| 239 |
+
AccountID int64 `json:"account_id" validate:"required"`
|
| 240 |
+
AcademyID int64 `json:"academy_id" validate:"required"`
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
ReviewQuizQuestion struct {
|
| 244 |
+
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
| 245 |
+
QuizID int64 `gorm:"column:quiz_id;not null" json:"quiz_id"`
|
| 246 |
+
Content string `gorm:"column:content" json:"content"`
|
| 247 |
+
Review string `gorm:"column:review" json:"review"`
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
ReviewQuiz struct {
|
| 251 |
+
Question ReviewQuizQuestion `json:"question"`
|
| 252 |
+
AnswerOptions []struct {
|
| 253 |
+
ID int64 `json:"id"`
|
| 254 |
+
Content string `json:"content"`
|
| 255 |
+
IsCorrect bool `json:"is_correct"`
|
| 256 |
+
} `json:"answer_options"`
|
| 257 |
+
AnswerID int64 `json:"answer_id"`
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
ReviewQuizResponse struct {
|
| 261 |
+
Reviews []ReviewQuiz
|
| 262 |
}
|
| 263 |
)
|
| 264 |
|
|
|
|
| 309 |
QuestionNo int `json:"question_no" binding:"required"`
|
| 310 |
}
|
| 311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
type (
|
| 313 |
ListCitiesByProvinceIdRequest struct {
|
| 314 |
ProvinceID int64 `json:"province_id" binding:"required"`
|
space/space/space/space/space/repositories/quiz_repository.go
CHANGED
|
@@ -2,6 +2,8 @@ package repositories
|
|
| 2 |
|
| 3 |
import (
|
| 4 |
"context"
|
|
|
|
|
|
|
| 5 |
|
| 6 |
"api.qobiltu.id/models"
|
| 7 |
"gorm.io/gorm"
|
|
@@ -10,13 +12,25 @@ import (
|
|
| 10 |
type QuizRepository interface {
|
| 11 |
UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
|
| 12 |
UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error)
|
| 13 |
-
UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
|
| 14 |
-
UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error)
|
| 15 |
-
UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
|
| 16 |
UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error)
|
| 18 |
UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error)
|
| 19 |
UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
type quizRepository struct {
|
|
@@ -31,24 +45,37 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
|
|
| 31 |
var quizResponse models.UserGetQuizResponse
|
| 32 |
|
| 33 |
rawQuery := `
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
err := r.db.Raw(rawQuery,
|
|
|
|
|
|
|
|
|
|
| 52 |
if err != nil {
|
| 53 |
return nil, err
|
| 54 |
}
|
|
@@ -63,7 +90,7 @@ func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQui
|
|
| 63 |
func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error) {
|
| 64 |
var quizAttempt models.QuizAttempt
|
| 65 |
|
| 66 |
-
err := r.db.Where("account_id = ? AND quiz_id = ? AND finished_at IS NULL", accountID, quizID).First(&quizAttempt).Error
|
| 67 |
if err != nil {
|
| 68 |
return nil, err
|
| 69 |
}
|
|
@@ -71,8 +98,19 @@ func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID
|
|
| 71 |
return &quizAttempt, nil
|
| 72 |
}
|
| 73 |
|
| 74 |
-
func (r *quizRepository)
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
}
|
| 77 |
|
| 78 |
func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error) {
|
|
@@ -105,7 +143,7 @@ func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context
|
|
| 105 |
q.id;
|
| 106 |
`
|
| 107 |
|
| 108 |
-
err := r.db.Raw(rawQuery, attemptID, accountID, quizID, attemptID, quizID).Scan(&questions).Error
|
| 109 |
if err != nil {
|
| 110 |
return nil, err
|
| 111 |
}
|
|
@@ -113,19 +151,21 @@ func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context
|
|
| 113 |
return questions, nil
|
| 114 |
}
|
| 115 |
|
| 116 |
-
func (r *quizRepository)
|
| 117 |
-
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error) {
|
| 121 |
-
var totalAttempts int64
|
| 122 |
|
| 123 |
-
err := r.db.
|
|
|
|
|
|
|
| 124 |
if err != nil {
|
| 125 |
-
return
|
| 126 |
}
|
| 127 |
|
| 128 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
|
| 131 |
func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error) {
|
|
@@ -153,3 +193,205 @@ func (r *quizRepository) UserGetTotalCorrectAnswerQuiz(ctx context.Context, quiz
|
|
| 153 |
func (r *quizRepository) UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error {
|
| 154 |
return r.db.Where("account_id = ? AND quiz_id = ?", accountID, quizID).Delete(&models.QuizAttempt{}).Error
|
| 155 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
import (
|
| 4 |
"context"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"fmt"
|
| 7 |
|
| 8 |
"api.qobiltu.id/models"
|
| 9 |
"gorm.io/gorm"
|
|
|
|
| 12 |
type QuizRepository interface {
|
| 13 |
UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
|
| 14 |
UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error)
|
|
|
|
|
|
|
|
|
|
| 15 |
UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error)
|
| 16 |
+
UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
|
| 17 |
+
UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error)
|
| 18 |
+
|
| 19 |
+
UserGetAttemptByID(ctx context.Context, attemptID int64) (*models.QuizAttempt, error)
|
| 20 |
+
UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
|
| 21 |
+
|
| 22 |
+
UserGetReviewQuiz(ctx context.Context, attemptID, accountID, quizID int64) (*models.ReviewQuizResponse, error)
|
| 23 |
+
UseGetLastAttemptQuiz(ctx context.Context, accountID int64, academyID int64) (*models.QuizAttempt, error)
|
| 24 |
+
|
| 25 |
UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error)
|
| 26 |
UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error)
|
| 27 |
UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error
|
| 28 |
+
UserGetRemainingAttempts(ctx context.Context, accountID int64, quizID int64) (int64, error)
|
| 29 |
+
UserGetQuestionQuiz(ctx context.Context, attemptID, questionID int64) (*models.GetQuestionQuizResponse, error)
|
| 30 |
+
UserGetUserAnswer(ctx context.Context, attemptID, questionID int64) (*models.UserAnswer, error)
|
| 31 |
+
UserSaveUserAnswer(ctx context.Context, answer *models.UserAnswer) error
|
| 32 |
+
GetCorrectOptionID(ctx context.Context, questionID int64) (int64, error)
|
| 33 |
+
UserDeleteProgressAndAttempt(ctx context.Context, accountID int64, academyID int64) error
|
| 34 |
}
|
| 35 |
|
| 36 |
type quizRepository struct {
|
|
|
|
| 45 |
var quizResponse models.UserGetQuizResponse
|
| 46 |
|
| 47 |
rawQuery := `
|
| 48 |
+
SELECT
|
| 49 |
+
q.*,
|
| 50 |
+
(SELECT COUNT(*) FROM questions ques WHERE ques.quiz_id = q.id) AS total_questions,
|
| 51 |
+
COALESCE(COUNT(qa.id), 0) AS user_attempts,
|
| 52 |
+
CASE
|
| 53 |
+
WHEN EXISTS (
|
| 54 |
+
SELECT 1
|
| 55 |
+
FROM quiz_attempts qa2
|
| 56 |
+
WHERE qa2.quiz_id = q.id
|
| 57 |
+
AND qa2.account_id = @accountID
|
| 58 |
+
AND qa2.finished_at IS NULL
|
| 59 |
+
) THEN TRUE
|
| 60 |
+
ELSE FALSE
|
| 61 |
+
END AS has_active_attempt
|
| 62 |
+
FROM
|
| 63 |
+
quizzes q
|
| 64 |
+
LEFT JOIN
|
| 65 |
+
quiz_attempts qa
|
| 66 |
+
ON q.id = qa.quiz_id
|
| 67 |
+
AND qa.account_id = @accountID
|
| 68 |
+
WHERE
|
| 69 |
+
q.academy_id = @academyID
|
| 70 |
+
GROUP BY
|
| 71 |
+
q.id, q.academy_id, q.slug, q.title, q.description,
|
| 72 |
+
q.attempt_limit, q.time_limit, q.min_score, q.created_at, q.updated_at;
|
| 73 |
+
`
|
| 74 |
|
| 75 |
+
err := r.db.Debug().Raw(rawQuery, map[string]any{
|
| 76 |
+
"accountID": req.AccountID,
|
| 77 |
+
"academyID": req.AcademyID,
|
| 78 |
+
}).Scan(&quizResponse).Error
|
| 79 |
if err != nil {
|
| 80 |
return nil, err
|
| 81 |
}
|
|
|
|
| 90 |
func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error) {
|
| 91 |
var quizAttempt models.QuizAttempt
|
| 92 |
|
| 93 |
+
err := r.db.Debug().Where("account_id = ? AND quiz_id = ? AND finished_at IS NULL", accountID, quizID).First(&quizAttempt).Error
|
| 94 |
if err != nil {
|
| 95 |
return nil, err
|
| 96 |
}
|
|
|
|
| 98 |
return &quizAttempt, nil
|
| 99 |
}
|
| 100 |
|
| 101 |
+
func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error) {
|
| 102 |
+
var totalAttempts int64
|
| 103 |
+
|
| 104 |
+
err := r.db.Debug().Model(&models.QuizAttempt{}).Where("account_id = ? AND quiz_id = ? AND finished_at IS NOT NULL", accountID, quizID).Count(&totalAttempts).Error
|
| 105 |
+
if err != nil {
|
| 106 |
+
return 0, err
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
return totalAttempts, nil
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
func (r *quizRepository) UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
|
| 113 |
+
return r.db.Debug().Create(attempt).Error
|
| 114 |
}
|
| 115 |
|
| 116 |
func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error) {
|
|
|
|
| 143 |
q.id;
|
| 144 |
`
|
| 145 |
|
| 146 |
+
err := r.db.Debug().Raw(rawQuery, attemptID, accountID, quizID, attemptID, quizID).Scan(&questions).Error
|
| 147 |
if err != nil {
|
| 148 |
return nil, err
|
| 149 |
}
|
|
|
|
| 151 |
return questions, nil
|
| 152 |
}
|
| 153 |
|
| 154 |
+
func (r *quizRepository) UserGetAttemptByID(ctx context.Context, attemptID int64) (*models.QuizAttempt, error) {
|
| 155 |
+
var quizAttempt models.QuizAttempt
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
+
err := r.db.Where("id = ?", attemptID).
|
| 158 |
+
Preload("Quiz").
|
| 159 |
+
First(&quizAttempt).Error
|
| 160 |
if err != nil {
|
| 161 |
+
return nil, err
|
| 162 |
}
|
| 163 |
|
| 164 |
+
return &quizAttempt, nil
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
func (r *quizRepository) UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
|
| 168 |
+
return r.db.Save(attempt).Error
|
| 169 |
}
|
| 170 |
|
| 171 |
func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error) {
|
|
|
|
| 193 |
func (r *quizRepository) UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error {
|
| 194 |
return r.db.Where("account_id = ? AND quiz_id = ?", accountID, quizID).Delete(&models.QuizAttempt{}).Error
|
| 195 |
}
|
| 196 |
+
|
| 197 |
+
func (r *quizRepository) UserGetRemainingAttempts(ctx context.Context, accountID int64, quizID int64) (int64, error) {
|
| 198 |
+
var remainingAttempts int64
|
| 199 |
+
|
| 200 |
+
rawQuery := `
|
| 201 |
+
SELECT
|
| 202 |
+
GREATEST(q.attempt_limit - COALESCE(COUNT(qa.id), 0), 0) AS remaining_attempts
|
| 203 |
+
FROM
|
| 204 |
+
quizzes q
|
| 205 |
+
LEFT JOIN
|
| 206 |
+
quiz_attempts qa
|
| 207 |
+
ON q.id = qa.quiz_id
|
| 208 |
+
AND qa.account_id = ?
|
| 209 |
+
AND qa.finished_at IS NOT NULL
|
| 210 |
+
WHERE
|
| 211 |
+
q.id = ?
|
| 212 |
+
GROUP BY
|
| 213 |
+
q.attempt_limit;
|
| 214 |
+
`
|
| 215 |
+
|
| 216 |
+
err := r.db.Raw(rawQuery, accountID, quizID).Scan(&remainingAttempts).Error
|
| 217 |
+
if err != nil {
|
| 218 |
+
return 0, err
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
return remainingAttempts, nil
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
func (r *quizRepository) UserGetQuestionQuiz(ctx context.Context, attemptID, questionID int64) (*models.GetQuestionQuizResponse, error) {
|
| 225 |
+
var resultJSON string
|
| 226 |
+
var response models.GetQuestionQuizResponse
|
| 227 |
+
|
| 228 |
+
rawQuery := `
|
| 229 |
+
SELECT
|
| 230 |
+
json_build_object(
|
| 231 |
+
'question', json_build_object(
|
| 232 |
+
'id', q.id,
|
| 233 |
+
'quiz_id', q.quiz_id,
|
| 234 |
+
'content', q.content
|
| 235 |
+
),
|
| 236 |
+
'answer_options', json_agg(
|
| 237 |
+
json_build_object(
|
| 238 |
+
'id', a.id,
|
| 239 |
+
'content', a.content
|
| 240 |
+
)
|
| 241 |
+
),
|
| 242 |
+
'answer_id', ua.selected_answer_id,
|
| 243 |
+
'is_doubt', COALESCE(ua.is_doubt, FALSE),
|
| 244 |
+
'is_answered', ua.selected_answer_id IS NOT NULL
|
| 245 |
+
) AS result
|
| 246 |
+
FROM
|
| 247 |
+
questions q
|
| 248 |
+
LEFT JOIN
|
| 249 |
+
answers a ON q.id = a.question_id
|
| 250 |
+
LEFT JOIN
|
| 251 |
+
user_answers ua
|
| 252 |
+
ON q.id = ua.question_id
|
| 253 |
+
AND ua.quiz_attempt_id = $1
|
| 254 |
+
WHERE
|
| 255 |
+
q.id = $2
|
| 256 |
+
GROUP BY
|
| 257 |
+
q.id, q.quiz_id, q.content, ua.selected_answer_id, ua.is_doubt;
|
| 258 |
+
`
|
| 259 |
+
|
| 260 |
+
err := r.db.Raw(rawQuery, attemptID, questionID).Scan(&resultJSON).Error
|
| 261 |
+
if err != nil {
|
| 262 |
+
return nil, err
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
err = json.Unmarshal([]byte(resultJSON), &response)
|
| 266 |
+
if err != nil {
|
| 267 |
+
return nil, err
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
return &response, nil
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
func (r *quizRepository) UserGetQuestionByID(ctx context.Context, questionID int64) (*models.Question, error) {
|
| 274 |
+
var question models.Question
|
| 275 |
+
|
| 276 |
+
err := r.db.Where("id = ?", questionID).First(&question).Error
|
| 277 |
+
if err != nil {
|
| 278 |
+
return nil, err
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
return &question, nil
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
func (r *quizRepository) UserGetUserAnswer(ctx context.Context, attemptID, questionID int64) (*models.UserAnswer, error) {
|
| 285 |
+
var userAnswer models.UserAnswer
|
| 286 |
+
|
| 287 |
+
err := r.db.Where("quiz_attempt_id = ? AND question_id = ?", attemptID, questionID).First(&userAnswer).Error
|
| 288 |
+
if err != nil {
|
| 289 |
+
return nil, err
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
return &userAnswer, nil
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
func (r *quizRepository) UserSaveUserAnswer(ctx context.Context, userAnswer *models.UserAnswer) error {
|
| 296 |
+
return r.db.Save(userAnswer).Error
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
func (r *quizRepository) UserGetReviewQuiz(ctx context.Context, attemptID, accountID, quizID int64) (*models.ReviewQuizResponse, error) {
|
| 300 |
+
var resultJSON string
|
| 301 |
+
var reviewQuizResponse models.ReviewQuizResponse
|
| 302 |
+
|
| 303 |
+
reviews := make([]models.ReviewQuiz, 0)
|
| 304 |
+
|
| 305 |
+
rawQuery := `
|
| 306 |
+
SELECT
|
| 307 |
+
COALESCE(
|
| 308 |
+
json_agg(
|
| 309 |
+
json_build_object(
|
| 310 |
+
'question', json_build_object(
|
| 311 |
+
'id', q.id,
|
| 312 |
+
'quiz_id', q.quiz_id,
|
| 313 |
+
'content', q.content,
|
| 314 |
+
'review', q.review
|
| 315 |
+
),
|
| 316 |
+
'answer_options', (
|
| 317 |
+
SELECT json_agg(
|
| 318 |
+
json_build_object(
|
| 319 |
+
'id', a.id,
|
| 320 |
+
'content', a.content,
|
| 321 |
+
'is_correct', a.is_correct
|
| 322 |
+
)
|
| 323 |
+
ORDER BY a.id
|
| 324 |
+
)
|
| 325 |
+
FROM answers a
|
| 326 |
+
WHERE a.question_id = q.id
|
| 327 |
+
),
|
| 328 |
+
'answer_id', ua.selected_answer_id
|
| 329 |
+
) ORDER BY q.order
|
| 330 |
+
),
|
| 331 |
+
'[]'::json
|
| 332 |
+
) AS reviews
|
| 333 |
+
FROM
|
| 334 |
+
questions q
|
| 335 |
+
LEFT JOIN
|
| 336 |
+
user_answers ua
|
| 337 |
+
ON q.id = ua.question_id
|
| 338 |
+
AND ua.quiz_attempt_id = ?
|
| 339 |
+
WHERE
|
| 340 |
+
q.quiz_id = ?
|
| 341 |
+
`
|
| 342 |
+
|
| 343 |
+
err := r.db.Raw(rawQuery, attemptID, quizID).Scan(&resultJSON).Error
|
| 344 |
+
if err != nil {
|
| 345 |
+
return nil, fmt.Errorf("failed to query reviews: %w", err)
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
// Unmarshal the JSON result into the Reviews field
|
| 349 |
+
err = json.Unmarshal([]byte(resultJSON), &reviews)
|
| 350 |
+
if err != nil {
|
| 351 |
+
return nil, fmt.Errorf("failed to unmarshal JSON result: %w", err)
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
reviewQuizResponse.Reviews = reviews
|
| 355 |
+
return &reviewQuizResponse, nil
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
func (r *quizRepository) UseGetLastAttemptQuiz(ctx context.Context, accountID int64, academyID int64) (*models.QuizAttempt, error) {
|
| 359 |
+
var attempt models.QuizAttempt
|
| 360 |
+
|
| 361 |
+
err := r.db.Where("account_id = ? AND academy_id = ?", accountID, academyID).Last(&attempt).Error
|
| 362 |
+
if err != nil {
|
| 363 |
+
return nil, err
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
return &attempt, nil
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
func (r *quizRepository) GetCorrectOptionID(ctx context.Context, questionID int64) (int64, error) {
|
| 370 |
+
var correctOptionID int64
|
| 371 |
+
|
| 372 |
+
rawQuery := `
|
| 373 |
+
SELECT id
|
| 374 |
+
FROM answers
|
| 375 |
+
WHERE question_id = ?
|
| 376 |
+
AND is_correct = TRUE
|
| 377 |
+
LIMIT 1;
|
| 378 |
+
`
|
| 379 |
+
|
| 380 |
+
err := r.db.Raw(rawQuery, questionID).Scan(&correctOptionID).Error
|
| 381 |
+
if err != nil {
|
| 382 |
+
return 0, err
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
return correctOptionID, nil
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
func (r *quizRepository) UserDeleteProgressAndAttempt(ctx context.Context, accountID int64, academyID int64) error {
|
| 389 |
+
r.db.Where("account_id = ? AND academy_material_id IN (SELECT id FROM academy_materials WHERE academy_id = ?)", accountID, academyID).
|
| 390 |
+
Delete(&models.AcademyMaterialProgress{})
|
| 391 |
+
|
| 392 |
+
// Delete quiz attempts for the quiz associated with the academy
|
| 393 |
+
r.db.Where("account_id = ? AND quiz_id IN (SELECT id FROM quizzes WHERE academy_id = ?)", accountID, academyID).
|
| 394 |
+
Delete(&models.QuizAttempt{})
|
| 395 |
+
|
| 396 |
+
return nil
|
| 397 |
+
}
|
space/space/space/space/space/router/quiz_route.go
CHANGED
|
@@ -7,8 +7,11 @@ import (
|
|
| 7 |
func (s *Server) QuizRoute() {
|
| 8 |
userRouterGroup := s.router.Group("/api/v1/quiz")
|
| 9 |
{
|
| 10 |
-
|
| 11 |
-
userRouterGroup.
|
| 12 |
-
userRouterGroup.
|
|
|
|
|
|
|
|
|
|
| 13 |
}
|
| 14 |
}
|
|
|
|
| 7 |
func (s *Server) QuizRoute() {
|
| 8 |
userRouterGroup := s.router.Group("/api/v1/quiz")
|
| 9 |
{
|
| 10 |
+
userRouterGroup.GET("", middleware.AuthUser, s.quizController.UserGetQuiz)
|
| 11 |
+
userRouterGroup.POST("/attempt", middleware.AuthUser, s.quizController.UserAttemptQuiz)
|
| 12 |
+
userRouterGroup.GET("/question", middleware.AuthUser, s.quizController.UserGetQuestionQuiz)
|
| 13 |
+
userRouterGroup.PUT("/answer", middleware.AuthUser, s.quizController.UserAnswerQuiz)
|
| 14 |
+
userRouterGroup.POST("/submit", middleware.AuthUser, s.quizController.UserSubmitQuiz)
|
| 15 |
+
userRouterGroup.GET("/review", middleware.AuthUser, s.quizController.UserReviewQuiz)
|
| 16 |
}
|
| 17 |
}
|
space/space/space/space/space/services/quiz_service.go
CHANGED
|
@@ -3,29 +3,26 @@ package services
|
|
| 3 |
import (
|
| 4 |
"context"
|
| 5 |
"errors"
|
| 6 |
-
"math/rand"
|
| 7 |
"time"
|
| 8 |
|
| 9 |
"api.qobiltu.id/models"
|
| 10 |
"api.qobiltu.id/pkg/validation"
|
| 11 |
"api.qobiltu.id/repositories"
|
| 12 |
"api.qobiltu.id/response"
|
|
|
|
| 13 |
"gorm.io/gorm"
|
| 14 |
)
|
| 15 |
|
| 16 |
type QuizService interface {
|
| 17 |
// === ADMIN ===
|
| 18 |
|
| 19 |
-
//
|
| 20 |
UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
|
| 21 |
UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error)
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
// UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
|
| 27 |
-
// UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error)
|
| 28 |
-
// UserListResultQuiz(ctx context.Context, req *models.ListResultQuizRequest) (*models.ListResultQuizResponse, *models.Paging, error)
|
| 29 |
}
|
| 30 |
|
| 31 |
type quizService struct {
|
|
@@ -48,6 +45,7 @@ func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRe
|
|
| 48 |
}
|
| 49 |
|
| 50 |
func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
|
|
|
|
| 51 |
quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, &models.UserGetQuizRequest{
|
| 52 |
AccountID: req.AccountID,
|
| 53 |
AcademyID: req.AcademyID,
|
|
@@ -58,66 +56,52 @@ func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttem
|
|
| 58 |
|
| 59 |
quiz := &quizAttempt.Quiz
|
| 60 |
|
|
|
|
| 61 |
existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, req.AccountID, quiz.ID)
|
| 62 |
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
| 63 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 64 |
}
|
| 65 |
|
|
|
|
| 66 |
if existingAttempt != nil {
|
| 67 |
-
return s.
|
| 68 |
}
|
| 69 |
|
| 70 |
return s.handleNewAttempt(ctx, req, quiz)
|
| 71 |
}
|
| 72 |
|
| 73 |
-
func (s *quizService)
|
| 74 |
now := time.Now()
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, attempt); err != nil {
|
| 82 |
-
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
|
| 86 |
-
if err != nil {
|
| 87 |
-
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
if totalAttempts >= quiz.AttemptLimit {
|
| 91 |
-
if err := s.academyRepository.UserResetAcademyProgressByID(ctx, req.AccountID, quiz.AcademyID); err != nil {
|
| 92 |
-
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
// hapus juga semua attempt quiz by account id dan quiz id
|
| 96 |
-
if err := s.quizRepository.UserDeleteAttemptQuizByAccountIDAndQuizID(ctx, req.AccountID, quiz.ID); err != nil {
|
| 97 |
-
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 98 |
-
}
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
return nil, models.Exception{QuizAlreadyFinished: true, Message: "Quiz already finished"}
|
| 105 |
}
|
| 106 |
|
|
|
|
| 107 |
questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
|
| 108 |
if err != nil {
|
| 109 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 110 |
}
|
| 111 |
|
| 112 |
-
questions =
|
| 113 |
|
| 114 |
return &models.UserAttemptQuizResponse{
|
| 115 |
-
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
}, nil
|
| 118 |
}
|
| 119 |
|
| 120 |
func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
|
|
|
|
|
|
|
| 121 |
totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
|
| 122 |
if err != nil {
|
| 123 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
|
@@ -134,67 +118,202 @@ func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAtte
|
|
| 134 |
return nil, models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
|
| 135 |
}
|
| 136 |
|
| 137 |
-
|
|
|
|
| 138 |
AccountID: req.AccountID,
|
| 139 |
QuizID: quiz.ID,
|
|
|
|
| 140 |
StartedAt: time.Now(),
|
| 141 |
DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
|
| 142 |
}
|
| 143 |
|
| 144 |
-
if err := s.quizRepository.UserCreateAttemptQuiz(ctx, &
|
| 145 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 146 |
}
|
| 147 |
|
| 148 |
-
|
|
|
|
| 149 |
if err != nil {
|
| 150 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 151 |
}
|
| 152 |
|
| 153 |
-
questions =
|
| 154 |
|
| 155 |
return &models.UserAttemptQuizResponse{
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
}, nil
|
| 159 |
}
|
| 160 |
|
| 161 |
-
func (s *quizService) calculateQuizScore(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
// ambil total question dari quiz
|
| 163 |
totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, attempt.QuizID)
|
| 164 |
if err != nil {
|
| 165 |
-
return
|
| 166 |
}
|
| 167 |
|
| 168 |
if totalQuestion == 0 {
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
}
|
| 171 |
|
| 172 |
// ambil semua user answer yang is_correct nya true
|
| 173 |
correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, attempt.ID)
|
| 174 |
if err != nil {
|
| 175 |
-
return
|
| 176 |
}
|
| 177 |
|
| 178 |
-
// hitung score nya
|
| 179 |
score := float64(correctAnswer) / float64(totalQuestion) * 100
|
| 180 |
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
}
|
| 183 |
|
| 184 |
-
|
| 185 |
-
// untuk memastikan hasil acak yang konsisten untuk key yang sama
|
| 186 |
-
func shuffleWithKey[T any](slice []T, key int64) []T {
|
| 187 |
-
// Buat salinan slice untuk menghindari modifikasi original
|
| 188 |
-
shuffled := make([]T, len(slice))
|
| 189 |
-
copy(shuffled, slice)
|
| 190 |
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
| 193 |
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
-
return
|
| 200 |
}
|
|
|
|
| 3 |
import (
|
| 4 |
"context"
|
| 5 |
"errors"
|
|
|
|
| 6 |
"time"
|
| 7 |
|
| 8 |
"api.qobiltu.id/models"
|
| 9 |
"api.qobiltu.id/pkg/validation"
|
| 10 |
"api.qobiltu.id/repositories"
|
| 11 |
"api.qobiltu.id/response"
|
| 12 |
+
"api.qobiltu.id/utils"
|
| 13 |
"gorm.io/gorm"
|
| 14 |
)
|
| 15 |
|
| 16 |
type QuizService interface {
|
| 17 |
// === ADMIN ===
|
| 18 |
|
| 19 |
+
// === USER ===
|
| 20 |
UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
|
| 21 |
UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error)
|
| 22 |
+
UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
|
| 23 |
+
UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error)
|
| 24 |
+
UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
|
| 25 |
+
UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
|
| 28 |
type quizService struct {
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
|
| 48 |
+
// ambil data quiz attempt
|
| 49 |
quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, &models.UserGetQuizRequest{
|
| 50 |
AccountID: req.AccountID,
|
| 51 |
AcademyID: req.AcademyID,
|
|
|
|
| 56 |
|
| 57 |
quiz := &quizAttempt.Quiz
|
| 58 |
|
| 59 |
+
// ambil attempt quiz yg sedang aktif
|
| 60 |
existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, req.AccountID, quiz.ID)
|
| 61 |
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
| 62 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 63 |
}
|
| 64 |
|
| 65 |
+
// jika ada attempt yang sedang active
|
| 66 |
if existingAttempt != nil {
|
| 67 |
+
return s.handleActiveAttempt(ctx, req, quiz, existingAttempt)
|
| 68 |
}
|
| 69 |
|
| 70 |
return s.handleNewAttempt(ctx, req, quiz)
|
| 71 |
}
|
| 72 |
|
| 73 |
+
func (s *quizService) handleActiveAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
|
| 74 |
now := time.Now()
|
| 75 |
|
| 76 |
+
// jika attempt nya telah melewati batas waktu dan belum submit / finish
|
| 77 |
+
if attempt.DueAt.Before(now) && attempt.FinishedAt == nil {
|
| 78 |
+
// frontend nge-trigger submit
|
| 79 |
+
return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
}
|
| 81 |
|
| 82 |
+
// jika masih dalam waktu yang ditentukan
|
| 83 |
questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
|
| 84 |
if err != nil {
|
| 85 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 86 |
}
|
| 87 |
|
| 88 |
+
questions = utils.ShuffleWithKey(questions, attempt.ID)
|
| 89 |
|
| 90 |
return &models.UserAttemptQuizResponse{
|
| 91 |
+
ID: attempt.ID,
|
| 92 |
+
AccountID: attempt.AccountID,
|
| 93 |
+
QuizID: attempt.QuizID,
|
| 94 |
+
StartedAt: attempt.StartedAt,
|
| 95 |
+
DueAt: attempt.DueAt,
|
| 96 |
+
FinishedAt: attempt.FinishedAt,
|
| 97 |
+
Score: attempt.Score,
|
| 98 |
+
Questions: questions,
|
| 99 |
}, nil
|
| 100 |
}
|
| 101 |
|
| 102 |
func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
|
| 103 |
+
|
| 104 |
+
// ini sekedar untuk make sure apakah masih bisa attemp dan telah membaca semua materi.
|
| 105 |
totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
|
| 106 |
if err != nil {
|
| 107 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
|
|
|
| 118 |
return nil, models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
|
| 119 |
}
|
| 120 |
|
| 121 |
+
// buat attempt quiz
|
| 122 |
+
attempt := models.QuizAttempt{
|
| 123 |
AccountID: req.AccountID,
|
| 124 |
QuizID: quiz.ID,
|
| 125 |
+
AcademyID: quiz.AcademyID,
|
| 126 |
StartedAt: time.Now(),
|
| 127 |
DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
|
| 128 |
}
|
| 129 |
|
| 130 |
+
if err := s.quizRepository.UserCreateAttemptQuiz(ctx, &attempt); err != nil {
|
| 131 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 132 |
}
|
| 133 |
|
| 134 |
+
// ambil question
|
| 135 |
+
questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
|
| 136 |
if err != nil {
|
| 137 |
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 138 |
}
|
| 139 |
|
| 140 |
+
questions = utils.ShuffleWithKey(questions, attempt.ID)
|
| 141 |
|
| 142 |
return &models.UserAttemptQuizResponse{
|
| 143 |
+
ID: attempt.ID,
|
| 144 |
+
AccountID: attempt.AccountID,
|
| 145 |
+
QuizID: attempt.QuizID,
|
| 146 |
+
StartedAt: attempt.StartedAt,
|
| 147 |
+
DueAt: attempt.DueAt,
|
| 148 |
+
FinishedAt: attempt.FinishedAt,
|
| 149 |
+
Score: attempt.Score,
|
| 150 |
+
Questions: questions,
|
| 151 |
}, nil
|
| 152 |
}
|
| 153 |
|
| 154 |
+
func (s *quizService) calculateQuizScore(
|
| 155 |
+
ctx context.Context,
|
| 156 |
+
quiz *models.Quiz,
|
| 157 |
+
attempt *models.QuizAttempt,
|
| 158 |
+
) error {
|
| 159 |
// ambil total question dari quiz
|
| 160 |
totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, attempt.QuizID)
|
| 161 |
if err != nil {
|
| 162 |
+
return err
|
| 163 |
}
|
| 164 |
|
| 165 |
if totalQuestion == 0 {
|
| 166 |
+
attempt.Score = 0
|
| 167 |
+
attempt.TotalQuestions = 0
|
| 168 |
+
attempt.TotalCorrectAnswer = 0
|
| 169 |
+
attempt.TotalWrongAnswer = 0
|
| 170 |
+
attempt.IsPassed = false
|
| 171 |
+
return nil
|
| 172 |
}
|
| 173 |
|
| 174 |
// ambil semua user answer yang is_correct nya true
|
| 175 |
correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, attempt.ID)
|
| 176 |
if err != nil {
|
| 177 |
+
return err
|
| 178 |
}
|
| 179 |
|
|
|
|
| 180 |
score := float64(correctAnswer) / float64(totalQuestion) * 100
|
| 181 |
|
| 182 |
+
attempt.Score = score
|
| 183 |
+
attempt.TotalQuestions = totalQuestion
|
| 184 |
+
attempt.TotalCorrectAnswer = correctAnswer
|
| 185 |
+
attempt.TotalWrongAnswer = totalQuestion - correctAnswer
|
| 186 |
+
attempt.IsPassed = score >= float64(quiz.MinScore)
|
| 187 |
+
|
| 188 |
+
now := time.Now()
|
| 189 |
+
attempt.FinishedAt = &now
|
| 190 |
+
|
| 191 |
+
return nil
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
func (s *quizService) UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error) {
|
| 195 |
+
// pastiin dulu attemp nya ada atau tidak
|
| 196 |
+
_, err := s.quizRepository.UserGetAttemptByID(ctx, req.AttemptID)
|
| 197 |
+
if err != nil {
|
| 198 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// ambil pertanyaan dan jawaban pengguna
|
| 202 |
+
question, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
|
| 203 |
+
if err != nil {
|
| 204 |
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 205 |
+
return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
|
| 206 |
+
}
|
| 207 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
return question, nil
|
| 211 |
}
|
| 212 |
|
| 213 |
+
func (s *quizService) UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.GetQuestionQuizResponse, error) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
+
correctOptionID, err := s.quizRepository.GetCorrectOptionID(ctx, req.QuestionID)
|
| 216 |
+
if err != nil {
|
| 217 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 218 |
+
}
|
| 219 |
|
| 220 |
+
question, err := s.quizRepository.UserGetUserAnswer(ctx, req.AttemptID, req.QuestionID)
|
| 221 |
+
if err != nil {
|
| 222 |
+
|
| 223 |
+
// jika belum ada answer
|
| 224 |
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 225 |
+
userAnswer := models.UserAnswer{
|
| 226 |
+
QuizAttemptID: req.AttemptID,
|
| 227 |
+
QuestionID: req.QuestionID,
|
| 228 |
+
AnswerID: req.AnswerID,
|
| 229 |
+
IsDoubt: req.IsDoubt,
|
| 230 |
+
IsCorrect: false,
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
if req.AnswerID != nil {
|
| 234 |
+
userAnswer.IsCorrect = *req.AnswerID == correctOptionID
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
if err := s.quizRepository.UserSaveUserAnswer(ctx, &userAnswer); err != nil {
|
| 238 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
res, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
|
| 242 |
+
if err != nil {
|
| 243 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
return res, nil
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
// updating answer
|
| 253 |
+
question.AnswerID = req.AnswerID
|
| 254 |
+
question.IsDoubt = req.IsDoubt
|
| 255 |
+
|
| 256 |
+
if req.AnswerID != nil {
|
| 257 |
+
question.IsCorrect = *req.AnswerID == correctOptionID
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
if err := s.quizRepository.UserSaveUserAnswer(ctx, question); err != nil {
|
| 261 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
res, err := s.quizRepository.UserGetQuestionQuiz(ctx, req.AttemptID, req.QuestionID)
|
| 265 |
+
if err != nil {
|
| 266 |
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 267 |
+
return nil, models.Exception{DataNotFound: true, Message: "Question not found"}
|
| 268 |
+
}
|
| 269 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
return res, nil
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
func (s *quizService) UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error) {
|
| 276 |
+
attempt, err := s.quizRepository.UserGetAttemptByID(ctx, req.AttemptID)
|
| 277 |
+
if err != nil {
|
| 278 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
if err := s.calculateQuizScore(ctx, attempt.Quiz, attempt); err != nil {
|
| 282 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, attempt); err != nil {
|
| 286 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
remainingAttempts, err := s.quizRepository.UserGetRemainingAttempts(ctx, attempt.AccountID, attempt.AcademyID)
|
| 290 |
+
if err != nil {
|
| 291 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
if remainingAttempts == 0 {
|
| 295 |
+
// hapus progress dan semua attempt pada quiz
|
| 296 |
+
if err := s.quizRepository.UserDeleteProgressAndAttempt(ctx, attempt.AccountID, attempt.QuizID); err != nil {
|
| 297 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
return &models.SubmitQuizResponse{
|
| 302 |
+
QuizAttempt: *attempt,
|
| 303 |
+
RemainingAttempts: remainingAttempts,
|
| 304 |
+
}, nil
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
func (s *quizService) UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error) {
|
| 308 |
+
attempt, err := s.quizRepository.UseGetLastAttemptQuiz(ctx, req.AccountID, req.AcademyID)
|
| 309 |
+
if err != nil {
|
| 310 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
review, err := s.quizRepository.UserGetReviewQuiz(ctx, attempt.ID, attempt.AccountID, attempt.QuizID)
|
| 314 |
+
if err != nil {
|
| 315 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 316 |
+
}
|
| 317 |
|
| 318 |
+
return review, nil
|
| 319 |
}
|
space/space/space/space/space/space/response/api_response_v2.go
CHANGED
|
@@ -41,6 +41,15 @@ func HandleError(c *gin.Context, err error) {
|
|
| 41 |
responseError(c, http.StatusRequestTimeout, exception)
|
| 42 |
case exception.AttemptNotFound:
|
| 43 |
responseError(c, http.StatusNotFound, exception)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
case exception.ValidationError:
|
| 45 |
responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
|
| 46 |
default:
|
|
|
|
| 41 |
responseError(c, http.StatusRequestTimeout, exception)
|
| 42 |
case exception.AttemptNotFound:
|
| 43 |
responseError(c, http.StatusNotFound, exception)
|
| 44 |
+
case exception.QuizTimeExpired:
|
| 45 |
+
responseError(c, http.StatusBadRequest, exception)
|
| 46 |
+
case exception.QuizAlreadyFinished:
|
| 47 |
+
responseError(c, http.StatusBadRequest, exception)
|
| 48 |
+
case exception.QuizAttemptLimit:
|
| 49 |
+
responseError(c, http.StatusBadRequest, exception)
|
| 50 |
+
case exception.AcademyNotFinished:
|
| 51 |
+
responseError(c, http.StatusBadRequest, exception)
|
| 52 |
+
|
| 53 |
case exception.ValidationError:
|
| 54 |
responseValidationError(c, http.StatusUnprocessableEntity, exception.ValidationErrorFields)
|
| 55 |
default:
|
space/space/space/space/space/space/space/config/database_connection_config.go
CHANGED
|
@@ -62,11 +62,13 @@ func AutoMigrateAll(db *gorm.DB) {
|
|
| 62 |
&models.RegionProvince{},
|
| 63 |
&models.OptionCategory{},
|
| 64 |
&models.OptionValues{},
|
|
|
|
| 65 |
&models.Quiz{},
|
| 66 |
-
&models.QuizAttempt{},
|
| 67 |
&models.Question{},
|
| 68 |
&models.Answer{},
|
|
|
|
| 69 |
&models.UserAnswer{},
|
|
|
|
| 70 |
&models.PersonalityAndPreferenceCV{},
|
| 71 |
&models.FamilyMemberCV{},
|
| 72 |
&models.PhysicalAndHealthCV{},
|
|
|
|
| 62 |
&models.RegionProvince{},
|
| 63 |
&models.OptionCategory{},
|
| 64 |
&models.OptionValues{},
|
| 65 |
+
|
| 66 |
&models.Quiz{},
|
|
|
|
| 67 |
&models.Question{},
|
| 68 |
&models.Answer{},
|
| 69 |
+
&models.QuizAttempt{},
|
| 70 |
&models.UserAnswer{},
|
| 71 |
+
|
| 72 |
&models.PersonalityAndPreferenceCV{},
|
| 73 |
&models.FamilyMemberCV{},
|
| 74 |
&models.PhysicalAndHealthCV{},
|
space/space/space/space/space/space/space/controller/quiz/quiz_controller.go
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package quiz_controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
"strconv"
|
| 6 |
+
|
| 7 |
+
"api.qobiltu.id/middleware"
|
| 8 |
+
"api.qobiltu.id/models"
|
| 9 |
+
"api.qobiltu.id/response"
|
| 10 |
+
"api.qobiltu.id/services"
|
| 11 |
+
"github.com/gin-gonic/gin"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
type QuizController interface {
|
| 15 |
+
// === ADMIN ===
|
| 16 |
+
|
| 17 |
+
// === USER ===
|
| 18 |
+
UserGetQuiz(ctx *gin.Context)
|
| 19 |
+
UserAttemptQuiz(ctx *gin.Context)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
type quizController struct {
|
| 23 |
+
quizService services.QuizService
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
func NewQuizController(quizService services.QuizService) QuizController {
|
| 27 |
+
return &quizController{
|
| 28 |
+
quizService: quizService,
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
func (c *quizController) UserGetQuiz(ctx *gin.Context) {
|
| 33 |
+
academyID := ctx.Param("id")
|
| 34 |
+
academyIDInt, err := strconv.Atoi(academyID)
|
| 35 |
+
if err != nil {
|
| 36 |
+
response.HandleError(ctx, err)
|
| 37 |
+
return
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
accountData := middleware.GetAccountData(ctx)
|
| 41 |
+
req := models.UserGetQuizRequest{
|
| 42 |
+
AccountID: int64(accountData.UserID),
|
| 43 |
+
AcademyID: int64(academyIDInt),
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
res, err := c.quizService.UserGetQuiz(ctx, &req)
|
| 47 |
+
if err != nil {
|
| 48 |
+
response.HandleError(ctx, err)
|
| 49 |
+
return
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
response.HandleSuccess(ctx, http.StatusOK, "Quiz retrieved successfully", http.StatusOK, res)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
func (c *quizController) UserAttemptQuiz(ctx *gin.Context) {
|
| 56 |
+
academyID := ctx.Param("id")
|
| 57 |
+
academyIDInt, err := strconv.Atoi(academyID)
|
| 58 |
+
if err != nil {
|
| 59 |
+
response.HandleError(ctx, err)
|
| 60 |
+
return
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
accountData := middleware.GetAccountData(ctx)
|
| 64 |
+
req := models.UserAttemptQuizRequest{
|
| 65 |
+
AccountID: int64(accountData.UserID),
|
| 66 |
+
AcademyID: int64(academyIDInt),
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
res, err := c.quizService.UserAttemptQuiz(ctx, &req)
|
| 70 |
+
if err != nil {
|
| 71 |
+
response.HandleError(ctx, err)
|
| 72 |
+
return
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
response.HandleSuccess(ctx, http.StatusOK, "Quiz attempt retrieved successfully", http.StatusOK, res)
|
| 76 |
+
}
|
space/space/space/space/space/space/space/main.go
CHANGED
|
@@ -12,6 +12,7 @@ import (
|
|
| 12 |
marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
|
| 13 |
options_controller "api.qobiltu.id/controller/options"
|
| 14 |
partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
|
|
|
|
| 15 |
region_controller "api.qobiltu.id/controller/region"
|
| 16 |
"api.qobiltu.id/pkg/mail"
|
| 17 |
"api.qobiltu.id/pkg/storage"
|
|
@@ -75,6 +76,10 @@ func main() {
|
|
| 75 |
academyService := services.NewAcademyService(academyRepository, validator)
|
| 76 |
academyController := academy_controller.NewAcademyController(academyService)
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
cvRepository := repositories.NewCVRepository(config.DB)
|
| 79 |
cvService := services.NewCVService(cvRepository, localStorage, validator)
|
| 80 |
cvController := cv_controller.NewCVController(cvService)
|
|
@@ -98,6 +103,7 @@ func main() {
|
|
| 98 |
optionController,
|
| 99 |
emailController,
|
| 100 |
academyController,
|
|
|
|
| 101 |
cvController,
|
| 102 |
marriageReadinessProfileController,
|
| 103 |
partnerCriteriaController,
|
|
|
|
| 12 |
marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
|
| 13 |
options_controller "api.qobiltu.id/controller/options"
|
| 14 |
partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
|
| 15 |
+
quiz_controller "api.qobiltu.id/controller/quiz"
|
| 16 |
region_controller "api.qobiltu.id/controller/region"
|
| 17 |
"api.qobiltu.id/pkg/mail"
|
| 18 |
"api.qobiltu.id/pkg/storage"
|
|
|
|
| 76 |
academyService := services.NewAcademyService(academyRepository, validator)
|
| 77 |
academyController := academy_controller.NewAcademyController(academyService)
|
| 78 |
|
| 79 |
+
quizRepository := repositories.NewQuizRepository(config.DB)
|
| 80 |
+
quizService := services.NewQuizService(quizRepository, academyRepository, validator)
|
| 81 |
+
quizController := quiz_controller.NewQuizController(quizService)
|
| 82 |
+
|
| 83 |
cvRepository := repositories.NewCVRepository(config.DB)
|
| 84 |
cvService := services.NewCVService(cvRepository, localStorage, validator)
|
| 85 |
cvController := cv_controller.NewCVController(cvService)
|
|
|
|
| 103 |
optionController,
|
| 104 |
emailController,
|
| 105 |
academyController,
|
| 106 |
+
quizController,
|
| 107 |
cvController,
|
| 108 |
marriageReadinessProfileController,
|
| 109 |
partnerCriteriaController,
|
space/space/space/space/space/space/space/models/database_orm_model.go
CHANGED
|
@@ -134,57 +134,71 @@ type RegionCity struct {
|
|
| 134 |
FullCode string `json:"full_code"`
|
| 135 |
ProvinceID uint `json:"province_id"`
|
| 136 |
}
|
| 137 |
-
type Answer struct {
|
| 138 |
-
ID uint `gorm:"primaryKey" json:"id"`
|
| 139 |
-
QuestionID uint `json:"question_id"`
|
| 140 |
-
Content string `json:"content"`
|
| 141 |
-
IsCorrect bool `json:"-"`
|
| 142 |
-
}
|
| 143 |
-
type Question struct {
|
| 144 |
-
ID uint `gorm:"primaryKey" json:"id"`
|
| 145 |
-
QuizID uint `json:"quiz_id"`
|
| 146 |
-
Content string `json:"content"`
|
| 147 |
-
Order int `json:"order"`
|
| 148 |
-
CorrectAnswer uint `json:"correct_answer"`
|
| 149 |
-
Review string `json:"reviews"`
|
| 150 |
-
}
|
| 151 |
-
type Quiz struct {
|
| 152 |
-
ID uint `gorm:"primaryKey" json:"id"`
|
| 153 |
-
AcademyID uint `json:"academy_id"`
|
| 154 |
-
Slug string `json:"slug" gorm:"uniqueIndex" `
|
| 155 |
-
Title string `json:"title"`
|
| 156 |
-
Description string `json:"description"`
|
| 157 |
-
TotalQuestions int `json:"total_questions"`
|
| 158 |
-
AttemptLimit int `json:"attempt_limit"`
|
| 159 |
-
TimeLimit int `json:"time_limit"`
|
| 160 |
-
MinScore int `json:"min_score"`
|
| 161 |
-
CreatedAt time.Time `json:"created_at"`
|
| 162 |
-
}
|
| 163 |
|
| 164 |
-
type
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
type (
|
| 190 |
PersonalityAndPreferenceCV struct {
|
|
@@ -243,27 +257,34 @@ type (
|
|
| 243 |
}
|
| 244 |
|
| 245 |
WorshipAndReligiousUnderstandingCV struct {
|
| 246 |
-
ID
|
| 247 |
-
AccountID
|
| 248 |
-
Account
|
|
|
|
|
|
|
| 249 |
ObligatoryPrayer *string `gorm:"column:obligatory_prayer" json:"obligatory_prayer"` // sholat_wajib_5_waktu
|
| 250 |
CongregationalPrayer *string `gorm:"column:congregational_prayer" json:"congregational_prayer"` // sholat_berjamaah_di_masjid
|
| 251 |
TahajjudPrayer *string `gorm:"column:tahajjud_prayer" json:"tahajjud_prayer"` // sholat_tahajud
|
| 252 |
DhuhaPrayer *string `gorm:"column:dhuha_prayer" json:"dhuha_prayer"` // sholat_dhuha
|
| 253 |
-
QuranMemorization *string `gorm:"column:quran_memorization" json:"quran_memorization"` // hafalan_alquran
|
| 254 |
-
QuranReadingAbility *string `gorm:"column:quran_reading_ability" json:"quran_reading_ability"` // kemampuan_baca_alquran
|
| 255 |
-
WeeklyReligiousStudyFrequency *string `gorm:"column:weekly_religious_study_frequency" json:"weekly_religious_study_frequency"` // kajian_yang_diikuti_dalam_sepekan
|
| 256 |
DaudFasting *string `gorm:"column:daud_fasting" json:"daud_fasting"` // puasa_daud
|
| 257 |
AyyamulBidhFasting *string `gorm:"column:ayyamul_bidh_fasting" json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
|
|
|
|
|
|
|
|
|
|
| 258 |
HajjOrUmrah *pq.StringArray `gorm:"column:hajj_or_umrah;type:varchar(255)[]" json:"hajj_or_umrah"` // ibadah_haji_umroh
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
FieldCounter
|
| 268 |
}
|
| 269 |
|
|
@@ -335,6 +356,46 @@ func (w WorshipAndReligiousUnderstandingCV) GetFilledFields() []string {
|
|
| 335 |
return w.FieldCounter.GetFilledFields(w)
|
| 336 |
}
|
| 337 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
type (
|
| 339 |
MarriageReadinessProfile struct {
|
| 340 |
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
|
@@ -431,9 +492,9 @@ func (AcademyMaterial) TableName() string { return "academy_materials
|
|
| 431 |
func (AcademyMaterialProgress) TableName() string { return "academy_materials_progress" }
|
| 432 |
func (RegionProvince) TableName() string { return "region_provinces" }
|
| 433 |
func (RegionCity) TableName() string { return "region_cities" }
|
| 434 |
-
func (Answer) TableName() string { return "answers" }
|
| 435 |
-
func (Question) TableName() string { return "questions" }
|
| 436 |
func (Quiz) TableName() string { return "quizzes" }
|
|
|
|
|
|
|
| 437 |
func (QuizAttempt) TableName() string { return "quiz_attempts" }
|
| 438 |
func (UserAnswer) TableName() string { return "user_answers" }
|
| 439 |
func (OptionCategory) TableName() string { return "option_categories" }
|
|
|
|
| 134 |
FullCode string `json:"full_code"`
|
| 135 |
ProvinceID uint `json:"province_id"`
|
| 136 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
+
type (
|
| 139 |
+
Quiz struct {
|
| 140 |
+
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
| 141 |
+
AcademyID int64 `gorm:"column:academy_id;not null" json:"academy_id"`
|
| 142 |
+
Academy *Academy `gorm:"foreignKey:AcademyID;constraint:OnDelete:CASCADE" json:"academy,omitempty"`
|
| 143 |
+
Slug string `gorm:"column:slug;uniqueIndex" json:"slug" `
|
| 144 |
+
Title string `gorm:"column:title" json:"title"`
|
| 145 |
+
Description string `gorm:"column:description" json:"description"`
|
| 146 |
+
AttemptLimit int64 `gorm:"column:attempt_limit" json:"attempt_limit"`
|
| 147 |
+
TimeLimit int64 `gorm:"column:time_limit" json:"time_limit"`
|
| 148 |
+
MinScore int64 `gorm:"column:min_score" json:"min_score"`
|
| 149 |
+
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
| 150 |
+
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
Question struct {
|
| 154 |
+
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
| 155 |
+
QuizID int64 `gorm:"column:quiz_id;not null" json:"quiz_id"`
|
| 156 |
+
Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"quiz,omitempty"`
|
| 157 |
+
Content string `gorm:"column:content" json:"content"`
|
| 158 |
+
Order int `gorm:"column:order" json:"order"`
|
| 159 |
+
Review string `gorm:"column:reviews" json:"reviews"`
|
| 160 |
+
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
| 161 |
+
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
Answer struct {
|
| 165 |
+
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
| 166 |
+
QuestionID int64 `gorm:"column:question_id;not null" json:"question_id"`
|
| 167 |
+
Question *Question `gorm:"foreignKey:QuestionID;constraint:OnDelete:CASCADE" json:"question,omitempty"`
|
| 168 |
+
Content string `gorm:"column:content" json:"content"`
|
| 169 |
+
IsCorrect bool `gorm:"column:is_correct" json:"is_correct"`
|
| 170 |
+
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
| 171 |
+
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
QuizAttempt struct {
|
| 175 |
+
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
| 176 |
+
AccountID int64 `gorm:"column:account_id;not null" json:"account_id"`
|
| 177 |
+
Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty"`
|
| 178 |
+
QuizID int64 `gorm:"column:quiz_id;not null" json:"quiz_id"`
|
| 179 |
+
Quiz *Quiz `gorm:"foreignKey:QuizID;constraint:OnDelete:CASCADE" json:"quiz,omitempty"`
|
| 180 |
+
StartedAt time.Time `gorm:"column:started_at;autoCreateTime" json:"started_at"`
|
| 181 |
+
DueAt time.Time `gorm:"column:due_at" json:"due_at"`
|
| 182 |
+
FinishedAt *time.Time `gorm:"column:finished_at" json:"finished_at"`
|
| 183 |
+
Score float64 `gorm:"column:score" json:"score"`
|
| 184 |
+
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
| 185 |
+
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
UserAnswer struct {
|
| 189 |
+
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
| 190 |
+
QuizAttemptID int64 `gorm:"column:quiz_attempt_id;not null" json:"quiz_attempt_id"`
|
| 191 |
+
QuizAttempt *QuizAttempt `gorm:"foreignKey:QuizAttemptID;constraint:OnDelete:CASCADE" json:"quiz_attempt,omitempty"`
|
| 192 |
+
QuestionID int64 `gorm:"column:question_id;not null" json:"question_id"`
|
| 193 |
+
Question *Question `gorm:"foreignKey:QuestionID;constraint:OnDelete:CASCADE" json:"question,omitempty"`
|
| 194 |
+
AnswerID *int64 `gorm:"column:selected_answer_id" json:"selected_answer"`
|
| 195 |
+
Answer *Answer `gorm:"foreignKey:AnswerID;constraint:OnDelete:CASCADE" json:"answer,omitempty"`
|
| 196 |
+
IsDoubt bool `gorm:"column:id_doubt" json:"id_doubt"`
|
| 197 |
+
IsCorrect bool `gorm:"column:is_correct" json:"is_correct"`
|
| 198 |
+
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
| 199 |
+
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
| 200 |
+
}
|
| 201 |
+
)
|
| 202 |
|
| 203 |
type (
|
| 204 |
PersonalityAndPreferenceCV struct {
|
|
|
|
| 257 |
}
|
| 258 |
|
| 259 |
WorshipAndReligiousUnderstandingCV struct {
|
| 260 |
+
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id" counter:"skip"`
|
| 261 |
+
AccountID int64 `gorm:"column:account_id;not null;unique" json:"account_id" counter:"skip"`
|
| 262 |
+
Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty" counter:"skip"`
|
| 263 |
+
|
| 264 |
+
// Data ibadah
|
| 265 |
ObligatoryPrayer *string `gorm:"column:obligatory_prayer" json:"obligatory_prayer"` // sholat_wajib_5_waktu
|
| 266 |
CongregationalPrayer *string `gorm:"column:congregational_prayer" json:"congregational_prayer"` // sholat_berjamaah_di_masjid
|
| 267 |
TahajjudPrayer *string `gorm:"column:tahajjud_prayer" json:"tahajjud_prayer"` // sholat_tahajud
|
| 268 |
DhuhaPrayer *string `gorm:"column:dhuha_prayer" json:"dhuha_prayer"` // sholat_dhuha
|
|
|
|
|
|
|
|
|
|
| 269 |
DaudFasting *string `gorm:"column:daud_fasting" json:"daud_fasting"` // puasa_daud
|
| 270 |
AyyamulBidhFasting *string `gorm:"column:ayyamul_bidh_fasting" json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
|
| 271 |
+
QuranReadingAbility *string `gorm:"column:quran_reading_ability" json:"quran_reading_ability"` // kemampuan_baca_alquran
|
| 272 |
+
QuranMemorization *string `gorm:"column:quran_memorization" json:"quran_memorization"` // hafalan_alquran
|
| 273 |
+
WeeklyReligiousStudyFrequency *string `gorm:"column:weekly_religious_study_frequency" json:"weekly_religious_study_frequency"` // jumlah kajian yang diikuti dalam seminggu
|
| 274 |
HajjOrUmrah *pq.StringArray `gorm:"column:hajj_or_umrah;type:varchar(255)[]" json:"hajj_or_umrah"` // ibadah_haji_umroh
|
| 275 |
+
|
| 276 |
+
// Data Pemahaman Agama
|
| 277 |
+
ListeningToMusic *bool `gorm:"column:listening_to_music" json:"listening_to_music"` // mendengarkan_musik
|
| 278 |
+
OpinionOnIkhtilat *string `gorm:"column:opinion_on_ikhtilat" json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
|
| 279 |
+
OpinionOnTouchingNonMahram *string `gorm:"column:opinion_on_touching_non_mahram" json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
|
| 280 |
+
OpinionOnVeil *string `gorm:"column:opinion_on_veil" json:"opinion_on_veil"` // pendapat_tentang_cadar
|
| 281 |
+
OpinionOnBeard *string `gorm:"column:opinion_on_beard" json:"opinion_on_beard"` // pendapat_tentang_jenggot_pada_laki_laki
|
| 282 |
+
OpinionOnPantsAboveAnkle *string `gorm:"column:opinion_on_pants_above_ankle" json:"opinion_on_pants_above_ankle"` // pendapat_tentang_celana_di_atas_mata_kaki
|
| 283 |
+
WeeklyReligiousStudies *string `gorm:"column:weekly_religious_studies" json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
|
| 284 |
+
FollowedUstadz *string `gorm:"column:followed_ustadz" json:"followed_ustadz"` // ustadz_yang_diikuti
|
| 285 |
+
|
| 286 |
+
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at" counter:"skip"`
|
| 287 |
+
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at" counter:"skip"`
|
| 288 |
FieldCounter
|
| 289 |
}
|
| 290 |
|
|
|
|
| 356 |
return w.FieldCounter.GetFilledFields(w)
|
| 357 |
}
|
| 358 |
|
| 359 |
+
var WorshipFields = []string{
|
| 360 |
+
"ObligatoryPrayer",
|
| 361 |
+
"CongregationalPrayer",
|
| 362 |
+
"TahajjudPrayer",
|
| 363 |
+
"DhuhaPrayer",
|
| 364 |
+
"DaudFasting",
|
| 365 |
+
"AyyamulBidhFasting",
|
| 366 |
+
"QuranReadingAbility",
|
| 367 |
+
"QuranMemorization",
|
| 368 |
+
"WeeklyReligiousStudyFrequency",
|
| 369 |
+
"HajjOrUmrah",
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
var ReligiousUnderstandingFields = []string{
|
| 373 |
+
"ListeningToMusic",
|
| 374 |
+
"OpinionOnIkhtilat",
|
| 375 |
+
"OpinionOnTouchingNonMahram",
|
| 376 |
+
"OpinionOnVeil",
|
| 377 |
+
"OpinionOnBeard",
|
| 378 |
+
"OpinionOnPantsAboveAnkle",
|
| 379 |
+
"WeeklyReligiousStudies",
|
| 380 |
+
"FollowedUstadz",
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
func (w WorshipAndReligiousUnderstandingCV) GetTotalFieldsWorship() int {
|
| 384 |
+
return w.FieldCounter.CountFieldsByNames(w, WorshipFields)
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
func (w WorshipAndReligiousUnderstandingCV) GetTotalFieldsReligiousUnderstanding() int {
|
| 388 |
+
return w.FieldCounter.CountFieldsByNames(w, ReligiousUnderstandingFields)
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
func (w WorshipAndReligiousUnderstandingCV) GetFilledFieldsWorship() []string {
|
| 392 |
+
return w.FieldCounter.GetFilledFieldsByNames(w, WorshipFields)
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
func (w WorshipAndReligiousUnderstandingCV) GetFilledFieldsReligiousUnderstanding() []string {
|
| 396 |
+
return w.FieldCounter.GetFilledFieldsByNames(w, ReligiousUnderstandingFields)
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
type (
|
| 400 |
MarriageReadinessProfile struct {
|
| 401 |
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
|
|
|
| 492 |
func (AcademyMaterialProgress) TableName() string { return "academy_materials_progress" }
|
| 493 |
func (RegionProvince) TableName() string { return "region_provinces" }
|
| 494 |
func (RegionCity) TableName() string { return "region_cities" }
|
|
|
|
|
|
|
| 495 |
func (Quiz) TableName() string { return "quizzes" }
|
| 496 |
+
func (Question) TableName() string { return "questions" }
|
| 497 |
+
func (Answer) TableName() string { return "answers" }
|
| 498 |
func (QuizAttempt) TableName() string { return "quiz_attempts" }
|
| 499 |
func (UserAnswer) TableName() string { return "user_answers" }
|
| 500 |
func (OptionCategory) TableName() string { return "option_categories" }
|
space/space/space/space/space/space/space/models/exception_model.go
CHANGED
|
@@ -18,6 +18,12 @@ type Exception struct {
|
|
| 18 |
Forbidden bool `json:"forbidden,omitempty"`
|
| 19 |
ValidationError bool `json:"validation_error,omitempty"`
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
Message string `json:"message,omitempty"`
|
| 22 |
Err error `json:"-"`
|
| 23 |
ValidationErrorFields []validation.ErrorMessage `json:"validation_error_fields,omitempty"`
|
|
|
|
| 18 |
Forbidden bool `json:"forbidden,omitempty"`
|
| 19 |
ValidationError bool `json:"validation_error,omitempty"`
|
| 20 |
|
| 21 |
+
// quiz context
|
| 22 |
+
QuizTimeExpired bool `json:"quiz_time_expired,omitempty"`
|
| 23 |
+
QuizAttemptLimit bool `json:"quiz_attempt_limit,omitempty"`
|
| 24 |
+
QuizAlreadyFinished bool `json:"quiz_already_finished,omitempty"`
|
| 25 |
+
AcademyNotFinished bool `json:"academy_not_finished,omitempty"`
|
| 26 |
+
|
| 27 |
Message string `json:"message,omitempty"`
|
| 28 |
Err error `json:"-"`
|
| 29 |
ValidationErrorFields []validation.ErrorMessage `json:"validation_error_fields,omitempty"`
|
space/space/space/space/space/space/space/models/field_counter.go
CHANGED
|
@@ -97,6 +97,46 @@ func (fc FieldCounter) GetFilledFields(s any) []string {
|
|
| 97 |
return filledFields
|
| 98 |
}
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
// isFieldFilled memeriksa apakah field memiliki nilai non-zero
|
| 101 |
func isFieldFilled(field reflect.Value) bool {
|
| 102 |
// Jika field tidak dapat di-address atau diakses, anggap kosong
|
|
|
|
| 97 |
return filledFields
|
| 98 |
}
|
| 99 |
|
| 100 |
+
// GetFilledFieldsByNames mengembalikan field terisi berdasarkan nama field yang diberikan
|
| 101 |
+
func (fc FieldCounter) GetFilledFieldsByNames(s any, fieldNames []string) []string {
|
| 102 |
+
var filledFields []string
|
| 103 |
+
v := reflect.ValueOf(s)
|
| 104 |
+
|
| 105 |
+
if v.Kind() == reflect.Ptr {
|
| 106 |
+
if v.IsNil() {
|
| 107 |
+
return filledFields
|
| 108 |
+
}
|
| 109 |
+
v = v.Elem()
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
t := v.Type()
|
| 113 |
+
fieldNameSet := make(map[string]struct{}, len(fieldNames))
|
| 114 |
+
for _, name := range fieldNames {
|
| 115 |
+
fieldNameSet[name] = struct{}{}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
for i := 0; i < v.NumField(); i++ {
|
| 119 |
+
field := v.Field(i)
|
| 120 |
+
fieldType := t.Field(i)
|
| 121 |
+
name := fieldType.Name
|
| 122 |
+
|
| 123 |
+
if shouldSkipField(fieldType) {
|
| 124 |
+
continue
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
if _, ok := fieldNameSet[name]; ok && isFieldFilled(field) {
|
| 128 |
+
filledFields = append(filledFields, name)
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
return filledFields
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// CountFieldsByNames menghitung total field dari daftar yang diberikan (dianggap valid dan bukan skip)
|
| 136 |
+
func (fc FieldCounter) CountFieldsByNames(s any, fieldNames []string) int {
|
| 137 |
+
return len(fieldNames)
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
// isFieldFilled memeriksa apakah field memiliki nilai non-zero
|
| 141 |
func isFieldFilled(field reflect.Value) bool {
|
| 142 |
// Jika field tidak dapat di-address atau diakses, anggap kosong
|
space/space/space/space/space/space/space/models/request_model.go
CHANGED
|
@@ -152,22 +152,31 @@ func NewListAcademyContentRequest() ListAcademyContentRequest {
|
|
| 152 |
}
|
| 153 |
|
| 154 |
type (
|
| 155 |
-
|
| 156 |
-
AccountID int64 `json:"account_id"`
|
| 157 |
AcademyID int64 `json:"academy_id" validate:"required"`
|
| 158 |
}
|
| 159 |
|
| 160 |
-
|
| 161 |
-
Quiz
|
|
|
|
|
|
|
| 162 |
}
|
| 163 |
|
| 164 |
-
|
| 165 |
AccountID int64 `json:"account_id"`
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
}
|
| 168 |
|
| 169 |
-
|
| 170 |
-
QuizAttempt
|
|
|
|
| 171 |
}
|
| 172 |
|
| 173 |
GetQuestionQuizRequest struct {
|
|
@@ -356,10 +365,12 @@ type (
|
|
| 356 |
DaudFasting *string `json:"daud_fasting"` // puasa_daud
|
| 357 |
AyyamulBidhFasting *string `json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
|
| 358 |
HajjOrUmrah *pq.StringArray `json:"hajj_or_umrah"` // ibadah_haji_umroh
|
| 359 |
-
ListeningToMusic *
|
| 360 |
OpinionOnIkhtilat *string `json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
|
| 361 |
OpinionOnTouchingNonMahram *string `json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
|
| 362 |
OpinionOnVeil *string `json:"opinion_on_veil"` // pendapat_tentang_cadar
|
|
|
|
|
|
|
| 363 |
WeeklyReligiousStudies *string `json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
|
| 364 |
FollowedUstadz *string `json:"followed_ustadz"` // ustadz_yang_diikuti
|
| 365 |
}
|
|
@@ -467,15 +478,16 @@ type (
|
|
| 467 |
}
|
| 468 |
|
| 469 |
GetProgressCVResponse struct {
|
| 470 |
-
AccountDetailsProgress
|
| 471 |
-
PersonalityAndPreferenceCVProgress
|
| 472 |
-
FamilyMemberCVProgress
|
| 473 |
-
PhysicalAndHealthCVProgress
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
|
|
|
| 479 |
}
|
| 480 |
)
|
| 481 |
|
|
|
|
| 152 |
}
|
| 153 |
|
| 154 |
type (
|
| 155 |
+
UserGetQuizRequest struct {
|
| 156 |
+
AccountID int64 `json:"account_id" validate:"required"`
|
| 157 |
AcademyID int64 `json:"academy_id" validate:"required"`
|
| 158 |
}
|
| 159 |
|
| 160 |
+
UserGetQuizResponse struct {
|
| 161 |
+
Quiz
|
| 162 |
+
TotalQuestions int64 `json:"total_questions"`
|
| 163 |
+
UserAttempts int64 `json:"user_attempts"`
|
| 164 |
}
|
| 165 |
|
| 166 |
+
UserAttemptQuizRequest struct {
|
| 167 |
AccountID int64 `json:"account_id"`
|
| 168 |
+
AcademyID int64 `json:"academy_id"`
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
UserAttemptQuizQuestionsResponse struct {
|
| 172 |
+
ID int64 `gorm:"column:id" json:"id"`
|
| 173 |
+
IsDoubt bool `gorm:"column:is_doubt" json:"is_doubt"`
|
| 174 |
+
IsAnswered bool `gorm:"column:is_answered" json:"is_answered"`
|
| 175 |
}
|
| 176 |
|
| 177 |
+
UserAttemptQuizResponse struct {
|
| 178 |
+
QuizAttempt
|
| 179 |
+
Questions []UserAttemptQuizQuestionsResponse `json:"questions"`
|
| 180 |
}
|
| 181 |
|
| 182 |
GetQuestionQuizRequest struct {
|
|
|
|
| 365 |
DaudFasting *string `json:"daud_fasting"` // puasa_daud
|
| 366 |
AyyamulBidhFasting *string `json:"ayyamul_bidh_fasting"` // puasa_ayyamul_bidh
|
| 367 |
HajjOrUmrah *pq.StringArray `json:"hajj_or_umrah"` // ibadah_haji_umroh
|
| 368 |
+
ListeningToMusic *bool `json:"listening_to_music"` // mendengarkan_musik
|
| 369 |
OpinionOnIkhtilat *string `json:"opinion_on_ikhtilat"` // pendapat_ikhtilat
|
| 370 |
OpinionOnTouchingNonMahram *string `json:"opinion_on_touching_non_mahram"` // pendapat_menyentuh_non_mahram
|
| 371 |
OpinionOnVeil *string `json:"opinion_on_veil"` // pendapat_tentang_cadar
|
| 372 |
+
OpinionOnBeard *string `json:"opinion_on_beard"` // pendapat_tentang_jenggot_pada_laki_laki
|
| 373 |
+
OpinionOnPantsAboveAnkle *string `json:"opinion_on_pants_above_ankle"` // pendapat_tentang_celana_di_atas_mata_kaki
|
| 374 |
WeeklyReligiousStudies *string `json:"weekly_religious_studies"` // kajian_yang_diikuti_dalam_sepekan
|
| 375 |
FollowedUstadz *string `json:"followed_ustadz"` // ustadz_yang_diikuti
|
| 376 |
}
|
|
|
|
| 478 |
}
|
| 479 |
|
| 480 |
GetProgressCVResponse struct {
|
| 481 |
+
AccountDetailsProgress float64 `json:"account_details_progress"`
|
| 482 |
+
PersonalityAndPreferenceCVProgress float64 `json:"personality_and_preference_cv_progress"`
|
| 483 |
+
FamilyMemberCVProgress float64 `json:"family_member_cv_progress"`
|
| 484 |
+
PhysicalAndHealthCVProgress float64 `json:"physical_and_health_cv_progress"`
|
| 485 |
+
WorshipCVProgress float64 `json:"worship_cv_progress"`
|
| 486 |
+
ReligiousUnderstandingCVProgress float64 `json:"religious_understanding_cv_progress"`
|
| 487 |
+
EducationCVProgress float64 `json:"education_cv_progress"`
|
| 488 |
+
JobCVProgress float64 `json:"job_cv_progress"`
|
| 489 |
+
AchievementCVProgress float64 `json:"achievement_cv_progress"`
|
| 490 |
+
TotalProgress float64 `json:"total_progress"`
|
| 491 |
}
|
| 492 |
)
|
| 493 |
|
space/space/space/space/space/space/space/models/response_model.go
CHANGED
|
@@ -45,11 +45,6 @@ type QuestionResponse struct {
|
|
| 45 |
IsDoubt bool `json:"is_doubt"`
|
| 46 |
}
|
| 47 |
|
| 48 |
-
type QuizResultResponse struct {
|
| 49 |
-
QuizAttempt QuizAttempt `json:"quiz_attempt"`
|
| 50 |
-
Result QuizResult `json:"result"`
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
type OnExamUserAnswerResponse struct {
|
| 54 |
ID uint `gorm:"primaryKey" json:"id"`
|
| 55 |
QuizAttemptID uint `json:"quiz_attempt_id"`
|
|
|
|
| 45 |
IsDoubt bool `json:"is_doubt"`
|
| 46 |
}
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
type OnExamUserAnswerResponse struct {
|
| 49 |
ID uint `gorm:"primaryKey" json:"id"`
|
| 50 |
QuizAttemptID uint `json:"quiz_attempt_id"`
|
space/space/space/space/space/space/space/repositories/academy_repository.go
CHANGED
|
@@ -26,12 +26,14 @@ type AcademyRepository interface {
|
|
| 26 |
|
| 27 |
// === USER ===
|
| 28 |
UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
|
|
|
|
| 29 |
UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
|
| 30 |
UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
|
| 31 |
UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error)
|
| 32 |
UserSaveAcademyMaterialProgress(ctx context.Context, req *models.AcademyMaterialProgress) error
|
| 33 |
UserGetAcademyMaterialProgressByAccountID(ctx context.Context, accountID int64, materialID int64) (*models.AcademyMaterialProgress, error)
|
| 34 |
UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
type academyRepository struct {
|
|
@@ -286,6 +288,37 @@ func (r *academyRepository) UserListAcademy(ctx context.Context, req *models.Lis
|
|
| 286 |
return academies, pageInfo, nil
|
| 287 |
}
|
| 288 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
func (r *academyRepository) UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error) {
|
| 290 |
materials := make([]models.UserAcademyMaterialResponse, 0)
|
| 291 |
offset := req.Filter.GetOffset()
|
|
@@ -414,3 +447,18 @@ func (r *academyRepository) UserGetAcademyMaterialProgressByAccountID(ctx contex
|
|
| 414 |
func (r *academyRepository) UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error {
|
| 415 |
return r.db.WithContext(ctx).Where("account_id = ? AND academy_material_id = ?", accountID, materialID).Delete(&models.AcademyMaterialProgress{}).Error
|
| 416 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
// === USER ===
|
| 28 |
UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
|
| 29 |
+
UserGetPercentageProgressAcademyByID(ctx context.Context, accountID int64, academyID int64) (float64, error)
|
| 30 |
UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
|
| 31 |
UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
|
| 32 |
UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error)
|
| 33 |
UserSaveAcademyMaterialProgress(ctx context.Context, req *models.AcademyMaterialProgress) error
|
| 34 |
UserGetAcademyMaterialProgressByAccountID(ctx context.Context, accountID int64, materialID int64) (*models.AcademyMaterialProgress, error)
|
| 35 |
UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error
|
| 36 |
+
UserResetAcademyProgressByID(ctx context.Context, accountID int64, academyID int64) error
|
| 37 |
}
|
| 38 |
|
| 39 |
type academyRepository struct {
|
|
|
|
| 288 |
return academies, pageInfo, nil
|
| 289 |
}
|
| 290 |
|
| 291 |
+
func (r *academyRepository) UserGetPercentageProgressAcademyByID(ctx context.Context, accountID int64, academyID int64) (float64, error) {
|
| 292 |
+
var totalMaterial int64
|
| 293 |
+
var totalReadMaterial int64
|
| 294 |
+
|
| 295 |
+
// Hitung total materi dalam academy
|
| 296 |
+
err := r.db.WithContext(ctx).
|
| 297 |
+
Model(&models.AcademyMaterial{}).
|
| 298 |
+
Where("academy_id = ?", academyID).
|
| 299 |
+
Count(&totalMaterial).Error
|
| 300 |
+
if err != nil {
|
| 301 |
+
return 0, err
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
// Hitung total materi yang sudah dibaca oleh user
|
| 305 |
+
err = r.db.WithContext(ctx).
|
| 306 |
+
Model(&models.AcademyMaterialProgress{}).
|
| 307 |
+
Joins("JOIN academy_materials am ON am.id = academy_materials_progress.academy_material_id").
|
| 308 |
+
Where("academy_materials_progress.account_id = ? AND am.academy_id = ?", accountID, academyID).
|
| 309 |
+
Count(&totalReadMaterial).Error
|
| 310 |
+
if err != nil {
|
| 311 |
+
return 0, err
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
// Hitung persentase
|
| 315 |
+
if totalMaterial == 0 {
|
| 316 |
+
return 0, nil
|
| 317 |
+
}
|
| 318 |
+
percentage := float64(totalReadMaterial) / float64(totalMaterial) * 100
|
| 319 |
+
return percentage, nil
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
func (r *academyRepository) UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error) {
|
| 323 |
materials := make([]models.UserAcademyMaterialResponse, 0)
|
| 324 |
offset := req.Filter.GetOffset()
|
|
|
|
| 447 |
func (r *academyRepository) UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error {
|
| 448 |
return r.db.WithContext(ctx).Where("account_id = ? AND academy_material_id = ?", accountID, materialID).Delete(&models.AcademyMaterialProgress{}).Error
|
| 449 |
}
|
| 450 |
+
|
| 451 |
+
func (r *academyRepository) UserResetAcademyProgressByID(ctx context.Context, accountID int64, academyID int64) error {
|
| 452 |
+
// Subquery untuk mendapatkan semua material ID dari academyID
|
| 453 |
+
subQuery := r.db.
|
| 454 |
+
WithContext(ctx).
|
| 455 |
+
Model(&models.AcademyMaterial{}).
|
| 456 |
+
Select("id").
|
| 457 |
+
Where("academy_id = ?", academyID)
|
| 458 |
+
|
| 459 |
+
// Delete progress berdasarkan account_id dan academy_material_id yang ada di subquery
|
| 460 |
+
return r.db.
|
| 461 |
+
WithContext(ctx).
|
| 462 |
+
Where("account_id = ? AND academy_material_id IN (?)", accountID, subQuery).
|
| 463 |
+
Delete(&models.AcademyMaterialProgress{}).Error
|
| 464 |
+
}
|
space/space/space/space/space/space/space/repositories/quiz_repository.go
CHANGED
|
@@ -8,7 +8,15 @@ import (
|
|
| 8 |
)
|
| 9 |
|
| 10 |
type QuizRepository interface {
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
}
|
| 13 |
|
| 14 |
type quizRepository struct {
|
|
@@ -19,144 +27,129 @@ func NewQuizRepository(db *gorm.DB) QuizRepository {
|
|
| 19 |
return &quizRepository{db: db}
|
| 20 |
}
|
| 21 |
|
| 22 |
-
func (r *quizRepository)
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
repo.Transactions(
|
| 31 |
-
WhereGivenConstructor[models.Quiz, []models.Quiz],
|
| 32 |
-
Find[models.Quiz, []models.Quiz],
|
| 33 |
-
)
|
| 34 |
-
return *repo
|
| 35 |
-
}
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
WhereGivenConstructor[models.QuizAttempt, []models.QuizAttempt],
|
| 43 |
-
Find[models.QuizAttempt, []models.QuizAttempt],
|
| 44 |
-
)
|
| 45 |
-
return *repo
|
| 46 |
}
|
| 47 |
|
| 48 |
-
func
|
| 49 |
-
|
| 50 |
-
models.Quiz{ID: quizId},
|
| 51 |
-
)
|
| 52 |
-
repo.Transactions(
|
| 53 |
-
WhereGivenConstructor[models.Quiz, models.Quiz],
|
| 54 |
-
Find[models.Quiz, models.Quiz],
|
| 55 |
-
)
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
repo := Construct[models.QuizAttempt, models.QuizAttempt](
|
| 62 |
-
models.QuizAttempt{AccountID: userId, QuizID: quizId},
|
| 63 |
-
)
|
| 64 |
-
repo.Transaction.Where(&repo.Constructor).Last(&repo.Result)
|
| 65 |
-
repo.RowsError = repo.Transaction.Error
|
| 66 |
-
repo.NoRecord = false
|
| 67 |
-
// fmt.Println(repo.Transaction.RowsAffected) Kenapa 0 !!!!
|
| 68 |
-
return *repo
|
| 69 |
}
|
| 70 |
|
| 71 |
-
func
|
| 72 |
-
|
| 73 |
-
models.QuizAttempt{
|
| 74 |
-
ID: attemptId,
|
| 75 |
-
AccountID: userId,
|
| 76 |
-
},
|
| 77 |
-
)
|
| 78 |
-
repo.Transactions(
|
| 79 |
-
WhereGivenConstructor[models.QuizAttempt, models.QuizAttempt],
|
| 80 |
-
Find[models.QuizAttempt, models.QuizAttempt],
|
| 81 |
-
)
|
| 82 |
-
return *repo
|
| 83 |
}
|
| 84 |
|
| 85 |
-
func
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
-
|
| 103 |
-
return *repo
|
| 104 |
}
|
| 105 |
|
| 106 |
-
func
|
| 107 |
-
|
| 108 |
-
models.UserAnswer{
|
| 109 |
-
QuizAttemptID: attemptId,
|
| 110 |
-
QuestionID: questionId,
|
| 111 |
-
},
|
| 112 |
-
)
|
| 113 |
-
repo.Transactions(
|
| 114 |
-
WhereGivenConstructor[models.UserAnswer, models.UserAnswer],
|
| 115 |
-
Find[models.UserAnswer, models.UserAnswer],
|
| 116 |
-
)
|
| 117 |
-
return *repo
|
| 118 |
}
|
| 119 |
|
| 120 |
-
func
|
| 121 |
-
|
| 122 |
-
userAnswer,
|
| 123 |
-
)
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
| 128 |
|
| 129 |
-
|
| 130 |
-
repo := Construct[models.UserAnswer, models.UserAnswer](
|
| 131 |
-
userAnswer,
|
| 132 |
-
)
|
| 133 |
-
Update(repo)
|
| 134 |
-
return *repo
|
| 135 |
}
|
| 136 |
|
| 137 |
-
func
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
)
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
}
|
| 144 |
|
| 145 |
-
|
| 146 |
-
repo := Construct[models.QuizAttempt, models.QuizResult](
|
| 147 |
-
models.QuizAttempt{ID: attemptId},
|
| 148 |
-
)
|
| 149 |
-
repo.Transaction.Model(&repo.Constructor).Raw("SELECT quiz_attempt_id,COUNT(*) AS total_questions,SUM(CASE WHEN is_correct = true THEN 1 ELSE 0 END) AS correct_answers,CAST(SUM(CASE WHEN is_correct = true THEN 1 ELSE 0 END) AS FLOAT) / COUNT(*) AS average_score FROM user_answers WHERE quiz_attempt_id = ? GROUP BY quiz_attempt_id", attemptId).Scan(&repo.Result)
|
| 150 |
-
repo.RowsError = repo.Transaction.Error
|
| 151 |
-
repo.NoRecord = false
|
| 152 |
-
return *repo
|
| 153 |
}
|
| 154 |
|
| 155 |
-
func
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
-
|
| 161 |
-
return
|
| 162 |
}
|
|
|
|
| 8 |
)
|
| 9 |
|
| 10 |
type QuizRepository interface {
|
| 11 |
+
UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
|
| 12 |
+
UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error)
|
| 13 |
+
UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
|
| 14 |
+
UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error)
|
| 15 |
+
UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error
|
| 16 |
+
UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error)
|
| 17 |
+
UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error)
|
| 18 |
+
UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error)
|
| 19 |
+
UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error
|
| 20 |
}
|
| 21 |
|
| 22 |
type quizRepository struct {
|
|
|
|
| 27 |
return &quizRepository{db: db}
|
| 28 |
}
|
| 29 |
|
| 30 |
+
func (r *quizRepository) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
|
| 31 |
+
var quizResponse models.UserGetQuizResponse
|
| 32 |
+
|
| 33 |
+
rawQuery := `
|
| 34 |
+
SELECT
|
| 35 |
+
q.*,
|
| 36 |
+
(SELECT COUNT(*) FROM questions ques WHERE ques.quiz_id = q.id) AS total_questions,
|
| 37 |
+
COALESCE(COUNT(qa.id), 0) AS user_attempts
|
| 38 |
+
FROM
|
| 39 |
+
quizzes q
|
| 40 |
+
LEFT JOIN
|
| 41 |
+
quiz_attempts qa
|
| 42 |
+
ON q.id = qa.quiz_id
|
| 43 |
+
AND qa.account_id = ?
|
| 44 |
+
WHERE
|
| 45 |
+
q.academy_id = ?
|
| 46 |
+
GROUP BY
|
| 47 |
+
q.id, q.academy_id, q.slug, q.title, q.description,
|
| 48 |
+
q.attempt_limit, q.time_limit, q.min_score, q.created_at, q.updated_at;
|
| 49 |
+
`
|
| 50 |
|
| 51 |
+
err := r.db.Raw(rawQuery, req.AccountID, req.AcademyID).Scan(&quizResponse).Error
|
| 52 |
+
if err != nil {
|
| 53 |
+
return nil, err
|
| 54 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
+
if quizResponse.ID == 0 {
|
| 57 |
+
return nil, gorm.ErrRecordNotFound
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
return &quizResponse, nil
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
}
|
| 62 |
|
| 63 |
+
func (r *quizRepository) UserGetActiveAttemptQuiz(ctx context.Context, accountID int64, quizID int64) (*models.QuizAttempt, error) {
|
| 64 |
+
var quizAttempt models.QuizAttempt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
+
err := r.db.Where("account_id = ? AND quiz_id = ? AND finished_at IS NULL", accountID, quizID).First(&quizAttempt).Error
|
| 67 |
+
if err != nil {
|
| 68 |
+
return nil, err
|
| 69 |
+
}
|
| 70 |
|
| 71 |
+
return &quizAttempt, nil
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
}
|
| 73 |
|
| 74 |
+
func (r *quizRepository) UserUpdateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
|
| 75 |
+
return r.db.Model(&models.QuizAttempt{}).Where("id = ?", attempt.ID).Updates(attempt).Error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
}
|
| 77 |
|
| 78 |
+
func (r *quizRepository) UserGetAttemptQuizQuestionsResponse(ctx context.Context, accountID int64, quizID int64, attemptID int64) ([]models.UserAttemptQuizQuestionsResponse, error) {
|
| 79 |
+
questions := make([]models.UserAttemptQuizQuestionsResponse, 0)
|
| 80 |
+
|
| 81 |
+
rawQuery := `
|
| 82 |
+
SELECT
|
| 83 |
+
q.id,
|
| 84 |
+
COALESCE(ua.is_doubt, FALSE) AS is_doubt,
|
| 85 |
+
CASE
|
| 86 |
+
WHEN ua.id IS NOT NULL
|
| 87 |
+
AND EXISTS (
|
| 88 |
+
SELECT 1
|
| 89 |
+
FROM quiz_attempts qa
|
| 90 |
+
WHERE qa.id = ?
|
| 91 |
+
AND qa.account_id = ?
|
| 92 |
+
AND qa.quiz_id = ?
|
| 93 |
+
) THEN TRUE
|
| 94 |
+
ELSE FALSE
|
| 95 |
+
END AS is_answered
|
| 96 |
+
FROM
|
| 97 |
+
questions q
|
| 98 |
+
LEFT JOIN
|
| 99 |
+
user_answers ua
|
| 100 |
+
ON q.id = ua.question_id
|
| 101 |
+
AND ua.quiz_attempt_id = ?
|
| 102 |
+
WHERE
|
| 103 |
+
q.quiz_id = ?
|
| 104 |
+
ORDER BY
|
| 105 |
+
q.id;
|
| 106 |
+
`
|
| 107 |
+
|
| 108 |
+
err := r.db.Raw(rawQuery, attemptID, accountID, quizID, attemptID, quizID).Scan(&questions).Error
|
| 109 |
+
if err != nil {
|
| 110 |
+
return nil, err
|
| 111 |
+
}
|
| 112 |
|
| 113 |
+
return questions, nil
|
|
|
|
| 114 |
}
|
| 115 |
|
| 116 |
+
func (r *quizRepository) UserCreateAttemptQuiz(ctx context.Context, attempt *models.QuizAttempt) error {
|
| 117 |
+
return r.db.Create(attempt).Error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
}
|
| 119 |
|
| 120 |
+
func (r *quizRepository) UserGetTotalAttemptsQuiz(ctx context.Context, accountID int64, quizID int64) (int64, error) {
|
| 121 |
+
var totalAttempts int64
|
|
|
|
|
|
|
| 122 |
|
| 123 |
+
err := r.db.Model(&models.QuizAttempt{}).Where("account_id = ? AND quiz_id = ? AND finished_at IS NOT NULL", accountID, quizID).Count(&totalAttempts).Error
|
| 124 |
+
if err != nil {
|
| 125 |
+
return 0, err
|
| 126 |
+
}
|
| 127 |
|
| 128 |
+
return totalAttempts, nil
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
|
| 131 |
+
func (r *quizRepository) UserGetTotalQuestionQuiz(ctx context.Context, quizID int64) (int64, error) {
|
| 132 |
+
var totalQuestion int64
|
| 133 |
+
|
| 134 |
+
err := r.db.Model(&models.Question{}).Where("quiz_id = ?", quizID).Count(&totalQuestion).Error
|
| 135 |
+
if err != nil {
|
| 136 |
+
return 0, err
|
| 137 |
+
}
|
| 138 |
|
| 139 |
+
return totalQuestion, nil
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
}
|
| 141 |
|
| 142 |
+
func (r *quizRepository) UserGetTotalCorrectAnswerQuiz(ctx context.Context, quizAttemptID int64) (int64, error) {
|
| 143 |
+
var totalCorrectAnswer int64
|
| 144 |
+
|
| 145 |
+
err := r.db.Model(&models.UserAnswer{}).Where("quiz_attempt_id = ? AND is_correct = TRUE", quizAttemptID).Count(&totalCorrectAnswer).Error
|
| 146 |
+
if err != nil {
|
| 147 |
+
return 0, err
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
return totalCorrectAnswer, nil
|
| 151 |
+
}
|
| 152 |
|
| 153 |
+
func (r *quizRepository) UserDeleteAttemptQuizByAccountIDAndQuizID(ctx context.Context, accountID int64, quizID int64) error {
|
| 154 |
+
return r.db.Where("account_id = ? AND quiz_id = ?", accountID, quizID).Delete(&models.QuizAttempt{}).Error
|
| 155 |
}
|
space/space/space/space/space/space/space/router/quiz_route.go
CHANGED
|
@@ -1,22 +1,14 @@
|
|
| 1 |
package router
|
| 2 |
|
| 3 |
import (
|
| 4 |
-
QuizController "api.qobiltu.id/controller/quiz"
|
| 5 |
"api.qobiltu.id/middleware"
|
| 6 |
-
"github.com/gin-gonic/gin"
|
| 7 |
)
|
| 8 |
|
| 9 |
-
func
|
| 10 |
-
|
| 11 |
{
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
routerGroup.GET("/:academy_id/:quiz_id/review", middleware.AuthUser, QuizController.Review)
|
| 16 |
-
routerGroup.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)
|
| 17 |
-
routerGroup.GET("result/:attempt_id", middleware.AuthUser, QuizController.Result)
|
| 18 |
-
routerGroup.GET("result", middleware.AuthUser, QuizController.Result)
|
| 19 |
-
routerGroup.POST("/submit-attempt/:attempt_id", middleware.AuthUser, QuizController.Submit)
|
| 20 |
-
routerGroup.GET("/navigation/:attempt_id", middleware.AuthUser, QuizController.Navigation)
|
| 21 |
}
|
| 22 |
}
|
|
|
|
| 1 |
package router
|
| 2 |
|
| 3 |
import (
|
|
|
|
| 4 |
"api.qobiltu.id/middleware"
|
|
|
|
| 5 |
)
|
| 6 |
|
| 7 |
+
func (s *Server) QuizRoute() {
|
| 8 |
+
userRouterGroup := s.router.Group("/api/v1/quiz")
|
| 9 |
{
|
| 10 |
+
// :id is academy id
|
| 11 |
+
userRouterGroup.GET("/:id", middleware.AuthUser, s.quizController.UserGetQuiz)
|
| 12 |
+
userRouterGroup.POST("/:id/attempt", middleware.AuthUser, s.quizController.UserAttemptQuiz)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
}
|
| 14 |
}
|
space/space/space/space/space/space/space/router/router.go
CHANGED
|
@@ -10,11 +10,11 @@ func (s *Server) setupRoutes() {
|
|
| 10 |
|
| 11 |
AuthRoute(s.router)
|
| 12 |
UserRoute(s.router)
|
| 13 |
-
QuizRoute(s.router)
|
| 14 |
|
| 15 |
s.OptionsRoute()
|
| 16 |
s.EmailRoute()
|
| 17 |
s.AcademyRoute()
|
|
|
|
| 18 |
s.CVRoute()
|
| 19 |
s.MarriageReadinessProfileRoute()
|
| 20 |
s.PartnerCriteriaRoute()
|
|
|
|
| 10 |
|
| 11 |
AuthRoute(s.router)
|
| 12 |
UserRoute(s.router)
|
|
|
|
| 13 |
|
| 14 |
s.OptionsRoute()
|
| 15 |
s.EmailRoute()
|
| 16 |
s.AcademyRoute()
|
| 17 |
+
s.QuizRoute()
|
| 18 |
s.CVRoute()
|
| 19 |
s.MarriageReadinessProfileRoute()
|
| 20 |
s.PartnerCriteriaRoute()
|
space/space/space/space/space/space/space/router/server.go
CHANGED
|
@@ -7,6 +7,7 @@ import (
|
|
| 7 |
marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
|
| 8 |
options_controller "api.qobiltu.id/controller/options"
|
| 9 |
partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
|
|
|
|
| 10 |
region_controller "api.qobiltu.id/controller/region"
|
| 11 |
"github.com/gin-gonic/gin"
|
| 12 |
)
|
|
@@ -17,6 +18,7 @@ type Server struct {
|
|
| 17 |
optionsController options_controller.OptionsController
|
| 18 |
emailController email_controller.EmailController
|
| 19 |
academyController academy_controller.AcademyController
|
|
|
|
| 20 |
cvController cv_controller.CVController
|
| 21 |
marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController
|
| 22 |
partnerCriteriaController partner_criteria_controller.PartnerCriteriaController
|
|
@@ -27,6 +29,7 @@ func NewServer(
|
|
| 27 |
optionsController options_controller.OptionsController,
|
| 28 |
emailController email_controller.EmailController,
|
| 29 |
academyController academy_controller.AcademyController,
|
|
|
|
| 30 |
cvController cv_controller.CVController,
|
| 31 |
marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController,
|
| 32 |
partnerCriteriaController partner_criteria_controller.PartnerCriteriaController,
|
|
@@ -40,6 +43,7 @@ func NewServer(
|
|
| 40 |
optionsController: optionsController,
|
| 41 |
emailController: emailController,
|
| 42 |
academyController: academyController,
|
|
|
|
| 43 |
cvController: cvController,
|
| 44 |
marriageReadinessProfileController: marriageReadinessProfileController,
|
| 45 |
partnerCriteriaController: partnerCriteriaController,
|
|
|
|
| 7 |
marriage_readiness_profile_controller "api.qobiltu.id/controller/marriage_readiness_profile"
|
| 8 |
options_controller "api.qobiltu.id/controller/options"
|
| 9 |
partner_criteria_controller "api.qobiltu.id/controller/partner_criteria"
|
| 10 |
+
quiz_controller "api.qobiltu.id/controller/quiz"
|
| 11 |
region_controller "api.qobiltu.id/controller/region"
|
| 12 |
"github.com/gin-gonic/gin"
|
| 13 |
)
|
|
|
|
| 18 |
optionsController options_controller.OptionsController
|
| 19 |
emailController email_controller.EmailController
|
| 20 |
academyController academy_controller.AcademyController
|
| 21 |
+
quizController quiz_controller.QuizController
|
| 22 |
cvController cv_controller.CVController
|
| 23 |
marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController
|
| 24 |
partnerCriteriaController partner_criteria_controller.PartnerCriteriaController
|
|
|
|
| 29 |
optionsController options_controller.OptionsController,
|
| 30 |
emailController email_controller.EmailController,
|
| 31 |
academyController academy_controller.AcademyController,
|
| 32 |
+
quizController quiz_controller.QuizController,
|
| 33 |
cvController cv_controller.CVController,
|
| 34 |
marriageReadinessProfileController marriage_readiness_profile_controller.MarriageReadinessProfileController,
|
| 35 |
partnerCriteriaController partner_criteria_controller.PartnerCriteriaController,
|
|
|
|
| 43 |
optionsController: optionsController,
|
| 44 |
emailController: emailController,
|
| 45 |
academyController: academyController,
|
| 46 |
+
quizController: quizController,
|
| 47 |
cvController: cvController,
|
| 48 |
marriageReadinessProfileController: marriageReadinessProfileController,
|
| 49 |
partnerCriteriaController: partnerCriteriaController,
|
space/space/space/space/space/space/space/services/cv_service.go
CHANGED
|
@@ -3,6 +3,7 @@ package services
|
|
| 3 |
import (
|
| 4 |
"context"
|
| 5 |
"errors"
|
|
|
|
| 6 |
"math"
|
| 7 |
"strconv"
|
| 8 |
"strings"
|
|
@@ -318,6 +319,7 @@ func (s *cvService) GetPhysicalAndHealth(ctx context.Context, req *models.GetPhy
|
|
| 318 |
|
| 319 |
func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.SaveWorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
|
| 320 |
if err := s.validator.Validate(req); err != nil {
|
|
|
|
| 321 |
return nil, response.HandleValidationError(err)
|
| 322 |
}
|
| 323 |
|
|
@@ -339,16 +341,19 @@ func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, re
|
|
| 339 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.CongregationalPrayer, req.CongregationalPrayer)
|
| 340 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.TahajjudPrayer, req.TahajjudPrayer)
|
| 341 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DhuhaPrayer, req.DhuhaPrayer)
|
| 342 |
-
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranMemorization, req.QuranMemorization)
|
| 343 |
-
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranReadingAbility, req.QuranReadingAbility)
|
| 344 |
-
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.WeeklyReligiousStudyFrequency, req.WeeklyReligiousStudyFrequency)
|
| 345 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DaudFasting, req.DaudFasting)
|
| 346 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.AyyamulBidhFasting, req.AyyamulBidhFasting)
|
|
|
|
|
|
|
|
|
|
| 347 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.HajjOrUmrah, req.HajjOrUmrah)
|
|
|
|
| 348 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.ListeningToMusic, req.ListeningToMusic)
|
| 349 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnIkhtilat, req.OpinionOnIkhtilat)
|
| 350 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnTouchingNonMahram, req.OpinionOnTouchingNonMahram)
|
| 351 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnVeil, req.OpinionOnVeil)
|
|
|
|
|
|
|
| 352 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.WeeklyReligiousStudies, req.WeeklyReligiousStudies)
|
| 353 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.FollowedUstadz, req.FollowedUstadz)
|
| 354 |
|
|
@@ -694,24 +699,27 @@ func calculateProgress(
|
|
| 694 |
accountDetailsPercentage := float64(len(accountDetails.GetFilledFields())) / float64(accountDetails.TotalFields()) * 100
|
| 695 |
personalityAndPreferenceCVPercentage := float64(len(personalityAndPreferenceCV.GetFilledFields())) / float64(personalityAndPreferenceCV.TotalFields()) * 100
|
| 696 |
physicalAndHealthCVPercentage := float64(len(physicalAndHealthCV.GetFilledFields())) / float64(physicalAndHealthCV.TotalFields()) * 100
|
| 697 |
-
|
|
|
|
|
|
|
| 698 |
|
| 699 |
educationCVPercentage := fullIfPresent(educationCV)
|
| 700 |
familyMemberCVPercentage := fullIfPresent(familyMemberCV)
|
| 701 |
jobCVPercentage := fullIfPresent(jobCV)
|
| 702 |
achievementCVPercentage := fullIfPresent(achievementCV)
|
| 703 |
|
| 704 |
-
overallProgress := (accountDetailsPercentage + personalityAndPreferenceCVPercentage + physicalAndHealthCVPercentage +
|
| 705 |
|
| 706 |
return &models.GetProgressCVResponse{
|
| 707 |
-
AccountDetailsProgress:
|
| 708 |
-
PersonalityAndPreferenceCVProgress:
|
| 709 |
-
FamilyMemberCVProgress:
|
| 710 |
-
PhysicalAndHealthCVProgress:
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
|
|
|
| 716 |
}
|
| 717 |
}
|
|
|
|
| 3 |
import (
|
| 4 |
"context"
|
| 5 |
"errors"
|
| 6 |
+
"fmt"
|
| 7 |
"math"
|
| 8 |
"strconv"
|
| 9 |
"strings"
|
|
|
|
| 319 |
|
| 320 |
func (s *cvService) SaveWorshipAndReligiousUnderstanding(ctx context.Context, req *models.SaveWorshipAndReligiousUnderstandingRequest) (*models.WorshipAndReligiousUnderstandingCV, error) {
|
| 321 |
if err := s.validator.Validate(req); err != nil {
|
| 322 |
+
fmt.Println(err)
|
| 323 |
return nil, response.HandleValidationError(err)
|
| 324 |
}
|
| 325 |
|
|
|
|
| 341 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.CongregationalPrayer, req.CongregationalPrayer)
|
| 342 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.TahajjudPrayer, req.TahajjudPrayer)
|
| 343 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DhuhaPrayer, req.DhuhaPrayer)
|
|
|
|
|
|
|
|
|
|
| 344 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.DaudFasting, req.DaudFasting)
|
| 345 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.AyyamulBidhFasting, req.AyyamulBidhFasting)
|
| 346 |
+
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranReadingAbility, req.QuranReadingAbility)
|
| 347 |
+
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.QuranMemorization, req.QuranMemorization)
|
| 348 |
+
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.WeeklyReligiousStudyFrequency, req.WeeklyReligiousStudyFrequency)
|
| 349 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.HajjOrUmrah, req.HajjOrUmrah)
|
| 350 |
+
|
| 351 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.ListeningToMusic, req.ListeningToMusic)
|
| 352 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnIkhtilat, req.OpinionOnIkhtilat)
|
| 353 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnTouchingNonMahram, req.OpinionOnTouchingNonMahram)
|
| 354 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnVeil, req.OpinionOnVeil)
|
| 355 |
+
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnBeard, req.OpinionOnBeard)
|
| 356 |
+
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.OpinionOnPantsAboveAnkle, req.OpinionOnPantsAboveAnkle)
|
| 357 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.WeeklyReligiousStudies, req.WeeklyReligiousStudies)
|
| 358 |
utils.AssignIfNotNil(&worshipAndReligiousUnderstanding.FollowedUstadz, req.FollowedUstadz)
|
| 359 |
|
|
|
|
| 699 |
accountDetailsPercentage := float64(len(accountDetails.GetFilledFields())) / float64(accountDetails.TotalFields()) * 100
|
| 700 |
personalityAndPreferenceCVPercentage := float64(len(personalityAndPreferenceCV.GetFilledFields())) / float64(personalityAndPreferenceCV.TotalFields()) * 100
|
| 701 |
physicalAndHealthCVPercentage := float64(len(physicalAndHealthCV.GetFilledFields())) / float64(physicalAndHealthCV.TotalFields()) * 100
|
| 702 |
+
|
| 703 |
+
worshipCVPercentage := float64(len(worshipAndReligiousUnderstandingCV.GetFilledFieldsWorship())) / float64(worshipAndReligiousUnderstandingCV.GetTotalFieldsWorship()) * 100
|
| 704 |
+
religiousUnderstandingCVPercentage := float64(len(worshipAndReligiousUnderstandingCV.GetFilledFieldsReligiousUnderstanding())) / float64(worshipAndReligiousUnderstandingCV.GetTotalFieldsReligiousUnderstanding()) * 100
|
| 705 |
|
| 706 |
educationCVPercentage := fullIfPresent(educationCV)
|
| 707 |
familyMemberCVPercentage := fullIfPresent(familyMemberCV)
|
| 708 |
jobCVPercentage := fullIfPresent(jobCV)
|
| 709 |
achievementCVPercentage := fullIfPresent(achievementCV)
|
| 710 |
|
| 711 |
+
overallProgress := (accountDetailsPercentage + personalityAndPreferenceCVPercentage + physicalAndHealthCVPercentage + worshipCVPercentage + religiousUnderstandingCVPercentage + educationCVPercentage + familyMemberCVPercentage + jobCVPercentage + achievementCVPercentage) / 9
|
| 712 |
|
| 713 |
return &models.GetProgressCVResponse{
|
| 714 |
+
AccountDetailsProgress: math.Round(accountDetailsPercentage*100) / 100,
|
| 715 |
+
PersonalityAndPreferenceCVProgress: math.Round(personalityAndPreferenceCVPercentage*100) / 100,
|
| 716 |
+
FamilyMemberCVProgress: math.Round(familyMemberCVPercentage*100) / 100,
|
| 717 |
+
PhysicalAndHealthCVProgress: math.Round(physicalAndHealthCVPercentage*100) / 100,
|
| 718 |
+
WorshipCVProgress: math.Round(worshipCVPercentage*100) / 100,
|
| 719 |
+
ReligiousUnderstandingCVProgress: math.Round(religiousUnderstandingCVPercentage*100) / 100,
|
| 720 |
+
EducationCVProgress: math.Round(educationCVPercentage*100) / 100,
|
| 721 |
+
JobCVProgress: math.Round(jobCVPercentage*100) / 100,
|
| 722 |
+
AchievementCVProgress: math.Round(achievementCVPercentage*100) / 100,
|
| 723 |
+
TotalProgress: math.Round(overallProgress*100) / 100,
|
| 724 |
}
|
| 725 |
}
|
space/space/space/space/space/space/space/services/quiz_service.go
CHANGED
|
@@ -1,37 +1,200 @@
|
|
| 1 |
package services
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
/
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
//
|
| 18 |
-
|
| 19 |
-
//
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
//
|
| 24 |
-
//
|
| 25 |
-
|
| 26 |
-
//
|
| 27 |
-
//
|
| 28 |
-
//
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
package services
|
| 2 |
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"errors"
|
| 6 |
+
"math/rand"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
"api.qobiltu.id/models"
|
| 10 |
+
"api.qobiltu.id/pkg/validation"
|
| 11 |
+
"api.qobiltu.id/repositories"
|
| 12 |
+
"api.qobiltu.id/response"
|
| 13 |
+
"gorm.io/gorm"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
type QuizService interface {
|
| 17 |
+
// === ADMIN ===
|
| 18 |
+
|
| 19 |
+
// // === USER ===
|
| 20 |
+
UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error)
|
| 21 |
+
UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error)
|
| 22 |
+
|
| 23 |
+
// UserGetQuestionQuiz(ctx context.Context, req *models.GetQuestionQuizRequest) (*models.GetQuestionQuizResponse, error)
|
| 24 |
+
// UserAnswerQuiz(ctx context.Context, req *models.AnswerQuizRequest) (*models.AnswerQuizResponse, error)
|
| 25 |
+
// UserReviewQuiz(ctx context.Context, req *models.ReviewQuizRequest) (*models.ReviewQuizResponse, error)
|
| 26 |
+
// UserSubmitQuiz(ctx context.Context, req *models.SubmitQuizRequest) (*models.SubmitQuizResponse, error)
|
| 27 |
+
// UserResultQuiz(ctx context.Context, req *models.ResultQuizRequest) (*models.ResultQuizResponse, error)
|
| 28 |
+
// UserListResultQuiz(ctx context.Context, req *models.ListResultQuizRequest) (*models.ListResultQuizResponse, *models.Paging, error)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
type quizService struct {
|
| 32 |
+
quizRepository repositories.QuizRepository
|
| 33 |
+
academyRepository repositories.AcademyRepository
|
| 34 |
+
validator *validation.Validator
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
func NewQuizService(quizRepository repositories.QuizRepository, academyRepository repositories.AcademyRepository, validator *validation.Validator) QuizService {
|
| 38 |
+
return &quizService{quizRepository: quizRepository, academyRepository: academyRepository, validator: validator}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
func (s *quizService) UserGetQuiz(ctx context.Context, req *models.UserGetQuizRequest) (*models.UserGetQuizResponse, error) {
|
| 42 |
+
quiz, err := s.quizRepository.UserGetQuiz(ctx, req)
|
| 43 |
+
if err != nil {
|
| 44 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return quiz, nil
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
func (s *quizService) UserAttemptQuiz(ctx context.Context, req *models.UserAttemptQuizRequest) (*models.UserAttemptQuizResponse, error) {
|
| 51 |
+
quizAttempt, err := s.quizRepository.UserGetQuiz(ctx, &models.UserGetQuizRequest{
|
| 52 |
+
AccountID: req.AccountID,
|
| 53 |
+
AcademyID: req.AcademyID,
|
| 54 |
+
})
|
| 55 |
+
if err != nil {
|
| 56 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
quiz := &quizAttempt.Quiz
|
| 60 |
+
|
| 61 |
+
existingAttempt, err := s.quizRepository.UserGetActiveAttemptQuiz(ctx, req.AccountID, quiz.ID)
|
| 62 |
+
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
| 63 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
if existingAttempt != nil {
|
| 67 |
+
return s.handleExistingAttempt(ctx, req, quiz, existingAttempt)
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
return s.handleNewAttempt(ctx, req, quiz)
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
func (s *quizService) handleExistingAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz, attempt *models.QuizAttempt) (*models.UserAttemptQuizResponse, error) {
|
| 74 |
+
now := time.Now()
|
| 75 |
+
|
| 76 |
+
if attempt.DueAt.Before(now) {
|
| 77 |
+
if attempt.FinishedAt == nil {
|
| 78 |
+
attempt.FinishedAt = &now
|
| 79 |
+
attempt.Score = s.calculateQuizScore(ctx, attempt)
|
| 80 |
+
|
| 81 |
+
if err := s.quizRepository.UserUpdateAttemptQuiz(ctx, attempt); err != nil {
|
| 82 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
|
| 86 |
+
if err != nil {
|
| 87 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
if totalAttempts >= quiz.AttemptLimit {
|
| 91 |
+
if err := s.academyRepository.UserResetAcademyProgressByID(ctx, req.AccountID, quiz.AcademyID); err != nil {
|
| 92 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// hapus juga semua attempt quiz by account id dan quiz id
|
| 96 |
+
if err := s.quizRepository.UserDeleteAttemptQuizByAccountIDAndQuizID(ctx, req.AccountID, quiz.ID); err != nil {
|
| 97 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
return nil, models.Exception{QuizTimeExpired: true, Message: "Quiz time has expired"}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
return nil, models.Exception{QuizAlreadyFinished: true, Message: "Quiz already finished"}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, attempt.ID)
|
| 108 |
+
if err != nil {
|
| 109 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
questions = shuffleWithKey(questions, attempt.ID)
|
| 113 |
+
|
| 114 |
+
return &models.UserAttemptQuizResponse{
|
| 115 |
+
QuizAttempt: *attempt,
|
| 116 |
+
Questions: questions,
|
| 117 |
+
}, nil
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
func (s *quizService) handleNewAttempt(ctx context.Context, req *models.UserAttemptQuizRequest, quiz *models.Quiz) (*models.UserAttemptQuizResponse, error) {
|
| 121 |
+
totalAttempts, err := s.quizRepository.UserGetTotalAttemptsQuiz(ctx, req.AccountID, quiz.ID)
|
| 122 |
+
if err != nil {
|
| 123 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 124 |
+
}
|
| 125 |
+
if totalAttempts >= quiz.AttemptLimit {
|
| 126 |
+
return nil, models.Exception{QuizAttemptLimit: true, Message: "Attempt limit reached"}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
percentage, err := s.academyRepository.UserGetPercentageProgressAcademyByID(ctx, req.AccountID, quiz.AcademyID)
|
| 130 |
+
if err != nil {
|
| 131 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 132 |
+
}
|
| 133 |
+
if percentage < 100 {
|
| 134 |
+
return nil, models.Exception{AcademyNotFinished: true, Message: "Academy not finished"}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
quizAttempt := models.QuizAttempt{
|
| 138 |
+
AccountID: req.AccountID,
|
| 139 |
+
QuizID: quiz.ID,
|
| 140 |
+
StartedAt: time.Now(),
|
| 141 |
+
DueAt: time.Now().Add(time.Duration(quiz.TimeLimit) * time.Minute),
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
if err := s.quizRepository.UserCreateAttemptQuiz(ctx, &quizAttempt); err != nil {
|
| 145 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
questions, err := s.quizRepository.UserGetAttemptQuizQuestionsResponse(ctx, req.AccountID, quiz.ID, quizAttempt.ID)
|
| 149 |
+
if err != nil {
|
| 150 |
+
return nil, response.HandleGormError(err, "Internal Server Error")
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
questions = shuffleWithKey(questions, quizAttempt.ID)
|
| 154 |
+
|
| 155 |
+
return &models.UserAttemptQuizResponse{
|
| 156 |
+
QuizAttempt: quizAttempt,
|
| 157 |
+
Questions: questions,
|
| 158 |
+
}, nil
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
func (s *quizService) calculateQuizScore(ctx context.Context, attempt *models.QuizAttempt) float64 {
|
| 162 |
+
// ambil total question dari quiz
|
| 163 |
+
totalQuestion, err := s.quizRepository.UserGetTotalQuestionQuiz(ctx, attempt.QuizID)
|
| 164 |
+
if err != nil {
|
| 165 |
+
return 0
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
if totalQuestion == 0 {
|
| 169 |
+
return 0
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
// ambil semua user answer yang is_correct nya true
|
| 173 |
+
correctAnswer, err := s.quizRepository.UserGetTotalCorrectAnswerQuiz(ctx, attempt.ID)
|
| 174 |
+
if err != nil {
|
| 175 |
+
return 0
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
// hitung score nya
|
| 179 |
+
score := float64(correctAnswer) / float64(totalQuestion) * 100
|
| 180 |
+
|
| 181 |
+
return score
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// shuffleWithKey mengacak slice dengan menggunakan integer key sebagai seed
|
| 185 |
+
// untuk memastikan hasil acak yang konsisten untuk key yang sama
|
| 186 |
+
func shuffleWithKey[T any](slice []T, key int64) []T {
|
| 187 |
+
// Buat salinan slice untuk menghindari modifikasi original
|
| 188 |
+
shuffled := make([]T, len(slice))
|
| 189 |
+
copy(shuffled, slice)
|
| 190 |
+
|
| 191 |
+
// Buat random source dengan seed dari key
|
| 192 |
+
r := rand.New(rand.NewSource(key))
|
| 193 |
+
|
| 194 |
+
// Lakukan shuffling
|
| 195 |
+
r.Shuffle(len(shuffled), func(i, j int) {
|
| 196 |
+
shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
|
| 197 |
+
})
|
| 198 |
+
|
| 199 |
+
return shuffled
|
| 200 |
+
}
|
space/space/space/space/space/space/space/space/space/controller/academy/academy_controller.go
CHANGED
|
@@ -349,15 +349,21 @@ func (c *academyController) UserGetAcademyMaterialBySlug(ctx *gin.Context) {
|
|
| 349 |
}
|
| 350 |
|
| 351 |
func (c *academyController) UserToggleAcademyMaterialProgress(ctx *gin.Context) {
|
| 352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
|
| 354 |
accountData := middleware.GetAccountData(ctx)
|
| 355 |
accountID := int64(accountData.UserID)
|
| 356 |
|
| 357 |
-
req
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
}
|
| 361 |
|
| 362 |
err := c.academyService.UserToggleAcademyMaterialProgress(ctx, &req)
|
| 363 |
if err != nil {
|
|
|
|
| 349 |
}
|
| 350 |
|
| 351 |
func (c *academyController) UserToggleAcademyMaterialProgress(ctx *gin.Context) {
|
| 352 |
+
var req models.ToggleAcademyMaterialProgressRequest
|
| 353 |
+
if err := ctx.ShouldBindJSON(&req); err != nil {
|
| 354 |
+
response.HandleError(ctx, err)
|
| 355 |
+
return
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
academySlug := ctx.Param("slug")
|
| 359 |
+
materialSlug := ctx.Param("materialSlug")
|
| 360 |
|
| 361 |
accountData := middleware.GetAccountData(ctx)
|
| 362 |
accountID := int64(accountData.UserID)
|
| 363 |
|
| 364 |
+
req.AccountID = accountID
|
| 365 |
+
req.AcademySlug = academySlug
|
| 366 |
+
req.MaterialSlug = materialSlug
|
|
|
|
| 367 |
|
| 368 |
err := c.academyService.UserToggleAcademyMaterialProgress(ctx, &req)
|
| 369 |
if err != nil {
|
space/space/space/space/space/space/space/space/space/models/database_orm_model.go
CHANGED
|
@@ -80,6 +80,7 @@ type (
|
|
| 80 |
Slug string `gorm:"column:slug;uniqueIndex" json:"slug"`
|
| 81 |
Description string `gorm:"column:description" json:"description"`
|
| 82 |
Order uint `gorm:"column:order" json:"order"`
|
|
|
|
| 83 |
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
| 84 |
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
| 85 |
}
|
|
@@ -101,7 +102,8 @@ type (
|
|
| 101 |
Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty" counter:"skip"`
|
| 102 |
AcademyMaterialID int64 `gorm:"primaryKey;column:academy_material_id" json:"academy_material_id"`
|
| 103 |
AcademyMaterial *AcademyMaterial `gorm:"foreignKey:AcademyMaterialID;constraint:OnDelete:CASCADE" json:"academy_material,omitempty"`
|
| 104 |
-
|
|
|
|
| 105 |
}
|
| 106 |
)
|
| 107 |
|
|
|
|
| 80 |
Slug string `gorm:"column:slug;uniqueIndex" json:"slug"`
|
| 81 |
Description string `gorm:"column:description" json:"description"`
|
| 82 |
Order uint `gorm:"column:order" json:"order"`
|
| 83 |
+
Image *string `gorm:"column:image" json:"image"`
|
| 84 |
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
| 85 |
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
| 86 |
}
|
|
|
|
| 102 |
Account *Account `gorm:"foreignKey:AccountID;constraint:OnDelete:CASCADE" json:"account,omitempty" counter:"skip"`
|
| 103 |
AcademyMaterialID int64 `gorm:"primaryKey;column:academy_material_id" json:"academy_material_id"`
|
| 104 |
AcademyMaterial *AcademyMaterial `gorm:"foreignKey:AcademyMaterialID;constraint:OnDelete:CASCADE" json:"academy_material,omitempty"`
|
| 105 |
+
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
|
| 106 |
+
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updated_at"`
|
| 107 |
}
|
| 108 |
)
|
| 109 |
|
space/space/space/space/space/space/space/space/space/models/request_model.go
CHANGED
|
@@ -56,8 +56,26 @@ type (
|
|
| 56 |
}
|
| 57 |
|
| 58 |
UserAcademyMaterialResponse struct {
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
}
|
| 62 |
|
| 63 |
CreateAcademyMaterialRequest struct {
|
|
@@ -108,8 +126,10 @@ type (
|
|
| 108 |
}
|
| 109 |
|
| 110 |
ToggleAcademyMaterialProgressRequest struct {
|
| 111 |
-
AccountID
|
| 112 |
-
|
|
|
|
|
|
|
| 113 |
}
|
| 114 |
)
|
| 115 |
|
|
@@ -131,6 +151,38 @@ func NewListAcademyContentRequest() ListAcademyContentRequest {
|
|
| 131 |
}
|
| 132 |
}
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
type (
|
| 135 |
MultipleOptionsRequest struct {
|
| 136 |
OptionName string `json:"option_name" validate:"required"`
|
|
|
|
| 56 |
}
|
| 57 |
|
| 58 |
UserAcademyMaterialResponse struct {
|
| 59 |
+
ID int64 `gorm:"column:id" json:"id"`
|
| 60 |
+
AcademyID int64 `gorm:"column:academy_id" json:"academy_id"`
|
| 61 |
+
Title string `gorm:"column:title" json:"title"`
|
| 62 |
+
Slug string `gorm:"column:slug" json:"slug"`
|
| 63 |
+
Order uint `gorm:"column:order" json:"order"`
|
| 64 |
+
CreatedAt *time.Time `gorm:"column:read_created_at" json:"created_at"`
|
| 65 |
+
UpdatedAt *time.Time `gorm:"column:read_updated_at" json:"updated_at"`
|
| 66 |
+
IsRead bool `gorm:"is_read" json:"is_read"`
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
UserAcademyMaterialDetailResponse struct {
|
| 70 |
+
ID int64 `gorm:"column:id" json:"id"`
|
| 71 |
+
AcademyID int64 `gorm:"column:academy_id" json:"academy_id"`
|
| 72 |
+
Title string `gorm:"column:title" json:"title"`
|
| 73 |
+
Slug string `gorm:"column:slug" json:"slug"`
|
| 74 |
+
Content string `gorm:"column:content" json:"content"`
|
| 75 |
+
Order uint `gorm:"column:order" json:"order"`
|
| 76 |
+
CreatedAt *time.Time `gorm:"column:read_created_at" json:"created_at"`
|
| 77 |
+
UpdatedAt *time.Time `gorm:"column:read_updated_at" json:"updated_at"`
|
| 78 |
+
IsRead bool `gorm:"is_read" json:"is_read"`
|
| 79 |
}
|
| 80 |
|
| 81 |
CreateAcademyMaterialRequest struct {
|
|
|
|
| 126 |
}
|
| 127 |
|
| 128 |
ToggleAcademyMaterialProgressRequest struct {
|
| 129 |
+
AccountID int64 `json:"-"`
|
| 130 |
+
MaterialSlug string `json:"-"`
|
| 131 |
+
AcademySlug string `json:"-"`
|
| 132 |
+
IsRead bool `json:"is_read"`
|
| 133 |
}
|
| 134 |
)
|
| 135 |
|
|
|
|
| 151 |
}
|
| 152 |
}
|
| 153 |
|
| 154 |
+
type (
|
| 155 |
+
ListQuizRequest struct {
|
| 156 |
+
AccountID int64 `json:"account_id"`
|
| 157 |
+
AcademyID int64 `json:"academy_id" validate:"required"`
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
ListQuizResponse struct {
|
| 161 |
+
Quiz []Quiz `json:"quiz"`
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
AttemptQuizRequest struct {
|
| 165 |
+
AccountID int64 `json:"account_id"`
|
| 166 |
+
QuizID int64 `json:"quiz_id" validate:"required"`
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
AttemptQuizResponse struct {
|
| 170 |
+
QuizAttempt QuizAttempt `json:"quiz_attempt"`
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
GetQuestionQuizRequest struct {
|
| 174 |
+
AccountID int64 `json:"account_id"`
|
| 175 |
+
QuizID int64 `json:"quiz_id" validate:"required"`
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
GetQuestionQuizResponse struct {
|
| 179 |
+
Question Question `json:"question"`
|
| 180 |
+
Answer []Answer `json:"answer_options"`
|
| 181 |
+
UserAnswer int `json:"current_user_answer"`
|
| 182 |
+
IsDoubt bool `json:"is_doubt"`
|
| 183 |
+
}
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
type (
|
| 187 |
MultipleOptionsRequest struct {
|
| 188 |
OptionName string `json:"option_name" validate:"required"`
|
space/space/space/space/space/space/space/space/space/repositories/academy_repository.go
CHANGED
|
@@ -28,7 +28,7 @@ type AcademyRepository interface {
|
|
| 28 |
UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
|
| 29 |
UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
|
| 30 |
UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
|
| 31 |
-
UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.
|
| 32 |
UserSaveAcademyMaterialProgress(ctx context.Context, req *models.AcademyMaterialProgress) error
|
| 33 |
UserGetAcademyMaterialProgressByAccountID(ctx context.Context, accountID int64, materialID int64) (*models.AcademyMaterialProgress, error)
|
| 34 |
UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error
|
|
@@ -245,7 +245,7 @@ func (r *academyRepository) UserListAcademy(ctx context.Context, req *models.Lis
|
|
| 245 |
Model(&models.Academy{}).
|
| 246 |
Select(`academy.*,
|
| 247 |
COUNT(DISTINCT am.id) AS total_material,
|
| 248 |
-
COUNT(DISTINCT CASE WHEN amp.
|
| 249 |
Joins("LEFT JOIN academy_materials am ON am.academy_id = academy.id").
|
| 250 |
Joins(`LEFT JOIN "academy_materials_progress" amp ON amp.academy_material_id = am.id AND amp.account_id = ?`, req.AccountID).
|
| 251 |
Group("academy.id")
|
|
@@ -294,14 +294,16 @@ func (r *academyRepository) UserListAcademyMaterial(ctx context.Context, req *mo
|
|
| 294 |
q := r.db.WithContext(ctx).
|
| 295 |
Model(&models.AcademyMaterial{}).
|
| 296 |
Select(`academy_materials.*,
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
|
|
|
|
|
|
| 301 |
Joins("JOIN academy ON academy.id = academy_materials.academy_id").
|
| 302 |
Joins(`LEFT JOIN academy_materials_progress amp ON
|
| 303 |
-
|
| 304 |
-
|
| 305 |
Where("academy.slug = ?", req.Slug)
|
| 306 |
|
| 307 |
if req.Filter.HasKeyword() {
|
|
@@ -359,8 +361,8 @@ func (r *academyRepository) UserGetAcademyBySlug(ctx context.Context, slug strin
|
|
| 359 |
return &academy, nil
|
| 360 |
}
|
| 361 |
|
| 362 |
-
func (r *academyRepository) UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.
|
| 363 |
-
var result models.
|
| 364 |
|
| 365 |
err := r.db.WithContext(ctx).
|
| 366 |
Model(&models.AcademyMaterial{}).
|
|
@@ -369,22 +371,28 @@ func (r *academyRepository) UserGetAcademyMaterialBySlug(ctx context.Context, sl
|
|
| 369 |
academy.title as academy_title,
|
| 370 |
academy.slug as academy_slug,
|
| 371 |
CASE
|
| 372 |
-
WHEN amp.
|
| 373 |
ELSE false
|
| 374 |
-
END AS is_read
|
|
|
|
|
|
|
| 375 |
`).
|
| 376 |
Joins("JOIN academy ON academy.id = academy_materials.academy_id").
|
| 377 |
Joins(`LEFT JOIN academy_materials_progress amp ON
|
| 378 |
amp.academy_material_id = academy_materials.id AND
|
| 379 |
amp.account_id = ?`, accountID).
|
| 380 |
Where("academy_materials.slug = ?", slug).
|
| 381 |
-
|
| 382 |
Error
|
| 383 |
|
| 384 |
if err != nil {
|
| 385 |
return nil, err
|
| 386 |
}
|
| 387 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
return &result, nil
|
| 389 |
}
|
| 390 |
|
|
|
|
| 28 |
UserListAcademy(ctx context.Context, req *models.ListAcademyRequest) ([]models.UserAcademyResponse, *models.Paging, error)
|
| 29 |
UserListAcademyMaterial(ctx context.Context, req *models.ListAcademyMaterialRequest) ([]models.UserAcademyMaterialResponse, *models.Paging, error)
|
| 30 |
UserGetAcademyBySlug(ctx context.Context, slug string) (*models.AcademyResponse, error)
|
| 31 |
+
UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error)
|
| 32 |
UserSaveAcademyMaterialProgress(ctx context.Context, req *models.AcademyMaterialProgress) error
|
| 33 |
UserGetAcademyMaterialProgressByAccountID(ctx context.Context, accountID int64, materialID int64) (*models.AcademyMaterialProgress, error)
|
| 34 |
UserDeleteAcademyMaterialProgress(ctx context.Context, accountID int64, materialID int64) error
|
|
|
|
| 245 |
Model(&models.Academy{}).
|
| 246 |
Select(`academy.*,
|
| 247 |
COUNT(DISTINCT am.id) AS total_material,
|
| 248 |
+
COUNT(DISTINCT CASE WHEN amp.created_at IS NOT NULL THEN amp.academy_material_id END) AS total_read_material`).
|
| 249 |
Joins("LEFT JOIN academy_materials am ON am.academy_id = academy.id").
|
| 250 |
Joins(`LEFT JOIN "academy_materials_progress" amp ON amp.academy_material_id = am.id AND amp.account_id = ?`, req.AccountID).
|
| 251 |
Group("academy.id")
|
|
|
|
| 294 |
q := r.db.WithContext(ctx).
|
| 295 |
Model(&models.AcademyMaterial{}).
|
| 296 |
Select(`academy_materials.*,
|
| 297 |
+
amp.created_at AS read_created_at,
|
| 298 |
+
amp.updated_at AS read_updated_at,
|
| 299 |
+
CASE
|
| 300 |
+
WHEN amp.created_at IS NOT NULL THEN true
|
| 301 |
+
ELSE false
|
| 302 |
+
END AS is_read`).
|
| 303 |
Joins("JOIN academy ON academy.id = academy_materials.academy_id").
|
| 304 |
Joins(`LEFT JOIN academy_materials_progress amp ON
|
| 305 |
+
amp.academy_material_id = academy_materials.id AND
|
| 306 |
+
amp.account_id = ?`, req.AccountID).
|
| 307 |
Where("academy.slug = ?", req.Slug)
|
| 308 |
|
| 309 |
if req.Filter.HasKeyword() {
|
|
|
|
| 361 |
return &academy, nil
|
| 362 |
}
|
| 363 |
|
| 364 |
+
func (r *academyRepository) UserGetAcademyMaterialBySlug(ctx context.Context, slug string, accountID int64) (*models.UserAcademyMaterialDetailResponse, error) {
|
| 365 |
+
var result models.UserAcademyMaterialDetailResponse
|
| 366 |
|
| 367 |
err := r.db.WithContext(ctx).
|
| 368 |
Model(&models.AcademyMaterial{}).
|
|
|
|
| 371 |
academy.title as academy_title,
|
| 372 |
academy.slug as academy_slug,
|
| 373 |
CASE
|
| 374 |
+
WHEN amp.created_at IS NOT NULL THEN true
|
| 375 |
ELSE false
|
| 376 |
+
END AS is_read,
|
| 377 |
+
amp.created_at AS read_created_at,
|
| 378 |
+
amp.updated_at AS read_updated_at
|
| 379 |
`).
|
| 380 |
Joins("JOIN academy ON academy.id = academy_materials.academy_id").
|
| 381 |
Joins(`LEFT JOIN academy_materials_progress amp ON
|
| 382 |
amp.academy_material_id = academy_materials.id AND
|
| 383 |
amp.account_id = ?`, accountID).
|
| 384 |
Where("academy_materials.slug = ?", slug).
|
| 385 |
+
Scan(&result).
|
| 386 |
Error
|
| 387 |
|
| 388 |
if err != nil {
|
| 389 |
return nil, err
|
| 390 |
}
|
| 391 |
|
| 392 |
+
if result.ID == 0 {
|
| 393 |
+
return nil, gorm.ErrRecordNotFound
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
return &result, nil
|
| 397 |
}
|
| 398 |
|
space/space/space/space/space/space/space/space/space/repositories/quiz_repository.go
CHANGED
|
@@ -1,9 +1,28 @@
|
|
| 1 |
package repositories
|
| 2 |
|
| 3 |
import (
|
|
|
|
|
|
|
| 4 |
"api.qobiltu.id/models"
|
|
|
|
| 5 |
)
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
func GetQuizbyAcademyId(academyId uint) Repository[models.Quiz, []models.Quiz] {
|
| 8 |
repo := Construct[models.Quiz, []models.Quiz](
|
| 9 |
models.Quiz{AcademyID: academyId},
|
|
|
|
| 1 |
package repositories
|
| 2 |
|
| 3 |
import (
|
| 4 |
+
"context"
|
| 5 |
+
|
| 6 |
"api.qobiltu.id/models"
|
| 7 |
+
"gorm.io/gorm"
|
| 8 |
)
|
| 9 |
|
| 10 |
+
type QuizRepository interface {
|
| 11 |
+
UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error)
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
type quizRepository struct {
|
| 15 |
+
db *gorm.DB
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
func NewQuizRepository(db *gorm.DB) QuizRepository {
|
| 19 |
+
return &quizRepository{db: db}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
func (r *quizRepository) UserListQuiz(ctx context.Context, req *models.ListQuizRequest) (*models.ListQuizResponse, *models.Paging, error) {
|
| 23 |
+
panic("not implemented")
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
func GetQuizbyAcademyId(academyId uint) Repository[models.Quiz, []models.Quiz] {
|
| 27 |
repo := Construct[models.Quiz, []models.Quiz](
|
| 28 |
models.Quiz{AcademyID: academyId},
|
space/space/space/space/space/space/space/space/space/router/quiz_route.go
CHANGED
|
@@ -15,7 +15,7 @@ func QuizRoute(router *gin.Engine) {
|
|
| 15 |
routerGroup.GET("/:academy_id/:quiz_id/review", middleware.AuthUser, QuizController.Review)
|
| 16 |
routerGroup.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)
|
| 17 |
routerGroup.GET("result/:attempt_id", middleware.AuthUser, QuizController.Result)
|
| 18 |
-
routerGroup.GET("result
|
| 19 |
routerGroup.POST("/submit-attempt/:attempt_id", middleware.AuthUser, QuizController.Submit)
|
| 20 |
routerGroup.GET("/navigation/:attempt_id", middleware.AuthUser, QuizController.Navigation)
|
| 21 |
}
|
|
|
|
| 15 |
routerGroup.GET("/:academy_id/:quiz_id/review", middleware.AuthUser, QuizController.Review)
|
| 16 |
routerGroup.PUT("/:academy_id/:quiz_id/choose-answer", middleware.AuthUser, QuizController.Answer)
|
| 17 |
routerGroup.GET("result/:attempt_id", middleware.AuthUser, QuizController.Result)
|
| 18 |
+
routerGroup.GET("result", middleware.AuthUser, QuizController.Result)
|
| 19 |
routerGroup.POST("/submit-attempt/:attempt_id", middleware.AuthUser, QuizController.Submit)
|
| 20 |
routerGroup.GET("/navigation/:attempt_id", middleware.AuthUser, QuizController.Navigation)
|
| 21 |
}
|