mirror of
https://github.com/EDeev/tablo.git
synced 2026-10-07 20:49:31 +03:00
- экспорт в JSON/CSV/PDF/PNG пускал любого вошедшего к любому расписанию, у которого есть хоть одна ссылка-приглашение (достаточно перебрать id); теперь — только владелец и редакторы, как и просмотр по id; - три копии проверки прав сведены в app/access.py; - без SECRET_KEY приложение не запускается (кроме отладки и тестов): со значением по умолчанию cookie сессии можно было подделать и войти под любым пользователем; - текст исключения от ИИ-провайдера больше не уходит в интерфейс, ошибка пишется в лог; - DATABASE_URL как альтернатива DB_*; debug в run.py — из FLASK_DEBUG; - неиспользуемые импорты и переменные удалены (ruff).
87 lines
2.9 KiB
Python
87 lines
2.9 KiB
Python
import urllib.parse
|
||
from flask import Blueprint, Response, redirect, url_for, flash
|
||
from flask_login import login_required
|
||
from app.models.schedule import Schedule
|
||
from app.access import can_view as _can_view
|
||
|
||
bp = Blueprint('export', __name__)
|
||
|
||
|
||
@bp.route('/schedules/<int:schedule_id>/export/json')
|
||
@login_required
|
||
def export_json(schedule_id: int):
|
||
schedule = Schedule.query.get_or_404(schedule_id)
|
||
if not _can_view(schedule):
|
||
flash('Нет доступа', 'danger')
|
||
return redirect(url_for('schedules.profile'))
|
||
from app.services.export import export_json as svc_json
|
||
data = svc_json(schedule)
|
||
filename = _safe_name(schedule.name) + '.json'
|
||
return Response(
|
||
data,
|
||
mimetype='application/json',
|
||
headers={'Content-Disposition': _cd(filename)},
|
||
)
|
||
|
||
|
||
@bp.route('/schedules/<int:schedule_id>/export/csv')
|
||
@login_required
|
||
def export_csv(schedule_id: int):
|
||
schedule = Schedule.query.get_or_404(schedule_id)
|
||
if not _can_view(schedule):
|
||
flash('Нет доступа', 'danger')
|
||
return redirect(url_for('schedules.profile'))
|
||
from app.services.export import export_csv as svc_csv
|
||
data = svc_csv(schedule)
|
||
filename = _safe_name(schedule.name) + '.csv'
|
||
return Response(
|
||
data,
|
||
mimetype='text/csv; charset=utf-8-sig',
|
||
headers={'Content-Disposition': _cd(filename)},
|
||
)
|
||
|
||
|
||
@bp.route('/schedules/<int:schedule_id>/export/pdf')
|
||
@login_required
|
||
def export_pdf(schedule_id: int):
|
||
schedule = Schedule.query.get_or_404(schedule_id)
|
||
if not _can_view(schedule):
|
||
flash('Нет доступа', 'danger')
|
||
return redirect(url_for('schedules.profile'))
|
||
from app.services.export import export_pdf as svc_pdf
|
||
data = svc_pdf(schedule)
|
||
filename = _safe_name(schedule.name) + '.pdf'
|
||
return Response(
|
||
data,
|
||
mimetype='application/pdf',
|
||
headers={'Content-Disposition': _cd(filename)},
|
||
)
|
||
|
||
|
||
@bp.route('/schedules/<int:schedule_id>/export/png')
|
||
@login_required
|
||
def export_png(schedule_id: int):
|
||
schedule = Schedule.query.get_or_404(schedule_id)
|
||
if not _can_view(schedule):
|
||
flash('Нет доступа', 'danger')
|
||
return redirect(url_for('schedules.profile'))
|
||
from app.services.export import export_png as svc_png
|
||
data = svc_png(schedule)
|
||
filename = _safe_name(schedule.name) + '.png'
|
||
return Response(
|
||
data,
|
||
mimetype='image/png',
|
||
headers={'Content-Disposition': _cd(filename)},
|
||
)
|
||
|
||
|
||
def _safe_name(name: str) -> str:
|
||
safe = ''.join(c if c.isalnum() or c in ' ._-' else '_' for c in name)
|
||
return safe.strip() or 'schedule'
|
||
|
||
|
||
def _cd(filename: str) -> str:
|
||
"""Content-Disposition с поддержкой Unicode (RFC 5987)."""
|
||
ascii_name = filename.encode('ascii', 'replace').decode('ascii')
|
||
utf8_name = urllib.parse.quote(filename, safe='')
|
||
return f"attachment; filename=\"{ascii_name}\"; filename*=UTF-8''{utf8_name}"
|