Flatten budget-app contents into repo root
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"""Who may do what with a department's budget.
|
||||
|
||||
Everyone signed in can view every budget. Changing and approving is limited to the budget's own
|
||||
department, following the workflow the administrator set up for it. Upper Management only views,
|
||||
downloads and emails budgets and reports, in every department.
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
from .models import BudgetYear, Department, User, WorkflowStep
|
||||
from .security import ROLES
|
||||
|
||||
|
||||
def rank(user: User) -> int:
|
||||
return ROLES.index(user.role)
|
||||
|
||||
|
||||
def is_admin(user: User) -> bool:
|
||||
return user.role == "admin"
|
||||
|
||||
|
||||
def is_management(user: User) -> bool:
|
||||
return user.role == "management"
|
||||
|
||||
|
||||
def member(user: User, department_id: int) -> bool:
|
||||
return user.department_id == department_id
|
||||
|
||||
|
||||
def can_edit_department(user: User, department_id: int) -> bool:
|
||||
"""Create, import and edit draft budgets of the department."""
|
||||
return is_admin(user) or (member(user, department_id) and rank(user) >= ROLES.index("editor"))
|
||||
|
||||
|
||||
def steps_of(department: Department) -> list[WorkflowStep]:
|
||||
return sorted(department.steps, key=lambda s: (s.position, s.id))
|
||||
|
||||
|
||||
def can_approve_step(user: User, step: WorkflowStep | None, department_id: int) -> bool:
|
||||
if step is None:
|
||||
# No workflow configured: the department's approvers sign off in one step.
|
||||
return member(user, department_id) and rank(user) >= ROLES.index("approver")
|
||||
if step.approver_mode == "users":
|
||||
return user.id in (step.approver_ids or []) and not is_management(user)
|
||||
return member(user, department_id) and rank(user) >= ROLES.index("approver")
|
||||
|
||||
|
||||
def current_step(budget: BudgetYear) -> WorkflowStep | None:
|
||||
steps = steps_of(budget.department)
|
||||
if budget.status != "in_review" or not steps:
|
||||
return None
|
||||
index = min(budget.current_step or 0, len(steps) - 1)
|
||||
return steps[index]
|
||||
|
||||
|
||||
def can_reopen(user: User, budget: BudgetYear) -> bool:
|
||||
return is_admin(user) or (member(user, budget.department_id) and rank(user) >= ROLES.index("approver"))
|
||||
|
||||
|
||||
def pending_reopen(budget: BudgetYear):
|
||||
return next((r for r in budget.reopen_requests if r.status == "pending"), None)
|
||||
|
||||
|
||||
def allowed_actions(user: User, budget: BudgetYear) -> dict:
|
||||
if budget.archived:
|
||||
# a replaced version: read-only for everyone; administrators may still delete it
|
||||
return {"edit": False, "submit": False, "approve": False, "return": False, "reopen": False,
|
||||
"request_reopen": False, "decide_reopen": False, "upload": False, "delete": is_admin(user)}
|
||||
dept = budget.department_id
|
||||
editor = can_edit_department(user, dept)
|
||||
approver = budget.status == "in_review" and can_approve_step(user, current_step(budget), dept)
|
||||
return {
|
||||
"edit": budget.status == "draft" and editor,
|
||||
"submit": budget.status == "draft" and editor,
|
||||
"approve": approver,
|
||||
"return": approver or (budget.status == "in_review" and is_admin(user)),
|
||||
"reopen": budget.status == "approved" and can_reopen(user, budget),
|
||||
# anyone may ask for an approved budget to be reopened (approvers reopen it straight away)
|
||||
"request_reopen": budget.status == "approved" and pending_reopen(budget) is None and not is_management(user),
|
||||
"decide_reopen": budget.status == "approved" and pending_reopen(budget) is not None and can_reopen(user, budget),
|
||||
"upload": budget.status == "draft" and editor,
|
||||
# Deleting a budget removes it and its reports for everyone, so only administrators may, in any status.
|
||||
"delete": is_admin(user),
|
||||
}
|
||||
|
||||
|
||||
def require_action(user: User, budget: BudgetYear, action: str) -> None:
|
||||
if not allowed_actions(user, budget)[action]:
|
||||
if action == "edit" and budget.status != "draft":
|
||||
raise HTTPException(409, "year_not_draft")
|
||||
raise HTTPException(403, "forbidden")
|
||||
|
||||
|
||||
def require_department_editor(user: User, department_id: int) -> None:
|
||||
if not can_edit_department(user, department_id):
|
||||
raise HTTPException(403, "forbidden")
|
||||
Reference in New Issue
Block a user