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) # 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}")