Files
InviteMe/app/__init__.py
T

215 lines
9.4 KiB
Python

import os
import logging
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_wtf.csrf import CSRFProtect
from sqlalchemy import text
from config import Config
logger = logging.getLogger(__name__)
db = SQLAlchemy()
limiter = Limiter(key_func=get_remote_address)
csrf = CSRFProtect()
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
secret_key = app.config.get("SECRET_KEY", "")
if not secret_key or secret_key == "change-me-in-production":
raise RuntimeError(
"SECRET_KEY не задан или равен значению по умолчанию. "
"Задайте уникальный секретный ключ через переменную окружения SECRET_KEY или файл .env."
)
# Ensure upload directories exist
os.makedirs(app.config["PHOTO_FOLDER"], exist_ok=True)
os.makedirs(app.config["QR_FOLDER"], exist_ok=True)
os.makedirs(app.config["LOGO_FOLDER"], exist_ok=True)
os.makedirs(os.path.dirname(app.config["SQLALCHEMY_DATABASE_URI"].replace("sqlite:///", "")), exist_ok=True)
db.init_app(app)
limiter.init_app(app)
csrf.init_app(app)
from app.routes import main_bp
app.register_blueprint(main_bp)
with app.app_context():
db.create_all()
_migrate_existing_data(app)
_load_smtp_settings(app)
return app
def _load_smtp_settings(app):
"""Загружает SMTP-настройки из базы в конфиг приложения."""
try:
from app.models import SmtpSettings
SmtpSettings.apply_to_app(app)
except Exception as e:
logger.warning(f"SMTP settings load skipped: {e}")
def _column_exists(table_name, column_name):
return db.session.execute(
text(f"SELECT COUNT(*) FROM pragma_table_info('{table_name}') WHERE name='{column_name}'")
).scalar()
def _add_column_if_missing(table_name, column_name, column_def):
if not _column_exists(table_name, column_name):
db.session.execute(text(f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_def}"))
def _safe_move(src, dst):
"""Перемещает файл, игнорируя ошибки."""
try:
import shutil
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(src), str(dst))
return True
except Exception as e:
logger.warning(f"Не удалось переместить {src} -> {dst}: {e}")
return False
def _migrate_files_to_event_folders():
"""Переносит существующие файлы в папки по мероприятиям."""
from pathlib import Path
from flask import current_app
from app.models import Event, Guest
photo_base = Path(current_app.config["PHOTO_FOLDER"])
qr_base = Path(current_app.config["QR_FOLDER"])
logo_base = Path(current_app.config["LOGO_FOLDER"])
tickets_base = Path(current_app.config["UPLOAD_FOLDER"]) / "tickets"
# Фото гостей
for guest in Guest.query.filter(Guest.photo_filename.isnot(None)).all():
if not guest.photo_filename:
continue
src = photo_base / guest.photo_filename
if src.exists() and src.is_file():
dst = photo_base / f"event_{guest.event_id}" / guest.photo_filename
_safe_move(src, dst)
# QR-коды гостей
for guest in Guest.query.filter(Guest.qr_filename.isnot(None)).all():
if not guest.qr_filename:
continue
src = qr_base / guest.qr_filename
if src.exists() and src.is_file():
dst = qr_base / f"event_{guest.event_id}" / guest.qr_filename
_safe_move(src, dst)
# Логотипы мероприятий
for event in Event.query.filter(Event.logo_filename.isnot(None)).all():
if not event.logo_filename:
continue
src = logo_base / event.logo_filename
if src.exists() and src.is_file():
dst = logo_base / f"event_{event.id}" / event.logo_filename
_safe_move(src, dst)
# Сгенерированные билеты
if tickets_base.exists():
for event in Event.query.all():
event_tickets_dir = tickets_base / f"event_{event.id}"
event_tickets_dir.mkdir(parents=True, exist_ok=True)
for ticket_file in tickets_base.glob(f"ticket_{event.id}_*.png"):
_safe_move(ticket_file, event_tickets_dir / ticket_file.name)
def _migrate_existing_data(app):
"""Миграция существующих гостей в дефолтное мероприятие и добавление новых колонок."""
from app.models import Event, Guest
try:
# Добавляем колонки дизайна билета в таблицу events
_add_column_if_missing("events", "ticket_title", "VARCHAR(255)")
_add_column_if_missing("events", "ticket_subtitle", "VARCHAR(255)")
_add_column_if_missing("events", "ticket_body_text", "TEXT")
_add_column_if_missing("events", "ticket_background_color", "VARCHAR(7) DEFAULT '#ffffff'")
_add_column_if_missing("events", "ticket_text_color", "VARCHAR(7) DEFAULT '#000000'")
_add_column_if_missing("events", "ticket_accent_color", "VARCHAR(7) DEFAULT '#0d6efd'")
_add_column_if_missing("events", "ticket_show_qr", "BOOLEAN DEFAULT 1")
_add_column_if_missing("events", "ticket_show_photo", "BOOLEAN DEFAULT 0")
_add_column_if_missing("events", "ticket_layout", "VARCHAR(20) DEFAULT 'portrait'")
_add_column_if_missing("events", "logo_filename", "VARCHAR(255)")
# Добавляем колонки ФИО/телефона в таблицу guests
_add_column_if_missing("guests", "last_name", "VARCHAR(255)")
_add_column_if_missing("guests", "first_name", "VARCHAR(255)")
_add_column_if_missing("guests", "middle_name", "VARCHAR(255)")
_add_column_if_missing("guests", "phone", "VARCHAR(50)")
# Версия сессии пользователя для инвалидации после смены пароля
_add_column_if_missing("users", "session_version", "INTEGER DEFAULT 0")
# Флаг отправки через локальный Outlook (MAPI)
_add_column_if_missing("smtp_settings", "use_local_outlook", "BOOLEAN DEFAULT 0")
# Флаг отправки через Exchange Web Services (EWS)
_add_column_if_missing("smtp_settings", "use_ews", "BOOLEAN DEFAULT 0")
_add_column_if_missing("smtp_settings", "ews_url", "VARCHAR(500)")
# Настройки IMAP для сохранения копий отправленных писем
_add_column_if_missing("smtp_settings", "imap_server", "VARCHAR(255)")
_add_column_if_missing("smtp_settings", "imap_port", "INTEGER DEFAULT 993")
_add_column_if_missing("smtp_settings", "imap_use_ssl", "BOOLEAN DEFAULT 1")
_add_column_if_missing("smtp_settings", "imap_username", "VARCHAR(255)")
_add_column_if_missing("smtp_settings", "imap_password", "VARCHAR(255)")
_add_column_if_missing("smtp_settings", "imap_folder", "VARCHAR(255)")
_add_column_if_missing("smtp_settings", "imap_save_copy", "BOOLEAN DEFAULT 0")
# Переносим файлы в папки по мероприятиям
_migrate_files_to_event_folders()
# Проверяем, есть ли колонка event_id в таблице guests
if _column_exists("guests", "event_id"):
db.session.commit()
return # Миграция гостей уже выполнена
# Создаём дефолтное мероприятие для существующих гостей
default_event = Event(
name=app.config.get("DEFAULT_EVENT_NAME", "Архивное мероприятие"),
description="Автоматически созданное мероприятие для существующих гостей",
is_active=True,
)
db.session.add(default_event)
db.session.flush()
# Добавляем колонку event_id
db.session.execute(text("ALTER TABLE guests ADD COLUMN event_id INTEGER REFERENCES events(id)"))
# Привязываем всех существующих гостей к дефолтному мероприятию
db.session.execute(
text("UPDATE guests SET event_id = :event_id"),
{"event_id": default_event.id},
)
# Добавляем колонку event_id в check_ins (опционально)
if not _column_exists("check_ins", "event_id"):
db.session.execute(text("ALTER TABLE check_ins ADD COLUMN event_id INTEGER REFERENCES events(id)"))
db.session.execute(
text("""
UPDATE check_ins
SET event_id = (
SELECT event_id FROM guests WHERE guests.id = check_ins.guest_id
)
""")
)
db.session.commit()
logger.info(f"Migration completed: existing guests moved to event '{default_event.name}' (id={default_event.id})")
except Exception as e:
db.session.rollback()
logger.warning(f"Migration skipped or failed: {e}")