import io from pathlib import Path from flask import current_app from openpyxl import load_workbook, Workbook from openpyxl.styles import Font, PatternFill, Alignment from app import db from app.models import Guest from app.utils.security import safe_upload_filename REQUIRED_COLUMNS = ["Отдел", "Фамилия", "Имя", "Отчество", "Должность", "Почта", "Телефон", "Название фото"] def build_guest_import_template(): wb = Workbook() ws = wb.active ws.title = "Гости" ws.append(REQUIRED_COLUMNS) ws.append(["IT", "Иванов", "Иван", "Иванович", "Разработчик", "ivanov@example.com", "+7 999 111-22-33", "ivanov.jpg"]) for cell in ws[1]: cell.font = Font(bold=True, color="FFFFFF") cell.fill = PatternFill(start_color="0891B2", end_color="0891B2", fill_type="solid") cell.alignment = Alignment(horizontal="center") for index, width in enumerate([22, 20, 18, 22, 24, 30, 22, 24], start=1): ws.column_dimensions[ws.cell(row=1, column=index).column_letter].width = width ws.freeze_panes = "A2" ws.auto_filter.ref = "A1:H2" output = io.BytesIO() wb.save(output) wb.close() output.seek(0) return output def _build_full_name(last_name, first_name, middle_name): parts = [p for p in [last_name, first_name, middle_name] if p] return " ".join(parts) def _safe_excel_value(value): """Prevent spreadsheet formula injection in exported user-controlled cells.""" if isinstance(value, str) and value.startswith(("=", "+", "-", "@", "\t", "\r")): return f"'{value}" return value def read_guests_from_excel(file_path): """Читает список гостей из Excel-файла (путь или файловый объект).""" if not hasattr(file_path, "read"): file_path = Path(file_path) wb = load_workbook(file_path, data_only=True) try: ws = wb.active if ws.max_row - 1 > current_app.config["MAX_EXCEL_ROWS"]: raise ValueError(f"В Excel допускается не более {current_app.config['MAX_EXCEL_ROWS']} строк") # Определяем заголовки и сразу считываем все строки headers = [str(cell.value).strip() if cell.value else "" for cell in ws[1]] rows = list(ws.iter_rows(min_row=2, values_only=True)) finally: wb.close() missing = [col for col in REQUIRED_COLUMNS if col not in headers] if missing: raise ValueError(f"В файле отсутствуют обязательные столбцы: {', '.join(missing)}") col_indices = {col: headers.index(col) for col in REQUIRED_COLUMNS} guests_data = [] for row in rows: last_name = str(row[col_indices["Фамилия"]]).strip() if row[col_indices["Фамилия"]] else "" first_name = str(row[col_indices["Имя"]]).strip() if row[col_indices["Имя"]] else "" middle_name = str(row[col_indices["Отчество"]]).strip() if row[col_indices["Отчество"]] else "" email = str(row[col_indices["Почта"]]).strip() if row[col_indices["Почта"]] else "" if not email: continue full_name = _build_full_name(last_name, first_name, middle_name) if not full_name: # Если ФИО не заполнено, но есть email — пропускаем строку continue raw_photo_filename = str(row[col_indices["Название фото"]]).strip() if row[col_indices["Название фото"]] else "" photo_filename = safe_upload_filename(raw_photo_filename) if raw_photo_filename else "" guests_data.append({ "department": str(row[col_indices["Отдел"]]).strip() if row[col_indices["Отдел"]] else "", "last_name": last_name, "first_name": first_name, "middle_name": middle_name, "full_name": full_name, "position": str(row[col_indices["Должность"]]).strip() if row[col_indices["Должность"]] else "", "email": email, "phone": str(row[col_indices["Телефон"]]).strip() if row[col_indices["Телефон"]] else "", "photo_filename": photo_filename, }) return guests_data def import_guests_from_excel(file_path, event_id): """Импортирует гостей из Excel в базу данных в рамках указанного мероприятия.""" if not event_id: raise ValueError("Не выбрано мероприятие для импорта") guests_data = read_guests_from_excel(file_path) imported = 0 updated = 0 for data in guests_data: guest = Guest.query.filter_by(event_id=event_id, email=data["email"]).first() if guest: guest.department = data["department"] guest.last_name = data["last_name"] guest.first_name = data["first_name"] guest.middle_name = data["middle_name"] guest.full_name = data["full_name"] guest.position = data["position"] guest.phone = data["phone"] guest.photo_filename = data["photo_filename"] guest.is_active = True updated += 1 else: guest = Guest( event_id=event_id, department=data["department"], last_name=data["last_name"], first_name=data["first_name"], middle_name=data["middle_name"], full_name=data["full_name"], position=data["position"], email=data["email"], phone=data["phone"], photo_filename=data["photo_filename"], ) db.session.add(guest) imported += 1 db.session.commit() return {"imported": imported, "updated": updated, "total": len(guests_data)} def export_report_to_excel(output_path, event_id, event_name=None): """Выгружает отчёт о посещаемости для мероприятия в Excel.""" if not event_id: raise ValueError("Не выбрано мероприятие для отчёта") guests = Guest.query.filter_by(event_id=event_id).order_by(Guest.department, Guest.full_name).all() wb = Workbook() ws = wb.active ws.title = "Отчёт" headers = [ "Мероприятие", "Отдел", "Фамилия", "Имя", "Отчество", "Должность", "Email", "Телефон", "Название фото", "Статус приглашения", "Попыток отправки", "Ошибка отправки", "RSVP", "Ответ получен", "Особенности питания", "Комментарий", "Статус входа", "Время входа" ] ws.append(headers) # Стили заголовков for cell in ws[1]: cell.font = Font(bold=True) cell.fill = PatternFill(start_color="DDDDDD", end_color="DDDDDD", fill_type="solid") cell.alignment = Alignment(horizontal="center") for guest in guests: status = "Пришёл" if guest.is_checked_in else "Не пришёл" check_in_time = guest.last_check_in.strftime("%d.%m.%Y %H:%M:%S") if guest.last_check_in else "" ws.append([ _safe_excel_value(event_name or ""), _safe_excel_value(guest.department), _safe_excel_value(guest.last_name or ""), _safe_excel_value(guest.first_name or ""), _safe_excel_value(guest.middle_name or ""), _safe_excel_value(guest.position or ""), _safe_excel_value(guest.email), _safe_excel_value(guest.phone or ""), _safe_excel_value(guest.photo_filename or ""), guest.invitation_status or "pending", guest.invitation_attempts or 0, _safe_excel_value(guest.invitation_last_error or ""), guest.rsvp_status or "pending", guest.rsvp_responded_at.strftime("%d.%m.%Y %H:%M:%S") if guest.rsvp_responded_at else "", _safe_excel_value(guest.dietary_requirements or ""), _safe_excel_value(guest.rsvp_comment or ""), status, check_in_time, ]) # Автоширина колонок for column in ws.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if cell.value: max_length = max(max_length, len(str(cell.value))) except Exception: pass ws.column_dimensions[column_letter].width = min(max_length + 2, 50) wb.save(output_path) return output_path