This commit is contained in:
2026-05-08 15:23:29 +03:00
parent 951f4c5c60
commit f828068dd7
5 changed files with 1229 additions and 127 deletions
+365 -37
View File
@@ -2,24 +2,248 @@ from flask import Flask, render_template, request, jsonify, session, send_file
from questions import QUESTION_POOL
import random
import uuid
import qrcode
from io import BytesIO
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_secret_key_2024'
app.secret_key = 'dnr_youth_parliament_secretary_key_2024'
# Хранилище сессий игроков и результатов
active_games = {}
saved_results = {} # Сохраняем результаты для QR-кодов
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 = QUESTION_POOL.copy()
self.questions_pool = load_questions()
self.selected_questions = []
self.current_question_index = 0
self.score = 0
@@ -35,18 +259,18 @@ class QuizGame:
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
@@ -56,7 +280,6 @@ class QuizGame:
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'],
@@ -87,21 +310,19 @@ class QuizGame:
total = len(self.selected_questions)
percentage = (self.score / total) * 100
# Определение звания/результата
if percentage >= 90:
grade = "🏆 Депутат высшей категории!"
grade = "ДЕПУТАТ ВЫСШЕЙ КАТЕГОРИИ"
message = "Вы отлично знаете историю и структуру Молодёжного Парламента ДНР!"
elif percentage >= 70:
grade = "⭐ Активный парламентарий!"
grade = "АКТИВНЫЙ ПАРЛАМЕНТАРИЙ"
message = "Хороший результат! Вы достойно показали свои знания."
elif percentage >= 50:
grade = "📚 Кандидат в парламент!"
grade = "КАНДИДАТ В ПАРЛАМЕНТ"
message = "Неплохо, но есть куда расти. Рекомендуем изучить материалы о работе Парламента."
else:
grade = "🌱 Будущий лидер!"
grade = "БУДУЩИЙ ЛИДЕР"
message = "Не расстраивайтесь! Участие в викторине - первый шаг к большим достижениям."
# Сохраняем результат для QR-кода
result_data = {
'game_id': self.game_id,
'score': self.score,
@@ -121,7 +342,7 @@ class QuizGame:
'grade': grade,
'message': message,
'questions_summary': self.user_answers,
'result_id': self.game_id # Возвращаем ID для QR-кода
'result_id': self.game_id
}
@@ -130,6 +351,87 @@ 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()
@@ -155,14 +457,12 @@ def check_answer():
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,
@@ -173,31 +473,14 @@ def check_answer():
@app.route('/api/result/<result_id>')
def get_result_page(result_id):
"""Страница с результатами по QR-коду"""
def get_result_image(result_id):
"""Возвращает изображение с результатами в формате 16:9"""
result = saved_results.get(result_id)
if not result:
return "Результат не найден", 404
return render_template('result_page.html', result=result)
@app.route('/api/qrcode/<result_id>')
def generate_qrcode(result_id):
"""Генерация QR-кода со ссылкой на результат"""
result_url = request.host_url.rstrip('/') + f'/api/result/{result_id}'
# Создаём QR-код
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="black", back_color="white")
img = create_result_image(result)
# Сохраняем в байты
img_io = BytesIO()
@@ -207,5 +490,50 @@ def generate_qrcode(result_id):
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)