90 lines
3.6 KiB
Python
90 lines
3.6 KiB
Python
"""Departments, branches and teams, and who administers which."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String, Text, func
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
from sqlalchemy.orm import relationship
|
|
|
|
from app.config.database import Base
|
|
|
|
|
|
class OrgUnit(Base):
|
|
__tablename__ = "org_units"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True,
|
|
server_default=func.gen_random_uuid())
|
|
tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"),
|
|
nullable=False)
|
|
name = Column(String(160), nullable=False)
|
|
code = Column(String(60))
|
|
parent_id = Column(UUID(as_uuid=True), ForeignKey("org_units.id", ondelete="RESTRICT"))
|
|
path = Column(Text, nullable=False, default="/")
|
|
is_active = Column(Boolean, nullable=False, default=True)
|
|
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
|
deleted_at = Column(DateTime(timezone=True))
|
|
deleted_by_id = Column(UUID(as_uuid=True))
|
|
|
|
parent = relationship("OrgUnit", remote_side=[id], lazy="raise")
|
|
|
|
@property
|
|
def depth(self) -> int:
|
|
"""How far down the tree. `/` is depth 0."""
|
|
return max(0, len([part for part in (self.path or "/").split("/") if part]) - 1)
|
|
|
|
@property
|
|
def is_deleted(self) -> bool:
|
|
return self.deleted_at is not None
|
|
|
|
def descendant_prefix(self) -> str:
|
|
"""What a descendant's path starts with.
|
|
|
|
Includes this unit itself, because "everything I administer" always
|
|
means "this unit and everything under it" — a lead who could not manage
|
|
their own unit would be a strange kind of lead.
|
|
"""
|
|
return self.path or "/"
|
|
|
|
|
|
class UserOrgUnit(Base):
|
|
__tablename__ = "user_org_units"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True,
|
|
server_default=func.gen_random_uuid())
|
|
tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"),
|
|
nullable=False)
|
|
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False)
|
|
org_unit_id = Column(UUID(as_uuid=True),
|
|
ForeignKey("org_units.id", ondelete="CASCADE"), nullable=False)
|
|
is_primary = Column(Boolean, nullable=False, default=False)
|
|
is_lead = Column(Boolean, nullable=False, default=False)
|
|
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
|
|
|
org_unit = relationship("OrgUnit", lazy="raise")
|
|
|
|
|
|
class UserAdminScope(Base):
|
|
"""A unit whose people this person may administer.
|
|
|
|
Separate from membership and from being a lead. All three exist because they
|
|
are genuinely different: somebody can be *in* a unit without administering
|
|
it, can *lead* it without the platform granting them anything, and can
|
|
administer a unit they do not belong to — a regional HR administrator, for
|
|
instance.
|
|
"""
|
|
|
|
__tablename__ = "user_admin_scopes"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True,
|
|
server_default=func.gen_random_uuid())
|
|
tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"),
|
|
nullable=False)
|
|
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False)
|
|
org_unit_id = Column(UUID(as_uuid=True),
|
|
ForeignKey("org_units.id", ondelete="CASCADE"), nullable=False)
|
|
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
|
|
|
org_unit = relationship("OrgUnit", lazy="raise")
|