344 lines
12 KiB
Python
344 lines
12 KiB
Python
from flask import Blueprint, request, jsonify, current_app
|
|
from backend.models import db, Question, QuestionType
|
|
from backend.services.image_service import save_image, delete_image
|
|
|
|
bp = Blueprint('questions', __name__, url_prefix='/api/questions')
|
|
|
|
|
|
@bp.route('', methods=['GET'])
|
|
def list_questions():
|
|
"""Get all questions"""
|
|
questions = Question.query.order_by(Question.created_at.desc()).all()
|
|
return jsonify([q.to_dict(include_answer=True) for q in questions]), 200
|
|
|
|
|
|
@bp.route('/<int:question_id>', methods=['GET'])
|
|
def get_question(question_id):
|
|
"""Get a single question by ID"""
|
|
question = Question.query.get_or_404(question_id)
|
|
return jsonify(question.to_dict(include_answer=True)), 200
|
|
|
|
|
|
@bp.route('', methods=['POST'])
|
|
def create_question():
|
|
"""Create a new question"""
|
|
try:
|
|
# Check if it's a multipart form (for image uploads) or JSON
|
|
if request.content_type and 'multipart/form-data' in request.content_type:
|
|
# Image question
|
|
data = request.form
|
|
question_type = data.get('type', 'text')
|
|
question_content = data.get('question_content', '')
|
|
answer = data.get('answer', '')
|
|
category = data.get('category', '')
|
|
|
|
image_path = None
|
|
if question_type == 'image':
|
|
if 'image' not in request.files:
|
|
return jsonify({'error': 'Image file required for image questions'}), 400
|
|
|
|
file = request.files['image']
|
|
try:
|
|
image_path = save_image(
|
|
file,
|
|
current_app.config['UPLOAD_FOLDER'],
|
|
current_app.config['ALLOWED_EXTENSIONS']
|
|
)
|
|
except ValueError as e:
|
|
return jsonify({'error': str(e)}), 400
|
|
|
|
youtube_url = None
|
|
audio_path = None
|
|
start_time = None
|
|
end_time = None
|
|
|
|
else:
|
|
# JSON request for text or YouTube audio questions
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({'error': 'No data provided'}), 400
|
|
|
|
question_type = data.get('type', 'text')
|
|
question_content = data.get('question_content', '')
|
|
answer = data.get('answer', '')
|
|
category = data.get('category', '')
|
|
image_path = None
|
|
|
|
# Handle YouTube audio questions
|
|
youtube_url = None
|
|
audio_path = None
|
|
start_time = None
|
|
end_time = None
|
|
|
|
if question_type == 'youtube_audio':
|
|
youtube_url = data.get('youtube_url', '')
|
|
|
|
if not youtube_url:
|
|
return jsonify({'error': 'youtube_url required for YouTube audio questions'}), 400
|
|
|
|
# Validate YouTube URL
|
|
from backend.services.youtube_service import validate_youtube_url, validate_timestamps, get_video_duration
|
|
|
|
is_valid, result = validate_youtube_url(youtube_url)
|
|
if not is_valid:
|
|
return jsonify({'error': result}), 400
|
|
|
|
# Get and validate timestamps
|
|
try:
|
|
start_time = int(data.get('start_time', 0))
|
|
end_time = int(data.get('end_time', 0))
|
|
except ValueError:
|
|
return jsonify({'error': 'start_time and end_time must be integers'}), 400
|
|
|
|
# Validate timestamp range
|
|
video_duration = get_video_duration(youtube_url)
|
|
is_valid, error = validate_timestamps(start_time, end_time, video_duration)
|
|
if not is_valid:
|
|
return jsonify({'error': error}), 400
|
|
|
|
# Note: audio_path will be null until download completes
|
|
|
|
# Validation
|
|
if not question_content:
|
|
return jsonify({'error': 'question_content is required'}), 400
|
|
if not answer:
|
|
return jsonify({'error': 'answer is required'}), 400
|
|
|
|
# Create question
|
|
question = Question(
|
|
type=QuestionType(question_type),
|
|
question_content=question_content,
|
|
answer=answer,
|
|
image_path=image_path,
|
|
youtube_url=youtube_url,
|
|
audio_path=audio_path, # Will be None initially for YouTube
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
category=category if category else None
|
|
)
|
|
|
|
db.session.add(question)
|
|
db.session.commit()
|
|
|
|
# For YouTube audio, start async download
|
|
if question_type == 'youtube_audio':
|
|
from backend.tasks.youtube_tasks import download_youtube_audio
|
|
from backend.models import DownloadJob, DownloadJobStatus
|
|
|
|
# Start Celery task
|
|
task = download_youtube_audio.delay(question.id, youtube_url, start_time, end_time)
|
|
|
|
# Create job tracking record
|
|
job = DownloadJob(
|
|
question_id=question.id,
|
|
celery_task_id=task.id,
|
|
status=DownloadJobStatus.PENDING
|
|
)
|
|
db.session.add(job)
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'question': question.to_dict(include_answer=True),
|
|
'job': job.to_dict()
|
|
}), 202 # 202 Accepted (async processing)
|
|
|
|
return jsonify(question.to_dict(include_answer=True)), 201
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
@bp.route('/<int:question_id>', methods=['PUT'])
|
|
def update_question(question_id):
|
|
"""Update an existing question"""
|
|
question = Question.query.get_or_404(question_id)
|
|
|
|
try:
|
|
# Handle multipart form data or JSON
|
|
if request.content_type and 'multipart/form-data' in request.content_type:
|
|
data = request.form
|
|
|
|
# Update fields if provided
|
|
if 'question_content' in data:
|
|
question.question_content = data['question_content']
|
|
if 'answer' in data:
|
|
question.answer = data['answer']
|
|
if 'type' in data:
|
|
question.type = QuestionType(data['type'])
|
|
if 'category' in data:
|
|
question.category = data['category'] if data['category'] else None
|
|
|
|
# Handle new image upload
|
|
if 'image' in request.files:
|
|
file = request.files['image']
|
|
if file and file.filename:
|
|
# Delete old image if exists
|
|
if question.image_path:
|
|
delete_image(question.image_path, current_app.root_path)
|
|
|
|
# Save new image
|
|
try:
|
|
question.image_path = save_image(
|
|
file,
|
|
current_app.config['UPLOAD_FOLDER'],
|
|
current_app.config['ALLOWED_EXTENSIONS']
|
|
)
|
|
except ValueError as e:
|
|
return jsonify({'error': str(e)}), 400
|
|
|
|
else:
|
|
# JSON request
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({'error': 'No data provided'}), 400
|
|
|
|
if 'question_content' in data:
|
|
question.question_content = data['question_content']
|
|
if 'answer' in data:
|
|
question.answer = data['answer']
|
|
if 'type' in data:
|
|
question.type = QuestionType(data['type'])
|
|
if 'category' in data:
|
|
question.category = data['category'] if data['category'] else None
|
|
|
|
db.session.commit()
|
|
return jsonify(question.to_dict(include_answer=True)), 200
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
@bp.route('/<int:question_id>', methods=['DELETE'])
|
|
def delete_question(question_id):
|
|
"""Delete a question"""
|
|
question = Question.query.get_or_404(question_id)
|
|
|
|
try:
|
|
# Delete associated image if exists
|
|
if question.image_path:
|
|
delete_image(question.image_path, current_app.root_path)
|
|
|
|
# Delete associated audio if exists
|
|
if question.audio_path:
|
|
from backend.services.audio_service import delete_audio
|
|
delete_audio(question.audio_path, current_app.root_path)
|
|
|
|
db.session.delete(question)
|
|
db.session.commit()
|
|
|
|
return jsonify({'message': 'Question deleted successfully'}), 200
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
@bp.route('/random', methods=['GET'])
|
|
def get_random_questions():
|
|
"""Get random questions by category
|
|
|
|
Query parameters:
|
|
- category: Category name (required)
|
|
- count: Number of random questions to return (default: 5)
|
|
"""
|
|
category = request.args.get('category')
|
|
if not category:
|
|
return jsonify({'error': 'category parameter is required'}), 400
|
|
|
|
try:
|
|
count = int(request.args.get('count', 5))
|
|
if count < 1:
|
|
return jsonify({'error': 'count must be at least 1'}), 400
|
|
except ValueError:
|
|
return jsonify({'error': 'count must be a valid integer'}), 400
|
|
|
|
try:
|
|
# Get all questions for the category
|
|
questions = Question.query.filter_by(category=category).all()
|
|
|
|
if not questions:
|
|
return jsonify({'error': f'No questions found for category: {category}'}), 404
|
|
|
|
# Randomly select questions
|
|
import random
|
|
selected = random.sample(questions, min(count, len(questions)))
|
|
|
|
return jsonify({
|
|
'category': category,
|
|
'requested': count,
|
|
'returned': len(selected),
|
|
'questions': [q.to_dict(include_answer=True) for q in selected]
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
@bp.route('/bulk', methods=['POST'])
|
|
def bulk_create_questions():
|
|
"""Bulk create questions
|
|
|
|
Expected JSON: {
|
|
"questions": [
|
|
{
|
|
"question_content": "What is 2+2?",
|
|
"answer": "4",
|
|
"category": "Math",
|
|
"type": "text"
|
|
},
|
|
...
|
|
]
|
|
}
|
|
"""
|
|
data = request.get_json()
|
|
if not data or 'questions' not in data:
|
|
return jsonify({'error': 'questions array is required'}), 400
|
|
|
|
questions_data = data['questions']
|
|
if not isinstance(questions_data, list):
|
|
return jsonify({'error': 'questions must be an array'}), 400
|
|
|
|
created_questions = []
|
|
errors = []
|
|
|
|
try:
|
|
for idx, q_data in enumerate(questions_data):
|
|
try:
|
|
# Validate required fields
|
|
if not q_data.get('question_content'):
|
|
errors.append({'index': idx, 'error': 'question_content is required'})
|
|
continue
|
|
if not q_data.get('answer'):
|
|
errors.append({'index': idx, 'error': 'answer is required'})
|
|
continue
|
|
|
|
# Create question
|
|
question = Question(
|
|
type=QuestionType(q_data.get('type', 'text')),
|
|
question_content=q_data['question_content'],
|
|
answer=q_data['answer'],
|
|
category=q_data.get('category') if q_data.get('category') else None,
|
|
image_path=None # Bulk import doesn't support images
|
|
)
|
|
|
|
db.session.add(question)
|
|
created_questions.append(question)
|
|
|
|
except Exception as e:
|
|
errors.append({'index': idx, 'error': str(e)})
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'message': f'Successfully created {len(created_questions)} questions',
|
|
'created': len(created_questions),
|
|
'errors': errors,
|
|
'questions': [q.to_dict(include_answer=True) for q in created_questions]
|
|
}), 201
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
return jsonify({'error': str(e)}), 500
|