You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
135 lines
4.9 KiB
135 lines
4.9 KiB
from __future__ import annotations
|
|
|
|
from flask import Blueprint, Response, flash, g, redirect, render_template, request, stream_with_context, url_for
|
|
|
|
from app.auth import role_required
|
|
from app.extensions import db
|
|
from app.models import Account
|
|
from app.services import (
|
|
build_household_csv_template,
|
|
build_household_export_filename,
|
|
delete_household_import_preview,
|
|
get_household_import_conflict_modes,
|
|
list_households,
|
|
load_household_import_preview,
|
|
preview_household_csv,
|
|
stream_household_csv,
|
|
write_audit_log,
|
|
apply_household_import_preview,
|
|
)
|
|
from app.services.csv_households import CsvImportError
|
|
|
|
csv_bp = Blueprint("csv", __name__, url_prefix="/csv")
|
|
|
|
|
|
@csv_bp.get("/households/import")
|
|
@role_required("admin", "editor")
|
|
def import_households_page() -> str:
|
|
preview_token = request.args.get("preview_token", "").strip()
|
|
preview = load_household_import_preview(preview_token) if preview_token else None
|
|
return render_template(
|
|
"csv/import_preview.html",
|
|
preview=preview,
|
|
conflict_modes=get_household_import_conflict_modes(),
|
|
)
|
|
|
|
|
|
@csv_bp.post("/households/import/preview")
|
|
@role_required("admin", "editor")
|
|
def preview_households_import():
|
|
file_storage = request.files.get("file")
|
|
if file_storage is None or not file_storage.filename:
|
|
flash("请先选择一个 CSV 文件。", "error")
|
|
return redirect(url_for("csv.import_households_page"))
|
|
|
|
try:
|
|
preview = preview_household_csv(
|
|
file_name=file_storage.filename,
|
|
file_bytes=file_storage.read(),
|
|
)
|
|
except CsvImportError as exc:
|
|
flash(str(exc), "error")
|
|
return redirect(url_for("csv.import_households_page"))
|
|
|
|
flash("CSV 预览已生成,请先检查无效行和冲突行。", "success")
|
|
return redirect(url_for("csv.import_households_page", preview_token=preview["token"]))
|
|
|
|
|
|
@csv_bp.post("/households/import/confirm")
|
|
@role_required("admin", "editor")
|
|
def confirm_households_import():
|
|
preview_token = request.form.get("preview_token", "").strip()
|
|
conflict_mode = request.form.get("conflict_mode", "").strip()
|
|
preview = load_household_import_preview(preview_token)
|
|
if preview is None:
|
|
flash("导入预览已失效,请重新上传 CSV。", "error")
|
|
return redirect(url_for("csv.import_households_page"))
|
|
|
|
current_account = g.current_account if isinstance(getattr(g, "current_account", None), Account) else None
|
|
try:
|
|
summary = apply_household_import_preview(
|
|
preview=preview,
|
|
actor_id=current_account.id if current_account is not None else None,
|
|
conflict_mode=conflict_mode,
|
|
)
|
|
except CsvImportError as exc:
|
|
flash(str(exc), "error")
|
|
return redirect(url_for("csv.import_households_page", preview_token=preview_token))
|
|
|
|
write_audit_log(
|
|
action_type="import_households_csv",
|
|
target_type="household",
|
|
actor=current_account,
|
|
target_display_name=str(summary["file_name"]),
|
|
after_data=summary,
|
|
commit=False,
|
|
)
|
|
db.session.commit()
|
|
delete_household_import_preview(preview_token)
|
|
flash(
|
|
f"CSV 导入完成:新增 {summary['rows_created']} 户,更新 {summary['rows_updated']} 户,跳过 {summary['rows_skipped']} 行,无效 {summary['rows_invalid']} 行。",
|
|
"success",
|
|
)
|
|
return redirect(url_for("main.index"))
|
|
|
|
|
|
@csv_bp.get("/households/template")
|
|
@role_required("admin", "editor")
|
|
def download_household_template() -> Response:
|
|
return Response(
|
|
build_household_csv_template(),
|
|
mimetype="text/csv; charset=utf-8",
|
|
headers={
|
|
"Content-Disposition": f"attachment; filename={build_household_export_filename(scope='template')}",
|
|
"Cache-Control": "no-store",
|
|
},
|
|
)
|
|
|
|
|
|
@csv_bp.get("/households/export")
|
|
@role_required("admin", "editor")
|
|
def export_households() -> Response:
|
|
scope = request.args.get("scope", "filtered").strip()
|
|
if scope == "all":
|
|
households = list_households(limit=None)
|
|
filename = build_household_export_filename(scope="all")
|
|
else:
|
|
households = list_households(
|
|
search_term=request.args.get("q", "").strip(),
|
|
side=request.args.get("side", "").strip(),
|
|
invite_status=request.args.get("invite_status", "").strip(),
|
|
attendance_status=request.args.get("attendance_status", "").strip(),
|
|
sort_by=request.args.get("sort_by", "updated_at").strip(),
|
|
sort_order=request.args.get("sort_order", "desc").strip(),
|
|
limit=None,
|
|
)
|
|
filename = build_household_export_filename(scope="filtered")
|
|
|
|
return Response(
|
|
stream_with_context(stream_household_csv(households)),
|
|
mimetype="text/csv; charset=utf-8",
|
|
headers={
|
|
"Content-Disposition": f"attachment; filename={filename}",
|
|
"Cache-Control": "no-store",
|
|
},
|
|
)
|
|
|