initial assessment implementation
This commit is contained in:
parent
7309a2bc83
commit
19ac718526
|
|
@ -1,5 +1,74 @@
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
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)
|
-- Global Settings (LMS)
|
||||||
-- ======================================================
|
-- ======================================================
|
||||||
|
|
@ -14,195 +83,144 @@ VALUES
|
||||||
ON CONFLICT (key) DO NOTHING;
|
ON CONFLICT (key) DO NOTHING;
|
||||||
-- ======================================================
|
-- ======================================================
|
||||||
|
|
||||||
INSERT INTO users (
|
-- ======================================================
|
||||||
id,
|
-- Assessment Questions – Level A2 (EASY)
|
||||||
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;
|
|
||||||
|
|
||||||
-- ======================================================
|
INSERT INTO assessment_questions (id, title, question_type, difficulty_level, points, is_active)
|
||||||
-- Courses
|
|
||||||
-- ======================================================
|
|
||||||
-- ======================================================
|
|
||||||
-- Course Categories
|
|
||||||
-- ======================================================
|
|
||||||
INSERT INTO course_categories (
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
is_active,
|
|
||||||
created_at
|
|
||||||
)
|
|
||||||
VALUES
|
VALUES
|
||||||
(1, 'Learning English', TRUE, CURRENT_TIMESTAMP),
|
(1, 'What would you say to greet someone before lunchtime?', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
|
||||||
(2, 'Other Courses', TRUE, CURRENT_TIMESTAMP)
|
(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;
|
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,
|
INSERT INTO assessment_questions (id, title, question_type, difficulty_level, points, is_active)
|
||||||
type,
|
VALUES
|
||||||
level,
|
(6, 'How do you introduce your friend to another person?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
|
||||||
channel,
|
(7, 'How would you ask for the price of an item in a shop?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
|
||||||
title,
|
(8, 'Which sentence correctly gives simple directions?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
|
||||||
message,
|
(9, 'The watch shows 10:50, but the real time is 10:45. What can you say?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
|
||||||
created_at
|
(10, 'Which instruction is correct when giving directions?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE)
|
||||||
)
|
ON CONFLICT (id) DO NOTHING;
|
||||||
VALUES (
|
|
||||||
3,
|
INSERT INTO assessment_question_options (question_id, option_text, option_order, is_correct)
|
||||||
'course_enrolled',
|
VALUES
|
||||||
'info',
|
-- Q6
|
||||||
'in_app',
|
(6, 'Hello, my name is Samson.', 1, FALSE),
|
||||||
'Welcome to your course',
|
(6, 'Good morning. Nice to meet you.', 2, FALSE),
|
||||||
'You have successfully enrolled in Introduction to Go Programming.',
|
(6, 'Let me introduce myself to my friend.', 3, FALSE),
|
||||||
CURRENT_TIMESTAMP
|
(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;
|
FROM notifications;
|
||||||
|
|
||||||
SELECT setval(
|
-- SELECT setval(
|
||||||
pg_get_serial_sequence('referral_codes', 'id'),
|
-- pg_get_serial_sequence('referral_codes', 'id'),
|
||||||
COALESCE(MAX(id), 1)
|
-- COALESCE(MAX(id), 1)
|
||||||
)
|
-- )
|
||||||
FROM referral_codes;
|
-- FROM referral_codes;
|
||||||
|
|
||||||
SELECT setval(
|
-- SELECT setval(
|
||||||
pg_get_serial_sequence('user_referrals', 'id'),
|
-- pg_get_serial_sequence('user_referrals', 'id'),
|
||||||
COALESCE(MAX(id), 1)
|
-- COALESCE(MAX(id), 1)
|
||||||
)
|
-- )
|
||||||
FROM user_referrals;
|
-- FROM user_referrals;
|
||||||
|
|
|
||||||
|
|
@ -36,42 +36,126 @@ CREATE TABLE IF NOT EXISTS users (
|
||||||
CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
|
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,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
description TEXT,
|
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,
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMPTZ
|
updated_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE assessment_question_options (
|
CREATE TABLE IF NOT EXISTS assessment_question_options (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
||||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
|
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
option_text TEXT NOT NULL,
|
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,
|
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,
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
total_questions INT NOT NULL,
|
total_questions INT NOT NULL,
|
||||||
correct_answers INT NOT NULL,
|
total_points INT NOT NULL,
|
||||||
score_percentage NUMERIC(5,2) NOT NULL,
|
|
||||||
knowledge_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED
|
score INT,
|
||||||
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
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,
|
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),
|
question_id BIGINT NOT NULL REFERENCES assessment_questions(id),
|
||||||
selected_option_id BIGINT REFERENCES assessment_question_options(id),
|
|
||||||
short_answer TEXT,
|
question_type VARCHAR(50) NOT NULL,
|
||||||
is_correct BOOLEAN 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 (
|
CREATE TABLE refresh_tokens (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
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
|
-- name: CreateAssessmentQuestion :one
|
||||||
INSERT INTO assessment_questions (
|
INSERT INTO assessment_questions (
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
question_type,
|
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 *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: CreateAssessmentQuestionOption :exec
|
-- name: GetAssessmentQuestionByID :one
|
||||||
INSERT INTO assessment_question_options (
|
SELECT *
|
||||||
question_id,
|
FROM assessment_questions
|
||||||
option_text,
|
WHERE id = $1;
|
||||||
is_correct
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3);
|
|
||||||
|
|
||||||
-- name: GetActiveAssessmentQuestions :many
|
-- name: GetActiveAssessmentQuestions :many
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM assessment_questions
|
FROM assessment_questions
|
||||||
WHERE is_active = TRUE
|
WHERE is_active = true
|
||||||
ORDER BY difficulty_level, id;
|
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
|
-- name: GetQuestionOptions :many
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM assessment_question_options
|
FROM assessment_question_options
|
||||||
|
WHERE question_id = $1
|
||||||
|
ORDER BY option_order;
|
||||||
|
|
||||||
|
-- name: DeleteQuestionOptionsByQuestionID :exec
|
||||||
|
DELETE FROM assessment_question_options
|
||||||
WHERE question_id = $1;
|
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
|
container_name: yimaru-backend-postgres-1
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
ports:
|
ports:
|
||||||
- "5422:5432"
|
- "5432:5422"
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_PASSWORD=secret
|
- POSTGRES_PASSWORD=secret
|
||||||
- POSTGRES_USER=root
|
- POSTGRES_USER=root
|
||||||
|
|
|
||||||
|
|
@ -11,56 +11,78 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"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
|
const CreateAssessmentAttempt = `-- name: CreateAssessmentAttempt :one
|
||||||
|
|
||||||
INSERT INTO assessment_attempts (
|
INSERT INTO assessment_attempts (
|
||||||
user_id,
|
user_id,
|
||||||
total_questions,
|
total_questions,
|
||||||
correct_answers,
|
total_points,
|
||||||
score_percentage,
|
status
|
||||||
knowledge_level
|
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, -- user_id
|
$1, -- user_id
|
||||||
$2, -- total_questions
|
$2, -- total_questions
|
||||||
$3, -- correct_answers
|
$3, -- total_points
|
||||||
$4, -- score_percentage
|
'IN_PROGRESS'
|
||||||
$5 -- knowledge_level
|
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING id, user_id, total_questions, total_points, score, percentage, status, started_at, submitted_at, evaluated_at, created_at, updated_at
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
total_questions,
|
|
||||||
correct_answers,
|
|
||||||
score_percentage,
|
|
||||||
knowledge_level,
|
|
||||||
completed_at
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateAssessmentAttemptParams struct {
|
type CreateAssessmentAttemptParams struct {
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
TotalQuestions int32 `json:"total_questions"`
|
TotalQuestions int32 `json:"total_questions"`
|
||||||
CorrectAnswers int32 `json:"correct_answers"`
|
TotalPoints int32 `json:"total_points"`
|
||||||
ScorePercentage pgtype.Numeric `json:"score_percentage"`
|
|
||||||
KnowledgeLevel string `json:"knowledge_level"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
func (q *Queries) CreateAssessmentAttempt(ctx context.Context, arg CreateAssessmentAttemptParams) (AssessmentAttempt, error) {
|
func (q *Queries) CreateAssessmentAttempt(ctx context.Context, arg CreateAssessmentAttemptParams) (AssessmentAttempt, error) {
|
||||||
row := q.db.QueryRow(ctx, CreateAssessmentAttempt,
|
row := q.db.QueryRow(ctx, CreateAssessmentAttempt, arg.UserID, arg.TotalQuestions, arg.TotalPoints)
|
||||||
arg.UserID,
|
|
||||||
arg.TotalQuestions,
|
|
||||||
arg.CorrectAnswers,
|
|
||||||
arg.ScorePercentage,
|
|
||||||
arg.KnowledgeLevel,
|
|
||||||
)
|
|
||||||
var i AssessmentAttempt
|
var i AssessmentAttempt
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.UserID,
|
&i.UserID,
|
||||||
&i.TotalQuestions,
|
&i.TotalQuestions,
|
||||||
&i.CorrectAnswers,
|
&i.TotalPoints,
|
||||||
&i.ScorePercentage,
|
&i.Score,
|
||||||
&i.KnowledgeLevel,
|
&i.Percentage,
|
||||||
&i.CompletedAt,
|
&i.Status,
|
||||||
|
&i.StartedAt,
|
||||||
|
&i.SubmittedAt,
|
||||||
|
&i.EvaluatedAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -70,17 +92,28 @@ INSERT INTO assessment_questions (
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
question_type,
|
question_type,
|
||||||
difficulty_level
|
difficulty_level,
|
||||||
|
points,
|
||||||
|
is_active
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4)
|
VALUES (
|
||||||
RETURNING id, title, description, question_type, difficulty_level, is_active, created_at, updated_at
|
$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 {
|
type CreateAssessmentQuestionParams struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
QuestionType string `json:"question_type"`
|
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) {
|
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.Description,
|
||||||
arg.QuestionType,
|
arg.QuestionType,
|
||||||
arg.DifficultyLevel,
|
arg.DifficultyLevel,
|
||||||
|
arg.Points,
|
||||||
|
arg.IsActive,
|
||||||
)
|
)
|
||||||
var i AssessmentQuestion
|
var i AssessmentQuestion
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
|
|
@ -97,6 +132,7 @@ func (q *Queries) CreateAssessmentQuestion(ctx context.Context, arg CreateAssess
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.QuestionType,
|
&i.QuestionType,
|
||||||
&i.DifficultyLevel,
|
&i.DifficultyLevel,
|
||||||
|
&i.Points,
|
||||||
&i.IsActive,
|
&i.IsActive,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
|
@ -104,31 +140,169 @@ func (q *Queries) CreateAssessmentQuestion(ctx context.Context, arg CreateAssess
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateAssessmentQuestionOption = `-- name: CreateAssessmentQuestionOption :exec
|
const CreateQuestionOption = `-- name: CreateQuestionOption :one
|
||||||
INSERT INTO assessment_question_options (
|
INSERT INTO assessment_question_options (
|
||||||
question_id,
|
question_id,
|
||||||
option_text,
|
option_text,
|
||||||
|
option_order,
|
||||||
is_correct
|
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 {
|
type CreateQuestionOptionParams struct {
|
||||||
QuestionID int64 `json:"question_id"`
|
QuestionID int64 `json:"question_id"`
|
||||||
OptionText string `json:"option_text"`
|
OptionText string `json:"option_text"`
|
||||||
|
OptionOrder int32 `json:"option_order"`
|
||||||
IsCorrect bool `json:"is_correct"`
|
IsCorrect bool `json:"is_correct"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateAssessmentQuestionOption(ctx context.Context, arg CreateAssessmentQuestionOptionParams) error {
|
func (q *Queries) CreateQuestionOption(ctx context.Context, arg CreateQuestionOptionParams) (AssessmentQuestionOption, error) {
|
||||||
_, err := q.db.Exec(ctx, CreateAssessmentQuestionOption, arg.QuestionID, arg.OptionText, arg.IsCorrect)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetActiveAssessmentQuestions = `-- name: GetActiveAssessmentQuestions :many
|
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
|
FROM assessment_questions
|
||||||
WHERE is_active = TRUE
|
WHERE is_active = true
|
||||||
ORDER BY difficulty_level, id
|
ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetActiveAssessmentQuestions(ctx context.Context) ([]AssessmentQuestion, error) {
|
func (q *Queries) GetActiveAssessmentQuestions(ctx context.Context) ([]AssessmentQuestion, error) {
|
||||||
|
|
@ -146,6 +320,7 @@ func (q *Queries) GetActiveAssessmentQuestions(ctx context.Context) ([]Assessmen
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.QuestionType,
|
&i.QuestionType,
|
||||||
&i.DifficultyLevel,
|
&i.DifficultyLevel,
|
||||||
|
&i.Points,
|
||||||
&i.IsActive,
|
&i.IsActive,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
|
@ -160,92 +335,219 @@ func (q *Queries) GetActiveAssessmentQuestions(ctx context.Context) ([]Assessmen
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetAssessmentOptionByID = `-- name: GetAssessmentOptionByID :one
|
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
|
||||||
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
|
|
||||||
FROM assessment_attempts
|
FROM assessment_attempts
|
||||||
WHERE user_id = $1
|
WHERE id = $1
|
||||||
ORDER BY completed_at DESC
|
|
||||||
LIMIT 1
|
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetLatestAssessmentAttempt(ctx context.Context, userID int64) (AssessmentAttempt, error) {
|
func (q *Queries) GetAssessmentAttemptByID(ctx context.Context, id int64) (AssessmentAttempt, error) {
|
||||||
row := q.db.QueryRow(ctx, GetLatestAssessmentAttempt, userID)
|
row := q.db.QueryRow(ctx, GetAssessmentAttemptByID, id)
|
||||||
var i AssessmentAttempt
|
var i AssessmentAttempt
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.UserID,
|
&i.UserID,
|
||||||
&i.TotalQuestions,
|
&i.TotalQuestions,
|
||||||
&i.CorrectAnswers,
|
&i.TotalPoints,
|
||||||
&i.ScorePercentage,
|
&i.Score,
|
||||||
&i.KnowledgeLevel,
|
&i.Percentage,
|
||||||
&i.CompletedAt,
|
&i.Status,
|
||||||
|
&i.StartedAt,
|
||||||
|
&i.SubmittedAt,
|
||||||
|
&i.EvaluatedAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
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
|
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
|
FROM assessment_question_options
|
||||||
WHERE question_id = $1
|
WHERE question_id = $1
|
||||||
|
ORDER BY option_order
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetQuestionOptions(ctx context.Context, questionID int64) ([]AssessmentQuestionOption, error) {
|
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.ID,
|
||||||
&i.QuestionID,
|
&i.QuestionID,
|
||||||
&i.OptionText,
|
&i.OptionText,
|
||||||
|
&i.OptionOrder,
|
||||||
&i.IsCorrect,
|
&i.IsCorrect,
|
||||||
|
&i.CreatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -272,3 +576,181 @@ func (q *Queries) GetQuestionOptions(ctx context.Context, questionID int64) ([]A
|
||||||
}
|
}
|
||||||
return items, nil
|
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"
|
"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 {
|
type AssessmentAttempt struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
TotalQuestions int32 `json:"total_questions"`
|
TotalQuestions int32 `json:"total_questions"`
|
||||||
CorrectAnswers int32 `json:"correct_answers"`
|
TotalPoints int32 `json:"total_points"`
|
||||||
ScorePercentage pgtype.Numeric `json:"score_percentage"`
|
Score pgtype.Int4 `json:"score"`
|
||||||
KnowledgeLevel string `json:"knowledge_level"`
|
Percentage pgtype.Numeric `json:"percentage"`
|
||||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
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 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 {
|
type AssessmentQuestion struct {
|
||||||
|
|
@ -31,7 +48,8 @@ type AssessmentQuestion struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
QuestionType string `json:"question_type"`
|
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"`
|
IsActive bool `json:"is_active"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
|
@ -41,7 +59,16 @@ type AssessmentQuestionOption struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
QuestionID int64 `json:"question_id"`
|
QuestionID int64 `json:"question_id"`
|
||||||
OptionText string `json:"option_text"`
|
OptionText string `json:"option_text"`
|
||||||
|
OptionOrder int32 `json:"option_order"`
|
||||||
IsCorrect bool `json:"is_correct"`
|
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 {
|
type Course struct {
|
||||||
|
|
@ -197,7 +224,6 @@ type User struct {
|
||||||
EducationLevel pgtype.Text `json:"education_level"`
|
EducationLevel pgtype.Text `json:"education_level"`
|
||||||
Country pgtype.Text `json:"country"`
|
Country pgtype.Text `json:"country"`
|
||||||
Region pgtype.Text `json:"region"`
|
Region pgtype.Text `json:"region"`
|
||||||
Medium string `json:"medium"`
|
|
||||||
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
|
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
|
||||||
NickName pgtype.Text `json:"nick_name"`
|
NickName pgtype.Text `json:"nick_name"`
|
||||||
Occupation pgtype.Text `json:"occupation"`
|
Occupation pgtype.Text `json:"occupation"`
|
||||||
|
|
|
||||||
|
|
@ -423,7 +423,6 @@ SELECT
|
||||||
language_challange,
|
language_challange,
|
||||||
favoutite_topic,
|
favoutite_topic,
|
||||||
|
|
||||||
medium,
|
|
||||||
email_verified,
|
email_verified,
|
||||||
phone_verified,
|
phone_verified,
|
||||||
status,
|
status,
|
||||||
|
|
@ -463,7 +462,6 @@ type GetUserByEmailPhoneRow struct {
|
||||||
LanguageGoal pgtype.Text `json:"language_goal"`
|
LanguageGoal pgtype.Text `json:"language_goal"`
|
||||||
LanguageChallange pgtype.Text `json:"language_challange"`
|
LanguageChallange pgtype.Text `json:"language_challange"`
|
||||||
FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
|
FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
|
||||||
Medium string `json:"medium"`
|
|
||||||
EmailVerified bool `json:"email_verified"`
|
EmailVerified bool `json:"email_verified"`
|
||||||
PhoneVerified bool `json:"phone_verified"`
|
PhoneVerified bool `json:"phone_verified"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
|
@ -497,7 +495,6 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
||||||
&i.LanguageGoal,
|
&i.LanguageGoal,
|
||||||
&i.LanguageChallange,
|
&i.LanguageChallange,
|
||||||
&i.FavoutiteTopic,
|
&i.FavoutiteTopic,
|
||||||
&i.Medium,
|
|
||||||
&i.EmailVerified,
|
&i.EmailVerified,
|
||||||
&i.PhoneVerified,
|
&i.PhoneVerified,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
|
|
@ -512,7 +509,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetUserByID = `-- name: GetUserByID :one
|
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
|
FROM users
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -533,7 +530,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||||
&i.EducationLevel,
|
&i.EducationLevel,
|
||||||
&i.Country,
|
&i.Country,
|
||||||
&i.Region,
|
&i.Region,
|
||||||
&i.Medium,
|
|
||||||
&i.KnowledgeLevel,
|
&i.KnowledgeLevel,
|
||||||
&i.NickName,
|
&i.NickName,
|
||||||
&i.Occupation,
|
&i.Occupation,
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,70 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
import "time"
|
import dbgen "Yimaru-Backend/gen/db"
|
||||||
|
|
||||||
type QuestionType string
|
type QuestionType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
QuestionTypeMultipleChoice QuestionType = "multiple_choice"
|
MultipleChoice QuestionType = "MULTIPLE_CHOICE"
|
||||||
QuestionTypeTrueFalse QuestionType = "true_false"
|
TrueFalse QuestionType = "TRUE_FALSE"
|
||||||
QuestionTypeShortAnswer QuestionType = "short_answer"
|
ShortAnswer QuestionType = "SHORT_ANSWER"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SubmitAssessmentReq struct {
|
type QuestionWithDetails struct {
|
||||||
Answers []UserAnswer `json:"answers" validate:"required,min=1"`
|
Question dbgen.AssessmentQuestion
|
||||||
|
Options []QuestionOption
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssessmentQuestion struct {
|
type QuestionOption struct {
|
||||||
ID int64
|
QuestionID int64 `json:"question_id"`
|
||||||
|
OptionText string `json:"option_text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateAssessmentQuestionInput struct {
|
||||||
Title string
|
Title string
|
||||||
Description string
|
Description *string
|
||||||
QuestionType string
|
QuestionType QuestionType
|
||||||
DifficultyLevel string
|
DifficultyLevel string
|
||||||
Options []AssessmentOption
|
Points int32
|
||||||
|
IsActive bool
|
||||||
|
|
||||||
|
// Multiple Choice only
|
||||||
|
Options []CreateQuestionOptionInput
|
||||||
|
|
||||||
|
// Short Answer only
|
||||||
|
CorrectAnswer *string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssessmentOption struct {
|
type CreateQuestionOptionInput struct {
|
||||||
ID int64
|
Text string
|
||||||
OptionText string
|
Order int32
|
||||||
IsCorrect bool
|
IsCorrect bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserAnswer struct {
|
// type AssessmentQuestion struct {
|
||||||
QuestionID int64
|
// ID int64
|
||||||
SelectedOptionID int64
|
// QuestionText string
|
||||||
ShortAnswer string
|
// Type QuestionType
|
||||||
IsCorrect bool
|
// Options []string
|
||||||
}
|
// CorrectAnswer string
|
||||||
|
// }
|
||||||
|
|
||||||
type AssessmentAttempt struct {
|
// type AssessmentOption struct {
|
||||||
ID int64
|
// ID int64
|
||||||
UserID int64
|
// OptionText string
|
||||||
TotalQuestions int
|
// IsCorrect bool
|
||||||
CorrectAnswers int
|
// }
|
||||||
ScorePercentage float64
|
|
||||||
KnowledgeLevel string
|
// type AttemptAnswer struct {
|
||||||
CompletedAt time.Time
|
// 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
|
package ports
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InitialAssessmentStore interface {
|
type InitialAssessmentStore interface {
|
||||||
CreateAssessmentQuestion(
|
CreateAssessmentQuestion(ctx context.Context, arg dbgen.CreateAssessmentQuestionParams) (dbgen.AssessmentQuestion, error)
|
||||||
ctx context.Context,
|
GetAssessmentQuestionByID(ctx context.Context, id int64) (dbgen.AssessmentQuestion, error)
|
||||||
q domain.AssessmentQuestion,
|
GetActiveAssessmentQuestions(ctx context.Context) ([]dbgen.AssessmentQuestion, error)
|
||||||
) (domain.AssessmentQuestion, error)
|
GetAssessmentQuestionsPaginated(ctx context.Context, arg dbgen.GetAssessmentQuestionsPaginatedParams) ([]dbgen.GetAssessmentQuestionsPaginatedRow, error)
|
||||||
GetActiveAssessmentQuestions(ctx context.Context) ([]domain.AssessmentQuestion, error)
|
UpdateAssessmentQuestion(ctx context.Context, arg dbgen.UpdateAssessmentQuestionParams) error
|
||||||
// SaveAssessmentAttempt(ctx context.Context, userID int64, answers []domain.UserAnswer) (domain.AssessmentAttempt, error)
|
DeleteAssessmentQuestion(ctx context.Context, id int64) error
|
||||||
GetOptionByID(ctx context.Context, optionID int64) (domain.AssessmentOption, 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"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserStore interface {
|
type UserStore interface {
|
||||||
UpdateUserStatus(ctx context.Context, user domain.UpdateUserReq) error
|
UpdateUserStatus(ctx context.Context, user domain.UpdateUserReq) error
|
||||||
GetCorrectOptionForQuestion(
|
// GetCorrectOptionForQuestion(
|
||||||
ctx context.Context,
|
// ctx context.Context,
|
||||||
questionID int64,
|
// questionID int64,
|
||||||
) (int64, error)
|
// ) (int64, error)
|
||||||
GetLatestAssessmentAttempt(
|
// GetLatestAssessmentAttempt(
|
||||||
ctx context.Context,
|
// ctx context.Context,
|
||||||
userID int64,
|
// userID int64,
|
||||||
) (*dbgen.AssessmentAttempt, error)
|
// ) (*dbgen.AssessmentAttempt, error)
|
||||||
UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error
|
UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error
|
||||||
IsUserNameUnique(ctx context.Context, userName string) (bool, error)
|
IsUserNameUnique(ctx context.Context, userName string) (bool, error)
|
||||||
IsUserPending(ctx context.Context, UserName string) (bool, error)
|
IsUserPending(ctx context.Context, UserName string) (bool, error)
|
||||||
|
|
|
||||||
|
|
@ -2,173 +2,84 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"Yimaru-Backend/internal/ports"
|
"Yimaru-Backend/internal/ports"
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewInitialAssessmentStore(s *Store) ports.InitialAssessmentStore { return s }
|
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(
|
func (s *Store) CreateAssessmentQuestion(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
q domain.AssessmentQuestion,
|
arg dbgen.CreateAssessmentQuestionParams,
|
||||||
) (domain.AssessmentQuestion, error) {
|
) (dbgen.AssessmentQuestion, error) {
|
||||||
|
return s.queries.CreateAssessmentQuestion(ctx, arg)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetActiveAssessmentQuestions(ctx context.Context) ([]domain.AssessmentQuestion, error) {
|
func (s *Store) GetAssessmentQuestionByID(
|
||||||
questionsRows, err := s.queries.GetActiveAssessmentQuestions(ctx)
|
ctx context.Context,
|
||||||
if err != nil {
|
id int64,
|
||||||
return nil, err
|
) (dbgen.AssessmentQuestion, error) {
|
||||||
}
|
return s.queries.GetAssessmentQuestionByID(ctx, id)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveAssessmentAttempt saves the attempt summary and answers
|
func (s *Store) GetActiveAssessmentQuestions(
|
||||||
// func (s *Store) SaveAssessmentAttempt(ctx context.Context, userID int64, answers []domain.UserAnswer) (domain.AssessmentAttempt, error) {
|
ctx context.Context,
|
||||||
// total := len(answers)
|
) ([]dbgen.AssessmentQuestion, error) {
|
||||||
// correct := 0
|
return s.queries.GetActiveAssessmentQuestions(ctx)
|
||||||
|
}
|
||||||
// for _, ans := range answers {
|
|
||||||
// if ans.IsCorrect {
|
func (s *Store) GetAssessmentQuestionsPaginated(
|
||||||
// correct++
|
ctx context.Context,
|
||||||
// }
|
arg dbgen.GetAssessmentQuestionsPaginatedParams,
|
||||||
// }
|
) ([]dbgen.GetAssessmentQuestionsPaginatedRow, error) {
|
||||||
|
return s.queries.GetAssessmentQuestionsPaginated(ctx, arg)
|
||||||
// score := float64(correct) / float64(total) * 100
|
}
|
||||||
// knowledgeLevel := "BEGINNER"
|
|
||||||
// switch {
|
func (s *Store) UpdateAssessmentQuestion(
|
||||||
// case score >= 80:
|
ctx context.Context,
|
||||||
// knowledgeLevel = "ADVANCED"
|
arg dbgen.UpdateAssessmentQuestionParams,
|
||||||
// case score >= 50:
|
) error {
|
||||||
// knowledgeLevel = "INTERMEDIATE"
|
return s.queries.UpdateAssessmentQuestion(ctx, arg)
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Save attempt
|
func (s *Store) DeleteAssessmentQuestion(
|
||||||
// attemptRow, err := s.queries.CreateAssessmentAttempt(ctx, dbgen.CreateAssessmentAttemptParams{
|
ctx context.Context,
|
||||||
// UserID: userID,
|
id int64,
|
||||||
// TotalQuestions: int32(total),
|
) error {
|
||||||
// CorrectAnswers: int32(correct),
|
return s.queries.DeleteAssessmentQuestion(ctx, id)
|
||||||
// ScorePercentage: pgtype.Numeric{Int: big.NewInt(int64(score * 100)), Valid: true},
|
}
|
||||||
// KnowledgeLevel: knowledgeLevel,
|
|
||||||
// })
|
func (s *Store) CreateQuestionOption(
|
||||||
// if err != nil {
|
ctx context.Context,
|
||||||
// return domain.AssessmentAttempt{}, err
|
arg dbgen.CreateQuestionOptionParams,
|
||||||
// }
|
) (dbgen.AssessmentQuestionOption, error) {
|
||||||
|
return s.queries.CreateQuestionOption(ctx, arg)
|
||||||
// // Save answers
|
}
|
||||||
// for _, ans := range answers {
|
|
||||||
// err := s.queries.CreateAssessmentAnswer(ctx, dbgen.CreateAssessmentAnswerParams{
|
func (s *Store) GetQuestionOptions(
|
||||||
// AttemptID: attemptRow.ID,
|
ctx context.Context,
|
||||||
// QuestionID: ans.QuestionID,
|
questionID int64,
|
||||||
// SelectedOptionID: pgtype.Int8{Int64: ans.SelectedOptionID, Valid: true},
|
) ([]dbgen.AssessmentQuestionOption, error) {
|
||||||
// ShortAnswer: pgtype.Text{String: ans.ShortAnswer, Valid: true},
|
return s.queries.GetQuestionOptions(ctx, questionID)
|
||||||
// IsCorrect: ans.IsCorrect,
|
}
|
||||||
// })
|
|
||||||
// if err != nil {
|
func (s *Store) DeleteQuestionOptionsByQuestionID(
|
||||||
// return domain.AssessmentAttempt{}, err
|
ctx context.Context,
|
||||||
// }
|
questionID int64,
|
||||||
// }
|
) error {
|
||||||
|
return s.queries.DeleteQuestionOptionsByQuestionID(ctx, questionID)
|
||||||
// return domain.AssessmentAttempt{
|
}
|
||||||
// ID: attemptRow.ID,
|
|
||||||
// UserID: userID,
|
func (s *Store) CreateShortAnswer(
|
||||||
// TotalQuestions: total,
|
ctx context.Context,
|
||||||
// CorrectAnswers: correct,
|
arg dbgen.CreateShortAnswerParams,
|
||||||
// ScorePercentage: score,
|
) (dbgen.AssessmentShortAnswer, error) {
|
||||||
// KnowledgeLevel: knowledgeLevel,
|
return s.queries.CreateShortAnswer(ctx, arg)
|
||||||
// CompletedAt: attemptRow.CompletedAt.Time,
|
}
|
||||||
// }, nil
|
|
||||||
// }
|
func (s *Store) GetShortAnswersByQuestionID(
|
||||||
|
ctx context.Context,
|
||||||
// GetOptionByID fetches a single option to validate correctness
|
questionID int64,
|
||||||
func (s *Store) GetOptionByID(ctx context.Context, optionID int64) (domain.AssessmentOption, error) {
|
) ([]dbgen.AssessmentShortAnswer, error) {
|
||||||
o, err := s.queries.GetAssessmentOptionByID(ctx, optionID)
|
return s.queries.GetShortAnswersByQuestionID(ctx, questionID)
|
||||||
if err != nil {
|
|
||||||
return domain.AssessmentOption{}, err
|
|
||||||
}
|
|
||||||
return domain.AssessmentOption{
|
|
||||||
ID: o.ID,
|
|
||||||
OptionText: o.OptionText,
|
|
||||||
IsCorrect: o.IsCorrect,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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
|
package assessment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) GetActiveAssessmentQuestions(
|
func (s *Service) CreateQuestion(
|
||||||
ctx context.Context,
|
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)
|
questions, err := s.initialAssessmentStore.GetActiveAssessmentQuestions(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT:
|
out := make([]domain.QuestionWithDetails, 0, len(questions))
|
||||||
// Do NOT expose correct answers to the client
|
for _, q := range questions {
|
||||||
for i := range questions {
|
item := domain.QuestionWithDetails{Question: q}
|
||||||
for j := range questions[i].Options {
|
|
||||||
questions[i].Options[j].IsCorrect = false
|
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(
|
func (s *Service) GetQuestionByID(ctx context.Context, id int64) (domain.QuestionWithDetails, error) {
|
||||||
ctx context.Context,
|
repo := s.initialAssessmentStore
|
||||||
q domain.AssessmentQuestion,
|
|
||||||
) (domain.AssessmentQuestion, error) {
|
|
||||||
|
|
||||||
// Basic validation
|
q, err := repo.GetAssessmentQuestionByID(ctx, id)
|
||||||
if q.Title == "" {
|
if err != nil {
|
||||||
return domain.AssessmentQuestion{}, errors.New("question title is required")
|
return domain.QuestionWithDetails{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if q.QuestionType == "" {
|
item := domain.QuestionWithDetails{Question: q}
|
||||||
return domain.AssessmentQuestion{}, errors.New("question type is required")
|
switch domain.QuestionType(q.QuestionType) {
|
||||||
|
case domain.MultipleChoice, domain.TrueFalse:
|
||||||
|
opts, err := repo.GetQuestionOptions(ctx, q.ID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.QuestionWithDetails{}, 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 := repo.GetShortAnswerByQuestionID(ctx, q.ID)
|
||||||
|
// if err != nil {
|
||||||
|
// return QuestionWithDetails{}, err
|
||||||
|
// }
|
||||||
|
// item.ShortAnswer = &sa
|
||||||
}
|
}
|
||||||
|
|
||||||
if q.DifficultyLevel == "" {
|
return item, nil
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasCorrect {
|
|
||||||
return domain.AssessmentQuestion{}, errors.New("at least one correct option is required")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persist via repository
|
|
||||||
return s.initialAssessmentStore.CreateAssessmentQuestion(ctx, q)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (s *Service) SubmitAssessment(
|
// func (s *Service) UpdateQuestion(ctx context.Context, id int64, input domain.UpdateAssessmentQuestionInput) error {
|
||||||
// ctx context.Context,
|
// repo := s.initialAssessmentStore
|
||||||
// userID int64,
|
|
||||||
// responses []domain.UserAnswer,
|
|
||||||
// ) (domain.AssessmentAttempt, error) {
|
|
||||||
|
|
||||||
// if userID <= 0 {
|
// // fetch existing
|
||||||
// return domain.AssessmentAttempt{}, errors.New("invalid user id")
|
// existing, err := repo.GetAssessmentQuestionByID(ctx, id)
|
||||||
// }
|
|
||||||
|
|
||||||
// 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 {
|
// if err != nil {
|
||||||
// return domain.AssessmentAttempt{}, err
|
// return err
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// responses[i].IsCorrect = isCorrect
|
// // update base question
|
||||||
// }
|
// _, err = repo.UpdateAssessmentQuestion(
|
||||||
|
|
||||||
// // Step 2: Persist assessment attempt + answers
|
|
||||||
// attempt, err := s.initialAssessmentStore.SaveAssessmentAttempt(
|
|
||||||
// ctx,
|
// ctx,
|
||||||
// userID,
|
// dbgen.UpdateAssessmentQuestionParams{
|
||||||
// responses,
|
// 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 {
|
// if err != nil {
|
||||||
// return domain.AssessmentAttempt{}, err
|
// return err
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// // Step 3: Update user's knowledge level
|
// // remove previous dependents (safe to remove regardless of new type)
|
||||||
// if err := s.userStore.UpdateUserKnowledgeLevel(
|
// // 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
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 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,
|
// ctx,
|
||||||
// userID,
|
// dbgen.CreateQuestionOptionParams{
|
||||||
// attempt.KnowledgeLevel,
|
// QuestionID: id,
|
||||||
// ); err != nil {
|
// OptionText: opt.Text,
|
||||||
// return domain.AssessmentAttempt{}, err
|
// OptionOrder: opt.Order,
|
||||||
// }
|
// IsCorrect: opt.IsCorrect,
|
||||||
|
|
||||||
// // 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(),
|
// ); err != nil {
|
||||||
// Type: domain.NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE,
|
// 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 {
|
// _ = existing
|
||||||
// return domain.AssessmentAttempt{}, err
|
// return nil
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// return attempt, nil
|
// func (s *Service) DeleteQuestion(ctx context.Context, id int64) error {
|
||||||
|
// repo := s.initialAssessmentStore
|
||||||
|
|
||||||
|
// q, err := repo.GetAssessmentQuestionByID(ctx, id)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
// }
|
// }
|
||||||
|
|
||||||
func (s *Service) validateAnswer(
|
// // delete dependents by existing type
|
||||||
ctx context.Context,
|
// switch domain.QuestionType(q.QuestionType) {
|
||||||
answer domain.UserAnswer,
|
// case domain.MultipleChoice, domain.TrueFalse:
|
||||||
) (bool, error) {
|
// if err := repo.DeleteQuestionOptionsByQuestionID(ctx, id); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// case domain.ShortAnswer:
|
||||||
|
// if err := repo.DeleteShortAnswerByQuestionID(ctx, id); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// Multiple choice / True-False
|
// if err := repo.DeleteAssessmentQuestion(ctx, id); err != nil {
|
||||||
if answer.SelectedOptionID != 0 {
|
// return err
|
||||||
option, err := s.initialAssessmentStore.GetOptionByID(
|
// }
|
||||||
ctx,
|
|
||||||
answer.SelectedOptionID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return option.IsCorrect, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Short answer (future-proofing)
|
// return nil
|
||||||
if answer.ShortAnswer != "" {
|
// }
|
||||||
// Placeholder: subjective/manual evaluation
|
|
||||||
// For now, mark incorrect
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, errors.New("invalid answer submission")
|
// ...existing code...
|
||||||
}
|
|
||||||
|
|
||||||
func CalculateKnowledgeLevel(score float64) string {
|
|
||||||
switch {
|
|
||||||
case score >= 80:
|
|
||||||
return "ADVANCED"
|
|
||||||
case score >= 50:
|
|
||||||
return "INTERMEDIATE"
|
|
||||||
default:
|
|
||||||
return "BEGINNER"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -108,19 +108,19 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if successRes.Role != domain.RoleStudent {
|
// if successRes.Role != domain.RoleStudent {
|
||||||
h.mongoLoggerSvc.Info("Login attempt: user login of other role",
|
// h.mongoLoggerSvc.Info("Login attempt: user login of other role",
|
||||||
zap.Int("status_code", fiber.StatusForbidden),
|
// zap.Int("status_code", fiber.StatusForbidden),
|
||||||
zap.String("role", string(successRes.Role)),
|
// zap.String("role", string(successRes.Role)),
|
||||||
zap.String("email", req.Email),
|
// zap.String("email", req.Email),
|
||||||
zap.String("phone_number", req.PhoneNumber),
|
// zap.String("phone_number", req.PhoneNumber),
|
||||||
zap.Time("timestamp", time.Now()),
|
// zap.Time("timestamp", time.Now()),
|
||||||
)
|
// )
|
||||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
// return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to login",
|
// Message: "Failed to login",
|
||||||
Error: "Only users are allowed to login",
|
// Error: "Only users are allowed to login",
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
accessToken, err := jwtutil.CreateJwt(
|
accessToken, err := jwtutil.CreateJwt(
|
||||||
successRes.UserId,
|
successRes.UserId,
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,24 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateAssessmentQuestion godoc
|
// CreateAssessmentQuestion godoc
|
||||||
// @Summary Create assessment question
|
// @Summary Create assessment question
|
||||||
// @Description Creates a new question for the initial knowledge assessment
|
// @Description Creates a new assessment question with options or short answer depending on question type
|
||||||
// @Tags assessment
|
// @Tags assessment-question
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param question body domain.AssessmentQuestion true "Assessment question payload"
|
// @Param body body domain.CreateAssessmentQuestionInput true "Create question payload"
|
||||||
// @Success 201 {object} domain.Response{data=domain.AssessmentQuestion}
|
// @Success 201 {object} domain.Response
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
// @Router /api/v1/assessment/questions [post]
|
// @Router /api/v1/assessment/questions [post]
|
||||||
func (h *Handler) CreateAssessmentQuestion(c *fiber.Ctx) error {
|
func (h *Handler) CreateAssessmentQuestion(c *fiber.Ctx) error {
|
||||||
var req domain.AssessmentQuestion
|
var req domain.CreateAssessmentQuestionInput
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "Invalid request body",
|
Message: "Invalid request body",
|
||||||
|
|
@ -26,9 +27,16 @@ func (h *Handler) CreateAssessmentQuestion(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
question, err := h.assessmentSvc.CreateAssessmentQuestion(c.Context(), req)
|
// Basic validation
|
||||||
if err != nil {
|
if req.Title == "" {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
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",
|
Message: "Failed to create assessment question",
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
|
|
@ -36,21 +44,21 @@ func (h *Handler) CreateAssessmentQuestion(c *fiber.Ctx) error {
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
Message: "Assessment question created successfully",
|
Message: "Assessment question created successfully",
|
||||||
Data: question,
|
StatusCode: fiber.StatusCreated,
|
||||||
|
Success: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveAssessmentQuestions godoc
|
// ListAssessmentQuestions godoc
|
||||||
// @Summary Get active initial assessment questions
|
// @Summary List assessment questions
|
||||||
// @Description Returns all active questions used for initial knowledge assessment
|
// @Description Returns all active assessment questions with their options or answers
|
||||||
// @Tags assessment
|
// @Tags assessment-question
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} domain.Response{data=[]domain.AssessmentQuestion}
|
// @Success 200 {array} domain.QuestionWithDetails
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
// @Router /api/v1/assessment/questions [get]
|
// @Router /api/v1/assessment/questions [get]
|
||||||
func (h *Handler) GetActiveAssessmentQuestions(c *fiber.Ctx) error {
|
func (h *Handler) ListAssessmentQuestions(c *fiber.Ctx) error {
|
||||||
questions, err := h.assessmentSvc.GetActiveAssessmentQuestions(c.Context())
|
questions, err := h.assessmentSvc.ListQuestions(c.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to fetch assessment questions",
|
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{
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||||
Message: "Assessment questions fetched successfully",
|
Message: "Questions fetched successfully",
|
||||||
Data: questions,
|
Data: questions,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: 200,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubmitAssessment godoc
|
// GetAssessmentQuestionByID godoc
|
||||||
// @Summary Submit initial knowledge assessment
|
// @Summary Get assessment question by ID
|
||||||
// @Description Evaluates user responses, calculates knowledge level, updates user profile, and sends notification
|
// @Description Returns a single assessment question with its options or answer
|
||||||
// @Tags assessment
|
// @Tags assessment-question
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param user_id path int true "User ID"
|
// @Param id path int true "Question ID"
|
||||||
// @Param payload body domain.SubmitAssessmentReq true "Assessment responses"
|
// @Success 200 {object} domain.QuestionWithDetails
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
// @Failure 404 {object} domain.ErrorResponse
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
// @Router /api/v1/{tenant_slug}/assessment/submit [post]
|
// @Router /api/v1/assessment/questions/{id} [get]
|
||||||
// func (h *Handler) SubmitAssessment(c *fiber.Ctx) error {
|
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)
|
question, err := h.assessmentSvc.GetQuestionByID(c.Context(), id)
|
||||||
// userIDStr, ok := c.Locals("user_id").(string)
|
if err != nil {
|
||||||
// if !ok || userIDStr == "" {
|
// Adjust if you introduce a sentinel error (e.g. ErrQuestionNotFound)
|
||||||
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
// Message: "Invalid user context",
|
Message: "Failed to fetch assessment question",
|
||||||
// Error: "User ID not found in request context",
|
Error: err.Error(),
|
||||||
// })
|
})
|
||||||
// }
|
}
|
||||||
|
|
||||||
// userID, err := strconv.ParseInt(userIDStr, 10, 64)
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||||
// if err != nil || userID <= 0 {
|
Message: "Question fetched successfully",
|
||||||
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
Data: question,
|
||||||
// Message: "Invalid user ID",
|
Success: true,
|
||||||
// Error: "User ID must be a positive integer",
|
StatusCode: 200,
|
||||||
// })
|
})
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // 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,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
|
||||||
|
|
@ -81,10 +81,48 @@ func (a *App) initAppRoutes() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
//assessment Routes
|
// Assessment questions
|
||||||
groupV1.Post("/assessment/questions", h.CreateAssessmentQuestion)
|
groupV1.Post("/assessment/questions", h.CreateAssessmentQuestion)
|
||||||
groupV1.Get("/assessment/questions", h.GetActiveAssessmentQuestions)
|
groupV1.Get("/assessment/questions", h.ListAssessmentQuestions)
|
||||||
// groupV1.Post("/assessment/submit", a.authMiddleware, h.SubmitAssessment)
|
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
|
// Course Management Routes
|
||||||
groupV1.Post("/course-categories", h.CreateCourseCategory)
|
groupV1.Post("/course-categories", h.CreateCourseCategory)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user