Files
dr_mp_quiz/server.py
T
2026-05-08 15:23:29 +03:00

539 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from flask import Flask, render_template, request, jsonify, session, send_file
from questions import QUESTION_POOL
import random
import uuid
import os
import json
from datetime import datetime
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageFilter
# Пытаемся импортировать qrcode с обработкой ошибок
try:
import qrcode
from qrcode.constants import ERROR_CORRECT_L
QRCODE_AVAILABLE = True
except ImportError:
QRCODE_AVAILABLE = False
print("Warning: qrcode library not installed. Install with: pip install qrcode[pil]")
app = Flask(__name__)
app.secret_key = 'dnr_youth_parliament_secretary_key_2024'
# Хранилище сессий игроков и результатов
active_games = {}
saved_results = {}
# Файл для сохранения вопросов
QUESTIONS_FILE = 'questions_custom.json'
def load_questions():
"""Загружает вопросы из файла или использует стандартный пул"""
if os.path.exists(QUESTIONS_FILE):
with open(QUESTIONS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return QUESTION_POOL.copy()
def save_questions(questions):
"""Сохраняет вопросы в файл"""
with open(QUESTIONS_FILE, 'w', encoding='utf-8') as f:
json.dump(questions, f, ensure_ascii=False, indent=2)
def create_result_image(result_data):
"""Создаёт изображение с результатами викторины в формате 16:9"""
# Формат 16:9 (1920x1080)
width = 1920
height = 1080
# Создаём изображение с градиентным фоном
img = Image.new('RGB', (width, height))
draw = ImageDraw.Draw(img)
# Новый градиентный фон (тёмный градиент)
for i in range(height):
r = int(18 + (i / height) * 35) # RGB(18, 25, 45) -> RGB(53, 66, 89)
g = int(25 + (i / height) * 41)
b = int(45 + (i / height) * 44)
draw.line([(0, i), (width, i)], fill=(r, g, b))
# Декоративные элементы
# Большой круг в правом верхнем углу
circle1_radius = 300
draw.ellipse([width - circle1_radius - 50, -circle1_radius // 2,
width + 50, circle1_radius + 50],
fill=(255, 100, 100, 30), outline=None)
# Круг в левом нижнем углу
circle2_radius = 250
draw.ellipse([-circle2_radius // 2, height - circle2_radius - 50,
circle2_radius + 50, height + 50],
fill=(100, 150, 255, 30), outline=None)
# Золотая рамка с двойным контуром
border_margin = 15
border_color = (218, 165, 32) # Золотой
# Внешняя рамка
draw.rectangle([border_margin, border_margin,
width - border_margin, height - border_margin],
outline=border_color, width=3)
# Внутренняя рамка
draw.rectangle([border_margin + 10, border_margin + 10,
width - border_margin - 10, height - border_margin - 10],
outline=border_color, width=1)
# Угловые украшения
corner_length = 60
corner_width = 5
# Верхний левый угол
draw.line([border_margin, border_margin + corner_length,
border_margin, border_margin], fill=border_color, width=corner_width)
draw.line([border_margin + corner_length, border_margin,
border_margin, border_margin], fill=border_color, width=corner_width)
# Верхний правый угол
draw.line([width - border_margin, border_margin + corner_length,
width - border_margin, border_margin], fill=border_color, width=corner_width)
draw.line([width - border_margin - corner_length, border_margin,
width - border_margin, border_margin], fill=border_color, width=corner_width)
# Нижний левый угол
draw.line([border_margin, height - border_margin - corner_length,
border_margin, height - border_margin], fill=border_color, width=corner_width)
draw.line([border_margin + corner_length, height - border_margin,
border_margin, height - border_margin], fill=border_color, width=corner_width)
# Нижний правый угол
draw.line([width - border_margin, height - border_margin - corner_length,
width - border_margin, height - border_margin], fill=border_color, width=corner_width)
draw.line([width - border_margin - corner_length, height - border_margin,
width - border_margin, height - border_margin], fill=border_color, width=corner_width)
# Пытаемся загрузить шрифты
try:
font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 64)
font_score = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 160)
font_percent = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 56)
font_normal = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 32)
font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 24)
except:
try:
# Альтернативные шрифты для разных ОС
font_title = ImageFont.truetype("arial.ttf", 64)
font_score = ImageFont.truetype("arial.ttf", 160)
font_percent = ImageFont.truetype("arial.ttf", 56)
font_normal = ImageFont.truetype("arial.ttf", 32)
font_small = ImageFont.truetype("arial.ttf", 24)
except:
font_title = ImageFont.load_default()
font_score = ImageFont.load_default()
font_percent = ImageFont.load_default()
font_normal = ImageFont.load_default()
font_small = ImageFont.load_default()
# Центральный элемент - звезда/символ
center_x = width // 2
# Заголовок с тенью
title1 = "МОЛОДЁЖНЫЙ ПАРЛАМЕНТ"
title1_shadow = "МОЛОДЁЖНЫЙ ПАРЛАМЕНТ"
shadow_offset = 3
# Тень для заголовка
title1_width = draw.textlength(title1, font=font_title)
draw.text(((width - title1_width) // 2 + shadow_offset, 130 + shadow_offset),
title1_shadow, fill=(30, 30, 50), font=font_title)
draw.text(((width - title1_width) // 2, 130), title1, fill='white', font=font_title)
# Подзаголовок
title2 = "ДОНЕЦКОЙ НАРОДНОЙ РЕСПУБЛИКИ"
title2_width = draw.textlength(title2, font=font_normal)
draw.text(((width - title2_width) // 2 + shadow_offset, 210 + shadow_offset),
title2, fill=(30, 30, 50), font=font_normal)
draw.text(((width - title2_width) // 2, 210), title2, fill=(218, 165, 32), font=font_normal)
# Декоративная линия с точками
line_y = 270
line_length = 400
for i in range(0, line_length, 15):
x1 = center_x - line_length // 2 + i
if i % 30 == 0:
draw.ellipse([x1 - 3, line_y - 3, x1 + 3, line_y + 3], fill=(218, 165, 32))
else:
draw.line([x1, line_y, x1 + 10, line_y], fill=(218, 165, 32), width=2)
# Карточка с результатами
card_width = 600
card_height = 380 # Увеличена высота карточки
card_x = (width - card_width) // 2
card_y = 310 # Немного поднята карточка
# Полупрозрачная карточка
card_overlay = Image.new('RGBA', (card_width, card_height), (0, 0, 0, 0))
card_draw = ImageDraw.Draw(card_overlay)
card_draw.rectangle([0, 0, card_width, card_height], fill=(255, 255, 255, 30))
# Рамка карточки
card_draw.rectangle([0, 0, card_width, card_height], outline=(218, 165, 32), width=2)
# Вставляем карточку
img.paste(card_overlay, (card_x, card_y), card_overlay)
# Основной счёт (поднят выше)
score_y = card_y + 70
score_text = f"{result_data['score']} / {result_data['total']}"
score_width = draw.textlength(score_text, font=font_score)
draw.text(((width - score_width) // 2, score_y), score_text, fill=(255, 215, 0), font=font_score)
# Проценты (опущены ниже)
percent_y = card_y + 230 # Было 200, стало 230 - опустили на 30px
percent_text = f"{result_data['percentage']}%"
percent_width = draw.textlength(percent_text, font=font_percent)
# Радужный градиент для процентов
draw.text(((width - percent_width) // 2, percent_y), percent_text, fill=(255, 215, 0), font=font_percent)
# Звание (опущено ниже)
grade_y = card_y + 310 # Было 280, стало 310 - опустили на 30px
grade_text = result_data['grade']
grade_width = draw.textlength(grade_text, font=font_normal)
draw.text(((width - grade_width) // 2, grade_y), grade_text, fill=(255, 215, 0), font=font_normal)
# Нижняя часть
footer_y = height - 60
# Левая часть - QR-код
if QRCODE_AVAILABLE:
try:
result_url = f"http://localhost:5000/api/result/{result_data['game_id']}"
qr = qrcode.QRCode(version=1, box_size=4, border=2)
qr.add_data(result_url)
qr.make(fit=True)
qr_img = qr.make_image(fill_color=(218, 165, 32), back_color=(18, 25, 45))
qr_img = qr_img.resize((100, 100))
img.paste(qr_img, (80, footer_y - 70))
except:
pass
# Центральная часть - девиз
footer_text = "МОЛОДОСТЬ. ПАТРИОТИЗМ. РАЗВИТИЕ."
footer_width = draw.textlength(footer_text, font=font_small)
draw.text(((width - footer_width) // 2, footer_y), footer_text, fill=(218, 165, 32), font=font_small)
# Правая часть - дата
date_text = datetime.now().strftime("%d.%m.%Y")
date_width = draw.textlength(date_text, font=font_small)
draw.text((width - date_width - 80, footer_y), date_text, fill=(150, 150, 180), font=font_small)
# Подпись
sign_y = footer_y + 35
sign_text = "Молодёжный Парламент Донецкой Народной Республики"
sign_width = draw.textlength(sign_text, font=font_small)
draw.text(((width - sign_width) // 2, sign_y), sign_text, fill=(100, 100, 130), font=font_small)
return img
class QuizGame:
def __init__(self):
self.game_id = str(uuid.uuid4())[:8]
self.questions_pool = load_questions()
self.selected_questions = []
self.current_question_index = 0
self.score = 0
self.is_active = True
self.user_answers = []
self.start_time = datetime.now()
self._select_random_questions()
def _select_random_questions(self):
"""Выбирает 7 случайных вопросов из пула"""
if len(self.questions_pool) >= 7:
self.selected_questions = random.sample(self.questions_pool, 7)
else:
self.selected_questions = self.questions_pool.copy()
for q in self.selected_questions:
q['time_limit'] = 10
def get_current_question(self):
"""Возвращает текущий вопрос (с правильным ответом для подсветки)"""
if self.current_question_index < len(self.selected_questions):
q = self.selected_questions[self.current_question_index]
return {
'id': self.current_question_index,
'text': q['text'],
'options': q['options'],
'correct': q['correct'],
'time_limit': q.get('time_limit', 10)
}
return None
def check_answer(self, answer_index):
"""Проверяет ответ и обновляет счет"""
current_q = self.selected_questions[self.current_question_index]
is_correct = (answer_index == current_q['correct'])
self.user_answers.append({
'question_text': current_q['text'],
'options': current_q['options'],
'user_answer': answer_index,
'user_answer_text': current_q['options'][answer_index] if answer_index != -1 else 'Время вышло',
'correct_answer': current_q['options'][current_q['correct']],
'correct_index': current_q['correct'],
'is_correct': is_correct,
'explanation': current_q.get('explanation', '')
})
if is_correct:
self.score += 1
self.current_question_index += 1
return {
'is_correct': is_correct,
'correct_answer': current_q['options'][current_q['correct']],
'explanation': current_q.get('explanation', ''),
'score': self.score,
'is_finished': self.current_question_index >= len(self.selected_questions),
'total_questions': len(self.selected_questions)
}
def get_result(self):
"""Возвращает результат теста со всеми вопросами"""
total = len(self.selected_questions)
percentage = (self.score / total) * 100
if percentage >= 90:
grade = "ДЕПУТАТ ВЫСШЕЙ КАТЕГОРИИ"
message = "Вы отлично знаете историю и структуру Молодёжного Парламента ДНР!"
elif percentage >= 70:
grade = "АКТИВНЫЙ ПАРЛАМЕНТАРИЙ"
message = "Хороший результат! Вы достойно показали свои знания."
elif percentage >= 50:
grade = "КАНДИДАТ В ПАРЛАМЕНТ"
message = "Неплохо, но есть куда расти. Рекомендуем изучить материалы о работе Парламента."
else:
grade = "БУДУЩИЙ ЛИДЕР"
message = "Не расстраивайтесь! Участие в викторине - первый шаг к большим достижениям."
result_data = {
'game_id': self.game_id,
'score': self.score,
'total': total,
'percentage': round(percentage, 1),
'grade': grade,
'message': message,
'questions_summary': self.user_answers,
'completed_at': self.start_time.isoformat()
}
saved_results[self.game_id] = result_data
return {
'score': self.score,
'total': total,
'percentage': round(percentage, 1),
'grade': grade,
'message': message,
'questions_summary': self.user_answers,
'result_id': self.game_id
}
@app.route('/')
def index():
return render_template('index.html')
@app.route('/admin')
def admin():
return render_template('admin.html')
@app.route('/api/questions', methods=['GET'])
def get_questions():
questions = load_questions()
return jsonify(questions)
@app.route('/api/questions', methods=['POST'])
def add_question():
data = request.json
questions = load_questions()
new_question = {
'text': data['text'],
'options': data['options'],
'correct': data['correct'],
'explanation': data.get('explanation', '')
}
questions.append(new_question)
save_questions(questions)
return jsonify({'success': True, 'message': 'Вопрос добавлен', 'question': new_question})
@app.route('/api/questions/<int:index>', methods=['PUT'])
def update_question(index):
data = request.json
questions = load_questions()
if 0 <= index < len(questions):
questions[index] = {
'text': data['text'],
'options': data['options'],
'correct': data['correct'],
'explanation': data.get('explanation', '')
}
save_questions(questions)
return jsonify({'success': True, 'message': 'Вопрос обновлён'})
return jsonify({'success': False, 'message': 'Вопрос не найден'}), 404
@app.route('/api/questions/<int:index>', methods=['DELETE'])
def delete_question(index):
questions = load_questions()
if 0 <= index < len(questions):
deleted = questions.pop(index)
save_questions(questions)
return jsonify({'success': True, 'message': 'Вопрос удалён', 'question': deleted})
return jsonify({'success': False, 'message': 'Вопрос не найден'}), 404
@app.route('/api/questions/reset', methods=['POST'])
def reset_questions():
from questions import QUESTION_POOL
save_questions(QUESTION_POOL.copy())
return jsonify({'success': True, 'message': 'Вопросы сброшены к стандартному пулу'})
@app.route('/api/questions/export', methods=['GET'])
def export_questions():
questions = load_questions()
return jsonify(questions)
@app.route('/api/questions/import', methods=['POST'])
def import_questions():
data = request.json
if isinstance(data, list) and len(data) > 0:
save_questions(data)
return jsonify({'success': True, 'message': f'Импортировано {len(data)} вопросов'})
return jsonify({'success': False, 'message': 'Неверный формат данных'}), 400
@app.route('/api/new_game', methods=['POST'])
def new_game():
game = QuizGame()
active_games[game.game_id] = game
return jsonify({
'game_id': game.game_id,
'question': game.get_current_question(),
'question_number': 1,
'total_questions': len(game.selected_questions)
})
@app.route('/api/check_answer', methods=['POST'])
def check_answer():
data = request.json
game_id = data.get('game_id')
answer = data.get('answer')
game = active_games.get(game_id)
if not game or not game.is_active:
return jsonify({'error': 'Игра не найдена'}), 404
result = game.check_answer(answer)
if result['is_finished']:
final_result = game.get_result()
return jsonify({
'is_finished': True,
'result': final_result
})
else:
return jsonify({
'is_finished': False,
'result': result,
'next_question': game.get_current_question(),
'question_number': game.current_question_index + 1,
'total_questions': len(game.selected_questions)
})
@app.route('/api/result/<result_id>')
def get_result_image(result_id):
"""Возвращает изображение с результатами в формате 16:9"""
result = saved_results.get(result_id)
if not result:
return "Результат не найден", 404
# Создаём изображение
img = create_result_image(result)
# Сохраняем в байты
img_io = BytesIO()
img.save(img_io, 'PNG')
img_io.seek(0)
return send_file(img_io, mimetype='image/png')
@app.route('/api/qrcode/<result_id>')
def generate_qrcode(result_id):
if not QRCODE_AVAILABLE:
from PIL import Image, ImageDraw
img = Image.new('RGB', (200, 200), color='white')
draw = ImageDraw.Draw(img)
draw.text((20, 80), "QR Code", fill='black')
draw.text((20, 100), "not available", fill='black')
img_io = BytesIO()
img.save(img_io, 'PNG')
img_io.seek(0)
return send_file(img_io, mimetype='image/png')
result_url = request.host_url.rstrip('/') + f'/api/result/{result_id}'
try:
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(result_url)
qr.make(fit=True)
img = qr.make_image(fill_color=(218, 165, 32), back_color=(18, 25, 45))
img_io = BytesIO()
img.save(img_io, 'PNG')
img_io.seek(0)
return send_file(img_io, mimetype='image/png')
except Exception as e:
print(f"QR Code generation error: {e}")
from PIL import Image, ImageDraw
img = Image.new('RGB', (200, 200), color='white')
draw = ImageDraw.Draw(img)
draw.text((10, 80), "QR Error", fill='black')
draw.text((10, 100), result_url[:30], fill='black')
img_io = BytesIO()
img.save(img_io, 'PNG')
img_io.seek(0)
return send_file(img_io, mimetype='image/png')
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)