customer RBAC
This commit is contained in:
parent
d470b024b4
commit
0226275d47
316
db/data/007_course_management_seed.sql
Normal file
316
db/data/007_course_management_seed.sql
Normal file
|
|
@ -0,0 +1,316 @@
|
||||||
|
-- ======================================================
|
||||||
|
-- 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, is_active) VALUES
|
||||||
|
-- Flutter sub-courses (course 8) — IDs 18-21
|
||||||
|
(18, 8, 'Dart Language Basics', 'Learn Dart programming language fundamentals', NULL, 1, 'BEGINNER', TRUE),
|
||||||
|
(19, 8, 'Flutter UI Widgets', 'Build beautiful UIs with Flutter widgets', NULL, 2, 'BEGINNER', TRUE),
|
||||||
|
(20, 8, 'State Management', 'Manage app state with Provider and Riverpod', NULL, 3, 'INTERMEDIATE', TRUE),
|
||||||
|
(21, 8, 'Flutter Networking & APIs', 'HTTP requests, REST APIs, and data persistence', NULL, 4, 'ADVANCED', TRUE),
|
||||||
|
|
||||||
|
-- React Native sub-courses (course 9) — IDs 22-24
|
||||||
|
(22, 9, 'React Native Setup', 'Environment setup and first app', NULL, 1, 'BEGINNER', TRUE),
|
||||||
|
(23, 9, 'Navigation & Routing', 'React Navigation and screen management', NULL, 2, 'INTERMEDIATE', TRUE),
|
||||||
|
(24, 9, 'Native Modules', 'Bridge native code with React Native', NULL, 3, 'ADVANCED', TRUE),
|
||||||
|
|
||||||
|
-- Docker & Kubernetes sub-courses (course 10) — IDs 25-27
|
||||||
|
(25, 10, 'Docker Fundamentals', 'Containers, images, and Dockerfiles', NULL, 1, 'BEGINNER', TRUE),
|
||||||
|
(26, 10, 'Docker Compose', 'Multi-container applications', NULL, 2, 'INTERMEDIATE', TRUE),
|
||||||
|
(27, 10, 'Kubernetes Basics', 'Pods, services, and deployments', NULL, 3, 'ADVANCED', TRUE),
|
||||||
|
|
||||||
|
-- CI/CD sub-courses (course 11) — IDs 28-29
|
||||||
|
(28, 11, 'Git Workflows', 'Branching strategies and pull requests', NULL, 1, 'BEGINNER', TRUE),
|
||||||
|
(29, 11, 'GitHub Actions', 'Automate workflows with GitHub Actions', NULL, 2, 'INTERMEDIATE', TRUE),
|
||||||
|
|
||||||
|
-- Cybersecurity sub-courses (course 12) — IDs 30-31
|
||||||
|
(30, 12, 'Network Security Basics', 'Firewalls, VPNs, and network protocols', NULL, 1, 'BEGINNER', TRUE),
|
||||||
|
(31, 12, 'Penetration Testing', 'Tools and techniques for pen testing', NULL, 2, 'ADVANCED', 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;
|
||||||
|
|
||||||
|
-- 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;
|
||||||
|
|
||||||
|
-- ======================================================
|
||||||
|
-- User Sub-course Progress
|
||||||
|
-- Simulate realistic student progress for admin panel
|
||||||
|
-- ======================================================
|
||||||
|
INSERT INTO user_sub_course_progress (user_id, sub_course_id, status, progress_percentage, started_at, completed_at) VALUES
|
||||||
|
-- Student 10 (Demo Student): working through Python course
|
||||||
|
(10, 1, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '30 days', CURRENT_TIMESTAMP - INTERVAL '20 days'),
|
||||||
|
(10, 2, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '20 days', CURRENT_TIMESTAMP - INTERVAL '12 days'),
|
||||||
|
(10, 3, 'IN_PROGRESS', 65, CURRENT_TIMESTAMP - INTERVAL '12 days', NULL),
|
||||||
|
|
||||||
|
-- Student 10: started Flutter
|
||||||
|
(10, 18, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '15 days', CURRENT_TIMESTAMP - INTERVAL '8 days'),
|
||||||
|
(10, 19, 'IN_PROGRESS', 40, CURRENT_TIMESTAMP - INTERVAL '8 days', NULL),
|
||||||
|
|
||||||
|
-- Student 11 (Abebe): completed Python, started JavaScript
|
||||||
|
(11, 1, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '45 days', CURRENT_TIMESTAMP - INTERVAL '35 days'),
|
||||||
|
(11, 2, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '35 days', CURRENT_TIMESTAMP - INTERVAL '25 days'),
|
||||||
|
(11, 3, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '25 days', CURRENT_TIMESTAMP - INTERVAL '15 days'),
|
||||||
|
(11, 4, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '15 days', CURRENT_TIMESTAMP - INTERVAL '5 days'),
|
||||||
|
(11, 5, 'IN_PROGRESS', 30, CURRENT_TIMESTAMP - INTERVAL '5 days', NULL),
|
||||||
|
(11, 6, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '20 days', CURRENT_TIMESTAMP - INTERVAL '10 days'),
|
||||||
|
(11, 7, 'IN_PROGRESS', 50, CURRENT_TIMESTAMP - INTERVAL '10 days', NULL),
|
||||||
|
|
||||||
|
-- Student 11: Docker course
|
||||||
|
(11, 25, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '10 days', CURRENT_TIMESTAMP - INTERVAL '3 days'),
|
||||||
|
(11, 26, 'IN_PROGRESS', 20, CURRENT_TIMESTAMP - INTERVAL '3 days', NULL),
|
||||||
|
|
||||||
|
-- Student 12 (Sara): just started
|
||||||
|
(12, 1, 'IN_PROGRESS', 25, CURRENT_TIMESTAMP - INTERVAL '7 days', NULL),
|
||||||
|
(12, 18, 'IN_PROGRESS', 10, CURRENT_TIMESTAMP - INTERVAL '3 days', NULL),
|
||||||
|
(12, 22, 'COMPLETED', 100, CURRENT_TIMESTAMP - INTERVAL '14 days', CURRENT_TIMESTAMP - INTERVAL '7 days'),
|
||||||
|
(12, 23, 'IN_PROGRESS', 60, CURRENT_TIMESTAMP - INTERVAL '7 days', NULL)
|
||||||
|
ON CONFLICT (user_id, sub_course_id) DO NOTHING;
|
||||||
|
|
||||||
|
-- ======================================================
|
||||||
|
-- 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);
|
||||||
15
db/migrations/000022_audio_questions.down.sql
Normal file
15
db/migrations/000022_audio_questions.down.sql
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
-- Revert AUDIO question type changes
|
||||||
|
|
||||||
|
-- 1. Drop question_audio_answers table
|
||||||
|
DROP TABLE IF EXISTS question_audio_answers CASCADE;
|
||||||
|
|
||||||
|
-- 2. Remove image_url column
|
||||||
|
ALTER TABLE questions DROP COLUMN IF EXISTS image_url;
|
||||||
|
|
||||||
|
-- 3. Revert question_type CHECK constraint
|
||||||
|
ALTER TABLE questions
|
||||||
|
DROP CONSTRAINT IF EXISTS questions_question_type_check;
|
||||||
|
|
||||||
|
ALTER TABLE questions
|
||||||
|
ADD CONSTRAINT questions_question_type_check
|
||||||
|
CHECK (question_type IN ('MCQ', 'TRUE_FALSE', 'SHORT_ANSWER'));
|
||||||
24
db/migrations/000022_audio_questions.up.sql
Normal file
24
db/migrations/000022_audio_questions.up.sql
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
-- Add AUDIO question type and image_url support
|
||||||
|
|
||||||
|
-- 1. Extend question_type CHECK constraint to include AUDIO
|
||||||
|
ALTER TABLE questions
|
||||||
|
DROP CONSTRAINT IF EXISTS questions_question_type_check;
|
||||||
|
|
||||||
|
ALTER TABLE questions
|
||||||
|
ADD CONSTRAINT questions_question_type_check
|
||||||
|
CHECK (question_type IN ('MCQ', 'TRUE_FALSE', 'SHORT_ANSWER', 'AUDIO'));
|
||||||
|
|
||||||
|
-- 2. Add image_url column to questions
|
||||||
|
ALTER TABLE questions
|
||||||
|
ADD COLUMN IF NOT EXISTS image_url TEXT;
|
||||||
|
|
||||||
|
-- 3. Create question_audio_answers table for storing correct answer text
|
||||||
|
CREATE TABLE IF NOT EXISTS question_audio_answers (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE,
|
||||||
|
correct_answer_text TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_question_audio_answers_question_id
|
||||||
|
ON question_audio_answers(question_id);
|
||||||
|
|
@ -9,3 +9,49 @@ FROM courses c
|
||||||
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
|
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
|
||||||
WHERE c.is_active = true
|
WHERE c.is_active = true
|
||||||
ORDER BY c.id, sc.display_order, sc.id;
|
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,
|
||||||
|
(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,
|
||||||
|
(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.created_at;
|
||||||
|
|
||||||
|
-- name: GetSubCoursePrerequisitesForLearningPath :many
|
||||||
|
SELECT p.prerequisite_sub_course_id, sc.title, sc.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;
|
||||||
|
|
|
||||||
18
db/query/question_audio_answers.sql
Normal file
18
db/query/question_audio_answers.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
-- name: CreateQuestionAudioAnswer :one
|
||||||
|
INSERT INTO question_audio_answers (question_id, correct_answer_text)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetAudioAnswerByQuestionID :one
|
||||||
|
SELECT *
|
||||||
|
FROM question_audio_answers
|
||||||
|
WHERE question_id = $1;
|
||||||
|
|
||||||
|
-- name: GetAudioAnswersByQuestionIDs :many
|
||||||
|
SELECT *
|
||||||
|
FROM question_audio_answers
|
||||||
|
WHERE question_id = ANY($1::BIGINT[]);
|
||||||
|
|
||||||
|
-- name: DeleteAudioAnswerByQuestionID :exec
|
||||||
|
DELETE FROM question_audio_answers
|
||||||
|
WHERE question_id = $1;
|
||||||
|
|
@ -21,6 +21,7 @@ SELECT
|
||||||
q.explanation,
|
q.explanation,
|
||||||
q.tips,
|
q.tips,
|
||||||
q.voice_prompt,
|
q.voice_prompt,
|
||||||
|
q.image_url,
|
||||||
q.status as question_status
|
q.status as question_status
|
||||||
FROM question_set_items qsi
|
FROM question_set_items qsi
|
||||||
JOIN questions q ON q.id = qsi.question_id
|
JOIN questions q ON q.id = qsi.question_id
|
||||||
|
|
@ -40,7 +41,8 @@ SELECT
|
||||||
q.points,
|
q.points,
|
||||||
q.explanation,
|
q.explanation,
|
||||||
q.tips,
|
q.tips,
|
||||||
q.voice_prompt
|
q.voice_prompt,
|
||||||
|
q.image_url
|
||||||
FROM question_set_items qsi
|
FROM question_set_items qsi
|
||||||
JOIN questions q ON q.id = qsi.question_id
|
JOIN questions q ON q.id = qsi.question_id
|
||||||
WHERE qsi.set_id = $1
|
WHERE qsi.set_id = $1
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,10 @@ INSERT INTO questions (
|
||||||
tips,
|
tips,
|
||||||
voice_prompt,
|
voice_prompt,
|
||||||
sample_answer_voice_prompt,
|
sample_answer_voice_prompt,
|
||||||
|
image_url,
|
||||||
status
|
status
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, COALESCE($9, 'DRAFT'))
|
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, $9, COALESCE($10, 'DRAFT'))
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetQuestionByID :one
|
-- name: GetQuestionByID :one
|
||||||
|
|
@ -59,9 +60,10 @@ SET
|
||||||
tips = COALESCE($6, tips),
|
tips = COALESCE($6, tips),
|
||||||
voice_prompt = COALESCE($7, voice_prompt),
|
voice_prompt = COALESCE($7, voice_prompt),
|
||||||
sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt),
|
sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt),
|
||||||
status = COALESCE($9, status),
|
image_url = COALESCE($9, image_url),
|
||||||
|
status = COALESCE($10, status),
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $10;
|
WHERE id = $11;
|
||||||
|
|
||||||
-- name: ArchiveQuestion :exec
|
-- name: ArchiveQuestion :exec
|
||||||
UPDATE questions
|
UPDATE questions
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,87 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"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,
|
||||||
|
(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"`
|
||||||
|
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.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
|
const GetFullLearningTree = `-- name: GetFullLearningTree :many
|
||||||
SELECT
|
SELECT
|
||||||
c.id AS course_id,
|
c.id AS course_id,
|
||||||
|
|
@ -57,3 +138,134 @@ func (q *Queries) GetFullLearningTree(ctx context.Context) ([]GetFullLearningTre
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetSubCoursePracticesForLearningPath = `-- name: GetSubCoursePracticesForLearningPath :many
|
||||||
|
SELECT id, title, description, persona, status,
|
||||||
|
(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.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"`
|
||||||
|
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.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
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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); 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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,14 @@ type Question struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
ImageUrl pgtype.Text `json:"image_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuestionAudioAnswer struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
QuestionID int64 `json:"question_id"`
|
||||||
|
CorrectAnswerText string `json:"correct_answer_text"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type QuestionOption struct {
|
type QuestionOption struct {
|
||||||
|
|
|
||||||
92
gen/db/question_audio_answers.sql.go
Normal file
92
gen/db/question_audio_answers.sql.go
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: question_audio_answers.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateQuestionAudioAnswer = `-- name: CreateQuestionAudioAnswer :one
|
||||||
|
INSERT INTO question_audio_answers (question_id, correct_answer_text)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING id, question_id, correct_answer_text, created_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateQuestionAudioAnswerParams struct {
|
||||||
|
QuestionID int64 `json:"question_id"`
|
||||||
|
CorrectAnswerText string `json:"correct_answer_text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateQuestionAudioAnswer(ctx context.Context, arg CreateQuestionAudioAnswerParams) (QuestionAudioAnswer, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateQuestionAudioAnswer, arg.QuestionID, arg.CorrectAnswerText)
|
||||||
|
var i QuestionAudioAnswer
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.QuestionID,
|
||||||
|
&i.CorrectAnswerText,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteAudioAnswerByQuestionID = `-- name: DeleteAudioAnswerByQuestionID :exec
|
||||||
|
DELETE FROM question_audio_answers
|
||||||
|
WHERE question_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteAudioAnswerByQuestionID(ctx context.Context, questionID int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, DeleteAudioAnswerByQuestionID, questionID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetAudioAnswerByQuestionID = `-- name: GetAudioAnswerByQuestionID :one
|
||||||
|
SELECT id, question_id, correct_answer_text, created_at
|
||||||
|
FROM question_audio_answers
|
||||||
|
WHERE question_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAudioAnswerByQuestionID(ctx context.Context, questionID int64) (QuestionAudioAnswer, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetAudioAnswerByQuestionID, questionID)
|
||||||
|
var i QuestionAudioAnswer
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.QuestionID,
|
||||||
|
&i.CorrectAnswerText,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetAudioAnswersByQuestionIDs = `-- name: GetAudioAnswersByQuestionIDs :many
|
||||||
|
SELECT id, question_id, correct_answer_text, created_at
|
||||||
|
FROM question_audio_answers
|
||||||
|
WHERE question_id = ANY($1::BIGINT[])
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAudioAnswersByQuestionIDs(ctx context.Context, dollar_1 []int64) ([]QuestionAudioAnswer, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetAudioAnswersByQuestionIDs, dollar_1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []QuestionAudioAnswer
|
||||||
|
for rows.Next() {
|
||||||
|
var i QuestionAudioAnswer
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.QuestionID,
|
||||||
|
&i.CorrectAnswerText,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
@ -68,7 +68,8 @@ SELECT
|
||||||
q.points,
|
q.points,
|
||||||
q.explanation,
|
q.explanation,
|
||||||
q.tips,
|
q.tips,
|
||||||
q.voice_prompt
|
q.voice_prompt,
|
||||||
|
q.image_url
|
||||||
FROM question_set_items qsi
|
FROM question_set_items qsi
|
||||||
JOIN questions q ON q.id = qsi.question_id
|
JOIN questions q ON q.id = qsi.question_id
|
||||||
WHERE qsi.set_id = $1
|
WHERE qsi.set_id = $1
|
||||||
|
|
@ -88,6 +89,7 @@ type GetPublishedQuestionsInSetRow struct {
|
||||||
Explanation pgtype.Text `json:"explanation"`
|
Explanation pgtype.Text `json:"explanation"`
|
||||||
Tips pgtype.Text `json:"tips"`
|
Tips pgtype.Text `json:"tips"`
|
||||||
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
||||||
|
ImageUrl pgtype.Text `json:"image_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]GetPublishedQuestionsInSetRow, error) {
|
func (q *Queries) GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]GetPublishedQuestionsInSetRow, error) {
|
||||||
|
|
@ -111,6 +113,7 @@ func (q *Queries) GetPublishedQuestionsInSet(ctx context.Context, setID int64) (
|
||||||
&i.Explanation,
|
&i.Explanation,
|
||||||
&i.Tips,
|
&i.Tips,
|
||||||
&i.VoicePrompt,
|
&i.VoicePrompt,
|
||||||
|
&i.ImageUrl,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -135,6 +138,7 @@ SELECT
|
||||||
q.explanation,
|
q.explanation,
|
||||||
q.tips,
|
q.tips,
|
||||||
q.voice_prompt,
|
q.voice_prompt,
|
||||||
|
q.image_url,
|
||||||
q.status as question_status
|
q.status as question_status
|
||||||
FROM question_set_items qsi
|
FROM question_set_items qsi
|
||||||
JOIN questions q ON q.id = qsi.question_id
|
JOIN questions q ON q.id = qsi.question_id
|
||||||
|
|
@ -155,6 +159,7 @@ type GetQuestionSetItemsRow struct {
|
||||||
Explanation pgtype.Text `json:"explanation"`
|
Explanation pgtype.Text `json:"explanation"`
|
||||||
Tips pgtype.Text `json:"tips"`
|
Tips pgtype.Text `json:"tips"`
|
||||||
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
||||||
|
ImageUrl pgtype.Text `json:"image_url"`
|
||||||
QuestionStatus string `json:"question_status"`
|
QuestionStatus string `json:"question_status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,6 +184,7 @@ func (q *Queries) GetQuestionSetItems(ctx context.Context, setID int64) ([]GetQu
|
||||||
&i.Explanation,
|
&i.Explanation,
|
||||||
&i.Tips,
|
&i.Tips,
|
||||||
&i.VoicePrompt,
|
&i.VoicePrompt,
|
||||||
|
&i.ImageUrl,
|
||||||
&i.QuestionStatus,
|
&i.QuestionStatus,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,11 @@ INSERT INTO questions (
|
||||||
tips,
|
tips,
|
||||||
voice_prompt,
|
voice_prompt,
|
||||||
sample_answer_voice_prompt,
|
sample_answer_voice_prompt,
|
||||||
|
image_url,
|
||||||
status
|
status
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, COALESCE($9, 'DRAFT'))
|
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, $9, COALESCE($10, 'DRAFT'))
|
||||||
RETURNING id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at
|
RETURNING id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at, image_url
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateQuestionParams struct {
|
type CreateQuestionParams struct {
|
||||||
|
|
@ -47,7 +48,8 @@ type CreateQuestionParams struct {
|
||||||
Tips pgtype.Text `json:"tips"`
|
Tips pgtype.Text `json:"tips"`
|
||||||
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
||||||
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
|
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
|
||||||
Column9 interface{} `json:"column_9"`
|
ImageUrl pgtype.Text `json:"image_url"`
|
||||||
|
Column10 interface{} `json:"column_10"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams) (Question, error) {
|
func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams) (Question, error) {
|
||||||
|
|
@ -60,7 +62,8 @@ func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams)
|
||||||
arg.Tips,
|
arg.Tips,
|
||||||
arg.VoicePrompt,
|
arg.VoicePrompt,
|
||||||
arg.SampleAnswerVoicePrompt,
|
arg.SampleAnswerVoicePrompt,
|
||||||
arg.Column9,
|
arg.ImageUrl,
|
||||||
|
arg.Column10,
|
||||||
)
|
)
|
||||||
var i Question
|
var i Question
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
|
|
@ -76,6 +79,7 @@ func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams)
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.ImageUrl,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +95,7 @@ func (q *Queries) DeleteQuestion(ctx context.Context, id int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetQuestionByID = `-- name: GetQuestionByID :one
|
const GetQuestionByID = `-- name: GetQuestionByID :one
|
||||||
SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at
|
SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at, image_url
|
||||||
FROM questions
|
FROM questions
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -112,6 +116,7 @@ func (q *Queries) GetQuestionByID(ctx context.Context, id int64) (Question, erro
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.ImageUrl,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +193,7 @@ func (q *Queries) GetQuestionWithOptions(ctx context.Context, id int64) ([]GetQu
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetQuestionsByIDs = `-- name: GetQuestionsByIDs :many
|
const GetQuestionsByIDs = `-- name: GetQuestionsByIDs :many
|
||||||
SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at
|
SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at, image_url
|
||||||
FROM questions
|
FROM questions
|
||||||
WHERE id = ANY($1::BIGINT[])
|
WHERE id = ANY($1::BIGINT[])
|
||||||
ORDER BY id
|
ORDER BY id
|
||||||
|
|
@ -216,6 +221,7 @@ func (q *Queries) GetQuestionsByIDs(ctx context.Context, dollar_1 []int64) ([]Qu
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.ImageUrl,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -230,7 +236,7 @@ func (q *Queries) GetQuestionsByIDs(ctx context.Context, dollar_1 []int64) ([]Qu
|
||||||
const ListQuestions = `-- name: ListQuestions :many
|
const ListQuestions = `-- name: ListQuestions :many
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) OVER () AS total_count,
|
COUNT(*) OVER () AS total_count,
|
||||||
q.id, q.question_text, q.question_type, q.difficulty_level, q.points, q.explanation, q.tips, q.voice_prompt, q.sample_answer_voice_prompt, q.status, q.created_at, q.updated_at
|
q.id, q.question_text, q.question_type, q.difficulty_level, q.points, q.explanation, q.tips, q.voice_prompt, q.sample_answer_voice_prompt, q.status, q.created_at, q.updated_at, q.image_url
|
||||||
FROM questions q
|
FROM questions q
|
||||||
WHERE status != 'ARCHIVED'
|
WHERE status != 'ARCHIVED'
|
||||||
AND ($1::VARCHAR IS NULL OR $1 = '' OR question_type = $1)
|
AND ($1::VARCHAR IS NULL OR $1 = '' OR question_type = $1)
|
||||||
|
|
@ -263,6 +269,7 @@ type ListQuestionsRow struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
ImageUrl pgtype.Text `json:"image_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([]ListQuestionsRow, error) {
|
func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([]ListQuestionsRow, error) {
|
||||||
|
|
@ -294,6 +301,7 @@ func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.ImageUrl,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -308,7 +316,7 @@ func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([
|
||||||
const SearchQuestions = `-- name: SearchQuestions :many
|
const SearchQuestions = `-- name: SearchQuestions :many
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) OVER () AS total_count,
|
COUNT(*) OVER () AS total_count,
|
||||||
q.id, q.question_text, q.question_type, q.difficulty_level, q.points, q.explanation, q.tips, q.voice_prompt, q.sample_answer_voice_prompt, q.status, q.created_at, q.updated_at
|
q.id, q.question_text, q.question_type, q.difficulty_level, q.points, q.explanation, q.tips, q.voice_prompt, q.sample_answer_voice_prompt, q.status, q.created_at, q.updated_at, q.image_url
|
||||||
FROM questions q
|
FROM questions q
|
||||||
WHERE status != 'ARCHIVED'
|
WHERE status != 'ARCHIVED'
|
||||||
AND question_text ILIKE '%' || $1 || '%'
|
AND question_text ILIKE '%' || $1 || '%'
|
||||||
|
|
@ -337,6 +345,7 @@ type SearchQuestionsRow struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
ImageUrl pgtype.Text `json:"image_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) SearchQuestions(ctx context.Context, arg SearchQuestionsParams) ([]SearchQuestionsRow, error) {
|
func (q *Queries) SearchQuestions(ctx context.Context, arg SearchQuestionsParams) ([]SearchQuestionsRow, error) {
|
||||||
|
|
@ -362,6 +371,7 @@ func (q *Queries) SearchQuestions(ctx context.Context, arg SearchQuestionsParams
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.ImageUrl,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -384,9 +394,10 @@ SET
|
||||||
tips = COALESCE($6, tips),
|
tips = COALESCE($6, tips),
|
||||||
voice_prompt = COALESCE($7, voice_prompt),
|
voice_prompt = COALESCE($7, voice_prompt),
|
||||||
sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt),
|
sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt),
|
||||||
status = COALESCE($9, status),
|
image_url = COALESCE($9, image_url),
|
||||||
|
status = COALESCE($10, status),
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $10
|
WHERE id = $11
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateQuestionParams struct {
|
type UpdateQuestionParams struct {
|
||||||
|
|
@ -398,6 +409,7 @@ type UpdateQuestionParams struct {
|
||||||
Tips pgtype.Text `json:"tips"`
|
Tips pgtype.Text `json:"tips"`
|
||||||
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
||||||
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
|
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
|
||||||
|
ImageUrl pgtype.Text `json:"image_url"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
@ -412,6 +424,7 @@ func (q *Queries) UpdateQuestion(ctx context.Context, arg UpdateQuestionParams)
|
||||||
arg.Tips,
|
arg.Tips,
|
||||||
arg.VoicePrompt,
|
arg.VoicePrompt,
|
||||||
arg.SampleAnswerVoicePrompt,
|
arg.SampleAnswerVoicePrompt,
|
||||||
|
arg.ImageUrl,
|
||||||
arg.Status,
|
arg.Status,
|
||||||
arg.ID,
|
arg.ID,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -87,3 +87,58 @@ const (
|
||||||
VideoHostProviderDirect VideoHostProvider = "DIRECT"
|
VideoHostProviderDirect VideoHostProvider = "DIRECT"
|
||||||
VideoHostProviderVimeo VideoHostProvider = "VIMEO"
|
VideoHostProviderVimeo VideoHostProvider = "VIMEO"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Learning Path types — full nested structure for a course
|
||||||
|
type LearningPathVideo struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
VideoURL string `json:"video_url"`
|
||||||
|
Duration int32 `json:"duration"`
|
||||||
|
Resolution *string `json:"resolution,omitempty"`
|
||||||
|
DisplayOrder int32 `json:"display_order"`
|
||||||
|
VimeoID *string `json:"vimeo_id,omitempty"`
|
||||||
|
VimeoEmbedURL *string `json:"vimeo_embed_url,omitempty"`
|
||||||
|
VideoHostProvider *string `json:"video_host_provider,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LearningPathPractice struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Persona *string `json:"persona,omitempty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
QuestionCount int64 `json:"question_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LearningPathPrerequisite struct {
|
||||||
|
SubCourseID int64 `json:"sub_course_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LearningPathSubCourse struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
DisplayOrder int32 `json:"display_order"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
PrerequisiteCount int64 `json:"prerequisite_count"`
|
||||||
|
VideoCount int64 `json:"video_count"`
|
||||||
|
PracticeCount int64 `json:"practice_count"`
|
||||||
|
Prerequisites []LearningPathPrerequisite `json:"prerequisites"`
|
||||||
|
Videos []LearningPathVideo `json:"videos"`
|
||||||
|
Practices []LearningPathPractice `json:"practices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LearningPath struct {
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
CourseTitle string `json:"course_title"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
IntroVideoURL *string `json:"intro_video_url,omitempty"`
|
||||||
|
CategoryID int64 `json:"category_id"`
|
||||||
|
CategoryName string `json:"category_name"`
|
||||||
|
SubCourses []LearningPathSubCourse `json:"sub_courses"`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const (
|
||||||
QuestionTypeMCQ QuestionType = "MCQ"
|
QuestionTypeMCQ QuestionType = "MCQ"
|
||||||
QuestionTypeTrueFalse QuestionType = "TRUE_FALSE"
|
QuestionTypeTrueFalse QuestionType = "TRUE_FALSE"
|
||||||
QuestionTypeShortAnswer QuestionType = "SHORT_ANSWER"
|
QuestionTypeShortAnswer QuestionType = "SHORT_ANSWER"
|
||||||
|
QuestionTypeAudio QuestionType = "AUDIO"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DifficultyLevel string
|
type DifficultyLevel string
|
||||||
|
|
@ -46,15 +47,24 @@ type Question struct {
|
||||||
Tips *string
|
Tips *string
|
||||||
VoicePrompt *string
|
VoicePrompt *string
|
||||||
SampleAnswerVoicePrompt *string
|
SampleAnswerVoicePrompt *string
|
||||||
|
ImageURL *string
|
||||||
Status string
|
Status string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt *time.Time
|
UpdatedAt *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QuestionAudioAnswer struct {
|
||||||
|
ID int64
|
||||||
|
QuestionID int64
|
||||||
|
CorrectAnswerText string
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type QuestionWithDetails struct {
|
type QuestionWithDetails struct {
|
||||||
Question
|
Question
|
||||||
Options []QuestionOption
|
Options []QuestionOption
|
||||||
ShortAnswers []QuestionShortAnswer
|
ShortAnswers []QuestionShortAnswer
|
||||||
|
AudioAnswer *QuestionAudioAnswer
|
||||||
}
|
}
|
||||||
|
|
||||||
type QuestionOption struct {
|
type QuestionOption struct {
|
||||||
|
|
@ -110,6 +120,7 @@ type QuestionSetItemWithQuestion struct {
|
||||||
Explanation *string
|
Explanation *string
|
||||||
Tips *string
|
Tips *string
|
||||||
VoicePrompt *string
|
VoicePrompt *string
|
||||||
|
ImageURL *string
|
||||||
QuestionStatus string
|
QuestionStatus string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,9 +133,11 @@ type CreateQuestionInput struct {
|
||||||
Tips *string
|
Tips *string
|
||||||
VoicePrompt *string
|
VoicePrompt *string
|
||||||
SampleAnswerVoicePrompt *string
|
SampleAnswerVoicePrompt *string
|
||||||
|
ImageURL *string
|
||||||
Status *string
|
Status *string
|
||||||
Options []CreateQuestionOptionInput
|
Options []CreateQuestionOptionInput
|
||||||
ShortAnswers []CreateShortAnswerInput
|
ShortAnswers []CreateShortAnswerInput
|
||||||
|
AudioCorrectAnswerText *string
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateQuestionOptionInput struct {
|
type CreateQuestionOptionInput struct {
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,9 @@ type CourseStore interface {
|
||||||
|
|
||||||
// Learning Tree
|
// Learning Tree
|
||||||
GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error)
|
GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error)
|
||||||
|
|
||||||
|
// Learning Path (full nested structure for a course)
|
||||||
|
GetCourseLearningPath(ctx context.Context, courseID int64) (domain.LearningPath, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProgressionStore interface {
|
type ProgressionStore interface {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ package repository
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Store) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) {
|
func (s *Store) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) {
|
||||||
|
|
@ -41,3 +44,115 @@ func (s *Store) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, e
|
||||||
|
|
||||||
return courses, nil
|
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,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
QuestionCount: row.QuestionCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ func questionToDomain(q dbgen.Question) domain.Question {
|
||||||
Tips: fromPgText(q.Tips),
|
Tips: fromPgText(q.Tips),
|
||||||
VoicePrompt: fromPgText(q.VoicePrompt),
|
VoicePrompt: fromPgText(q.VoicePrompt),
|
||||||
SampleAnswerVoicePrompt: fromPgText(q.SampleAnswerVoicePrompt),
|
SampleAnswerVoicePrompt: fromPgText(q.SampleAnswerVoicePrompt),
|
||||||
|
ImageURL: fromPgText(q.ImageUrl),
|
||||||
Status: q.Status,
|
Status: q.Status,
|
||||||
CreatedAt: q.CreatedAt.Time,
|
CreatedAt: q.CreatedAt.Time,
|
||||||
UpdatedAt: timePtr(q.UpdatedAt),
|
UpdatedAt: timePtr(q.UpdatedAt),
|
||||||
|
|
@ -97,6 +98,15 @@ func questionShortAnswerToDomain(a dbgen.QuestionShortAnswer) domain.QuestionSho
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func questionAudioAnswerToDomain(a dbgen.QuestionAudioAnswer) domain.QuestionAudioAnswer {
|
||||||
|
return domain.QuestionAudioAnswer{
|
||||||
|
ID: a.ID,
|
||||||
|
QuestionID: a.QuestionID,
|
||||||
|
CorrectAnswerText: a.CorrectAnswerText,
|
||||||
|
CreatedAt: a.CreatedAt.Time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func questionSetToDomain(qs dbgen.QuestionSet) domain.QuestionSet {
|
func questionSetToDomain(qs dbgen.QuestionSet) domain.QuestionSet {
|
||||||
return domain.QuestionSet{
|
return domain.QuestionSet{
|
||||||
ID: qs.ID,
|
ID: qs.ID,
|
||||||
|
|
@ -152,7 +162,8 @@ func (s *Store) CreateQuestion(ctx context.Context, input domain.CreateQuestionI
|
||||||
Tips: toPgText(input.Tips),
|
Tips: toPgText(input.Tips),
|
||||||
VoicePrompt: toPgText(input.VoicePrompt),
|
VoicePrompt: toPgText(input.VoicePrompt),
|
||||||
SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt),
|
SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt),
|
||||||
Column9: status,
|
ImageUrl: toPgText(input.ImageURL),
|
||||||
|
Column10: status,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Question{}, err
|
return domain.Question{}, err
|
||||||
|
|
@ -189,6 +200,16 @@ func (s *Store) CreateQuestion(ctx context.Context, input domain.CreateQuestionI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if input.AudioCorrectAnswerText != nil && *input.AudioCorrectAnswerText != "" {
|
||||||
|
_, err = q.CreateQuestionAudioAnswer(ctx, dbgen.CreateQuestionAudioAnswerParams{
|
||||||
|
QuestionID: question.ID,
|
||||||
|
CorrectAnswerText: *input.AudioCorrectAnswerText,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return domain.Question{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err = tx.Commit(ctx); err != nil {
|
if err = tx.Commit(ctx); err != nil {
|
||||||
return domain.Question{}, err
|
return domain.Question{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -230,10 +251,18 @@ func (s *Store) GetQuestionWithDetails(ctx context.Context, id int64) (domain.Qu
|
||||||
answers[i] = questionShortAnswerToDomain(a)
|
answers[i] = questionShortAnswerToDomain(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var audioAnswer *domain.QuestionAudioAnswer
|
||||||
|
aa, err := s.queries.GetAudioAnswerByQuestionID(ctx, id)
|
||||||
|
if err == nil {
|
||||||
|
mapped := questionAudioAnswerToDomain(aa)
|
||||||
|
audioAnswer = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
return domain.QuestionWithDetails{
|
return domain.QuestionWithDetails{
|
||||||
Question: questionToDomain(q),
|
Question: questionToDomain(q),
|
||||||
Options: options,
|
Options: options,
|
||||||
ShortAnswers: answers,
|
ShortAnswers: answers,
|
||||||
|
AudioAnswer: audioAnswer,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,6 +305,7 @@ func (s *Store) ListQuestions(ctx context.Context, questionType, difficulty, sta
|
||||||
Tips: fromPgText(r.Tips),
|
Tips: fromPgText(r.Tips),
|
||||||
VoicePrompt: fromPgText(r.VoicePrompt),
|
VoicePrompt: fromPgText(r.VoicePrompt),
|
||||||
SampleAnswerVoicePrompt: fromPgText(r.SampleAnswerVoicePrompt),
|
SampleAnswerVoicePrompt: fromPgText(r.SampleAnswerVoicePrompt),
|
||||||
|
ImageURL: fromPgText(r.ImageUrl),
|
||||||
Status: r.Status,
|
Status: r.Status,
|
||||||
CreatedAt: r.CreatedAt.Time,
|
CreatedAt: r.CreatedAt.Time,
|
||||||
UpdatedAt: timePtr(r.UpdatedAt),
|
UpdatedAt: timePtr(r.UpdatedAt),
|
||||||
|
|
@ -311,6 +341,7 @@ func (s *Store) SearchQuestions(ctx context.Context, query string, limit, offset
|
||||||
Tips: fromPgText(r.Tips),
|
Tips: fromPgText(r.Tips),
|
||||||
VoicePrompt: fromPgText(r.VoicePrompt),
|
VoicePrompt: fromPgText(r.VoicePrompt),
|
||||||
SampleAnswerVoicePrompt: fromPgText(r.SampleAnswerVoicePrompt),
|
SampleAnswerVoicePrompt: fromPgText(r.SampleAnswerVoicePrompt),
|
||||||
|
ImageURL: fromPgText(r.ImageUrl),
|
||||||
Status: r.Status,
|
Status: r.Status,
|
||||||
CreatedAt: r.CreatedAt.Time,
|
CreatedAt: r.CreatedAt.Time,
|
||||||
UpdatedAt: timePtr(r.UpdatedAt),
|
UpdatedAt: timePtr(r.UpdatedAt),
|
||||||
|
|
@ -330,7 +361,7 @@ func (s *Store) UpdateQuestion(ctx context.Context, id int64, input domain.Creat
|
||||||
status = *input.Status
|
status = *input.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.queries.UpdateQuestion(ctx, dbgen.UpdateQuestionParams{
|
err := s.queries.UpdateQuestion(ctx, dbgen.UpdateQuestionParams{
|
||||||
ID: id,
|
ID: id,
|
||||||
QuestionText: input.QuestionText,
|
QuestionText: input.QuestionText,
|
||||||
QuestionType: input.QuestionType,
|
QuestionType: input.QuestionType,
|
||||||
|
|
@ -340,8 +371,27 @@ func (s *Store) UpdateQuestion(ctx context.Context, id int64, input domain.Creat
|
||||||
Tips: toPgText(input.Tips),
|
Tips: toPgText(input.Tips),
|
||||||
VoicePrompt: toPgText(input.VoicePrompt),
|
VoicePrompt: toPgText(input.VoicePrompt),
|
||||||
SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt),
|
SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt),
|
||||||
|
ImageUrl: toPgText(input.ImageURL),
|
||||||
Status: status,
|
Status: status,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.AudioCorrectAnswerText != nil {
|
||||||
|
_ = s.queries.DeleteAudioAnswerByQuestionID(ctx, id)
|
||||||
|
if *input.AudioCorrectAnswerText != "" {
|
||||||
|
_, err = s.queries.CreateQuestionAudioAnswer(ctx, dbgen.CreateQuestionAudioAnswerParams{
|
||||||
|
QuestionID: id,
|
||||||
|
CorrectAnswerText: *input.AudioCorrectAnswerText,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ArchiveQuestion(ctx context.Context, id int64) error {
|
func (s *Store) ArchiveQuestion(ctx context.Context, id int64) error {
|
||||||
|
|
@ -653,6 +703,7 @@ func (s *Store) GetQuestionSetItems(ctx context.Context, setID int64) ([]domain.
|
||||||
Explanation: fromPgText(r.Explanation),
|
Explanation: fromPgText(r.Explanation),
|
||||||
Tips: fromPgText(r.Tips),
|
Tips: fromPgText(r.Tips),
|
||||||
VoicePrompt: fromPgText(r.VoicePrompt),
|
VoicePrompt: fromPgText(r.VoicePrompt),
|
||||||
|
ImageURL: fromPgText(r.ImageUrl),
|
||||||
QuestionStatus: r.QuestionStatus,
|
QuestionStatus: r.QuestionStatus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -681,6 +732,7 @@ func (s *Store) GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]
|
||||||
Explanation: fromPgText(r.Explanation),
|
Explanation: fromPgText(r.Explanation),
|
||||||
Tips: fromPgText(r.Tips),
|
Tips: fromPgText(r.Tips),
|
||||||
VoicePrompt: fromPgText(r.VoicePrompt),
|
VoicePrompt: fromPgText(r.VoicePrompt),
|
||||||
|
ImageURL: fromPgText(r.ImageUrl),
|
||||||
QuestionStatus: "PUBLISHED",
|
QuestionStatus: "PUBLISHED",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,7 @@ import (
|
||||||
func (s *Service) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) {
|
func (s *Service) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) {
|
||||||
return s.courseStore.GetFullLearningTree(ctx)
|
return s.courseStore.GetFullLearningTree(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetCourseLearningPath(ctx context.Context, courseID int64) (domain.LearningPath, error) {
|
||||||
|
return s.courseStore.GetCourseLearningPath(ctx, courseID)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1420,6 +1420,41 @@ func (h *Handler) GetFullLearningTree(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCourseLearningPath godoc
|
||||||
|
// @Summary Get course learning path
|
||||||
|
// @Description Returns the complete learning path for a course including sub-courses (by level),
|
||||||
|
// @Description video lessons, practices, prerequisites, and counts — structured for admin panel flexible path configuration
|
||||||
|
// @Tags learning-tree
|
||||||
|
// @Produce json
|
||||||
|
// @Param courseId path int true "Course ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/courses/{courseId}/learning-path [get]
|
||||||
|
func (h *Handler) GetCourseLearningPath(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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := h.courseMgmtSvc.GetCourseLearningPath(c.Context(), courseID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Course not found or has no learning path",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Learning path retrieved successfully",
|
||||||
|
Data: path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// UploadSubCourseVideo godoc
|
// UploadSubCourseVideo godoc
|
||||||
// @Summary Upload a video file and create sub-course video
|
// @Summary Upload a video file and create sub-course video
|
||||||
// @Description Accepts a video file upload, uploads it to Vimeo via TUS, and creates a sub-course video record
|
// @Description Accepts a video file upload, uploads it to Vimeo via TUS, and creates a sub-course video record
|
||||||
|
|
|
||||||
|
|
@ -25,16 +25,18 @@ type shortAnswerInput struct {
|
||||||
|
|
||||||
type createQuestionReq struct {
|
type createQuestionReq struct {
|
||||||
QuestionText string `json:"question_text" validate:"required"`
|
QuestionText string `json:"question_text" validate:"required"`
|
||||||
QuestionType string `json:"question_type" validate:"required,oneof=MCQ TRUE_FALSE SHORT_ANSWER"`
|
QuestionType string `json:"question_type" validate:"required,oneof=MCQ TRUE_FALSE SHORT_ANSWER AUDIO"`
|
||||||
DifficultyLevel *string `json:"difficulty_level"`
|
DifficultyLevel *string `json:"difficulty_level"`
|
||||||
Points *int32 `json:"points"`
|
Points *int32 `json:"points"`
|
||||||
Explanation *string `json:"explanation"`
|
Explanation *string `json:"explanation"`
|
||||||
Tips *string `json:"tips"`
|
Tips *string `json:"tips"`
|
||||||
VoicePrompt *string `json:"voice_prompt"`
|
VoicePrompt *string `json:"voice_prompt"`
|
||||||
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"`
|
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"`
|
||||||
|
ImageURL *string `json:"image_url"`
|
||||||
Status *string `json:"status"`
|
Status *string `json:"status"`
|
||||||
Options []optionInput `json:"options"`
|
Options []optionInput `json:"options"`
|
||||||
ShortAnswers []shortAnswerInput `json:"short_answers"`
|
ShortAnswers []shortAnswerInput `json:"short_answers"`
|
||||||
|
AudioCorrectAnswerText *string `json:"audio_correct_answer_text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type optionRes struct {
|
type optionRes struct {
|
||||||
|
|
@ -60,10 +62,12 @@ type questionRes struct {
|
||||||
Tips *string `json:"tips,omitempty"`
|
Tips *string `json:"tips,omitempty"`
|
||||||
VoicePrompt *string `json:"voice_prompt,omitempty"`
|
VoicePrompt *string `json:"voice_prompt,omitempty"`
|
||||||
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt,omitempty"`
|
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt,omitempty"`
|
||||||
|
ImageURL *string `json:"image_url,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
Options []optionRes `json:"options,omitempty"`
|
Options []optionRes `json:"options,omitempty"`
|
||||||
ShortAnswers []shortAnswerRes `json:"short_answers,omitempty"`
|
ShortAnswers []shortAnswerRes `json:"short_answers,omitempty"`
|
||||||
|
AudioCorrectAnswerText *string `json:"audio_correct_answer_text,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type listQuestionsRes struct {
|
type listQuestionsRes struct {
|
||||||
|
|
@ -119,9 +123,11 @@ func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
|
||||||
Tips: req.Tips,
|
Tips: req.Tips,
|
||||||
VoicePrompt: req.VoicePrompt,
|
VoicePrompt: req.VoicePrompt,
|
||||||
SampleAnswerVoicePrompt: req.SampleAnswerVoicePrompt,
|
SampleAnswerVoicePrompt: req.SampleAnswerVoicePrompt,
|
||||||
|
ImageURL: req.ImageURL,
|
||||||
Status: req.Status,
|
Status: req.Status,
|
||||||
Options: options,
|
Options: options,
|
||||||
ShortAnswers: shortAnswers,
|
ShortAnswers: shortAnswers,
|
||||||
|
AudioCorrectAnswerText: req.AudioCorrectAnswerText,
|
||||||
}
|
}
|
||||||
|
|
||||||
question, err := h.questionsSvc.CreateQuestion(c.Context(), input)
|
question, err := h.questionsSvc.CreateQuestion(c.Context(), input)
|
||||||
|
|
@ -151,6 +157,7 @@ func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
|
||||||
Tips: question.Tips,
|
Tips: question.Tips,
|
||||||
VoicePrompt: question.VoicePrompt,
|
VoicePrompt: question.VoicePrompt,
|
||||||
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
|
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
|
||||||
|
ImageURL: question.ImageURL,
|
||||||
Status: question.Status,
|
Status: question.Status,
|
||||||
CreatedAt: question.CreatedAt.String(),
|
CreatedAt: question.CreatedAt.String(),
|
||||||
},
|
},
|
||||||
|
|
@ -204,6 +211,11 @@ func (h *Handler) GetQuestionByID(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var audioCorrectAnswerText *string
|
||||||
|
if question.AudioAnswer != nil {
|
||||||
|
audioCorrectAnswerText = &question.AudioAnswer.CorrectAnswerText
|
||||||
|
}
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Question retrieved successfully",
|
Message: "Question retrieved successfully",
|
||||||
Data: questionRes{
|
Data: questionRes{
|
||||||
|
|
@ -216,10 +228,12 @@ func (h *Handler) GetQuestionByID(c *fiber.Ctx) error {
|
||||||
Tips: question.Tips,
|
Tips: question.Tips,
|
||||||
VoicePrompt: question.VoicePrompt,
|
VoicePrompt: question.VoicePrompt,
|
||||||
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
|
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
|
||||||
|
ImageURL: question.ImageURL,
|
||||||
Status: question.Status,
|
Status: question.Status,
|
||||||
CreatedAt: question.CreatedAt.String(),
|
CreatedAt: question.CreatedAt.String(),
|
||||||
Options: options,
|
Options: options,
|
||||||
ShortAnswers: shortAnswers,
|
ShortAnswers: shortAnswers,
|
||||||
|
AudioCorrectAnswerText: audioCorrectAnswerText,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -356,9 +370,11 @@ type updateQuestionReq struct {
|
||||||
Tips *string `json:"tips"`
|
Tips *string `json:"tips"`
|
||||||
VoicePrompt *string `json:"voice_prompt"`
|
VoicePrompt *string `json:"voice_prompt"`
|
||||||
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"`
|
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"`
|
||||||
|
ImageURL *string `json:"image_url"`
|
||||||
Status *string `json:"status"`
|
Status *string `json:"status"`
|
||||||
Options []optionInput `json:"options"`
|
Options []optionInput `json:"options"`
|
||||||
ShortAnswers []shortAnswerInput `json:"short_answers"`
|
ShortAnswers []shortAnswerInput `json:"short_answers"`
|
||||||
|
AudioCorrectAnswerText *string `json:"audio_correct_answer_text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateQuestion godoc
|
// UpdateQuestion godoc
|
||||||
|
|
@ -426,9 +442,11 @@ func (h *Handler) UpdateQuestion(c *fiber.Ctx) error {
|
||||||
Tips: req.Tips,
|
Tips: req.Tips,
|
||||||
VoicePrompt: req.VoicePrompt,
|
VoicePrompt: req.VoicePrompt,
|
||||||
SampleAnswerVoicePrompt: req.SampleAnswerVoicePrompt,
|
SampleAnswerVoicePrompt: req.SampleAnswerVoicePrompt,
|
||||||
|
ImageURL: req.ImageURL,
|
||||||
Status: req.Status,
|
Status: req.Status,
|
||||||
Options: options,
|
Options: options,
|
||||||
ShortAnswers: shortAnswers,
|
ShortAnswers: shortAnswers,
|
||||||
|
AudioCorrectAnswerText: req.AudioCorrectAnswerText,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.questionsSvc.UpdateQuestion(c.Context(), id, input)
|
err = h.questionsSvc.UpdateQuestion(c.Context(), id, input)
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ func (a *App) initAppRoutes() {
|
||||||
|
|
||||||
// Learning Tree
|
// Learning Tree
|
||||||
groupV1.Get("/course-management/learning-tree", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetFullLearningTree)
|
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)
|
||||||
|
|
||||||
// Questions
|
// Questions
|
||||||
groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)
|
groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user