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/', 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/', 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/') 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/') 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)