Add firm admin bulk user import with template
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import date, datetime, timezone
|
from datetime import date, datetime, timezone
|
||||||
|
from io import BytesIO
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from openpyxl import Workbook, load_workbook
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -520,6 +522,196 @@ def create_firm_internal_user(
|
|||||||
"email_error": email_error,
|
"email_error": email_error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FIRM_USER_IMPORT_HEADERS = [
|
||||||
|
"role",
|
||||||
|
"full_name",
|
||||||
|
"email",
|
||||||
|
"branch_id",
|
||||||
|
"branch_code",
|
||||||
|
"branch_name",
|
||||||
|
"employee_code",
|
||||||
|
"mobile",
|
||||||
|
"department",
|
||||||
|
"designation",
|
||||||
|
"date_of_joining",
|
||||||
|
"employment_type",
|
||||||
|
]
|
||||||
|
|
||||||
|
FIRM_USER_IMPORT_SAMPLE_ROWS = [
|
||||||
|
["partner", "Sample Partner", "partner@example.com", "", "HO", "", "PTR001", "9999999999", "Management", "Partner", "2026-04-01", "full_time"],
|
||||||
|
["manager", "Sample Manager", "manager@example.com", "", "HO", "", "MGR001", "9999999998", "Operations", "Branch Manager", "2026-04-01", "full_time"],
|
||||||
|
["staff", "Sample Staff", "staff@example.com", "", "HO", "", "EMP001", "9999999997", "Audit", "Associate", "2026-04-01", "full_time"],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def build_firm_user_import_template(db: Session, request, current_user) -> bytes:
|
||||||
|
roles = set(get_user_role_names(db, current_user.id))
|
||||||
|
tenant_id = _active_tenant_id(request, current_user, roles)
|
||||||
|
tenant = _tenant(db, tenant_id)
|
||||||
|
branches = _branch_rows(db, tenant_id)
|
||||||
|
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "Firm Users"
|
||||||
|
ws.append(FIRM_USER_IMPORT_HEADERS)
|
||||||
|
for row in FIRM_USER_IMPORT_SAMPLE_ROWS:
|
||||||
|
ws.append(row)
|
||||||
|
|
||||||
|
notes = wb.create_sheet("Instructions")
|
||||||
|
notes.append(["Firm", getattr(tenant, "display_name", None) or getattr(tenant, "name", None) or "Active firm"])
|
||||||
|
notes.append(["Allowed role values", "partner, manager, staff"])
|
||||||
|
notes.append(["Manager mapping", "manager imports as existing Branch Manager role"])
|
||||||
|
notes.append(["Branch selection", "Use branch_id OR branch_code OR exact branch_name from Branches sheet"])
|
||||||
|
notes.append(["Employment type values", "full_time, part_time, article_assistant, intern, consultant, contract"])
|
||||||
|
notes.append(["Date format", "YYYY-MM-DD preferred"])
|
||||||
|
notes.append(["Invite", "Each valid row creates user + employee link + invite token. Invite link is shown in import result."])
|
||||||
|
|
||||||
|
branch_sheet = wb.create_sheet("Branches")
|
||||||
|
branch_sheet.append(["branch_id", "branch_code", "branch_name", "is_active", "is_head_office"])
|
||||||
|
for branch in branches:
|
||||||
|
branch_sheet.append([branch.get("id"), branch.get("code"), branch.get("name"), branch.get("is_active"), branch.get("is_head_office")])
|
||||||
|
|
||||||
|
for sheet in wb.worksheets:
|
||||||
|
for cell in sheet[1]:
|
||||||
|
cell.font = cell.font.copy(bold=True)
|
||||||
|
for column_cells in sheet.columns:
|
||||||
|
letter = column_cells[0].column_letter
|
||||||
|
width = max(14, min(34, max(len(str(c.value or "")) for c in column_cells) + 3))
|
||||||
|
sheet.column_dimensions[letter].width = width
|
||||||
|
|
||||||
|
bio = BytesIO()
|
||||||
|
wb.save(bio)
|
||||||
|
return bio.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def _cell_text(value: Any) -> str:
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value.date().isoformat()
|
||||||
|
if isinstance(value, date):
|
||||||
|
return value.isoformat()
|
||||||
|
return str(value).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_firm_user_import_rows(content: bytes) -> list[dict[str, Any]]:
|
||||||
|
try:
|
||||||
|
wb = load_workbook(BytesIO(content), data_only=True)
|
||||||
|
except Exception as exc:
|
||||||
|
raise ValueError(f"Unable to read Excel file: {exc}") from exc
|
||||||
|
ws = wb["Firm Users"] if "Firm Users" in wb.sheetnames else wb.active
|
||||||
|
headers = [str(cell.value or "").strip().lower() for cell in ws[1]]
|
||||||
|
if not any(headers):
|
||||||
|
raise ValueError("Excel file has no header row.")
|
||||||
|
rows: list[dict[str, Any]] = []
|
||||||
|
for row_no, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
|
||||||
|
if not any(value not in (None, "") for value in row):
|
||||||
|
continue
|
||||||
|
item = {headers[idx]: _cell_text(row[idx] if idx < len(row) else None) for idx in range(len(headers)) if headers[idx]}
|
||||||
|
item["_row_no"] = row_no
|
||||||
|
rows.append(item)
|
||||||
|
if not rows:
|
||||||
|
raise ValueError("Excel file has no data rows.")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_import_branch_id(db: Session, tenant_id: int, row: dict[str, Any]) -> int:
|
||||||
|
raw_id = _cell_text(row.get("branch_id"))
|
||||||
|
raw_code = _cell_text(row.get("branch_code"))
|
||||||
|
raw_name = _cell_text(row.get("branch_name"))
|
||||||
|
|
||||||
|
branch = None
|
||||||
|
if raw_id:
|
||||||
|
try:
|
||||||
|
branch = db.get(Branch, int(float(raw_id)))
|
||||||
|
except Exception as exc:
|
||||||
|
raise ValueError(f"Invalid branch_id '{raw_id}'.") from exc
|
||||||
|
elif raw_code:
|
||||||
|
branch = db.execute(
|
||||||
|
select(Branch).where(Branch.tenant_id == int(tenant_id), func.lower(Branch.code) == raw_code.lower())
|
||||||
|
).scalar_one_or_none()
|
||||||
|
elif raw_name:
|
||||||
|
branch = db.execute(
|
||||||
|
select(Branch).where(Branch.tenant_id == int(tenant_id), func.lower(Branch.name) == raw_name.lower())
|
||||||
|
).scalar_one_or_none()
|
||||||
|
else:
|
||||||
|
active = db.execute(
|
||||||
|
select(Branch)
|
||||||
|
.where(Branch.tenant_id == int(tenant_id), Branch.is_active.is_(True))
|
||||||
|
.order_by(Branch.is_head_office.desc(), Branch.id.asc())
|
||||||
|
).scalars().all()
|
||||||
|
if len(active) == 1:
|
||||||
|
branch = active[0]
|
||||||
|
else:
|
||||||
|
raise ValueError("Branch is required when the firm has multiple active branches.")
|
||||||
|
|
||||||
|
if not branch or int(branch.tenant_id) != int(tenant_id):
|
||||||
|
raise ValueError("Branch does not belong to the active firm.")
|
||||||
|
if not branch.is_active:
|
||||||
|
raise ValueError("Selected branch is inactive.")
|
||||||
|
return int(branch.id)
|
||||||
|
|
||||||
|
|
||||||
|
def import_firm_internal_users(db: Session, *, request, actor: User, content: bytes) -> dict[str, Any]:
|
||||||
|
actor_roles = set(get_user_role_names(db, actor.id))
|
||||||
|
if not actor_roles.intersection(FIRM_ADMIN_ROLES):
|
||||||
|
raise PermissionError("Only Firm Admin or System Admin can import firm users.")
|
||||||
|
tenant_id = _active_tenant_id(request, actor, actor_roles)
|
||||||
|
if not tenant_id:
|
||||||
|
raise ValueError("Active firm context is required before importing users.")
|
||||||
|
|
||||||
|
rows = _load_firm_user_import_rows(content)
|
||||||
|
results: list[dict[str, Any]] = []
|
||||||
|
created = failed = 0
|
||||||
|
invite_links: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
row_no = int(row.get("_row_no") or 0)
|
||||||
|
role_key = _cell_text(row.get("role") or "staff").lower()
|
||||||
|
email = _cell_text(row.get("email")).lower()
|
||||||
|
full_name = _cell_text(row.get("full_name"))
|
||||||
|
try:
|
||||||
|
if not get_onboarding_role_config(role_key):
|
||||||
|
raise ValueError("Role must be one of partner, manager or staff.")
|
||||||
|
branch_id = _resolve_import_branch_id(db, int(tenant_id), row)
|
||||||
|
result = create_firm_internal_user(
|
||||||
|
db,
|
||||||
|
request=request,
|
||||||
|
actor=actor,
|
||||||
|
role_key=role_key,
|
||||||
|
email=email,
|
||||||
|
full_name=full_name,
|
||||||
|
branch_id=branch_id,
|
||||||
|
employee_code=_cell_text(row.get("employee_code")),
|
||||||
|
mobile=_cell_text(row.get("mobile")),
|
||||||
|
department=_cell_text(row.get("department")),
|
||||||
|
designation=_cell_text(row.get("designation")),
|
||||||
|
date_of_joining=_cell_text(row.get("date_of_joining")),
|
||||||
|
employment_type=_cell_text(row.get("employment_type")) or None,
|
||||||
|
)
|
||||||
|
created += 1
|
||||||
|
invite_url = result.get("invite_url")
|
||||||
|
invite_links.append({
|
||||||
|
"row_no": row_no,
|
||||||
|
"name": full_name,
|
||||||
|
"email": email,
|
||||||
|
"role": result.get("role_name"),
|
||||||
|
"invite_url": invite_url,
|
||||||
|
"email_status": result.get("email_status"),
|
||||||
|
})
|
||||||
|
results.append({"row_no": row_no, "status": "created", "email": email, "name": full_name, "role": result.get("role_name"), "message": "User, role, employee link and invite created."})
|
||||||
|
except Exception as exc:
|
||||||
|
db.rollback()
|
||||||
|
failed += 1
|
||||||
|
results.append({"row_no": row_no, "status": "failed", "email": email, "name": full_name, "role": role_key, "message": str(exc)})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"summary": {"total": len(rows), "created": created, "failed": failed},
|
||||||
|
"rows": results,
|
||||||
|
"invite_links": invite_links,
|
||||||
|
}
|
||||||
|
|
||||||
def build_dashboard_payload(db: Session, request, current_user) -> dict[str, Any]:
|
def build_dashboard_payload(db: Session, request, current_user) -> dict[str, Any]:
|
||||||
roles = set(get_user_role_names(db, current_user.id))
|
roles = set(get_user_role_names(db, current_user.id))
|
||||||
tenant_id = _active_tenant_id(request, current_user, roles)
|
tenant_id = _active_tenant_id(request, current_user, roles)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<a href="/firm-admin/users/new?role=partner" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">+ Add Partner</a>
|
<a href="/firm-admin/users/new?role=partner" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">+ Add Partner</a>
|
||||||
<a href="/firm-admin/users/new?role=manager" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">+ Add Manager</a>
|
<a href="/firm-admin/users/new?role=manager" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">+ Add Manager</a>
|
||||||
<a href="/firm-admin/users/new?role=staff" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">+ Add Staff</a>
|
<a href="/firm-admin/users/new?role=staff" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">+ Add Staff</a>
|
||||||
|
<a href="/firm-admin/users/import" class="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700">Import Users</a>
|
||||||
|
<a href="/firm-admin/users/import/template" class="rounded-xl border border-emerald-200 px-4 py-2 text-sm font-semibold text-emerald-700 hover:bg-emerald-50">Template</a>
|
||||||
<a href="/system-settings/users" class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Manage Users</a>
|
<a href="/system-settings/users" class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Manage Users</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{% extends "ui/templates/base/layout.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="mx-auto max-w-4xl space-y-6">
|
||||||
|
<div class="rounded-3xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||||
|
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-brand-600">Firm Admin Bulk Onboarding</p>
|
||||||
|
<h1 class="mt-2 text-2xl font-bold text-slate-900">Import Partner / Manager / Staff</h1>
|
||||||
|
<p class="mt-1 text-sm text-slate-500">Upload the Excel template to create user accounts, map roles and branches, create/link employee masters and generate invite links in bulk.</p>
|
||||||
|
</div>
|
||||||
|
<a href="/firm-admin/dashboard?tab=users" class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Back to Users</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if flash %}
|
||||||
|
<div class="rounded-3xl border border-red-200 bg-red-50 p-5 text-sm font-semibold text-red-800 shadow-soft">{{ flash }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="grid gap-5 md:grid-cols-2">
|
||||||
|
<div class="rounded-3xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||||
|
<h2 class="text-lg font-bold text-slate-900">1. Download template</h2>
|
||||||
|
<p class="mt-2 text-sm text-slate-500">The template includes role values, sample rows and your active firm's branch list.</p>
|
||||||
|
<a href="/firm-admin/users/import/template" class="mt-5 inline-flex rounded-xl bg-emerald-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-emerald-700">Download Excel Template</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" action="/firm-admin/users/import" enctype="multipart/form-data" class="rounded-3xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||||
|
<h2 class="text-lg font-bold text-slate-900">2. Upload completed file</h2>
|
||||||
|
<p class="mt-2 text-sm text-slate-500">Allowed roles: <b>partner</b>, <b>manager</b>, <b>staff</b>. Manager will be assigned your existing Branch Manager role.</p>
|
||||||
|
<label class="mt-5 block">
|
||||||
|
<span class="text-sm font-semibold text-slate-700">Excel File <span class="text-red-600">*</span></span>
|
||||||
|
<input type="file" name="import_file" accept=".xlsx,.xlsm" required class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100" />
|
||||||
|
</label>
|
||||||
|
<button class="mt-5 rounded-xl bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white shadow-soft hover:bg-brand-700">Import Users</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-3xl border border-amber-200 bg-amber-50 p-5 text-sm text-amber-900 shadow-soft">
|
||||||
|
<div class="font-bold">Important</div>
|
||||||
|
<p class="mt-1">Do not keep the sample rows unless you want them imported. Email and employee code must be unique. If email sending fails, the fallback invite links will still be shown after import.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{% extends "ui/templates/base/layout.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="mx-auto max-w-6xl space-y-6">
|
||||||
|
<div class="rounded-3xl border border-slate-200 bg-white p-6 shadow-soft">
|
||||||
|
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-brand-600">Firm Admin Bulk Onboarding</p>
|
||||||
|
<h1 class="mt-2 text-2xl font-bold text-slate-900">Import Result</h1>
|
||||||
|
<p class="mt-1 text-sm text-slate-500">Created {{ import_result.summary.created }} of {{ import_result.summary.total }} rows. Failed {{ import_result.summary.failed }} rows.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<a href="/firm-admin/users/import" class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Import More</a>
|
||||||
|
<a href="/firm-admin/dashboard?tab=users" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Back to Users</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if import_result.invite_links %}
|
||||||
|
<div class="rounded-3xl border border-emerald-200 bg-emerald-50 p-5 shadow-soft">
|
||||||
|
<h2 class="text-lg font-bold text-emerald-900">Invite link fallback</h2>
|
||||||
|
<p class="mt-1 text-sm text-emerald-800">Copy these links if automatic email is not configured or failed for any user.</p>
|
||||||
|
<div class="mt-4 space-y-3">
|
||||||
|
{% for item in import_result.invite_links %}
|
||||||
|
<div class="rounded-2xl border border-emerald-100 bg-white p-4">
|
||||||
|
<div class="text-sm font-bold text-slate-900">Row {{ item.row_no }} — {{ item.name }} / {{ item.email }} / {{ item.role }}</div>
|
||||||
|
<div class="mt-1 text-xs font-semibold text-slate-500">Email status: {{ item.email_status or 'not_attempted' }}</div>
|
||||||
|
<input value="{{ item.invite_url }}" readonly onclick="this.select()" class="mt-2 w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-700" />
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-3xl border border-slate-200 bg-white shadow-soft">
|
||||||
|
<table class="min-w-full divide-y divide-slate-100 text-sm">
|
||||||
|
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||||
|
<tr><th class="px-5 py-3">Row</th><th class="px-5 py-3">Status</th><th class="px-5 py-3">Name</th><th class="px-5 py-3">Email</th><th class="px-5 py-3">Role</th><th class="px-5 py-3">Message</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
{% for row in import_result.rows %}
|
||||||
|
<tr class="hover:bg-slate-50">
|
||||||
|
<td class="px-5 py-3">{{ row.row_no }}</td>
|
||||||
|
<td class="px-5 py-3"><span class="rounded-full px-2 py-1 text-xs font-semibold {% if row.status == 'created' %}bg-emerald-50 text-emerald-700{% else %}bg-red-50 text-red-700{% endif %}">{{ row.status }}</span></td>
|
||||||
|
<td class="px-5 py-3 text-slate-700">{{ row.name or '-' }}</td>
|
||||||
|
<td class="px-5 py-3 text-slate-700">{{ row.email or '-' }}</td>
|
||||||
|
<td class="px-5 py-3 text-slate-700">{{ row.role or '-' }}</td>
|
||||||
|
<td class="px-5 py-3 text-slate-600">{{ row.message }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from fastapi import APIRouter, Form, Request
|
from io import BytesIO
|
||||||
from fastapi.responses import RedirectResponse
|
|
||||||
|
from fastapi import APIRouter, File, Form, Request, UploadFile
|
||||||
|
from fastapi.responses import RedirectResponse, StreamingResponse
|
||||||
|
|
||||||
from app.core.db.common import CommonSessionLocal
|
from app.core.db.common import CommonSessionLocal
|
||||||
from app.core.http_responses import ui_access_denied
|
from app.core.http_responses import ui_access_denied
|
||||||
@@ -13,8 +15,10 @@ from app.modules.firm_admin_dashboard.service import (
|
|||||||
build_dashboard_payload,
|
build_dashboard_payload,
|
||||||
build_user_onboarding_payload,
|
build_user_onboarding_payload,
|
||||||
can_access_firm_admin_dashboard,
|
can_access_firm_admin_dashboard,
|
||||||
|
build_firm_user_import_template,
|
||||||
create_firm_internal_user,
|
create_firm_internal_user,
|
||||||
get_onboarding_role_config,
|
get_onboarding_role_config,
|
||||||
|
import_firm_internal_users,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/firm-admin", tags=["firm-admin-dashboard-ui"])
|
router = APIRouter(prefix="/firm-admin", tags=["firm-admin-dashboard-ui"])
|
||||||
@@ -192,3 +196,73 @@ def user_onboarding_submit(
|
|||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
@router.get("/users/import")
|
||||||
|
def user_import_form(request: Request):
|
||||||
|
db = CommonSessionLocal()
|
||||||
|
try:
|
||||||
|
current_user = get_current_user(request, db=db)
|
||||||
|
if not current_user:
|
||||||
|
return _redirect_login()
|
||||||
|
if not can_access_firm_admin_dashboard(db, current_user):
|
||||||
|
return ui_access_denied()
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"modules/firm_admin_dashboard/templates/firm_admin_dashboard/user_import.html",
|
||||||
|
_ctx(request, db, current_user, active_tab="users", title="Import Firm Users"),
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/users/import/template")
|
||||||
|
def user_import_template(request: Request):
|
||||||
|
db = CommonSessionLocal()
|
||||||
|
try:
|
||||||
|
current_user = get_current_user(request, db=db)
|
||||||
|
if not current_user:
|
||||||
|
return _redirect_login()
|
||||||
|
if not can_access_firm_admin_dashboard(db, current_user):
|
||||||
|
return ui_access_denied()
|
||||||
|
content = build_firm_user_import_template(db, request, current_user)
|
||||||
|
return StreamingResponse(
|
||||||
|
BytesIO(content),
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
headers={"Content-Disposition": 'attachment; filename="firm_admin_user_import_template.xlsx"'},
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/users/import")
|
||||||
|
async def user_import_submit(request: Request, import_file: UploadFile = File(...), csrf_token: str = Form(...)):
|
||||||
|
validate_csrf(request, csrf_token)
|
||||||
|
db = CommonSessionLocal()
|
||||||
|
try:
|
||||||
|
current_user = get_current_user(request, db=db)
|
||||||
|
if not current_user:
|
||||||
|
return _redirect_login()
|
||||||
|
if not can_access_firm_admin_dashboard(db, current_user):
|
||||||
|
return ui_access_denied()
|
||||||
|
if not import_file.filename or not import_file.filename.lower().endswith((".xlsx", ".xlsm")):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"modules/firm_admin_dashboard/templates/firm_admin_dashboard/user_import.html",
|
||||||
|
_ctx(request, db, current_user, active_tab="users", title="Import Firm Users", flash="Please upload an .xlsx Excel file."),
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
content = await import_file.read()
|
||||||
|
result = import_firm_internal_users(db, request=request, actor=current_user, content=content)
|
||||||
|
except Exception as exc:
|
||||||
|
db.rollback()
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"modules/firm_admin_dashboard/templates/firm_admin_dashboard/user_import.html",
|
||||||
|
_ctx(request, db, current_user, active_tab="users", title="Import Firm Users", flash=str(exc)),
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"modules/firm_admin_dashboard/templates/firm_admin_dashboard/user_import_done.html",
|
||||||
|
_ctx(request, db, current_user, active_tab="users", title="Import Result", import_result=result),
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user