209 lines
11 KiB
Python
209 lines
11 KiB
Python
"""Роуты расписания: публичная страница, экспорт 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/<int:event_id>/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/<int:event_id>/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")
|