"""Роуты расписания: публичная страница, экспорт ICS, ассистент, админка.""" import pytz from datetime import datetime, timezone, timedelta from flask import render_template, request, redirect, url_for, session, jsonify, make_response from icalendar import Calendar, Event as ICalEvent from sqlmodel import Session, select from ..db import engine from ..models import Event, GlobalStatus, User from ..helpers import ( to_msk, login_required, require_role, parse_datetime, status_label, event_summary_text, STATUS_LABELS, send_email, notify_subscribers, ) from .. import assistant, llm_assistant def register(app): @app.route("/schedule") def public_schedule(): with Session(engine) as db_session: current_status = db_session.exec(select(GlobalStatus)).first() now = datetime.now(timezone.utc) events = db_session.exec( select(Event).where(Event.is_public == True) .where(Event.start_time >= now - timedelta(days=180)) .where(Event.start_time <= now + timedelta(days=365)) .order_by(Event.start_time) ).all() from ..helpers import get_user_subscription my_subscription = None current_user = None if session.get("user_id"): current_user = db_session.get(User, session["user_id"]) if current_user: my_subscription = get_user_subscription(db_session, current_user) events_json = [{"id": e.id, "title": e.title, "type": e.event_type, "start": to_msk(e.start_time).isoformat(), "end": to_msk(e.end_time).isoformat(), "location": e.location, "description": e.description} for e in events] return render_template("schedule.html", status=current_status, events_json=events_json, my_subscription=my_subscription, current_user=current_user, msg=request.args.get("msg"), error=request.args.get("error")) @app.route("/schedule/export.ics") def export_ics(): with Session(engine) as db_session: events = db_session.exec(select(Event).where(Event.is_public == True)).all() cal = Calendar() cal.add('prodid', '-//Morozovka Portal//mxm.dk//') cal.add('version', '2.0') for ev in events: ievent = ICalEvent() ievent.add('summary', ev.title) ievent.add('dtstart', ev.start_time.astimezone(pytz.utc)) ievent.add('dtend', ev.end_time.astimezone(pytz.utc)) if ev.location: ievent.add('location', ev.location) if ev.description: ievent.add('description', ev.description) cal.add_component(ievent) response = make_response(cal.to_ical()) response.headers["Content-Disposition"] = "attachment; filename=morozovka_schedule.ics" response.headers["Content-Type"] = "text/calendar; charset=utf-8" return response @app.route("/api/assistant", methods=["POST"]) def api_assistant(): data = request.get_json() or {} message = (data.get("message") or "").strip() if not message: return jsonify({"error": "Нет сообщения"}), 400 history = data.get("history") or [] if llm_assistant.is_enabled(): try: reply = llm_assistant.chat(message, history) if reply: return jsonify({"reply": reply, "source": "llm"}) except Exception as e: print(f"[llm] fallback: {e}", flush=True) return jsonify({"reply": assistant.get_response(message), "source": "regex"}) @app.route("/admin/schedule") @require_role(["admin", "professor"]) def admin_schedule(): with Session(engine) as db_session: current_status = db_session.exec(select(GlobalStatus)).first() events = db_session.exec(select(Event).order_by(Event.start_time.desc()).limit(50)).all() return render_template("admin_schedule.html", status=current_status, events=events) @app.route("/admin/schedule/status", methods=["POST"]) @require_role(["admin", "professor"]) def update_status(): status_val = request.form.get("status") delay = int(request.form.get("delay_minutes") or 0) message = request.form.get("message") with Session(engine) as db_session: cs = db_session.exec(select(GlobalStatus)).first() if cs: old_label = status_label(cs.status, cs.delay_minutes) cs.status = status_val cs.delay_minutes = delay cs.message = message cs.updated_at = datetime.now(timezone.utc) db_session.add(cs); db_session.commit() new_label = status_label(status_val, delay) body = f"Новый статус: {new_label}\nБыло: {old_label}\n" if message: body += f"Сообщение: {message}\n" body += f"Обновлено: {to_msk(cs.updated_at).strftime('%d.%m.%Y %H:%M')}" notify_subscribers(f"[Расписание] {new_label}", body, kind="status") return redirect(url_for("admin_schedule")) @app.route("/admin/schedule/event/add", methods=["POST"]) @require_role(["admin", "professor"]) def add_event(): with Session(engine) as db_session: ev = Event(title=request.form.get("title"), event_type=request.form.get("event_type"), start_time=parse_datetime(request.form.get("start_time")), end_time=parse_datetime(request.form.get("end_time")), location=request.form.get("location"), description=request.form.get("description"), is_public=request.form.get("is_public") == "on") db_session.add(ev); db_session.commit(); db_session.refresh(ev) if ev.is_public: notify_subscribers(f"[Расписание] Новое: {ev.title}", "Добавлено событие.\n\n" + event_summary_text(ev), kind="event") return redirect(url_for("admin_schedule")) @app.route("/admin/schedule/event//edit", methods=["GET", "POST"]) @require_role(["admin", "professor"]) def edit_event(event_id): with Session(engine) as db_session: event = db_session.get(Event, event_id) if not event: return "Не найдено", 404 if request.method == "POST": event.title = request.form.get("title") event.event_type = request.form.get("event_type") event.start_time = parse_datetime(request.form.get("start_time")) event.end_time = parse_datetime(request.form.get("end_time")) event.location = request.form.get("location") event.description = request.form.get("description") event.is_public = request.form.get("is_public") == "on" event.manually_edited = True db_session.add(event); db_session.commit() if event.is_public: notify_subscribers(f"[Расписание] Изменено: {event.title}", "Событие изменено.\n\n" + event_summary_text(event), kind="event") return redirect(url_for("admin_schedule")) return render_template("admin_event_edit.html", event=event) @app.route("/admin/schedule/event//delete", methods=["POST"]) @require_role(["admin", "professor"]) def delete_event(event_id): with Session(engine) as db_session: event = db_session.get(Event, event_id) if event: t_snap = event.title s_snap = event.start_time was_public = event.is_public db_session.delete(event); db_session.commit() if was_public: body = f"Событие удалено.\n\nНазвание: {t_snap}\nБыло: {to_msk(s_snap).strftime('%d.%m.%Y %H:%M')}" notify_subscribers(f"[Расписание] Удалено: {t_snap}", body, kind="event") return redirect(url_for("admin_schedule")) @app.route("/admin/schedule/import", methods=["GET", "POST"]) @require_role(["admin", "professor"]) def import_ics(): if request.method == "POST": if 'file' not in request.files: return "Файл не загружен", 400 file = request.files['file'] if not file or not file.filename: return "Файл не выбран", 400 if file.filename.endswith('.ics'): try: cal = Calendar.from_ical(file.read()) count = 0 with Session(engine) as db_session: for comp in cal.walk(): if comp.name == "VEVENT": summary = str(comp.get('summary', 'Без названия')) ds = comp.get('dtstart'); de = comp.get('dtend') if not ds or not de: continue dtstart = ds.dt; dtend = de.dt if not isinstance(dtstart, datetime): dtstart = datetime.combine(dtstart, datetime.min.time()) if not isinstance(dtend, datetime): dtend = datetime.combine(dtend, datetime.min.time()) if dtstart.tzinfo is None: dtstart = dtstart.replace(tzinfo=timezone.utc) if dtend.tzinfo is None: dtend = dtend.replace(tzinfo=timezone.utc) db_session.add(Event( title=summary, event_type='lecture', start_time=dtstart.astimezone(timezone.utc), end_time=dtend.astimezone(timezone.utc), location=str(comp.get('location', '')), description=str(comp.get('description', '')), is_public=True)) count += 1 db_session.commit() if count > 0: notify_subscribers(f"[Расписание] Импорт: {count}", f"Добавлено: {count}", kind="event") return redirect(url_for("admin_schedule")) except Exception as e: return f"Ошибка: {e}", 400 return render_template("admin_schedule_import.html")