Compare commits
No commits in common. "0226275d47b379496f069cd30d59bf4be945e3ee" and "9cb1c3e32b26510f58e9269d837c4f25e3250b5f" have entirely different histories.
0226275d47
...
9cb1c3e32b
|
|
@ -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);
|
||||
|
|
@ -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.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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.created_at
|
||||
`
|
||||
|
||||
type GetSubCoursePracticesForLearningPathRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Persona pgtype.Text `json:"persona"`
|
||||
Status string `json:"status"`
|
||||
QuestionCount int64 `json:"question_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCoursePracticesForLearningPath(ctx context.Context, ownerID pgtype.Int8) ([]GetSubCoursePracticesForLearningPathRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCoursePracticesForLearningPath, ownerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCoursePracticesForLearningPathRow
|
||||
for rows.Next() {
|
||||
var i GetSubCoursePracticesForLearningPathRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Persona,
|
||||
&i.Status,
|
||||
&i.QuestionCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubCoursePrerequisitesForLearningPath = `-- name: GetSubCoursePrerequisitesForLearningPath :many
|
||||
SELECT p.prerequisite_sub_course_id, sc.title, sc.level
|
||||
FROM sub_course_prerequisites p
|
||||
JOIN sub_courses sc ON sc.id = p.prerequisite_sub_course_id
|
||||
WHERE p.sub_course_id = $1
|
||||
ORDER BY sc.display_order
|
||||
`
|
||||
|
||||
type GetSubCoursePrerequisitesForLearningPathRow struct {
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
Title string `json:"title"`
|
||||
Level string `json:"level"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCoursePrerequisitesForLearningPath(ctx context.Context, subCourseID int64) ([]GetSubCoursePrerequisitesForLearningPathRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCoursePrerequisitesForLearningPath, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCoursePrerequisitesForLearningPathRow
|
||||
for rows.Next() {
|
||||
var i GetSubCoursePrerequisitesForLearningPathRow
|
||||
if err := rows.Scan(&i.PrerequisiteSubCourseID, &i.Title, &i.Level); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubCourseVideosForLearningPath = `-- name: GetSubCourseVideosForLearningPath :many
|
||||
SELECT id, title, description, video_url, duration, resolution, display_order,
|
||||
vimeo_id, vimeo_embed_url, video_host_provider
|
||||
FROM sub_course_videos
|
||||
WHERE sub_course_id = $1 AND status = 'PUBLISHED'
|
||||
ORDER BY display_order, id
|
||||
`
|
||||
|
||||
type GetSubCourseVideosForLearningPathRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
Duration int32 `json:"duration"`
|
||||
Resolution pgtype.Text `json:"resolution"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
||||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCourseVideosForLearningPath(ctx context.Context, subCourseID int64) ([]GetSubCourseVideosForLearningPathRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCourseVideosForLearningPath, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCourseVideosForLearningPathRow
|
||||
for rows.Next() {
|
||||
var i GetSubCourseVideosForLearningPathRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.DisplayOrder,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VideoHostProvider,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
4
go.mod
4
go.mod
|
|
@ -6,12 +6,14 @@ toolchain go1.24.11
|
|||
|
||||
require (
|
||||
firebase.google.com/go/v4 v4.19.0
|
||||
github.com/amanuelabay/afrosms-go v1.0.6
|
||||
github.com/go-playground/validator/v10 v10.29.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/resend/resend-go/v2 v2.28.0
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/swaggo/fiber-swagger v1.3.0
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/twilio/twilio-go v1.30.0
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
google.golang.org/api v0.239.0
|
||||
|
|
@ -111,6 +113,7 @@ require (
|
|||
github.com/bytedance/sonic v1.14.2
|
||||
github.com/gofiber/fiber/v2 v2.52.10
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
// github.com/jackc/pgtype v1.14.4
|
||||
|
|
@ -119,6 +122,7 @@ require (
|
|||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0
|
||||
|
|
|
|||
15
go.sum
15
go.sum
|
|
@ -42,6 +42,8 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
|
|||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
||||
github.com/amanuelabay/afrosms-go v1.0.6 h1:/B9upckMqzr5/de7dbXPqIfmJm4utbQq0QUQePxmXtk=
|
||||
github.com/amanuelabay/afrosms-go v1.0.6/go.mod h1:5mzzZtWSCDdvQsA0OyYf5CtbdGpl9lXyrcpl8/DyBj0=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
|
|
@ -109,6 +111,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXe
|
|||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
|
|
@ -157,6 +161,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q=
|
||||
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
|
|
@ -176,6 +182,8 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ
|
|||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
|
@ -215,6 +223,8 @@ github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9J
|
|||
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/twilio/twilio-go v1.30.0 h1:86FBso7jFqpSZ0XC0GKJcEY2KOeUNOFh6zLhTbUMlnc=
|
||||
github.com/twilio/twilio-go v1.30.0/go.mod h1:QbitvbvtkV77Jn4BABAKVmxabYSjMyQG4tHey9gfPqg=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
|
|
@ -234,6 +244,7 @@ github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6
|
|||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
|
||||
|
|
@ -282,6 +293,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
|
|
@ -299,8 +311,10 @@ golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
@ -325,6 +339,7 @@ golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
|||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ var (
|
|||
|
||||
ErrMissingResendApiKey = errors.New("missing Resend Api key")
|
||||
ErrMissingResendSenderEmail = errors.New("missing Resend sender name")
|
||||
|
||||
ErrMissingTwilioAccountSid = errors.New("missing twilio account sid")
|
||||
ErrMissingTwilioAuthToken = errors.New("missing twilio auth token")
|
||||
ErrMissingTwilioSenderPhoneNumber = errors.New("missing twilio sender phone number")
|
||||
)
|
||||
|
||||
type AFROSMSConfig struct {
|
||||
|
|
@ -125,6 +127,9 @@ type Config struct {
|
|||
TELEBIRR TELEBIRRConfig `mapstructure:"telebirr_config"`
|
||||
ResendApiKey string
|
||||
ResendSenderEmail string
|
||||
TwilioAccountSid string
|
||||
TwilioAuthToken string
|
||||
TwilioSenderPhoneNumber string
|
||||
RedisAddr string
|
||||
KafkaBrokers []string
|
||||
FCMServiceAccountKey string
|
||||
|
|
@ -457,6 +462,24 @@ func (c *Config) loadEnv() error {
|
|||
}
|
||||
c.ResendSenderEmail = resendSenderEmail
|
||||
|
||||
twilioAccountSid := os.Getenv("TWILIO_ACCOUNT_SID")
|
||||
if twilioAccountSid == "" {
|
||||
return ErrMissingTwilioAccountSid
|
||||
}
|
||||
c.TwilioAccountSid = twilioAccountSid
|
||||
|
||||
twilioAuthToken := os.Getenv("TWILIO_AUTH_TOKEN")
|
||||
if twilioAuthToken == "" {
|
||||
return ErrMissingTwilioAuthToken
|
||||
}
|
||||
c.TwilioAuthToken = twilioAuthToken
|
||||
|
||||
twilioSenderPhoneNumber := os.Getenv("TWILIO_SENDER_PHONE_NUMBER")
|
||||
if twilioSenderPhoneNumber == "" {
|
||||
return ErrMissingTwilioSenderPhoneNumber
|
||||
}
|
||||
c.TwilioSenderPhoneNumber = twilioSenderPhoneNumber
|
||||
|
||||
c.FCMServiceAccountKey = os.Getenv("FCM_SERVICE_ACCOUNT_KEY")
|
||||
|
||||
// Vimeo configuration
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ package domain
|
|||
type SMSProvider string
|
||||
|
||||
const (
|
||||
TwilioSms SMSProvider = "twilio"
|
||||
AfroMessage SMSProvider = "afro_message"
|
||||
)
|
||||
|
||||
// IsValid checks if the SMSProvider is a valid enum value
|
||||
func (s SMSProvider) IsValid() bool {
|
||||
switch s {
|
||||
case AfroMessage:
|
||||
case TwilioSms, AfroMessage:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -172,9 +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)
|
||||
}
|
||||
|
||||
type ProgressionStore interface {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,68 @@
|
|||
package messenger
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
afro "github.com/amanuelabay/afrosms-go"
|
||||
"github.com/twilio/twilio-go"
|
||||
twilioApi "github.com/twilio/twilio-go/rest/api/v2010"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSMSProviderNotFound = errors.New("SMS Provider Not Found")
|
||||
)
|
||||
|
||||
// If the company id is valid, it is a company based notification else its a global notification (by the super admin)
|
||||
func (s *Service) SendSMS(ctx context.Context, receiverPhone, message string) error {
|
||||
return s.SendAfroMessageSMSLatest(ctx, receiverPhone, message, nil)
|
||||
|
||||
var settingsList domain.SettingList
|
||||
|
||||
switch settingsList.SMSProvider {
|
||||
case domain.AfroMessage:
|
||||
return s.SendAfroMessageSMS(ctx, receiverPhone, message)
|
||||
case domain.TwilioSms:
|
||||
return s.SendTwilioSMS(ctx, receiverPhone, message)
|
||||
default:
|
||||
return ErrSMSProviderNotFound
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) SendAfroMessageSMS(ctx context.Context, receiverPhone, message string) error {
|
||||
apiKey := s.config.AFRO_SMS_API_KEY
|
||||
senderName := s.config.AFRO_SMS_SENDER_NAME
|
||||
hostURL := s.config.AFRO_SMS_HOST_URL
|
||||
endpoint := "/api/send"
|
||||
|
||||
// API endpoint has been updated
|
||||
// TODO: no need for package for the afro message operations (pretty simple stuff)
|
||||
request := afro.GetRequest(apiKey, endpoint, hostURL)
|
||||
request.BaseURL = "https://api.afromessage.com"
|
||||
|
||||
request.Method = "GET"
|
||||
request.Sender(senderName)
|
||||
request.To(receiverPhone, message)
|
||||
|
||||
fmt.Printf("the afro SMS request is: %v", request)
|
||||
|
||||
response, err := afro.MakeRequestWithContext(ctx, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if response["acknowledge"] == "success" {
|
||||
return nil
|
||||
} else {
|
||||
fmt.Println(response["response"].(map[string]interface{}))
|
||||
return errors.New("SMS delivery failed")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) SendAfroMessageSMSLatest(
|
||||
|
|
@ -39,7 +90,7 @@ func (s *Service) SendAfroMessageSMSLatest(
|
|||
}
|
||||
|
||||
// Construct full URL
|
||||
reqURL := fmt.Sprintf("%s/api/send?%s", baseURL, params.Encode())
|
||||
reqURL := fmt.Sprintf("%s?%s", baseURL+"/api/send", params.Encode())
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
|
||||
if err != nil {
|
||||
|
|
@ -86,3 +137,26 @@ func (s *Service) SendAfroMessageSMSLatest(
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) SendTwilioSMS(ctx context.Context, receiverPhone, message string) error {
|
||||
accountSid := s.config.TwilioAccountSid
|
||||
authToken := s.config.TwilioAuthToken
|
||||
senderPhone := s.config.TwilioSenderPhoneNumber
|
||||
|
||||
client := twilio.NewRestClientWithParams(twilio.ClientParams{
|
||||
Username: accountSid,
|
||||
Password: authToken,
|
||||
})
|
||||
|
||||
params := &twilioApi.CreateMessageParams{}
|
||||
params.SetTo(receiverPhone)
|
||||
params.SetFrom(senderPhone)
|
||||
params.SetBody(message)
|
||||
|
||||
_, err := client.Api.CreateMessage(params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s", "Error sending SMS message: %s"+err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,10 @@ import (
|
|||
|
||||
// "github.com/segmentio/kafka-go"
|
||||
"go.uber.org/zap"
|
||||
// afro "github.com/amanuelabay/afrosms-go"
|
||||
firebase "firebase.google.com/go/v4"
|
||||
"firebase.google.com/go/v4/messaging"
|
||||
afro "github.com/amanuelabay/afrosms-go"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/resend/resend-go/v2"
|
||||
"google.golang.org/api/option"
|
||||
|
|
@ -110,6 +112,38 @@ func (s *Service) initFCMClient() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) SendAfroMessageSMS(ctx context.Context, receiverPhone, message string) error {
|
||||
apiKey := s.config.AFRO_SMS_API_KEY
|
||||
senderName := s.config.AFRO_SMS_SENDER_NAME
|
||||
|
||||
baseURL := "https://api.afromessage.com"
|
||||
endpoint := "/api/send"
|
||||
|
||||
request := afro.GetRequest(apiKey, endpoint, baseURL)
|
||||
|
||||
// MUST be POST
|
||||
request.Method = "POST"
|
||||
|
||||
request.Sender(senderName)
|
||||
request.To(receiverPhone, message)
|
||||
|
||||
response, err := afro.MakeRequestWithContext(ctx, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ack, ok := response["acknowledge"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected SMS response format: %v", response)
|
||||
}
|
||||
|
||||
if ack != "success" {
|
||||
return fmt.Errorf("SMS delivery failed: %v", response)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) SendAfroMessageSMSTemp(
|
||||
ctx context.Context,
|
||||
receiverPhone string,
|
||||
|
|
@ -709,7 +743,7 @@ func (s *Service) SendBulkPushNotification(ctx context.Context, userIDs []int64,
|
|||
// It sends sequentially and returns the count of successful and failed deliveries.
|
||||
func (s *Service) SendBulkSMS(ctx context.Context, recipients []string, message string) (sent int, failed int) {
|
||||
for _, phone := range recipients {
|
||||
if err := s.SendAfroMessageSMSTemp(ctx, phone, message, nil); err != nil {
|
||||
if err := s.SendAfroMessageSMS(ctx, phone, message); err != nil {
|
||||
s.mongoLogger.Error("[NotificationSvc.SendBulkSMS] Failed to send SMS",
|
||||
zap.String("phone", phone),
|
||||
zap.Error(err),
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func (s *Service) ResendOtp(
|
|||
// Broadcast OTP (same logic as SendOtp)
|
||||
switch otp.Medium {
|
||||
case domain.OtpMediumSms:
|
||||
if err := s.messengerSvc.SendAfroMessageSMSLatest(ctx, otp.SentTo, message, nil); err != nil {
|
||||
if err := s.messengerSvc.SendAfroMessageSMS(ctx, otp.SentTo, message); err != nil {
|
||||
return err
|
||||
}
|
||||
case domain.OtpMediumEmail:
|
||||
|
|
@ -68,9 +68,18 @@ func (s *Service) SendOtp(ctx context.Context, userID int64, sentTo string, otpF
|
|||
switch medium {
|
||||
case domain.OtpMediumSms:
|
||||
|
||||
if err := s.messengerSvc.SendAfroMessageSMSLatest(ctx, sentTo, message, nil); err != nil {
|
||||
switch provider {
|
||||
case domain.TwilioSms:
|
||||
if err := s.messengerSvc.SendTwilioSMS(ctx, sentTo, message); err != nil {
|
||||
return err
|
||||
}
|
||||
case domain.AfroMessage:
|
||||
if err := s.messengerSvc.SendAfroMessageSMS(ctx, sentTo, message); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid sms provider: %s", provider)
|
||||
}
|
||||
case domain.OtpMediumEmail:
|
||||
if err := s.messengerSvc.SendEmail(ctx, sentTo, message, message, "Yimaru - One Time Password"); err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterU
|
|||
return domain.User{}, err
|
||||
}
|
||||
|
||||
if err := s.SendOtp(ctx, user.ID, sentTo, domain.OtpRegister, registerReq.OtpMedium, domain.AfroMessage); err != nil {
|
||||
if err := s.SendOtp(ctx, user.ID, sentTo, domain.OtpRegister, registerReq.OtpMedium, domain.TwilioSms); err != nil {
|
||||
return domain.User{}, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1420,41 +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,
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
|
|||
|
|
@ -111,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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user