Compare commits
No commits in common. "3500db643593158c7ed78d2baaa023808a55b460" and "d470b024b4033b30c402b10dd383a15c3bac1bf1" have entirely different histories.
3500db6435
...
d470b024b4
66
README.md
66
README.md
|
|
@ -90,17 +90,17 @@ created_at – Audit timestamp
|
|||
|
||||
Relationships:
|
||||
|
||||
One Course Category → Many Course Sub-categories
|
||||
One Course Category → Many Courses
|
||||
|
||||
Course Category
|
||||
└── Course Sub-categories[]
|
||||
└── Courses[]
|
||||
|
||||
2. Course Sub-category
|
||||
2. Course
|
||||
|
||||
Table: course_sub_categories
|
||||
Table: courses
|
||||
|
||||
Purpose:
|
||||
A grouping within a category (e.g., Speaking, Listening under Learning English).
|
||||
Represents a full course offering under a category.
|
||||
|
||||
Key Fields:
|
||||
|
||||
|
|
@ -114,23 +114,23 @@ Relationships:
|
|||
|
||||
Belongs to one Course Category
|
||||
|
||||
Has many Courses
|
||||
Has many Sub-courses
|
||||
|
||||
Course Category
|
||||
└── Course Sub-category
|
||||
└── Courses[]
|
||||
└── Course
|
||||
└── Sub-courses[]
|
||||
|
||||
3. Course
|
||||
3. Sub-course
|
||||
|
||||
Table: courses
|
||||
Table: sub_courses
|
||||
|
||||
Purpose:
|
||||
A learning unit within a sub-category representing different skill levels
|
||||
A learning unit within a course representing different skill levels
|
||||
(e.g., Beginner, Intermediate, Advanced).
|
||||
|
||||
Key Fields:
|
||||
|
||||
sub_category_id – FK → course_sub_categories.id
|
||||
course_id – FK → courses.id
|
||||
|
||||
title, description
|
||||
|
||||
|
|
@ -144,27 +144,27 @@ is_active
|
|||
|
||||
Relationships:
|
||||
|
||||
Belongs to one Course Sub-category
|
||||
Belongs to one Course
|
||||
|
||||
Has many Course Videos
|
||||
Has many Sub-course Videos
|
||||
|
||||
Has many Practices
|
||||
|
||||
Course Sub-category
|
||||
└── Course
|
||||
├── Course Videos[]
|
||||
Course
|
||||
└── Sub-course
|
||||
├── Sub-course Videos[]
|
||||
└── Practices[]
|
||||
|
||||
4. Course Video
|
||||
4. Sub-course Video
|
||||
|
||||
Table: course_videos
|
||||
Table: sub_course_videos
|
||||
|
||||
Purpose:
|
||||
Video learning content attached to a course.
|
||||
Video learning content attached to a sub-course.
|
||||
|
||||
Key Fields:
|
||||
|
||||
course_id – FK → courses.id
|
||||
sub_course_id – FK → sub_courses.id
|
||||
|
||||
title, description
|
||||
|
||||
|
|
@ -190,21 +190,21 @@ is_active
|
|||
|
||||
Relationships:
|
||||
|
||||
Belongs to one Course
|
||||
Belongs to one Sub-course
|
||||
|
||||
Course
|
||||
└── Course Video
|
||||
Sub-course
|
||||
└── Sub-course Video
|
||||
|
||||
5. Practice
|
||||
|
||||
Table: practices
|
||||
|
||||
Purpose:
|
||||
Exercises or assessments that belong to a course.
|
||||
Exercises or assessments that belong to a sub-course.
|
||||
|
||||
Key Fields:
|
||||
|
||||
course_id – FK → courses.id
|
||||
sub_course_id – FK → sub_courses.id
|
||||
|
||||
title, description
|
||||
|
||||
|
|
@ -216,11 +216,11 @@ is_active
|
|||
|
||||
Relationships:
|
||||
|
||||
Belongs to one Course
|
||||
Belongs to one Sub-course
|
||||
|
||||
One Practice → Many Practice Questions
|
||||
|
||||
Course
|
||||
Sub-course
|
||||
└── Practice
|
||||
└── Practice Questions[]
|
||||
|
||||
|
|
@ -258,17 +258,17 @@ Practice
|
|||
|
||||
Complete Hierarchical Flow (Compact View)
|
||||
Course Category
|
||||
└── Course Sub-category
|
||||
└── Course (with levels: BEGINNER, INTERMEDIATE, ADVANCED)
|
||||
├── Course Video
|
||||
└── Course
|
||||
└── Sub-course (with levels: BEGINNER, INTERMEDIATE, ADVANCED)
|
||||
├── Sub-course Video
|
||||
└── Practice
|
||||
└── Practice Question
|
||||
|
||||
Architectural Observations
|
||||
|
||||
Simple three-level hierarchy: Category → Sub-category → Course
|
||||
Simple three-level hierarchy: Category → Course → Sub-course
|
||||
|
||||
Level is now a property of Course, not a separate entity
|
||||
Level is now a property of sub-course, not a separate entity
|
||||
|
||||
Cascade deletes ensure referential integrity
|
||||
|
||||
|
|
|
|||
|
|
@ -1,316 +0,0 @@
|
|||
-- ======================================================
|
||||
-- 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);
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
-- 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'));
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
-- 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);
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
ALTER TABLE course_categories DROP COLUMN display_order;
|
||||
ALTER TABLE courses DROP COLUMN display_order;
|
||||
ALTER TABLE question_sets DROP COLUMN display_order;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
ALTER TABLE course_categories ADD COLUMN display_order INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE courses ADD COLUMN display_order INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE question_sets ADD COLUMN display_order INT NOT NULL DEFAULT 0;
|
||||
|
|
@ -21,7 +21,7 @@ SELECT
|
|||
is_active,
|
||||
created_at
|
||||
FROM course_categories
|
||||
ORDER BY display_order ASC, created_at DESC
|
||||
ORDER BY created_at DESC
|
||||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
||||
|
|
@ -37,11 +37,3 @@ WHERE id = $3;
|
|||
-- name: DeleteCourseCategory :exec
|
||||
DELETE FROM course_categories
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ReorderCourseCategories :exec
|
||||
UPDATE course_categories
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE course_categories.id = bulk.id;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ SELECT
|
|||
is_active
|
||||
FROM courses
|
||||
WHERE category_id = $1
|
||||
ORDER BY display_order ASC, id ASC
|
||||
ORDER BY id DESC
|
||||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
||||
|
|
@ -48,11 +48,3 @@ WHERE id = $6;
|
|||
-- name: DeleteCourse :exec
|
||||
DELETE FROM courses
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ReorderCourses :exec
|
||||
UPDATE courses
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE courses.id = bulk.id;
|
||||
|
|
|
|||
|
|
@ -9,49 +9,3 @@ 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,
|
||||
(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.display_order ASC, 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;
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
-- 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,7 +21,6 @@ SELECT
|
|||
q.explanation,
|
||||
q.tips,
|
||||
q.voice_prompt,
|
||||
q.image_url,
|
||||
q.status as question_status
|
||||
FROM question_set_items qsi
|
||||
JOIN questions q ON q.id = qsi.question_id
|
||||
|
|
@ -41,8 +40,7 @@ SELECT
|
|||
q.points,
|
||||
q.explanation,
|
||||
q.tips,
|
||||
q.voice_prompt,
|
||||
q.image_url
|
||||
q.voice_prompt
|
||||
FROM question_set_items qsi
|
||||
JOIN questions q ON q.id = qsi.question_id
|
||||
WHERE qsi.set_id = $1
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ FROM question_sets
|
|||
WHERE owner_type = $1
|
||||
AND owner_id = $2
|
||||
AND status != 'ARCHIVED'
|
||||
ORDER BY display_order ASC, created_at DESC;
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: GetQuestionSetsByType :many
|
||||
SELECT
|
||||
|
|
@ -114,11 +114,3 @@ SET
|
|||
sub_course_video_id = $1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2;
|
||||
|
||||
-- name: ReorderQuestionSets :exec
|
||||
UPDATE question_sets
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE question_sets.id = bulk.id;
|
||||
|
|
|
|||
|
|
@ -8,10 +8,9 @@ INSERT INTO questions (
|
|||
tips,
|
||||
voice_prompt,
|
||||
sample_answer_voice_prompt,
|
||||
image_url,
|
||||
status
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, $9, COALESCE($10, 'DRAFT'))
|
||||
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, COALESCE($9, 'DRAFT'))
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetQuestionByID :one
|
||||
|
|
@ -60,10 +59,9 @@ SET
|
|||
tips = COALESCE($6, tips),
|
||||
voice_prompt = COALESCE($7, voice_prompt),
|
||||
sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt),
|
||||
image_url = COALESCE($9, image_url),
|
||||
status = COALESCE($10, status),
|
||||
status = COALESCE($9, status),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $11;
|
||||
WHERE id = $10;
|
||||
|
||||
-- name: ArchiveQuestion :exec
|
||||
UPDATE questions
|
||||
|
|
|
|||
|
|
@ -112,11 +112,3 @@ 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;
|
||||
|
|
|
|||
|
|
@ -80,11 +80,3 @@ RETURNING *;
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -376,13 +376,6 @@ SET
|
|||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2;
|
||||
|
||||
-- name: GetUserSummary :one
|
||||
SELECT
|
||||
COUNT(*) AS total_users,
|
||||
COUNT(*) FILTER (WHERE status = 'ACTIVE') AS active_users,
|
||||
COUNT(*) FILTER (WHERE created_at >= date_trunc('month', CURRENT_DATE)) AS joined_this_month
|
||||
FROM users;
|
||||
|
||||
-- name: UpdateUserKnowledgeLevel :exec
|
||||
UPDATE users
|
||||
SET
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ services:
|
|||
container_name: yimaru-backend-postgres-1
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- "5592:5422"
|
||||
- "5432:5422"
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=secret
|
||||
- POSTGRES_USER=root
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ INSERT INTO course_categories (
|
|||
is_active
|
||||
)
|
||||
VALUES ($1, COALESCE($2, true))
|
||||
RETURNING id, name, is_active, created_at, display_order
|
||||
RETURNING id, name, is_active, created_at
|
||||
`
|
||||
|
||||
type CreateCourseCategoryParams struct {
|
||||
|
|
@ -33,7 +33,6 @@ func (q *Queries) CreateCourseCategory(ctx context.Context, arg CreateCourseCate
|
|||
&i.Name,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.DisplayOrder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -56,7 +55,7 @@ SELECT
|
|||
is_active,
|
||||
created_at
|
||||
FROM course_categories
|
||||
ORDER BY display_order ASC, created_at DESC
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2::INT
|
||||
OFFSET $1::INT
|
||||
`
|
||||
|
|
@ -101,7 +100,7 @@ func (q *Queries) GetAllCourseCategories(ctx context.Context, arg GetAllCourseCa
|
|||
}
|
||||
|
||||
const GetCourseCategoryByID = `-- name: GetCourseCategoryByID :one
|
||||
SELECT id, name, is_active, created_at, display_order
|
||||
SELECT id, name, is_active, created_at
|
||||
FROM course_categories
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -114,30 +113,10 @@ func (q *Queries) GetCourseCategoryByID(ctx context.Context, id int64) (CourseCa
|
|||
&i.Name,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.DisplayOrder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const ReorderCourseCategories = `-- name: ReorderCourseCategories :exec
|
||||
UPDATE course_categories
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE course_categories.id = bulk.id
|
||||
`
|
||||
|
||||
type ReorderCourseCategoriesParams struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Positions []int32 `json:"positions"`
|
||||
}
|
||||
|
||||
func (q *Queries) ReorderCourseCategories(ctx context.Context, arg ReorderCourseCategoriesParams) error {
|
||||
_, err := q.db.Exec(ctx, ReorderCourseCategories, arg.Ids, arg.Positions)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateCourseCategory = `-- name: UpdateCourseCategory :exec
|
||||
UPDATE course_categories
|
||||
SET
|
||||
|
|
|
|||
|
|
@ -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
|
||||
RETURNING id, category_id, title, description, is_active, thumbnail, intro_video_url
|
||||
`
|
||||
|
||||
type CreateCourseParams struct {
|
||||
|
|
@ -51,7 +51,6 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
|
|||
&i.IsActive,
|
||||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -67,7 +66,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
|
||||
SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url
|
||||
FROM courses
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -83,7 +82,6 @@ func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
|
|||
&i.IsActive,
|
||||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -100,7 +98,7 @@ SELECT
|
|||
is_active
|
||||
FROM courses
|
||||
WHERE category_id = $1
|
||||
ORDER BY display_order ASC, id ASC
|
||||
ORDER BY id DESC
|
||||
LIMIT $3::INT
|
||||
OFFSET $2::INT
|
||||
`
|
||||
|
|
@ -151,25 +149,6 @@ func (q *Queries) GetCoursesByCategory(ctx context.Context, arg GetCoursesByCate
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const ReorderCourses = `-- name: ReorderCourses :exec
|
||||
UPDATE courses
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE courses.id = bulk.id
|
||||
`
|
||||
|
||||
type ReorderCoursesParams struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Positions []int32 `json:"positions"`
|
||||
}
|
||||
|
||||
func (q *Queries) ReorderCourses(ctx context.Context, arg ReorderCoursesParams) error {
|
||||
_, err := q.db.Exec(ctx, ReorderCourses, arg.Ids, arg.Positions)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateCourse = `-- name: UpdateCourse :exec
|
||||
UPDATE courses
|
||||
SET
|
||||
|
|
|
|||
|
|
@ -11,87 +11,6 @@ import (
|
|||
"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
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
|
|
@ -138,134 +57,3 @@ func (q *Queries) GetFullLearningTree(ctx context.Context) ([]GetFullLearningTre
|
|||
}
|
||||
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.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"`
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ type Course struct {
|
|||
IsActive bool `json:"is_active"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
type CourseCategory struct {
|
||||
|
|
@ -38,7 +37,6 @@ type CourseCategory struct {
|
|||
Name string `json:"name"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
|
|
@ -137,14 +135,6 @@ type Question struct {
|
|||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_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 {
|
||||
|
|
@ -172,7 +162,6 @@ type QuestionSet struct {
|
|||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
type QuestionSetItem struct {
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
// 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,8 +68,7 @@ SELECT
|
|||
q.points,
|
||||
q.explanation,
|
||||
q.tips,
|
||||
q.voice_prompt,
|
||||
q.image_url
|
||||
q.voice_prompt
|
||||
FROM question_set_items qsi
|
||||
JOIN questions q ON q.id = qsi.question_id
|
||||
WHERE qsi.set_id = $1
|
||||
|
|
@ -89,7 +88,6 @@ type GetPublishedQuestionsInSetRow struct {
|
|||
Explanation pgtype.Text `json:"explanation"`
|
||||
Tips pgtype.Text `json:"tips"`
|
||||
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
||||
ImageUrl pgtype.Text `json:"image_url"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]GetPublishedQuestionsInSetRow, error) {
|
||||
|
|
@ -113,7 +111,6 @@ func (q *Queries) GetPublishedQuestionsInSet(ctx context.Context, setID int64) (
|
|||
&i.Explanation,
|
||||
&i.Tips,
|
||||
&i.VoicePrompt,
|
||||
&i.ImageUrl,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -138,7 +135,6 @@ SELECT
|
|||
q.explanation,
|
||||
q.tips,
|
||||
q.voice_prompt,
|
||||
q.image_url,
|
||||
q.status as question_status
|
||||
FROM question_set_items qsi
|
||||
JOIN questions q ON q.id = qsi.question_id
|
||||
|
|
@ -159,7 +155,6 @@ type GetQuestionSetItemsRow struct {
|
|||
Explanation pgtype.Text `json:"explanation"`
|
||||
Tips pgtype.Text `json:"tips"`
|
||||
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
||||
ImageUrl pgtype.Text `json:"image_url"`
|
||||
QuestionStatus string `json:"question_status"`
|
||||
}
|
||||
|
||||
|
|
@ -184,7 +179,6 @@ func (q *Queries) GetQuestionSetItems(ctx context.Context, setID int64) ([]GetQu
|
|||
&i.Explanation,
|
||||
&i.Tips,
|
||||
&i.VoicePrompt,
|
||||
&i.ImageUrl,
|
||||
&i.QuestionStatus,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -198,7 +192,7 @@ func (q *Queries) GetQuestionSetItems(ctx context.Context, setID int64) ([]GetQu
|
|||
}
|
||||
|
||||
const GetQuestionSetsContainingQuestion = `-- name: GetQuestionSetsContainingQuestion :many
|
||||
SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order
|
||||
SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id
|
||||
FROM question_sets qs
|
||||
JOIN question_set_items qsi ON qsi.set_id = qs.id
|
||||
WHERE qsi.question_id = $1
|
||||
|
|
@ -230,7 +224,6 @@ func (q *Queries) GetQuestionSetsContainingQuestion(ctx context.Context, questio
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ INSERT INTO question_sets (
|
|||
sub_course_video_id
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12)
|
||||
RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
|
||||
`
|
||||
|
||||
type CreateQuestionSetParams struct {
|
||||
|
|
@ -117,7 +117,6 @@ func (q *Queries) CreateQuestionSet(ctx context.Context, arg CreateQuestionSetPa
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -133,7 +132,7 @@ func (q *Queries) DeleteQuestionSet(ctx context.Context, id int64) error {
|
|||
}
|
||||
|
||||
const GetInitialAssessmentSet = `-- name: GetInitialAssessmentSet :one
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
|
||||
FROM question_sets
|
||||
WHERE set_type = 'INITIAL_ASSESSMENT'
|
||||
AND status = 'PUBLISHED'
|
||||
|
|
@ -160,13 +159,12 @@ func (q *Queries) GetInitialAssessmentSet(ctx context.Context) (QuestionSet, err
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetPublishedQuestionSetsByOwner = `-- name: GetPublishedQuestionSetsByOwner :many
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
|
||||
FROM question_sets
|
||||
WHERE owner_type = $1
|
||||
AND owner_id = $2
|
||||
|
|
@ -204,7 +202,6 @@ func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPu
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -217,7 +214,7 @@ func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPu
|
|||
}
|
||||
|
||||
const GetQuestionSetByID = `-- name: GetQuestionSetByID :one
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
|
||||
FROM question_sets
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -241,18 +238,17 @@ func (q *Queries) GetQuestionSetByID(ctx context.Context, id int64) (QuestionSet
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetQuestionSetsByOwner = `-- name: GetQuestionSetsByOwner :many
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
|
||||
FROM question_sets
|
||||
WHERE owner_type = $1
|
||||
AND owner_id = $2
|
||||
AND status != 'ARCHIVED'
|
||||
ORDER BY display_order ASC, created_at DESC
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
type GetQuestionSetsByOwnerParams struct {
|
||||
|
|
@ -285,7 +281,6 @@ func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSet
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -300,7 +295,7 @@ func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSet
|
|||
const GetQuestionSetsByType = `-- name: GetQuestionSetsByType :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order
|
||||
qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id
|
||||
FROM question_sets qs
|
||||
WHERE set_type = $1
|
||||
AND status != 'ARCHIVED'
|
||||
|
|
@ -332,7 +327,6 @@ type GetQuestionSetsByTypeRow struct {
|
|||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSetsByTypeParams) ([]GetQuestionSetsByTypeRow, error) {
|
||||
|
|
@ -361,7 +355,6 @@ func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSets
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -442,25 +435,6 @@ func (q *Queries) RemoveUserPersonaFromQuestionSet(ctx context.Context, arg Remo
|
|||
return err
|
||||
}
|
||||
|
||||
const ReorderQuestionSets = `-- name: ReorderQuestionSets :exec
|
||||
UPDATE question_sets
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE question_sets.id = bulk.id
|
||||
`
|
||||
|
||||
type ReorderQuestionSetsParams struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Positions []int32 `json:"positions"`
|
||||
}
|
||||
|
||||
func (q *Queries) ReorderQuestionSets(ctx context.Context, arg ReorderQuestionSetsParams) error {
|
||||
_, err := q.db.Exec(ctx, ReorderQuestionSets, arg.Ids, arg.Positions)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateQuestionSet = `-- name: UpdateQuestionSet :exec
|
||||
UPDATE question_sets
|
||||
SET
|
||||
|
|
|
|||
|
|
@ -32,11 +32,10 @@ INSERT INTO questions (
|
|||
tips,
|
||||
voice_prompt,
|
||||
sample_answer_voice_prompt,
|
||||
image_url,
|
||||
status
|
||||
)
|
||||
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, image_url
|
||||
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, COALESCE($9, 'DRAFT'))
|
||||
RETURNING id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateQuestionParams struct {
|
||||
|
|
@ -48,8 +47,7 @@ type CreateQuestionParams struct {
|
|||
Tips pgtype.Text `json:"tips"`
|
||||
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
||||
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
|
||||
ImageUrl pgtype.Text `json:"image_url"`
|
||||
Column10 interface{} `json:"column_10"`
|
||||
Column9 interface{} `json:"column_9"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams) (Question, error) {
|
||||
|
|
@ -62,8 +60,7 @@ func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams)
|
|||
arg.Tips,
|
||||
arg.VoicePrompt,
|
||||
arg.SampleAnswerVoicePrompt,
|
||||
arg.ImageUrl,
|
||||
arg.Column10,
|
||||
arg.Column9,
|
||||
)
|
||||
var i Question
|
||||
err := row.Scan(
|
||||
|
|
@ -79,7 +76,6 @@ func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams)
|
|||
&i.Status,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ImageUrl,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -95,7 +91,7 @@ func (q *Queries) DeleteQuestion(ctx context.Context, id int64) error {
|
|||
}
|
||||
|
||||
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, image_url
|
||||
SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at
|
||||
FROM questions
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -116,7 +112,6 @@ func (q *Queries) GetQuestionByID(ctx context.Context, id int64) (Question, erro
|
|||
&i.Status,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ImageUrl,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -193,7 +188,7 @@ func (q *Queries) GetQuestionWithOptions(ctx context.Context, id int64) ([]GetQu
|
|||
}
|
||||
|
||||
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, image_url
|
||||
SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at
|
||||
FROM questions
|
||||
WHERE id = ANY($1::BIGINT[])
|
||||
ORDER BY id
|
||||
|
|
@ -221,7 +216,6 @@ func (q *Queries) GetQuestionsByIDs(ctx context.Context, dollar_1 []int64) ([]Qu
|
|||
&i.Status,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ImageUrl,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -236,7 +230,7 @@ func (q *Queries) GetQuestionsByIDs(ctx context.Context, dollar_1 []int64) ([]Qu
|
|||
const ListQuestions = `-- name: ListQuestions :many
|
||||
SELECT
|
||||
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.image_url
|
||||
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
|
||||
FROM questions q
|
||||
WHERE status != 'ARCHIVED'
|
||||
AND ($1::VARCHAR IS NULL OR $1 = '' OR question_type = $1)
|
||||
|
|
@ -269,7 +263,6 @@ type ListQuestionsRow struct {
|
|||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
ImageUrl pgtype.Text `json:"image_url"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([]ListQuestionsRow, error) {
|
||||
|
|
@ -301,7 +294,6 @@ func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([
|
|||
&i.Status,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ImageUrl,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -316,7 +308,7 @@ func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([
|
|||
const SearchQuestions = `-- name: SearchQuestions :many
|
||||
SELECT
|
||||
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.image_url
|
||||
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
|
||||
FROM questions q
|
||||
WHERE status != 'ARCHIVED'
|
||||
AND question_text ILIKE '%' || $1 || '%'
|
||||
|
|
@ -345,7 +337,6 @@ type SearchQuestionsRow struct {
|
|||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
ImageUrl pgtype.Text `json:"image_url"`
|
||||
}
|
||||
|
||||
func (q *Queries) SearchQuestions(ctx context.Context, arg SearchQuestionsParams) ([]SearchQuestionsRow, error) {
|
||||
|
|
@ -371,7 +362,6 @@ func (q *Queries) SearchQuestions(ctx context.Context, arg SearchQuestionsParams
|
|||
&i.Status,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ImageUrl,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -394,10 +384,9 @@ SET
|
|||
tips = COALESCE($6, tips),
|
||||
voice_prompt = COALESCE($7, voice_prompt),
|
||||
sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt),
|
||||
image_url = COALESCE($9, image_url),
|
||||
status = COALESCE($10, status),
|
||||
status = COALESCE($9, status),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $11
|
||||
WHERE id = $10
|
||||
`
|
||||
|
||||
type UpdateQuestionParams struct {
|
||||
|
|
@ -409,7 +398,6 @@ type UpdateQuestionParams struct {
|
|||
Tips pgtype.Text `json:"tips"`
|
||||
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
||||
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
|
||||
ImageUrl pgtype.Text `json:"image_url"`
|
||||
Status string `json:"status"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
|
@ -424,7 +412,6 @@ func (q *Queries) UpdateQuestion(ctx context.Context, arg UpdateQuestionParams)
|
|||
arg.Tips,
|
||||
arg.VoicePrompt,
|
||||
arg.SampleAnswerVoicePrompt,
|
||||
arg.ImageUrl,
|
||||
arg.Status,
|
||||
arg.ID,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -345,25 +345,6 @@ func (q *Queries) PublishSubCourseVideo(ctx context.Context, id int64) error {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -261,25 +261,6 @@ func (q *Queries) ListSubCoursesByCourse(ctx context.Context, courseID int64) ([
|
|||
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
|
||||
|
|
|
|||
|
|
@ -764,27 +764,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
|||
return i, err
|
||||
}
|
||||
|
||||
const GetUserSummary = `-- name: GetUserSummary :one
|
||||
SELECT
|
||||
COUNT(*) AS total_users,
|
||||
COUNT(*) FILTER (WHERE status = 'ACTIVE') AS active_users,
|
||||
COUNT(*) FILTER (WHERE created_at >= date_trunc('month', CURRENT_DATE)) AS joined_this_month
|
||||
FROM users
|
||||
`
|
||||
|
||||
type GetUserSummaryRow struct {
|
||||
TotalUsers int64 `json:"total_users"`
|
||||
ActiveUsers int64 `json:"active_users"`
|
||||
JoinedThisMonth int64 `json:"joined_this_month"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserSummary(ctx context.Context) (GetUserSummaryRow, error) {
|
||||
row := q.db.QueryRow(ctx, GetUserSummary)
|
||||
var i GetUserSummaryRow
|
||||
err := row.Scan(&i.TotalUsers, &i.ActiveUsers, &i.JoinedThisMonth)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const IsUserNameUnique = `-- name: IsUserNameUnique :one
|
||||
SELECT
|
||||
CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique
|
||||
|
|
|
|||
|
|
@ -87,58 +87,3 @@ const (
|
|||
VideoHostProviderDirect VideoHostProvider = "DIRECT"
|
||||
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,7 +8,6 @@ const (
|
|||
QuestionTypeMCQ QuestionType = "MCQ"
|
||||
QuestionTypeTrueFalse QuestionType = "TRUE_FALSE"
|
||||
QuestionTypeShortAnswer QuestionType = "SHORT_ANSWER"
|
||||
QuestionTypeAudio QuestionType = "AUDIO"
|
||||
)
|
||||
|
||||
type DifficultyLevel string
|
||||
|
|
@ -47,24 +46,15 @@ type Question struct {
|
|||
Tips *string
|
||||
VoicePrompt *string
|
||||
SampleAnswerVoicePrompt *string
|
||||
ImageURL *string
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
}
|
||||
|
||||
type QuestionAudioAnswer struct {
|
||||
ID int64
|
||||
QuestionID int64
|
||||
CorrectAnswerText string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type QuestionWithDetails struct {
|
||||
Question
|
||||
Options []QuestionOption
|
||||
ShortAnswers []QuestionShortAnswer
|
||||
AudioAnswer *QuestionAudioAnswer
|
||||
}
|
||||
|
||||
type QuestionOption struct {
|
||||
|
|
@ -120,7 +110,6 @@ type QuestionSetItemWithQuestion struct {
|
|||
Explanation *string
|
||||
Tips *string
|
||||
VoicePrompt *string
|
||||
ImageURL *string
|
||||
QuestionStatus string
|
||||
}
|
||||
|
||||
|
|
@ -133,11 +122,9 @@ type CreateQuestionInput struct {
|
|||
Tips *string
|
||||
VoicePrompt *string
|
||||
SampleAnswerVoicePrompt *string
|
||||
ImageURL *string
|
||||
Status *string
|
||||
Options []CreateQuestionOptionInput
|
||||
ShortAnswers []CreateShortAnswerInput
|
||||
AudioCorrectAnswerText *string
|
||||
}
|
||||
|
||||
type CreateQuestionOptionInput struct {
|
||||
|
|
|
|||
|
|
@ -181,12 +181,6 @@ type UpdateUserStatusReq struct {
|
|||
UserID int64
|
||||
}
|
||||
|
||||
type UserSummary struct {
|
||||
TotalUsers int64 `json:"total_users"`
|
||||
ActiveUsers int64 `json:"active_users"`
|
||||
JoinedThisMonth int64 `json:"joined_this_month"`
|
||||
}
|
||||
|
||||
type UpdateUserReq struct {
|
||||
UserID int64 `json:"-"`
|
||||
|
||||
|
|
|
|||
|
|
@ -172,16 +172,6 @@ type CourseStore interface {
|
|||
|
||||
// Learning Tree
|
||||
GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error)
|
||||
|
||||
// Learning Path (full nested structure for a course)
|
||||
GetCourseLearningPath(ctx context.Context, courseID int64) (domain.LearningPath, error)
|
||||
|
||||
// Reorder (drag-and-drop support)
|
||||
ReorderCourseCategories(ctx context.Context, ids []int64, positions []int32) error
|
||||
ReorderCourses(ctx context.Context, ids []int64, positions []int32) error
|
||||
ReorderSubCourses(ctx context.Context, ids []int64, positions []int32) error
|
||||
ReorderSubCourseVideos(ctx context.Context, ids []int64, positions []int32) error
|
||||
ReorderQuestionSets(ctx context.Context, ids []int64, positions []int32) error
|
||||
}
|
||||
|
||||
type ProgressionStore interface {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ type UserStore interface {
|
|||
limit, offset int32,
|
||||
) ([]domain.User, int64, error)
|
||||
GetTotalUsers(ctx context.Context, role *string) (int64, error)
|
||||
GetUserSummary(ctx context.Context) (domain.UserSummary, error)
|
||||
SearchUserByNameOrPhone(
|
||||
ctx context.Context,
|
||||
search string,
|
||||
|
|
|
|||
|
|
@ -119,10 +119,3 @@ func (s *Store) DeleteCourseCategory(
|
|||
|
||||
return s.queries.DeleteCourseCategory(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Store) ReorderCourseCategories(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.queries.ReorderCourseCategories(ctx, dbgen.ReorderCourseCategoriesParams{
|
||||
Ids: ids,
|
||||
Positions: positions,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,13 +158,6 @@ func mapCourse(row dbgen.Course) domain.Course {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Store) ReorderCourses(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.queries.ReorderCourses(ctx, dbgen.ReorderCoursesParams{
|
||||
Ids: ids,
|
||||
Positions: positions,
|
||||
})
|
||||
}
|
||||
|
||||
func ptrText(t pgtype.Text) *string {
|
||||
if t.Valid {
|
||||
return &t.String
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@ 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) {
|
||||
|
|
@ -44,115 +41,3 @@ func (s *Store) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, e
|
|||
|
||||
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,7 +70,6 @@ func questionToDomain(q dbgen.Question) domain.Question {
|
|||
Tips: fromPgText(q.Tips),
|
||||
VoicePrompt: fromPgText(q.VoicePrompt),
|
||||
SampleAnswerVoicePrompt: fromPgText(q.SampleAnswerVoicePrompt),
|
||||
ImageURL: fromPgText(q.ImageUrl),
|
||||
Status: q.Status,
|
||||
CreatedAt: q.CreatedAt.Time,
|
||||
UpdatedAt: timePtr(q.UpdatedAt),
|
||||
|
|
@ -98,15 +97,6 @@ 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 {
|
||||
return domain.QuestionSet{
|
||||
ID: qs.ID,
|
||||
|
|
@ -162,8 +152,7 @@ func (s *Store) CreateQuestion(ctx context.Context, input domain.CreateQuestionI
|
|||
Tips: toPgText(input.Tips),
|
||||
VoicePrompt: toPgText(input.VoicePrompt),
|
||||
SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt),
|
||||
ImageUrl: toPgText(input.ImageURL),
|
||||
Column10: status,
|
||||
Column9: status,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.Question{}, err
|
||||
|
|
@ -200,16 +189,6 @@ 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 {
|
||||
return domain.Question{}, err
|
||||
}
|
||||
|
|
@ -251,18 +230,10 @@ func (s *Store) GetQuestionWithDetails(ctx context.Context, id int64) (domain.Qu
|
|||
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{
|
||||
Question: questionToDomain(q),
|
||||
Options: options,
|
||||
ShortAnswers: answers,
|
||||
AudioAnswer: audioAnswer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -305,7 +276,6 @@ func (s *Store) ListQuestions(ctx context.Context, questionType, difficulty, sta
|
|||
Tips: fromPgText(r.Tips),
|
||||
VoicePrompt: fromPgText(r.VoicePrompt),
|
||||
SampleAnswerVoicePrompt: fromPgText(r.SampleAnswerVoicePrompt),
|
||||
ImageURL: fromPgText(r.ImageUrl),
|
||||
Status: r.Status,
|
||||
CreatedAt: r.CreatedAt.Time,
|
||||
UpdatedAt: timePtr(r.UpdatedAt),
|
||||
|
|
@ -341,7 +311,6 @@ func (s *Store) SearchQuestions(ctx context.Context, query string, limit, offset
|
|||
Tips: fromPgText(r.Tips),
|
||||
VoicePrompt: fromPgText(r.VoicePrompt),
|
||||
SampleAnswerVoicePrompt: fromPgText(r.SampleAnswerVoicePrompt),
|
||||
ImageURL: fromPgText(r.ImageUrl),
|
||||
Status: r.Status,
|
||||
CreatedAt: r.CreatedAt.Time,
|
||||
UpdatedAt: timePtr(r.UpdatedAt),
|
||||
|
|
@ -361,7 +330,7 @@ func (s *Store) UpdateQuestion(ctx context.Context, id int64, input domain.Creat
|
|||
status = *input.Status
|
||||
}
|
||||
|
||||
err := s.queries.UpdateQuestion(ctx, dbgen.UpdateQuestionParams{
|
||||
return s.queries.UpdateQuestion(ctx, dbgen.UpdateQuestionParams{
|
||||
ID: id,
|
||||
QuestionText: input.QuestionText,
|
||||
QuestionType: input.QuestionType,
|
||||
|
|
@ -371,27 +340,8 @@ func (s *Store) UpdateQuestion(ctx context.Context, id int64, input domain.Creat
|
|||
Tips: toPgText(input.Tips),
|
||||
VoicePrompt: toPgText(input.VoicePrompt),
|
||||
SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt),
|
||||
ImageUrl: toPgText(input.ImageURL),
|
||||
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 {
|
||||
|
|
@ -703,7 +653,6 @@ func (s *Store) GetQuestionSetItems(ctx context.Context, setID int64) ([]domain.
|
|||
Explanation: fromPgText(r.Explanation),
|
||||
Tips: fromPgText(r.Tips),
|
||||
VoicePrompt: fromPgText(r.VoicePrompt),
|
||||
ImageURL: fromPgText(r.ImageUrl),
|
||||
QuestionStatus: r.QuestionStatus,
|
||||
}
|
||||
}
|
||||
|
|
@ -732,7 +681,6 @@ func (s *Store) GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]
|
|||
Explanation: fromPgText(r.Explanation),
|
||||
Tips: fromPgText(r.Tips),
|
||||
VoicePrompt: fromPgText(r.VoicePrompt),
|
||||
ImageURL: fromPgText(r.ImageUrl),
|
||||
QuestionStatus: "PUBLISHED",
|
||||
}
|
||||
}
|
||||
|
|
@ -809,10 +757,3 @@ func (s *Store) GetUserPersonasByQuestionSetID(ctx context.Context, questionSetI
|
|||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) ReorderQuestionSets(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.queries.ReorderQuestionSets(ctx, dbgen.ReorderQuestionSetsParams{
|
||||
Ids: ids,
|
||||
Positions: positions,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,10 +283,3 @@ func (s *Store) GetVideoByVimeoID(ctx context.Context, vimeoID string) (domain.S
|
|||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,11 +233,4 @@ func (s *Store) DeleteSubCourse(
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) ReorderSubCourses(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.queries.ReorderSubCourses(ctx, dbgen.ReorderSubCoursesParams{
|
||||
Ids: ids,
|
||||
Positions: positions,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -515,19 +515,6 @@ func (s *Store) GetAllUsers(
|
|||
}
|
||||
|
||||
// GetTotalUsers counts users with optional filters
|
||||
func (s *Store) GetUserSummary(ctx context.Context) (domain.UserSummary, error) {
|
||||
res, err := s.queries.GetUserSummary(ctx)
|
||||
if err != nil {
|
||||
return domain.UserSummary{}, err
|
||||
}
|
||||
|
||||
return domain.UserSummary{
|
||||
TotalUsers: res.TotalUsers,
|
||||
ActiveUsers: res.ActiveUsers,
|
||||
JoinedThisMonth: res.JoinedThisMonth,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetTotalUsers(ctx context.Context, role *string) (int64, error) {
|
||||
count, err := s.queries.GetTotalUsers(ctx, *role)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -8,27 +8,3 @@ import (
|
|||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ var AllPermissions = []domain.PermissionSeed{
|
|||
{Key: "course_categories.get", Name: "Get Course Category", Description: "Get a course category by ID", GroupName: "Course Categories"},
|
||||
{Key: "course_categories.update", Name: "Update Course Category", Description: "Update a course category", GroupName: "Course Categories"},
|
||||
{Key: "course_categories.delete", Name: "Delete Course Category", Description: "Delete a course category", GroupName: "Course Categories"},
|
||||
{Key: "course_categories.reorder", Name: "Reorder Course Categories", Description: "Reorder course categories", GroupName: "Course Categories"},
|
||||
|
||||
// Course Management - Courses
|
||||
{Key: "courses.create", Name: "Create Course", Description: "Create a new course", GroupName: "Courses"},
|
||||
|
|
@ -18,7 +17,6 @@ var AllPermissions = []domain.PermissionSeed{
|
|||
{Key: "courses.update", Name: "Update Course", Description: "Update a course", GroupName: "Courses"},
|
||||
{Key: "courses.upload_thumbnail", Name: "Upload Course Thumbnail", Description: "Upload course thumbnail image", GroupName: "Courses"},
|
||||
{Key: "courses.delete", Name: "Delete Course", Description: "Delete a course", GroupName: "Courses"},
|
||||
{Key: "courses.reorder", Name: "Reorder Courses", Description: "Reorder courses", GroupName: "Courses"},
|
||||
|
||||
// Course Management - Sub-courses
|
||||
{Key: "subcourses.create", Name: "Create Sub-course", Description: "Create a new sub-course", GroupName: "Sub-courses"},
|
||||
|
|
@ -30,7 +28,6 @@ var AllPermissions = []domain.PermissionSeed{
|
|||
{Key: "subcourses.upload_thumbnail", Name: "Upload Sub-course Thumbnail", Description: "Upload sub-course thumbnail", GroupName: "Sub-courses"},
|
||||
{Key: "subcourses.deactivate", Name: "Deactivate Sub-course", Description: "Deactivate a sub-course", GroupName: "Sub-courses"},
|
||||
{Key: "subcourses.delete", Name: "Delete Sub-course", Description: "Delete a sub-course", GroupName: "Sub-courses"},
|
||||
{Key: "subcourses.reorder", Name: "Reorder Sub-courses", Description: "Reorder sub-courses", GroupName: "Sub-courses"},
|
||||
|
||||
// Course Management - Videos
|
||||
{Key: "videos.create", Name: "Create Video", Description: "Create a sub-course video", GroupName: "Videos"},
|
||||
|
|
@ -43,11 +40,9 @@ var AllPermissions = []domain.PermissionSeed{
|
|||
{Key: "videos.publish", Name: "Publish Video", Description: "Publish a video", GroupName: "Videos"},
|
||||
{Key: "videos.update", Name: "Update Video", Description: "Update a video", GroupName: "Videos"},
|
||||
{Key: "videos.delete", Name: "Delete Video", Description: "Delete a video", GroupName: "Videos"},
|
||||
{Key: "videos.reorder", Name: "Reorder Videos", Description: "Reorder videos", GroupName: "Videos"},
|
||||
|
||||
// Learning Tree
|
||||
{Key: "learning_tree.get", Name: "Get Learning Tree", Description: "Get full learning tree", GroupName: "Learning Tree"},
|
||||
{Key: "practices.reorder", Name: "Reorder Practices", Description: "Reorder practices", GroupName: "Learning Tree"},
|
||||
|
||||
// Questions
|
||||
{Key: "questions.create", Name: "Create Question", Description: "Create a new question", GroupName: "Questions"},
|
||||
|
|
@ -227,13 +222,13 @@ var AllPermissions = []domain.PermissionSeed{
|
|||
var DefaultRolePermissions = map[string][]string{
|
||||
"ADMIN": {
|
||||
// Course Management (full access)
|
||||
"course_categories.create", "course_categories.list", "course_categories.get", "course_categories.update", "course_categories.delete", "course_categories.reorder",
|
||||
"courses.create", "courses.get", "courses.list_by_category", "courses.update", "courses.upload_thumbnail", "courses.delete", "courses.reorder",
|
||||
"course_categories.create", "course_categories.list", "course_categories.get", "course_categories.update", "course_categories.delete",
|
||||
"courses.create", "courses.get", "courses.list_by_category", "courses.update", "courses.upload_thumbnail", "courses.delete",
|
||||
"subcourses.create", "subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
|
||||
"subcourses.update", "subcourses.upload_thumbnail", "subcourses.deactivate", "subcourses.delete", "subcourses.reorder",
|
||||
"subcourses.update", "subcourses.upload_thumbnail", "subcourses.deactivate", "subcourses.delete",
|
||||
"videos.create", "videos.create_vimeo", "videos.upload", "videos.import_vimeo", "videos.get",
|
||||
"videos.list_by_subcourse", "videos.list_published", "videos.publish", "videos.update", "videos.delete", "videos.reorder",
|
||||
"learning_tree.get", "practices.reorder",
|
||||
"videos.list_by_subcourse", "videos.list_published", "videos.publish", "videos.update", "videos.delete",
|
||||
"learning_tree.get",
|
||||
|
||||
// Questions (full access)
|
||||
"questions.create", "questions.list", "questions.search", "questions.get", "questions.update", "questions.delete",
|
||||
|
|
|
|||
|
|
@ -91,10 +91,6 @@ func (s *Service) GetAllUsers(
|
|||
)
|
||||
}
|
||||
|
||||
func (s *Service) GetUserSummary(ctx context.Context) (domain.UserSummary, error) {
|
||||
return s.userStore.GetUserSummary(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateUserStatus(ctx context.Context, req domain.UpdateUserStatusReq) error {
|
||||
return s.userStore.UpdateUserStatus(ctx, req)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,19 +195,3 @@ func (s *Service) GetOEmbed(ctx context.Context, vimeoURL string, width, height
|
|||
func (s *Service) GeneratePlayerURL(videoID string, opts *vimeo.EmbedOptions) string {
|
||||
return vimeo.GenerateEmbedURL(videoID, opts)
|
||||
}
|
||||
|
||||
// GetSampleVideo fetches a public Vimeo video by ID and returns its info along with an embeddable iframe.
|
||||
func (s *Service) GetSampleVideo(ctx context.Context, videoID string, width, height int) (*VideoInfo, string, error) {
|
||||
info, err := s.GetVideoInfo(ctx, videoID)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to get sample video: %w", err)
|
||||
}
|
||||
|
||||
iframe := vimeo.GenerateIframeEmbed(videoID, width, height, &vimeo.EmbedOptions{
|
||||
Title: true,
|
||||
Byline: true,
|
||||
Portrait: true,
|
||||
})
|
||||
|
||||
return info, iframe, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1420,292 +1420,6 @@ 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,
|
||||
})
|
||||
}
|
||||
|
||||
// Reorder Handlers — support drag-and-drop ordering from admin panel
|
||||
|
||||
type reorderItem struct {
|
||||
ID int64 `json:"id" validate:"required"`
|
||||
Position int32 `json:"position"`
|
||||
}
|
||||
|
||||
type reorderReq struct {
|
||||
Items []reorderItem `json:"items" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
func parseReorderItems(items []reorderItem) ([]int64, []int32) {
|
||||
ids := make([]int64, len(items))
|
||||
positions := make([]int32, len(items))
|
||||
for i, item := range items {
|
||||
ids[i] = item.ID
|
||||
positions[i] = item.Position
|
||||
}
|
||||
return ids, positions
|
||||
}
|
||||
|
||||
// ReorderCourseCategories godoc
|
||||
// @Summary Reorder course categories
|
||||
// @Description Updates the display_order of course categories for drag-and-drop sorting
|
||||
// @Tags course-categories
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body reorderReq true "Reorder payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/categories/reorder [put]
|
||||
func (h *Handler) ReorderCourseCategories(c *fiber.Ctx) error {
|
||||
var req reorderReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if len(req.Items) == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Items array is required",
|
||||
Error: "items must not be empty",
|
||||
})
|
||||
}
|
||||
|
||||
ids, positions := parseReorderItems(req.Items)
|
||||
if err := h.courseMgmtSvc.ReorderCourseCategories(c.Context(), ids, positions); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to reorder course categories",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCategoryUpdated, domain.ResourceCategory, nil, "Reordered course categories", meta, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Course categories reordered successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ReorderCourses godoc
|
||||
// @Summary Reorder courses within a category
|
||||
// @Description Updates the display_order of courses for drag-and-drop sorting
|
||||
// @Tags courses
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body reorderReq true "Reorder payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/courses/reorder [put]
|
||||
func (h *Handler) ReorderCourses(c *fiber.Ctx) error {
|
||||
var req reorderReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if len(req.Items) == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Items array is required",
|
||||
Error: "items must not be empty",
|
||||
})
|
||||
}
|
||||
|
||||
ids, positions := parseReorderItems(req.Items)
|
||||
if err := h.courseMgmtSvc.ReorderCourses(c.Context(), ids, positions); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to reorder courses",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCourseUpdated, domain.ResourceCourse, nil, "Reordered courses", meta, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Courses reordered successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ReorderSubCourses godoc
|
||||
// @Summary Reorder sub-courses within a course
|
||||
// @Description Updates the display_order of sub-courses for drag-and-drop sorting
|
||||
// @Tags sub-courses
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body reorderReq true "Reorder payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-courses/reorder [put]
|
||||
func (h *Handler) ReorderSubCourses(c *fiber.Ctx) error {
|
||||
var req reorderReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if len(req.Items) == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Items array is required",
|
||||
Error: "items must not be empty",
|
||||
})
|
||||
}
|
||||
|
||||
ids, positions := parseReorderItems(req.Items)
|
||||
if err := h.courseMgmtSvc.ReorderSubCourses(c.Context(), ids, positions); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to reorder sub-courses",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionSubCourseUpdated, domain.ResourceSubCourse, nil, "Reordered sub-courses", meta, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Sub-courses reordered successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ReorderSubCourseVideos godoc
|
||||
// @Summary Reorder videos within a sub-course
|
||||
// @Description Updates the display_order of videos for drag-and-drop sorting
|
||||
// @Tags sub-course-videos
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body reorderReq true "Reorder payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/videos/reorder [put]
|
||||
func (h *Handler) ReorderSubCourseVideos(c *fiber.Ctx) error {
|
||||
var req reorderReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if len(req.Items) == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Items array is required",
|
||||
Error: "items must not be empty",
|
||||
})
|
||||
}
|
||||
|
||||
ids, positions := parseReorderItems(req.Items)
|
||||
if err := h.courseMgmtSvc.ReorderSubCourseVideos(c.Context(), ids, positions); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to reorder videos",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionVideoUpdated, domain.ResourceVideo, nil, "Reordered sub-course videos", meta, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Videos reordered successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ReorderPractices godoc
|
||||
// @Summary Reorder practices (question sets) within a sub-course
|
||||
// @Description Updates the display_order of practices for drag-and-drop sorting
|
||||
// @Tags question-sets
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body reorderReq true "Reorder payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/practices/reorder [put]
|
||||
func (h *Handler) ReorderPractices(c *fiber.Ctx) error {
|
||||
var req reorderReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if len(req.Items) == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Items array is required",
|
||||
Error: "items must not be empty",
|
||||
})
|
||||
}
|
||||
|
||||
ids, positions := parseReorderItems(req.Items)
|
||||
if err := h.courseMgmtSvc.ReorderQuestionSets(c.Context(), ids, positions); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to reorder practices",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
meta, _ := json.Marshal(map[string]interface{}{"ids": ids, "positions": positions})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, nil, "Reordered practices", meta, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Practices reordered successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// UploadSubCourseVideo godoc
|
||||
// @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
|
||||
|
|
|
|||
|
|
@ -25,18 +25,16 @@ type shortAnswerInput struct {
|
|||
|
||||
type createQuestionReq struct {
|
||||
QuestionText string `json:"question_text" validate:"required"`
|
||||
QuestionType string `json:"question_type" validate:"required,oneof=MCQ TRUE_FALSE SHORT_ANSWER AUDIO"`
|
||||
QuestionType string `json:"question_type" validate:"required,oneof=MCQ TRUE_FALSE SHORT_ANSWER"`
|
||||
DifficultyLevel *string `json:"difficulty_level"`
|
||||
Points *int32 `json:"points"`
|
||||
Explanation *string `json:"explanation"`
|
||||
Tips *string `json:"tips"`
|
||||
VoicePrompt *string `json:"voice_prompt"`
|
||||
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"`
|
||||
ImageURL *string `json:"image_url"`
|
||||
Status *string `json:"status"`
|
||||
Options []optionInput `json:"options"`
|
||||
ShortAnswers []shortAnswerInput `json:"short_answers"`
|
||||
AudioCorrectAnswerText *string `json:"audio_correct_answer_text"`
|
||||
}
|
||||
|
||||
type optionRes struct {
|
||||
|
|
@ -62,12 +60,10 @@ type questionRes struct {
|
|||
Tips *string `json:"tips,omitempty"`
|
||||
VoicePrompt *string `json:"voice_prompt,omitempty"`
|
||||
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt,omitempty"`
|
||||
ImageURL *string `json:"image_url,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Options []optionRes `json:"options,omitempty"`
|
||||
ShortAnswers []shortAnswerRes `json:"short_answers,omitempty"`
|
||||
AudioCorrectAnswerText *string `json:"audio_correct_answer_text,omitempty"`
|
||||
}
|
||||
|
||||
type listQuestionsRes struct {
|
||||
|
|
@ -123,11 +119,9 @@ func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
|
|||
Tips: req.Tips,
|
||||
VoicePrompt: req.VoicePrompt,
|
||||
SampleAnswerVoicePrompt: req.SampleAnswerVoicePrompt,
|
||||
ImageURL: req.ImageURL,
|
||||
Status: req.Status,
|
||||
Options: options,
|
||||
ShortAnswers: shortAnswers,
|
||||
AudioCorrectAnswerText: req.AudioCorrectAnswerText,
|
||||
}
|
||||
|
||||
question, err := h.questionsSvc.CreateQuestion(c.Context(), input)
|
||||
|
|
@ -157,7 +151,6 @@ func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
|
|||
Tips: question.Tips,
|
||||
VoicePrompt: question.VoicePrompt,
|
||||
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
|
||||
ImageURL: question.ImageURL,
|
||||
Status: question.Status,
|
||||
CreatedAt: question.CreatedAt.String(),
|
||||
},
|
||||
|
|
@ -211,11 +204,6 @@ func (h *Handler) GetQuestionByID(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
var audioCorrectAnswerText *string
|
||||
if question.AudioAnswer != nil {
|
||||
audioCorrectAnswerText = &question.AudioAnswer.CorrectAnswerText
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Question retrieved successfully",
|
||||
Data: questionRes{
|
||||
|
|
@ -228,12 +216,10 @@ func (h *Handler) GetQuestionByID(c *fiber.Ctx) error {
|
|||
Tips: question.Tips,
|
||||
VoicePrompt: question.VoicePrompt,
|
||||
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
|
||||
ImageURL: question.ImageURL,
|
||||
Status: question.Status,
|
||||
CreatedAt: question.CreatedAt.String(),
|
||||
Options: options,
|
||||
ShortAnswers: shortAnswers,
|
||||
AudioCorrectAnswerText: audioCorrectAnswerText,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -370,11 +356,9 @@ type updateQuestionReq struct {
|
|||
Tips *string `json:"tips"`
|
||||
VoicePrompt *string `json:"voice_prompt"`
|
||||
SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"`
|
||||
ImageURL *string `json:"image_url"`
|
||||
Status *string `json:"status"`
|
||||
Options []optionInput `json:"options"`
|
||||
ShortAnswers []shortAnswerInput `json:"short_answers"`
|
||||
AudioCorrectAnswerText *string `json:"audio_correct_answer_text"`
|
||||
}
|
||||
|
||||
// UpdateQuestion godoc
|
||||
|
|
@ -442,11 +426,9 @@ func (h *Handler) UpdateQuestion(c *fiber.Ctx) error {
|
|||
Tips: req.Tips,
|
||||
VoicePrompt: req.VoicePrompt,
|
||||
SampleAnswerVoicePrompt: req.SampleAnswerVoicePrompt,
|
||||
ImageURL: req.ImageURL,
|
||||
Status: req.Status,
|
||||
Options: options,
|
||||
ShortAnswers: shortAnswers,
|
||||
AudioCorrectAnswerText: req.AudioCorrectAnswerText,
|
||||
}
|
||||
|
||||
err = h.questionsSvc.UpdateQuestion(c.Context(), id, input)
|
||||
|
|
|
|||
|
|
@ -22,33 +22,6 @@ import (
|
|||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// GetUserSummary godoc
|
||||
// @Summary Get user summary statistics
|
||||
// @Description Returns total users, active users, and users who joined this month
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Success 200 {object} domain.Response{data=domain.UserSummary}
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/users/summary [get]
|
||||
func (h *Handler) GetUserSummary(c *fiber.Ctx) error {
|
||||
summary, err := h.userSvc.GetUserSummary(c.Context())
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to get user summary",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "User summary retrieved successfully",
|
||||
Data: summary,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// CheckProfileCompleted godoc
|
||||
// @Summary Check if user profile is completed
|
||||
// @Description Returns the profile completion status and percentage for the specified user
|
||||
|
|
|
|||
|
|
@ -378,63 +378,6 @@ func (h *Handler) GetTranscodeStatus(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
// GetSampleVideo godoc
|
||||
// @Summary Get a sample Vimeo video with iframe embed
|
||||
// @Description Fetches a sample video from Vimeo and returns video details along with an embeddable iframe for client-side integration
|
||||
// @Tags Vimeo
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param video_id query string false "Vimeo Video ID to use as sample" default(76979871)
|
||||
// @Param width query int false "Player width" default(640)
|
||||
// @Param height query int false "Player height" default(360)
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/vimeo/sample [get]
|
||||
func (h *Handler) GetSampleVideo(c *fiber.Ctx) error {
|
||||
if h.vimeoSvc == nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
|
||||
Message: "Vimeo service is not configured",
|
||||
Error: "Vimeo service is not enabled or missing access token",
|
||||
})
|
||||
}
|
||||
|
||||
videoID := c.Query("video_id", "76979871")
|
||||
width, _ := strconv.Atoi(c.Query("width", "640"))
|
||||
height, _ := strconv.Atoi(c.Query("height", "360"))
|
||||
|
||||
info, iframe, err := h.vimeoSvc.GetSampleVideo(c.Context(), videoID, width, height)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to get sample video",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Sample video retrieved successfully",
|
||||
Data: fiber.Map{
|
||||
"video": VimeoVideoResponse{
|
||||
VimeoID: info.VimeoID,
|
||||
URI: info.URI,
|
||||
Name: info.Name,
|
||||
Description: info.Description,
|
||||
Duration: info.Duration,
|
||||
Width: info.Width,
|
||||
Height: info.Height,
|
||||
Link: info.Link,
|
||||
EmbedURL: info.EmbedURL,
|
||||
EmbedHTML: info.EmbedHTML,
|
||||
ThumbnailURL: info.ThumbnailURL,
|
||||
Status: info.Status,
|
||||
TranscodeStatus: info.TranscodeStatus,
|
||||
},
|
||||
"iframe": iframe,
|
||||
},
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// GetOEmbed godoc
|
||||
// @Summary Get oEmbed data for a Vimeo URL
|
||||
// @Description Fetches oEmbed metadata for a Vimeo video URL
|
||||
|
|
|
|||
|
|
@ -71,15 +71,6 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Get("/assessment/questions", h.ListAssessmentQuestions)
|
||||
groupV1.Get("/assessment/questions/:id", h.GetAssessmentQuestionByID)
|
||||
|
||||
// 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)
|
||||
|
|
@ -120,7 +111,6 @@ func (a *App) initAppRoutes() {
|
|||
|
||||
// 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)
|
||||
|
||||
// Questions
|
||||
groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)
|
||||
|
|
@ -218,7 +208,6 @@ func (a *App) initAppRoutes() {
|
|||
// User Routes
|
||||
groupV1.Get("/user/:user_id/is-profile-completed", a.authMiddleware, a.RequirePermission("users.profile_completed"), h.CheckProfileCompleted)
|
||||
groupV1.Get("/users", a.authMiddleware, a.RequirePermission("users.list"), h.GetAllUsers)
|
||||
groupV1.Get("/users/summary", a.authMiddleware, a.RequirePermission("users.summary"), h.GetUserSummary)
|
||||
groupV1.Put("/user", a.authMiddleware, a.RequirePermission("users.update_self"), h.UpdateUser)
|
||||
groupV1.Patch("/user/status", a.authMiddleware, a.RequirePermission("users.update_status"), h.UpdateUserStatus)
|
||||
groupV1.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel)
|
||||
|
|
@ -302,7 +291,6 @@ func (a *App) initAppRoutes() {
|
|||
vimeoGroup.Post("/uploads/pull", a.authMiddleware, a.RequirePermission("vimeo.uploads.pull"), h.CreatePullUpload)
|
||||
vimeoGroup.Post("/uploads/tus", a.authMiddleware, a.RequirePermission("vimeo.uploads.tus"), h.CreateTusUpload)
|
||||
vimeoGroup.Get("/oembed", h.GetOEmbed)
|
||||
vimeoGroup.Get("/sample", h.GetSampleVideo)
|
||||
|
||||
// Team Management
|
||||
teamGroup := groupV1.Group("/team")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user