initial assessment implementation
This commit is contained in:
parent
7309a2bc83
commit
19ac718526
|
|
@ -1,5 +1,74 @@
|
|||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
|
||||
INSERT INTO users (
|
||||
id,
|
||||
first_name,
|
||||
last_name,
|
||||
user_name,
|
||||
email,
|
||||
phone_number,
|
||||
role,
|
||||
password,
|
||||
status,
|
||||
email_verified,
|
||||
phone_verified,
|
||||
profile_completed,
|
||||
preferred_language,
|
||||
created_at
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
10,
|
||||
'Demo',
|
||||
'Student',
|
||||
'demo_student',
|
||||
'student10@yimaru.com',
|
||||
NULL,
|
||||
'USER',
|
||||
crypt('password@123', gen_salt('bf'))::bytea,
|
||||
'ACTIVE',
|
||||
TRUE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
'en',
|
||||
CURRENT_TIMESTAMP
|
||||
),
|
||||
(
|
||||
11,
|
||||
'System',
|
||||
'Admin',
|
||||
'sys_admin',
|
||||
'admin@yimaru.com',
|
||||
'0911001100',
|
||||
'ADMIN',
|
||||
crypt('password@123', gen_salt('bf'))::bytea,
|
||||
'ACTIVE',
|
||||
TRUE,
|
||||
TRUE,
|
||||
TRUE,
|
||||
'en',
|
||||
CURRENT_TIMESTAMP
|
||||
),
|
||||
(
|
||||
12,
|
||||
'Support',
|
||||
'Agent',
|
||||
'support_agent',
|
||||
'support@yimaru.com',
|
||||
'0911223344',
|
||||
'SUPPORT',
|
||||
crypt('password@123', gen_salt('bf'))::bytea,
|
||||
'ACTIVE',
|
||||
TRUE,
|
||||
TRUE,
|
||||
TRUE,
|
||||
'en',
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
|
||||
-- ======================================================
|
||||
-- Global Settings (LMS)
|
||||
-- ======================================================
|
||||
|
|
@ -14,195 +83,144 @@ VALUES
|
|||
ON CONFLICT (key) DO NOTHING;
|
||||
-- ======================================================
|
||||
|
||||
INSERT INTO users (
|
||||
id,
|
||||
first_name,
|
||||
last_name,
|
||||
user_name,
|
||||
email,
|
||||
phone_number,
|
||||
role,
|
||||
password,
|
||||
age,
|
||||
education_level,
|
||||
country,
|
||||
region,
|
||||
knowledge_level,
|
||||
nick_name,
|
||||
occupation,
|
||||
learning_goal,
|
||||
language_goal,
|
||||
language_challange,
|
||||
favoutite_topic,
|
||||
initial_assessment_completed,
|
||||
email_verified,
|
||||
phone_verified,
|
||||
status,
|
||||
last_login,
|
||||
profile_completed,
|
||||
profile_picture_url,
|
||||
preferred_language,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
1,
|
||||
'Sarah',
|
||||
'Connor',
|
||||
'SarahC',
|
||||
'yaredyemane1@gmail.com',
|
||||
NULL,
|
||||
'SUPER_ADMIN',
|
||||
crypt('password@123', gen_salt('bf'))::bytea,
|
||||
35,
|
||||
'Masters',
|
||||
'USA',
|
||||
'California',
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
FALSE,
|
||||
TRUE,
|
||||
FALSE,
|
||||
'ACTIVE',
|
||||
NULL,
|
||||
FALSE,
|
||||
NULL,
|
||||
'en',
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
),
|
||||
(
|
||||
2,
|
||||
'Test',
|
||||
'Instructor',
|
||||
'InstructorT',
|
||||
'instructor@yimaru.com',
|
||||
'0988554466',
|
||||
'INSTRUCTOR',
|
||||
crypt('password@123', gen_salt('bf'))::bytea,
|
||||
30,
|
||||
'Bachelors',
|
||||
'USA',
|
||||
'New York',
|
||||
NULL,
|
||||
NULL,
|
||||
'Instructor',
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
FALSE,
|
||||
TRUE,
|
||||
TRUE,
|
||||
'ACTIVE',
|
||||
NULL,
|
||||
FALSE,
|
||||
NULL,
|
||||
'en',
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
),
|
||||
(
|
||||
3,
|
||||
'Demo',
|
||||
'Student',
|
||||
'DemoS',
|
||||
'student@yimaru.com',
|
||||
NULL,
|
||||
'STUDENT',
|
||||
crypt('password@123', gen_salt('bf'))::bytea,
|
||||
22,
|
||||
'High School',
|
||||
'USA',
|
||||
'Texas',
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
FALSE,
|
||||
TRUE,
|
||||
FALSE,
|
||||
'ACTIVE',
|
||||
NULL,
|
||||
FALSE,
|
||||
NULL,
|
||||
'en',
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET first_name = EXCLUDED.first_name,
|
||||
last_name = EXCLUDED.last_name,
|
||||
user_name = EXCLUDED.user_name,
|
||||
email = EXCLUDED.email,
|
||||
phone_number = EXCLUDED.phone_number,
|
||||
role = EXCLUDED.role,
|
||||
password = EXCLUDED.password,
|
||||
age = EXCLUDED.age,
|
||||
education_level = EXCLUDED.education_level,
|
||||
country = EXCLUDED.country,
|
||||
region = EXCLUDED.region,
|
||||
knowledge_level = EXCLUDED.knowledge_level,
|
||||
nick_name = EXCLUDED.nick_name,
|
||||
occupation = EXCLUDED.occupation,
|
||||
learning_goal = EXCLUDED.learning_goal,
|
||||
language_goal = EXCLUDED.language_goal,
|
||||
language_challange = EXCLUDED.language_challange,
|
||||
favoutite_topic = EXCLUDED.favoutite_topic,
|
||||
initial_assessment_completed = EXCLUDED.initial_assessment_completed,
|
||||
email_verified = EXCLUDED.email_verified,
|
||||
phone_verified = EXCLUDED.phone_verified,
|
||||
status = EXCLUDED.status,
|
||||
last_login = EXCLUDED.last_login,
|
||||
profile_completed = EXCLUDED.profile_completed,
|
||||
profile_picture_url = EXCLUDED.profile_picture_url,
|
||||
preferred_language = EXCLUDED.preferred_language,
|
||||
updated_at = CURRENT_TIMESTAMP;
|
||||
-- ======================================================
|
||||
-- Assessment Questions – Level A2 (EASY)
|
||||
-- ======================================================
|
||||
|
||||
-- ======================================================
|
||||
-- Courses
|
||||
-- ======================================================
|
||||
-- ======================================================
|
||||
-- Course Categories
|
||||
-- ======================================================
|
||||
INSERT INTO course_categories (
|
||||
id,
|
||||
name,
|
||||
is_active,
|
||||
created_at
|
||||
)
|
||||
INSERT INTO assessment_questions (id, title, question_type, difficulty_level, points, is_active)
|
||||
VALUES
|
||||
(1, 'Learning English', TRUE, CURRENT_TIMESTAMP),
|
||||
(2, 'Other Courses', TRUE, CURRENT_TIMESTAMP)
|
||||
(1, 'What would you say to greet someone before lunchtime?', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
|
||||
(2, 'Which question is correct to ask about your routine?', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
|
||||
(3, 'She ___ like pizza.', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
|
||||
(4, 'I usually go to school and start class ____ eight o’clock.', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
|
||||
(5, 'Someone says, “Here is the book you asked for.” What is the best response?', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO assessment_question_options (question_id, option_text, option_order, is_correct)
|
||||
VALUES
|
||||
-- Q1
|
||||
(1, 'Good morning.', 1, TRUE),
|
||||
(1, 'How do you do?', 2, FALSE),
|
||||
(1, 'Good afternoon.', 3, FALSE),
|
||||
(1, 'Goodbye.', 4, FALSE),
|
||||
|
||||
-- Q2
|
||||
(2, 'What time you wake up?', 1, FALSE),
|
||||
(2, 'What time do you wake up?', 2, TRUE),
|
||||
(2, 'What time are you wake up?', 3, FALSE),
|
||||
(2, 'What time waking you?', 4, FALSE),
|
||||
|
||||
-- Q3
|
||||
(3, 'do not', 1, FALSE),
|
||||
(3, 'not', 2, FALSE),
|
||||
(3, 'is not', 3, FALSE),
|
||||
(3, 'does not', 4, TRUE),
|
||||
|
||||
-- Q4
|
||||
(4, 'about', 1, FALSE),
|
||||
(4, 'on', 2, FALSE),
|
||||
(4, 'at', 3, TRUE),
|
||||
(4, 'in', 4, FALSE),
|
||||
|
||||
-- Q5
|
||||
(5, 'Never mind.', 1, FALSE),
|
||||
(5, 'Really?', 2, FALSE),
|
||||
(5, 'What a pity!', 3, FALSE),
|
||||
(5, 'Thank you.', 4, TRUE);
|
||||
|
||||
-- ======================================================
|
||||
-- Notifications (Sample)
|
||||
-- Assessment Questions – Level B1 (MEDIUM)
|
||||
-- ======================================================
|
||||
INSERT INTO notifications (
|
||||
user_id,
|
||||
type,
|
||||
level,
|
||||
channel,
|
||||
title,
|
||||
message,
|
||||
created_at
|
||||
)
|
||||
VALUES (
|
||||
3,
|
||||
'course_enrolled',
|
||||
'info',
|
||||
'in_app',
|
||||
'Welcome to your course',
|
||||
'You have successfully enrolled in Introduction to Go Programming.',
|
||||
CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO assessment_questions (id, title, question_type, difficulty_level, points, is_active)
|
||||
VALUES
|
||||
(6, 'How do you introduce your friend to another person?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
|
||||
(7, 'How would you ask for the price of an item in a shop?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
|
||||
(8, 'Which sentence correctly gives simple directions?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
|
||||
(9, 'The watch shows 10:50, but the real time is 10:45. What can you say?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
|
||||
(10, 'Which instruction is correct when giving directions?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO assessment_question_options (question_id, option_text, option_order, is_correct)
|
||||
VALUES
|
||||
-- Q6
|
||||
(6, 'Hello, my name is Samson.', 1, FALSE),
|
||||
(6, 'Good morning. Nice to meet you.', 2, FALSE),
|
||||
(6, 'Let me introduce myself to my friend.', 3, FALSE),
|
||||
(6, 'This is my friend, Samson.', 4, TRUE),
|
||||
|
||||
-- Q7
|
||||
(7, 'How many are these?', 1, FALSE),
|
||||
(7, 'What is this?', 2, FALSE),
|
||||
(7, 'How much is this?', 3, TRUE),
|
||||
(7, 'Where is the nearest shop?', 4, FALSE),
|
||||
|
||||
-- Q8
|
||||
(8, 'Thank you very much for asking.', 1, FALSE),
|
||||
(8, 'Turn left and walk two blocks.', 2, TRUE),
|
||||
(8, 'Why don’t you eat out.', 3, FALSE),
|
||||
(8, 'Take the bus to the park.', 4, FALSE),
|
||||
|
||||
-- Q9
|
||||
(9, 'My watch is slow.', 1, TRUE),
|
||||
(9, 'My watch is late.', 2, FALSE),
|
||||
(9, 'My watch is fast.', 3, FALSE),
|
||||
(9, 'My watch is early.', 4, FALSE),
|
||||
|
||||
-- Q10
|
||||
(10, 'Turn left.', 1, TRUE),
|
||||
(10, 'Turn on left.', 2, FALSE),
|
||||
(10, 'Turn left side.', 3, FALSE),
|
||||
(10, 'Turn to straight.', 4, FALSE);
|
||||
|
||||
-- ======================================================
|
||||
-- Assessment Questions – Level B2 (HARD)
|
||||
-- ======================================================
|
||||
|
||||
INSERT INTO assessment_questions (id, title, question_type, difficulty_level, points, is_active)
|
||||
VALUES
|
||||
(11, 'What is the most polite way to ask to speak to someone on the phone?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
|
||||
(12, 'How do you correctly state the age of a person who is 30 years old?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
|
||||
(13, 'When asking for help with a new Yimaru App feature, which option is most appropriate?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
|
||||
(14, 'Which word has the unvoiced “th” sound?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
|
||||
(15, 'Which sentence sounds like a warning, not friendly advice?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
|
||||
(16, 'What does this sentence mean? “I will definitely be there on time.”', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO assessment_question_options (question_id, option_text, option_order, is_correct)
|
||||
VALUES
|
||||
-- Q11
|
||||
(11, 'May I speak to Mr. Tesfaye, please?', 1, TRUE),
|
||||
(11, 'Can I talk to Mr. Tesfaye?', 2, FALSE),
|
||||
(11, 'Is Mr. Tesfaye there?', 3, FALSE),
|
||||
(11, 'I want to talk to Mr. Tesfaye.', 4, FALSE),
|
||||
|
||||
-- Q12
|
||||
(12, 'He is thirty years.', 1, FALSE),
|
||||
(12, 'He has thirty years.', 2, FALSE),
|
||||
(12, 'He has thirty years old.', 3, FALSE),
|
||||
(12, 'He is thirty.', 4, TRUE),
|
||||
|
||||
-- Q13
|
||||
(13, 'Are you familiar with how this feature works?', 1, FALSE),
|
||||
(13, 'Could you walk me through how this feature works?', 2, TRUE),
|
||||
(13, 'I believe I understand how this feature works.', 3, FALSE),
|
||||
(13, 'I’ve tried similar features before.', 4, FALSE),
|
||||
|
||||
-- Q14
|
||||
(14, 'That', 1, FALSE),
|
||||
(14, 'They', 2, FALSE),
|
||||
(14, 'These', 3, FALSE),
|
||||
(14, 'Three', 4, TRUE),
|
||||
|
||||
-- Q15
|
||||
(15, 'You might want to plan your time better.', 1, FALSE),
|
||||
(15, 'If I were you, I’d start earlier.', 2, FALSE),
|
||||
(15, 'You’d better meet the deadline this time.', 3, TRUE),
|
||||
(15, 'Why don’t you try using a planner?', 4, FALSE),
|
||||
|
||||
-- Q16
|
||||
(16, 'The speaker is unsure about arriving.', 1, FALSE),
|
||||
(16, 'The speaker is promising to arrive on time.', 2, TRUE),
|
||||
(16, 'The speaker might arrive late.', 3, FALSE),
|
||||
(16, 'The speaker has already arrived.', 4, FALSE);
|
||||
|
|
|
|||
|
|
@ -51,14 +51,14 @@ SELECT setval(
|
|||
)
|
||||
FROM notifications;
|
||||
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('referral_codes', 'id'),
|
||||
COALESCE(MAX(id), 1)
|
||||
)
|
||||
FROM referral_codes;
|
||||
-- SELECT setval(
|
||||
-- pg_get_serial_sequence('referral_codes', 'id'),
|
||||
-- COALESCE(MAX(id), 1)
|
||||
-- )
|
||||
-- FROM referral_codes;
|
||||
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('user_referrals', 'id'),
|
||||
COALESCE(MAX(id), 1)
|
||||
)
|
||||
FROM user_referrals;
|
||||
-- SELECT setval(
|
||||
-- pg_get_serial_sequence('user_referrals', 'id'),
|
||||
-- COALESCE(MAX(id), 1)
|
||||
-- )
|
||||
-- FROM user_referrals;
|
||||
|
|
|
|||
|
|
@ -36,42 +36,126 @@ CREATE TABLE IF NOT EXISTS users (
|
|||
CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
|
||||
);
|
||||
|
||||
CREATE TABLE assessment_questions (
|
||||
CREATE TABLE IF NOT EXISTS assessment_questions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
question_type VARCHAR(50) NOT NULL, -- MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER
|
||||
difficulty_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED
|
||||
|
||||
question_type VARCHAR(50) NOT NULL,
|
||||
-- MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER
|
||||
|
||||
difficulty_level VARCHAR(50),
|
||||
-- EASY, MEDIUM, HARD
|
||||
|
||||
points INT NOT NULL DEFAULT 1,
|
||||
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE TABLE assessment_question_options (
|
||||
CREATE TABLE IF NOT EXISTS assessment_question_options (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
|
||||
|
||||
option_text TEXT NOT NULL,
|
||||
is_correct BOOLEAN NOT NULL DEFAULT FALSE
|
||||
option_order INT NOT NULL,
|
||||
|
||||
is_correct BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE (question_id, option_order)
|
||||
);
|
||||
|
||||
CREATE TABLE assessment_attempts (
|
||||
CREATE TABLE IF NOT EXISTS assessment_short_answers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
|
||||
|
||||
correct_answer TEXT NOT NULL,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
ALTER TABLE assessment_questions
|
||||
ADD CONSTRAINT chk_question_type
|
||||
CHECK (question_type IN ('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER'));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assessment_attempts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
total_questions INT NOT NULL,
|
||||
correct_answers INT NOT NULL,
|
||||
score_percentage NUMERIC(5,2) NOT NULL,
|
||||
knowledge_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED
|
||||
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
total_points INT NOT NULL,
|
||||
|
||||
score INT,
|
||||
percentage NUMERIC(5,2),
|
||||
|
||||
status VARCHAR(50) NOT NULL,
|
||||
-- IN_PROGRESS, SUBMITTED, EVALUATED
|
||||
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
submitted_at TIMESTAMPTZ,
|
||||
evaluated_at TIMESTAMPTZ,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE TABLE assessment_answers (
|
||||
CREATE TABLE IF NOT EXISTS assessment_attempt_questions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
|
||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id),
|
||||
selected_option_id BIGINT REFERENCES assessment_question_options(id),
|
||||
short_answer TEXT,
|
||||
is_correct BOOLEAN NOT NULL
|
||||
|
||||
question_type VARCHAR(50) NOT NULL,
|
||||
points INT NOT NULL,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE (attempt_id, question_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assessment_attempt_answers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
|
||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
|
||||
|
||||
-- For MCQ / TRUE_FALSE
|
||||
selected_option_id BIGINT
|
||||
REFERENCES assessment_question_options(id),
|
||||
|
||||
-- For SHORT_ANSWER
|
||||
submitted_text TEXT,
|
||||
|
||||
is_correct BOOLEAN,
|
||||
awarded_points INT NOT NULL DEFAULT 0,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE (attempt_id, question_id),
|
||||
|
||||
CHECK (
|
||||
(selected_option_id IS NOT NULL AND submitted_text IS NULL)
|
||||
OR
|
||||
(selected_option_id IS NULL AND submitted_text IS NOT NULL)
|
||||
)
|
||||
);
|
||||
|
||||
ALTER TABLE assessment_attempts
|
||||
ADD CONSTRAINT chk_attempt_status
|
||||
CHECK (status IN ('IN_PROGRESS', 'SUBMITTED', 'EVALUATED'));
|
||||
|
||||
ALTER TABLE assessment_attempt_questions
|
||||
ADD CONSTRAINT chk_attempt_question_type
|
||||
CHECK (question_type IN ('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER'));
|
||||
|
||||
CREATE TABLE refresh_tokens (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
|
|
|||
|
|
@ -1,93 +1,251 @@
|
|||
-- name: CreateAssessmentAttempt :one
|
||||
INSERT INTO assessment_attempts (
|
||||
user_id,
|
||||
total_questions,
|
||||
correct_answers,
|
||||
score_percentage,
|
||||
knowledge_level
|
||||
)
|
||||
VALUES (
|
||||
$1, -- user_id
|
||||
$2, -- total_questions
|
||||
$3, -- correct_answers
|
||||
$4, -- score_percentage
|
||||
$5 -- knowledge_level
|
||||
)
|
||||
RETURNING
|
||||
id,
|
||||
user_id,
|
||||
total_questions,
|
||||
correct_answers,
|
||||
score_percentage,
|
||||
knowledge_level,
|
||||
completed_at;
|
||||
|
||||
-- -- name: CreateAssessmentAnswer :exec
|
||||
-- INSERT INTO assessment_answers (
|
||||
-- attempt_id,
|
||||
-- question_id,
|
||||
-- selected_option_id,
|
||||
-- short_answer,
|
||||
-- is_correct
|
||||
-- )
|
||||
-- VALUES (
|
||||
-- $1, -- attempt_id
|
||||
-- $2, -- question_id
|
||||
-- $3, -- selected_option_id
|
||||
-- $4, -- short_answer
|
||||
-- $5 -- is_correct
|
||||
-- );
|
||||
|
||||
-- name: GetAssessmentOptionByID :one
|
||||
SELECT
|
||||
id,
|
||||
question_id,
|
||||
option_text,
|
||||
is_correct
|
||||
FROM assessment_question_options
|
||||
WHERE id = $1
|
||||
LIMIT 1;
|
||||
|
||||
-- name: GetCorrectOptionForQuestion :one
|
||||
SELECT
|
||||
id
|
||||
FROM assessment_question_options
|
||||
WHERE question_id = $1
|
||||
AND is_correct = TRUE
|
||||
LIMIT 1;
|
||||
|
||||
-- name: GetLatestAssessmentAttempt :one
|
||||
SELECT *
|
||||
FROM assessment_attempts
|
||||
WHERE user_id = $1
|
||||
ORDER BY completed_at DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- name: CreateAssessmentQuestion :one
|
||||
INSERT INTO assessment_questions (
|
||||
title,
|
||||
description,
|
||||
question_type,
|
||||
difficulty_level
|
||||
difficulty_level,
|
||||
points,
|
||||
is_active
|
||||
)
|
||||
VALUES (
|
||||
$1, -- title
|
||||
$2, -- description
|
||||
$3, -- question_type
|
||||
$4, -- difficulty_level
|
||||
$5, -- points
|
||||
$6 -- is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateAssessmentQuestionOption :exec
|
||||
INSERT INTO assessment_question_options (
|
||||
question_id,
|
||||
option_text,
|
||||
is_correct
|
||||
)
|
||||
VALUES ($1, $2, $3);
|
||||
-- name: GetAssessmentQuestionByID :one
|
||||
SELECT *
|
||||
FROM assessment_questions
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetActiveAssessmentQuestions :many
|
||||
SELECT *
|
||||
FROM assessment_questions
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY difficulty_level, id;
|
||||
WHERE is_active = true
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: GetAssessmentQuestionsPaginated :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
question_type,
|
||||
difficulty_level,
|
||||
points,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM assessment_questions
|
||||
WHERE ($1 IS NULL OR question_type = $1)
|
||||
AND ($2 IS NULL OR difficulty_level = $2)
|
||||
AND ($3 IS NULL OR is_active = $3)
|
||||
LIMIT $4
|
||||
OFFSET $5;
|
||||
|
||||
-- name: UpdateAssessmentQuestion :exec
|
||||
UPDATE assessment_questions
|
||||
SET
|
||||
title = COALESCE($1, title),
|
||||
description = COALESCE($2, description),
|
||||
question_type = COALESCE($3, question_type),
|
||||
difficulty_level = COALESCE($4, difficulty_level),
|
||||
points = COALESCE($5, points),
|
||||
is_active = COALESCE($6, is_active),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $7;
|
||||
|
||||
-- name: DeleteAssessmentQuestion :exec
|
||||
DELETE FROM assessment_questions
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: CreateQuestionOption :one
|
||||
INSERT INTO assessment_question_options (
|
||||
question_id,
|
||||
option_text,
|
||||
option_order,
|
||||
is_correct
|
||||
)
|
||||
VALUES (
|
||||
$1, -- question_id
|
||||
$2, -- option_text
|
||||
$3, -- option_order
|
||||
$4 -- is_correct
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetQuestionOptions :many
|
||||
SELECT *
|
||||
FROM assessment_question_options
|
||||
WHERE question_id = $1
|
||||
ORDER BY option_order;
|
||||
|
||||
-- name: DeleteQuestionOptionsByQuestionID :exec
|
||||
DELETE FROM assessment_question_options
|
||||
WHERE question_id = $1;
|
||||
|
||||
-- name: CreateShortAnswer :one
|
||||
INSERT INTO assessment_short_answers (
|
||||
question_id,
|
||||
correct_answer
|
||||
)
|
||||
VALUES (
|
||||
$1, -- question_id
|
||||
$2 -- correct_answer
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetShortAnswersByQuestionID :many
|
||||
SELECT *
|
||||
FROM assessment_short_answers
|
||||
WHERE question_id = $1;
|
||||
|
||||
--------------------------------------------------------------------------------------
|
||||
|
||||
-- name: CreateAssessmentAttempt :one
|
||||
INSERT INTO assessment_attempts (
|
||||
user_id,
|
||||
total_questions,
|
||||
total_points,
|
||||
status
|
||||
)
|
||||
VALUES (
|
||||
$1, -- user_id
|
||||
$2, -- total_questions
|
||||
$3, -- total_points
|
||||
'IN_PROGRESS'
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetAssessmentAttemptByID :one
|
||||
SELECT *
|
||||
FROM assessment_attempts
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetUserAssessmentAttempts :many
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
total_questions,
|
||||
total_points,
|
||||
score,
|
||||
percentage,
|
||||
status,
|
||||
started_at,
|
||||
submitted_at,
|
||||
evaluated_at
|
||||
FROM assessment_attempts
|
||||
WHERE user_id = $1
|
||||
ORDER BY started_at DESC;
|
||||
|
||||
-- name: SubmitAssessmentAttempt :exec
|
||||
UPDATE assessment_attempts
|
||||
SET
|
||||
status = 'SUBMITTED',
|
||||
submitted_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: AddAttemptQuestion :exec
|
||||
INSERT INTO assessment_attempt_questions (
|
||||
attempt_id,
|
||||
question_id,
|
||||
question_type,
|
||||
points
|
||||
)
|
||||
VALUES (
|
||||
$1, -- attempt_id
|
||||
$2, -- question_id
|
||||
$3, -- question_type
|
||||
$4 -- points
|
||||
);
|
||||
|
||||
-- name: GetAttemptQuestions :many
|
||||
SELECT
|
||||
aq.question_id,
|
||||
aq.question_type,
|
||||
aq.points,
|
||||
q.title,
|
||||
q.description
|
||||
FROM assessment_attempt_questions aq
|
||||
JOIN assessment_questions q ON q.id = aq.question_id
|
||||
WHERE aq.attempt_id = $1;
|
||||
|
||||
-- name: UpsertAttemptAnswer :exec
|
||||
INSERT INTO assessment_attempt_answers (
|
||||
attempt_id,
|
||||
question_id,
|
||||
selected_option_id,
|
||||
submitted_text
|
||||
)
|
||||
VALUES (
|
||||
$1, -- attempt_id
|
||||
$2, -- question_id
|
||||
$3, -- selected_option_id
|
||||
$4 -- submitted_text
|
||||
)
|
||||
ON CONFLICT (attempt_id, question_id)
|
||||
DO UPDATE SET
|
||||
selected_option_id = EXCLUDED.selected_option_id,
|
||||
submitted_text = EXCLUDED.submitted_text;
|
||||
|
||||
-- name: GetAttemptAnswers :many
|
||||
SELECT *
|
||||
FROM assessment_attempt_answers
|
||||
WHERE attempt_id = $1;
|
||||
|
||||
-- name: EvaluateMCQAnswer :exec
|
||||
UPDATE assessment_attempt_answers a
|
||||
SET
|
||||
is_correct = o.is_correct,
|
||||
awarded_points = CASE WHEN o.is_correct THEN q.points ELSE 0 END
|
||||
FROM assessment_question_options o
|
||||
JOIN assessment_questions q ON q.id = a.question_id
|
||||
WHERE a.selected_option_id = o.id
|
||||
AND a.attempt_id = $1;
|
||||
|
||||
-- name: EvaluateShortAnswer :exec
|
||||
UPDATE assessment_attempt_answers a
|
||||
SET
|
||||
is_correct = EXISTS (
|
||||
SELECT 1
|
||||
FROM assessment_short_answers s
|
||||
WHERE s.question_id = a.question_id
|
||||
AND LOWER(TRIM(s.correct_answer)) = LOWER(TRIM(a.submitted_text))
|
||||
),
|
||||
awarded_points = CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM assessment_short_answers s
|
||||
WHERE s.question_id = a.question_id
|
||||
AND LOWER(TRIM(s.correct_answer)) = LOWER(TRIM(a.submitted_text))
|
||||
)
|
||||
THEN q.points
|
||||
ELSE 0
|
||||
END
|
||||
FROM assessment_questions q
|
||||
WHERE a.question_id = q.id
|
||||
AND a.attempt_id = $1;
|
||||
|
||||
-- name: FinalizeAssessmentAttempt :exec
|
||||
UPDATE assessment_attempts
|
||||
SET
|
||||
score = sub.total_score,
|
||||
percentage = ROUND((sub.total_score::NUMERIC / total_points) * 100, 2),
|
||||
status = 'EVALUATED',
|
||||
evaluated_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
FROM (
|
||||
SELECT attempt_id, SUM(awarded_points) AS total_score
|
||||
FROM assessment_attempt_answers
|
||||
WHERE attempt_id = $1
|
||||
GROUP BY attempt_id
|
||||
) sub
|
||||
WHERE assessment_attempts.id = sub.attempt_id;
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ services:
|
|||
container_name: yimaru-backend-postgres-1
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- "5422:5432"
|
||||
- "5432:5422"
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=secret
|
||||
- POSTGRES_USER=root
|
||||
|
|
|
|||
|
|
@ -11,56 +11,78 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const AddAttemptQuestion = `-- name: AddAttemptQuestion :exec
|
||||
INSERT INTO assessment_attempt_questions (
|
||||
attempt_id,
|
||||
question_id,
|
||||
question_type,
|
||||
points
|
||||
)
|
||||
VALUES (
|
||||
$1, -- attempt_id
|
||||
$2, -- question_id
|
||||
$3, -- question_type
|
||||
$4 -- points
|
||||
)
|
||||
`
|
||||
|
||||
type AddAttemptQuestionParams struct {
|
||||
AttemptID int64 `json:"attempt_id"`
|
||||
QuestionID int64 `json:"question_id"`
|
||||
QuestionType string `json:"question_type"`
|
||||
Points int32 `json:"points"`
|
||||
}
|
||||
|
||||
func (q *Queries) AddAttemptQuestion(ctx context.Context, arg AddAttemptQuestionParams) error {
|
||||
_, err := q.db.Exec(ctx, AddAttemptQuestion,
|
||||
arg.AttemptID,
|
||||
arg.QuestionID,
|
||||
arg.QuestionType,
|
||||
arg.Points,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const CreateAssessmentAttempt = `-- name: CreateAssessmentAttempt :one
|
||||
|
||||
INSERT INTO assessment_attempts (
|
||||
user_id,
|
||||
total_questions,
|
||||
correct_answers,
|
||||
score_percentage,
|
||||
knowledge_level
|
||||
total_points,
|
||||
status
|
||||
)
|
||||
VALUES (
|
||||
$1, -- user_id
|
||||
$2, -- total_questions
|
||||
$3, -- correct_answers
|
||||
$4, -- score_percentage
|
||||
$5 -- knowledge_level
|
||||
$3, -- total_points
|
||||
'IN_PROGRESS'
|
||||
)
|
||||
RETURNING
|
||||
id,
|
||||
user_id,
|
||||
total_questions,
|
||||
correct_answers,
|
||||
score_percentage,
|
||||
knowledge_level,
|
||||
completed_at
|
||||
RETURNING id, user_id, total_questions, total_points, score, percentage, status, started_at, submitted_at, evaluated_at, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateAssessmentAttemptParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
TotalQuestions int32 `json:"total_questions"`
|
||||
CorrectAnswers int32 `json:"correct_answers"`
|
||||
ScorePercentage pgtype.Numeric `json:"score_percentage"`
|
||||
KnowledgeLevel string `json:"knowledge_level"`
|
||||
UserID int64 `json:"user_id"`
|
||||
TotalQuestions int32 `json:"total_questions"`
|
||||
TotalPoints int32 `json:"total_points"`
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
func (q *Queries) CreateAssessmentAttempt(ctx context.Context, arg CreateAssessmentAttemptParams) (AssessmentAttempt, error) {
|
||||
row := q.db.QueryRow(ctx, CreateAssessmentAttempt,
|
||||
arg.UserID,
|
||||
arg.TotalQuestions,
|
||||
arg.CorrectAnswers,
|
||||
arg.ScorePercentage,
|
||||
arg.KnowledgeLevel,
|
||||
)
|
||||
row := q.db.QueryRow(ctx, CreateAssessmentAttempt, arg.UserID, arg.TotalQuestions, arg.TotalPoints)
|
||||
var i AssessmentAttempt
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.TotalQuestions,
|
||||
&i.CorrectAnswers,
|
||||
&i.ScorePercentage,
|
||||
&i.KnowledgeLevel,
|
||||
&i.CompletedAt,
|
||||
&i.TotalPoints,
|
||||
&i.Score,
|
||||
&i.Percentage,
|
||||
&i.Status,
|
||||
&i.StartedAt,
|
||||
&i.SubmittedAt,
|
||||
&i.EvaluatedAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -70,17 +92,28 @@ INSERT INTO assessment_questions (
|
|||
title,
|
||||
description,
|
||||
question_type,
|
||||
difficulty_level
|
||||
difficulty_level,
|
||||
points,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, title, description, question_type, difficulty_level, is_active, created_at, updated_at
|
||||
VALUES (
|
||||
$1, -- title
|
||||
$2, -- description
|
||||
$3, -- question_type
|
||||
$4, -- difficulty_level
|
||||
$5, -- points
|
||||
$6 -- is_active
|
||||
)
|
||||
RETURNING id, title, description, question_type, difficulty_level, points, is_active, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateAssessmentQuestionParams struct {
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
QuestionType string `json:"question_type"`
|
||||
DifficultyLevel string `json:"difficulty_level"`
|
||||
DifficultyLevel pgtype.Text `json:"difficulty_level"`
|
||||
Points int32 `json:"points"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateAssessmentQuestion(ctx context.Context, arg CreateAssessmentQuestionParams) (AssessmentQuestion, error) {
|
||||
|
|
@ -89,6 +122,8 @@ func (q *Queries) CreateAssessmentQuestion(ctx context.Context, arg CreateAssess
|
|||
arg.Description,
|
||||
arg.QuestionType,
|
||||
arg.DifficultyLevel,
|
||||
arg.Points,
|
||||
arg.IsActive,
|
||||
)
|
||||
var i AssessmentQuestion
|
||||
err := row.Scan(
|
||||
|
|
@ -97,6 +132,7 @@ func (q *Queries) CreateAssessmentQuestion(ctx context.Context, arg CreateAssess
|
|||
&i.Description,
|
||||
&i.QuestionType,
|
||||
&i.DifficultyLevel,
|
||||
&i.Points,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
|
|
@ -104,31 +140,169 @@ func (q *Queries) CreateAssessmentQuestion(ctx context.Context, arg CreateAssess
|
|||
return i, err
|
||||
}
|
||||
|
||||
const CreateAssessmentQuestionOption = `-- name: CreateAssessmentQuestionOption :exec
|
||||
const CreateQuestionOption = `-- name: CreateQuestionOption :one
|
||||
INSERT INTO assessment_question_options (
|
||||
question_id,
|
||||
option_text,
|
||||
option_order,
|
||||
is_correct
|
||||
)
|
||||
VALUES ($1, $2, $3)
|
||||
VALUES (
|
||||
$1, -- question_id
|
||||
$2, -- option_text
|
||||
$3, -- option_order
|
||||
$4 -- is_correct
|
||||
)
|
||||
RETURNING id, question_id, option_text, option_order, is_correct, created_at
|
||||
`
|
||||
|
||||
type CreateAssessmentQuestionOptionParams struct {
|
||||
QuestionID int64 `json:"question_id"`
|
||||
OptionText string `json:"option_text"`
|
||||
IsCorrect bool `json:"is_correct"`
|
||||
type CreateQuestionOptionParams struct {
|
||||
QuestionID int64 `json:"question_id"`
|
||||
OptionText string `json:"option_text"`
|
||||
OptionOrder int32 `json:"option_order"`
|
||||
IsCorrect bool `json:"is_correct"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateAssessmentQuestionOption(ctx context.Context, arg CreateAssessmentQuestionOptionParams) error {
|
||||
_, err := q.db.Exec(ctx, CreateAssessmentQuestionOption, arg.QuestionID, arg.OptionText, arg.IsCorrect)
|
||||
func (q *Queries) CreateQuestionOption(ctx context.Context, arg CreateQuestionOptionParams) (AssessmentQuestionOption, error) {
|
||||
row := q.db.QueryRow(ctx, CreateQuestionOption,
|
||||
arg.QuestionID,
|
||||
arg.OptionText,
|
||||
arg.OptionOrder,
|
||||
arg.IsCorrect,
|
||||
)
|
||||
var i AssessmentQuestionOption
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.QuestionID,
|
||||
&i.OptionText,
|
||||
&i.OptionOrder,
|
||||
&i.IsCorrect,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateShortAnswer = `-- name: CreateShortAnswer :one
|
||||
INSERT INTO assessment_short_answers (
|
||||
question_id,
|
||||
correct_answer
|
||||
)
|
||||
VALUES (
|
||||
$1, -- question_id
|
||||
$2 -- correct_answer
|
||||
)
|
||||
RETURNING id, question_id, correct_answer, created_at
|
||||
`
|
||||
|
||||
type CreateShortAnswerParams struct {
|
||||
QuestionID int64 `json:"question_id"`
|
||||
CorrectAnswer string `json:"correct_answer"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateShortAnswer(ctx context.Context, arg CreateShortAnswerParams) (AssessmentShortAnswer, error) {
|
||||
row := q.db.QueryRow(ctx, CreateShortAnswer, arg.QuestionID, arg.CorrectAnswer)
|
||||
var i AssessmentShortAnswer
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.QuestionID,
|
||||
&i.CorrectAnswer,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const DeleteAssessmentQuestion = `-- name: DeleteAssessmentQuestion :exec
|
||||
DELETE FROM assessment_questions
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteAssessmentQuestion(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, DeleteAssessmentQuestion, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const DeleteQuestionOptionsByQuestionID = `-- name: DeleteQuestionOptionsByQuestionID :exec
|
||||
DELETE FROM assessment_question_options
|
||||
WHERE question_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteQuestionOptionsByQuestionID(ctx context.Context, questionID int64) error {
|
||||
_, err := q.db.Exec(ctx, DeleteQuestionOptionsByQuestionID, questionID)
|
||||
return err
|
||||
}
|
||||
|
||||
const EvaluateMCQAnswer = `-- name: EvaluateMCQAnswer :exec
|
||||
UPDATE assessment_attempt_answers a
|
||||
SET
|
||||
is_correct = o.is_correct,
|
||||
awarded_points = CASE WHEN o.is_correct THEN q.points ELSE 0 END
|
||||
FROM assessment_question_options o
|
||||
JOIN assessment_questions q ON q.id = a.question_id
|
||||
WHERE a.selected_option_id = o.id
|
||||
AND a.attempt_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) EvaluateMCQAnswer(ctx context.Context, attemptID int64) error {
|
||||
_, err := q.db.Exec(ctx, EvaluateMCQAnswer, attemptID)
|
||||
return err
|
||||
}
|
||||
|
||||
const EvaluateShortAnswer = `-- name: EvaluateShortAnswer :exec
|
||||
UPDATE assessment_attempt_answers a
|
||||
SET
|
||||
is_correct = EXISTS (
|
||||
SELECT 1
|
||||
FROM assessment_short_answers s
|
||||
WHERE s.question_id = a.question_id
|
||||
AND LOWER(TRIM(s.correct_answer)) = LOWER(TRIM(a.submitted_text))
|
||||
),
|
||||
awarded_points = CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM assessment_short_answers s
|
||||
WHERE s.question_id = a.question_id
|
||||
AND LOWER(TRIM(s.correct_answer)) = LOWER(TRIM(a.submitted_text))
|
||||
)
|
||||
THEN q.points
|
||||
ELSE 0
|
||||
END
|
||||
FROM assessment_questions q
|
||||
WHERE a.question_id = q.id
|
||||
AND a.attempt_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) EvaluateShortAnswer(ctx context.Context, attemptID int64) error {
|
||||
_, err := q.db.Exec(ctx, EvaluateShortAnswer, attemptID)
|
||||
return err
|
||||
}
|
||||
|
||||
const FinalizeAssessmentAttempt = `-- name: FinalizeAssessmentAttempt :exec
|
||||
UPDATE assessment_attempts
|
||||
SET
|
||||
score = sub.total_score,
|
||||
percentage = ROUND((sub.total_score::NUMERIC / total_points) * 100, 2),
|
||||
status = 'EVALUATED',
|
||||
evaluated_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
FROM (
|
||||
SELECT attempt_id, SUM(awarded_points) AS total_score
|
||||
FROM assessment_attempt_answers
|
||||
WHERE attempt_id = $1
|
||||
GROUP BY attempt_id
|
||||
) sub
|
||||
WHERE assessment_attempts.id = sub.attempt_id
|
||||
`
|
||||
|
||||
func (q *Queries) FinalizeAssessmentAttempt(ctx context.Context, attemptID int64) error {
|
||||
_, err := q.db.Exec(ctx, FinalizeAssessmentAttempt, attemptID)
|
||||
return err
|
||||
}
|
||||
|
||||
const GetActiveAssessmentQuestions = `-- name: GetActiveAssessmentQuestions :many
|
||||
SELECT id, title, description, question_type, difficulty_level, is_active, created_at, updated_at
|
||||
SELECT id, title, description, question_type, difficulty_level, points, is_active, created_at, updated_at
|
||||
FROM assessment_questions
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY difficulty_level, id
|
||||
WHERE is_active = true
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
func (q *Queries) GetActiveAssessmentQuestions(ctx context.Context) ([]AssessmentQuestion, error) {
|
||||
|
|
@ -146,6 +320,7 @@ func (q *Queries) GetActiveAssessmentQuestions(ctx context.Context) ([]Assessmen
|
|||
&i.Description,
|
||||
&i.QuestionType,
|
||||
&i.DifficultyLevel,
|
||||
&i.Points,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
|
|
@ -160,92 +335,219 @@ func (q *Queries) GetActiveAssessmentQuestions(ctx context.Context) ([]Assessmen
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const GetAssessmentOptionByID = `-- name: GetAssessmentOptionByID :one
|
||||
|
||||
SELECT
|
||||
id,
|
||||
question_id,
|
||||
option_text,
|
||||
is_correct
|
||||
FROM assessment_question_options
|
||||
WHERE id = $1
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
// -- name: CreateAssessmentAnswer :exec
|
||||
// INSERT INTO assessment_answers (
|
||||
//
|
||||
// attempt_id,
|
||||
// question_id,
|
||||
// selected_option_id,
|
||||
// short_answer,
|
||||
// is_correct
|
||||
//
|
||||
// )
|
||||
// VALUES (
|
||||
//
|
||||
// $1, -- attempt_id
|
||||
// $2, -- question_id
|
||||
// $3, -- selected_option_id
|
||||
// $4, -- short_answer
|
||||
// $5 -- is_correct
|
||||
//
|
||||
// );
|
||||
func (q *Queries) GetAssessmentOptionByID(ctx context.Context, id int64) (AssessmentQuestionOption, error) {
|
||||
row := q.db.QueryRow(ctx, GetAssessmentOptionByID, id)
|
||||
var i AssessmentQuestionOption
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.QuestionID,
|
||||
&i.OptionText,
|
||||
&i.IsCorrect,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetCorrectOptionForQuestion = `-- name: GetCorrectOptionForQuestion :one
|
||||
SELECT
|
||||
id
|
||||
FROM assessment_question_options
|
||||
WHERE question_id = $1
|
||||
AND is_correct = TRUE
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetCorrectOptionForQuestion(ctx context.Context, questionID int64) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, GetCorrectOptionForQuestion, questionID)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const GetLatestAssessmentAttempt = `-- name: GetLatestAssessmentAttempt :one
|
||||
SELECT id, user_id, total_questions, correct_answers, score_percentage, knowledge_level, completed_at
|
||||
const GetAssessmentAttemptByID = `-- name: GetAssessmentAttemptByID :one
|
||||
SELECT id, user_id, total_questions, total_points, score, percentage, status, started_at, submitted_at, evaluated_at, created_at, updated_at
|
||||
FROM assessment_attempts
|
||||
WHERE user_id = $1
|
||||
ORDER BY completed_at DESC
|
||||
LIMIT 1
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetLatestAssessmentAttempt(ctx context.Context, userID int64) (AssessmentAttempt, error) {
|
||||
row := q.db.QueryRow(ctx, GetLatestAssessmentAttempt, userID)
|
||||
func (q *Queries) GetAssessmentAttemptByID(ctx context.Context, id int64) (AssessmentAttempt, error) {
|
||||
row := q.db.QueryRow(ctx, GetAssessmentAttemptByID, id)
|
||||
var i AssessmentAttempt
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.TotalQuestions,
|
||||
&i.CorrectAnswers,
|
||||
&i.ScorePercentage,
|
||||
&i.KnowledgeLevel,
|
||||
&i.CompletedAt,
|
||||
&i.TotalPoints,
|
||||
&i.Score,
|
||||
&i.Percentage,
|
||||
&i.Status,
|
||||
&i.StartedAt,
|
||||
&i.SubmittedAt,
|
||||
&i.EvaluatedAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetAssessmentQuestionByID = `-- name: GetAssessmentQuestionByID :one
|
||||
SELECT id, title, description, question_type, difficulty_level, points, is_active, created_at, updated_at
|
||||
FROM assessment_questions
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetAssessmentQuestionByID(ctx context.Context, id int64) (AssessmentQuestion, error) {
|
||||
row := q.db.QueryRow(ctx, GetAssessmentQuestionByID, id)
|
||||
var i AssessmentQuestion
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.QuestionType,
|
||||
&i.DifficultyLevel,
|
||||
&i.Points,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetAssessmentQuestionsPaginated = `-- name: GetAssessmentQuestionsPaginated :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
question_type,
|
||||
difficulty_level,
|
||||
points,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM assessment_questions
|
||||
WHERE ($1 IS NULL OR question_type = $1)
|
||||
AND ($2 IS NULL OR difficulty_level = $2)
|
||||
AND ($3 IS NULL OR is_active = $3)
|
||||
LIMIT $4
|
||||
OFFSET $5
|
||||
`
|
||||
|
||||
type GetAssessmentQuestionsPaginatedParams struct {
|
||||
Column1 interface{} `json:"column_1"`
|
||||
Column2 interface{} `json:"column_2"`
|
||||
Column3 interface{} `json:"column_3"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
type GetAssessmentQuestionsPaginatedRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
QuestionType string `json:"question_type"`
|
||||
DifficultyLevel pgtype.Text `json:"difficulty_level"`
|
||||
Points int32 `json:"points"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetAssessmentQuestionsPaginated(ctx context.Context, arg GetAssessmentQuestionsPaginatedParams) ([]GetAssessmentQuestionsPaginatedRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetAssessmentQuestionsPaginated,
|
||||
arg.Column1,
|
||||
arg.Column2,
|
||||
arg.Column3,
|
||||
arg.Limit,
|
||||
arg.Offset,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetAssessmentQuestionsPaginatedRow
|
||||
for rows.Next() {
|
||||
var i GetAssessmentQuestionsPaginatedRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.QuestionType,
|
||||
&i.DifficultyLevel,
|
||||
&i.Points,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetAttemptAnswers = `-- name: GetAttemptAnswers :many
|
||||
SELECT id, attempt_id, question_id, selected_option_id, submitted_text, is_correct, awarded_points, created_at
|
||||
FROM assessment_attempt_answers
|
||||
WHERE attempt_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetAttemptAnswers(ctx context.Context, attemptID int64) ([]AssessmentAttemptAnswer, error) {
|
||||
rows, err := q.db.Query(ctx, GetAttemptAnswers, attemptID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AssessmentAttemptAnswer
|
||||
for rows.Next() {
|
||||
var i AssessmentAttemptAnswer
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.AttemptID,
|
||||
&i.QuestionID,
|
||||
&i.SelectedOptionID,
|
||||
&i.SubmittedText,
|
||||
&i.IsCorrect,
|
||||
&i.AwardedPoints,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetAttemptQuestions = `-- name: GetAttemptQuestions :many
|
||||
SELECT
|
||||
aq.question_id,
|
||||
aq.question_type,
|
||||
aq.points,
|
||||
q.title,
|
||||
q.description
|
||||
FROM assessment_attempt_questions aq
|
||||
JOIN assessment_questions q ON q.id = aq.question_id
|
||||
WHERE aq.attempt_id = $1
|
||||
`
|
||||
|
||||
type GetAttemptQuestionsRow struct {
|
||||
QuestionID int64 `json:"question_id"`
|
||||
QuestionType string `json:"question_type"`
|
||||
Points int32 `json:"points"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetAttemptQuestions(ctx context.Context, attemptID int64) ([]GetAttemptQuestionsRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetAttemptQuestions, attemptID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetAttemptQuestionsRow
|
||||
for rows.Next() {
|
||||
var i GetAttemptQuestionsRow
|
||||
if err := rows.Scan(
|
||||
&i.QuestionID,
|
||||
&i.QuestionType,
|
||||
&i.Points,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetQuestionOptions = `-- name: GetQuestionOptions :many
|
||||
SELECT id, question_id, option_text, is_correct
|
||||
SELECT id, question_id, option_text, option_order, is_correct, created_at
|
||||
FROM assessment_question_options
|
||||
WHERE question_id = $1
|
||||
ORDER BY option_order
|
||||
`
|
||||
|
||||
func (q *Queries) GetQuestionOptions(ctx context.Context, questionID int64) ([]AssessmentQuestionOption, error) {
|
||||
|
|
@ -261,7 +563,9 @@ func (q *Queries) GetQuestionOptions(ctx context.Context, questionID int64) ([]A
|
|||
&i.ID,
|
||||
&i.QuestionID,
|
||||
&i.OptionText,
|
||||
&i.OptionOrder,
|
||||
&i.IsCorrect,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -272,3 +576,181 @@ func (q *Queries) GetQuestionOptions(ctx context.Context, questionID int64) ([]A
|
|||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetShortAnswersByQuestionID = `-- name: GetShortAnswersByQuestionID :many
|
||||
SELECT id, question_id, correct_answer, created_at
|
||||
FROM assessment_short_answers
|
||||
WHERE question_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetShortAnswersByQuestionID(ctx context.Context, questionID int64) ([]AssessmentShortAnswer, error) {
|
||||
rows, err := q.db.Query(ctx, GetShortAnswersByQuestionID, questionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AssessmentShortAnswer
|
||||
for rows.Next() {
|
||||
var i AssessmentShortAnswer
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.QuestionID,
|
||||
&i.CorrectAnswer,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetUserAssessmentAttempts = `-- name: GetUserAssessmentAttempts :many
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
total_questions,
|
||||
total_points,
|
||||
score,
|
||||
percentage,
|
||||
status,
|
||||
started_at,
|
||||
submitted_at,
|
||||
evaluated_at
|
||||
FROM assessment_attempts
|
||||
WHERE user_id = $1
|
||||
ORDER BY started_at DESC
|
||||
`
|
||||
|
||||
type GetUserAssessmentAttemptsRow struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
TotalQuestions int32 `json:"total_questions"`
|
||||
TotalPoints int32 `json:"total_points"`
|
||||
Score pgtype.Int4 `json:"score"`
|
||||
Percentage pgtype.Numeric `json:"percentage"`
|
||||
Status string `json:"status"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
SubmittedAt pgtype.Timestamptz `json:"submitted_at"`
|
||||
EvaluatedAt pgtype.Timestamptz `json:"evaluated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserAssessmentAttempts(ctx context.Context, userID int64) ([]GetUserAssessmentAttemptsRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetUserAssessmentAttempts, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetUserAssessmentAttemptsRow
|
||||
for rows.Next() {
|
||||
var i GetUserAssessmentAttemptsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.TotalQuestions,
|
||||
&i.TotalPoints,
|
||||
&i.Score,
|
||||
&i.Percentage,
|
||||
&i.Status,
|
||||
&i.StartedAt,
|
||||
&i.SubmittedAt,
|
||||
&i.EvaluatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const SubmitAssessmentAttempt = `-- name: SubmitAssessmentAttempt :exec
|
||||
UPDATE assessment_attempts
|
||||
SET
|
||||
status = 'SUBMITTED',
|
||||
submitted_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) SubmitAssessmentAttempt(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, SubmitAssessmentAttempt, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateAssessmentQuestion = `-- name: UpdateAssessmentQuestion :exec
|
||||
UPDATE assessment_questions
|
||||
SET
|
||||
title = COALESCE($1, title),
|
||||
description = COALESCE($2, description),
|
||||
question_type = COALESCE($3, question_type),
|
||||
difficulty_level = COALESCE($4, difficulty_level),
|
||||
points = COALESCE($5, points),
|
||||
is_active = COALESCE($6, is_active),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $7
|
||||
`
|
||||
|
||||
type UpdateAssessmentQuestionParams struct {
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
QuestionType string `json:"question_type"`
|
||||
DifficultyLevel pgtype.Text `json:"difficulty_level"`
|
||||
Points int32 `json:"points"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateAssessmentQuestion(ctx context.Context, arg UpdateAssessmentQuestionParams) error {
|
||||
_, err := q.db.Exec(ctx, UpdateAssessmentQuestion,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.QuestionType,
|
||||
arg.DifficultyLevel,
|
||||
arg.Points,
|
||||
arg.IsActive,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpsertAttemptAnswer = `-- name: UpsertAttemptAnswer :exec
|
||||
INSERT INTO assessment_attempt_answers (
|
||||
attempt_id,
|
||||
question_id,
|
||||
selected_option_id,
|
||||
submitted_text
|
||||
)
|
||||
VALUES (
|
||||
$1, -- attempt_id
|
||||
$2, -- question_id
|
||||
$3, -- selected_option_id
|
||||
$4 -- submitted_text
|
||||
)
|
||||
ON CONFLICT (attempt_id, question_id)
|
||||
DO UPDATE SET
|
||||
selected_option_id = EXCLUDED.selected_option_id,
|
||||
submitted_text = EXCLUDED.submitted_text
|
||||
`
|
||||
|
||||
type UpsertAttemptAnswerParams struct {
|
||||
AttemptID int64 `json:"attempt_id"`
|
||||
QuestionID int64 `json:"question_id"`
|
||||
SelectedOptionID pgtype.Int8 `json:"selected_option_id"`
|
||||
SubmittedText pgtype.Text `json:"submitted_text"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertAttemptAnswer(ctx context.Context, arg UpsertAttemptAnswerParams) error {
|
||||
_, err := q.db.Exec(ctx, UpsertAttemptAnswer,
|
||||
arg.AttemptID,
|
||||
arg.QuestionID,
|
||||
arg.SelectedOptionID,
|
||||
arg.SubmittedText,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,22 +8,39 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type AssessmentAnswer struct {
|
||||
ID int64 `json:"id"`
|
||||
QuestionID int64 `json:"question_id"`
|
||||
SelectedOptionID pgtype.Int8 `json:"selected_option_id"`
|
||||
ShortAnswer pgtype.Text `json:"short_answer"`
|
||||
IsCorrect bool `json:"is_correct"`
|
||||
type AssessmentAttempt struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
TotalQuestions int32 `json:"total_questions"`
|
||||
TotalPoints int32 `json:"total_points"`
|
||||
Score pgtype.Int4 `json:"score"`
|
||||
Percentage pgtype.Numeric `json:"percentage"`
|
||||
Status string `json:"status"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
SubmittedAt pgtype.Timestamptz `json:"submitted_at"`
|
||||
EvaluatedAt pgtype.Timestamptz `json:"evaluated_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type AssessmentAttempt struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
TotalQuestions int32 `json:"total_questions"`
|
||||
CorrectAnswers int32 `json:"correct_answers"`
|
||||
ScorePercentage pgtype.Numeric `json:"score_percentage"`
|
||||
KnowledgeLevel string `json:"knowledge_level"`
|
||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||
type AssessmentAttemptAnswer struct {
|
||||
ID int64 `json:"id"`
|
||||
AttemptID int64 `json:"attempt_id"`
|
||||
QuestionID int64 `json:"question_id"`
|
||||
SelectedOptionID pgtype.Int8 `json:"selected_option_id"`
|
||||
SubmittedText pgtype.Text `json:"submitted_text"`
|
||||
IsCorrect pgtype.Bool `json:"is_correct"`
|
||||
AwardedPoints int32 `json:"awarded_points"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type AssessmentAttemptQuestion struct {
|
||||
ID int64 `json:"id"`
|
||||
AttemptID int64 `json:"attempt_id"`
|
||||
QuestionID int64 `json:"question_id"`
|
||||
QuestionType string `json:"question_type"`
|
||||
Points int32 `json:"points"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type AssessmentQuestion struct {
|
||||
|
|
@ -31,17 +48,27 @@ type AssessmentQuestion struct {
|
|||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
QuestionType string `json:"question_type"`
|
||||
DifficultyLevel string `json:"difficulty_level"`
|
||||
DifficultyLevel pgtype.Text `json:"difficulty_level"`
|
||||
Points int32 `json:"points"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type AssessmentQuestionOption struct {
|
||||
ID int64 `json:"id"`
|
||||
QuestionID int64 `json:"question_id"`
|
||||
OptionText string `json:"option_text"`
|
||||
IsCorrect bool `json:"is_correct"`
|
||||
ID int64 `json:"id"`
|
||||
QuestionID int64 `json:"question_id"`
|
||||
OptionText string `json:"option_text"`
|
||||
OptionOrder int32 `json:"option_order"`
|
||||
IsCorrect bool `json:"is_correct"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type AssessmentShortAnswer struct {
|
||||
ID int64 `json:"id"`
|
||||
QuestionID int64 `json:"question_id"`
|
||||
CorrectAnswer string `json:"correct_answer"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type Course struct {
|
||||
|
|
@ -197,7 +224,6 @@ type User struct {
|
|||
EducationLevel pgtype.Text `json:"education_level"`
|
||||
Country pgtype.Text `json:"country"`
|
||||
Region pgtype.Text `json:"region"`
|
||||
Medium string `json:"medium"`
|
||||
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
|
||||
NickName pgtype.Text `json:"nick_name"`
|
||||
Occupation pgtype.Text `json:"occupation"`
|
||||
|
|
|
|||
|
|
@ -423,7 +423,6 @@ SELECT
|
|||
language_challange,
|
||||
favoutite_topic,
|
||||
|
||||
medium,
|
||||
email_verified,
|
||||
phone_verified,
|
||||
status,
|
||||
|
|
@ -463,7 +462,6 @@ type GetUserByEmailPhoneRow struct {
|
|||
LanguageGoal pgtype.Text `json:"language_goal"`
|
||||
LanguageChallange pgtype.Text `json:"language_challange"`
|
||||
FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
|
||||
Medium string `json:"medium"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
PhoneVerified bool `json:"phone_verified"`
|
||||
Status string `json:"status"`
|
||||
|
|
@ -497,7 +495,6 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
|||
&i.LanguageGoal,
|
||||
&i.LanguageChallange,
|
||||
&i.FavoutiteTopic,
|
||||
&i.Medium,
|
||||
&i.EmailVerified,
|
||||
&i.PhoneVerified,
|
||||
&i.Status,
|
||||
|
|
@ -512,7 +509,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
|||
}
|
||||
|
||||
const GetUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, first_name, last_name, user_name, email, phone_number, role, password, age, education_level, country, region, medium, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favoutite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at
|
||||
SELECT id, first_name, last_name, user_name, email, phone_number, role, password, age, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favoutite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -533,7 +530,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
|||
&i.EducationLevel,
|
||||
&i.Country,
|
||||
&i.Region,
|
||||
&i.Medium,
|
||||
&i.KnowledgeLevel,
|
||||
&i.NickName,
|
||||
&i.Occupation,
|
||||
|
|
|
|||
|
|
@ -1,47 +1,70 @@
|
|||
package domain
|
||||
|
||||
import "time"
|
||||
import dbgen "Yimaru-Backend/gen/db"
|
||||
|
||||
type QuestionType string
|
||||
|
||||
const (
|
||||
QuestionTypeMultipleChoice QuestionType = "multiple_choice"
|
||||
QuestionTypeTrueFalse QuestionType = "true_false"
|
||||
QuestionTypeShortAnswer QuestionType = "short_answer"
|
||||
MultipleChoice QuestionType = "MULTIPLE_CHOICE"
|
||||
TrueFalse QuestionType = "TRUE_FALSE"
|
||||
ShortAnswer QuestionType = "SHORT_ANSWER"
|
||||
)
|
||||
|
||||
type SubmitAssessmentReq struct {
|
||||
Answers []UserAnswer `json:"answers" validate:"required,min=1"`
|
||||
type QuestionWithDetails struct {
|
||||
Question dbgen.AssessmentQuestion
|
||||
Options []QuestionOption
|
||||
}
|
||||
|
||||
type AssessmentQuestion struct {
|
||||
ID int64
|
||||
type QuestionOption struct {
|
||||
QuestionID int64 `json:"question_id"`
|
||||
OptionText string `json:"option_text"`
|
||||
}
|
||||
|
||||
type CreateAssessmentQuestionInput struct {
|
||||
Title string
|
||||
Description string
|
||||
QuestionType string
|
||||
Description *string
|
||||
QuestionType QuestionType
|
||||
DifficultyLevel string
|
||||
Options []AssessmentOption
|
||||
Points int32
|
||||
IsActive bool
|
||||
|
||||
// Multiple Choice only
|
||||
Options []CreateQuestionOptionInput
|
||||
|
||||
// Short Answer only
|
||||
CorrectAnswer *string
|
||||
}
|
||||
|
||||
type AssessmentOption struct {
|
||||
ID int64
|
||||
OptionText string
|
||||
IsCorrect bool
|
||||
type CreateQuestionOptionInput struct {
|
||||
Text string
|
||||
Order int32
|
||||
IsCorrect bool
|
||||
}
|
||||
|
||||
type UserAnswer struct {
|
||||
QuestionID int64
|
||||
SelectedOptionID int64
|
||||
ShortAnswer string
|
||||
IsCorrect bool
|
||||
}
|
||||
// type AssessmentQuestion struct {
|
||||
// ID int64
|
||||
// QuestionText string
|
||||
// Type QuestionType
|
||||
// Options []string
|
||||
// CorrectAnswer string
|
||||
// }
|
||||
|
||||
type AssessmentAttempt struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
TotalQuestions int
|
||||
CorrectAnswers int
|
||||
ScorePercentage float64
|
||||
KnowledgeLevel string
|
||||
CompletedAt time.Time
|
||||
}
|
||||
// type AssessmentOption struct {
|
||||
// ID int64
|
||||
// OptionText string
|
||||
// IsCorrect bool
|
||||
// }
|
||||
|
||||
// type AttemptAnswer struct {
|
||||
// QuestionID int64
|
||||
// Answer string
|
||||
// IsCorrect *bool
|
||||
// }
|
||||
|
||||
// type AssessmentAttempt struct {
|
||||
// ID int64
|
||||
// UserID int64
|
||||
// Answers []AttemptAnswer
|
||||
// Score int
|
||||
// Completed bool
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,16 +1,23 @@
|
|||
package ports
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
)
|
||||
|
||||
type InitialAssessmentStore interface {
|
||||
CreateAssessmentQuestion(
|
||||
ctx context.Context,
|
||||
q domain.AssessmentQuestion,
|
||||
) (domain.AssessmentQuestion, error)
|
||||
GetActiveAssessmentQuestions(ctx context.Context) ([]domain.AssessmentQuestion, error)
|
||||
// SaveAssessmentAttempt(ctx context.Context, userID int64, answers []domain.UserAnswer) (domain.AssessmentAttempt, error)
|
||||
GetOptionByID(ctx context.Context, optionID int64) (domain.AssessmentOption, error)
|
||||
CreateAssessmentQuestion(ctx context.Context, arg dbgen.CreateAssessmentQuestionParams) (dbgen.AssessmentQuestion, error)
|
||||
GetAssessmentQuestionByID(ctx context.Context, id int64) (dbgen.AssessmentQuestion, error)
|
||||
GetActiveAssessmentQuestions(ctx context.Context) ([]dbgen.AssessmentQuestion, error)
|
||||
GetAssessmentQuestionsPaginated(ctx context.Context, arg dbgen.GetAssessmentQuestionsPaginatedParams) ([]dbgen.GetAssessmentQuestionsPaginatedRow, error)
|
||||
UpdateAssessmentQuestion(ctx context.Context, arg dbgen.UpdateAssessmentQuestionParams) error
|
||||
DeleteAssessmentQuestion(ctx context.Context, id int64) error
|
||||
|
||||
CreateQuestionOption(ctx context.Context, arg dbgen.CreateQuestionOptionParams) (dbgen.AssessmentQuestionOption, error)
|
||||
GetQuestionOptions(ctx context.Context, questionID int64) ([]dbgen.AssessmentQuestionOption, error)
|
||||
DeleteQuestionOptionsByQuestionID(ctx context.Context, questionID int64) error
|
||||
|
||||
CreateShortAnswer(ctx context.Context, arg dbgen.CreateShortAnswerParams) (dbgen.AssessmentShortAnswer, error)
|
||||
GetShortAnswersByQuestionID(ctx context.Context, questionID int64) ([]dbgen.AssessmentShortAnswer, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,20 +4,19 @@ import (
|
|||
"context"
|
||||
"time"
|
||||
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
)
|
||||
|
||||
type UserStore interface {
|
||||
UpdateUserStatus(ctx context.Context, user domain.UpdateUserReq) error
|
||||
GetCorrectOptionForQuestion(
|
||||
ctx context.Context,
|
||||
questionID int64,
|
||||
) (int64, error)
|
||||
GetLatestAssessmentAttempt(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
) (*dbgen.AssessmentAttempt, error)
|
||||
// GetCorrectOptionForQuestion(
|
||||
// ctx context.Context,
|
||||
// questionID int64,
|
||||
// ) (int64, error)
|
||||
// GetLatestAssessmentAttempt(
|
||||
// ctx context.Context,
|
||||
// userID int64,
|
||||
// ) (*dbgen.AssessmentAttempt, error)
|
||||
UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error
|
||||
IsUserNameUnique(ctx context.Context, userName string) (bool, error)
|
||||
IsUserPending(ctx context.Context, UserName string) (bool, error)
|
||||
|
|
|
|||
|
|
@ -2,173 +2,84 @@ package repository
|
|||
|
||||
import (
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/ports"
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func NewInitialAssessmentStore(s *Store) ports.InitialAssessmentStore { return s }
|
||||
|
||||
func (r *Store) GetCorrectOptionForQuestion(
|
||||
ctx context.Context,
|
||||
questionID int64,
|
||||
) (int64, error) {
|
||||
|
||||
optId, err := r.queries.GetCorrectOptionForQuestion(ctx, questionID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return optId, nil
|
||||
}
|
||||
|
||||
func (r *Store) GetLatestAssessmentAttempt(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
) (*dbgen.AssessmentAttempt, error) {
|
||||
|
||||
attempt, err := r.queries.GetLatestAssessmentAttempt(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &attempt, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateAssessmentQuestion(
|
||||
ctx context.Context,
|
||||
q domain.AssessmentQuestion,
|
||||
) (domain.AssessmentQuestion, error) {
|
||||
|
||||
row, err := s.queries.CreateAssessmentQuestion(ctx, dbgen.CreateAssessmentQuestionParams{
|
||||
Title: q.Title,
|
||||
Description: pgtype.Text{String: q.Description, Valid: q.Description != ""},
|
||||
QuestionType: q.QuestionType,
|
||||
DifficultyLevel: q.DifficultyLevel,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.AssessmentQuestion{}, err
|
||||
}
|
||||
|
||||
for _, opt := range q.Options {
|
||||
if err := s.queries.CreateAssessmentQuestionOption(ctx,
|
||||
dbgen.CreateAssessmentQuestionOptionParams{
|
||||
QuestionID: row.ID,
|
||||
OptionText: opt.OptionText,
|
||||
IsCorrect: opt.IsCorrect,
|
||||
},
|
||||
); err != nil {
|
||||
return domain.AssessmentQuestion{}, err
|
||||
}
|
||||
}
|
||||
|
||||
q.ID = row.ID
|
||||
return q, nil
|
||||
arg dbgen.CreateAssessmentQuestionParams,
|
||||
) (dbgen.AssessmentQuestion, error) {
|
||||
return s.queries.CreateAssessmentQuestion(ctx, arg)
|
||||
}
|
||||
|
||||
func (s *Store) GetActiveAssessmentQuestions(ctx context.Context) ([]domain.AssessmentQuestion, error) {
|
||||
questionsRows, err := s.queries.GetActiveAssessmentQuestions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
questions := make([]domain.AssessmentQuestion, 0, len(questionsRows))
|
||||
for _, q := range questionsRows {
|
||||
optionsRows, err := s.queries.GetQuestionOptions(ctx, q.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
options := make([]domain.AssessmentOption, 0, len(optionsRows))
|
||||
for _, o := range optionsRows {
|
||||
options = append(options, domain.AssessmentOption{
|
||||
ID: o.ID,
|
||||
OptionText: o.OptionText,
|
||||
IsCorrect: o.IsCorrect,
|
||||
})
|
||||
}
|
||||
|
||||
questions = append(questions, domain.AssessmentQuestion{
|
||||
ID: q.ID,
|
||||
Title: q.Title,
|
||||
Description: q.Description.String,
|
||||
QuestionType: q.QuestionType,
|
||||
DifficultyLevel: q.DifficultyLevel,
|
||||
Options: options,
|
||||
})
|
||||
}
|
||||
|
||||
return questions, nil
|
||||
func (s *Store) GetAssessmentQuestionByID(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (dbgen.AssessmentQuestion, error) {
|
||||
return s.queries.GetAssessmentQuestionByID(ctx, id)
|
||||
}
|
||||
|
||||
// SaveAssessmentAttempt saves the attempt summary and answers
|
||||
// func (s *Store) SaveAssessmentAttempt(ctx context.Context, userID int64, answers []domain.UserAnswer) (domain.AssessmentAttempt, error) {
|
||||
// total := len(answers)
|
||||
// correct := 0
|
||||
|
||||
// for _, ans := range answers {
|
||||
// if ans.IsCorrect {
|
||||
// correct++
|
||||
// }
|
||||
// }
|
||||
|
||||
// score := float64(correct) / float64(total) * 100
|
||||
// knowledgeLevel := "BEGINNER"
|
||||
// switch {
|
||||
// case score >= 80:
|
||||
// knowledgeLevel = "ADVANCED"
|
||||
// case score >= 50:
|
||||
// knowledgeLevel = "INTERMEDIATE"
|
||||
// }
|
||||
|
||||
// // Save attempt
|
||||
// attemptRow, err := s.queries.CreateAssessmentAttempt(ctx, dbgen.CreateAssessmentAttemptParams{
|
||||
// UserID: userID,
|
||||
// TotalQuestions: int32(total),
|
||||
// CorrectAnswers: int32(correct),
|
||||
// ScorePercentage: pgtype.Numeric{Int: big.NewInt(int64(score * 100)), Valid: true},
|
||||
// KnowledgeLevel: knowledgeLevel,
|
||||
// })
|
||||
// if err != nil {
|
||||
// return domain.AssessmentAttempt{}, err
|
||||
// }
|
||||
|
||||
// // Save answers
|
||||
// for _, ans := range answers {
|
||||
// err := s.queries.CreateAssessmentAnswer(ctx, dbgen.CreateAssessmentAnswerParams{
|
||||
// AttemptID: attemptRow.ID,
|
||||
// QuestionID: ans.QuestionID,
|
||||
// SelectedOptionID: pgtype.Int8{Int64: ans.SelectedOptionID, Valid: true},
|
||||
// ShortAnswer: pgtype.Text{String: ans.ShortAnswer, Valid: true},
|
||||
// IsCorrect: ans.IsCorrect,
|
||||
// })
|
||||
// if err != nil {
|
||||
// return domain.AssessmentAttempt{}, err
|
||||
// }
|
||||
// }
|
||||
|
||||
// return domain.AssessmentAttempt{
|
||||
// ID: attemptRow.ID,
|
||||
// UserID: userID,
|
||||
// TotalQuestions: total,
|
||||
// CorrectAnswers: correct,
|
||||
// ScorePercentage: score,
|
||||
// KnowledgeLevel: knowledgeLevel,
|
||||
// CompletedAt: attemptRow.CompletedAt.Time,
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
// GetOptionByID fetches a single option to validate correctness
|
||||
func (s *Store) GetOptionByID(ctx context.Context, optionID int64) (domain.AssessmentOption, error) {
|
||||
o, err := s.queries.GetAssessmentOptionByID(ctx, optionID)
|
||||
if err != nil {
|
||||
return domain.AssessmentOption{}, err
|
||||
}
|
||||
return domain.AssessmentOption{
|
||||
ID: o.ID,
|
||||
OptionText: o.OptionText,
|
||||
IsCorrect: o.IsCorrect,
|
||||
}, nil
|
||||
func (s *Store) GetActiveAssessmentQuestions(
|
||||
ctx context.Context,
|
||||
) ([]dbgen.AssessmentQuestion, error) {
|
||||
return s.queries.GetActiveAssessmentQuestions(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) GetAssessmentQuestionsPaginated(
|
||||
ctx context.Context,
|
||||
arg dbgen.GetAssessmentQuestionsPaginatedParams,
|
||||
) ([]dbgen.GetAssessmentQuestionsPaginatedRow, error) {
|
||||
return s.queries.GetAssessmentQuestionsPaginated(ctx, arg)
|
||||
}
|
||||
|
||||
func (s *Store) UpdateAssessmentQuestion(
|
||||
ctx context.Context,
|
||||
arg dbgen.UpdateAssessmentQuestionParams,
|
||||
) error {
|
||||
return s.queries.UpdateAssessmentQuestion(ctx, arg)
|
||||
}
|
||||
|
||||
func (s *Store) DeleteAssessmentQuestion(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.queries.DeleteAssessmentQuestion(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Store) CreateQuestionOption(
|
||||
ctx context.Context,
|
||||
arg dbgen.CreateQuestionOptionParams,
|
||||
) (dbgen.AssessmentQuestionOption, error) {
|
||||
return s.queries.CreateQuestionOption(ctx, arg)
|
||||
}
|
||||
|
||||
func (s *Store) GetQuestionOptions(
|
||||
ctx context.Context,
|
||||
questionID int64,
|
||||
) ([]dbgen.AssessmentQuestionOption, error) {
|
||||
return s.queries.GetQuestionOptions(ctx, questionID)
|
||||
}
|
||||
|
||||
func (s *Store) DeleteQuestionOptionsByQuestionID(
|
||||
ctx context.Context,
|
||||
questionID int64,
|
||||
) error {
|
||||
return s.queries.DeleteQuestionOptionsByQuestionID(ctx, questionID)
|
||||
}
|
||||
|
||||
func (s *Store) CreateShortAnswer(
|
||||
ctx context.Context,
|
||||
arg dbgen.CreateShortAnswerParams,
|
||||
) (dbgen.AssessmentShortAnswer, error) {
|
||||
return s.queries.CreateShortAnswer(ctx, arg)
|
||||
}
|
||||
|
||||
func (s *Store) GetShortAnswersByQuestionID(
|
||||
ctx context.Context,
|
||||
questionID int64,
|
||||
) ([]dbgen.AssessmentShortAnswer, error) {
|
||||
return s.queries.GetShortAnswersByQuestionID(ctx, questionID)
|
||||
}
|
||||
|
|
|
|||
13
internal/services/assessment/helpers.go
Normal file
13
internal/services/assessment/helpers.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package assessment
|
||||
|
||||
import "database/sql"
|
||||
|
||||
func toNullString(v *string) sql.NullString {
|
||||
if v == nil {
|
||||
return sql.NullString{}
|
||||
}
|
||||
return sql.NullString{
|
||||
String: *v,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,180 +1,303 @@
|
|||
package assessment
|
||||
|
||||
import (
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func (s *Service) GetActiveAssessmentQuestions(
|
||||
func (s *Service) CreateQuestion(
|
||||
ctx context.Context,
|
||||
) ([]domain.AssessmentQuestion, error) {
|
||||
input domain.CreateAssessmentQuestionInput,
|
||||
) error {
|
||||
repo := s.initialAssessmentStore
|
||||
|
||||
// 1. Create Question
|
||||
question, err := repo.CreateAssessmentQuestion(
|
||||
ctx,
|
||||
dbgen.CreateAssessmentQuestionParams{
|
||||
Title: input.Title,
|
||||
Description: func() pgtype.Text {
|
||||
ns := toNullString(input.Description)
|
||||
return pgtype.Text{String: ns.String, Valid: ns.Valid}
|
||||
}(),
|
||||
QuestionType: string(input.QuestionType),
|
||||
DifficultyLevel: pgtype.Text{String: input.DifficultyLevel},
|
||||
Points: input.Points,
|
||||
IsActive: input.IsActive,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Branch by Question Type
|
||||
switch input.QuestionType {
|
||||
|
||||
case domain.MultipleChoice:
|
||||
if len(input.Options) == 0 {
|
||||
return errors.New("multiple choice question requires options")
|
||||
}
|
||||
|
||||
for _, opt := range input.Options {
|
||||
_, err := repo.CreateQuestionOption(
|
||||
ctx,
|
||||
dbgen.CreateQuestionOptionParams{
|
||||
QuestionID: question.ID,
|
||||
OptionText: opt.Text,
|
||||
OptionOrder: opt.Order,
|
||||
IsCorrect: opt.IsCorrect,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case domain.TrueFalse:
|
||||
// TRUE
|
||||
if _, err := repo.CreateQuestionOption(
|
||||
ctx,
|
||||
dbgen.CreateQuestionOptionParams{
|
||||
QuestionID: question.ID,
|
||||
OptionText: "True",
|
||||
OptionOrder: 1,
|
||||
IsCorrect: true,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FALSE
|
||||
if _, err := repo.CreateQuestionOption(
|
||||
ctx,
|
||||
dbgen.CreateQuestionOptionParams{
|
||||
QuestionID: question.ID,
|
||||
OptionText: "False",
|
||||
OptionOrder: 2,
|
||||
IsCorrect: false,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case domain.ShortAnswer:
|
||||
if input.CorrectAnswer == nil || *input.CorrectAnswer == "" {
|
||||
return errors.New("short answer question requires correct_answer")
|
||||
}
|
||||
|
||||
_, err := repo.CreateShortAnswer(
|
||||
ctx,
|
||||
dbgen.CreateShortAnswerParams{
|
||||
QuestionID: question.ID,
|
||||
CorrectAnswer: *input.CorrectAnswer,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return errors.New("unsupported question type")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ListQuestions(ctx context.Context) ([]domain.QuestionWithDetails, error) {
|
||||
// repo := s.initialAssessmentStore
|
||||
|
||||
questions, err := s.initialAssessmentStore.GetActiveAssessmentQuestions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// IMPORTANT:
|
||||
// Do NOT expose correct answers to the client
|
||||
for i := range questions {
|
||||
for j := range questions[i].Options {
|
||||
questions[i].Options[j].IsCorrect = false
|
||||
out := make([]domain.QuestionWithDetails, 0, len(questions))
|
||||
for _, q := range questions {
|
||||
item := domain.QuestionWithDetails{Question: q}
|
||||
|
||||
switch domain.QuestionType(q.QuestionType) {
|
||||
case domain.MultipleChoice, domain.TrueFalse:
|
||||
opts, err := s.initialAssessmentStore.GetQuestionOptions(ctx, q.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, opt := range opts {
|
||||
tempOpt := domain.QuestionOption{
|
||||
QuestionID: opt.ID,
|
||||
OptionText: opt.OptionText,
|
||||
}
|
||||
item.Options = append(item.Options, tempOpt)
|
||||
}
|
||||
|
||||
// case domain.ShortAnswer:
|
||||
// sa, err := s.initialAssessmentStore.GetShortAnswerByQuestionID(ctx, q.ID)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// item.ShortAnswer = &sa
|
||||
// }
|
||||
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
|
||||
return questions, nil
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateAssessmentQuestion(
|
||||
ctx context.Context,
|
||||
q domain.AssessmentQuestion,
|
||||
) (domain.AssessmentQuestion, error) {
|
||||
func (s *Service) GetQuestionByID(ctx context.Context, id int64) (domain.QuestionWithDetails, error) {
|
||||
repo := s.initialAssessmentStore
|
||||
|
||||
// Basic validation
|
||||
if q.Title == "" {
|
||||
return domain.AssessmentQuestion{}, errors.New("question title is required")
|
||||
q, err := repo.GetAssessmentQuestionByID(ctx, id)
|
||||
if err != nil {
|
||||
return domain.QuestionWithDetails{}, err
|
||||
}
|
||||
|
||||
if q.QuestionType == "" {
|
||||
return domain.AssessmentQuestion{}, errors.New("question type is required")
|
||||
}
|
||||
|
||||
if q.DifficultyLevel == "" {
|
||||
return domain.AssessmentQuestion{}, errors.New("difficulty level is required")
|
||||
}
|
||||
|
||||
// Multiple choice / true-false must have options
|
||||
if q.QuestionType != string(domain.QuestionTypeShortAnswer) {
|
||||
if len(q.Options) < 2 {
|
||||
return domain.AssessmentQuestion{}, errors.New("at least two options are required")
|
||||
item := domain.QuestionWithDetails{Question: q}
|
||||
switch domain.QuestionType(q.QuestionType) {
|
||||
case domain.MultipleChoice, domain.TrueFalse:
|
||||
opts, err := repo.GetQuestionOptions(ctx, q.ID)
|
||||
if err != nil {
|
||||
return domain.QuestionWithDetails{}, err
|
||||
}
|
||||
|
||||
hasCorrect := false
|
||||
for _, opt := range q.Options {
|
||||
if opt.OptionText == "" {
|
||||
return domain.AssessmentQuestion{}, errors.New("option text cannot be empty")
|
||||
}
|
||||
if opt.IsCorrect {
|
||||
hasCorrect = true
|
||||
for _, opt := range opts {
|
||||
tempOpt := domain.QuestionOption{
|
||||
QuestionID: opt.ID,
|
||||
OptionText: opt.OptionText,
|
||||
}
|
||||
item.Options = append(item.Options, tempOpt)
|
||||
}
|
||||
|
||||
if !hasCorrect {
|
||||
return domain.AssessmentQuestion{}, errors.New("at least one correct option is required")
|
||||
}
|
||||
// case domain.ShortAnswer:
|
||||
// sa, err := repo.GetShortAnswerByQuestionID(ctx, q.ID)
|
||||
// if err != nil {
|
||||
// return QuestionWithDetails{}, err
|
||||
// }
|
||||
// item.ShortAnswer = &sa
|
||||
}
|
||||
|
||||
// Persist via repository
|
||||
return s.initialAssessmentStore.CreateAssessmentQuestion(ctx, q)
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// func (s *Service) SubmitAssessment(
|
||||
// ctx context.Context,
|
||||
// userID int64,
|
||||
// responses []domain.UserAnswer,
|
||||
// ) (domain.AssessmentAttempt, error) {
|
||||
// func (s *Service) UpdateQuestion(ctx context.Context, id int64, input domain.UpdateAssessmentQuestionInput) error {
|
||||
// repo := s.initialAssessmentStore
|
||||
|
||||
// if userID <= 0 {
|
||||
// return domain.AssessmentAttempt{}, errors.New("invalid user id")
|
||||
// // fetch existing
|
||||
// existing, err := repo.GetAssessmentQuestionByID(ctx, id)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// if len(responses) == 0 {
|
||||
// return domain.AssessmentAttempt{}, errors.New("no responses submitted")
|
||||
// }
|
||||
|
||||
// // Step 1: Validate and evaluate answers
|
||||
// for i, ans := range responses {
|
||||
// if ans.QuestionID == 0 {
|
||||
// return domain.AssessmentAttempt{}, errors.New("invalid question id")
|
||||
// }
|
||||
|
||||
// isCorrect, err := s.validateAnswer(ctx, ans)
|
||||
// if err != nil {
|
||||
// return domain.AssessmentAttempt{}, err
|
||||
// }
|
||||
|
||||
// responses[i].IsCorrect = isCorrect
|
||||
// }
|
||||
|
||||
// // Step 2: Persist assessment attempt + answers
|
||||
// attempt, err := s.initialAssessmentStore.SaveAssessmentAttempt(
|
||||
// // update base question
|
||||
// _, err = repo.UpdateAssessmentQuestion(
|
||||
// ctx,
|
||||
// userID,
|
||||
// responses,
|
||||
// dbgen.UpdateAssessmentQuestionParams{
|
||||
// ID: id,
|
||||
// Title: input.Title,
|
||||
// Description: func() pgtype.Text {
|
||||
// ns := toNullString(input.Description)
|
||||
// return pgtype.Text{String: ns.String, Valid: ns.Valid}
|
||||
// }(),
|
||||
// QuestionType: string(input.QuestionType),
|
||||
// DifficultyLevel: pgtype.Text{String: input.DifficultyLevel},
|
||||
// Points: input.Points,
|
||||
// IsActive: input.IsActive,
|
||||
// },
|
||||
// )
|
||||
// if err != nil {
|
||||
// return domain.AssessmentAttempt{}, err
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // Step 3: Update user's knowledge level
|
||||
// if err := s.userStore.UpdateUserKnowledgeLevel(
|
||||
// ctx,
|
||||
// userID,
|
||||
// attempt.KnowledgeLevel,
|
||||
// ); err != nil {
|
||||
// return domain.AssessmentAttempt{}, err
|
||||
// // remove previous dependents (safe to remove regardless of new type)
|
||||
// // try delete options and short answer; ignore not-found errors if repo returns them
|
||||
// if err := repo.DeleteQuestionOptionsByQuestionID(ctx, id); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := repo.DeleteShortAnswerByQuestionID(ctx, id); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // Step 4: Send in-app notification
|
||||
// notification := &domain.Notification{
|
||||
|
||||
// RecipientID: userID,
|
||||
// Level: domain.NotificationLevelInfo,
|
||||
// Reciever: domain.NotificationRecieverSideCustomer,
|
||||
// IsRead: false,
|
||||
// DeliveryStatus: domain.DeliveryStatusSent,
|
||||
// DeliveryChannel: domain.DeliveryChannelInApp,
|
||||
// Payload: domain.NotificationPayload{
|
||||
// Headline: "Knowledge Assessment Completed",
|
||||
// Message: "Your knowledge assessment is complete. Your knowledge level is " + attempt.KnowledgeLevel + ".",
|
||||
// Tags: []string{"assessment", "knowledge-level"},
|
||||
// },
|
||||
// Timestamp: time.Now(),
|
||||
// Type: domain.NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE,
|
||||
// // create dependents for new type
|
||||
// switch input.QuestionType {
|
||||
// case domain.MultipleChoice:
|
||||
// if len(input.Options) == 0 {
|
||||
// return errors.New("multiple choice question requires options")
|
||||
// }
|
||||
// for _, opt := range input.Options {
|
||||
// if _, err := repo.CreateQuestionOption(
|
||||
// ctx,
|
||||
// dbgen.CreateQuestionOptionParams{
|
||||
// QuestionID: id,
|
||||
// OptionText: opt.Text,
|
||||
// OptionOrder: opt.Order,
|
||||
// IsCorrect: opt.IsCorrect,
|
||||
// },
|
||||
// ); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// case domain.TrueFalse:
|
||||
// if _, err := repo.CreateQuestionOption(ctx, dbgen.CreateQuestionOptionParams{
|
||||
// QuestionID: id,
|
||||
// OptionText: "True",
|
||||
// OptionOrder: 1,
|
||||
// IsCorrect: true,
|
||||
// }); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if _, err := repo.CreateQuestionOption(ctx, dbgen.CreateQuestionOptionParams{
|
||||
// QuestionID: id,
|
||||
// OptionText: "False",
|
||||
// OptionOrder: 2,
|
||||
// IsCorrect: false,
|
||||
// }); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// case domain.ShortAnswer:
|
||||
// if input.CorrectAnswer == nil || *input.CorrectAnswer == "" {
|
||||
// return errors.New("short answer question requires correct_answer")
|
||||
// }
|
||||
// if _, err := repo.CreateShortAnswer(ctx, dbgen.CreateShortAnswerParams{
|
||||
// QuestionID: id,
|
||||
// CorrectAnswer: *input.CorrectAnswer,
|
||||
// }); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// default:
|
||||
// return errors.New("unsupported question type")
|
||||
// }
|
||||
|
||||
// if err := s.notificationSvc.SendNotification(ctx, notification); err != nil {
|
||||
// return domain.AssessmentAttempt{}, err
|
||||
// }
|
||||
|
||||
// return attempt, nil
|
||||
// _ = existing
|
||||
// return nil
|
||||
// }
|
||||
|
||||
func (s *Service) validateAnswer(
|
||||
ctx context.Context,
|
||||
answer domain.UserAnswer,
|
||||
) (bool, error) {
|
||||
// func (s *Service) DeleteQuestion(ctx context.Context, id int64) error {
|
||||
// repo := s.initialAssessmentStore
|
||||
|
||||
// Multiple choice / True-False
|
||||
if answer.SelectedOptionID != 0 {
|
||||
option, err := s.initialAssessmentStore.GetOptionByID(
|
||||
ctx,
|
||||
answer.SelectedOptionID,
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return option.IsCorrect, nil
|
||||
}
|
||||
// q, err := repo.GetAssessmentQuestionByID(ctx, id)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// Short answer (future-proofing)
|
||||
if answer.ShortAnswer != "" {
|
||||
// Placeholder: subjective/manual evaluation
|
||||
// For now, mark incorrect
|
||||
return false, nil
|
||||
}
|
||||
// // delete dependents by existing type
|
||||
// switch domain.QuestionType(q.QuestionType) {
|
||||
// case domain.MultipleChoice, domain.TrueFalse:
|
||||
// if err := repo.DeleteQuestionOptionsByQuestionID(ctx, id); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// case domain.ShortAnswer:
|
||||
// if err := repo.DeleteShortAnswerByQuestionID(ctx, id); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
return false, errors.New("invalid answer submission")
|
||||
}
|
||||
// if err := repo.DeleteAssessmentQuestion(ctx, id); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
func CalculateKnowledgeLevel(score float64) string {
|
||||
switch {
|
||||
case score >= 80:
|
||||
return "ADVANCED"
|
||||
case score >= 50:
|
||||
return "INTERMEDIATE"
|
||||
default:
|
||||
return "BEGINNER"
|
||||
}
|
||||
}
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// ...existing code...
|
||||
|
|
|
|||
|
|
@ -108,19 +108,19 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
|||
}
|
||||
}
|
||||
|
||||
if successRes.Role != domain.RoleStudent {
|
||||
h.mongoLoggerSvc.Info("Login attempt: user login of other role",
|
||||
zap.Int("status_code", fiber.StatusForbidden),
|
||||
zap.String("role", string(successRes.Role)),
|
||||
zap.String("email", req.Email),
|
||||
zap.String("phone_number", req.PhoneNumber),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to login",
|
||||
Error: "Only users are allowed to login",
|
||||
})
|
||||
}
|
||||
// if successRes.Role != domain.RoleStudent {
|
||||
// h.mongoLoggerSvc.Info("Login attempt: user login of other role",
|
||||
// zap.Int("status_code", fiber.StatusForbidden),
|
||||
// zap.String("role", string(successRes.Role)),
|
||||
// zap.String("email", req.Email),
|
||||
// zap.String("phone_number", req.PhoneNumber),
|
||||
// zap.Time("timestamp", time.Now()),
|
||||
// )
|
||||
// return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
// Message: "Failed to login",
|
||||
// Error: "Only users are allowed to login",
|
||||
// })
|
||||
// }
|
||||
|
||||
accessToken, err := jwtutil.CreateJwt(
|
||||
successRes.UserId,
|
||||
|
|
|
|||
|
|
@ -2,23 +2,24 @@ package handlers
|
|||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// CreateAssessmentQuestion godoc
|
||||
// @Summary Create assessment question
|
||||
// @Description Creates a new question for the initial knowledge assessment
|
||||
// @Tags assessment
|
||||
// @Description Creates a new assessment question with options or short answer depending on question type
|
||||
// @Tags assessment-question
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param question body domain.AssessmentQuestion true "Assessment question payload"
|
||||
// @Success 201 {object} domain.Response{data=domain.AssessmentQuestion}
|
||||
// @Param body body domain.CreateAssessmentQuestionInput true "Create question payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/assessment/questions [post]
|
||||
func (h *Handler) CreateAssessmentQuestion(c *fiber.Ctx) error {
|
||||
var req domain.AssessmentQuestion
|
||||
var req domain.CreateAssessmentQuestionInput
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
|
|
@ -26,31 +27,38 @@ func (h *Handler) CreateAssessmentQuestion(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
question, err := h.assessmentSvc.CreateAssessmentQuestion(c.Context(), req)
|
||||
if err != nil {
|
||||
// Basic validation
|
||||
if req.Title == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Validation error",
|
||||
Error: "title is required",
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.assessmentSvc.CreateQuestion(c.Context(), req); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to create assessment question",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Assessment question created successfully",
|
||||
Data: question,
|
||||
Message: "Assessment question created successfully",
|
||||
StatusCode: fiber.StatusCreated,
|
||||
Success: true,
|
||||
})
|
||||
}
|
||||
|
||||
// GetActiveAssessmentQuestions godoc
|
||||
// @Summary Get active initial assessment questions
|
||||
// @Description Returns all active questions used for initial knowledge assessment
|
||||
// @Tags assessment
|
||||
// @Accept json
|
||||
// ListAssessmentQuestions godoc
|
||||
// @Summary List assessment questions
|
||||
// @Description Returns all active assessment questions with their options or answers
|
||||
// @Tags assessment-question
|
||||
// @Produce json
|
||||
// @Success 200 {object} domain.Response{data=[]domain.AssessmentQuestion}
|
||||
// @Success 200 {array} domain.QuestionWithDetails
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/assessment/questions [get]
|
||||
func (h *Handler) GetActiveAssessmentQuestions(c *fiber.Ctx) error {
|
||||
questions, err := h.assessmentSvc.GetActiveAssessmentQuestions(c.Context())
|
||||
func (h *Handler) ListAssessmentQuestions(c *fiber.Ctx) error {
|
||||
questions, err := h.assessmentSvc.ListQuestions(c.Context())
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to fetch assessment questions",
|
||||
|
|
@ -59,81 +67,47 @@ func (h *Handler) GetActiveAssessmentQuestions(c *fiber.Ctx) error {
|
|||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Assessment questions fetched successfully",
|
||||
Data: questions,
|
||||
Message: "Questions fetched successfully",
|
||||
Data: questions,
|
||||
Success: true,
|
||||
StatusCode: 200,
|
||||
})
|
||||
}
|
||||
|
||||
// SubmitAssessment godoc
|
||||
// @Summary Submit initial knowledge assessment
|
||||
// @Description Evaluates user responses, calculates knowledge level, updates user profile, and sends notification
|
||||
// @Tags assessment
|
||||
// @Accept json
|
||||
// GetAssessmentQuestionByID godoc
|
||||
// @Summary Get assessment question by ID
|
||||
// @Description Returns a single assessment question with its options or answer
|
||||
// @Tags assessment-question
|
||||
// @Produce json
|
||||
// @Param user_id path int true "User ID"
|
||||
// @Param payload body domain.SubmitAssessmentReq true "Assessment responses"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Param id path int true "Question ID"
|
||||
// @Success 200 {object} domain.QuestionWithDetails
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 404 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/{tenant_slug}/assessment/submit [post]
|
||||
// func (h *Handler) SubmitAssessment(c *fiber.Ctx) error {
|
||||
// @Router /api/v1/assessment/questions/{id} [get]
|
||||
func (h *Handler) GetAssessmentQuestionByID(c *fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid question ID",
|
||||
Error: "question ID must be a positive integer",
|
||||
})
|
||||
}
|
||||
|
||||
// // User ID (from auth context or path, depending on your setup)
|
||||
// userIDStr, ok := c.Locals("user_id").(string)
|
||||
// if !ok || userIDStr == "" {
|
||||
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
// Message: "Invalid user context",
|
||||
// Error: "User ID not found in request context",
|
||||
// })
|
||||
// }
|
||||
question, err := h.assessmentSvc.GetQuestionByID(c.Context(), id)
|
||||
if err != nil {
|
||||
// Adjust if you introduce a sentinel error (e.g. ErrQuestionNotFound)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to fetch assessment question",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// userID, err := strconv.ParseInt(userIDStr, 10, 64)
|
||||
// if err != nil || userID <= 0 {
|
||||
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
// Message: "Invalid user ID",
|
||||
// Error: "User ID must be a positive integer",
|
||||
// })
|
||||
// }
|
||||
|
||||
// // Parse request body
|
||||
// var req domain.SubmitAssessmentReq
|
||||
// if err := c.BodyParser(&req); err != nil {
|
||||
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
// Message: "Invalid request body",
|
||||
// Error: err.Error(),
|
||||
// })
|
||||
// }
|
||||
|
||||
// if len(req.Answers) == 0 {
|
||||
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
// Message: "No answers submitted",
|
||||
// Error: "Assessment answers cannot be empty",
|
||||
// })
|
||||
// }
|
||||
|
||||
// // Submit assessment
|
||||
// attempt, err := h.assessmentSvc.SubmitAssessment(
|
||||
// c.Context(),
|
||||
// userID,
|
||||
// req.Answers,
|
||||
// )
|
||||
// if err != nil {
|
||||
// if errors.Is(err, authentication.ErrUserNotFound) {
|
||||
// return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
// Message: "User not found",
|
||||
// Error: err.Error(),
|
||||
// })
|
||||
// }
|
||||
|
||||
// return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
// Message: "Failed to submit assessment",
|
||||
// Error: err.Error(),
|
||||
// })
|
||||
// }
|
||||
|
||||
// return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
// Message: "Assessment submitted successfully",
|
||||
// Data: attempt,
|
||||
// })
|
||||
// }
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Question fetched successfully",
|
||||
Data: question,
|
||||
Success: true,
|
||||
StatusCode: 200,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,10 +81,48 @@ func (a *App) initAppRoutes() {
|
|||
})
|
||||
})
|
||||
|
||||
//assessment Routes
|
||||
// Assessment questions
|
||||
groupV1.Post("/assessment/questions", h.CreateAssessmentQuestion)
|
||||
groupV1.Get("/assessment/questions", h.GetActiveAssessmentQuestions)
|
||||
// groupV1.Post("/assessment/submit", a.authMiddleware, h.SubmitAssessment)
|
||||
groupV1.Get("/assessment/questions", h.ListAssessmentQuestions)
|
||||
groupV1.Get("/assessment/questions/:id", h.GetAssessmentQuestionByID)
|
||||
// groupV1.Put("/assessment/questions/:id", h.UpdateAssessmentQuestion)
|
||||
// groupV1.Delete("/assessment/questions/:id", h.DeleteAssessmentQuestion)
|
||||
|
||||
// Start a new assessment attempt
|
||||
// groupV1.Post(
|
||||
// "/assessment/attempts",
|
||||
// h.StartAssessmentAttempt,
|
||||
// )
|
||||
|
||||
// // Submit or update an answer
|
||||
// groupV1.Post(
|
||||
// "/assessment/attempts/:attempt_id/answers",
|
||||
// h.SubmitAssessmentAnswer,
|
||||
// )
|
||||
|
||||
// // Final submission (locks answers)
|
||||
// groupV1.Post(
|
||||
// "/assessment/attempts/:attempt_id/submit",
|
||||
// h.SubmitAssessmentAttempt,
|
||||
// )
|
||||
|
||||
// // Get attempt details
|
||||
// groupV1.Get(
|
||||
// "/assessment/attempts/:attempt_id",
|
||||
// h.GetAssessmentAttemptByID,
|
||||
// )
|
||||
|
||||
// Get final result + answers
|
||||
// groupV1.Get(
|
||||
// "/assessment/attempts/:attempt_id/result",
|
||||
// h.GetAssessmentResult,
|
||||
// )
|
||||
|
||||
// // Evaluate attempt (admin / system)
|
||||
// groupV1.Post(
|
||||
// "/assessment/attempts/:attempt_id/evaluate",
|
||||
// h.EvaluateAssessmentAttempt,
|
||||
// )
|
||||
|
||||
// Course Management Routes
|
||||
groupV1.Post("/course-categories", h.CreateCourseCategory)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user