Files
cowork-local/core/account_excel.py
T
2026-08-09 20:12:05 +07:00

153 lines
6.2 KiB
Python

"""Bulk-create Groups + Accounts from an Excel list (Admin only).
Mirrors ``task_excel.py``'s template/import pattern: the app exports a
ready-made ``.xlsx`` template (with a README sheet), the Admin fills one row
per person, and importing creates any missing Groups by name plus one
Account per row — each with a freshly generated 12-character access code.
The generated codes are returned so the caller can show/export them (they
exist nowhere else in plain sight; the Admin hands them out).
"""
from __future__ import annotations
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from . import accounts, groups
HEADERS = ["Username", "Display name", "Email", "Department", "Role", "Group"]
_EXAMPLE_ROWS = [
["nguyenva1", "Nguyen Van A", "nguyenva1@company.com", "CAE", "user", "CAE Team"],
["tranthib2", "Tran Thi B", "tranthib2@company.com", "CAE", "subadmin", "CAE Team"],
]
def export_template(path: str | Path) -> Path:
"""Write the import template (Accounts sheet + README) to ``path``."""
from openpyxl import Workbook
path = Path(path)
wb = Workbook()
ws = wb.active
ws.title = "Accounts"
ws.append(HEADERS)
for row in _EXAMPLE_ROWS:
ws.append(row)
for col, width in zip("ABCDEF", (18, 24, 26, 16, 12, 20)):
ws.column_dimensions[col].width = width
readme = wb.create_sheet("README")
for line in (
"One row per person. Username: lowercase letters/digits (the login name).",
"Role: user or subadmin. 'admin' is NOT allowed here — the app has exactly one Admin.",
"Group: a group name; groups that don't exist yet are created automatically.",
"On import, every account gets a fresh 12-character access code —",
"the app shows/exports the codes once so the Admin can distribute them.",
"Rows whose Username already exists are skipped (never overwritten).",
):
readme.append([line])
readme.column_dimensions["A"].width = 100
path.parent.mkdir(parents=True, exist_ok=True)
wb.save(path)
return path
def import_accounts(path: str | Path, shared_dir: str,
created_by: str = "") -> Tuple[List[accounts.Account], List[str]]:
"""Create groups + accounts from a filled template.
Returns ``(created_accounts, warnings)``. Never creates a second admin
(role 'admin' rows are downgraded to 'user' with a warning), never
overwrites an existing username (skipped with a warning). Raises
``ValueError`` for an unreadable/empty file — mirroring
``task_excel.import_tasks``'s error contract."""
from openpyxl import load_workbook
try:
wb = load_workbook(str(path), data_only=True)
except Exception as exc: # noqa: BLE001 — one readable error for any bad file
raise ValueError(f"Could not read the Excel file: {exc}") from exc
ws = wb["Accounts"] if "Accounts" in wb.sheetnames else wb.worksheets[0]
acc_dir = accounts.accounts_dir(shared_dir)
grp_dir = groups.groups_dir(shared_dir)
existing_codes = {a.code for a in accounts.list_accounts(acc_dir)}
groups_by_name: Dict[str, groups.Group] = {
g.name.strip().lower(): g for g in groups.list_groups(grp_dir)
}
created: List[accounts.Account] = []
warnings: List[str] = []
rows = ws.iter_rows(min_row=2, values_only=True)
for i, row in enumerate(rows, start=2):
cells = [str(c).strip() if c is not None else "" for c in (row or ())]
cells += [""] * (len(HEADERS) - len(cells))
username, display_name, email, department, role, group_name = cells[:6]
if not username:
continue
role = (role or "user").lower()
if role == "admin":
warnings.append(f"Row {i}: role 'admin' is not allowed (single-admin app) — created as 'user'.")
role = "user"
if role not in accounts.ROLES:
warnings.append(f"Row {i}: unknown role '{role}' — created as 'user'.")
role = "user"
if accounts.load_account(username, acc_dir) is not None:
warnings.append(f"Row {i}: account '{username}' already exists — skipped.")
continue
group_id = ""
if group_name:
key = group_name.strip().lower()
group = groups_by_name.get(key)
if group is None:
group = groups.new_group(group_name.strip())
groups.save_group(group, grp_dir)
groups_by_name[key] = group
group_id = group.group_id
account = accounts.new_account(
username, role, display_name=display_name, department=department,
email=email, group_id=group_id, created_by=created_by,
existing_codes=existing_codes)
accounts.save_account(account, acc_dir)
existing_codes.add(account.code)
created.append(account)
if group_id:
group = groups_by_name[group_name.strip().lower()]
if role == "subadmin" and not group.subadmin_username:
group.subadmin_username = account.username
groups.save_group(group, grp_dir)
elif account.username not in group.member_usernames:
group.member_usernames.append(account.username)
groups.save_group(group, grp_dir)
if not created and not warnings:
raise ValueError("No account rows found in the file (fill the 'Accounts' sheet).")
return created, warnings
def export_issued_codes(created: List[accounts.Account], path: str | Path) -> Optional[Path]:
"""Write the just-created accounts + their access codes to an xlsx the
Admin can distribute from. Best-effort: returns None on write failure
(the codes were already shown in the UI)."""
from openpyxl import Workbook
if not created:
return None
try:
path = Path(path)
wb = Workbook()
ws = wb.active
ws.title = "Issued codes"
ws.append(["Username", "Display name", "Role", "Group", "Access code"])
for a in created:
ws.append([a.username, a.display_name, a.role, a.group_id, a.code])
for col, width in zip("ABCDE", (18, 24, 12, 20, 18)):
ws.column_dimensions[col].width = width
wb.save(path)
return path
except Exception: # noqa: BLE001
return None