Compare commits
No commits in common. "894e18bcaed3d7dfa4af93342bc8396a1b645d56" and "7ecfdd9cc8d28a6aebfabff671c1ddbac41ed227" have entirely different histories.
894e18bcae
...
7ecfdd9cc8
|
|
@ -136,6 +136,190 @@ VALUES
|
|||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Ensure seeded admin has full panel permissions in legacy team_members.permissions JSON.
|
||||
-- RBAC permissions are managed separately, but this keeps seed behavior consistent.
|
||||
UPDATE team_members
|
||||
SET permissions = '["*"]'::jsonb
|
||||
WHERE id = 2 OR email = 'admin@yimaru.com';
|
||||
|
||||
-- ======================================================
|
||||
-- Global Settings (LMS)
|
||||
-- ======================================================
|
||||
INSERT INTO global_settings (key, value)
|
||||
VALUES
|
||||
('platform_name', 'Yimaru LMS'),
|
||||
('default_language', 'en'),
|
||||
('allow_self_signup', 'true'),
|
||||
('otp_expiry_minutes', '5'),
|
||||
('certificate_enabled', 'true'),
|
||||
('max_courses_per_instructor', '50')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
-- ======================================================
|
||||
|
||||
-- ======================================================
|
||||
-- Questions - Level A2 (EASY)
|
||||
-- ======================================================
|
||||
|
||||
INSERT INTO questions (id, question_text, question_type, difficulty_level, points, status)
|
||||
VALUES
|
||||
(1, 'What would you say to greet someone before lunchtime?', 'MCQ', 'EASY', 1, 'PUBLISHED'),
|
||||
(2, 'Which question is correct to ask about your routine?', 'MCQ', 'EASY', 1, 'PUBLISHED'),
|
||||
(3, 'She ___ like pizza.', 'MCQ', 'EASY', 1, 'PUBLISHED'),
|
||||
(4, 'I usually go to school and start class ____ eight o''clock.', 'MCQ', 'EASY', 1, 'PUBLISHED'),
|
||||
(5, 'Someone says, "Here is the book you asked for." What is the best response?', 'MCQ', 'EASY', 1, 'PUBLISHED')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO 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);
|
||||
|
||||
-- ======================================================
|
||||
-- Questions - Level B1 (MEDIUM)
|
||||
-- ======================================================
|
||||
|
||||
INSERT INTO questions (id, question_text, question_type, difficulty_level, points, status)
|
||||
VALUES
|
||||
(6, 'How do you introduce your friend to another person?', 'MCQ', 'MEDIUM', 1, 'PUBLISHED'),
|
||||
(7, 'How would you ask for the price of an item in a shop?', 'MCQ', 'MEDIUM', 1, 'PUBLISHED'),
|
||||
(8, 'Which sentence correctly gives simple directions?', 'MCQ', 'MEDIUM', 1, 'PUBLISHED'),
|
||||
(9, 'The watch shows 10:50, but the real time is 10:45. What can you say?', 'MCQ', 'MEDIUM', 1, 'PUBLISHED'),
|
||||
(10, 'Which instruction is correct when giving directions?', 'MCQ', 'MEDIUM', 1, 'PUBLISHED')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO question_options (question_id, option_text, option_order, is_correct)
|
||||
VALUES
|
||||
-- Q6
|
||||
(6, 'Hello, my name is Samson.', 1, FALSE),
|
||||
(6, 'Good morning. Nice to meet you.', 2, FALSE),
|
||||
(6, 'Let me introduce myself to my friend.', 3, FALSE),
|
||||
(6, 'This is my friend, Samson.', 4, TRUE),
|
||||
|
||||
-- Q7
|
||||
(7, 'How many are these?', 1, FALSE),
|
||||
(7, 'What is this?', 2, FALSE),
|
||||
(7, 'How much is this?', 3, TRUE),
|
||||
(7, 'Where is the nearest shop?', 4, FALSE),
|
||||
|
||||
-- Q8
|
||||
(8, 'Thank you very much for asking.', 1, FALSE),
|
||||
(8, 'Turn left and walk two blocks.', 2, TRUE),
|
||||
(8, 'Why don''t you eat out.', 3, FALSE),
|
||||
(8, 'Take the bus to the park.', 4, FALSE),
|
||||
|
||||
-- Q9
|
||||
(9, 'My watch is slow.', 1, TRUE),
|
||||
(9, 'My watch is late.', 2, FALSE),
|
||||
(9, 'My watch is fast.', 3, FALSE),
|
||||
(9, 'My watch is early.', 4, FALSE),
|
||||
|
||||
-- Q10
|
||||
(10, 'Turn left.', 1, TRUE),
|
||||
(10, 'Turn on left.', 2, FALSE),
|
||||
(10, 'Turn left side.', 3, FALSE),
|
||||
(10, 'Turn to straight.', 4, FALSE);
|
||||
|
||||
-- ======================================================
|
||||
-- Questions - Level B2 (HARD)
|
||||
-- ======================================================
|
||||
|
||||
INSERT INTO questions (id, question_text, question_type, difficulty_level, points, status)
|
||||
VALUES
|
||||
(11, 'What is the most polite way to ask to speak to someone on the phone?', 'MCQ', 'HARD', 1, 'PUBLISHED'),
|
||||
(12, 'How do you correctly state the age of a person who is 30 years old?', 'MCQ', 'HARD', 1, 'PUBLISHED'),
|
||||
(13, 'When asking for help with a new Yimaru App feature, which option is most appropriate?', 'MCQ', 'HARD', 1, 'PUBLISHED'),
|
||||
(14, 'Which word has the unvoiced "th" sound?', 'MCQ', 'HARD', 1, 'PUBLISHED'),
|
||||
(15, 'Which sentence sounds like a warning, not friendly advice?', 'MCQ', 'HARD', 1, 'PUBLISHED'),
|
||||
(16, 'What does this sentence mean? "I will definitely be there on time."', 'MCQ', 'HARD', 1, 'PUBLISHED')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO 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);
|
||||
|
||||
-- ======================================================
|
||||
-- Initial Assessment Question Set
|
||||
-- ======================================================
|
||||
|
||||
INSERT INTO question_sets (id, title, description, set_type, owner_type, status)
|
||||
VALUES
|
||||
(1, 'Initial Assessment', 'Default initial assessment for new users', 'INITIAL_ASSESSMENT', 'STANDALONE', 'PUBLISHED')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO question_set_items (set_id, question_id, display_order)
|
||||
VALUES
|
||||
(1, 1, 1), (1, 2, 2), (1, 3, 3), (1, 4, 4), (1, 5, 5),
|
||||
(1, 6, 6), (1, 7, 7), (1, 8, 8), (1, 9, 9), (1, 10, 10),
|
||||
(1, 11, 11), (1, 12, 12), (1, 13, 13), (1, 14, 14), (1, 15, 15), (1, 16, 16)
|
||||
ON CONFLICT (set_id, question_id) DO NOTHING;
|
||||
|
||||
-- ======================================================
|
||||
-- Course Management seed data removed intentionally.
|
||||
-- Course/category/sub-course/video/practice/question-set fixtures
|
||||
-- are no longer seeded from this baseline script.
|
||||
-- ======================================================
|
||||
|
||||
-- ======================================================
|
||||
-- Team Members / Admin Panel Users (login via /api/v1/team/login)
|
||||
-- Credentials: email + password@123
|
||||
|
|
@ -287,8 +471,3 @@ VALUES
|
|||
CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Legacy team_members row may pre-exist; align admin permissions with seed expectations.
|
||||
UPDATE team_members
|
||||
SET permissions = '["*"]'::jsonb
|
||||
WHERE id = 2 OR email = 'admin@yimaru.com';
|
||||
|
|
|
|||
|
|
@ -1,25 +1,108 @@
|
|||
-- Reset sequences for tables touched by login-only seed (PostgreSQL)
|
||||
-- ======================================================
|
||||
-- Reset sequences for LMS tables (PostgreSQL)
|
||||
-- ======================================================
|
||||
|
||||
-- users.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('users', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM users), 1),
|
||||
true
|
||||
);
|
||||
|
||||
-- questions.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('team_members', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM team_members), 1),
|
||||
pg_get_serial_sequence('questions', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM questions), 1),
|
||||
true
|
||||
);
|
||||
|
||||
-- question_options.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('question_options', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM question_options), 1),
|
||||
true
|
||||
);
|
||||
|
||||
-- question_short_answers.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('question_short_answers', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM question_short_answers), 1),
|
||||
true
|
||||
);
|
||||
|
||||
-- question_sets.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('question_sets', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM question_sets), 1),
|
||||
true
|
||||
);
|
||||
|
||||
-- question_set_items.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('question_set_items', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM question_set_items), 1),
|
||||
true
|
||||
);
|
||||
|
||||
-- refresh_tokens.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('refresh_tokens', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM refresh_tokens), 1),
|
||||
true
|
||||
);
|
||||
|
||||
-- otps.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('otps', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM otps), 1),
|
||||
true
|
||||
);
|
||||
|
||||
-- notifications.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('notifications', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM notifications), 1),
|
||||
true
|
||||
);
|
||||
|
||||
-- reported_issues.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('reported_issues', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM reported_issues), 1),
|
||||
true
|
||||
);
|
||||
|
||||
-- course_categories.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('course_categories', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM course_categories), 1),
|
||||
true
|
||||
);
|
||||
|
||||
-- courses.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('courses', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM courses), 1),
|
||||
true
|
||||
);
|
||||
|
||||
-- sub_courses.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('sub_courses', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM sub_courses), 1),
|
||||
true
|
||||
);
|
||||
|
||||
-- sub_course_videos.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('sub_course_videos', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM sub_course_videos), 1),
|
||||
true
|
||||
);
|
||||
|
||||
-- question_set_personas.id (BIGSERIAL)
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('question_set_personas', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM question_set_personas), 1),
|
||||
true
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1 +1,31 @@
|
|||
-- Intentionally empty: no demo activity log seed (login-only seed in 001).
|
||||
INSERT INTO activity_logs (actor_id, actor_role, action, resource_type, resource_id, message, metadata, ip_address, user_agent, created_at) VALUES
|
||||
(1, 'SUPER_ADMIN', 'CATEGORY_CREATED', 'CATEGORY', 1, 'Created course category: Mathematics', '{"name": "Mathematics"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '30 days'),
|
||||
(1, 'SUPER_ADMIN', 'CATEGORY_CREATED', 'CATEGORY', 2, 'Created course category: Science', '{"name": "Science"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '29 days'),
|
||||
(1, 'SUPER_ADMIN', 'CATEGORY_CREATED', 'CATEGORY', 3, 'Created course category: Language Arts', '{"name": "Language Arts"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '28 days'),
|
||||
(1, 'SUPER_ADMIN', 'COURSE_CREATED', 'COURSE', 1, 'Created course: Algebra Fundamentals', '{"title": "Algebra Fundamentals", "category_id": 1}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '27 days'),
|
||||
(1, 'SUPER_ADMIN', 'COURSE_CREATED', 'COURSE', 2, 'Created course: Biology 101', '{"title": "Biology 101", "category_id": 2}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '26 days'),
|
||||
(2, 'ADMIN', 'COURSE_CREATED', 'COURSE', 3, 'Created course: English Grammar', '{"title": "English Grammar", "category_id": 3}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '25 days'),
|
||||
(1, 'SUPER_ADMIN', 'SUB_COURSE_CREATED', 'SUB_COURSE', 1, 'Created sub-course: Linear Equations', '{"title": "Linear Equations", "course_id": 1, "level": "BEGINNER"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '24 days'),
|
||||
(1, 'SUPER_ADMIN', 'SUB_COURSE_CREATED', 'SUB_COURSE', 2, 'Created sub-course: Quadratic Equations', '{"title": "Quadratic Equations", "course_id": 1, "level": "INTERMEDIATE"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '23 days'),
|
||||
(2, 'ADMIN', 'SUB_COURSE_CREATED', 'SUB_COURSE', 3, 'Created sub-course: Cell Biology', '{"title": "Cell Biology", "course_id": 2, "level": "BEGINNER"}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '22 days'),
|
||||
(1, 'SUPER_ADMIN', 'VIDEO_CREATED', 'VIDEO', 1, 'Created video: Introduction to Algebra', '{"title": "Introduction to Algebra", "sub_course_id": 1}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '21 days'),
|
||||
(1, 'SUPER_ADMIN', 'VIDEO_UPLOADED', 'VIDEO', 1, 'Uploaded video to Vimeo: Introduction to Algebra', '{"title": "Introduction to Algebra", "vimeo_id": "987654321", "file_size": 52428800}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '21 days'),
|
||||
(1, 'SUPER_ADMIN', 'VIDEO_PUBLISHED', 'VIDEO', 1, 'Published video: Introduction to Algebra', '{"title": "Introduction to Algebra"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '20 days'),
|
||||
(2, 'ADMIN', 'VIDEO_CREATED', 'VIDEO', 2, 'Created video: Solving for X', '{"title": "Solving for X", "sub_course_id": 1}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '19 days'),
|
||||
(2, 'ADMIN', 'VIDEO_UPLOADED', 'VIDEO', 2, 'Uploaded video to Vimeo: Solving for X', '{"title": "Solving for X", "vimeo_id": "987654322", "file_size": 41943040}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '19 days'),
|
||||
(1, 'SUPER_ADMIN', 'COURSE_UPDATED', 'COURSE', 1, 'Updated course: Algebra Fundamentals', '{"title": "Algebra Fundamentals", "changed_fields": ["description", "thumbnail"]}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '18 days'),
|
||||
(1, 'SUPER_ADMIN', 'CATEGORY_UPDATED', 'CATEGORY', 1, 'Updated course category: Mathematics & Statistics', '{"name": "Mathematics & Statistics"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '17 days'),
|
||||
(2, 'ADMIN', 'VIDEO_CREATED', 'VIDEO', 3, 'Created video: Cell Structure Overview', '{"title": "Cell Structure Overview", "sub_course_id": 3}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '15 days'),
|
||||
(2, 'ADMIN', 'VIDEO_UPLOADED', 'VIDEO', 3, 'Uploaded video to Vimeo: Cell Structure Overview', '{"title": "Cell Structure Overview", "vimeo_id": "987654323", "file_size": 73400320}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '15 days'),
|
||||
(2, 'ADMIN', 'VIDEO_PUBLISHED', 'VIDEO', 2, 'Published video: Solving for X', '{"title": "Solving for X"}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '14 days'),
|
||||
(1, 'SUPER_ADMIN', 'SUB_COURSE_UPDATED', 'SUB_COURSE', 2, 'Updated sub-course: Quadratic Equations', '{"title": "Quadratic Equations", "changed_fields": ["description"]}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '12 days'),
|
||||
(2, 'ADMIN', 'VIDEO_UPDATED', 'VIDEO', 3, 'Updated video: Cell Structure Overview', '{"title": "Cell Structure Overview", "changed_fields": ["thumbnail", "resolution"]}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '10 days'),
|
||||
(2, 'ADMIN', 'VIDEO_PUBLISHED', 'VIDEO', 3, 'Published video: Cell Structure Overview', '{"title": "Cell Structure Overview"}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '9 days'),
|
||||
(1, 'SUPER_ADMIN', 'VIDEO_ARCHIVED', 'VIDEO', 4, 'Archived video ID: 4', '{"id": 4}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '7 days'),
|
||||
(1, 'SUPER_ADMIN', 'SETTINGS_UPDATED', 'SETTINGS', NULL, 'Updated global settings', '{"keys": ["site_name", "maintenance_mode"]}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '5 days'),
|
||||
(1, 'SUPER_ADMIN', 'TEAM_MEMBER_CREATED', 'TEAM_MEMBER', 3, 'Created team member: John Doe', '{"name": "John Doe", "role": "instructor"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '4 days'),
|
||||
(1, 'SUPER_ADMIN', 'COURSE_CREATED', 'COURSE', 4, 'Created course: Advanced Physics', '{"title": "Advanced Physics", "category_id": 2}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '3 days'),
|
||||
(2, 'ADMIN', 'CATEGORY_DELETED', 'CATEGORY', 5, 'Deleted category ID: 5', '{"id": 5}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '2 days'),
|
||||
(1, 'SUPER_ADMIN', 'SUB_COURSE_DELETED', 'SUB_COURSE', 6, 'Deleted sub-course ID: 6', '{"id": 6}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '1 day'),
|
||||
(2, 'ADMIN', 'VIDEO_DELETED', 'VIDEO', 5, 'Deleted video ID: 5', '{"id": 5}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '6 hours'),
|
||||
(1, 'SUPER_ADMIN', 'TEAM_MEMBER_UPDATED', 'TEAM_MEMBER', 3, 'Updated team member: John Doe', '{"name": "John Doe", "changed_fields": ["role"]}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '2 hours');
|
||||
|
|
|
|||
|
|
@ -1 +1,14 @@
|
|||
-- Intentionally empty: no demo issue-report seed (login-only seed in 001).
|
||||
INSERT INTO reported_issues (user_id, user_role, subject, description, issue_type, status, metadata, created_at, updated_at) VALUES
|
||||
(10, 'USER', 'Video not loading on mobile', 'When I try to play the Algebra Fundamentals introduction video on my phone, it shows a blank screen with a spinner that never stops.', 'video', 'pending', '{"course": "Algebra Fundamentals", "device": "iPhone 14", "browser": "Safari 17"}', now() - interval '14 days', now() - interval '14 days'),
|
||||
(10, 'USER', 'Payment confirmation not received', 'I subscribed to the premium plan yesterday and the money was deducted from my account, but I have not received any confirmation email or SMS.', 'payment', 'in_progress', '{"plan": "Premium", "amount": 500, "payment_method": "telebirr"}', now() - interval '10 days', now() - interval '8 days'),
|
||||
(10, 'USER', 'Cannot change profile picture', 'I am trying to upload a new profile picture but the upload button does not respond when I click it.', 'account', 'resolved', '{"browser": "Chrome 120", "file_type": "jpg", "file_size_kb": 2048}', now() - interval '20 days', now() - interval '15 days'),
|
||||
(10, 'USER', 'Add dark mode support', 'It would be great if the platform had a dark mode option. Studying at night with the bright white background is hard on the eyes.', 'feature_request', 'pending', '{"platform": "web"}', now() - interval '7 days', now() - interval '7 days'),
|
||||
(10, 'USER', 'Quiz results not saving', 'I completed the Biology 101 quiz but when I go back to check my results, it shows as incomplete.', 'bug', 'in_progress', '{"course": "Biology 101", "quiz_id": 5, "attempts": 3}', now() - interval '5 days', now() - interval '3 days'),
|
||||
(12, 'SUPPORT', 'Course content displays incorrectly on tablets', 'Multiple users have reported that course text overlaps with images on tablet devices in landscape mode.', 'content', 'pending', '{"affected_devices": ["iPad Air", "Samsung Galaxy Tab S9"], "orientation": "landscape"}', now() - interval '12 days', now() - interval '12 days'),
|
||||
(12, 'SUPPORT', 'Login fails after password reset', 'After resetting my password through the forgot password flow, the new password is not accepted for login.', 'login', 'resolved', '{"browser": "Firefox 121", "reset_method": "email"}', now() - interval '25 days', now() - interval '18 days'),
|
||||
(12, 'SUPPORT', 'Slow page load times', 'The course listing page takes over 10 seconds to load, especially when filtering by category.', 'performance', 'in_progress', '{"page": "/courses", "avg_load_time_ms": 12500, "filter": "category=Science"}', now() - interval '9 days', now() - interval '6 days'),
|
||||
(10, 'USER', 'Subscription auto-renewal not working', 'My monthly subscription expired even though I had auto-renewal enabled. I had to manually resubscribe.', 'subscription', 'rejected', '{"plan": "Monthly Basic", "expected_renewal": "2026-01-15"}', now() - interval '30 days', now() - interval '22 days'),
|
||||
(12, 'SUPPORT', 'Screen reader cannot read course navigation', 'The course sidebar navigation is not accessible with screen readers. ARIA labels are missing on several interactive elements.', 'accessibility', 'pending', '{"screen_reader": "NVDA", "browser": "Chrome 120", "affected_elements": ["sidebar nav", "progress bar", "video controls"]}', now() - interval '4 days', now() - interval '4 days'),
|
||||
(10, 'USER', 'Certificate download gives 404 error', 'After completing the English Grammar course, clicking the download certificate button returns a page not found error.', 'course', 'pending', '{"course": "English Grammar", "completion_date": "2026-01-28"}', now() - interval '2 days', now() - interval '2 days'),
|
||||
(10, 'USER', 'Cannot access course after subscription renewal', 'I renewed my subscription but I still cannot access premium courses. It says my subscription is inactive.', 'subscription', 'in_progress', '{"plan": "Premium Annual", "renewal_date": "2026-02-01"}', now() - interval '1 day', now() - interval '12 hours')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
|
|
|||
|
|
@ -1 +1,40 @@
|
|||
-- Intentionally empty: no demo notification seed (login-only seed in 001).
|
||||
INSERT INTO notifications (
|
||||
id, user_id, receiver_type, type, level, channel, title, message, payload, is_read, created_at
|
||||
) VALUES
|
||||
-- Learner notifications (receiver_type=user, user_id=10)
|
||||
(1001, 10, 'user', 'course_created', 'info', 'in_app', 'New Course Available', 'A new course "Algebra Fundamentals" has been added. Check it out!', '{"course_title": "Algebra Fundamentals", "category": "Mathematics"}', false, now() - interval '30 days'),
|
||||
(1002, 10, 'user', 'course_created', 'info', 'in_app', 'New Course Available', 'A new course "English Grammar 101" has been added. Check it out!', '{"course_title": "English Grammar 101", "category": "Language"}', false, now() - interval '25 days'),
|
||||
(1003, 10, 'user', 'sub_course_created', 'info', 'in_app', 'New Content Available', 'A new sub-course "Linear Equations" has been added.', '{"sub_course_title": "Linear Equations", "course": "Algebra Fundamentals"}', false, now() - interval '24 days'),
|
||||
(1004, 10, 'user', 'video_added', 'info', 'in_app', 'New Video Available', 'A new video "Introduction to Variables" has been added.', '{"video_title": "Introduction to Variables", "sub_course": "Linear Equations"}', false, now() - interval '23 days'),
|
||||
(1005, 10, 'user', 'payment_verified', 'success', 'in_app', 'Payment Successful', 'Your payment has been verified successfully. Your subscription is now active.', '{"plan": "Premium Monthly", "amount": 500}', true, now() - interval '20 days'),
|
||||
(1006, 10, 'user', 'subscription_activated', 'success', 'in_app', 'Subscription Activated', 'Your Premium Monthly subscription is now active until March 20, 2026.', '{"plan": "Premium Monthly", "expires": "2026-03-20"}', true, now() - interval '20 days'),
|
||||
(1007, 10, 'user', 'knowledge_level_update', 'info', 'in_app', 'Knowledge Level Updated', 'Your knowledge level has been updated to: Intermediate', '{"previous_level": "Beginner", "new_level": "Intermediate"}', false, now() - interval '15 days'),
|
||||
(1008, 10, 'user', 'issue_status_updated', 'info', 'in_app', 'Issue Status Updated', 'Your issue "Video not loading on mobile" has been updated to: in_progress', '{"issue_id": 1, "subject": "Video not loading on mobile", "status": "in_progress"}', true, now() - interval '12 days'),
|
||||
(1009, 10, 'user', 'issue_status_updated', 'success', 'in_app', 'Issue Status Updated', 'Your issue "Cannot change profile picture" has been updated to: resolved', '{"issue_id": 3, "subject": "Cannot change profile picture", "status": "resolved"}', true, now() - interval '10 days'),
|
||||
(1010, 10, 'user', 'course_enrolled', 'success', 'in_app', 'Course Enrolled', 'You have been enrolled in "Biology 101".', '{"course_title": "Biology 101"}', false, now() - interval '8 days'),
|
||||
(1011, 10, 'user', 'assessment_assigned', 'info', 'in_app', 'New Assessment Available', 'A new assessment is available for "Algebra Fundamentals".', '{"course": "Algebra Fundamentals", "assessment_type": "quiz"}', false, now() - interval '5 days'),
|
||||
(1012, 10, 'user', 'announcement', 'info', 'in_app', 'Platform Maintenance', 'Scheduled maintenance on Feb 15, 2026 from 2:00 AM - 4:00 AM EAT.', '{"scheduled_at": "2026-02-15T02:00:00+03:00", "duration_hours": 2}', false, now() - interval '2 days'),
|
||||
(1013, 10, 'user', 'video_added', 'info', 'in_app', 'New Video Available', 'A new video "Solving Quadratic Equations" has been added.', '{"video_title": "Solving Quadratic Equations", "sub_course": "Quadratics"}', false, now() - interval '1 day'),
|
||||
|
||||
-- Team member notifications (receiver_type=team_member, user_id references team_members.id)
|
||||
(1014, 2, 'team_member', 'issue_created', 'info', 'in_app', 'New Issue Reported', 'A new issue "Video not loading on mobile" has been reported.', '{"issue_id": 1, "subject": "Video not loading on mobile", "reporter_id": 10}', false, now() - interval '14 days'),
|
||||
(1015, 2, 'team_member', 'issue_created', 'info', 'in_app', 'New Issue Reported', 'A new issue "Payment confirmation not received" has been reported.', '{"issue_id": 2, "subject": "Payment confirmation not received", "reporter_id": 10}', false, now() - interval '10 days'),
|
||||
(1016, 2, 'team_member', 'issue_created', 'info', 'in_app', 'New Issue Reported', 'A new issue "Quiz results not saving" has been reported.', '{"issue_id": 5, "subject": "Quiz results not saving", "reporter_id": 10}', false, now() - interval '5 days'),
|
||||
(1017, 2, 'team_member', 'user_deleted', 'warning', 'in_app', 'User Deleted', 'User ID 99 has been deleted.', '{"deleted_user_id": 99, "deleted_by": 2}', true, now() - interval '18 days'),
|
||||
(1018, 2, 'team_member', 'admin_created', 'info', 'in_app', 'New Admin Created', 'A new admin account has been created for admin@yimaru.com.', '{"admin_email": "admin@yimaru.com"}', true, now() - interval '28 days'),
|
||||
(1019, 2, 'team_member', 'team_member_created','info', 'in_app', 'New Team Member', 'A new team member has been added.', '{"member_email": "support@yimaru.com", "role": "support"}', true, now() - interval '26 days'),
|
||||
(1020, 2, 'team_member', 'system_alert', 'warning', 'in_app', 'High Error Rate Detected', 'The notification delivery failure rate exceeded 5% in the last hour.', '{"failure_rate": 5.2, "window": "1h"}', false, now() - interval '3 days'),
|
||||
(1021, 3, 'team_member', 'announcement', 'info', 'in_app', 'Weekly Registration Report','15 new students registered this week.', '{"count": 15, "period": "weekly"}', false, now() - interval '1 day')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Scheduled notifications seeds (created_by references users.id)
|
||||
INSERT INTO scheduled_notifications (
|
||||
id, channel, title, message, html, scheduled_at, status, target_user_ids, target_role, target_raw,
|
||||
attempt_count, last_error, processing_started_at, sent_at, cancelled_at, created_by, created_at, updated_at
|
||||
) VALUES
|
||||
(2001, 'push', 'Reminder: Continue Your Lesson', 'Pick up where you left off and continue learning today.', NULL, now() + interval '6 hours', 'pending', ARRAY[10,11], NULL, NULL, 0, NULL, NULL, NULL, NULL, 10, now() - interval '1 day', now() - interval '1 day'),
|
||||
(2002, 'email', 'Weekly Progress Summary', 'Your weekly course progress summary is ready.', '<p>Your weekly course progress summary is ready.</p>', now() + interval '1 day', 'pending', NULL, 'STUDENT', NULL, 0, NULL, NULL, NULL, NULL, 10, now() - interval '1 day', now() - interval '1 day'),
|
||||
(2003, 'sms', 'Platform Maintenance', 'Scheduled maintenance tonight from 02:00 to 04:00 EAT.', NULL, now() - interval '2 days', 'sent', ARRAY[10,12], NULL, NULL, 1, NULL, now() - interval '2 days' - interval '5 minutes', now() - interval '2 days', NULL, 10, now() - interval '3 days', now() - interval '2 days'),
|
||||
(2004, 'email', 'Payment Service Alert', 'Some users may experience delayed payment confirmation.', '<p>Some users may experience delayed payment confirmation.</p>', now() - interval '1 day', 'failed', NULL, 'SUPPORT', NULL, 3, 'SMTP temporary outage', now() - interval '1 day' - interval '15 minutes', NULL, NULL, 10, now() - interval '2 days', now() - interval '1 day'),
|
||||
(2005, 'push', 'Obsolete Campaign', 'This campaign was cancelled by admin.', NULL, now() + interval '2 days', 'cancelled', NULL, NULL, '{"segment":"inactive_users"}'::jsonb, 0, NULL, NULL, NULL, now() - interval '12 hours', 10, now() - interval '1 day', now() - interval '12 hours')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
|
|
|||
|
|
@ -1,2 +1,469 @@
|
|||
-- Intentionally empty: course hierarchy is not seeded from SQL.
|
||||
-- Use admin/API or migrations to create content.
|
||||
-- ======================================================
|
||||
-- Complete Course Management Seed Data
|
||||
-- Covers: categories, courses, sub-courses, videos,
|
||||
-- question sets, questions, options, prerequisites,
|
||||
-- and user progress for admin panel integration
|
||||
-- ======================================================
|
||||
|
||||
-- ======================================================
|
||||
-- Course Categories (supplement existing 3 categories)
|
||||
-- Existing: 1=Programming, 2=Data Science, 3=Web Development
|
||||
-- ======================================================
|
||||
INSERT INTO course_categories (id, name, is_active, created_at) VALUES
|
||||
(4, 'Mobile Development', TRUE, CURRENT_TIMESTAMP),
|
||||
(5, 'DevOps & Cloud', TRUE, CURRENT_TIMESTAMP),
|
||||
(6, 'Cybersecurity', FALSE, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ======================================================
|
||||
-- Courses (supplement existing 7 courses)
|
||||
-- Existing: 1-7 in categories 1-3
|
||||
-- ======================================================
|
||||
INSERT INTO courses (id, category_id, title, description, thumbnail, intro_video_url, is_active) VALUES
|
||||
(8, 4, 'Flutter App Development', 'Build cross-platform mobile apps with Flutter and Dart', 'https://example.com/thumbnails/flutter.jpg', 'https://example.com/intro/flutter.mp4', TRUE),
|
||||
(9, 4, 'React Native Essentials', 'Create native mobile apps using React Native', 'https://example.com/thumbnails/react-native.jpg', NULL, TRUE),
|
||||
(10, 5, 'Docker & Kubernetes', 'Container orchestration and deployment strategies', 'https://example.com/thumbnails/docker-k8s.jpg', 'https://example.com/intro/docker.mp4', TRUE),
|
||||
(11, 5, 'CI/CD Pipeline Mastery', 'Automate your build, test, and deployment workflows', 'https://example.com/thumbnails/cicd.jpg', NULL, FALSE),
|
||||
(12, 6, 'Ethical Hacking Fundamentals', 'Learn penetration testing and security analysis', 'https://example.com/thumbnails/ethical-hacking.jpg', NULL, FALSE)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ======================================================
|
||||
-- Sub-courses (supplement existing 17 sub-courses: IDs 1-17)
|
||||
-- ======================================================
|
||||
INSERT INTO sub_courses (id, course_id, title, description, thumbnail, display_order, level, sub_level, is_active) VALUES
|
||||
-- Flutter sub-courses (course 8) — IDs 18-21
|
||||
(18, 8, 'Dart Language Basics', 'Learn Dart programming language fundamentals', NULL, 1, 'BEGINNER', 'A1', TRUE),
|
||||
(19, 8, 'Flutter UI Widgets', 'Build beautiful UIs with Flutter widgets', NULL, 2, 'BEGINNER', 'A2', TRUE),
|
||||
(20, 8, 'State Management', 'Manage app state with Provider and Riverpod', NULL, 3, 'INTERMEDIATE', 'B1', TRUE),
|
||||
(21, 8, 'Flutter Networking & APIs', 'HTTP requests, REST APIs, and data persistence', NULL, 4, 'ADVANCED', 'C1', TRUE),
|
||||
|
||||
-- React Native sub-courses (course 9) — IDs 22-24
|
||||
(22, 9, 'React Native Setup', 'Environment setup and first app', NULL, 1, 'BEGINNER', 'A1', TRUE),
|
||||
(23, 9, 'Navigation & Routing', 'React Navigation and screen management', NULL, 2, 'INTERMEDIATE', 'B1', TRUE),
|
||||
(24, 9, 'Native Modules', 'Bridge native code with React Native', NULL, 3, 'ADVANCED', 'C1', TRUE),
|
||||
|
||||
-- Docker & Kubernetes sub-courses (course 10) — IDs 25-27
|
||||
(25, 10, 'Docker Fundamentals', 'Containers, images, and Dockerfiles', NULL, 1, 'BEGINNER', 'A1', TRUE),
|
||||
(26, 10, 'Docker Compose', 'Multi-container applications', NULL, 2, 'INTERMEDIATE', 'B1', TRUE),
|
||||
(27, 10, 'Kubernetes Basics', 'Pods, services, and deployments', NULL, 3, 'ADVANCED', 'C1', TRUE),
|
||||
|
||||
-- CI/CD sub-courses (course 11) — IDs 28-29
|
||||
(28, 11, 'Git Workflows', 'Branching strategies and pull requests', NULL, 1, 'BEGINNER', 'A1', TRUE),
|
||||
(29, 11, 'GitHub Actions', 'Automate workflows with GitHub Actions', NULL, 2, 'INTERMEDIATE', 'B1', TRUE),
|
||||
|
||||
-- Cybersecurity sub-courses (course 12) — IDs 30-31
|
||||
(30, 12, 'Network Security Basics', 'Firewalls, VPNs, and network protocols', NULL, 1, 'BEGINNER', 'A1', TRUE),
|
||||
(31, 12, 'Penetration Testing', 'Tools and techniques for pen testing', NULL, 2, 'ADVANCED', 'C1', TRUE)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ======================================================
|
||||
-- Sub-course Videos (supplement existing 5 videos: IDs 1-5)
|
||||
-- ======================================================
|
||||
INSERT INTO sub_course_videos (
|
||||
id, sub_course_id, title, description, video_url,
|
||||
duration, resolution, visibility, display_order, status,
|
||||
video_host_provider, vimeo_id, vimeo_embed_url, vimeo_status
|
||||
) VALUES
|
||||
-- Dart Language Basics videos (sub_course 18)
|
||||
(6, 18, 'Introduction to Dart', 'Overview of Dart programming language', 'https://example.com/dart-intro.mp4', 720, '1080p', 'public', 1, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
|
||||
(7, 18, 'Variables and Data Types', 'Dart variables, constants, and types', 'https://example.com/dart-variables.mp4', 900, '1080p', 'public', 2, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
|
||||
(8, 18, 'Control Flow in Dart', 'If/else, loops, and switch statements', 'https://example.com/dart-control.mp4', 1100, '720p', 'public', 3, 'DRAFT', 'DIRECT', NULL, NULL, NULL),
|
||||
|
||||
-- Flutter UI Widgets videos (sub_course 19)
|
||||
(9, 19, 'Widget Tree Basics', 'Understanding the Flutter widget tree', 'https://player.vimeo.com/video/100000001', 1500, '1080p', 'public', 1, 'PUBLISHED', 'VIMEO', '100000001', 'https://player.vimeo.com/video/100000001', 'available'),
|
||||
(10, 19, 'Layout Widgets', 'Row, Column, Stack, and Container widgets', 'https://player.vimeo.com/video/100000002', 1800, '1080p', 'public', 2, 'PUBLISHED', 'VIMEO', '100000002', 'https://player.vimeo.com/video/100000002', 'available'),
|
||||
(11, 19, 'Custom Widgets', 'Building reusable custom widgets', 'https://player.vimeo.com/video/100000003', 2100, '1080p', 'public', 3, 'DRAFT', 'VIMEO', '100000003', 'https://player.vimeo.com/video/100000003', 'transcoding'),
|
||||
|
||||
-- State Management videos (sub_course 20)
|
||||
(12, 20, 'setState and Stateful Widgets', 'Managing local state in Flutter', 'https://example.com/flutter-setstate.mp4', 1200, '1080p', 'public', 1, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
|
||||
(13, 20, 'Provider Pattern', 'Global state management with Provider', 'https://example.com/flutter-provider.mp4', 1600, '1080p', 'public', 2, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
|
||||
|
||||
-- Docker Fundamentals videos (sub_course 25)
|
||||
(14, 25, 'What is Docker?', 'Introduction to containerization', 'https://example.com/docker-intro.mp4', 600, '1080p', 'public', 1, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
|
||||
(15, 25, 'Building Docker Images', 'Writing Dockerfiles and building images', 'https://example.com/docker-images.mp4', 1400, '1080p', 'public', 2, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
|
||||
|
||||
-- Docker Compose videos (sub_course 26)
|
||||
(16, 26, 'Docker Compose Basics', 'Defining multi-container applications', 'https://example.com/compose-basics.mp4', 1300, '1080p', 'public', 1, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
|
||||
|
||||
-- React Native Setup videos (sub_course 22)
|
||||
(17, 22, 'Setting Up React Native', 'Installing React Native CLI and Expo', 'https://example.com/rn-setup.mp4', 900, '1080p', 'public', 1, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL),
|
||||
(18, 22, 'Your First React Native App', 'Creating and running a basic app', 'https://example.com/rn-first-app.mp4', 1100, '1080p', 'public', 2, 'PUBLISHED', 'DIRECT', NULL, NULL, NULL)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ======================================================
|
||||
-- Question Options for existing practice questions (17-20)
|
||||
-- These were missing from the initial seed
|
||||
-- ======================================================
|
||||
INSERT INTO question_options (question_id, option_text, option_order, is_correct) VALUES
|
||||
-- Q17: What is the correct way to print "Hello World" in Python?
|
||||
(17, 'print("Hello World")', 1, TRUE),
|
||||
(17, 'echo "Hello World"', 2, FALSE),
|
||||
(17, 'console.log("Hello World")', 3, FALSE),
|
||||
(17, 'System.out.println("Hello World")', 4, FALSE),
|
||||
|
||||
-- Q18: Which is a valid Python variable name?
|
||||
(18, '2name', 1, FALSE),
|
||||
(18, 'my_name', 2, TRUE),
|
||||
(18, 'my-name', 3, FALSE),
|
||||
(18, 'class', 4, FALSE),
|
||||
|
||||
-- Q19: How do you convert "123" to an integer?
|
||||
(19, 'int("123")', 1, TRUE),
|
||||
(19, 'integer("123")', 2, FALSE),
|
||||
(19, 'str(123)', 3, FALSE),
|
||||
(19, 'toInt("123")', 4, FALSE),
|
||||
|
||||
-- Q20: How many times does range(3) loop run?
|
||||
(20, '2', 1, FALSE),
|
||||
(20, '3', 2, TRUE),
|
||||
(20, '4', 3, FALSE),
|
||||
(20, '1', 4, FALSE);
|
||||
|
||||
-- ======================================================
|
||||
-- Additional Practice Questions for new sub-courses
|
||||
-- ======================================================
|
||||
INSERT INTO questions (id, question_text, question_type, tips, status) VALUES
|
||||
(21, 'What keyword is used to declare a variable in Dart?', 'MCQ', 'Dart uses var, final, or const', 'PUBLISHED'),
|
||||
(22, 'Which widget is the root of every Flutter app?', 'MCQ', 'Think about the main() function', 'PUBLISHED'),
|
||||
(23, 'What is a StatefulWidget?', 'MCQ', 'Consider mutable state', 'PUBLISHED'),
|
||||
(24, 'What command creates a Docker container from an image?', 'MCQ', 'Think about docker run', 'PUBLISHED'),
|
||||
(25, 'What file defines a Docker Compose application?', 'MCQ', 'It is a YAML file', 'PUBLISHED'),
|
||||
(26, 'Which tool is used to create a new React Native project?', 'MCQ', 'Consider npx or expo', 'PUBLISHED')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO question_options (question_id, option_text, option_order, is_correct) VALUES
|
||||
-- Q21: Dart variable declaration
|
||||
(21, 'var', 1, TRUE),
|
||||
(21, 'let', 2, FALSE),
|
||||
(21, 'dim', 3, FALSE),
|
||||
(21, 'define', 4, FALSE),
|
||||
|
||||
-- Q22: Root Flutter widget
|
||||
(22, 'MaterialApp', 1, TRUE),
|
||||
(22, 'Container', 2, FALSE),
|
||||
(22, 'Scaffold', 3, FALSE),
|
||||
(22, 'AppBar', 4, FALSE),
|
||||
|
||||
-- Q23: StatefulWidget
|
||||
(23, 'A widget that can change its state during its lifetime', 1, TRUE),
|
||||
(23, 'A widget that never changes', 2, FALSE),
|
||||
(23, 'A widget for static content only', 3, FALSE),
|
||||
(23, 'A widget that cannot have children', 4, FALSE),
|
||||
|
||||
-- Q24: Docker container creation
|
||||
(24, 'docker run', 1, TRUE),
|
||||
(24, 'docker create', 2, FALSE),
|
||||
(24, 'docker start', 3, FALSE),
|
||||
(24, 'docker build', 4, FALSE),
|
||||
|
||||
-- Q25: Docker Compose file
|
||||
(25, 'docker-compose.yml', 1, TRUE),
|
||||
(25, 'Dockerfile', 2, FALSE),
|
||||
(25, 'docker.json', 3, FALSE),
|
||||
(25, 'compose.xml', 4, FALSE),
|
||||
|
||||
-- Q26: React Native project creation
|
||||
(26, 'npx react-native init', 1, TRUE),
|
||||
(26, 'npm create react-native', 2, FALSE),
|
||||
(26, 'react-native new', 3, FALSE),
|
||||
(26, 'rn init', 4, FALSE);
|
||||
|
||||
-- ======================================================
|
||||
-- Question Sets for new sub-courses
|
||||
-- ======================================================
|
||||
INSERT INTO question_sets (id, title, description, set_type, owner_type, owner_id, persona, status) VALUES
|
||||
(5, 'Dart Basics Quiz', 'Test your Dart fundamentals', 'PRACTICE', 'SUB_COURSE', 18, 'beginner', 'PUBLISHED'),
|
||||
(6, 'Flutter Widgets Assessment', 'Assess Flutter widget knowledge', 'PRACTICE', 'SUB_COURSE', 19, 'beginner', 'PUBLISHED'),
|
||||
(7, 'State Management Quiz', 'Test state management concepts', 'PRACTICE', 'SUB_COURSE', 20, 'intermediate', 'DRAFT'),
|
||||
(8, 'Docker Fundamentals Quiz', 'Test Docker basics', 'PRACTICE', 'SUB_COURSE', 25, 'beginner', 'PUBLISHED'),
|
||||
(9, 'Docker Compose Assessment', 'Assess Docker Compose skills', 'PRACTICE', 'SUB_COURSE', 26, 'intermediate', 'PUBLISHED'),
|
||||
(10, 'React Native Setup Quiz', 'Test React Native setup knowledge', 'PRACTICE', 'SUB_COURSE', 22, 'beginner', 'PUBLISHED')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Ensure every sub-course has at least one practice set
|
||||
INSERT INTO question_sets (title, description, set_type, owner_type, owner_id, status)
|
||||
SELECT
|
||||
sc.title || ' Practice',
|
||||
'Default practice set for ' || sc.title,
|
||||
'PRACTICE',
|
||||
'SUB_COURSE',
|
||||
sc.id,
|
||||
'DRAFT'
|
||||
FROM sub_courses sc
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.owner_id = sc.id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status != 'ARCHIVED'
|
||||
);
|
||||
|
||||
-- Ensure every sub-course has one initial assessment set
|
||||
INSERT INTO question_sets (title, description, set_type, owner_type, owner_id, status)
|
||||
SELECT
|
||||
sc.title || ' Entry Assessment',
|
||||
'Initial assessment used before learners start ' || sc.title,
|
||||
'INITIAL_ASSESSMENT',
|
||||
'SUB_COURSE',
|
||||
sc.id,
|
||||
'DRAFT'
|
||||
FROM sub_courses sc
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.owner_id = sc.id
|
||||
AND qs.set_type = 'INITIAL_ASSESSMENT'
|
||||
AND qs.status != 'ARCHIVED'
|
||||
);
|
||||
|
||||
-- Link questions to question sets
|
||||
INSERT INTO question_set_items (set_id, question_id, display_order) VALUES
|
||||
(5, 21, 1),
|
||||
(6, 22, 1),
|
||||
(7, 23, 1),
|
||||
(8, 24, 1),
|
||||
(9, 25, 1),
|
||||
(10, 26, 1)
|
||||
ON CONFLICT (set_id, question_id) DO NOTHING;
|
||||
|
||||
-- Link personas to question sets
|
||||
INSERT INTO question_set_personas (question_set_id, user_id, display_order) VALUES
|
||||
(5, 10, 1), (5, 11, 2),
|
||||
(6, 10, 1), (6, 12, 2),
|
||||
(8, 11, 1),
|
||||
(10, 10, 1)
|
||||
ON CONFLICT (question_set_id, user_id) DO NOTHING;
|
||||
|
||||
-- ======================================================
|
||||
-- Sub-course Prerequisites
|
||||
-- Defines the learning path / dependency graph
|
||||
-- ======================================================
|
||||
INSERT INTO sub_course_prerequisites (sub_course_id, prerequisite_sub_course_id) VALUES
|
||||
-- Python course (IDs 1-5): linear progression
|
||||
-- "Python Basics - Data Types" requires "Python Basics - Getting Started"
|
||||
(2, 1),
|
||||
-- "Python Intermediate - Functions" requires "Python Basics - Data Types"
|
||||
(3, 2),
|
||||
-- "Python Intermediate - Collections" requires "Python Intermediate - Functions"
|
||||
(4, 3),
|
||||
-- "Python Advanced - Best Practices" requires "Python Intermediate - Collections"
|
||||
(5, 4),
|
||||
|
||||
-- JavaScript course (IDs 6-7): linear
|
||||
-- "DOM Manipulation Basics" requires "JavaScript Fundamentals"
|
||||
(7, 6),
|
||||
|
||||
-- Java course (IDs 8-9): linear
|
||||
-- "Spring Framework Intro" requires "Java Core Concepts"
|
||||
(9, 8),
|
||||
|
||||
-- Data Science course (IDs 10-11): linear
|
||||
-- "Advanced Data Analysis" requires "Data Analysis Fundamentals"
|
||||
(11, 10),
|
||||
|
||||
-- ML course (IDs 12-13): linear
|
||||
-- "ML Algorithms" requires "ML Basics"
|
||||
(13, 12),
|
||||
|
||||
-- Full Stack course (IDs 14-15): linear
|
||||
-- "Backend Development" requires "Frontend Fundamentals"
|
||||
(15, 14),
|
||||
|
||||
-- React course (IDs 16-17): linear
|
||||
-- "React Advanced Patterns" requires "React Basics"
|
||||
(17, 16),
|
||||
|
||||
-- Flutter course (IDs 18-21): structured path
|
||||
-- "Flutter UI Widgets" requires "Dart Language Basics"
|
||||
(19, 18),
|
||||
-- "State Management" requires "Flutter UI Widgets"
|
||||
(20, 19),
|
||||
-- "Flutter Networking & APIs" requires "State Management"
|
||||
(21, 20),
|
||||
|
||||
-- React Native course (IDs 22-24): linear
|
||||
-- "Navigation & Routing" requires "React Native Setup"
|
||||
(23, 22),
|
||||
-- "Native Modules" requires "Navigation & Routing"
|
||||
(24, 23),
|
||||
|
||||
-- Docker & Kubernetes course (IDs 25-27): structured
|
||||
-- "Docker Compose" requires "Docker Fundamentals"
|
||||
(26, 25),
|
||||
-- "Kubernetes Basics" requires "Docker Compose"
|
||||
(27, 26),
|
||||
|
||||
-- CI/CD course (IDs 28-29): linear
|
||||
-- "GitHub Actions" requires "Git Workflows"
|
||||
(29, 28),
|
||||
|
||||
-- Cybersecurity course (IDs 30-31): linear
|
||||
-- "Penetration Testing" requires "Network Security Basics"
|
||||
(31, 30)
|
||||
ON CONFLICT (sub_course_id, prerequisite_sub_course_id) DO NOTHING;
|
||||
|
||||
-- ======================================================
|
||||
-- Completion-driven progress seed (auto-aggregate model)
|
||||
-- Seed video/practice completion records, then derive sub-course progress
|
||||
-- ======================================================
|
||||
|
||||
-- Video completions
|
||||
INSERT INTO user_sub_course_video_progress (user_id, sub_course_id, video_id, completed_at, updated_at)
|
||||
SELECT 10, v.sub_course_id, v.id, CURRENT_TIMESTAMP - INTERVAL '20 days', CURRENT_TIMESTAMP - INTERVAL '20 days'
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id IN (1, 2, 18)
|
||||
AND v.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, video_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_sub_course_video_progress (user_id, sub_course_id, video_id, completed_at, updated_at)
|
||||
SELECT 10, v.sub_course_id, v.id, CURRENT_TIMESTAMP - INTERVAL '8 days', CURRENT_TIMESTAMP - INTERVAL '8 days'
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id = 19
|
||||
AND v.status = 'PUBLISHED'
|
||||
AND v.display_order = 1
|
||||
ON CONFLICT (user_id, video_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_sub_course_video_progress (user_id, sub_course_id, video_id, completed_at, updated_at)
|
||||
SELECT 11, v.sub_course_id, v.id, CURRENT_TIMESTAMP - INTERVAL '25 days', CURRENT_TIMESTAMP - INTERVAL '25 days'
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id IN (1, 2, 25)
|
||||
AND v.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, video_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_sub_course_video_progress (user_id, sub_course_id, video_id, completed_at, updated_at)
|
||||
SELECT 11, v.sub_course_id, v.id, CURRENT_TIMESTAMP - INTERVAL '3 days', CURRENT_TIMESTAMP - INTERVAL '3 days'
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id = 26
|
||||
AND v.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, video_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_sub_course_video_progress (user_id, sub_course_id, video_id, completed_at, updated_at)
|
||||
SELECT 12, v.sub_course_id, v.id, CURRENT_TIMESTAMP - INTERVAL '7 days', CURRENT_TIMESTAMP - INTERVAL '7 days'
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id = 22
|
||||
AND v.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, video_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_sub_course_video_progress (user_id, sub_course_id, video_id, completed_at, updated_at)
|
||||
SELECT 12, v.sub_course_id, v.id, CURRENT_TIMESTAMP - INTERVAL '3 days', CURRENT_TIMESTAMP - INTERVAL '3 days'
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id = 18
|
||||
AND v.status = 'PUBLISHED'
|
||||
AND v.display_order = 1
|
||||
ON CONFLICT (user_id, video_id) DO NOTHING;
|
||||
|
||||
-- Practice completions
|
||||
INSERT INTO user_practice_progress (user_id, sub_course_id, question_set_id, completed_at, updated_at)
|
||||
SELECT 10, qs.owner_id::BIGINT, qs.id, CURRENT_TIMESTAMP - INTERVAL '18 days', CURRENT_TIMESTAMP - INTERVAL '18 days'
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND qs.owner_id IN (1, 2, 18)
|
||||
ON CONFLICT (user_id, question_set_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_practice_progress (user_id, sub_course_id, question_set_id, completed_at, updated_at)
|
||||
SELECT 11, qs.owner_id::BIGINT, qs.id, CURRENT_TIMESTAMP - INTERVAL '10 days', CURRENT_TIMESTAMP - INTERVAL '10 days'
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND qs.owner_id IN (1, 2, 25)
|
||||
ON CONFLICT (user_id, question_set_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_practice_progress (user_id, sub_course_id, question_set_id, completed_at, updated_at)
|
||||
SELECT 12, qs.owner_id::BIGINT, qs.id, CURRENT_TIMESTAMP - INTERVAL '7 days', CURRENT_TIMESTAMP - INTERVAL '7 days'
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND qs.owner_id IN (22)
|
||||
ON CONFLICT (user_id, question_set_id) DO NOTHING;
|
||||
|
||||
-- Derive sub-course progress from completion tables (same model as runtime auto-aggregate)
|
||||
WITH target_pairs AS (
|
||||
SELECT DISTINCT user_id, sub_course_id
|
||||
FROM user_sub_course_video_progress
|
||||
WHERE user_id IN (10, 11, 12)
|
||||
UNION
|
||||
SELECT DISTINCT user_id, sub_course_id
|
||||
FROM user_practice_progress
|
||||
WHERE user_id IN (10, 11, 12)
|
||||
),
|
||||
stats AS (
|
||||
SELECT
|
||||
tp.user_id,
|
||||
tp.sub_course_id,
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id = tp.sub_course_id
|
||||
AND v.status = 'PUBLISHED')
|
||||
+
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.owner_id = tp.sub_course_id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED') AS total_items,
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM user_sub_course_video_progress uv
|
||||
JOIN sub_course_videos v ON v.id = uv.video_id
|
||||
WHERE uv.user_id = tp.user_id
|
||||
AND uv.sub_course_id = tp.sub_course_id
|
||||
AND uv.completed_at IS NOT NULL
|
||||
AND v.status = 'PUBLISHED')
|
||||
+
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM user_practice_progress up
|
||||
JOIN question_sets qs ON qs.id = up.question_set_id
|
||||
WHERE up.user_id = tp.user_id
|
||||
AND up.sub_course_id = tp.sub_course_id
|
||||
AND up.completed_at IS NOT NULL
|
||||
AND qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.owner_id = tp.sub_course_id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED') AS completed_items
|
||||
FROM target_pairs tp
|
||||
)
|
||||
INSERT INTO user_sub_course_progress (user_id, sub_course_id, status, progress_percentage, started_at, completed_at, updated_at)
|
||||
SELECT
|
||||
user_id,
|
||||
sub_course_id,
|
||||
CASE
|
||||
WHEN total_items > 0 AND completed_items >= total_items THEN 'COMPLETED'
|
||||
WHEN completed_items > 0 THEN 'IN_PROGRESS'
|
||||
ELSE 'NOT_STARTED'
|
||||
END AS status,
|
||||
CASE
|
||||
WHEN total_items = 0 THEN 0
|
||||
ELSE ROUND((completed_items::NUMERIC * 100.0) / total_items::NUMERIC)::SMALLINT
|
||||
END AS progress_percentage,
|
||||
CASE WHEN completed_items > 0 THEN CURRENT_TIMESTAMP - INTERVAL '10 days' ELSE NULL END AS started_at,
|
||||
CASE WHEN total_items > 0 AND completed_items >= total_items THEN CURRENT_TIMESTAMP - INTERVAL '3 days' ELSE NULL END AS completed_at,
|
||||
CURRENT_TIMESTAMP AS updated_at
|
||||
FROM stats
|
||||
ON CONFLICT (user_id, sub_course_id) DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
progress_percentage = EXCLUDED.progress_percentage,
|
||||
started_at = COALESCE(user_sub_course_progress.started_at, EXCLUDED.started_at),
|
||||
completed_at = EXCLUDED.completed_at,
|
||||
updated_at = EXCLUDED.updated_at;
|
||||
|
||||
-- ======================================================
|
||||
-- Reset sequences to avoid ID conflicts after seeding
|
||||
-- ======================================================
|
||||
SELECT setval(pg_get_serial_sequence('course_categories', 'id'), COALESCE((SELECT MAX(id) FROM course_categories), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('courses', 'id'), COALESCE((SELECT MAX(id) FROM courses), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('sub_courses', 'id'), COALESCE((SELECT MAX(id) FROM sub_courses), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('sub_course_videos', 'id'), COALESCE((SELECT MAX(id) FROM sub_course_videos), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('questions', 'id'), COALESCE((SELECT MAX(id) FROM questions), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('question_options', 'id'), COALESCE((SELECT MAX(id) FROM question_options), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('question_sets', 'id'), COALESCE((SELECT MAX(id) FROM question_sets), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('question_set_items', 'id'), COALESCE((SELECT MAX(id) FROM question_set_items), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('question_set_personas', 'id'), COALESCE((SELECT MAX(id) FROM question_set_personas), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('sub_course_prerequisites', 'id'), COALESCE((SELECT MAX(id) FROM sub_course_prerequisites), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('user_sub_course_progress', 'id'), COALESCE((SELECT MAX(id) FROM user_sub_course_progress), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('user_sub_course_video_progress', 'id'), COALESCE((SELECT MAX(id) FROM user_sub_course_video_progress), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('user_practice_progress', 'id'), COALESCE((SELECT MAX(id) FROM user_practice_progress), 1), true);
|
||||
|
|
|
|||
|
|
@ -1 +1,29 @@
|
|||
-- Intentionally empty: no demo account-deletion seed (login-only seed in 001).
|
||||
-- Seed account deletion request states for admin panel tracking
|
||||
-- Users referenced here are seeded in 001_initial_seed_data.sql (IDs: 10, 11, 12).
|
||||
|
||||
-- Pending deletion request (within grace period)
|
||||
UPDATE users
|
||||
SET
|
||||
deletion_requested_at = now() - interval '2 days',
|
||||
deletion_scheduled_at = now() + interval '13 days',
|
||||
deletion_cancelled_at = NULL,
|
||||
updated_at = now()
|
||||
WHERE id = 10;
|
||||
|
||||
-- Due deletion request (grace period elapsed, awaiting purge worker)
|
||||
UPDATE users
|
||||
SET
|
||||
deletion_requested_at = now() - interval '20 days',
|
||||
deletion_scheduled_at = now() - interval '5 days',
|
||||
deletion_cancelled_at = NULL,
|
||||
updated_at = now()
|
||||
WHERE id = 11;
|
||||
|
||||
-- Cancelled deletion request (request made then cancelled)
|
||||
UPDATE users
|
||||
SET
|
||||
deletion_requested_at = now() - interval '10 days',
|
||||
deletion_scheduled_at = now() + interval '5 days',
|
||||
deletion_cancelled_at = now() - interval '3 days',
|
||||
updated_at = now()
|
||||
WHERE id = 12;
|
||||
|
|
|
|||
|
|
@ -1 +1,67 @@
|
|||
-- Intentionally empty: no demo question seed (login-only seed in 001).
|
||||
-- Seed TRUE_FALSE and SHORT_ANSWER question types
|
||||
-- Ensures question sets contain non-MCQ questions for end-to-end testing.
|
||||
|
||||
-- ======================================================
|
||||
-- TRUE_FALSE questions (stored in questions + question_options)
|
||||
-- ======================================================
|
||||
INSERT INTO questions (
|
||||
id,
|
||||
question_text,
|
||||
question_type,
|
||||
difficulty_level,
|
||||
points,
|
||||
status,
|
||||
created_at
|
||||
)
|
||||
VALUES
|
||||
(27, 'The Python interpreter executes Python code top-to-bottom.', 'TRUE_FALSE', 'EASY', 1, 'PUBLISHED', CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- question_options for TRUE_FALSE: use two options with exactly one correct
|
||||
INSERT INTO question_options (question_id, option_text, option_order, is_correct)
|
||||
VALUES
|
||||
(27, 'True', 1, TRUE),
|
||||
(27, 'False', 2, FALSE)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ======================================================
|
||||
-- SHORT_ANSWER questions (stored in questions + question_short_answers)
|
||||
-- ======================================================
|
||||
INSERT INTO questions (
|
||||
id,
|
||||
question_text,
|
||||
question_type,
|
||||
difficulty_level,
|
||||
points,
|
||||
status,
|
||||
created_at
|
||||
)
|
||||
VALUES
|
||||
(29, 'What keyword is used in Python to define a function?', 'SHORT_ANSWER', 'EASY', 1, 'PUBLISHED', CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO question_short_answers (question_id, acceptable_answer, match_type)
|
||||
VALUES
|
||||
(29, 'def', 'EXACT')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ======================================================
|
||||
-- Link new questions into existing question sets
|
||||
-- Question Set 1: Initial Assessment (set_id = 1, PUBLISHED)
|
||||
-- Question Set 2: Python Basics Assessment (set_id = 2, PUBLISHED)
|
||||
-- ======================================================
|
||||
INSERT INTO question_set_items (set_id, question_id, display_order)
|
||||
VALUES
|
||||
(1, 27, 17),
|
||||
(1, 29, 18),
|
||||
(2, 27, 3),
|
||||
(2, 29, 4)
|
||||
ON CONFLICT (set_id, question_id) DO NOTHING;
|
||||
|
||||
-- ======================================================
|
||||
-- Reset sequences to avoid ID collisions after seeding
|
||||
-- ======================================================
|
||||
SELECT setval(pg_get_serial_sequence('questions', 'id'), COALESCE((SELECT MAX(id) FROM questions), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('question_options', 'id'), COALESCE((SELECT MAX(id) FROM question_options), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('question_short_answers', 'id'), COALESCE((SELECT MAX(id) FROM question_short_answers), 1), true);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
UPDATE question_sets qs
|
||||
SET owner_type = 'SUB_COURSE',
|
||||
owner_id = sm.legacy_sub_course_id
|
||||
FROM sub_modules sm
|
||||
WHERE qs.owner_type = 'SUB_MODULE'
|
||||
AND qs.owner_id = sm.id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND sm.legacy_sub_course_id IS NOT NULL;
|
||||
|
||||
DROP TABLE IF EXISTS sub_module_practices CASCADE;
|
||||
DROP TABLE IF EXISTS sub_module_videos CASCADE;
|
||||
DROP TABLE IF EXISTS sub_modules CASCADE;
|
||||
DROP TABLE IF EXISTS modules CASCADE;
|
||||
DROP TABLE IF EXISTS levels CASCADE;
|
||||
|
||||
ALTER TABLE courses DROP COLUMN IF EXISTS sub_category_id;
|
||||
DROP TABLE IF EXISTS course_sub_categories CASCADE;
|
||||
|
||||
-- Best-effort rollback to old expectation.
|
||||
UPDATE user_practice_progress
|
||||
SET sub_course_id = 1
|
||||
WHERE sub_course_id IS NULL;
|
||||
ALTER TABLE user_practice_progress
|
||||
ALTER COLUMN sub_course_id SET NOT NULL;
|
||||
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
-- Unified hierarchy
|
||||
-- Course Category -> Course Sub-category -> Course -> Level -> Module -> Sub-Module
|
||||
-- -> Sub-Module Videos
|
||||
-- -> Sub-Module Practices (question sets)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS course_sub_categories (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
category_id BIGINT NOT NULL REFERENCES course_categories(id) ON DELETE CASCADE,
|
||||
name VARCHAR(150) NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(category_id, name)
|
||||
);
|
||||
|
||||
ALTER TABLE courses
|
||||
ADD COLUMN IF NOT EXISTS sub_category_id BIGINT REFERENCES course_sub_categories(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_courses_sub_category_id ON courses(sub_category_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS levels (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
course_id BIGINT NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
|
||||
cefr_level VARCHAR(2) NOT NULL,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(course_id, cefr_level),
|
||||
CHECK (cefr_level IN ('A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_levels_course_id ON levels(course_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS modules (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
level_id BIGINT NOT NULL REFERENCES levels(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_modules_level_id ON modules(level_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sub_modules (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
module_id BIGINT NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
legacy_sub_course_id BIGINT UNIQUE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sub_modules_module_id ON sub_modules(module_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sub_module_videos (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
sub_module_id BIGINT NOT NULL REFERENCES sub_modules(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
video_url TEXT NOT NULL,
|
||||
duration INT,
|
||||
resolution VARCHAR(20),
|
||||
is_published BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
publish_date TIMESTAMPTZ,
|
||||
visibility VARCHAR(50),
|
||||
instructor_id VARCHAR(100),
|
||||
thumbnail TEXT,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
|
||||
vimeo_id TEXT,
|
||||
vimeo_embed_url TEXT,
|
||||
vimeo_player_html TEXT,
|
||||
vimeo_status VARCHAR(50),
|
||||
video_host_provider VARCHAR(20),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sub_module_videos_sub_module_id ON sub_module_videos(sub_module_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sub_module_practices (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
sub_module_id BIGINT NOT NULL REFERENCES sub_modules(id) ON DELETE CASCADE,
|
||||
question_set_id BIGINT NOT NULL REFERENCES question_sets(id) ON DELETE CASCADE,
|
||||
intro_video_url TEXT,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(question_set_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sub_module_practices_sub_module_id ON sub_module_practices(sub_module_id);
|
||||
|
||||
-- Practice progress now supports sub-module owned practices where no legacy sub_course exists.
|
||||
ALTER TABLE user_practice_progress
|
||||
ALTER COLUMN sub_course_id DROP NOT NULL;
|
||||
|
||||
-- Backfill from existing structure
|
||||
INSERT INTO course_sub_categories (category_id, name, description, display_order, is_active)
|
||||
SELECT cc.id, c.title || ' Group', 'Auto-generated from existing course structure', 0, TRUE
|
||||
FROM courses c
|
||||
JOIN course_categories cc ON cc.id = c.category_id
|
||||
LEFT JOIN course_sub_categories csc
|
||||
ON csc.category_id = cc.id AND csc.name = c.title || ' Group'
|
||||
WHERE csc.id IS NULL;
|
||||
|
||||
UPDATE courses c
|
||||
SET sub_category_id = csc.id
|
||||
FROM course_sub_categories csc
|
||||
WHERE csc.category_id = c.category_id
|
||||
AND csc.name = c.title || ' Group'
|
||||
AND c.sub_category_id IS NULL;
|
||||
|
||||
INSERT INTO levels (course_id, cefr_level, display_order, is_active)
|
||||
SELECT
|
||||
sc.course_id,
|
||||
sc.sub_level,
|
||||
MIN(sc.display_order),
|
||||
BOOL_AND(sc.is_active)
|
||||
FROM sub_courses sc
|
||||
WHERE sc.sub_level IN ('A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3')
|
||||
GROUP BY sc.course_id, sc.sub_level
|
||||
ON CONFLICT (course_id, cefr_level) DO NOTHING;
|
||||
|
||||
INSERT INTO modules (level_id, title, description, display_order, is_active)
|
||||
SELECT
|
||||
l.id,
|
||||
l.cefr_level || ' Module 1',
|
||||
'Auto-generated default module for ' || l.cefr_level,
|
||||
1,
|
||||
l.is_active
|
||||
FROM levels l
|
||||
LEFT JOIN modules m ON m.level_id = l.id AND m.display_order = 1
|
||||
WHERE m.id IS NULL;
|
||||
|
||||
INSERT INTO sub_modules (module_id, title, description, display_order, is_active, legacy_sub_course_id)
|
||||
SELECT
|
||||
m.id,
|
||||
sc.title,
|
||||
sc.description,
|
||||
sc.display_order,
|
||||
sc.is_active,
|
||||
sc.id
|
||||
FROM sub_courses sc
|
||||
JOIN levels l
|
||||
ON l.course_id = sc.course_id
|
||||
AND l.cefr_level = sc.sub_level
|
||||
JOIN modules m
|
||||
ON m.level_id = l.id
|
||||
AND m.display_order = 1
|
||||
LEFT JOIN sub_modules sm ON sm.legacy_sub_course_id = sc.id
|
||||
WHERE sm.id IS NULL;
|
||||
|
||||
INSERT INTO sub_module_videos (
|
||||
sub_module_id,
|
||||
title,
|
||||
description,
|
||||
video_url,
|
||||
duration,
|
||||
resolution,
|
||||
is_published,
|
||||
publish_date,
|
||||
visibility,
|
||||
instructor_id,
|
||||
thumbnail,
|
||||
display_order,
|
||||
status,
|
||||
vimeo_id,
|
||||
vimeo_embed_url,
|
||||
vimeo_player_html,
|
||||
vimeo_status,
|
||||
video_host_provider
|
||||
)
|
||||
SELECT
|
||||
sm.id,
|
||||
scv.title,
|
||||
scv.description,
|
||||
scv.video_url,
|
||||
scv.duration,
|
||||
scv.resolution,
|
||||
scv.is_published,
|
||||
scv.publish_date,
|
||||
scv.visibility,
|
||||
scv.instructor_id,
|
||||
scv.thumbnail,
|
||||
scv.display_order,
|
||||
scv.status,
|
||||
scv.vimeo_id,
|
||||
scv.vimeo_embed_url,
|
||||
scv.vimeo_player_html,
|
||||
scv.vimeo_status,
|
||||
scv.video_host_provider
|
||||
FROM sub_course_videos scv
|
||||
JOIN sub_modules sm ON sm.legacy_sub_course_id = scv.sub_course_id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sub_module_videos smv
|
||||
WHERE smv.sub_module_id = sm.id
|
||||
AND smv.title = scv.title
|
||||
AND COALESCE(smv.video_url, '') = COALESCE(scv.video_url, '')
|
||||
);
|
||||
|
||||
UPDATE question_sets qs
|
||||
SET owner_type = 'SUB_MODULE',
|
||||
owner_id = sm.id
|
||||
FROM sub_modules sm
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.owner_id = sm.legacy_sub_course_id
|
||||
AND qs.set_type = 'PRACTICE';
|
||||
|
||||
INSERT INTO sub_module_practices (sub_module_id, question_set_id, intro_video_url, display_order, is_active)
|
||||
SELECT
|
||||
sm.id,
|
||||
qs.id,
|
||||
qs.intro_video_url,
|
||||
COALESCE(qs.display_order, 0),
|
||||
(qs.status != 'ARCHIVED')
|
||||
FROM question_sets qs
|
||||
JOIN sub_modules sm
|
||||
ON qs.owner_type = 'SUB_MODULE'
|
||||
AND qs.owner_id = sm.id
|
||||
WHERE qs.set_type = 'PRACTICE'
|
||||
ON CONFLICT (question_set_id) DO NOTHING;
|
||||
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
ALTER TABLE IF EXISTS sub_module_lessons
|
||||
RENAME TO sub_module_practices;
|
||||
|
||||
ALTER INDEX IF EXISTS idx_sub_module_lessons_sub_module_id
|
||||
RENAME TO idx_sub_module_practices_sub_module_id;
|
||||
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
ALTER TABLE IF EXISTS sub_module_practices
|
||||
RENAME TO sub_module_lessons;
|
||||
|
||||
ALTER INDEX IF EXISTS idx_sub_module_practices_sub_module_id
|
||||
RENAME TO idx_sub_module_lessons_sub_module_id;
|
||||
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
DROP INDEX IF EXISTS idx_sub_module_practices_sub_module_id;
|
||||
|
||||
DROP TABLE IF EXISTS sub_module_practices;
|
||||
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS sub_module_practices (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
sub_module_id BIGINT NOT NULL REFERENCES sub_modules(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
thumbnail TEXT,
|
||||
intro_video_url TEXT,
|
||||
question_set_id BIGINT NOT NULL REFERENCES question_sets(id) ON DELETE CASCADE,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(question_set_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sub_module_practices_sub_module_id
|
||||
ON sub_module_practices(sub_module_id);
|
||||
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
-- name: GetCoursesWithHierarchy :many
|
||||
SELECT
|
||||
cc.id AS category_id,
|
||||
cc.name AS category_name,
|
||||
csc.id AS sub_category_id,
|
||||
csc.name AS sub_category_name,
|
||||
c.id AS course_id,
|
||||
c.title AS course_title
|
||||
FROM course_categories cc
|
||||
LEFT JOIN course_sub_categories csc ON csc.category_id = cc.id AND csc.is_active = TRUE
|
||||
LEFT JOIN courses c ON c.sub_category_id = csc.id AND c.is_active = TRUE
|
||||
WHERE cc.is_active = TRUE
|
||||
ORDER BY cc.id, csc.display_order, csc.id, c.id;
|
||||
|
||||
-- name: GetLevelsByCourseID :many
|
||||
SELECT *
|
||||
FROM levels
|
||||
WHERE course_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: GetModulesByLevelID :many
|
||||
SELECT *
|
||||
FROM modules
|
||||
WHERE level_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: GetSubModulesByModuleID :many
|
||||
SELECT *
|
||||
FROM sub_modules
|
||||
WHERE module_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: GetSubModuleVideos :many
|
||||
SELECT *
|
||||
FROM sub_module_videos
|
||||
WHERE sub_module_id = $1
|
||||
AND status != 'ARCHIVED'
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: GetSubModuleLessons :many
|
||||
SELECT
|
||||
smp.id,
|
||||
smp.sub_module_id,
|
||||
smp.question_set_id,
|
||||
smp.intro_video_url,
|
||||
smp.display_order,
|
||||
smp.is_active,
|
||||
qs.title,
|
||||
qs.description,
|
||||
qs.status,
|
||||
qs.set_type,
|
||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
||||
FROM sub_module_lessons smp
|
||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
||||
WHERE smp.sub_module_id = $1
|
||||
AND smp.is_active = TRUE
|
||||
ORDER BY smp.display_order ASC, smp.id ASC;
|
||||
|
||||
-- name: GetSubModulePractices :many
|
||||
SELECT
|
||||
smp.id,
|
||||
smp.sub_module_id,
|
||||
smp.title,
|
||||
smp.description,
|
||||
smp.thumbnail,
|
||||
smp.intro_video_url,
|
||||
smp.question_set_id,
|
||||
smp.display_order,
|
||||
smp.is_active,
|
||||
qs.status,
|
||||
qs.set_type,
|
||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
||||
FROM sub_module_practices smp
|
||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
||||
WHERE smp.sub_module_id = $1
|
||||
AND smp.is_active = TRUE
|
||||
ORDER BY smp.display_order ASC, smp.id ASC;
|
||||
|
||||
-- name: GetFullHierarchyByCourseID :many
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
c.title AS course_title,
|
||||
l.id AS level_id,
|
||||
l.cefr_level,
|
||||
m.id AS module_id,
|
||||
m.title AS module_title,
|
||||
sm.id AS sub_module_id,
|
||||
sm.title AS sub_module_title
|
||||
FROM courses c
|
||||
LEFT JOIN levels l ON l.course_id = c.id AND l.is_active = TRUE
|
||||
LEFT JOIN modules m ON m.level_id = l.id AND m.is_active = TRUE
|
||||
LEFT JOIN sub_modules sm ON sm.module_id = m.id AND sm.is_active = TRUE
|
||||
WHERE c.id = $1
|
||||
ORDER BY l.display_order, l.id, m.display_order, m.id, sm.display_order, sm.id;
|
||||
|
||||
-- name: CreateCourseSubCategory :one
|
||||
INSERT INTO course_sub_categories (
|
||||
category_id,
|
||||
name,
|
||||
description,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateLevel :one
|
||||
INSERT INTO levels (
|
||||
course_id,
|
||||
cefr_level,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, COALESCE($3, 0), COALESCE($4, TRUE))
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateModule :one
|
||||
INSERT INTO modules (
|
||||
level_id,
|
||||
title,
|
||||
description,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateSubModule :one
|
||||
INSERT INTO sub_modules (
|
||||
module_id,
|
||||
title,
|
||||
description,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateSubModuleVideo :one
|
||||
INSERT INTO sub_module_videos (
|
||||
sub_module_id,
|
||||
title,
|
||||
description,
|
||||
video_url,
|
||||
duration,
|
||||
resolution,
|
||||
is_published,
|
||||
publish_date,
|
||||
visibility,
|
||||
instructor_id,
|
||||
thumbnail,
|
||||
display_order,
|
||||
status,
|
||||
vimeo_id,
|
||||
vimeo_embed_url,
|
||||
vimeo_player_html,
|
||||
vimeo_status,
|
||||
video_host_provider
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
COALESCE($7, FALSE), $8, $9, $10, $11,
|
||||
COALESCE($12, 0), COALESCE($13, 'DRAFT'),
|
||||
$14, $15, $16, $17, COALESCE($18, 'DIRECT')
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: AttachQuestionSetLessonToSubModule :one
|
||||
INSERT INTO sub_module_lessons (
|
||||
sub_module_id,
|
||||
question_set_id,
|
||||
intro_video_url,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateSubModulePractice :one
|
||||
INSERT INTO sub_module_practices (
|
||||
sub_module_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
intro_video_url,
|
||||
question_set_id,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, 0), COALESCE($8, TRUE))
|
||||
RETURNING *;
|
||||
|
||||
59
db/query/learning_tree.sql
Normal file
59
db/query/learning_tree.sql
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
-- name: GetFullLearningTree :many
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
c.title AS course_title,
|
||||
sc.id AS sub_course_id,
|
||||
sc.title AS sub_course_title,
|
||||
sc.level AS sub_course_level,
|
||||
sc.sub_level AS sub_course_sub_level
|
||||
FROM courses c
|
||||
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
|
||||
WHERE c.is_active = true
|
||||
ORDER BY c.id, sc.display_order, sc.id;
|
||||
|
||||
-- name: GetCourseLearningPath :many
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
c.title AS course_title,
|
||||
c.description AS course_description,
|
||||
c.thumbnail AS course_thumbnail,
|
||||
c.intro_video_url AS course_intro_video_url,
|
||||
cc.id AS category_id,
|
||||
cc.name AS category_name,
|
||||
sc.id AS sub_course_id,
|
||||
sc.title AS sub_course_title,
|
||||
sc.description AS sub_course_description,
|
||||
sc.thumbnail AS sub_course_thumbnail,
|
||||
sc.display_order AS sub_course_display_order,
|
||||
sc.level AS sub_course_level,
|
||||
sc.sub_level AS sub_course_sub_level,
|
||||
(SELECT COUNT(*) FROM sub_course_prerequisites WHERE sub_course_id = sc.id) AS prerequisite_count,
|
||||
(SELECT COUNT(*) FROM sub_course_videos WHERE sub_course_id = sc.id AND status = 'PUBLISHED') AS video_count,
|
||||
(SELECT COUNT(*) FROM question_sets WHERE set_type = 'PRACTICE' AND owner_type = 'SUB_COURSE' AND owner_id = sc.id AND status = 'PUBLISHED') AS practice_count
|
||||
FROM courses c
|
||||
JOIN course_categories cc ON cc.id = c.category_id
|
||||
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
|
||||
WHERE c.id = $1
|
||||
ORDER BY sc.display_order, sc.id;
|
||||
|
||||
-- name: GetSubCourseVideosForLearningPath :many
|
||||
SELECT id, title, description, video_url, duration, resolution, display_order,
|
||||
vimeo_id, vimeo_embed_url, video_host_provider
|
||||
FROM sub_course_videos
|
||||
WHERE sub_course_id = $1 AND status = 'PUBLISHED'
|
||||
ORDER BY display_order, id;
|
||||
|
||||
-- name: GetSubCoursePracticesForLearningPath :many
|
||||
SELECT id, title, description, persona, status, intro_video_url,
|
||||
(SELECT COUNT(*) FROM question_set_items WHERE set_id = qs.id) AS question_count
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE' AND qs.owner_id = $1
|
||||
AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED'
|
||||
ORDER BY qs.display_order ASC, qs.created_at;
|
||||
|
||||
-- name: GetSubCoursePrerequisitesForLearningPath :many
|
||||
SELECT p.prerequisite_sub_course_id, sc.title, sc.level, sc.sub_level
|
||||
FROM sub_course_prerequisites p
|
||||
JOIN sub_courses sc ON sc.id = p.prerequisite_sub_course_id
|
||||
WHERE p.sub_course_id = $1
|
||||
ORDER BY sc.display_order;
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
-- name: GetFirstIncompletePreviousPractice :one
|
||||
WITH target AS (
|
||||
SELECT id, owner_type, owner_id, COALESCE(display_order, 0) AS display_order
|
||||
FROM question_sets
|
||||
WHERE id = @question_set_id::BIGINT
|
||||
AND set_type = 'PRACTICE'
|
||||
AND status = 'PUBLISHED'
|
||||
),
|
||||
candidates AS (
|
||||
SELECT qs.id, qs.title, COALESCE(qs.display_order, 0) AS display_order
|
||||
FROM question_sets qs
|
||||
JOIN target t
|
||||
ON qs.owner_type = t.owner_type
|
||||
AND qs.owner_id = t.owner_id
|
||||
WHERE qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND COALESCE(qs.display_order, 0) < t.display_order
|
||||
)
|
||||
SELECT c.id, c.title, c.display_order
|
||||
FROM candidates c
|
||||
LEFT JOIN user_practice_progress upp
|
||||
ON upp.question_set_id = c.id
|
||||
AND upp.user_id = @user_id::BIGINT
|
||||
AND upp.completed_at IS NOT NULL
|
||||
WHERE upp.id IS NULL
|
||||
ORDER BY c.display_order ASC, c.id ASC
|
||||
LIMIT 1;
|
||||
|
||||
-- name: MarkPracticeCompleted :execrows
|
||||
INSERT INTO user_practice_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
question_set_id,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
@user_id::BIGINT,
|
||||
CASE
|
||||
WHEN qs.owner_type = 'SUB_COURSE' THEN qs.owner_id
|
||||
WHEN qs.owner_type = 'SUB_MODULE' THEN sm.legacy_sub_course_id
|
||||
ELSE NULL
|
||||
END,
|
||||
qs.id,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM question_sets qs
|
||||
LEFT JOIN sub_modules sm
|
||||
ON qs.owner_type = 'SUB_MODULE'
|
||||
AND qs.owner_id = sm.id
|
||||
WHERE qs.id = @question_set_id::BIGINT
|
||||
ON CONFLICT (user_id, question_set_id) DO UPDATE
|
||||
SET completed_at = EXCLUDED.completed_at,
|
||||
updated_at = EXCLUDED.updated_at;
|
||||
|
||||
50
db/query/sub_course_prerequisites.sql
Normal file
50
db/query/sub_course_prerequisites.sql
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
-- name: AddSubCoursePrerequisite :one
|
||||
INSERT INTO sub_course_prerequisites (sub_course_id, prerequisite_sub_course_id)
|
||||
VALUES ($1, $2)
|
||||
RETURNING *;
|
||||
|
||||
-- name: RemoveSubCoursePrerequisite :exec
|
||||
DELETE FROM sub_course_prerequisites
|
||||
WHERE sub_course_id = $1 AND prerequisite_sub_course_id = $2;
|
||||
|
||||
-- name: GetSubCoursePrerequisites :many
|
||||
SELECT
|
||||
p.id,
|
||||
p.sub_course_id,
|
||||
p.prerequisite_sub_course_id,
|
||||
p.created_at,
|
||||
sc.title AS prerequisite_title,
|
||||
sc.level AS prerequisite_level,
|
||||
sc.display_order AS prerequisite_display_order
|
||||
FROM sub_course_prerequisites p
|
||||
JOIN sub_courses sc ON sc.id = p.prerequisite_sub_course_id
|
||||
WHERE p.sub_course_id = $1
|
||||
ORDER BY sc.display_order;
|
||||
|
||||
-- name: GetSubCourseDependents :many
|
||||
SELECT
|
||||
p.id,
|
||||
p.sub_course_id,
|
||||
p.prerequisite_sub_course_id,
|
||||
p.created_at,
|
||||
sc.title AS dependent_title,
|
||||
sc.level AS dependent_level
|
||||
FROM sub_course_prerequisites p
|
||||
JOIN sub_courses sc ON sc.id = p.sub_course_id
|
||||
WHERE p.prerequisite_sub_course_id = $1
|
||||
ORDER BY sc.display_order;
|
||||
|
||||
-- name: CountUnmetPrerequisites :one
|
||||
SELECT COUNT(*)::bigint AS unmet_count
|
||||
FROM sub_course_prerequisites p
|
||||
WHERE p.sub_course_id = $1
|
||||
AND p.prerequisite_sub_course_id NOT IN (
|
||||
SELECT usp.sub_course_id
|
||||
FROM user_sub_course_progress usp
|
||||
WHERE usp.user_id = $2
|
||||
AND usp.status = 'COMPLETED'
|
||||
);
|
||||
|
||||
-- name: DeleteAllPrerequisitesForSubCourse :exec
|
||||
DELETE FROM sub_course_prerequisites
|
||||
WHERE sub_course_id = $1;
|
||||
122
db/query/sub_course_videos.sql
Normal file
122
db/query/sub_course_videos.sql
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
-- name: CreateSubCourseVideo :one
|
||||
INSERT INTO sub_course_videos (
|
||||
sub_course_id,
|
||||
title,
|
||||
description,
|
||||
video_url,
|
||||
duration,
|
||||
resolution,
|
||||
instructor_id,
|
||||
thumbnail,
|
||||
visibility,
|
||||
display_order,
|
||||
status,
|
||||
vimeo_id,
|
||||
vimeo_embed_url,
|
||||
vimeo_player_html,
|
||||
vimeo_status,
|
||||
video_host_provider
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9,
|
||||
COALESCE($10, 0),
|
||||
COALESCE($11, 'DRAFT'),
|
||||
$12, $13, $14,
|
||||
COALESCE($15, 'pending'),
|
||||
COALESCE($16, 'DIRECT')
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetSubCourseVideoByID :one
|
||||
SELECT *
|
||||
FROM sub_course_videos
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetVideosBySubCourse :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
id,
|
||||
sub_course_id,
|
||||
title,
|
||||
description,
|
||||
video_url,
|
||||
duration,
|
||||
resolution,
|
||||
is_published,
|
||||
publish_date,
|
||||
visibility,
|
||||
instructor_id,
|
||||
thumbnail,
|
||||
display_order,
|
||||
status,
|
||||
vimeo_id,
|
||||
vimeo_embed_url,
|
||||
vimeo_player_html,
|
||||
vimeo_status,
|
||||
video_host_provider
|
||||
FROM sub_course_videos
|
||||
WHERE sub_course_id = $1
|
||||
AND status != 'ARCHIVED'
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: GetPublishedVideosBySubCourse :many
|
||||
SELECT *
|
||||
FROM sub_course_videos
|
||||
WHERE sub_course_id = $1
|
||||
AND status = 'PUBLISHED'
|
||||
ORDER BY display_order ASC, publish_date ASC;
|
||||
|
||||
-- name: PublishSubCourseVideo :exec
|
||||
UPDATE sub_course_videos
|
||||
SET
|
||||
is_published = true,
|
||||
publish_date = CURRENT_TIMESTAMP,
|
||||
status = 'PUBLISHED'
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: UpdateSubCourseVideo :exec
|
||||
UPDATE sub_course_videos
|
||||
SET
|
||||
title = COALESCE($1, title),
|
||||
description = COALESCE($2, description),
|
||||
video_url = COALESCE($3, video_url),
|
||||
duration = COALESCE($4, duration),
|
||||
resolution = COALESCE($5, resolution),
|
||||
visibility = COALESCE($6, visibility),
|
||||
thumbnail = COALESCE($7, thumbnail),
|
||||
display_order = COALESCE($8, display_order),
|
||||
status = COALESCE($9, status),
|
||||
vimeo_id = COALESCE($10, vimeo_id),
|
||||
vimeo_embed_url = COALESCE($11, vimeo_embed_url),
|
||||
vimeo_player_html = COALESCE($12, vimeo_player_html),
|
||||
vimeo_status = COALESCE($13, vimeo_status),
|
||||
video_host_provider = COALESCE($14, video_host_provider)
|
||||
WHERE id = $15;
|
||||
|
||||
-- name: UpdateVimeoStatus :exec
|
||||
UPDATE sub_course_videos
|
||||
SET
|
||||
vimeo_status = $1
|
||||
WHERE id = $2;
|
||||
|
||||
-- name: GetVideosByVimeoID :one
|
||||
SELECT * FROM sub_course_videos
|
||||
WHERE vimeo_id = $1;
|
||||
|
||||
-- name: ArchiveSubCourseVideo :exec
|
||||
UPDATE sub_course_videos
|
||||
SET status = 'ARCHIVED'
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: DeleteSubCourseVideo :exec
|
||||
DELETE FROM sub_course_videos
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ReorderSubCourseVideos :exec
|
||||
UPDATE sub_course_videos
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE sub_course_videos.id = bulk.id;
|
||||
95
db/query/sub_courses.sql
Normal file
95
db/query/sub_courses.sql
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
-- name: CreateSubCourse :one
|
||||
INSERT INTO sub_courses (
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, COALESCE($5, 0), $6, $7, COALESCE($8, true))
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetSubCourseByID :one
|
||||
SELECT *
|
||||
FROM sub_courses
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetSubCoursesByCourse :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
id,
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
FROM sub_courses
|
||||
WHERE course_id = $1
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: ListSubCoursesByCourse :many
|
||||
SELECT
|
||||
id,
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
FROM sub_courses
|
||||
WHERE course_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: ListActiveSubCourses :many
|
||||
SELECT
|
||||
id,
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
FROM sub_courses
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY display_order ASC;
|
||||
|
||||
-- name: UpdateSubCourse :exec
|
||||
UPDATE sub_courses
|
||||
SET
|
||||
title = COALESCE($1, title),
|
||||
description = COALESCE($2, description),
|
||||
thumbnail = COALESCE($3, thumbnail),
|
||||
display_order = COALESCE($4, display_order),
|
||||
level = COALESCE($5, level),
|
||||
sub_level = COALESCE($6, sub_level),
|
||||
is_active = COALESCE($7, is_active)
|
||||
WHERE id = $8;
|
||||
|
||||
-- name: DeleteSubCourse :one
|
||||
DELETE FROM sub_courses
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeactivateSubCourse :exec
|
||||
UPDATE sub_courses
|
||||
SET is_active = FALSE
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ReorderSubCourses :exec
|
||||
UPDATE sub_courses
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE sub_courses.id = bulk.id;
|
||||
51
db/query/user_practice_progress.sql
Normal file
51
db/query/user_practice_progress.sql
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
-- name: MarkPracticeCompleted :one
|
||||
INSERT INTO user_practice_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
question_set_id,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
@user_id::BIGINT,
|
||||
qs.owner_id::BIGINT,
|
||||
qs.id,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM question_sets qs
|
||||
WHERE qs.id = @question_set_id::BIGINT
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, question_set_id)
|
||||
DO UPDATE SET
|
||||
completed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetFirstIncompletePreviousPractice :one
|
||||
SELECT
|
||||
p.id,
|
||||
p.title,
|
||||
p.display_order
|
||||
FROM question_sets target
|
||||
JOIN question_sets p
|
||||
ON p.owner_type = 'SUB_COURSE'
|
||||
AND p.owner_id = target.owner_id
|
||||
AND p.set_type = 'PRACTICE'
|
||||
AND p.status = 'PUBLISHED'
|
||||
AND (
|
||||
p.display_order < target.display_order OR
|
||||
(p.display_order = target.display_order AND p.id < target.id)
|
||||
)
|
||||
LEFT JOIN user_practice_progress upp
|
||||
ON upp.question_set_id = p.id
|
||||
AND upp.user_id = @user_id::BIGINT
|
||||
AND upp.completed_at IS NOT NULL
|
||||
WHERE target.id = @question_set_id::BIGINT
|
||||
AND target.set_type = 'PRACTICE'
|
||||
AND target.owner_type = 'SUB_COURSE'
|
||||
AND target.status = 'PUBLISHED'
|
||||
AND upp.question_set_id IS NULL
|
||||
ORDER BY p.display_order ASC, p.id ASC
|
||||
LIMIT 1;
|
||||
78
db/query/user_sub_course_progress.sql
Normal file
78
db/query/user_sub_course_progress.sql
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
-- name: StartSubCourseProgress :one
|
||||
INSERT INTO user_sub_course_progress (user_id, sub_course_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_id, sub_course_id) DO NOTHING
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateSubCourseProgress :exec
|
||||
UPDATE user_sub_course_progress
|
||||
SET
|
||||
progress_percentage = $1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = $2 AND sub_course_id = $3;
|
||||
|
||||
-- name: CompleteSubCourse :exec
|
||||
UPDATE user_sub_course_progress
|
||||
SET
|
||||
status = 'COMPLETED',
|
||||
progress_percentage = 100,
|
||||
completed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = $1 AND sub_course_id = $2;
|
||||
|
||||
-- name: GetUserSubCourseProgress :one
|
||||
SELECT * FROM user_sub_course_progress
|
||||
WHERE user_id = $1 AND sub_course_id = $2;
|
||||
|
||||
-- name: GetUserCourseProgress :many
|
||||
SELECT
|
||||
usp.id,
|
||||
usp.user_id,
|
||||
usp.sub_course_id,
|
||||
usp.status,
|
||||
usp.progress_percentage,
|
||||
usp.started_at,
|
||||
usp.completed_at,
|
||||
usp.created_at,
|
||||
usp.updated_at,
|
||||
sc.title AS sub_course_title,
|
||||
sc.level AS sub_course_level,
|
||||
sc.display_order AS sub_course_display_order
|
||||
FROM user_sub_course_progress usp
|
||||
JOIN sub_courses sc ON sc.id = usp.sub_course_id
|
||||
WHERE usp.user_id = $1 AND sc.course_id = $2
|
||||
ORDER BY sc.display_order;
|
||||
|
||||
-- name: GetSubCoursesWithProgressByCourse :many
|
||||
SELECT
|
||||
sc.id AS sub_course_id,
|
||||
sc.title,
|
||||
sc.description,
|
||||
sc.thumbnail,
|
||||
sc.display_order,
|
||||
sc.level,
|
||||
sc.is_active,
|
||||
COALESCE(usp.status, 'NOT_STARTED') AS progress_status,
|
||||
COALESCE(usp.progress_percentage, 0)::smallint AS progress_percentage,
|
||||
usp.started_at,
|
||||
usp.completed_at,
|
||||
(SELECT COUNT(*)::bigint
|
||||
FROM sub_course_prerequisites p
|
||||
WHERE p.sub_course_id = sc.id
|
||||
AND p.prerequisite_sub_course_id NOT IN (
|
||||
SELECT usp2.sub_course_id
|
||||
FROM user_sub_course_progress usp2
|
||||
WHERE usp2.user_id = $1
|
||||
AND usp2.status = 'COMPLETED'
|
||||
)
|
||||
) AS unmet_prerequisites_count
|
||||
FROM sub_courses sc
|
||||
LEFT JOIN user_sub_course_progress usp
|
||||
ON usp.sub_course_id = sc.id AND usp.user_id = $1
|
||||
WHERE sc.course_id = $2
|
||||
AND sc.is_active = true
|
||||
ORDER BY sc.display_order;
|
||||
|
||||
-- name: DeleteUserSubCourseProgress :exec
|
||||
DELETE FROM user_sub_course_progress
|
||||
WHERE user_id = $1 AND sub_course_id = $2;
|
||||
44
db/query/user_sub_course_video_progress.sql
Normal file
44
db/query/user_sub_course_video_progress.sql
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
-- name: MarkVideoCompleted :one
|
||||
INSERT INTO user_sub_course_video_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
video_id,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
@user_id::BIGINT,
|
||||
v.sub_course_id,
|
||||
v.id,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM sub_course_videos v
|
||||
WHERE v.id = @video_id::BIGINT
|
||||
AND v.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, video_id)
|
||||
DO UPDATE SET
|
||||
completed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetFirstIncompletePreviousVideo :one
|
||||
SELECT
|
||||
v.id,
|
||||
v.title,
|
||||
v.display_order
|
||||
FROM sub_course_videos target
|
||||
JOIN sub_course_videos v
|
||||
ON v.sub_course_id = target.sub_course_id
|
||||
AND v.status = 'PUBLISHED'
|
||||
AND (
|
||||
v.display_order < target.display_order OR
|
||||
(v.display_order = target.display_order AND v.id < target.id)
|
||||
)
|
||||
LEFT JOIN user_sub_course_video_progress p
|
||||
ON p.video_id = v.id
|
||||
AND p.user_id = @user_id::BIGINT
|
||||
AND p.completed_at IS NOT NULL
|
||||
WHERE target.id = @video_id::BIGINT
|
||||
AND p.video_id IS NULL
|
||||
ORDER BY v.display_order ASC, v.id ASC
|
||||
LIMIT 1;
|
||||
2848
docs/docs.go
2848
docs/docs.go
File diff suppressed because it is too large
Load Diff
2848
docs/swagger.json
2848
docs/swagger.json
File diff suppressed because it is too large
Load Diff
1860
docs/swagger.yaml
1860
docs/swagger.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -21,7 +21,7 @@ INSERT INTO courses (
|
|||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, COALESCE($6, true))
|
||||
RETURNING id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order, sub_category_id
|
||||
RETURNING id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order
|
||||
`
|
||||
|
||||
type CreateCourseParams struct {
|
||||
|
|
@ -52,7 +52,6 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
|
|||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
&i.SubCategoryID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -68,7 +67,7 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
|
|||
}
|
||||
|
||||
const GetCourseByID = `-- name: GetCourseByID :one
|
||||
SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order, sub_category_id
|
||||
SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order
|
||||
FROM courses
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -85,7 +84,6 @@ func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
|
|||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
&i.SubCategoryID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,766 +0,0 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: hierarchy.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const AttachQuestionSetLessonToSubModule = `-- name: AttachQuestionSetLessonToSubModule :one
|
||||
INSERT INTO sub_module_lessons (
|
||||
sub_module_id,
|
||||
question_set_id,
|
||||
intro_video_url,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING id, sub_module_id, question_set_id, intro_video_url, display_order, is_active, created_at
|
||||
`
|
||||
|
||||
type AttachQuestionSetLessonToSubModuleParams struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
Column4 interface{} `json:"column_4"`
|
||||
Column5 interface{} `json:"column_5"`
|
||||
}
|
||||
|
||||
func (q *Queries) AttachQuestionSetLessonToSubModule(ctx context.Context, arg AttachQuestionSetLessonToSubModuleParams) (SubModuleLesson, error) {
|
||||
row := q.db.QueryRow(ctx, AttachQuestionSetLessonToSubModule,
|
||||
arg.SubModuleID,
|
||||
arg.QuestionSetID,
|
||||
arg.IntroVideoUrl,
|
||||
arg.Column4,
|
||||
arg.Column5,
|
||||
)
|
||||
var i SubModuleLesson
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.QuestionSetID,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateCourseSubCategory = `-- name: CreateCourseSubCategory :one
|
||||
INSERT INTO course_sub_categories (
|
||||
category_id,
|
||||
name,
|
||||
description,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING id, category_id, name, description, is_active, display_order, created_at
|
||||
`
|
||||
|
||||
type CreateCourseSubCategoryParams struct {
|
||||
CategoryID int64 `json:"category_id"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Column4 interface{} `json:"column_4"`
|
||||
Column5 interface{} `json:"column_5"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateCourseSubCategory(ctx context.Context, arg CreateCourseSubCategoryParams) (CourseSubCategory, error) {
|
||||
row := q.db.QueryRow(ctx, CreateCourseSubCategory,
|
||||
arg.CategoryID,
|
||||
arg.Name,
|
||||
arg.Description,
|
||||
arg.Column4,
|
||||
arg.Column5,
|
||||
)
|
||||
var i CourseSubCategory
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CategoryID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.IsActive,
|
||||
&i.DisplayOrder,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateLevel = `-- name: CreateLevel :one
|
||||
INSERT INTO levels (
|
||||
course_id,
|
||||
cefr_level,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, COALESCE($3, 0), COALESCE($4, TRUE))
|
||||
RETURNING id, course_id, cefr_level, display_order, is_active, created_at
|
||||
`
|
||||
|
||||
type CreateLevelParams struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
CefrLevel string `json:"cefr_level"`
|
||||
Column3 interface{} `json:"column_3"`
|
||||
Column4 interface{} `json:"column_4"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateLevel(ctx context.Context, arg CreateLevelParams) (Level, error) {
|
||||
row := q.db.QueryRow(ctx, CreateLevel,
|
||||
arg.CourseID,
|
||||
arg.CefrLevel,
|
||||
arg.Column3,
|
||||
arg.Column4,
|
||||
)
|
||||
var i Level
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.CefrLevel,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateModule = `-- name: CreateModule :one
|
||||
INSERT INTO modules (
|
||||
level_id,
|
||||
title,
|
||||
description,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING id, level_id, title, description, display_order, is_active, created_at
|
||||
`
|
||||
|
||||
type CreateModuleParams struct {
|
||||
LevelID int64 `json:"level_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Column4 interface{} `json:"column_4"`
|
||||
Column5 interface{} `json:"column_5"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Module, error) {
|
||||
row := q.db.QueryRow(ctx, CreateModule,
|
||||
arg.LevelID,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.Column4,
|
||||
arg.Column5,
|
||||
)
|
||||
var i Module
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.LevelID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateSubModule = `-- name: CreateSubModule :one
|
||||
INSERT INTO sub_modules (
|
||||
module_id,
|
||||
title,
|
||||
description,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING id, module_id, title, description, display_order, is_active, created_at, legacy_sub_course_id
|
||||
`
|
||||
|
||||
type CreateSubModuleParams struct {
|
||||
ModuleID int64 `json:"module_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Column4 interface{} `json:"column_4"`
|
||||
Column5 interface{} `json:"column_5"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSubModule(ctx context.Context, arg CreateSubModuleParams) (SubModule, error) {
|
||||
row := q.db.QueryRow(ctx, CreateSubModule,
|
||||
arg.ModuleID,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.Column4,
|
||||
arg.Column5,
|
||||
)
|
||||
var i SubModule
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ModuleID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.LegacySubCourseID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateSubModulePractice = `-- name: CreateSubModulePractice :one
|
||||
INSERT INTO sub_module_practices (
|
||||
sub_module_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
intro_video_url,
|
||||
question_set_id,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, 0), COALESCE($8, TRUE))
|
||||
RETURNING id, sub_module_id, title, description, thumbnail, intro_video_url, question_set_id, display_order, is_active, created_at
|
||||
`
|
||||
|
||||
type CreateSubModulePracticeParams struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
Column7 interface{} `json:"column_7"`
|
||||
Column8 interface{} `json:"column_8"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSubModulePractice(ctx context.Context, arg CreateSubModulePracticeParams) (SubModulePractice, error) {
|
||||
row := q.db.QueryRow(ctx, CreateSubModulePractice,
|
||||
arg.SubModuleID,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.Thumbnail,
|
||||
arg.IntroVideoUrl,
|
||||
arg.QuestionSetID,
|
||||
arg.Column7,
|
||||
arg.Column8,
|
||||
)
|
||||
var i SubModulePractice
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.QuestionSetID,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateSubModuleVideo = `-- name: CreateSubModuleVideo :one
|
||||
INSERT INTO sub_module_videos (
|
||||
sub_module_id,
|
||||
title,
|
||||
description,
|
||||
video_url,
|
||||
duration,
|
||||
resolution,
|
||||
is_published,
|
||||
publish_date,
|
||||
visibility,
|
||||
instructor_id,
|
||||
thumbnail,
|
||||
display_order,
|
||||
status,
|
||||
vimeo_id,
|
||||
vimeo_embed_url,
|
||||
vimeo_player_html,
|
||||
vimeo_status,
|
||||
video_host_provider
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
COALESCE($7, FALSE), $8, $9, $10, $11,
|
||||
COALESCE($12, 0), COALESCE($13, 'DRAFT'),
|
||||
$14, $15, $16, $17, COALESCE($18, 'DIRECT')
|
||||
)
|
||||
RETURNING id, sub_module_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider, created_at
|
||||
`
|
||||
|
||||
type CreateSubModuleVideoParams struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
Duration pgtype.Int4 `json:"duration"`
|
||||
Resolution pgtype.Text `json:"resolution"`
|
||||
Column7 interface{} `json:"column_7"`
|
||||
PublishDate pgtype.Timestamptz `json:"publish_date"`
|
||||
Visibility pgtype.Text `json:"visibility"`
|
||||
InstructorID pgtype.Text `json:"instructor_id"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
Column12 interface{} `json:"column_12"`
|
||||
Column13 interface{} `json:"column_13"`
|
||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
||||
Column18 interface{} `json:"column_18"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSubModuleVideo(ctx context.Context, arg CreateSubModuleVideoParams) (SubModuleVideo, error) {
|
||||
row := q.db.QueryRow(ctx, CreateSubModuleVideo,
|
||||
arg.SubModuleID,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.VideoUrl,
|
||||
arg.Duration,
|
||||
arg.Resolution,
|
||||
arg.Column7,
|
||||
arg.PublishDate,
|
||||
arg.Visibility,
|
||||
arg.InstructorID,
|
||||
arg.Thumbnail,
|
||||
arg.Column12,
|
||||
arg.Column13,
|
||||
arg.VimeoID,
|
||||
arg.VimeoEmbedUrl,
|
||||
arg.VimeoPlayerHtml,
|
||||
arg.VimeoStatus,
|
||||
arg.Column18,
|
||||
)
|
||||
var i SubModuleVideo
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.IsPublished,
|
||||
&i.PublishDate,
|
||||
&i.Visibility,
|
||||
&i.InstructorID,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Status,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VimeoPlayerHtml,
|
||||
&i.VimeoStatus,
|
||||
&i.VideoHostProvider,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetCoursesWithHierarchy = `-- name: GetCoursesWithHierarchy :many
|
||||
SELECT
|
||||
cc.id AS category_id,
|
||||
cc.name AS category_name,
|
||||
csc.id AS sub_category_id,
|
||||
csc.name AS sub_category_name,
|
||||
c.id AS course_id,
|
||||
c.title AS course_title
|
||||
FROM course_categories cc
|
||||
LEFT JOIN course_sub_categories csc ON csc.category_id = cc.id AND csc.is_active = TRUE
|
||||
LEFT JOIN courses c ON c.sub_category_id = csc.id AND c.is_active = TRUE
|
||||
WHERE cc.is_active = TRUE
|
||||
ORDER BY cc.id, csc.display_order, csc.id, c.id
|
||||
`
|
||||
|
||||
type GetCoursesWithHierarchyRow struct {
|
||||
CategoryID int64 `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
||||
SubCategoryName pgtype.Text `json:"sub_category_name"`
|
||||
CourseID pgtype.Int8 `json:"course_id"`
|
||||
CourseTitle pgtype.Text `json:"course_title"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetCoursesWithHierarchy(ctx context.Context) ([]GetCoursesWithHierarchyRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetCoursesWithHierarchy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetCoursesWithHierarchyRow
|
||||
for rows.Next() {
|
||||
var i GetCoursesWithHierarchyRow
|
||||
if err := rows.Scan(
|
||||
&i.CategoryID,
|
||||
&i.CategoryName,
|
||||
&i.SubCategoryID,
|
||||
&i.SubCategoryName,
|
||||
&i.CourseID,
|
||||
&i.CourseTitle,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetFullHierarchyByCourseID = `-- name: GetFullHierarchyByCourseID :many
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
c.title AS course_title,
|
||||
l.id AS level_id,
|
||||
l.cefr_level,
|
||||
m.id AS module_id,
|
||||
m.title AS module_title,
|
||||
sm.id AS sub_module_id,
|
||||
sm.title AS sub_module_title
|
||||
FROM courses c
|
||||
LEFT JOIN levels l ON l.course_id = c.id AND l.is_active = TRUE
|
||||
LEFT JOIN modules m ON m.level_id = l.id AND m.is_active = TRUE
|
||||
LEFT JOIN sub_modules sm ON sm.module_id = m.id AND sm.is_active = TRUE
|
||||
WHERE c.id = $1
|
||||
ORDER BY l.display_order, l.id, m.display_order, m.id, sm.display_order, sm.id
|
||||
`
|
||||
|
||||
type GetFullHierarchyByCourseIDRow struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
CourseTitle string `json:"course_title"`
|
||||
LevelID pgtype.Int8 `json:"level_id"`
|
||||
CefrLevel pgtype.Text `json:"cefr_level"`
|
||||
ModuleID pgtype.Int8 `json:"module_id"`
|
||||
ModuleTitle pgtype.Text `json:"module_title"`
|
||||
SubModuleID pgtype.Int8 `json:"sub_module_id"`
|
||||
SubModuleTitle pgtype.Text `json:"sub_module_title"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetFullHierarchyByCourseID(ctx context.Context, id int64) ([]GetFullHierarchyByCourseIDRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetFullHierarchyByCourseID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetFullHierarchyByCourseIDRow
|
||||
for rows.Next() {
|
||||
var i GetFullHierarchyByCourseIDRow
|
||||
if err := rows.Scan(
|
||||
&i.CourseID,
|
||||
&i.CourseTitle,
|
||||
&i.LevelID,
|
||||
&i.CefrLevel,
|
||||
&i.ModuleID,
|
||||
&i.ModuleTitle,
|
||||
&i.SubModuleID,
|
||||
&i.SubModuleTitle,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetLevelsByCourseID = `-- name: GetLevelsByCourseID :many
|
||||
SELECT id, course_id, cefr_level, display_order, is_active, created_at
|
||||
FROM levels
|
||||
WHERE course_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
func (q *Queries) GetLevelsByCourseID(ctx context.Context, courseID int64) ([]Level, error) {
|
||||
rows, err := q.db.Query(ctx, GetLevelsByCourseID, courseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Level
|
||||
for rows.Next() {
|
||||
var i Level
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.CefrLevel,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetModulesByLevelID = `-- name: GetModulesByLevelID :many
|
||||
SELECT id, level_id, title, description, display_order, is_active, created_at
|
||||
FROM modules
|
||||
WHERE level_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
func (q *Queries) GetModulesByLevelID(ctx context.Context, levelID int64) ([]Module, error) {
|
||||
rows, err := q.db.Query(ctx, GetModulesByLevelID, levelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Module
|
||||
for rows.Next() {
|
||||
var i Module
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.LevelID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubModuleLessons = `-- name: GetSubModuleLessons :many
|
||||
SELECT
|
||||
smp.id,
|
||||
smp.sub_module_id,
|
||||
smp.question_set_id,
|
||||
smp.intro_video_url,
|
||||
smp.display_order,
|
||||
smp.is_active,
|
||||
qs.title,
|
||||
qs.description,
|
||||
qs.status,
|
||||
qs.set_type,
|
||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
||||
FROM sub_module_lessons smp
|
||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
||||
WHERE smp.sub_module_id = $1
|
||||
AND smp.is_active = TRUE
|
||||
ORDER BY smp.display_order ASC, smp.id ASC
|
||||
`
|
||||
|
||||
type GetSubModuleLessonsRow struct {
|
||||
ID int64 `json:"id"`
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Status string `json:"status"`
|
||||
SetType string `json:"set_type"`
|
||||
QuestionCount int64 `json:"question_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubModuleLessons(ctx context.Context, subModuleID int64) ([]GetSubModuleLessonsRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubModuleLessons, subModuleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubModuleLessonsRow
|
||||
for rows.Next() {
|
||||
var i GetSubModuleLessonsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.QuestionSetID,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Status,
|
||||
&i.SetType,
|
||||
&i.QuestionCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubModulePractices = `-- name: GetSubModulePractices :many
|
||||
SELECT
|
||||
smp.id,
|
||||
smp.sub_module_id,
|
||||
smp.title,
|
||||
smp.description,
|
||||
smp.thumbnail,
|
||||
smp.intro_video_url,
|
||||
smp.question_set_id,
|
||||
smp.display_order,
|
||||
smp.is_active,
|
||||
qs.status,
|
||||
qs.set_type,
|
||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
||||
FROM sub_module_practices smp
|
||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
||||
WHERE smp.sub_module_id = $1
|
||||
AND smp.is_active = TRUE
|
||||
ORDER BY smp.display_order ASC, smp.id ASC
|
||||
`
|
||||
|
||||
type GetSubModulePracticesRow struct {
|
||||
ID int64 `json:"id"`
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Status string `json:"status"`
|
||||
SetType string `json:"set_type"`
|
||||
QuestionCount int64 `json:"question_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubModulePractices(ctx context.Context, subModuleID int64) ([]GetSubModulePracticesRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubModulePractices, subModuleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubModulePracticesRow
|
||||
for rows.Next() {
|
||||
var i GetSubModulePracticesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.QuestionSetID,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.Status,
|
||||
&i.SetType,
|
||||
&i.QuestionCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubModuleVideos = `-- name: GetSubModuleVideos :many
|
||||
SELECT id, sub_module_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider, created_at
|
||||
FROM sub_module_videos
|
||||
WHERE sub_module_id = $1
|
||||
AND status != 'ARCHIVED'
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
func (q *Queries) GetSubModuleVideos(ctx context.Context, subModuleID int64) ([]SubModuleVideo, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubModuleVideos, subModuleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []SubModuleVideo
|
||||
for rows.Next() {
|
||||
var i SubModuleVideo
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.IsPublished,
|
||||
&i.PublishDate,
|
||||
&i.Visibility,
|
||||
&i.InstructorID,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Status,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VimeoPlayerHtml,
|
||||
&i.VimeoStatus,
|
||||
&i.VideoHostProvider,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubModulesByModuleID = `-- name: GetSubModulesByModuleID :many
|
||||
SELECT id, module_id, title, description, display_order, is_active, created_at, legacy_sub_course_id
|
||||
FROM sub_modules
|
||||
WHERE module_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
func (q *Queries) GetSubModulesByModuleID(ctx context.Context, moduleID int64) ([]SubModule, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubModulesByModuleID, moduleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []SubModule
|
||||
for rows.Next() {
|
||||
var i SubModule
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ModuleID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.LegacySubCourseID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
285
gen/db/learning_tree.sql.go
Normal file
285
gen/db/learning_tree.sql.go
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: learning_tree.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const GetCourseLearningPath = `-- name: GetCourseLearningPath :many
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
c.title AS course_title,
|
||||
c.description AS course_description,
|
||||
c.thumbnail AS course_thumbnail,
|
||||
c.intro_video_url AS course_intro_video_url,
|
||||
cc.id AS category_id,
|
||||
cc.name AS category_name,
|
||||
sc.id AS sub_course_id,
|
||||
sc.title AS sub_course_title,
|
||||
sc.description AS sub_course_description,
|
||||
sc.thumbnail AS sub_course_thumbnail,
|
||||
sc.display_order AS sub_course_display_order,
|
||||
sc.level AS sub_course_level,
|
||||
sc.sub_level AS sub_course_sub_level,
|
||||
(SELECT COUNT(*) FROM sub_course_prerequisites WHERE sub_course_id = sc.id) AS prerequisite_count,
|
||||
(SELECT COUNT(*) FROM sub_course_videos WHERE sub_course_id = sc.id AND status = 'PUBLISHED') AS video_count,
|
||||
(SELECT COUNT(*) FROM question_sets WHERE set_type = 'PRACTICE' AND owner_type = 'SUB_COURSE' AND owner_id = sc.id AND status = 'PUBLISHED') AS practice_count
|
||||
FROM courses c
|
||||
JOIN course_categories cc ON cc.id = c.category_id
|
||||
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
|
||||
WHERE c.id = $1
|
||||
ORDER BY sc.display_order, sc.id
|
||||
`
|
||||
|
||||
type GetCourseLearningPathRow struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
CourseTitle string `json:"course_title"`
|
||||
CourseDescription pgtype.Text `json:"course_description"`
|
||||
CourseThumbnail pgtype.Text `json:"course_thumbnail"`
|
||||
CourseIntroVideoUrl pgtype.Text `json:"course_intro_video_url"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
SubCourseID pgtype.Int8 `json:"sub_course_id"`
|
||||
SubCourseTitle pgtype.Text `json:"sub_course_title"`
|
||||
SubCourseDescription pgtype.Text `json:"sub_course_description"`
|
||||
SubCourseThumbnail pgtype.Text `json:"sub_course_thumbnail"`
|
||||
SubCourseDisplayOrder pgtype.Int4 `json:"sub_course_display_order"`
|
||||
SubCourseLevel pgtype.Text `json:"sub_course_level"`
|
||||
SubCourseSubLevel pgtype.Text `json:"sub_course_sub_level"`
|
||||
PrerequisiteCount int64 `json:"prerequisite_count"`
|
||||
VideoCount int64 `json:"video_count"`
|
||||
PracticeCount int64 `json:"practice_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetCourseLearningPath(ctx context.Context, id int64) ([]GetCourseLearningPathRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetCourseLearningPath, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetCourseLearningPathRow
|
||||
for rows.Next() {
|
||||
var i GetCourseLearningPathRow
|
||||
if err := rows.Scan(
|
||||
&i.CourseID,
|
||||
&i.CourseTitle,
|
||||
&i.CourseDescription,
|
||||
&i.CourseThumbnail,
|
||||
&i.CourseIntroVideoUrl,
|
||||
&i.CategoryID,
|
||||
&i.CategoryName,
|
||||
&i.SubCourseID,
|
||||
&i.SubCourseTitle,
|
||||
&i.SubCourseDescription,
|
||||
&i.SubCourseThumbnail,
|
||||
&i.SubCourseDisplayOrder,
|
||||
&i.SubCourseLevel,
|
||||
&i.SubCourseSubLevel,
|
||||
&i.PrerequisiteCount,
|
||||
&i.VideoCount,
|
||||
&i.PracticeCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetFullLearningTree = `-- name: GetFullLearningTree :many
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
c.title AS course_title,
|
||||
sc.id AS sub_course_id,
|
||||
sc.title AS sub_course_title,
|
||||
sc.level AS sub_course_level,
|
||||
sc.sub_level AS sub_course_sub_level
|
||||
FROM courses c
|
||||
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
|
||||
WHERE c.is_active = true
|
||||
ORDER BY c.id, sc.display_order, sc.id
|
||||
`
|
||||
|
||||
type GetFullLearningTreeRow struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
CourseTitle string `json:"course_title"`
|
||||
SubCourseID pgtype.Int8 `json:"sub_course_id"`
|
||||
SubCourseTitle pgtype.Text `json:"sub_course_title"`
|
||||
SubCourseLevel pgtype.Text `json:"sub_course_level"`
|
||||
SubCourseSubLevel pgtype.Text `json:"sub_course_sub_level"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetFullLearningTree(ctx context.Context) ([]GetFullLearningTreeRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetFullLearningTree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetFullLearningTreeRow
|
||||
for rows.Next() {
|
||||
var i GetFullLearningTreeRow
|
||||
if err := rows.Scan(
|
||||
&i.CourseID,
|
||||
&i.CourseTitle,
|
||||
&i.SubCourseID,
|
||||
&i.SubCourseTitle,
|
||||
&i.SubCourseLevel,
|
||||
&i.SubCourseSubLevel,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubCoursePracticesForLearningPath = `-- name: GetSubCoursePracticesForLearningPath :many
|
||||
SELECT id, title, description, persona, status, intro_video_url,
|
||||
(SELECT COUNT(*) FROM question_set_items WHERE set_id = qs.id) AS question_count
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE' AND qs.owner_id = $1
|
||||
AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED'
|
||||
ORDER BY qs.display_order ASC, qs.created_at
|
||||
`
|
||||
|
||||
type GetSubCoursePracticesForLearningPathRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Persona pgtype.Text `json:"persona"`
|
||||
Status string `json:"status"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
QuestionCount int64 `json:"question_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCoursePracticesForLearningPath(ctx context.Context, ownerID pgtype.Int8) ([]GetSubCoursePracticesForLearningPathRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCoursePracticesForLearningPath, ownerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCoursePracticesForLearningPathRow
|
||||
for rows.Next() {
|
||||
var i GetSubCoursePracticesForLearningPathRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Persona,
|
||||
&i.Status,
|
||||
&i.IntroVideoUrl,
|
||||
&i.QuestionCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubCoursePrerequisitesForLearningPath = `-- name: GetSubCoursePrerequisitesForLearningPath :many
|
||||
SELECT p.prerequisite_sub_course_id, sc.title, sc.level, sc.sub_level
|
||||
FROM sub_course_prerequisites p
|
||||
JOIN sub_courses sc ON sc.id = p.prerequisite_sub_course_id
|
||||
WHERE p.sub_course_id = $1
|
||||
ORDER BY sc.display_order
|
||||
`
|
||||
|
||||
type GetSubCoursePrerequisitesForLearningPathRow struct {
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
Title string `json:"title"`
|
||||
Level string `json:"level"`
|
||||
SubLevel string `json:"sub_level"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCoursePrerequisitesForLearningPath(ctx context.Context, subCourseID int64) ([]GetSubCoursePrerequisitesForLearningPathRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCoursePrerequisitesForLearningPath, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCoursePrerequisitesForLearningPathRow
|
||||
for rows.Next() {
|
||||
var i GetSubCoursePrerequisitesForLearningPathRow
|
||||
if err := rows.Scan(
|
||||
&i.PrerequisiteSubCourseID,
|
||||
&i.Title,
|
||||
&i.Level,
|
||||
&i.SubLevel,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubCourseVideosForLearningPath = `-- name: GetSubCourseVideosForLearningPath :many
|
||||
SELECT id, title, description, video_url, duration, resolution, display_order,
|
||||
vimeo_id, vimeo_embed_url, video_host_provider
|
||||
FROM sub_course_videos
|
||||
WHERE sub_course_id = $1 AND status = 'PUBLISHED'
|
||||
ORDER BY display_order, id
|
||||
`
|
||||
|
||||
type GetSubCourseVideosForLearningPathRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
Duration int32 `json:"duration"`
|
||||
Resolution pgtype.Text `json:"resolution"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
||||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCourseVideosForLearningPath(ctx context.Context, subCourseID int64) ([]GetSubCourseVideosForLearningPathRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCourseVideosForLearningPath, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCourseVideosForLearningPathRow
|
||||
for rows.Next() {
|
||||
var i GetSubCourseVideosForLearningPathRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.DisplayOrder,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VideoHostProvider,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
|
@ -31,7 +31,6 @@ type Course struct {
|
|||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
||||
}
|
||||
|
||||
type CourseCategory struct {
|
||||
|
|
@ -42,16 +41,6 @@ type CourseCategory struct {
|
|||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
type CourseSubCategory struct {
|
||||
ID int64 `json:"id"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
IsActive bool `json:"is_active"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
|
|
@ -69,30 +58,11 @@ type GlobalSetting struct {
|
|||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Level struct {
|
||||
ID int64 `json:"id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
CefrLevel string `json:"cefr_level"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type LevelToSubCourse struct {
|
||||
LevelID int64 `json:"level_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
}
|
||||
|
||||
type Module struct {
|
||||
ID int64 `json:"id"`
|
||||
LevelID int64 `json:"level_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type ModuleToSubCourse struct {
|
||||
ModuleID int64 `json:"module_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
|
|
@ -344,63 +314,6 @@ type SubCourseVideo struct {
|
|||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
||||
}
|
||||
|
||||
type SubModule struct {
|
||||
ID int64 `json:"id"`
|
||||
ModuleID int64 `json:"module_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
LegacySubCourseID pgtype.Int8 `json:"legacy_sub_course_id"`
|
||||
}
|
||||
|
||||
type SubModuleLesson struct {
|
||||
ID int64 `json:"id"`
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type SubModulePractice struct {
|
||||
ID int64 `json:"id"`
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type SubModuleVideo struct {
|
||||
ID int64 `json:"id"`
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
Duration pgtype.Int4 `json:"duration"`
|
||||
Resolution pgtype.Text `json:"resolution"`
|
||||
IsPublished bool `json:"is_published"`
|
||||
PublishDate pgtype.Timestamptz `json:"publish_date"`
|
||||
Visibility pgtype.Text `json:"visibility"`
|
||||
InstructorID pgtype.Text `json:"instructor_id"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Status string `json:"status"`
|
||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
||||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type SubscriptionPlan struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
|
@ -491,7 +404,7 @@ type UserAudioResponse struct {
|
|||
type UserPracticeProgress struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
SubCourseID pgtype.Int8 `json:"sub_course_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
CompletedAt pgtype.Timestamp `json:"completed_at"`
|
||||
CreatedAt pgtype.Timestamp `json:"created_at"`
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: practice_progress.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const GetFirstIncompletePreviousPractice = `-- name: GetFirstIncompletePreviousPractice :one
|
||||
WITH target AS (
|
||||
SELECT id, owner_type, owner_id, COALESCE(display_order, 0) AS display_order
|
||||
FROM question_sets
|
||||
WHERE id = $2::BIGINT
|
||||
AND set_type = 'PRACTICE'
|
||||
AND status = 'PUBLISHED'
|
||||
),
|
||||
candidates AS (
|
||||
SELECT qs.id, qs.title, COALESCE(qs.display_order, 0) AS display_order
|
||||
FROM question_sets qs
|
||||
JOIN target t
|
||||
ON qs.owner_type = t.owner_type
|
||||
AND qs.owner_id = t.owner_id
|
||||
WHERE qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND COALESCE(qs.display_order, 0) < t.display_order
|
||||
)
|
||||
SELECT c.id, c.title, c.display_order
|
||||
FROM candidates c
|
||||
LEFT JOIN user_practice_progress upp
|
||||
ON upp.question_set_id = c.id
|
||||
AND upp.user_id = $1::BIGINT
|
||||
AND upp.completed_at IS NOT NULL
|
||||
WHERE upp.id IS NULL
|
||||
ORDER BY c.display_order ASC, c.id ASC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type GetFirstIncompletePreviousPracticeParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
}
|
||||
|
||||
type GetFirstIncompletePreviousPracticeRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetFirstIncompletePreviousPractice(ctx context.Context, arg GetFirstIncompletePreviousPracticeParams) (GetFirstIncompletePreviousPracticeRow, error) {
|
||||
row := q.db.QueryRow(ctx, GetFirstIncompletePreviousPractice, arg.UserID, arg.QuestionSetID)
|
||||
var i GetFirstIncompletePreviousPracticeRow
|
||||
err := row.Scan(&i.ID, &i.Title, &i.DisplayOrder)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const MarkPracticeCompleted = `-- name: MarkPracticeCompleted :execrows
|
||||
INSERT INTO user_practice_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
question_set_id,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
$1::BIGINT,
|
||||
CASE
|
||||
WHEN qs.owner_type = 'SUB_COURSE' THEN qs.owner_id
|
||||
WHEN qs.owner_type = 'SUB_MODULE' THEN sm.legacy_sub_course_id
|
||||
ELSE NULL
|
||||
END,
|
||||
qs.id,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM question_sets qs
|
||||
LEFT JOIN sub_modules sm
|
||||
ON qs.owner_type = 'SUB_MODULE'
|
||||
AND qs.owner_id = sm.id
|
||||
WHERE qs.id = $2::BIGINT
|
||||
ON CONFLICT (user_id, question_set_id) DO UPDATE
|
||||
SET completed_at = EXCLUDED.completed_at,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`
|
||||
|
||||
type MarkPracticeCompletedParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) MarkPracticeCompleted(ctx context.Context, arg MarkPracticeCompletedParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, MarkPracticeCompleted, arg.UserID, arg.QuestionSetID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
187
gen/db/sub_course_prerequisites.sql.go
Normal file
187
gen/db/sub_course_prerequisites.sql.go
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: sub_course_prerequisites.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const AddSubCoursePrerequisite = `-- name: AddSubCoursePrerequisite :one
|
||||
INSERT INTO sub_course_prerequisites (sub_course_id, prerequisite_sub_course_id)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id, sub_course_id, prerequisite_sub_course_id, created_at
|
||||
`
|
||||
|
||||
type AddSubCoursePrerequisiteParams struct {
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) AddSubCoursePrerequisite(ctx context.Context, arg AddSubCoursePrerequisiteParams) (SubCoursePrerequisite, error) {
|
||||
row := q.db.QueryRow(ctx, AddSubCoursePrerequisite, arg.SubCourseID, arg.PrerequisiteSubCourseID)
|
||||
var i SubCoursePrerequisite
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.PrerequisiteSubCourseID,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CountUnmetPrerequisites = `-- name: CountUnmetPrerequisites :one
|
||||
SELECT COUNT(*)::bigint AS unmet_count
|
||||
FROM sub_course_prerequisites p
|
||||
WHERE p.sub_course_id = $1
|
||||
AND p.prerequisite_sub_course_id NOT IN (
|
||||
SELECT usp.sub_course_id
|
||||
FROM user_sub_course_progress usp
|
||||
WHERE usp.user_id = $2
|
||||
AND usp.status = 'COMPLETED'
|
||||
)
|
||||
`
|
||||
|
||||
type CountUnmetPrerequisitesParams struct {
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountUnmetPrerequisites(ctx context.Context, arg CountUnmetPrerequisitesParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, CountUnmetPrerequisites, arg.SubCourseID, arg.UserID)
|
||||
var unmet_count int64
|
||||
err := row.Scan(&unmet_count)
|
||||
return unmet_count, err
|
||||
}
|
||||
|
||||
const DeleteAllPrerequisitesForSubCourse = `-- name: DeleteAllPrerequisitesForSubCourse :exec
|
||||
DELETE FROM sub_course_prerequisites
|
||||
WHERE sub_course_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteAllPrerequisitesForSubCourse(ctx context.Context, subCourseID int64) error {
|
||||
_, err := q.db.Exec(ctx, DeleteAllPrerequisitesForSubCourse, subCourseID)
|
||||
return err
|
||||
}
|
||||
|
||||
const GetSubCourseDependents = `-- name: GetSubCourseDependents :many
|
||||
SELECT
|
||||
p.id,
|
||||
p.sub_course_id,
|
||||
p.prerequisite_sub_course_id,
|
||||
p.created_at,
|
||||
sc.title AS dependent_title,
|
||||
sc.level AS dependent_level
|
||||
FROM sub_course_prerequisites p
|
||||
JOIN sub_courses sc ON sc.id = p.sub_course_id
|
||||
WHERE p.prerequisite_sub_course_id = $1
|
||||
ORDER BY sc.display_order
|
||||
`
|
||||
|
||||
type GetSubCourseDependentsRow struct {
|
||||
ID int64 `json:"id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
DependentTitle string `json:"dependent_title"`
|
||||
DependentLevel string `json:"dependent_level"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCourseDependents(ctx context.Context, prerequisiteSubCourseID int64) ([]GetSubCourseDependentsRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCourseDependents, prerequisiteSubCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCourseDependentsRow
|
||||
for rows.Next() {
|
||||
var i GetSubCourseDependentsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.PrerequisiteSubCourseID,
|
||||
&i.CreatedAt,
|
||||
&i.DependentTitle,
|
||||
&i.DependentLevel,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubCoursePrerequisites = `-- name: GetSubCoursePrerequisites :many
|
||||
SELECT
|
||||
p.id,
|
||||
p.sub_course_id,
|
||||
p.prerequisite_sub_course_id,
|
||||
p.created_at,
|
||||
sc.title AS prerequisite_title,
|
||||
sc.level AS prerequisite_level,
|
||||
sc.display_order AS prerequisite_display_order
|
||||
FROM sub_course_prerequisites p
|
||||
JOIN sub_courses sc ON sc.id = p.prerequisite_sub_course_id
|
||||
WHERE p.sub_course_id = $1
|
||||
ORDER BY sc.display_order
|
||||
`
|
||||
|
||||
type GetSubCoursePrerequisitesRow struct {
|
||||
ID int64 `json:"id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
PrerequisiteTitle string `json:"prerequisite_title"`
|
||||
PrerequisiteLevel string `json:"prerequisite_level"`
|
||||
PrerequisiteDisplayOrder int32 `json:"prerequisite_display_order"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCoursePrerequisites(ctx context.Context, subCourseID int64) ([]GetSubCoursePrerequisitesRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCoursePrerequisites, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCoursePrerequisitesRow
|
||||
for rows.Next() {
|
||||
var i GetSubCoursePrerequisitesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.PrerequisiteSubCourseID,
|
||||
&i.CreatedAt,
|
||||
&i.PrerequisiteTitle,
|
||||
&i.PrerequisiteLevel,
|
||||
&i.PrerequisiteDisplayOrder,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const RemoveSubCoursePrerequisite = `-- name: RemoveSubCoursePrerequisite :exec
|
||||
DELETE FROM sub_course_prerequisites
|
||||
WHERE sub_course_id = $1 AND prerequisite_sub_course_id = $2
|
||||
`
|
||||
|
||||
type RemoveSubCoursePrerequisiteParams struct {
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) RemoveSubCoursePrerequisite(ctx context.Context, arg RemoveSubCoursePrerequisiteParams) error {
|
||||
_, err := q.db.Exec(ctx, RemoveSubCoursePrerequisite, arg.SubCourseID, arg.PrerequisiteSubCourseID)
|
||||
return err
|
||||
}
|
||||
441
gen/db/sub_course_videos.sql.go
Normal file
441
gen/db/sub_course_videos.sql.go
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: sub_course_videos.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const ArchiveSubCourseVideo = `-- name: ArchiveSubCourseVideo :exec
|
||||
UPDATE sub_course_videos
|
||||
SET status = 'ARCHIVED'
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) ArchiveSubCourseVideo(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, ArchiveSubCourseVideo, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const CreateSubCourseVideo = `-- name: CreateSubCourseVideo :one
|
||||
INSERT INTO sub_course_videos (
|
||||
sub_course_id,
|
||||
title,
|
||||
description,
|
||||
video_url,
|
||||
duration,
|
||||
resolution,
|
||||
instructor_id,
|
||||
thumbnail,
|
||||
visibility,
|
||||
display_order,
|
||||
status,
|
||||
vimeo_id,
|
||||
vimeo_embed_url,
|
||||
vimeo_player_html,
|
||||
vimeo_status,
|
||||
video_host_provider
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9,
|
||||
COALESCE($10, 0),
|
||||
COALESCE($11, 'DRAFT'),
|
||||
$12, $13, $14,
|
||||
COALESCE($15, 'pending'),
|
||||
COALESCE($16, 'DIRECT')
|
||||
)
|
||||
RETURNING id, sub_course_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider
|
||||
`
|
||||
|
||||
type CreateSubCourseVideoParams struct {
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
Duration int32 `json:"duration"`
|
||||
Resolution pgtype.Text `json:"resolution"`
|
||||
InstructorID pgtype.Text `json:"instructor_id"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
Visibility pgtype.Text `json:"visibility"`
|
||||
Column10 interface{} `json:"column_10"`
|
||||
Column11 interface{} `json:"column_11"`
|
||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
||||
Column15 interface{} `json:"column_15"`
|
||||
Column16 interface{} `json:"column_16"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSubCourseVideo(ctx context.Context, arg CreateSubCourseVideoParams) (SubCourseVideo, error) {
|
||||
row := q.db.QueryRow(ctx, CreateSubCourseVideo,
|
||||
arg.SubCourseID,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.VideoUrl,
|
||||
arg.Duration,
|
||||
arg.Resolution,
|
||||
arg.InstructorID,
|
||||
arg.Thumbnail,
|
||||
arg.Visibility,
|
||||
arg.Column10,
|
||||
arg.Column11,
|
||||
arg.VimeoID,
|
||||
arg.VimeoEmbedUrl,
|
||||
arg.VimeoPlayerHtml,
|
||||
arg.Column15,
|
||||
arg.Column16,
|
||||
)
|
||||
var i SubCourseVideo
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.IsPublished,
|
||||
&i.PublishDate,
|
||||
&i.Visibility,
|
||||
&i.InstructorID,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Status,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VimeoPlayerHtml,
|
||||
&i.VimeoStatus,
|
||||
&i.VideoHostProvider,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const DeleteSubCourseVideo = `-- name: DeleteSubCourseVideo :exec
|
||||
DELETE FROM sub_course_videos
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteSubCourseVideo(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, DeleteSubCourseVideo, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const GetPublishedVideosBySubCourse = `-- name: GetPublishedVideosBySubCourse :many
|
||||
SELECT id, sub_course_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider
|
||||
FROM sub_course_videos
|
||||
WHERE sub_course_id = $1
|
||||
AND status = 'PUBLISHED'
|
||||
ORDER BY display_order ASC, publish_date ASC
|
||||
`
|
||||
|
||||
func (q *Queries) GetPublishedVideosBySubCourse(ctx context.Context, subCourseID int64) ([]SubCourseVideo, error) {
|
||||
rows, err := q.db.Query(ctx, GetPublishedVideosBySubCourse, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []SubCourseVideo
|
||||
for rows.Next() {
|
||||
var i SubCourseVideo
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.IsPublished,
|
||||
&i.PublishDate,
|
||||
&i.Visibility,
|
||||
&i.InstructorID,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Status,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VimeoPlayerHtml,
|
||||
&i.VimeoStatus,
|
||||
&i.VideoHostProvider,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubCourseVideoByID = `-- name: GetSubCourseVideoByID :one
|
||||
SELECT id, sub_course_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider
|
||||
FROM sub_course_videos
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetSubCourseVideoByID(ctx context.Context, id int64) (SubCourseVideo, error) {
|
||||
row := q.db.QueryRow(ctx, GetSubCourseVideoByID, id)
|
||||
var i SubCourseVideo
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.IsPublished,
|
||||
&i.PublishDate,
|
||||
&i.Visibility,
|
||||
&i.InstructorID,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Status,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VimeoPlayerHtml,
|
||||
&i.VimeoStatus,
|
||||
&i.VideoHostProvider,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetVideosBySubCourse = `-- name: GetVideosBySubCourse :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
id,
|
||||
sub_course_id,
|
||||
title,
|
||||
description,
|
||||
video_url,
|
||||
duration,
|
||||
resolution,
|
||||
is_published,
|
||||
publish_date,
|
||||
visibility,
|
||||
instructor_id,
|
||||
thumbnail,
|
||||
display_order,
|
||||
status,
|
||||
vimeo_id,
|
||||
vimeo_embed_url,
|
||||
vimeo_player_html,
|
||||
vimeo_status,
|
||||
video_host_provider
|
||||
FROM sub_course_videos
|
||||
WHERE sub_course_id = $1
|
||||
AND status != 'ARCHIVED'
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
type GetVideosBySubCourseRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
Duration int32 `json:"duration"`
|
||||
Resolution pgtype.Text `json:"resolution"`
|
||||
IsPublished bool `json:"is_published"`
|
||||
PublishDate pgtype.Timestamptz `json:"publish_date"`
|
||||
Visibility pgtype.Text `json:"visibility"`
|
||||
InstructorID pgtype.Text `json:"instructor_id"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Status string `json:"status"`
|
||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
||||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetVideosBySubCourse(ctx context.Context, subCourseID int64) ([]GetVideosBySubCourseRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetVideosBySubCourse, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetVideosBySubCourseRow
|
||||
for rows.Next() {
|
||||
var i GetVideosBySubCourseRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.IsPublished,
|
||||
&i.PublishDate,
|
||||
&i.Visibility,
|
||||
&i.InstructorID,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Status,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VimeoPlayerHtml,
|
||||
&i.VimeoStatus,
|
||||
&i.VideoHostProvider,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetVideosByVimeoID = `-- name: GetVideosByVimeoID :one
|
||||
SELECT id, sub_course_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider FROM sub_course_videos
|
||||
WHERE vimeo_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetVideosByVimeoID(ctx context.Context, vimeoID pgtype.Text) (SubCourseVideo, error) {
|
||||
row := q.db.QueryRow(ctx, GetVideosByVimeoID, vimeoID)
|
||||
var i SubCourseVideo
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.IsPublished,
|
||||
&i.PublishDate,
|
||||
&i.Visibility,
|
||||
&i.InstructorID,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Status,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VimeoPlayerHtml,
|
||||
&i.VimeoStatus,
|
||||
&i.VideoHostProvider,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const PublishSubCourseVideo = `-- name: PublishSubCourseVideo :exec
|
||||
UPDATE sub_course_videos
|
||||
SET
|
||||
is_published = true,
|
||||
publish_date = CURRENT_TIMESTAMP,
|
||||
status = 'PUBLISHED'
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) PublishSubCourseVideo(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, PublishSubCourseVideo, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const ReorderSubCourseVideos = `-- name: ReorderSubCourseVideos :exec
|
||||
UPDATE sub_course_videos
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE sub_course_videos.id = bulk.id
|
||||
`
|
||||
|
||||
type ReorderSubCourseVideosParams struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Positions []int32 `json:"positions"`
|
||||
}
|
||||
|
||||
func (q *Queries) ReorderSubCourseVideos(ctx context.Context, arg ReorderSubCourseVideosParams) error {
|
||||
_, err := q.db.Exec(ctx, ReorderSubCourseVideos, arg.Ids, arg.Positions)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateSubCourseVideo = `-- name: UpdateSubCourseVideo :exec
|
||||
UPDATE sub_course_videos
|
||||
SET
|
||||
title = COALESCE($1, title),
|
||||
description = COALESCE($2, description),
|
||||
video_url = COALESCE($3, video_url),
|
||||
duration = COALESCE($4, duration),
|
||||
resolution = COALESCE($5, resolution),
|
||||
visibility = COALESCE($6, visibility),
|
||||
thumbnail = COALESCE($7, thumbnail),
|
||||
display_order = COALESCE($8, display_order),
|
||||
status = COALESCE($9, status),
|
||||
vimeo_id = COALESCE($10, vimeo_id),
|
||||
vimeo_embed_url = COALESCE($11, vimeo_embed_url),
|
||||
vimeo_player_html = COALESCE($12, vimeo_player_html),
|
||||
vimeo_status = COALESCE($13, vimeo_status),
|
||||
video_host_provider = COALESCE($14, video_host_provider)
|
||||
WHERE id = $15
|
||||
`
|
||||
|
||||
type UpdateSubCourseVideoParams struct {
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
Duration int32 `json:"duration"`
|
||||
Resolution pgtype.Text `json:"resolution"`
|
||||
Visibility pgtype.Text `json:"visibility"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Status string `json:"status"`
|
||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
||||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateSubCourseVideo(ctx context.Context, arg UpdateSubCourseVideoParams) error {
|
||||
_, err := q.db.Exec(ctx, UpdateSubCourseVideo,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.VideoUrl,
|
||||
arg.Duration,
|
||||
arg.Resolution,
|
||||
arg.Visibility,
|
||||
arg.Thumbnail,
|
||||
arg.DisplayOrder,
|
||||
arg.Status,
|
||||
arg.VimeoID,
|
||||
arg.VimeoEmbedUrl,
|
||||
arg.VimeoPlayerHtml,
|
||||
arg.VimeoStatus,
|
||||
arg.VideoHostProvider,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateVimeoStatus = `-- name: UpdateVimeoStatus :exec
|
||||
UPDATE sub_course_videos
|
||||
SET
|
||||
vimeo_status = $1
|
||||
WHERE id = $2
|
||||
`
|
||||
|
||||
type UpdateVimeoStatusParams struct {
|
||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateVimeoStatus(ctx context.Context, arg UpdateVimeoStatusParams) error {
|
||||
_, err := q.db.Exec(ctx, UpdateVimeoStatus, arg.VimeoStatus, arg.ID)
|
||||
return err
|
||||
}
|
||||
356
gen/db/sub_courses.sql.go
Normal file
356
gen/db/sub_courses.sql.go
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: sub_courses.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const CreateSubCourse = `-- name: CreateSubCourse :one
|
||||
INSERT INTO sub_courses (
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, COALESCE($5, 0), $6, $7, COALESCE($8, true))
|
||||
RETURNING id, course_id, title, description, thumbnail, display_order, level, is_active, sub_level
|
||||
`
|
||||
|
||||
type CreateSubCourseParams struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
Column5 interface{} `json:"column_5"`
|
||||
Level string `json:"level"`
|
||||
SubLevel string `json:"sub_level"`
|
||||
Column8 interface{} `json:"column_8"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSubCourse(ctx context.Context, arg CreateSubCourseParams) (SubCourse, error) {
|
||||
row := q.db.QueryRow(ctx, CreateSubCourse,
|
||||
arg.CourseID,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.Thumbnail,
|
||||
arg.Column5,
|
||||
arg.Level,
|
||||
arg.SubLevel,
|
||||
arg.Column8,
|
||||
)
|
||||
var i SubCourse
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Level,
|
||||
&i.IsActive,
|
||||
&i.SubLevel,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const DeactivateSubCourse = `-- name: DeactivateSubCourse :exec
|
||||
UPDATE sub_courses
|
||||
SET is_active = FALSE
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeactivateSubCourse(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, DeactivateSubCourse, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const DeleteSubCourse = `-- name: DeleteSubCourse :one
|
||||
DELETE FROM sub_courses
|
||||
WHERE id = $1
|
||||
RETURNING id, course_id, title, description, thumbnail, display_order, level, is_active, sub_level
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteSubCourse(ctx context.Context, id int64) (SubCourse, error) {
|
||||
row := q.db.QueryRow(ctx, DeleteSubCourse, id)
|
||||
var i SubCourse
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Level,
|
||||
&i.IsActive,
|
||||
&i.SubLevel,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetSubCourseByID = `-- name: GetSubCourseByID :one
|
||||
SELECT id, course_id, title, description, thumbnail, display_order, level, is_active, sub_level
|
||||
FROM sub_courses
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetSubCourseByID(ctx context.Context, id int64) (SubCourse, error) {
|
||||
row := q.db.QueryRow(ctx, GetSubCourseByID, id)
|
||||
var i SubCourse
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Level,
|
||||
&i.IsActive,
|
||||
&i.SubLevel,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetSubCoursesByCourse = `-- name: GetSubCoursesByCourse :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
id,
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
FROM sub_courses
|
||||
WHERE course_id = $1
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
type GetSubCoursesByCourseRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Level string `json:"level"`
|
||||
SubLevel string `json:"sub_level"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCoursesByCourse(ctx context.Context, courseID int64) ([]GetSubCoursesByCourseRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCoursesByCourse, courseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCoursesByCourseRow
|
||||
for rows.Next() {
|
||||
var i GetSubCoursesByCourseRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Level,
|
||||
&i.SubLevel,
|
||||
&i.IsActive,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const ListActiveSubCourses = `-- name: ListActiveSubCourses :many
|
||||
SELECT
|
||||
id,
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
FROM sub_courses
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY display_order ASC
|
||||
`
|
||||
|
||||
type ListActiveSubCoursesRow struct {
|
||||
ID int64 `json:"id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Level string `json:"level"`
|
||||
SubLevel string `json:"sub_level"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListActiveSubCourses(ctx context.Context) ([]ListActiveSubCoursesRow, error) {
|
||||
rows, err := q.db.Query(ctx, ListActiveSubCourses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListActiveSubCoursesRow
|
||||
for rows.Next() {
|
||||
var i ListActiveSubCoursesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Level,
|
||||
&i.SubLevel,
|
||||
&i.IsActive,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const ListSubCoursesByCourse = `-- name: ListSubCoursesByCourse :many
|
||||
SELECT
|
||||
id,
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
FROM sub_courses
|
||||
WHERE course_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
type ListSubCoursesByCourseRow struct {
|
||||
ID int64 `json:"id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Level string `json:"level"`
|
||||
SubLevel string `json:"sub_level"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListSubCoursesByCourse(ctx context.Context, courseID int64) ([]ListSubCoursesByCourseRow, error) {
|
||||
rows, err := q.db.Query(ctx, ListSubCoursesByCourse, courseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListSubCoursesByCourseRow
|
||||
for rows.Next() {
|
||||
var i ListSubCoursesByCourseRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Level,
|
||||
&i.SubLevel,
|
||||
&i.IsActive,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const ReorderSubCourses = `-- name: ReorderSubCourses :exec
|
||||
UPDATE sub_courses
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE sub_courses.id = bulk.id
|
||||
`
|
||||
|
||||
type ReorderSubCoursesParams struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Positions []int32 `json:"positions"`
|
||||
}
|
||||
|
||||
func (q *Queries) ReorderSubCourses(ctx context.Context, arg ReorderSubCoursesParams) error {
|
||||
_, err := q.db.Exec(ctx, ReorderSubCourses, arg.Ids, arg.Positions)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateSubCourse = `-- name: UpdateSubCourse :exec
|
||||
UPDATE sub_courses
|
||||
SET
|
||||
title = COALESCE($1, title),
|
||||
description = COALESCE($2, description),
|
||||
thumbnail = COALESCE($3, thumbnail),
|
||||
display_order = COALESCE($4, display_order),
|
||||
level = COALESCE($5, level),
|
||||
sub_level = COALESCE($6, sub_level),
|
||||
is_active = COALESCE($7, is_active)
|
||||
WHERE id = $8
|
||||
`
|
||||
|
||||
type UpdateSubCourseParams struct {
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Level string `json:"level"`
|
||||
SubLevel string `json:"sub_level"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateSubCourse(ctx context.Context, arg UpdateSubCourseParams) error {
|
||||
_, err := q.db.Exec(ctx, UpdateSubCourse,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.Thumbnail,
|
||||
arg.DisplayOrder,
|
||||
arg.Level,
|
||||
arg.SubLevel,
|
||||
arg.IsActive,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
102
gen/db/user_practice_progress.sql.go
Normal file
102
gen/db/user_practice_progress.sql.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: user_practice_progress.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const GetFirstIncompletePreviousPractice = `-- name: GetFirstIncompletePreviousPractice :one
|
||||
SELECT
|
||||
p.id,
|
||||
p.title,
|
||||
p.display_order
|
||||
FROM question_sets target
|
||||
JOIN question_sets p
|
||||
ON p.owner_type = 'SUB_COURSE'
|
||||
AND p.owner_id = target.owner_id
|
||||
AND p.set_type = 'PRACTICE'
|
||||
AND p.status = 'PUBLISHED'
|
||||
AND (
|
||||
p.display_order < target.display_order OR
|
||||
(p.display_order = target.display_order AND p.id < target.id)
|
||||
)
|
||||
LEFT JOIN user_practice_progress upp
|
||||
ON upp.question_set_id = p.id
|
||||
AND upp.user_id = $1::BIGINT
|
||||
AND upp.completed_at IS NOT NULL
|
||||
WHERE target.id = $2::BIGINT
|
||||
AND target.set_type = 'PRACTICE'
|
||||
AND target.owner_type = 'SUB_COURSE'
|
||||
AND target.status = 'PUBLISHED'
|
||||
AND upp.question_set_id IS NULL
|
||||
ORDER BY p.display_order ASC, p.id ASC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type GetFirstIncompletePreviousPracticeParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
}
|
||||
|
||||
type GetFirstIncompletePreviousPracticeRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetFirstIncompletePreviousPractice(ctx context.Context, arg GetFirstIncompletePreviousPracticeParams) (GetFirstIncompletePreviousPracticeRow, error) {
|
||||
row := q.db.QueryRow(ctx, GetFirstIncompletePreviousPractice, arg.UserID, arg.QuestionSetID)
|
||||
var i GetFirstIncompletePreviousPracticeRow
|
||||
err := row.Scan(&i.ID, &i.Title, &i.DisplayOrder)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const MarkPracticeCompleted = `-- name: MarkPracticeCompleted :one
|
||||
INSERT INTO user_practice_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
question_set_id,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
$1::BIGINT,
|
||||
qs.owner_id::BIGINT,
|
||||
qs.id,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM question_sets qs
|
||||
WHERE qs.id = $2::BIGINT
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, question_set_id)
|
||||
DO UPDATE SET
|
||||
completed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING id, user_id, sub_course_id, question_set_id, completed_at, created_at, updated_at
|
||||
`
|
||||
|
||||
type MarkPracticeCompletedParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) MarkPracticeCompleted(ctx context.Context, arg MarkPracticeCompletedParams) (UserPracticeProgress, error) {
|
||||
row := q.db.QueryRow(ctx, MarkPracticeCompleted, arg.UserID, arg.QuestionSetID)
|
||||
var i UserPracticeProgress
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.SubCourseID,
|
||||
&i.QuestionSetID,
|
||||
&i.CompletedAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
279
gen/db/user_sub_course_progress.sql.go
Normal file
279
gen/db/user_sub_course_progress.sql.go
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: user_sub_course_progress.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const CompleteSubCourse = `-- name: CompleteSubCourse :exec
|
||||
UPDATE user_sub_course_progress
|
||||
SET
|
||||
status = 'COMPLETED',
|
||||
progress_percentage = 100,
|
||||
completed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = $1 AND sub_course_id = $2
|
||||
`
|
||||
|
||||
type CompleteSubCourseParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CompleteSubCourse(ctx context.Context, arg CompleteSubCourseParams) error {
|
||||
_, err := q.db.Exec(ctx, CompleteSubCourse, arg.UserID, arg.SubCourseID)
|
||||
return err
|
||||
}
|
||||
|
||||
const DeleteUserSubCourseProgress = `-- name: DeleteUserSubCourseProgress :exec
|
||||
DELETE FROM user_sub_course_progress
|
||||
WHERE user_id = $1 AND sub_course_id = $2
|
||||
`
|
||||
|
||||
type DeleteUserSubCourseProgressParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteUserSubCourseProgress(ctx context.Context, arg DeleteUserSubCourseProgressParams) error {
|
||||
_, err := q.db.Exec(ctx, DeleteUserSubCourseProgress, arg.UserID, arg.SubCourseID)
|
||||
return err
|
||||
}
|
||||
|
||||
const GetSubCoursesWithProgressByCourse = `-- name: GetSubCoursesWithProgressByCourse :many
|
||||
SELECT
|
||||
sc.id AS sub_course_id,
|
||||
sc.title,
|
||||
sc.description,
|
||||
sc.thumbnail,
|
||||
sc.display_order,
|
||||
sc.level,
|
||||
sc.is_active,
|
||||
COALESCE(usp.status, 'NOT_STARTED') AS progress_status,
|
||||
COALESCE(usp.progress_percentage, 0)::smallint AS progress_percentage,
|
||||
usp.started_at,
|
||||
usp.completed_at,
|
||||
(SELECT COUNT(*)::bigint
|
||||
FROM sub_course_prerequisites p
|
||||
WHERE p.sub_course_id = sc.id
|
||||
AND p.prerequisite_sub_course_id NOT IN (
|
||||
SELECT usp2.sub_course_id
|
||||
FROM user_sub_course_progress usp2
|
||||
WHERE usp2.user_id = $1
|
||||
AND usp2.status = 'COMPLETED'
|
||||
)
|
||||
) AS unmet_prerequisites_count
|
||||
FROM sub_courses sc
|
||||
LEFT JOIN user_sub_course_progress usp
|
||||
ON usp.sub_course_id = sc.id AND usp.user_id = $1
|
||||
WHERE sc.course_id = $2
|
||||
AND sc.is_active = true
|
||||
ORDER BY sc.display_order
|
||||
`
|
||||
|
||||
type GetSubCoursesWithProgressByCourseParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
}
|
||||
|
||||
type GetSubCoursesWithProgressByCourseRow struct {
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Level string `json:"level"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ProgressStatus string `json:"progress_status"`
|
||||
ProgressPercentage int16 `json:"progress_percentage"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||
UnmetPrerequisitesCount int64 `json:"unmet_prerequisites_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCoursesWithProgressByCourse(ctx context.Context, arg GetSubCoursesWithProgressByCourseParams) ([]GetSubCoursesWithProgressByCourseRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCoursesWithProgressByCourse, arg.UserID, arg.CourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCoursesWithProgressByCourseRow
|
||||
for rows.Next() {
|
||||
var i GetSubCoursesWithProgressByCourseRow
|
||||
if err := rows.Scan(
|
||||
&i.SubCourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Level,
|
||||
&i.IsActive,
|
||||
&i.ProgressStatus,
|
||||
&i.ProgressPercentage,
|
||||
&i.StartedAt,
|
||||
&i.CompletedAt,
|
||||
&i.UnmetPrerequisitesCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetUserCourseProgress = `-- name: GetUserCourseProgress :many
|
||||
SELECT
|
||||
usp.id,
|
||||
usp.user_id,
|
||||
usp.sub_course_id,
|
||||
usp.status,
|
||||
usp.progress_percentage,
|
||||
usp.started_at,
|
||||
usp.completed_at,
|
||||
usp.created_at,
|
||||
usp.updated_at,
|
||||
sc.title AS sub_course_title,
|
||||
sc.level AS sub_course_level,
|
||||
sc.display_order AS sub_course_display_order
|
||||
FROM user_sub_course_progress usp
|
||||
JOIN sub_courses sc ON sc.id = usp.sub_course_id
|
||||
WHERE usp.user_id = $1 AND sc.course_id = $2
|
||||
ORDER BY sc.display_order
|
||||
`
|
||||
|
||||
type GetUserCourseProgressParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
}
|
||||
|
||||
type GetUserCourseProgressRow struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
Status string `json:"status"`
|
||||
ProgressPercentage int16 `json:"progress_percentage"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
SubCourseTitle string `json:"sub_course_title"`
|
||||
SubCourseLevel string `json:"sub_course_level"`
|
||||
SubCourseDisplayOrder int32 `json:"sub_course_display_order"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserCourseProgress(ctx context.Context, arg GetUserCourseProgressParams) ([]GetUserCourseProgressRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetUserCourseProgress, arg.UserID, arg.CourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetUserCourseProgressRow
|
||||
for rows.Next() {
|
||||
var i GetUserCourseProgressRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.SubCourseID,
|
||||
&i.Status,
|
||||
&i.ProgressPercentage,
|
||||
&i.StartedAt,
|
||||
&i.CompletedAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseTitle,
|
||||
&i.SubCourseLevel,
|
||||
&i.SubCourseDisplayOrder,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetUserSubCourseProgress = `-- name: GetUserSubCourseProgress :one
|
||||
SELECT id, user_id, sub_course_id, status, progress_percentage, started_at, completed_at, created_at, updated_at FROM user_sub_course_progress
|
||||
WHERE user_id = $1 AND sub_course_id = $2
|
||||
`
|
||||
|
||||
type GetUserSubCourseProgressParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserSubCourseProgress(ctx context.Context, arg GetUserSubCourseProgressParams) (UserSubCourseProgress, error) {
|
||||
row := q.db.QueryRow(ctx, GetUserSubCourseProgress, arg.UserID, arg.SubCourseID)
|
||||
var i UserSubCourseProgress
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.SubCourseID,
|
||||
&i.Status,
|
||||
&i.ProgressPercentage,
|
||||
&i.StartedAt,
|
||||
&i.CompletedAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const StartSubCourseProgress = `-- name: StartSubCourseProgress :one
|
||||
INSERT INTO user_sub_course_progress (user_id, sub_course_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_id, sub_course_id) DO NOTHING
|
||||
RETURNING id, user_id, sub_course_id, status, progress_percentage, started_at, completed_at, created_at, updated_at
|
||||
`
|
||||
|
||||
type StartSubCourseProgressParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) StartSubCourseProgress(ctx context.Context, arg StartSubCourseProgressParams) (UserSubCourseProgress, error) {
|
||||
row := q.db.QueryRow(ctx, StartSubCourseProgress, arg.UserID, arg.SubCourseID)
|
||||
var i UserSubCourseProgress
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.SubCourseID,
|
||||
&i.Status,
|
||||
&i.ProgressPercentage,
|
||||
&i.StartedAt,
|
||||
&i.CompletedAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const UpdateSubCourseProgress = `-- name: UpdateSubCourseProgress :exec
|
||||
UPDATE user_sub_course_progress
|
||||
SET
|
||||
progress_percentage = $1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = $2 AND sub_course_id = $3
|
||||
`
|
||||
|
||||
type UpdateSubCourseProgressParams struct {
|
||||
ProgressPercentage int16 `json:"progress_percentage"`
|
||||
UserID int64 `json:"user_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateSubCourseProgress(ctx context.Context, arg UpdateSubCourseProgressParams) error {
|
||||
_, err := q.db.Exec(ctx, UpdateSubCourseProgress, arg.ProgressPercentage, arg.UserID, arg.SubCourseID)
|
||||
return err
|
||||
}
|
||||
95
gen/db/user_sub_course_video_progress.sql.go
Normal file
95
gen/db/user_sub_course_video_progress.sql.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: user_sub_course_video_progress.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const GetFirstIncompletePreviousVideo = `-- name: GetFirstIncompletePreviousVideo :one
|
||||
SELECT
|
||||
v.id,
|
||||
v.title,
|
||||
v.display_order
|
||||
FROM sub_course_videos target
|
||||
JOIN sub_course_videos v
|
||||
ON v.sub_course_id = target.sub_course_id
|
||||
AND v.status = 'PUBLISHED'
|
||||
AND (
|
||||
v.display_order < target.display_order OR
|
||||
(v.display_order = target.display_order AND v.id < target.id)
|
||||
)
|
||||
LEFT JOIN user_sub_course_video_progress p
|
||||
ON p.video_id = v.id
|
||||
AND p.user_id = $1::BIGINT
|
||||
AND p.completed_at IS NOT NULL
|
||||
WHERE target.id = $2::BIGINT
|
||||
AND p.video_id IS NULL
|
||||
ORDER BY v.display_order ASC, v.id ASC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type GetFirstIncompletePreviousVideoParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
VideoID int64 `json:"video_id"`
|
||||
}
|
||||
|
||||
type GetFirstIncompletePreviousVideoRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetFirstIncompletePreviousVideo(ctx context.Context, arg GetFirstIncompletePreviousVideoParams) (GetFirstIncompletePreviousVideoRow, error) {
|
||||
row := q.db.QueryRow(ctx, GetFirstIncompletePreviousVideo, arg.UserID, arg.VideoID)
|
||||
var i GetFirstIncompletePreviousVideoRow
|
||||
err := row.Scan(&i.ID, &i.Title, &i.DisplayOrder)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const MarkVideoCompleted = `-- name: MarkVideoCompleted :one
|
||||
INSERT INTO user_sub_course_video_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
video_id,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
$1::BIGINT,
|
||||
v.sub_course_id,
|
||||
v.id,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM sub_course_videos v
|
||||
WHERE v.id = $2::BIGINT
|
||||
AND v.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, video_id)
|
||||
DO UPDATE SET
|
||||
completed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING id, user_id, sub_course_id, video_id, completed_at, created_at, updated_at
|
||||
`
|
||||
|
||||
type MarkVideoCompletedParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
VideoID int64 `json:"video_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) MarkVideoCompleted(ctx context.Context, arg MarkVideoCompletedParams) (UserSubCourseVideoProgress, error) {
|
||||
row := q.db.QueryRow(ctx, MarkVideoCompleted, arg.UserID, arg.VideoID)
|
||||
var i UserSubCourseVideoProgress
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.SubCourseID,
|
||||
&i.VideoID,
|
||||
&i.CompletedAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -3,13 +3,13 @@ package repository
|
|||
import (
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/ports"
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func NewCourseStore(s *Store) *Store { return s }
|
||||
func NewProgressionStore(s *Store) *Store { return s }
|
||||
func NewCourseStore(s *Store) ports.CourseStore { return s }
|
||||
|
||||
func (s *Store) CreateCourseCategory(
|
||||
ctx context.Context,
|
||||
|
|
|
|||
162
internal/repository/learning_tree.go
Normal file
162
internal/repository/learning_tree.go
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func (s *Store) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) {
|
||||
rows, err := s.queries.GetFullLearningTree(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
coursesMap := make(map[int64]*domain.TreeCourse)
|
||||
|
||||
for _, row := range rows {
|
||||
course, ok := coursesMap[row.CourseID]
|
||||
if !ok {
|
||||
course = &domain.TreeCourse{
|
||||
ID: row.CourseID,
|
||||
Title: row.CourseTitle,
|
||||
SubCourses: []domain.TreeSubCourse{},
|
||||
}
|
||||
coursesMap[row.CourseID] = course
|
||||
}
|
||||
|
||||
if row.SubCourseID.Valid {
|
||||
subCourse := domain.TreeSubCourse{
|
||||
ID: row.SubCourseID.Int64,
|
||||
Title: row.SubCourseTitle.String,
|
||||
Level: row.SubCourseLevel.String,
|
||||
SubLevel: row.SubCourseSubLevel.String,
|
||||
}
|
||||
course.SubCourses = append(course.SubCourses, subCourse)
|
||||
}
|
||||
}
|
||||
|
||||
courses := make([]domain.TreeCourse, 0, len(coursesMap))
|
||||
for _, course := range coursesMap {
|
||||
courses = append(courses, *course)
|
||||
}
|
||||
|
||||
return courses, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetCourseLearningPath(ctx context.Context, courseID int64) (domain.LearningPath, error) {
|
||||
rows, err := s.queries.GetCourseLearningPath(ctx, courseID)
|
||||
if err != nil {
|
||||
return domain.LearningPath{}, err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return domain.LearningPath{}, fmt.Errorf("course not found")
|
||||
}
|
||||
|
||||
first := rows[0]
|
||||
path := domain.LearningPath{
|
||||
CourseID: first.CourseID,
|
||||
CourseTitle: first.CourseTitle,
|
||||
Description: ptrString(first.CourseDescription),
|
||||
Thumbnail: ptrString(first.CourseThumbnail),
|
||||
IntroVideoURL: ptrString(first.CourseIntroVideoUrl),
|
||||
CategoryID: first.CategoryID,
|
||||
CategoryName: first.CategoryName,
|
||||
SubCourses: []domain.LearningPathSubCourse{},
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if !row.SubCourseID.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
scID := row.SubCourseID.Int64
|
||||
|
||||
// Fetch prerequisites, videos, practices for this sub-course
|
||||
prerequisites, _ := s.getSubCoursePrerequisitesForPath(ctx, scID)
|
||||
videos, _ := s.getSubCourseVideosForPath(ctx, scID)
|
||||
practices, _ := s.getSubCoursePracticesForPath(ctx, scID)
|
||||
|
||||
sc := domain.LearningPathSubCourse{
|
||||
ID: scID,
|
||||
Title: row.SubCourseTitle.String,
|
||||
Description: ptrString(row.SubCourseDescription),
|
||||
Thumbnail: ptrString(row.SubCourseThumbnail),
|
||||
DisplayOrder: row.SubCourseDisplayOrder.Int32,
|
||||
Level: row.SubCourseLevel.String,
|
||||
SubLevel: row.SubCourseSubLevel.String,
|
||||
PrerequisiteCount: row.PrerequisiteCount,
|
||||
VideoCount: row.VideoCount,
|
||||
PracticeCount: row.PracticeCount,
|
||||
Prerequisites: prerequisites,
|
||||
Videos: videos,
|
||||
Practices: practices,
|
||||
}
|
||||
path.SubCourses = append(path.SubCourses, sc)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (s *Store) getSubCoursePrerequisitesForPath(ctx context.Context, subCourseID int64) ([]domain.LearningPathPrerequisite, error) {
|
||||
rows, err := s.queries.GetSubCoursePrerequisitesForLearningPath(ctx, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]domain.LearningPathPrerequisite, len(rows))
|
||||
for i, row := range rows {
|
||||
result[i] = domain.LearningPathPrerequisite{
|
||||
SubCourseID: row.PrerequisiteSubCourseID,
|
||||
Title: row.Title,
|
||||
Level: row.Level,
|
||||
SubLevel: row.SubLevel,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) getSubCourseVideosForPath(ctx context.Context, subCourseID int64) ([]domain.LearningPathVideo, error) {
|
||||
rows, err := s.queries.GetSubCourseVideosForLearningPath(ctx, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]domain.LearningPathVideo, len(rows))
|
||||
for i, row := range rows {
|
||||
result[i] = domain.LearningPathVideo{
|
||||
ID: row.ID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
VideoURL: row.VideoUrl,
|
||||
Duration: row.Duration,
|
||||
Resolution: ptrString(row.Resolution),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
VimeoID: ptrString(row.VimeoID),
|
||||
VimeoEmbedURL: ptrString(row.VimeoEmbedUrl),
|
||||
VideoHostProvider: ptrString(row.VideoHostProvider),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) getSubCoursePracticesForPath(ctx context.Context, subCourseID int64) ([]domain.LearningPathPractice, error) {
|
||||
ownerID := pgtype.Int8{Int64: subCourseID, Valid: true}
|
||||
rows, err := s.queries.GetSubCoursePracticesForLearningPath(ctx, ownerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]domain.LearningPathPractice, len(rows))
|
||||
for i, row := range rows {
|
||||
result[i] = domain.LearningPathPractice{
|
||||
ID: row.ID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
Persona: ptrString(row.Persona),
|
||||
Status: row.Status,
|
||||
IntroVideoURL: ptrString(row.IntroVideoUrl),
|
||||
QuestionCount: row.QuestionCount,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
310
internal/repository/progression.go
Normal file
310
internal/repository/progression.go
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/ports"
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func NewProgressionStore(s *Store) ports.ProgressionStore { return s }
|
||||
|
||||
func (s *Store) AddSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error {
|
||||
_, err := s.queries.AddSubCoursePrerequisite(ctx, dbgen.AddSubCoursePrerequisiteParams{
|
||||
SubCourseID: subCourseID,
|
||||
PrerequisiteSubCourseID: prerequisiteSubCourseID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) RemoveSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error {
|
||||
return s.queries.RemoveSubCoursePrerequisite(ctx, dbgen.RemoveSubCoursePrerequisiteParams{
|
||||
SubCourseID: subCourseID,
|
||||
PrerequisiteSubCourseID: prerequisiteSubCourseID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) GetSubCoursePrerequisites(ctx context.Context, subCourseID int64) ([]domain.SubCoursePrerequisite, error) {
|
||||
rows, err := s.queries.GetSubCoursePrerequisites(ctx, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prereqs := make([]domain.SubCoursePrerequisite, len(rows))
|
||||
for i, row := range rows {
|
||||
prereqs[i] = domain.SubCoursePrerequisite{
|
||||
ID: row.ID,
|
||||
SubCourseID: row.SubCourseID,
|
||||
PrerequisiteSubCourseID: row.PrerequisiteSubCourseID,
|
||||
CreatedAt: row.CreatedAt.Time,
|
||||
PrerequisiteTitle: row.PrerequisiteTitle,
|
||||
PrerequisiteLevel: row.PrerequisiteLevel,
|
||||
PrerequisiteDisplayOrder: row.PrerequisiteDisplayOrder,
|
||||
}
|
||||
}
|
||||
return prereqs, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSubCourseDependents(ctx context.Context, prerequisiteSubCourseID int64) ([]domain.SubCourseDependent, error) {
|
||||
rows, err := s.queries.GetSubCourseDependents(ctx, prerequisiteSubCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deps := make([]domain.SubCourseDependent, len(rows))
|
||||
for i, row := range rows {
|
||||
deps[i] = domain.SubCourseDependent{
|
||||
ID: row.ID,
|
||||
SubCourseID: row.SubCourseID,
|
||||
PrerequisiteSubCourseID: row.PrerequisiteSubCourseID,
|
||||
CreatedAt: row.CreatedAt.Time,
|
||||
DependentTitle: row.DependentTitle,
|
||||
DependentLevel: row.DependentLevel,
|
||||
}
|
||||
}
|
||||
return deps, nil
|
||||
}
|
||||
|
||||
func (s *Store) CountUnmetPrerequisites(ctx context.Context, subCourseID, userID int64) (int64, error) {
|
||||
return s.queries.CountUnmetPrerequisites(ctx, dbgen.CountUnmetPrerequisitesParams{
|
||||
SubCourseID: subCourseID,
|
||||
UserID: userID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) DeleteAllPrerequisitesForSubCourse(ctx context.Context, subCourseID int64) error {
|
||||
return s.queries.DeleteAllPrerequisitesForSubCourse(ctx, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Store) StartSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error) {
|
||||
row, err := s.queries.StartSubCourseProgress(ctx, dbgen.StartSubCourseProgressParams{
|
||||
UserID: userID,
|
||||
SubCourseID: subCourseID,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return s.GetUserSubCourseProgress(ctx, userID, subCourseID)
|
||||
}
|
||||
return domain.UserSubCourseProgress{}, err
|
||||
}
|
||||
return mapUserSubCourseProgress(row), nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateSubCourseProgress(ctx context.Context, userID, subCourseID int64, percentage int16) error {
|
||||
return s.queries.UpdateSubCourseProgress(ctx, dbgen.UpdateSubCourseProgressParams{
|
||||
ProgressPercentage: percentage,
|
||||
UserID: userID,
|
||||
SubCourseID: subCourseID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) CompleteSubCourse(ctx context.Context, userID, subCourseID int64) error {
|
||||
return s.queries.CompleteSubCourse(ctx, dbgen.CompleteSubCourseParams{
|
||||
UserID: userID,
|
||||
SubCourseID: subCourseID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) RecalculateSubCourseProgress(ctx context.Context, userID, subCourseID int64) error {
|
||||
const query = `
|
||||
WITH totals AS (
|
||||
SELECT
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id = $2
|
||||
AND v.status = 'PUBLISHED') AS total_videos,
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.owner_id = $2
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED') AS total_practices
|
||||
),
|
||||
completed AS (
|
||||
SELECT
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM user_sub_course_video_progress uv
|
||||
JOIN sub_course_videos v ON v.id = uv.video_id
|
||||
WHERE uv.user_id = $1
|
||||
AND uv.sub_course_id = $2
|
||||
AND uv.completed_at IS NOT NULL
|
||||
AND v.status = 'PUBLISHED') AS completed_videos,
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM user_practice_progress up
|
||||
JOIN question_sets qs ON qs.id = up.question_set_id
|
||||
WHERE up.user_id = $1
|
||||
AND up.sub_course_id = $2
|
||||
AND up.completed_at IS NOT NULL
|
||||
AND qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.owner_id = $2
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED') AS completed_practices
|
||||
),
|
||||
stats AS (
|
||||
SELECT
|
||||
(total_videos + total_practices) AS total_items,
|
||||
(completed_videos + completed_practices) AS completed_items
|
||||
FROM totals, completed
|
||||
)
|
||||
INSERT INTO user_sub_course_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
status,
|
||||
progress_percentage,
|
||||
started_at,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
$1,
|
||||
$2,
|
||||
CASE
|
||||
WHEN stats.total_items > 0 AND stats.completed_items >= stats.total_items THEN 'COMPLETED'
|
||||
WHEN stats.completed_items > 0 THEN 'IN_PROGRESS'
|
||||
ELSE 'NOT_STARTED'
|
||||
END,
|
||||
CASE
|
||||
WHEN stats.total_items = 0 THEN 0
|
||||
ELSE ROUND((stats.completed_items::NUMERIC * 100.0) / stats.total_items::NUMERIC)::SMALLINT
|
||||
END,
|
||||
CASE
|
||||
WHEN stats.completed_items > 0 THEN CURRENT_TIMESTAMP
|
||||
ELSE NULL
|
||||
END,
|
||||
CASE
|
||||
WHEN stats.total_items > 0 AND stats.completed_items >= stats.total_items THEN CURRENT_TIMESTAMP
|
||||
ELSE NULL
|
||||
END,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM stats
|
||||
ON CONFLICT (user_id, sub_course_id) DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
progress_percentage = EXCLUDED.progress_percentage,
|
||||
started_at = COALESCE(user_sub_course_progress.started_at, EXCLUDED.started_at),
|
||||
completed_at = EXCLUDED.completed_at,
|
||||
updated_at = EXCLUDED.updated_at;
|
||||
`
|
||||
|
||||
_, err := s.conn.Exec(ctx, query, userID, subCourseID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) GetUserSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error) {
|
||||
row, err := s.queries.GetUserSubCourseProgress(ctx, dbgen.GetUserSubCourseProgressParams{
|
||||
UserID: userID,
|
||||
SubCourseID: subCourseID,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.UserSubCourseProgress{}, domain.ErrProgressNotFound
|
||||
}
|
||||
return domain.UserSubCourseProgress{}, err
|
||||
}
|
||||
return mapUserSubCourseProgress(row), nil
|
||||
}
|
||||
|
||||
func (s *Store) GetUserCourseProgress(ctx context.Context, userID, courseID int64) ([]domain.UserCourseProgressItem, error) {
|
||||
rows, err := s.queries.GetUserCourseProgress(ctx, dbgen.GetUserCourseProgressParams{
|
||||
UserID: userID,
|
||||
CourseID: courseID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]domain.UserCourseProgressItem, len(rows))
|
||||
for i, row := range rows {
|
||||
var startedAt, completedAt *time.Time
|
||||
if row.StartedAt.Valid {
|
||||
startedAt = &row.StartedAt.Time
|
||||
}
|
||||
if row.CompletedAt.Valid {
|
||||
completedAt = &row.CompletedAt.Time
|
||||
}
|
||||
var updatedAt *time.Time
|
||||
if row.UpdatedAt.Valid {
|
||||
updatedAt = &row.UpdatedAt.Time
|
||||
}
|
||||
items[i] = domain.UserCourseProgressItem{
|
||||
ID: row.ID,
|
||||
UserID: row.UserID,
|
||||
SubCourseID: row.SubCourseID,
|
||||
Status: domain.ProgressStatus(row.Status),
|
||||
ProgressPercentage: row.ProgressPercentage,
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: completedAt,
|
||||
CreatedAt: row.CreatedAt.Time,
|
||||
UpdatedAt: updatedAt,
|
||||
SubCourseTitle: row.SubCourseTitle,
|
||||
SubCourseLevel: row.SubCourseLevel,
|
||||
SubCourseDisplayOrder: row.SubCourseDisplayOrder,
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSubCoursesWithProgressByCourse(ctx context.Context, userID, courseID int64) ([]domain.SubCourseWithProgress, error) {
|
||||
rows, err := s.queries.GetSubCoursesWithProgressByCourse(ctx, dbgen.GetSubCoursesWithProgressByCourseParams{
|
||||
UserID: userID,
|
||||
CourseID: courseID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]domain.SubCourseWithProgress, len(rows))
|
||||
for i, row := range rows {
|
||||
var startedAt, completedAt *time.Time
|
||||
if row.StartedAt.Valid {
|
||||
startedAt = &row.StartedAt.Time
|
||||
}
|
||||
if row.CompletedAt.Valid {
|
||||
completedAt = &row.CompletedAt.Time
|
||||
}
|
||||
items[i] = domain.SubCourseWithProgress{
|
||||
SubCourseID: row.SubCourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrText(row.Description),
|
||||
Thumbnail: ptrText(row.Thumbnail),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
Level: row.Level,
|
||||
IsActive: row.IsActive,
|
||||
ProgressStatus: domain.ProgressStatus(row.ProgressStatus),
|
||||
ProgressPercentage: row.ProgressPercentage,
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: completedAt,
|
||||
UnmetPrerequisitesCount: row.UnmetPrerequisitesCount,
|
||||
IsLocked: row.UnmetPrerequisitesCount > 0,
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func mapUserSubCourseProgress(row dbgen.UserSubCourseProgress) domain.UserSubCourseProgress {
|
||||
var startedAt, completedAt *time.Time
|
||||
if row.StartedAt.Valid {
|
||||
startedAt = &row.StartedAt.Time
|
||||
}
|
||||
if row.CompletedAt.Valid {
|
||||
completedAt = &row.CompletedAt.Time
|
||||
}
|
||||
var updatedAt *time.Time
|
||||
if row.UpdatedAt.Valid {
|
||||
updatedAt = &row.UpdatedAt.Time
|
||||
}
|
||||
return domain.UserSubCourseProgress{
|
||||
ID: row.ID,
|
||||
UserID: row.UserID,
|
||||
SubCourseID: row.SubCourseID,
|
||||
Status: domain.ProgressStatus(row.Status),
|
||||
ProgressPercentage: row.ProgressPercentage,
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: completedAt,
|
||||
CreatedAt: row.CreatedAt.Time,
|
||||
UpdatedAt: updatedAt,
|
||||
}
|
||||
}
|
||||
329
internal/repository/sub_course_videos.go
Normal file
329
internal/repository/sub_course_videos.go
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func (s *Store) CreateSubCourseVideo(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
title string,
|
||||
description *string,
|
||||
videoURL string,
|
||||
duration int32,
|
||||
resolution *string,
|
||||
instructorID *string,
|
||||
thumbnail *string,
|
||||
visibility *string,
|
||||
displayOrder *int32,
|
||||
status *string,
|
||||
vimeoID *string,
|
||||
vimeoEmbedURL *string,
|
||||
vimeoPlayerHTML *string,
|
||||
vimeoStatus *string,
|
||||
videoHostProvider *string,
|
||||
) (domain.SubCourseVideo, error) {
|
||||
var descText, resText, instrText, thumbText, visText, statusText pgtype.Text
|
||||
var vimeoIDText, vimeoEmbedText, vimeoHTMLText, vimeoStatusText, hostProviderText pgtype.Text
|
||||
|
||||
if description != nil {
|
||||
descText = pgtype.Text{String: *description, Valid: true}
|
||||
}
|
||||
if resolution != nil {
|
||||
resText = pgtype.Text{String: *resolution, Valid: true}
|
||||
}
|
||||
if instructorID != nil {
|
||||
instrText = pgtype.Text{String: *instructorID, Valid: true}
|
||||
}
|
||||
if thumbnail != nil {
|
||||
thumbText = pgtype.Text{String: *thumbnail, Valid: true}
|
||||
}
|
||||
if visibility != nil {
|
||||
visText = pgtype.Text{String: *visibility, Valid: true}
|
||||
}
|
||||
if status != nil {
|
||||
statusText = pgtype.Text{String: *status, Valid: true}
|
||||
}
|
||||
if vimeoID != nil {
|
||||
vimeoIDText = pgtype.Text{String: *vimeoID, Valid: true}
|
||||
}
|
||||
if vimeoEmbedURL != nil {
|
||||
vimeoEmbedText = pgtype.Text{String: *vimeoEmbedURL, Valid: true}
|
||||
}
|
||||
if vimeoPlayerHTML != nil {
|
||||
vimeoHTMLText = pgtype.Text{String: *vimeoPlayerHTML, Valid: true}
|
||||
}
|
||||
if vimeoStatus != nil {
|
||||
vimeoStatusText = pgtype.Text{String: *vimeoStatus, Valid: true}
|
||||
}
|
||||
if videoHostProvider != nil {
|
||||
hostProviderText = pgtype.Text{String: *videoHostProvider, Valid: true}
|
||||
}
|
||||
|
||||
var dispOrder pgtype.Int4
|
||||
if displayOrder != nil {
|
||||
dispOrder = pgtype.Int4{Int32: *displayOrder, Valid: true}
|
||||
}
|
||||
|
||||
row, err := s.queries.CreateSubCourseVideo(ctx, dbgen.CreateSubCourseVideoParams{
|
||||
SubCourseID: subCourseID,
|
||||
Title: title,
|
||||
Description: descText,
|
||||
VideoUrl: videoURL,
|
||||
Duration: duration,
|
||||
Resolution: resText,
|
||||
InstructorID: instrText,
|
||||
Thumbnail: thumbText,
|
||||
Visibility: visText,
|
||||
Column10: dispOrder,
|
||||
Column11: statusText,
|
||||
VimeoID: vimeoIDText,
|
||||
VimeoEmbedUrl: vimeoEmbedText,
|
||||
VimeoPlayerHtml: vimeoHTMLText,
|
||||
Column15: vimeoStatusText,
|
||||
Column16: hostProviderText,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.SubCourseVideo{}, err
|
||||
}
|
||||
|
||||
return mapSubCourseVideoRow(row), nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSubCourseVideoByID(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.SubCourseVideo, error) {
|
||||
row, err := s.queries.GetSubCourseVideoByID(ctx, id)
|
||||
if err != nil {
|
||||
return domain.SubCourseVideo{}, err
|
||||
}
|
||||
|
||||
return mapSubCourseVideoRow(row), nil
|
||||
}
|
||||
|
||||
func (s *Store) GetVideosBySubCourse(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
) ([]domain.SubCourseVideo, int64, error) {
|
||||
rows, err := s.queries.GetVideosBySubCourse(ctx, subCourseID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var (
|
||||
videos []domain.SubCourseVideo
|
||||
totalCount int64
|
||||
)
|
||||
|
||||
for i, row := range rows {
|
||||
if i == 0 {
|
||||
totalCount = row.TotalCount
|
||||
}
|
||||
|
||||
videos = append(videos, domain.SubCourseVideo{
|
||||
ID: row.ID,
|
||||
SubCourseID: row.SubCourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
VideoURL: row.VideoUrl,
|
||||
Duration: row.Duration,
|
||||
Resolution: ptrString(row.Resolution),
|
||||
InstructorID: ptrString(row.InstructorID),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
Visibility: ptrString(row.Visibility),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
IsPublished: row.IsPublished,
|
||||
PublishDate: ptrTimestamptz(row.PublishDate),
|
||||
Status: row.Status,
|
||||
VimeoID: ptrString(row.VimeoID),
|
||||
VimeoEmbedURL: ptrString(row.VimeoEmbedUrl),
|
||||
VimeoPlayerHTML: ptrString(row.VimeoPlayerHtml),
|
||||
VimeoStatus: ptrString(row.VimeoStatus),
|
||||
})
|
||||
}
|
||||
|
||||
return videos, totalCount, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetPublishedVideosBySubCourse(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
) ([]domain.SubCourseVideo, error) {
|
||||
rows, err := s.queries.GetPublishedVideosBySubCourse(ctx, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
videos := make([]domain.SubCourseVideo, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
videos = append(videos, mapSubCourseVideoRow(row))
|
||||
}
|
||||
|
||||
return videos, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetFirstIncompletePreviousVideo(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
videoID int64,
|
||||
) (*domain.VideoAccessBlock, error) {
|
||||
row, err := s.queries.GetFirstIncompletePreviousVideo(ctx, dbgen.GetFirstIncompletePreviousVideoParams{
|
||||
UserID: userID,
|
||||
VideoID: videoID,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &domain.VideoAccessBlock{
|
||||
VideoID: row.ID,
|
||||
Title: row.Title,
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) MarkVideoCompleted(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
videoID int64,
|
||||
) error {
|
||||
_, err := s.queries.MarkVideoCompleted(ctx, dbgen.MarkVideoCompletedParams{
|
||||
UserID: userID,
|
||||
VideoID: videoID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) PublishSubCourseVideo(
|
||||
ctx context.Context,
|
||||
videoID int64,
|
||||
) error {
|
||||
return s.queries.PublishSubCourseVideo(ctx, videoID)
|
||||
}
|
||||
|
||||
func (s *Store) UpdateSubCourseVideo(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
title *string,
|
||||
description *string,
|
||||
videoURL *string,
|
||||
duration *int32,
|
||||
resolution *string,
|
||||
visibility *string,
|
||||
thumbnail *string,
|
||||
displayOrder *int32,
|
||||
status *string,
|
||||
) error {
|
||||
var titleVal, descVal, urlVal, resVal, visVal, thumbVal, statusVal string
|
||||
var durationVal, dispOrderVal int32
|
||||
|
||||
if title != nil {
|
||||
titleVal = *title
|
||||
}
|
||||
if description != nil {
|
||||
descVal = *description
|
||||
}
|
||||
if videoURL != nil {
|
||||
urlVal = *videoURL
|
||||
}
|
||||
if duration != nil {
|
||||
durationVal = *duration
|
||||
}
|
||||
if resolution != nil {
|
||||
resVal = *resolution
|
||||
}
|
||||
if visibility != nil {
|
||||
visVal = *visibility
|
||||
}
|
||||
if thumbnail != nil {
|
||||
thumbVal = *thumbnail
|
||||
}
|
||||
if displayOrder != nil {
|
||||
dispOrderVal = *displayOrder
|
||||
}
|
||||
if status != nil {
|
||||
statusVal = *status
|
||||
}
|
||||
|
||||
return s.queries.UpdateSubCourseVideo(ctx, dbgen.UpdateSubCourseVideoParams{
|
||||
Title: titleVal,
|
||||
Description: pgtype.Text{String: descVal, Valid: description != nil},
|
||||
VideoUrl: urlVal,
|
||||
Duration: durationVal,
|
||||
Resolution: pgtype.Text{String: resVal, Valid: resolution != nil},
|
||||
Visibility: pgtype.Text{String: visVal, Valid: visibility != nil},
|
||||
Thumbnail: pgtype.Text{String: thumbVal, Valid: thumbnail != nil},
|
||||
DisplayOrder: dispOrderVal,
|
||||
Status: statusVal,
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) ArchiveSubCourseVideo(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.queries.ArchiveSubCourseVideo(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Store) DeleteSubCourseVideo(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.queries.DeleteSubCourseVideo(ctx, id)
|
||||
}
|
||||
|
||||
func mapSubCourseVideoRow(row dbgen.SubCourseVideo) domain.SubCourseVideo {
|
||||
return domain.SubCourseVideo{
|
||||
ID: row.ID,
|
||||
SubCourseID: row.SubCourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
VideoURL: row.VideoUrl,
|
||||
Duration: row.Duration,
|
||||
Resolution: ptrString(row.Resolution),
|
||||
InstructorID: ptrString(row.InstructorID),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
Visibility: ptrString(row.Visibility),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
IsPublished: row.IsPublished,
|
||||
PublishDate: ptrTimestamptz(row.PublishDate),
|
||||
Status: row.Status,
|
||||
VimeoID: ptrString(row.VimeoID),
|
||||
VimeoEmbedURL: ptrString(row.VimeoEmbedUrl),
|
||||
VimeoPlayerHTML: ptrString(row.VimeoPlayerHtml),
|
||||
VimeoStatus: ptrString(row.VimeoStatus),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) UpdateVimeoStatus(ctx context.Context, videoID int64, status string) error {
|
||||
return s.queries.UpdateVimeoStatus(ctx, dbgen.UpdateVimeoStatusParams{
|
||||
VimeoStatus: pgtype.Text{String: status, Valid: true},
|
||||
ID: videoID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) GetVideoByVimeoID(ctx context.Context, vimeoID string) (domain.SubCourseVideo, error) {
|
||||
row, err := s.queries.GetVideosByVimeoID(ctx, pgtype.Text{String: vimeoID, Valid: true})
|
||||
if err != nil {
|
||||
return domain.SubCourseVideo{}, err
|
||||
}
|
||||
return mapSubCourseVideoRow(row), nil
|
||||
}
|
||||
|
||||
func (s *Store) ReorderSubCourseVideos(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.queries.ReorderSubCourseVideos(ctx, dbgen.ReorderSubCourseVideosParams{
|
||||
Ids: ids,
|
||||
Positions: positions,
|
||||
})
|
||||
}
|
||||
256
internal/repository/sub_courses.go
Normal file
256
internal/repository/sub_courses.go
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func (s *Store) CreateSubCourse(
|
||||
ctx context.Context,
|
||||
courseID int64,
|
||||
title string,
|
||||
description *string,
|
||||
thumbnail *string,
|
||||
displayOrder *int32,
|
||||
level string,
|
||||
subLevel string,
|
||||
) (domain.SubCourse, error) {
|
||||
var descText, thumbText pgtype.Text
|
||||
if description != nil {
|
||||
descText = pgtype.Text{String: *description, Valid: true}
|
||||
}
|
||||
if thumbnail != nil {
|
||||
thumbText = pgtype.Text{String: *thumbnail, Valid: true}
|
||||
}
|
||||
|
||||
var dispOrder pgtype.Int4
|
||||
if displayOrder != nil {
|
||||
dispOrder = pgtype.Int4{Int32: *displayOrder, Valid: true}
|
||||
}
|
||||
|
||||
row, err := s.queries.CreateSubCourse(ctx, dbgen.CreateSubCourseParams{
|
||||
CourseID: courseID,
|
||||
Title: title,
|
||||
Description: descText,
|
||||
Thumbnail: thumbText,
|
||||
Column5: dispOrder,
|
||||
Level: level,
|
||||
SubLevel: subLevel,
|
||||
Column8: pgtype.Bool{Bool: true, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return domain.SubCourse{}, err
|
||||
}
|
||||
|
||||
return domain.SubCourse{
|
||||
ID: row.ID,
|
||||
CourseID: row.CourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
Level: row.Level,
|
||||
SubLevel: row.SubLevel,
|
||||
IsActive: row.IsActive,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSubCourseByID(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.SubCourse, error) {
|
||||
row, err := s.queries.GetSubCourseByID(ctx, id)
|
||||
if err != nil {
|
||||
return domain.SubCourse{}, err
|
||||
}
|
||||
|
||||
return domain.SubCourse{
|
||||
ID: row.ID,
|
||||
CourseID: row.CourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
Level: row.Level,
|
||||
SubLevel: row.SubLevel,
|
||||
IsActive: row.IsActive,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSubCoursesByCourse(
|
||||
ctx context.Context,
|
||||
courseID int64,
|
||||
) ([]domain.SubCourse, int64, error) {
|
||||
rows, err := s.queries.GetSubCoursesByCourse(ctx, courseID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var (
|
||||
subCourses []domain.SubCourse
|
||||
totalCount int64
|
||||
)
|
||||
|
||||
for i, row := range rows {
|
||||
if i == 0 {
|
||||
totalCount = row.TotalCount
|
||||
}
|
||||
|
||||
subCourses = append(subCourses, domain.SubCourse{
|
||||
ID: row.ID,
|
||||
CourseID: row.CourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
Level: row.Level,
|
||||
SubLevel: row.SubLevel,
|
||||
IsActive: row.IsActive,
|
||||
})
|
||||
}
|
||||
|
||||
return subCourses, totalCount, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListSubCoursesByCourse(
|
||||
ctx context.Context,
|
||||
courseID int64,
|
||||
) ([]domain.SubCourse, error) {
|
||||
rows, err := s.queries.ListSubCoursesByCourse(ctx, courseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subCourses := make([]domain.SubCourse, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
subCourses = append(subCourses, domain.SubCourse{
|
||||
ID: row.ID,
|
||||
CourseID: row.CourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
Level: row.Level,
|
||||
SubLevel: row.SubLevel,
|
||||
IsActive: row.IsActive,
|
||||
})
|
||||
}
|
||||
|
||||
return subCourses, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListActiveSubCourses(
|
||||
ctx context.Context,
|
||||
) ([]domain.SubCourse, error) {
|
||||
rows, err := s.queries.ListActiveSubCourses(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subCourses := make([]domain.SubCourse, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
subCourses = append(subCourses, domain.SubCourse{
|
||||
ID: row.ID,
|
||||
CourseID: row.CourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
Level: row.Level,
|
||||
SubLevel: row.SubLevel,
|
||||
IsActive: row.IsActive,
|
||||
})
|
||||
}
|
||||
|
||||
return subCourses, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateSubCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
title *string,
|
||||
description *string,
|
||||
thumbnail *string,
|
||||
displayOrder *int32,
|
||||
level *string,
|
||||
subLevel *string,
|
||||
isActive *bool,
|
||||
) error {
|
||||
var titleVal, descVal, thumbVal, levelVal, subLevelVal string
|
||||
var dispOrderVal int32
|
||||
var isActiveVal bool
|
||||
|
||||
if title != nil {
|
||||
titleVal = *title
|
||||
}
|
||||
if description != nil {
|
||||
descVal = *description
|
||||
}
|
||||
if thumbnail != nil {
|
||||
thumbVal = *thumbnail
|
||||
}
|
||||
if displayOrder != nil {
|
||||
dispOrderVal = *displayOrder
|
||||
}
|
||||
if level != nil {
|
||||
levelVal = *level
|
||||
}
|
||||
if subLevel != nil {
|
||||
subLevelVal = *subLevel
|
||||
}
|
||||
if isActive != nil {
|
||||
isActiveVal = *isActive
|
||||
}
|
||||
|
||||
return s.queries.UpdateSubCourse(ctx, dbgen.UpdateSubCourseParams{
|
||||
Title: titleVal,
|
||||
Description: pgtype.Text{String: descVal, Valid: description != nil},
|
||||
Thumbnail: pgtype.Text{String: thumbVal, Valid: thumbnail != nil},
|
||||
DisplayOrder: dispOrderVal,
|
||||
Level: levelVal,
|
||||
SubLevel: subLevelVal,
|
||||
IsActive: isActiveVal,
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) DeactivateSubCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.queries.DeactivateSubCourse(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Store) DeleteSubCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.SubCourse, error) {
|
||||
row, err := s.queries.DeleteSubCourse(ctx, id)
|
||||
if err != nil {
|
||||
return domain.SubCourse{}, err
|
||||
}
|
||||
|
||||
return domain.SubCourse{
|
||||
ID: row.ID,
|
||||
CourseID: row.CourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
Level: row.Level,
|
||||
SubLevel: row.SubLevel,
|
||||
IsActive: row.IsActive,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) ReorderSubCourses(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.queries.ReorderSubCourses(ctx, dbgen.ReorderSubCoursesParams{
|
||||
Ids: ids,
|
||||
Positions: positions,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
44
internal/services/course_management/course_catagories.go
Normal file
44
internal/services/course_management/course_catagories.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package course_management
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
)
|
||||
|
||||
func (s *Service) CreateCourseCategory(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
) (domain.CourseCategory, error) {
|
||||
return s.courseStore.CreateCourseCategory(ctx, name)
|
||||
}
|
||||
|
||||
func (s *Service) GetCourseCategoryByID(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.CourseCategory, error) {
|
||||
return s.courseStore.GetCourseCategoryByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) GetAllCourseCategories(
|
||||
ctx context.Context,
|
||||
limit int32,
|
||||
offset int32,
|
||||
) ([]domain.CourseCategory, int64, error) {
|
||||
return s.courseStore.GetAllCourseCategories(ctx, limit, offset)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateCourseCategory(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
name *string,
|
||||
isActive *bool,
|
||||
) error {
|
||||
return s.courseStore.UpdateCourseCategory(ctx, id, name, isActive)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteCourseCategory(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.courseStore.DeleteCourseCategory(ctx, id)
|
||||
}
|
||||
52
internal/services/course_management/courses.go
Normal file
52
internal/services/course_management/courses.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package course_management
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
)
|
||||
|
||||
func (s *Service) CreateCourse(
|
||||
ctx context.Context,
|
||||
categoryID int64,
|
||||
title string,
|
||||
description *string,
|
||||
thumbnail *string,
|
||||
introVideoURL *string,
|
||||
) (domain.Course, error) {
|
||||
return s.courseStore.CreateCourse(ctx, categoryID, title, description, thumbnail, introVideoURL)
|
||||
}
|
||||
|
||||
func (s *Service) GetCourseByID(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.Course, error) {
|
||||
return s.courseStore.GetCourseByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) GetCoursesByCategory(
|
||||
ctx context.Context,
|
||||
categoryID int64,
|
||||
limit int32,
|
||||
offset int32,
|
||||
) ([]domain.Course, int64, error) {
|
||||
return s.courseStore.GetCoursesByCategory(ctx, categoryID, limit, offset)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
title *string,
|
||||
description *string,
|
||||
thumbnail *string,
|
||||
introVideoURL *string,
|
||||
isActive *bool,
|
||||
) error {
|
||||
return s.courseStore.UpdateCourse(ctx, id, title, description, thumbnail, introVideoURL, isActive)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.courseStore.DeleteCourse(ctx, id)
|
||||
}
|
||||
34
internal/services/course_management/learning_tree.go
Normal file
34
internal/services/course_management/learning_tree.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package course_management
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
)
|
||||
|
||||
func (s *Service) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) {
|
||||
return s.courseStore.GetFullLearningTree(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) GetCourseLearningPath(ctx context.Context, courseID int64) (domain.LearningPath, error) {
|
||||
return s.courseStore.GetCourseLearningPath(ctx, courseID)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderCourseCategories(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderCourseCategories(ctx, ids, positions)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderCourses(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderCourses(ctx, ids, positions)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderSubCourses(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderSubCourses(ctx, ids, positions)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderSubCourseVideos(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderSubCourseVideos(ctx, ids, positions)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderQuestionSets(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderQuestionSets(ctx, ids, positions)
|
||||
}
|
||||
73
internal/services/course_management/progression.go
Normal file
73
internal/services/course_management/progression.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package course_management
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
)
|
||||
|
||||
// --- Prerequisites (admin) ---
|
||||
|
||||
func (s *Service) AddSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error {
|
||||
if subCourseID == prerequisiteSubCourseID {
|
||||
return domain.ErrSelfPrerequisite
|
||||
}
|
||||
return s.progressionStore.AddSubCoursePrerequisite(ctx, subCourseID, prerequisiteSubCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) RemoveSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error {
|
||||
return s.progressionStore.RemoveSubCoursePrerequisite(ctx, subCourseID, prerequisiteSubCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetSubCoursePrerequisites(ctx context.Context, subCourseID int64) ([]domain.SubCoursePrerequisite, error) {
|
||||
return s.progressionStore.GetSubCoursePrerequisites(ctx, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetSubCourseDependents(ctx context.Context, prerequisiteSubCourseID int64) ([]domain.SubCourseDependent, error) {
|
||||
return s.progressionStore.GetSubCourseDependents(ctx, prerequisiteSubCourseID)
|
||||
}
|
||||
|
||||
// --- User progress ---
|
||||
|
||||
func (s *Service) CheckSubCourseAccess(ctx context.Context, userID, subCourseID int64) (bool, error) {
|
||||
unmet, err := s.progressionStore.CountUnmetPrerequisites(ctx, subCourseID, userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return unmet == 0, nil
|
||||
}
|
||||
|
||||
func (s *Service) StartSubCourse(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error) {
|
||||
accessible, err := s.CheckSubCourseAccess(ctx, userID, subCourseID)
|
||||
if err != nil {
|
||||
return domain.UserSubCourseProgress{}, err
|
||||
}
|
||||
if !accessible {
|
||||
return domain.UserSubCourseProgress{}, domain.ErrPrerequisiteNotMet
|
||||
}
|
||||
|
||||
return s.progressionStore.StartSubCourseProgress(ctx, userID, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateSubCourseProgress(ctx context.Context, userID, subCourseID int64, percentage int16) error {
|
||||
return s.progressionStore.UpdateSubCourseProgress(ctx, userID, subCourseID, percentage)
|
||||
}
|
||||
|
||||
func (s *Service) CompleteSubCourse(ctx context.Context, userID, subCourseID int64) error {
|
||||
return s.progressionStore.CompleteSubCourse(ctx, userID, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) RecalculateSubCourseProgress(ctx context.Context, userID, subCourseID int64) error {
|
||||
return s.progressionStore.RecalculateSubCourseProgress(ctx, userID, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetUserSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error) {
|
||||
return s.progressionStore.GetUserSubCourseProgress(ctx, userID, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetUserCourseProgress(ctx context.Context, userID, courseID int64) ([]domain.UserCourseProgressItem, error) {
|
||||
return s.progressionStore.GetUserCourseProgress(ctx, userID, courseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetSubCoursesWithProgress(ctx context.Context, userID, courseID int64) ([]domain.SubCourseWithProgress, error) {
|
||||
return s.progressionStore.GetSubCoursesWithProgressByCourse(ctx, userID, courseID)
|
||||
}
|
||||
|
|
@ -10,8 +10,8 @@ import (
|
|||
|
||||
type Service struct {
|
||||
userStore ports.UserStore
|
||||
courseStore interface{}
|
||||
progressionStore interface{}
|
||||
courseStore ports.CourseStore
|
||||
progressionStore ports.ProgressionStore
|
||||
notificationSvc *notificationservice.Service
|
||||
vimeoSvc *vimeoservice.Service
|
||||
cloudConvertSvc *cloudconvertservice.Service
|
||||
|
|
@ -20,8 +20,8 @@ type Service struct {
|
|||
|
||||
func NewService(
|
||||
userStore ports.UserStore,
|
||||
courseStore interface{},
|
||||
progressionStore interface{},
|
||||
courseStore ports.CourseStore,
|
||||
progressionStore ports.ProgressionStore,
|
||||
notificationSvc *notificationservice.Service,
|
||||
cfg *config.Config,
|
||||
) *Service {
|
||||
|
|
|
|||
299
internal/services/course_management/sub_course_videos.go
Normal file
299
internal/services/course_management/sub_course_videos.go
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
package course_management
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/pkgs/vimeo"
|
||||
vimeoservice "Yimaru-Backend/internal/services/vimeo"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Service) CreateSubCourseVideo(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
title string,
|
||||
description *string,
|
||||
videoURL string,
|
||||
duration int32,
|
||||
resolution *string,
|
||||
instructorID *string,
|
||||
thumbnail *string,
|
||||
visibility *string,
|
||||
displayOrder *int32,
|
||||
status *string,
|
||||
) (domain.SubCourseVideo, error) {
|
||||
// Default to DIRECT provider when no Vimeo info provided
|
||||
provider := string(domain.VideoHostProviderDirect)
|
||||
return s.courseStore.CreateSubCourseVideo(
|
||||
ctx, subCourseID, title, description, videoURL, duration,
|
||||
resolution, instructorID, thumbnail, visibility, displayOrder, status,
|
||||
nil, nil, nil, nil, &provider,
|
||||
)
|
||||
}
|
||||
|
||||
// CreateSubCourseVideoWithVimeo creates a video and uploads it to Vimeo
|
||||
func (s *Service) CreateSubCourseVideoWithVimeo(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
title string,
|
||||
description *string,
|
||||
sourceURL string,
|
||||
fileSize int64,
|
||||
duration int32,
|
||||
resolution *string,
|
||||
instructorID *string,
|
||||
thumbnail *string,
|
||||
visibility *string,
|
||||
displayOrder *int32,
|
||||
) (domain.SubCourseVideo, error) {
|
||||
if s.vimeoSvc == nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("vimeo service is not configured")
|
||||
}
|
||||
|
||||
descStr := ""
|
||||
if description != nil {
|
||||
descStr = *description
|
||||
}
|
||||
|
||||
var uploadResult *vimeoservice.UploadResult
|
||||
var err error
|
||||
|
||||
if s.cloudConvertSvc != nil {
|
||||
httpClient := &http.Client{Timeout: 30 * time.Minute}
|
||||
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL, nil)
|
||||
if reqErr != nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to create download request: %w", reqErr)
|
||||
}
|
||||
resp, dlErr := httpClient.Do(req)
|
||||
if dlErr != nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to download source video: %w", dlErr)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to download source video: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
dlSize := resp.ContentLength
|
||||
if dlSize <= 0 {
|
||||
dlSize = fileSize
|
||||
}
|
||||
|
||||
filename := path.Base(sourceURL)
|
||||
if filename == "" || filename == "." || filename == "/" {
|
||||
filename = "video.mp4"
|
||||
}
|
||||
|
||||
result, compErr := s.cloudConvertSvc.CompressVideo(ctx, filename, resp.Body, dlSize)
|
||||
if compErr != nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to compress video: %w", compErr)
|
||||
}
|
||||
defer result.Data.Close()
|
||||
|
||||
uploadResult, err = s.vimeoSvc.UploadVideoFile(ctx, title, descStr, result.Data, result.FileSize)
|
||||
} else {
|
||||
uploadResult, err = s.vimeoSvc.CreatePullUpload(ctx, title, descStr, sourceURL, fileSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to upload to Vimeo: %w", err)
|
||||
}
|
||||
|
||||
embedURL := vimeo.GenerateEmbedURL(uploadResult.VimeoID, &vimeo.EmbedOptions{
|
||||
Title: true,
|
||||
Byline: true,
|
||||
Portrait: true,
|
||||
})
|
||||
|
||||
embedHTML := vimeo.GenerateIframeEmbed(uploadResult.VimeoID, 640, 360, nil)
|
||||
|
||||
provider := string(domain.VideoHostProviderVimeo)
|
||||
vimeoStatus := "uploading"
|
||||
status := "DRAFT"
|
||||
|
||||
return s.courseStore.CreateSubCourseVideo(
|
||||
ctx, subCourseID, title, description,
|
||||
uploadResult.Link,
|
||||
duration, resolution, instructorID, thumbnail, visibility, displayOrder, &status,
|
||||
&uploadResult.VimeoID, &embedURL, &embedHTML, &vimeoStatus, &provider,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) CreateSubCourseVideoWithFileUpload(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
title string,
|
||||
description *string,
|
||||
filename string,
|
||||
fileData io.Reader,
|
||||
fileSize int64,
|
||||
duration int32,
|
||||
resolution *string,
|
||||
instructorID *string,
|
||||
thumbnail *string,
|
||||
visibility *string,
|
||||
displayOrder *int32,
|
||||
) (domain.SubCourseVideo, error) {
|
||||
if s.vimeoSvc == nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("vimeo service is not configured")
|
||||
}
|
||||
|
||||
descStr := ""
|
||||
if description != nil {
|
||||
descStr = *description
|
||||
}
|
||||
|
||||
videoReader := fileData
|
||||
videoSize := fileSize
|
||||
|
||||
if s.cloudConvertSvc != nil {
|
||||
result, err := s.cloudConvertSvc.CompressVideo(ctx, filename, fileData, fileSize)
|
||||
if err != nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to compress video: %w", err)
|
||||
}
|
||||
defer result.Data.Close()
|
||||
videoReader = result.Data
|
||||
videoSize = result.FileSize
|
||||
}
|
||||
|
||||
uploadResult, err := s.vimeoSvc.UploadVideoFile(ctx, title, descStr, videoReader, videoSize)
|
||||
if err != nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to upload video file to Vimeo: %w", err)
|
||||
}
|
||||
|
||||
embedURL := vimeo.GenerateEmbedURL(uploadResult.VimeoID, &vimeo.EmbedOptions{
|
||||
Title: true,
|
||||
Byline: true,
|
||||
Portrait: true,
|
||||
})
|
||||
|
||||
embedHTML := vimeo.GenerateIframeEmbed(uploadResult.VimeoID, 640, 360, nil)
|
||||
|
||||
provider := string(domain.VideoHostProviderVimeo)
|
||||
vimeoStatus := "uploading"
|
||||
status := "DRAFT"
|
||||
|
||||
return s.courseStore.CreateSubCourseVideo(
|
||||
ctx, subCourseID, title, description,
|
||||
uploadResult.Link,
|
||||
duration, resolution, instructorID, thumbnail, visibility, displayOrder, &status,
|
||||
&uploadResult.VimeoID, &embedURL, &embedHTML, &vimeoStatus, &provider,
|
||||
)
|
||||
}
|
||||
|
||||
// CreateSubCourseVideoFromVimeoID creates a video record from an existing Vimeo video
|
||||
func (s *Service) CreateSubCourseVideoFromVimeoID(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
vimeoVideoID string,
|
||||
title string,
|
||||
description *string,
|
||||
displayOrder *int32,
|
||||
instructorID *string,
|
||||
) (domain.SubCourseVideo, error) {
|
||||
if s.vimeoSvc == nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("vimeo service is not configured")
|
||||
}
|
||||
|
||||
// Fetch video info from Vimeo
|
||||
info, err := s.vimeoSvc.GetVideoInfo(ctx, vimeoVideoID)
|
||||
if err != nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to get Vimeo video info: %w", err)
|
||||
}
|
||||
|
||||
// Use Vimeo data
|
||||
embedHTML := vimeo.GenerateIframeEmbed(vimeoVideoID, 640, 360, nil)
|
||||
provider := string(domain.VideoHostProviderVimeo)
|
||||
vimeoStatus := info.TranscodeStatus
|
||||
if vimeoStatus == "" {
|
||||
vimeoStatus = "available"
|
||||
}
|
||||
status := "DRAFT"
|
||||
duration := int32(info.Duration)
|
||||
resolution := fmt.Sprintf("%dx%d", info.Width, info.Height)
|
||||
thumbnail := info.ThumbnailURL
|
||||
|
||||
return s.courseStore.CreateSubCourseVideo(
|
||||
ctx, subCourseID, title, description,
|
||||
info.Link, duration, &resolution, instructorID, &thumbnail, nil, displayOrder, &status,
|
||||
&vimeoVideoID, &info.EmbedURL, &embedHTML, &vimeoStatus, &provider,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) GetSubCourseVideoByID(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.SubCourseVideo, error) {
|
||||
return s.courseStore.GetSubCourseVideoByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) GetVideosBySubCourse(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
) ([]domain.SubCourseVideo, int64, error) {
|
||||
return s.courseStore.GetVideosBySubCourse(ctx, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetPublishedVideosBySubCourse(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
) ([]domain.SubCourseVideo, error) {
|
||||
return s.courseStore.GetPublishedVideosBySubCourse(ctx, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetFirstIncompletePreviousVideo(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
videoID int64,
|
||||
) (*domain.VideoAccessBlock, error) {
|
||||
return s.courseStore.GetFirstIncompletePreviousVideo(ctx, userID, videoID)
|
||||
}
|
||||
|
||||
func (s *Service) MarkVideoCompleted(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
videoID int64,
|
||||
) error {
|
||||
return s.courseStore.MarkVideoCompleted(ctx, userID, videoID)
|
||||
}
|
||||
|
||||
func (s *Service) PublishSubCourseVideo(
|
||||
ctx context.Context,
|
||||
videoID int64,
|
||||
) error {
|
||||
return s.courseStore.PublishSubCourseVideo(ctx, videoID)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateSubCourseVideo(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
title *string,
|
||||
description *string,
|
||||
videoURL *string,
|
||||
duration *int32,
|
||||
resolution *string,
|
||||
visibility *string,
|
||||
thumbnail *string,
|
||||
displayOrder *int32,
|
||||
status *string,
|
||||
) error {
|
||||
return s.courseStore.UpdateSubCourseVideo(ctx, id, title, description, videoURL, duration, resolution, visibility, thumbnail, displayOrder, status)
|
||||
}
|
||||
|
||||
func (s *Service) ArchiveSubCourseVideo(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.courseStore.ArchiveSubCourseVideo(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteSubCourseVideo(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.courseStore.DeleteSubCourseVideo(ctx, id)
|
||||
}
|
||||
74
internal/services/course_management/sub_courses.go
Normal file
74
internal/services/course_management/sub_courses.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package course_management
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
)
|
||||
|
||||
func (s *Service) CreateSubCourse(
|
||||
ctx context.Context,
|
||||
courseID int64,
|
||||
title string,
|
||||
description *string,
|
||||
thumbnail *string,
|
||||
displayOrder *int32,
|
||||
level string,
|
||||
subLevel string,
|
||||
) (domain.SubCourse, error) {
|
||||
return s.courseStore.CreateSubCourse(ctx, courseID, title, description, thumbnail, displayOrder, level, subLevel)
|
||||
}
|
||||
|
||||
func (s *Service) GetSubCourseByID(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.SubCourse, error) {
|
||||
return s.courseStore.GetSubCourseByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) GetSubCoursesByCourse(
|
||||
ctx context.Context,
|
||||
courseID int64,
|
||||
) ([]domain.SubCourse, int64, error) {
|
||||
return s.courseStore.GetSubCoursesByCourse(ctx, courseID)
|
||||
}
|
||||
|
||||
func (s *Service) ListSubCoursesByCourse(
|
||||
ctx context.Context,
|
||||
courseID int64,
|
||||
) ([]domain.SubCourse, error) {
|
||||
return s.courseStore.ListSubCoursesByCourse(ctx, courseID)
|
||||
}
|
||||
|
||||
func (s *Service) ListActiveSubCourses(
|
||||
ctx context.Context,
|
||||
) ([]domain.SubCourse, error) {
|
||||
return s.courseStore.ListActiveSubCourses(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateSubCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
title *string,
|
||||
description *string,
|
||||
thumbnail *string,
|
||||
displayOrder *int32,
|
||||
level *string,
|
||||
subLevel *string,
|
||||
isActive *bool,
|
||||
) error {
|
||||
return s.courseStore.UpdateSubCourse(ctx, id, title, description, thumbnail, displayOrder, level, subLevel, isActive)
|
||||
}
|
||||
|
||||
func (s *Service) DeactivateSubCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.courseStore.DeactivateSubCourse(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteSubCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.SubCourse, error) {
|
||||
return s.courseStore.DeleteSubCourse(ctx, id)
|
||||
}
|
||||
2872
internal/web_server/handlers/course_management.go
Normal file
2872
internal/web_server/handlers/course_management.go
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -313,7 +313,7 @@ func normalizeAndValidateMediaContentType(mediaType, contentType, fileName strin
|
|||
// @Summary Upload an audio file
|
||||
// @Tags files
|
||||
// @Accept multipart/form-data
|
||||
// @Param file formData file true "Audio file (mp3, wav, ogg, m4a, aac, webm)"
|
||||
// @Param file formance file true "Audio file (mp3, wav, ogg, m4a, aac, webm)"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/files/audio [post]
|
||||
func (h *Handler) UploadAudio(c *fiber.Ctx) error {
|
||||
|
|
|
|||
|
|
@ -1,382 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type createCourseSubCategoryReq struct {
|
||||
CategoryID int64 `json:"category_id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type createLevelReq struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
CEFRLevel string `json:"cefr_level"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type createModuleReq struct {
|
||||
LevelID int64 `json:"level_id"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type createSubModuleReq struct {
|
||||
ModuleID int64 `json:"module_id"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type createSubModuleVideoReq struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
VideoURL string `json:"video_url"`
|
||||
Duration *int32 `json:"duration"`
|
||||
Resolution *string `json:"resolution"`
|
||||
Visibility *string `json:"visibility"`
|
||||
InstructorID *string `json:"instructor_id"`
|
||||
Thumbnail *string `json:"thumbnail"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
Status *string `json:"status"`
|
||||
}
|
||||
|
||||
type attachSubModuleLessonReq struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
IntroVideoURL *string `json:"intro_video_url"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type createSubModulePracticeReq struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
Thumbnail *string `json:"thumbnail"`
|
||||
IntroVideoURL *string `json:"intro_video_url"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func toText(v *string) pgtype.Text {
|
||||
if v == nil {
|
||||
return pgtype.Text{Valid: false}
|
||||
}
|
||||
return pgtype.Text{String: *v, Valid: true}
|
||||
}
|
||||
|
||||
func toInt4(v *int32) pgtype.Int4 {
|
||||
if v == nil {
|
||||
return pgtype.Int4{Valid: false}
|
||||
}
|
||||
return pgtype.Int4{Int32: *v, Valid: true}
|
||||
}
|
||||
|
||||
func boolOrNil(v *bool) interface{} {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
func intOrNil(v *int32) interface{} {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
// UnifiedHierarchy godoc
|
||||
// @Summary Get unified course hierarchy
|
||||
// @Description Returns full hierarchy: category -> sub-category -> course
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/hierarchy [get]
|
||||
func (h *Handler) UnifiedHierarchy(c *fiber.Ctx) error {
|
||||
rows, err := h.analyticsDB.GetCoursesWithHierarchy(c.Context())
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load hierarchy", Error: err.Error()})
|
||||
}
|
||||
return c.JSON(domain.Response{Message: "Unified hierarchy retrieved successfully", Data: rows})
|
||||
}
|
||||
|
||||
// UnifiedHierarchyByCourse godoc
|
||||
// @Summary Get hierarchy for a course
|
||||
// @Description Returns hierarchy nodes for one course including levels/modules/sub-modules
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Param courseId path int true "Course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/courses/{courseId}/hierarchy [get]
|
||||
func (h *Handler) UnifiedHierarchyByCourse(c *fiber.Ctx) error {
|
||||
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course ID", Error: err.Error()})
|
||||
}
|
||||
rows, err := h.analyticsDB.GetFullHierarchyByCourseID(c.Context(), courseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course hierarchy", Error: err.Error()})
|
||||
}
|
||||
return c.JSON(domain.Response{Message: "Course hierarchy retrieved successfully", Data: rows})
|
||||
}
|
||||
|
||||
// CreateCourseSubCategory godoc
|
||||
// @Summary Create course sub-category
|
||||
// @Description Creates a sub-category under a course category
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body createCourseSubCategoryReq true "Create sub-category payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-categories [post]
|
||||
func (h *Handler) CreateCourseSubCategory(c *fiber.Ctx) error {
|
||||
var req createCourseSubCategoryReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
||||
}
|
||||
if req.CategoryID <= 0 || strings.TrimSpace(req.Name) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "category_id and name are required"})
|
||||
}
|
||||
created, err := h.analyticsDB.CreateCourseSubCategory(c.Context(), dbgen.CreateCourseSubCategoryParams{
|
||||
CategoryID: req.CategoryID,
|
||||
Name: req.Name,
|
||||
Description: toText(req.Description),
|
||||
Column4: intOrNil(req.DisplayOrder),
|
||||
Column5: boolOrNil(req.IsActive),
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create sub-category", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Course sub-category created", Data: created})
|
||||
}
|
||||
|
||||
// CreateLevel godoc
|
||||
// @Summary Create level
|
||||
// @Description Creates a CEFR level under a course
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body createLevelReq true "Create level payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/levels [post]
|
||||
func (h *Handler) CreateLevel(c *fiber.Ctx) error {
|
||||
var req createLevelReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
||||
}
|
||||
req.CEFRLevel = strings.ToUpper(strings.TrimSpace(req.CEFRLevel))
|
||||
validCEFR := map[string]bool{"A1": true, "A2": true, "A3": true, "B1": true, "B2": true, "B3": true, "C1": true, "C2": true, "C3": true}
|
||||
if req.CourseID <= 0 || !validCEFR[req.CEFRLevel] {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "course_id and valid cefr_level are required"})
|
||||
}
|
||||
created, err := h.analyticsDB.CreateLevel(c.Context(), dbgen.CreateLevelParams{
|
||||
CourseID: req.CourseID,
|
||||
CefrLevel: req.CEFRLevel,
|
||||
Column3: intOrNil(req.DisplayOrder),
|
||||
Column4: boolOrNil(req.IsActive),
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create level", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Level created", Data: created})
|
||||
}
|
||||
|
||||
// CreateModule godoc
|
||||
// @Summary Create module
|
||||
// @Description Creates a module under a level
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body createModuleReq true "Create module payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/modules [post]
|
||||
func (h *Handler) CreateModule(c *fiber.Ctx) error {
|
||||
var req createModuleReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
||||
}
|
||||
if req.LevelID <= 0 || strings.TrimSpace(req.Title) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "level_id and title are required"})
|
||||
}
|
||||
created, err := h.analyticsDB.CreateModule(c.Context(), dbgen.CreateModuleParams{
|
||||
LevelID: req.LevelID,
|
||||
Title: req.Title,
|
||||
Description: toText(req.Description),
|
||||
Column4: intOrNil(req.DisplayOrder),
|
||||
Column5: boolOrNil(req.IsActive),
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create module", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Module created", Data: created})
|
||||
}
|
||||
|
||||
// CreateSubModule godoc
|
||||
// @Summary Create sub-module
|
||||
// @Description Creates a sub-module under a module
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body createSubModuleReq true "Create sub-module payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-modules [post]
|
||||
func (h *Handler) CreateSubModule(c *fiber.Ctx) error {
|
||||
var req createSubModuleReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
||||
}
|
||||
if req.ModuleID <= 0 || strings.TrimSpace(req.Title) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "module_id and title are required"})
|
||||
}
|
||||
created, err := h.analyticsDB.CreateSubModule(c.Context(), dbgen.CreateSubModuleParams{
|
||||
ModuleID: req.ModuleID,
|
||||
Title: req.Title,
|
||||
Description: toText(req.Description),
|
||||
Column4: intOrNil(req.DisplayOrder),
|
||||
Column5: boolOrNil(req.IsActive),
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create sub-module", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Sub-module created", Data: created})
|
||||
}
|
||||
|
||||
// CreateSubModuleVideo godoc
|
||||
// @Summary Create sub-module video
|
||||
// @Description Creates a video under a sub-module
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body createSubModuleVideoReq true "Create sub-module video payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-module-videos [post]
|
||||
func (h *Handler) CreateSubModuleVideo(c *fiber.Ctx) error {
|
||||
var req createSubModuleVideoReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
||||
}
|
||||
if req.SubModuleID <= 0 || strings.TrimSpace(req.Title) == "" || strings.TrimSpace(req.VideoURL) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id, title and video_url are required"})
|
||||
}
|
||||
created, err := h.analyticsDB.CreateSubModuleVideo(c.Context(), dbgen.CreateSubModuleVideoParams{
|
||||
SubModuleID: req.SubModuleID,
|
||||
Title: req.Title,
|
||||
Description: toText(req.Description),
|
||||
VideoUrl: req.VideoURL,
|
||||
Duration: toInt4(req.Duration),
|
||||
Resolution: toText(req.Resolution),
|
||||
Column7: nil,
|
||||
Visibility: toText(req.Visibility),
|
||||
InstructorID: toText(req.InstructorID),
|
||||
Thumbnail: toText(req.Thumbnail),
|
||||
Column12: intOrNil(req.DisplayOrder),
|
||||
Column13: req.Status,
|
||||
VimeoID: pgtype.Text{Valid: false},
|
||||
VimeoEmbedUrl: pgtype.Text{Valid: false},
|
||||
VimeoPlayerHtml: pgtype.Text{Valid: false},
|
||||
VimeoStatus: pgtype.Text{Valid: false},
|
||||
Column18: nil,
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create sub-module video", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Sub-module video created", Data: created})
|
||||
}
|
||||
|
||||
// AttachSubModuleLesson godoc
|
||||
// @Summary Attach lesson to sub-module
|
||||
// @Description Links a question set lesson to a sub-module
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body attachSubModuleLessonReq true "Attach lesson payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-module-lessons [post]
|
||||
func (h *Handler) AttachSubModuleLesson(c *fiber.Ctx) error {
|
||||
var req attachSubModuleLessonReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
||||
}
|
||||
if req.SubModuleID <= 0 || req.QuestionSetID <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id and question_set_id are required"})
|
||||
}
|
||||
attached, err := h.analyticsDB.AttachQuestionSetLessonToSubModule(c.Context(), dbgen.AttachQuestionSetLessonToSubModuleParams{
|
||||
SubModuleID: req.SubModuleID,
|
||||
QuestionSetID: req.QuestionSetID,
|
||||
IntroVideoUrl: toText(req.IntroVideoURL),
|
||||
Column4: intOrNil(req.DisplayOrder),
|
||||
Column5: boolOrNil(req.IsActive),
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to attach lesson", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Lesson attached to sub-module", Data: attached})
|
||||
}
|
||||
|
||||
// CreateSubModulePractice godoc
|
||||
// @Summary Create practice under sub-module
|
||||
// @Description Creates a sub-module practice with metadata and linked question set
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body createSubModulePracticeReq true "Create practice payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-module-practices [post]
|
||||
func (h *Handler) CreateSubModulePractice(c *fiber.Ctx) error {
|
||||
var req createSubModulePracticeReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
||||
}
|
||||
if req.SubModuleID <= 0 || req.QuestionSetID <= 0 || strings.TrimSpace(req.Title) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id, title and question_set_id are required"})
|
||||
}
|
||||
created, err := h.analyticsDB.CreateSubModulePractice(c.Context(), dbgen.CreateSubModulePracticeParams{
|
||||
SubModuleID: req.SubModuleID,
|
||||
Title: req.Title,
|
||||
Description: toText(req.Description),
|
||||
Thumbnail: toText(req.Thumbnail),
|
||||
IntroVideoUrl: toText(req.IntroVideoURL),
|
||||
QuestionSetID: req.QuestionSetID,
|
||||
Column7: intOrNil(req.DisplayOrder),
|
||||
Column8: boolOrNil(req.IsActive),
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create practice", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Practice created", Data: created})
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ func resolveSeedDir(seedDir string) (string, error) {
|
|||
|
||||
// ResetAndReseedDatabase godoc
|
||||
// @Summary Reset and reseed database
|
||||
// @Description Truncates course_categories, courses, and sub_courses. If seed SQL contains INSERTs for those tables (e.g. 007_course_management_seed.sql), they are replayed; otherwise tables are left empty after truncate.
|
||||
// @Description Dangerous operation: clears and reseeds only course_categories, courses, and sub_courses from seed SQL files.
|
||||
// @Tags internal
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
|
@ -154,32 +154,26 @@ func (h *Handler) ResetAndReseedDatabase(c *fiber.Ctx) error {
|
|||
}
|
||||
}
|
||||
|
||||
missing := 0
|
||||
for _, tableName := range tableNames {
|
||||
if _, ok := statements[tableName]; !ok {
|
||||
missing++
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Missing required seed statement",
|
||||
Error: fmt.Sprintf("could not find INSERT INTO %s in seed files", tableName),
|
||||
})
|
||||
}
|
||||
}
|
||||
if missing != 0 && missing != len(tableNames) {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Incomplete course seed SQL",
|
||||
Error: "seed files must define INSERT for all of course_categories, courses, and sub_courses, or none of them (truncate-only)",
|
||||
})
|
||||
}
|
||||
|
||||
var sqlBuilder strings.Builder
|
||||
sqlBuilder.WriteString("BEGIN;\n")
|
||||
sqlBuilder.WriteString("TRUNCATE TABLE sub_courses, courses, course_categories RESTART IDENTITY CASCADE;\n")
|
||||
if missing == 0 {
|
||||
for _, tableName := range tableNames {
|
||||
sqlBuilder.WriteString("\n-- ")
|
||||
sqlBuilder.WriteString(tableName)
|
||||
sqlBuilder.WriteString(" from ")
|
||||
sqlBuilder.WriteString(statementSource[tableName])
|
||||
sqlBuilder.WriteString("\n")
|
||||
sqlBuilder.WriteString(statements[tableName])
|
||||
sqlBuilder.WriteString("\n")
|
||||
}
|
||||
for _, tableName := range tableNames {
|
||||
sqlBuilder.WriteString("\n-- ")
|
||||
sqlBuilder.WriteString(tableName)
|
||||
sqlBuilder.WriteString(" from ")
|
||||
sqlBuilder.WriteString(statementSource[tableName])
|
||||
sqlBuilder.WriteString("\n")
|
||||
sqlBuilder.WriteString(statements[tableName])
|
||||
sqlBuilder.WriteString("\n")
|
||||
}
|
||||
sqlBuilder.WriteString("COMMIT;")
|
||||
|
||||
|
|
@ -190,18 +184,12 @@ func (h *Handler) ResetAndReseedDatabase(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
msg := "Course management hierarchy reset and reseed completed successfully"
|
||||
if missing == len(tableNames) {
|
||||
msg = "Course management hierarchy truncated successfully (no INSERT seed configured; tables empty)"
|
||||
}
|
||||
return c.JSON(domain.Response{
|
||||
Message: msg,
|
||||
Message: "Course management hierarchy reset and reseed completed successfully",
|
||||
Data: map[string]interface{}{
|
||||
"seed_dir": seedDir,
|
||||
"tables": tableNames,
|
||||
"sources": statementSource,
|
||||
"reseeded": missing == 0,
|
||||
"truncate_only": missing == len(tableNames),
|
||||
"seed_dir": seedDir,
|
||||
"tables": tableNames,
|
||||
"sources": statementSource,
|
||||
},
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
|
|
|
|||
541
internal/web_server/handlers/progression_handler.go
Normal file
541
internal/web_server/handlers/progression_handler.go
Normal file
|
|
@ -0,0 +1,541 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// --- Request / Response types ---
|
||||
|
||||
type addPrerequisiteReq struct {
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id" validate:"required"`
|
||||
}
|
||||
|
||||
type prerequisiteRes struct {
|
||||
ID int64 `json:"id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
PrerequisiteTitle string `json:"prerequisite_title"`
|
||||
PrerequisiteLevel string `json:"prerequisite_level"`
|
||||
PrerequisiteDisplayOrder int32 `json:"prerequisite_display_order"`
|
||||
}
|
||||
|
||||
type dependentRes struct {
|
||||
ID int64 `json:"id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
DependentTitle string `json:"dependent_title"`
|
||||
DependentLevel string `json:"dependent_level"`
|
||||
}
|
||||
|
||||
type updateProgressReq struct {
|
||||
ProgressPercentage int16 `json:"progress_percentage" validate:"required,min=0,max=100"`
|
||||
}
|
||||
|
||||
type subCourseProgressRes struct {
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Level string `json:"level"`
|
||||
ProgressStatus string `json:"progress_status"`
|
||||
ProgressPercentage int16 `json:"progress_percentage"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
IsLocked bool `json:"is_locked"`
|
||||
}
|
||||
|
||||
type userProgressRes struct {
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
SubCourseTitle string `json:"sub_course_title"`
|
||||
SubCourseLevel string `json:"sub_course_level"`
|
||||
Status string `json:"status"`
|
||||
ProgressPercentage int16 `json:"progress_percentage"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
}
|
||||
|
||||
type courseProgressSummaryRes struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
LearnerUserID int64 `json:"learner_user_id"`
|
||||
OverallProgressPercentage int16 `json:"overall_progress_percentage"`
|
||||
TotalSubCourses int32 `json:"total_sub_courses"`
|
||||
CompletedSubCourses int32 `json:"completed_sub_courses"`
|
||||
InProgressSubCourses int32 `json:"in_progress_sub_courses"`
|
||||
NotStartedSubCourses int32 `json:"not_started_sub_courses"`
|
||||
LockedSubCourses int32 `json:"locked_sub_courses"`
|
||||
}
|
||||
|
||||
func mapSubCourseProgress(items []domain.SubCourseWithProgress) []subCourseProgressRes {
|
||||
res := make([]subCourseProgressRes, 0, len(items))
|
||||
for _, item := range items {
|
||||
res = append(res, subCourseProgressRes{
|
||||
SubCourseID: item.SubCourseID,
|
||||
Title: item.Title,
|
||||
Description: item.Description,
|
||||
Thumbnail: item.Thumbnail,
|
||||
DisplayOrder: item.DisplayOrder,
|
||||
Level: item.Level,
|
||||
ProgressStatus: string(item.ProgressStatus),
|
||||
ProgressPercentage: item.ProgressPercentage,
|
||||
StartedAt: item.StartedAt,
|
||||
CompletedAt: item.CompletedAt,
|
||||
IsLocked: item.IsLocked,
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// --- Prerequisite Handlers (admin) ---
|
||||
|
||||
// AddSubCoursePrerequisite godoc
|
||||
// @Summary Add prerequisite to sub-course
|
||||
// @Description Link a prerequisite sub-course that must be completed before accessing this sub-course
|
||||
// @Tags progression
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Sub-course ID"
|
||||
// @Param body body addPrerequisiteReq true "Prerequisite sub-course ID"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-courses/{id}/prerequisites [post]
|
||||
func (h *Handler) AddSubCoursePrerequisite(c *fiber.Ctx) error {
|
||||
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid sub-course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
var req addPrerequisiteReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.courseMgmtSvc.AddSubCoursePrerequisite(c.Context(), subCourseID, req.PrerequisiteSubCourseID); err != nil {
|
||||
if errors.Is(err, domain.ErrSelfPrerequisite) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid prerequisite",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to add prerequisite",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Prerequisite added successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetSubCoursePrerequisites godoc
|
||||
// @Summary Get sub-course prerequisites
|
||||
// @Description Returns all prerequisites for a sub-course
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param id path int true "Sub-course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-courses/{id}/prerequisites [get]
|
||||
func (h *Handler) GetSubCoursePrerequisites(c *fiber.Ctx) error {
|
||||
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid sub-course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
prerequisites, err := h.courseMgmtSvc.GetSubCoursePrerequisites(c.Context(), subCourseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to get prerequisites",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
var res []prerequisiteRes
|
||||
for _, p := range prerequisites {
|
||||
res = append(res, prerequisiteRes{
|
||||
ID: p.ID,
|
||||
SubCourseID: p.SubCourseID,
|
||||
PrerequisiteSubCourseID: p.PrerequisiteSubCourseID,
|
||||
PrerequisiteTitle: p.PrerequisiteTitle,
|
||||
PrerequisiteLevel: p.PrerequisiteLevel,
|
||||
PrerequisiteDisplayOrder: p.PrerequisiteDisplayOrder,
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Prerequisites retrieved successfully",
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveSubCoursePrerequisite godoc
|
||||
// @Summary Remove prerequisite from sub-course
|
||||
// @Description Unlink a prerequisite from a sub-course
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param id path int true "Sub-course ID"
|
||||
// @Param prerequisiteId path int true "Prerequisite sub-course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-courses/{id}/prerequisites/{prerequisiteId} [delete]
|
||||
func (h *Handler) RemoveSubCoursePrerequisite(c *fiber.Ctx) error {
|
||||
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid sub-course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
prerequisiteID, err := strconv.ParseInt(c.Params("prerequisiteId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid prerequisite ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.courseMgmtSvc.RemoveSubCoursePrerequisite(c.Context(), subCourseID, prerequisiteID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to remove prerequisite",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Prerequisite removed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// --- User Progress Handlers ---
|
||||
|
||||
// StartSubCourse godoc
|
||||
// @Summary Start a sub-course
|
||||
// @Description Mark a sub-course as started for the authenticated user (checks prerequisites)
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param id path int true "Sub-course ID"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 403 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/progress/sub-courses/{id}/start [post]
|
||||
func (h *Handler) StartSubCourse(c *fiber.Ctx) error {
|
||||
userID := c.Locals("user_id").(int64)
|
||||
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid sub-course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
progress, err := h.courseMgmtSvc.StartSubCourse(c.Context(), userID, subCourseID)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrPrerequisiteNotMet) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
Message: "Cannot start sub-course",
|
||||
Error: "Prerequisites not completed",
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to start sub-course",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Sub-course started",
|
||||
Data: userProgressRes{
|
||||
SubCourseID: progress.SubCourseID,
|
||||
Status: string(progress.Status),
|
||||
ProgressPercentage: progress.ProgressPercentage,
|
||||
StartedAt: progress.StartedAt,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateSubCourseProgress godoc
|
||||
// @Summary Update sub-course progress
|
||||
// @Description Update the progress percentage for a sub-course
|
||||
// @Tags progression
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Sub-course ID"
|
||||
// @Param body body updateProgressReq true "Progress update"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/progress/sub-courses/{id} [put]
|
||||
func (h *Handler) UpdateSubCourseProgress(c *fiber.Ctx) error {
|
||||
userID := c.Locals("user_id").(int64)
|
||||
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid sub-course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
var req updateProgressReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.courseMgmtSvc.UpdateSubCourseProgress(c.Context(), userID, subCourseID, req.ProgressPercentage); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update progress",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Progress updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// CompleteSubCourse godoc
|
||||
// @Summary Complete a sub-course
|
||||
// @Description Mark a sub-course as completed for the authenticated user
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param id path int true "Sub-course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/progress/sub-courses/{id}/complete [post]
|
||||
func (h *Handler) CompleteSubCourse(c *fiber.Ctx) error {
|
||||
userID := c.Locals("user_id").(int64)
|
||||
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid sub-course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.courseMgmtSvc.CompleteSubCourse(c.Context(), userID, subCourseID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to complete sub-course",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Sub-course completed",
|
||||
})
|
||||
}
|
||||
|
||||
// CheckSubCourseAccess godoc
|
||||
// @Summary Check sub-course access
|
||||
// @Description Check if the authenticated user has completed all prerequisites for a sub-course
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param id path int true "Sub-course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/progress/sub-courses/{id}/access [get]
|
||||
func (h *Handler) CheckSubCourseAccess(c *fiber.Ctx) error {
|
||||
userID := c.Locals("user_id").(int64)
|
||||
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid sub-course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
accessible, err := h.courseMgmtSvc.CheckSubCourseAccess(c.Context(), userID, subCourseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to check access",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Access check completed",
|
||||
Data: fiber.Map{
|
||||
"accessible": accessible,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserCourseProgress godoc
|
||||
// @Summary Get user's course progress
|
||||
// @Description Returns the authenticated user's progress for all sub-courses in a course, including lock status
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param courseId path int true "Course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/progress/courses/{courseId} [get]
|
||||
func (h *Handler) GetUserCourseProgress(c *fiber.Ctx) error {
|
||||
userID := c.Locals("user_id").(int64)
|
||||
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
items, err := h.courseMgmtSvc.GetSubCoursesWithProgress(c.Context(), userID, courseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to get course progress",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Course progress retrieved successfully",
|
||||
Data: mapSubCourseProgress(items),
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserCourseProgressForAdmin godoc
|
||||
// @Summary Get learner's course progress (admin)
|
||||
// @Description Returns a target learner's progress for all sub-courses in a course, including lock status
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param userId path int true "Learner User ID"
|
||||
// @Param courseId path int true "Course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/admin/users/{userId}/progress/courses/{courseId} [get]
|
||||
func (h *Handler) GetUserCourseProgressForAdmin(c *fiber.Ctx) error {
|
||||
targetUserID, err := strconv.ParseInt(c.Params("userId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid user ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
items, err := h.courseMgmtSvc.GetSubCoursesWithProgress(c.Context(), targetUserID, courseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to get learner course progress",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Learner course progress retrieved successfully",
|
||||
Data: mapSubCourseProgress(items),
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserCourseProgressSummaryForAdmin godoc
|
||||
// @Summary Get learner's course progress summary (admin)
|
||||
// @Description Returns course-level aggregated progress metrics for a target learner
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param userId path int true "Learner User ID"
|
||||
// @Param courseId path int true "Course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/admin/users/{userId}/progress/courses/{courseId}/summary [get]
|
||||
func (h *Handler) GetUserCourseProgressSummaryForAdmin(c *fiber.Ctx) error {
|
||||
targetUserID, err := strconv.ParseInt(c.Params("userId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid user ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
items, err := h.courseMgmtSvc.GetSubCoursesWithProgress(c.Context(), targetUserID, courseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to get learner course progress summary",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
completedCount int32
|
||||
inProgressCount int32
|
||||
notStartedCount int32
|
||||
lockedCount int32
|
||||
sumPercentage int64
|
||||
)
|
||||
|
||||
for _, item := range items {
|
||||
sumPercentage += int64(item.ProgressPercentage)
|
||||
switch item.ProgressStatus {
|
||||
case domain.ProgressStatusCompleted:
|
||||
completedCount++
|
||||
case domain.ProgressStatusInProgress:
|
||||
inProgressCount++
|
||||
default:
|
||||
notStartedCount++
|
||||
}
|
||||
if item.IsLocked {
|
||||
lockedCount++
|
||||
}
|
||||
}
|
||||
|
||||
totalSubCourses := int32(len(items))
|
||||
overall := int16(0)
|
||||
if totalSubCourses > 0 {
|
||||
overall = int16(math.Round(float64(sumPercentage) / float64(totalSubCourses)))
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Learner course progress summary retrieved successfully",
|
||||
Data: courseProgressSummaryRes{
|
||||
CourseID: courseID,
|
||||
LearnerUserID: targetUserID,
|
||||
OverallProgressPercentage: overall,
|
||||
TotalSubCourses: totalSubCourses,
|
||||
CompletedSubCourses: completedCount,
|
||||
InProgressSubCourses: inProgressCount,
|
||||
NotStartedSubCourses: notStartedCount,
|
||||
LockedSubCourses: lockedCount,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -1350,6 +1350,19 @@ func (h *Handler) CompletePractice(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if set.OwnerID == nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update sub-course progress",
|
||||
Error: "practice owner is missing",
|
||||
})
|
||||
}
|
||||
if err := h.courseMgmtSvc.RecalculateSubCourseProgress(c.Context(), userID, *set.OwnerID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update sub-course progress",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Practice completed",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,85 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"Yimaru-Backend/internal/domain"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (h *Handler) processAndSaveThumbnail(c *fiber.Ctx, subDir string) (string, error) {
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return "", c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Image file is required", Error: err.Error()})
|
||||
}
|
||||
if fileHeader.Size > 10*1024*1024 {
|
||||
return "", c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "File too large", Error: "Thumbnail image must be <= 10MB"})
|
||||
}
|
||||
|
||||
fh, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
return "", c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to read file", Error: err.Error()})
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
head := make([]byte, 512)
|
||||
n, _ := fh.Read(head)
|
||||
contentType := http.DetectContentType(head[:n])
|
||||
if contentType != "image/jpeg" && contentType != "image/png" && contentType != "image/webp" {
|
||||
return "", c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid file type", Error: "Only jpg, png and webp images are allowed"})
|
||||
}
|
||||
|
||||
rest, err := io.ReadAll(fh)
|
||||
if err != nil {
|
||||
return "", c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to read file", Error: err.Error()})
|
||||
}
|
||||
data := append(head[:n], rest...)
|
||||
|
||||
if h.cloudConvertSvc != nil {
|
||||
optimized, optErr := h.cloudConvertSvc.OptimizeImage(c.Context(), fileHeader.Filename, bytes.NewReader(data), int64(len(data)), 1200, 80)
|
||||
if optErr != nil {
|
||||
h.mongoLoggerSvc.Warn("CloudConvert thumbnail optimization failed, using original", zap.Error(optErr))
|
||||
} else {
|
||||
optimizedData, readErr := io.ReadAll(optimized.Data)
|
||||
optimized.Data.Close()
|
||||
if readErr == nil {
|
||||
data = optimizedData
|
||||
contentType = "image/webp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if h.minioSvc != nil {
|
||||
result, uploadErr := h.minioSvc.Upload(c.Context(), subDir, fileHeader.Filename, bytes.NewReader(data), int64(len(data)), contentType)
|
||||
if uploadErr != nil {
|
||||
return "", c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to upload file to storage", Error: uploadErr.Error()})
|
||||
}
|
||||
return "minio://" + result.ObjectKey, nil
|
||||
}
|
||||
|
||||
ext := ".jpg"
|
||||
if contentType == "image/png" {
|
||||
ext = ".png"
|
||||
}
|
||||
if contentType == "image/webp" {
|
||||
ext = ".webp"
|
||||
}
|
||||
dir := filepath.Join(".", "static", subDir)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create storage directory", Error: err.Error()})
|
||||
}
|
||||
filename := uuid.New().String() + ext
|
||||
fullpath := filepath.Join(dir, filename)
|
||||
if err := os.WriteFile(fullpath, data, 0o644); err != nil {
|
||||
return "", c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to save file", Error: err.Error()})
|
||||
}
|
||||
return "/static/" + subDir + "/" + filename, nil
|
||||
}
|
||||
|
||||
|
|
@ -78,16 +78,60 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Get("/assessment/questions", h.ListAssessmentQuestions)
|
||||
groupV1.Get("/assessment/questions/:id", h.GetAssessmentQuestionByID)
|
||||
|
||||
// Unified Course Management (single hierarchy model)
|
||||
groupV1.Get("/course-management/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy)
|
||||
groupV1.Get("/course-management/courses/:courseId/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchyByCourse)
|
||||
groupV1.Post("/course-management/sub-categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseSubCategory)
|
||||
groupV1.Post("/course-management/levels", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateLevel)
|
||||
groupV1.Post("/course-management/modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateModule)
|
||||
groupV1.Post("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubModule)
|
||||
groupV1.Post("/course-management/sub-module-videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubModuleVideo)
|
||||
groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("question_sets.update"), h.AttachSubModuleLesson)
|
||||
groupV1.Post("/course-management/sub-module-practices", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModulePractice)
|
||||
// Reorder (drag-and-drop support)
|
||||
// Keep static reorder routes before dynamic `/:id` routes to avoid route collisions
|
||||
// (e.g., `/courses/reorder` being parsed as `/courses/:id`).
|
||||
groupV1.Put("/course-management/categories/reorder", a.authMiddleware, a.RequirePermission("course_categories.reorder"), h.ReorderCourseCategories)
|
||||
groupV1.Put("/course-management/courses/reorder", a.authMiddleware, a.RequirePermission("courses.reorder"), h.ReorderCourses)
|
||||
groupV1.Put("/course-management/sub-courses/reorder", a.authMiddleware, a.RequirePermission("subcourses.reorder"), h.ReorderSubCourses)
|
||||
groupV1.Put("/course-management/videos/reorder", a.authMiddleware, a.RequirePermission("videos.reorder"), h.ReorderSubCourseVideos)
|
||||
groupV1.Put("/course-management/practices/reorder", a.authMiddleware, a.RequirePermission("practices.reorder"), h.ReorderPractices)
|
||||
|
||||
// Course Categories
|
||||
groupV1.Post("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseCategory)
|
||||
groupV1.Get("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.list"), h.GetAllCourseCategories)
|
||||
groupV1.Get("/course-management/categories/:id", a.authMiddleware, a.RequirePermission("course_categories.get"), h.GetCourseCategoryByID)
|
||||
groupV1.Put("/course-management/categories/:id", a.authMiddleware, a.RequirePermission("course_categories.update"), h.UpdateCourseCategory)
|
||||
groupV1.Delete("/course-management/categories/:id", a.authMiddleware, a.RequirePermission("course_categories.delete"), h.DeleteCourseCategory)
|
||||
|
||||
// Courses
|
||||
groupV1.Post("/course-management/courses", a.authMiddleware, a.RequirePermission("courses.create"), h.CreateCourse)
|
||||
groupV1.Get("/course-management/courses/:id", a.authMiddleware, a.RequirePermission("courses.get"), h.GetCourseByID)
|
||||
groupV1.Get("/course-management/categories/:categoryId/courses", a.authMiddleware, a.RequirePermission("courses.list_by_category"), h.GetCoursesByCategory)
|
||||
groupV1.Put("/course-management/courses/:id", a.authMiddleware, a.RequirePermission("courses.update"), h.UpdateCourse)
|
||||
groupV1.Post("/course-management/courses/:id/thumbnail", a.authMiddleware, a.RequirePermission("courses.upload_thumbnail"), h.UploadCourseThumbnail)
|
||||
groupV1.Delete("/course-management/courses/:id", a.authMiddleware, a.RequirePermission("courses.delete"), h.DeleteCourse)
|
||||
|
||||
// Sub-courses
|
||||
groupV1.Post("/course-management/sub-courses", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubCourse)
|
||||
groupV1.Get("/course-management/sub-courses/:id", a.authMiddleware, a.RequirePermission("subcourses.get"), h.GetSubCourseByID)
|
||||
groupV1.Get("/course-management/courses/:courseId/sub-courses", a.authMiddleware, a.RequirePermission("subcourses.list_by_course"), h.GetSubCoursesByCourse)
|
||||
groupV1.Get("/course-management/courses/:courseId/sub-courses/list", a.authMiddleware, a.RequirePermission("subcourses.list_by_course_list"), h.ListSubCoursesByCourse)
|
||||
groupV1.Get("/course-management/sub-courses/active", a.authMiddleware, a.RequirePermission("subcourses.list_active"), h.ListActiveSubCourses)
|
||||
groupV1.Patch("/course-management/sub-courses/:id", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateSubCourse)
|
||||
groupV1.Post("/course-management/sub-courses/:id/thumbnail", a.authMiddleware, a.RequirePermission("subcourses.upload_thumbnail"), h.UploadSubCourseThumbnail)
|
||||
groupV1.Put("/course-management/sub-courses/:id/deactivate", a.authMiddleware, a.RequirePermission("subcourses.deactivate"), h.DeactivateSubCourse)
|
||||
groupV1.Delete("/course-management/sub-courses/:id", a.authMiddleware, a.RequirePermission("subcourses.delete"), h.DeleteSubCourse)
|
||||
|
||||
// Sub-course Videos
|
||||
groupV1.Post("/course-management/videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubCourseVideo)
|
||||
groupV1.Post("/course-management/videos/vimeo", a.authMiddleware, a.RequirePermission("videos.create_vimeo"), h.CreateSubCourseVideoWithVimeo)
|
||||
groupV1.Post("/course-management/videos/upload", a.authMiddleware, a.RequirePermission("videos.upload"), h.UploadSubCourseVideo)
|
||||
groupV1.Post("/course-management/videos/vimeo/import", a.authMiddleware, a.RequirePermission("videos.import_vimeo"), h.CreateSubCourseVideoFromVimeoID)
|
||||
groupV1.Get("/course-management/videos/:id", a.authMiddleware, a.RequirePermission("videos.get"), h.GetSubCourseVideoByID)
|
||||
groupV1.Get("/course-management/sub-courses/:subCourseId/videos", a.authMiddleware, a.RequirePermission("videos.list_by_subcourse"), h.GetVideosBySubCourse)
|
||||
groupV1.Get("/course-management/sub-courses/:subCourseId/videos/published", a.authMiddleware, a.RequirePermission("videos.list_published"), h.GetPublishedVideosBySubCourse)
|
||||
groupV1.Put("/course-management/videos/:id/publish", a.authMiddleware, a.RequirePermission("videos.publish"), h.PublishSubCourseVideo)
|
||||
groupV1.Put("/course-management/videos/:id", a.authMiddleware, a.RequirePermission("videos.update"), h.UpdateSubCourseVideo)
|
||||
groupV1.Delete("/course-management/videos/:id", a.authMiddleware, a.RequirePermission("videos.delete"), h.DeleteSubCourseVideo)
|
||||
|
||||
// Learning Tree
|
||||
groupV1.Get("/course-management/learning-tree", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetFullLearningTree)
|
||||
groupV1.Get("/course-management/courses/:courseId/learning-path", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetCourseLearningPath)
|
||||
groupV1.Get("/course-management/human-language/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetHumanLanguageHierarchy)
|
||||
groupV1.Get("/course-management/human-language/courses/:courseId/lessons", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetHumanLanguageLessonsByCourse)
|
||||
groupV1.Post("/course-management/human-language/lessons", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateHumanLanguageLesson)
|
||||
groupV1.Patch("/course-management/human-language/lessons/:id", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateHumanLanguageLesson)
|
||||
|
||||
// Questions
|
||||
groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)
|
||||
|
|
@ -101,6 +145,7 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Post("/question-sets", a.authMiddleware, a.RequirePermission("question_sets.create"), h.CreateQuestionSet)
|
||||
groupV1.Get("/question-sets", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetQuestionSetsByType)
|
||||
groupV1.Get("/question-sets/by-owner", a.authMiddleware, a.RequirePermission("question_sets.list_by_owner"), h.GetQuestionSetsByOwner)
|
||||
groupV1.Get("/question-sets/sub-courses/:subCourseId/entry-assessment", a.authMiddleware, a.RequirePermission("question_sets.list_by_owner"), h.GetSubCourseEntryAssessmentSet)
|
||||
groupV1.Get("/question-sets/:id", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetQuestionSetByID)
|
||||
groupV1.Put("/question-sets/:id", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdateQuestionSet)
|
||||
groupV1.Delete("/question-sets/:id", a.authMiddleware, a.RequirePermission("question_sets.delete"), h.DeleteQuestionSet)
|
||||
|
|
@ -291,7 +336,21 @@ func (a *App) initAppRoutes() {
|
|||
teamGroup.Delete("/members/:id", a.authMiddleware, a.RequirePermission("team.members.delete"), h.DeleteTeamMember)
|
||||
teamGroup.Post("/members/:id/change-password", a.authMiddleware, a.RequirePermission("team.members.change_password"), h.ChangeTeamMemberPassword)
|
||||
|
||||
// Legacy sub-course prerequisite/progression routes removed after hierarchy cutover.
|
||||
// Sub-course Prerequisites
|
||||
groupV1.Post("/course-management/sub-courses/:id/prerequisites", a.authMiddleware, a.RequirePermission("subcourse_prerequisites.add"), h.AddSubCoursePrerequisite)
|
||||
groupV1.Get("/course-management/sub-courses/:id/prerequisites", a.authMiddleware, a.RequirePermission("subcourse_prerequisites.list"), h.GetSubCoursePrerequisites)
|
||||
groupV1.Delete("/course-management/sub-courses/:id/prerequisites/:prerequisiteId", a.authMiddleware, a.RequirePermission("subcourse_prerequisites.remove"), h.RemoveSubCoursePrerequisite)
|
||||
|
||||
// User Progression
|
||||
groupV1.Post("/progress/sub-courses/:id/start", a.authMiddleware, a.RequirePermission("progress.start"), h.StartSubCourse)
|
||||
groupV1.Put("/progress/sub-courses/:id", a.authMiddleware, a.RequirePermission("progress.update"), h.UpdateSubCourseProgress)
|
||||
groupV1.Post("/progress/sub-courses/:id/complete", a.authMiddleware, a.RequirePermission("progress.complete"), h.CompleteSubCourse)
|
||||
groupV1.Post("/progress/videos/:id/complete", a.authMiddleware, a.RequirePermission("progress.update"), h.CompleteSubCourseVideo)
|
||||
groupV1.Post("/progress/practices/:id/complete", a.authMiddleware, a.RequirePermission("progress.update"), h.CompletePractice)
|
||||
groupV1.Get("/progress/sub-courses/:id/access", a.authMiddleware, a.RequirePermission("progress.check_access"), h.CheckSubCourseAccess)
|
||||
groupV1.Get("/progress/courses/:courseId", a.authMiddleware, a.RequirePermission("progress.get_course"), h.GetUserCourseProgress)
|
||||
groupV1.Get("/admin/users/:userId/progress/courses/:courseId", a.authMiddleware, a.RequirePermission("progress.get_any_user"), h.GetUserCourseProgressForAdmin)
|
||||
groupV1.Get("/admin/users/:userId/progress/courses/:courseId/summary", a.authMiddleware, a.RequirePermission("progress.get_any_user"), h.GetUserCourseProgressSummaryForAdmin)
|
||||
|
||||
// Ratings
|
||||
groupV1.Post("/ratings", a.authMiddleware, a.RequirePermission("ratings.submit"), h.SubmitRating)
|
||||
|
|
|
|||
4
makefile
4
makefile
|
|
@ -66,6 +66,10 @@ seed_data:
|
|||
sleep 1; \
|
||||
done
|
||||
@for file in db/data/*.sql; do \
|
||||
if [ "$$(basename $$file)" = "007_course_management_seed.sql" ]; then \
|
||||
echo "Skipping $$file (course management seed disabled)"; \
|
||||
continue; \
|
||||
fi; \
|
||||
echo "Seeding $$file..."; \
|
||||
cat $$file | docker exec -i yimaru-backend-postgres-1 psql -U root -d gh; \
|
||||
done
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user