Initialize organizations module

This commit is contained in:
2026-07-10 11:01:37 +02:00
commit 34133ba9cd
20 changed files with 1005 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
---
name: "Bug"
about: "Report a reproducible defect, regression, or incorrect behavior"
title: "[Bug] "
labels:
- type/bug
- status/triage
- module/organizations
---
## Scope
- Repository:
- Area/module:
- Affected version or commit:
## Behavior
Expected:
Actual:
## Reproduction
1.
2.
3.
## Evidence
Logs, screenshots, traces, or failing test output:
## Verification Target
Command or workflow that should pass when fixed:

View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -0,0 +1,27 @@
---
name: "Docs / workflow"
about: "Request documentation, process, or developer workflow changes"
title: "[Docs] "
labels:
- type/docs
- status/triage
- module/organizations
- area/docs
---
## Scope
- Repository:
- Document or workflow:
## Current State
What is missing, unclear, duplicated, or stale?
## Desired State
What should the docs or workflow make clear?
## Verification Target
How should this be checked?

View File

@@ -0,0 +1,32 @@
---
name: "Feature"
about: "Propose new user-visible behavior or platform capability"
title: "[Feature] "
labels:
- type/feature
- status/triage
- module/organizations
---
## Problem
What user, operator, or developer problem should this solve?
## Proposed Capability
What should exist when this is done?
## Ownership
- Owning repository:
- Related module repositories:
- Extension point or integration boundary:
## Acceptance Criteria
- [ ]
- [ ]
## Verification Target
Command, scenario, or UI flow that should prove completion:

View File

@@ -0,0 +1,28 @@
---
name: "Task"
about: "Track implementation, maintenance, or migration work"
title: "[Task] "
labels:
- type/task
- status/triage
- module/organizations
---
## Objective
What needs to be completed?
## Scope
- Owning repository:
- In-scope:
- Out-of-scope:
## Checklist
- [ ]
- [ ]
## Verification Target
Command or manual check:

View File

@@ -0,0 +1,25 @@
---
name: "Tech debt"
about: "Track cleanup, refactoring, risk reduction, or deferred engineering work"
title: "[Debt] "
labels:
- type/debt
- status/triage
- module/organizations
---
## Current Cost
What does this make harder, riskier, slower, or more fragile?
## Desired Shape
What should the code, tests, or architecture look like afterwards?
## Constraints
What behavior, compatibility, or module boundary must be preserved?
## Verification Target
Focused checks that should pass:

View File

@@ -0,0 +1,15 @@
## Issue
Closes #
## Summary
-
## Verification
-
## Notes
Follow-up issues:

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
__pycache__/
*.py[cod]
*.egg-info/
.pytest_cache/
.mypy_cache/
.ruff_cache/
build/
dist/

29
AGENTS.md Normal file
View File

@@ -0,0 +1,29 @@
# GovOPlaN Organizations Codex Guide
## Scope
This repository owns the canonical GovOPlaN organizational model: tenant-local
organization units, hierarchy, functions, and account-held function
assignments.
The module describes responsibility in the institution. It does not decide
which rights those responsibilities grant; `govoplan-access` maps organization
facts to roles, rights, permission decisions, and explainability.
## Boundaries
- Depend on kernel contracts from `govoplan-core`.
- Depend on `govoplan-tenancy` for tenant ownership.
- Do not import access, identity, postbox, workflow, or portal internals.
- Expose integration through manifests, capabilities, API routes, events, and
typed DTOs.
- Treat function assignment as organizational responsibility, not as a login
permission by itself.
## Local Workflow
Use Gitea issues as the canonical backlog and state log. The shared workflow is
installed under `.gitea/`, with labels in `docs/gitea-labels.json`.
Focused verification is still driven from `/mnt/DATA/git/govoplan-core` for
cross-repository behavior.

44
README.md Normal file
View File

@@ -0,0 +1,44 @@
# GovOPlaN Organizations
`govoplan-organizations` is the canonical organizational model module for
GovOPlaN.
It owns organization units, organizational hierarchy, functions, and
account-held function assignments. These facts exist independently of
authentication and authorization. IDM can import them, Identity can link them
to people/accounts, and Access can project them into roles and rights.
## Boundary
Organizations owns:
- tenant-local organization units
- parent/child organizational hierarchy
- functions in organization units
- whether a function may be delegated or used for acting in place
- account-held function assignments
- whether an assignment applies only to one unit or also to subunits
Organizations does not own:
- login accounts or identity lifecycle
- role and permission evaluation
- postboxes, workflows, portals, files, or cases
- external IDM connector internals
## Module Contract
The module registers the `organizations.directory` capability from
`govoplan_core.core.organizations`.
Feature modules should consume the capability instead of importing
organization ORM models.
## Development Install
From the core checkout:
```bash
cd /mnt/DATA/git/govoplan-core
./.venv/bin/python -m pip install -e ../govoplan-organizations
```

View File

@@ -0,0 +1,36 @@
# Organization Model
The organization model answers where responsibility lives.
## Layers
- Tenant: the administrative space.
- Organization unit: a department, office, committee, service desk, school,
institute, or other structural unit.
- Function: a named responsibility in an organization unit, such as clerk,
reviewer, approver, committee secretary, intake desk, or resource manager.
- Function assignment: an account holds a function in an organization unit,
optionally for that unit and all subunits.
## Boundary With Identity
Organization assignments refer to account and identity IDs, but this module
does not own login accounts or identity lifecycle. `govoplan-identity` owns the
identity/account directory. The organization module only records that an
account or identity currently holds an organizational function.
## Boundary With Access
A function assignment does not grant rights on its own. `govoplan-access`
maps organization facts to roles, rights, permission decisions, delegation
enforcement, and explain responses.
This separation keeps the institution model stable even when authorization
policy changes.
## Boundary With IDM
`govoplan-idm` may import organization units, functions, and assignments from
external directories. It should write through organization capabilities or APIs
after preview, approval, and rollback planning. IDM is a synchronization layer,
not the canonical owner of the organization model.

416
docs/gitea-labels.json Normal file
View File

@@ -0,0 +1,416 @@
[
{
"name": "type/bug",
"color": "d73a4a",
"description": "A reproducible defect, regression, or incorrect behavior.",
"exclusive": true
},
{
"name": "type/feature",
"color": "0e8a16",
"description": "New user-visible behavior or platform capability.",
"exclusive": true
},
{
"name": "type/task",
"color": "1d76db",
"description": "Implementation, maintenance, migration, or operational work.",
"exclusive": true
},
{
"name": "type/debt",
"color": "fbca04",
"description": "Cleanup, refactoring, risk reduction, or deferred engineering work.",
"exclusive": true
},
{
"name": "type/docs",
"color": "5319e7",
"description": "Documentation, process, or developer workflow work.",
"exclusive": true
},
{
"name": "priority/p0",
"color": "b60205",
"description": "Immediate stop-the-line priority.",
"exclusive": true
},
{
"name": "priority/p1",
"color": "d93f0b",
"description": "High priority for the next focused work window.",
"exclusive": true
},
{
"name": "priority/p2",
"color": "fbca04",
"description": "Normal planned priority.",
"exclusive": true
},
{
"name": "priority/p3",
"color": "c2e0c6",
"description": "Low priority or opportunistic cleanup.",
"exclusive": true
},
{
"name": "status/triage",
"color": "d4c5f9",
"description": "Needs review, ownership, priority, or acceptance criteria.",
"exclusive": true
},
{
"name": "status/ready",
"color": "0e8a16",
"description": "Ready for implementation.",
"exclusive": true
},
{
"name": "status/in-progress",
"color": "0052cc",
"description": "Currently being worked.",
"exclusive": true
},
{
"name": "status/blocked",
"color": "b60205",
"description": "Cannot progress without a decision, dependency, credential, or external change.",
"exclusive": true
},
{
"name": "status/needs-info",
"color": "f9d0c4",
"description": "Needs clarifying input before implementation can proceed safely.",
"exclusive": true
},
{
"name": "module/access",
"color": "0e8a16",
"description": "GovOPlaN access, identity, authentication, RBAC, and administration behavior.",
"exclusive": false
},
{
"name": "module/addresses",
"color": "5319e7",
"description": "GovOPlaN Addresses module behavior or integration.",
"exclusive": false
},
{
"name": "module/admin",
"color": "006b75",
"description": "GovOPlaN Admin module behavior or integration.",
"exclusive": false
},
{
"name": "module/appointments",
"color": "d876e3",
"description": "GovOPlaN Appointments module behavior or integration.",
"exclusive": false
},
{
"name": "module/audit",
"color": "0e8a16",
"description": "GovOPlaN Audit module behavior or integration.",
"exclusive": false
},
{
"name": "module/calendar",
"color": "bfd4f2",
"description": "GovOPlaN Calendar module behavior or integration.",
"exclusive": false
},
{
"name": "module/campaign",
"color": "d876e3",
"description": "GovOPlaN campaign module behavior or integration.",
"exclusive": false
},
{
"name": "module/cases",
"color": "1d76db",
"description": "GovOPlaN Cases module behavior or integration.",
"exclusive": false
},
{
"name": "module/connectors",
"color": "c2e0c6",
"description": "GovOPlaN Connectors module behavior or integration.",
"exclusive": false
},
{
"name": "module/core",
"color": "0052cc",
"description": "GovOPlaN core runner, shared primitives, shell, or extension points.",
"exclusive": false
},
{
"name": "module/dms",
"color": "c5def5",
"description": "GovOPlaN Dms module behavior or integration.",
"exclusive": false
},
{
"name": "module/erp",
"color": "fef2c0",
"description": "GovOPlaN Erp module behavior or integration.",
"exclusive": false
},
{
"name": "module/files",
"color": "006b75",
"description": "GovOPlaN files module behavior or integration.",
"exclusive": false
},
{
"name": "module/fit-connect",
"color": "fbca04",
"description": "GovOPlaN Fit Connect module behavior or integration.",
"exclusive": false
},
{
"name": "module/forms",
"color": "f9d0c4",
"description": "GovOPlaN Forms module behavior or integration.",
"exclusive": false
},
{
"name": "module/identity-trust",
"color": "d4c5f9",
"description": "GovOPlaN Identity Trust module behavior or integration.",
"exclusive": false
},
{
"name": "module/identity",
"color": "bfd4f2",
"description": "GovOPlaN Identity module behavior or integration.",
"exclusive": false
},
{
"name": "module/idm",
"color": "0052cc",
"description": "GovOPlaN Idm module behavior or integration.",
"exclusive": false
},
{
"name": "module/ledger",
"color": "5319e7",
"description": "GovOPlaN Ledger module behavior or integration.",
"exclusive": false
},
{
"name": "module/mail",
"color": "5319e7",
"description": "GovOPlaN mail module behavior or integration.",
"exclusive": false
},
{
"name": "module/notifications",
"color": "d876e3",
"description": "GovOPlaN Notifications module behavior or integration.",
"exclusive": false
},
{
"name": "module/ops",
"color": "0e8a16",
"description": "GovOPlaN Ops module behavior or integration.",
"exclusive": false
},
{
"name": "module/organizations",
"color": "bfdadc",
"description": "GovOPlaN Organizations module behavior or integration.",
"exclusive": false
},
{
"name": "module/payments",
"color": "bfd4f2",
"description": "GovOPlaN Payments module behavior or integration.",
"exclusive": false
},
{
"name": "module/policy",
"color": "d93f0b",
"description": "GovOPlaN Policy module behavior or integration.",
"exclusive": false
},
{
"name": "module/portal",
"color": "1d76db",
"description": "GovOPlaN Portal module behavior or integration.",
"exclusive": false
},
{
"name": "module/postbox",
"color": "d93f0b",
"description": "GovOPlaN Postbox module behavior or integration.",
"exclusive": false
},
{
"name": "module/reporting",
"color": "c2e0c6",
"description": "GovOPlaN Reporting module behavior or integration.",
"exclusive": false
},
{
"name": "module/search",
"color": "bfdadc",
"description": "GovOPlaN Search module behavior or integration.",
"exclusive": false
},
{
"name": "module/scheduling",
"color": "d93f0b",
"description": "GovOPlaN Scheduling module behavior or integration.",
"exclusive": false
},
{
"name": "module/tasks",
"color": "c5def5",
"description": "GovOPlaN Tasks module behavior or integration.",
"exclusive": false
},
{
"name": "module/templates",
"color": "fef2c0",
"description": "GovOPlaN Templates module behavior or integration.",
"exclusive": false
},
{
"name": "module/tenancy",
"color": "e4e669",
"description": "GovOPlaN Tenancy module behavior or integration.",
"exclusive": false
},
{
"name": "module/web",
"color": "bfd4f2",
"description": "GovOPlaN public website, product page, or publication content.",
"exclusive": false
},
{
"name": "module/workflow",
"color": "f9d0c4",
"description": "GovOPlaN Workflow module behavior or integration.",
"exclusive": false
},
{
"name": "module/xoev",
"color": "d4c5f9",
"description": "GovOPlaN Xoev module behavior or integration.",
"exclusive": false
},
{
"name": "module/xrechnung",
"color": "0052cc",
"description": "GovOPlaN Xrechnung module behavior or integration.",
"exclusive": false
},
{
"name": "module/xta-osci",
"color": "5319e7",
"description": "GovOPlaN Xta Osci module behavior or integration.",
"exclusive": false
},
{
"name": "area/auth",
"color": "bfd4f2",
"description": "Authentication, sessions, access bootstrap, or login behavior.",
"exclusive": false
},
{
"name": "area/tenancy",
"color": "bfdadc",
"description": "Tenant boundaries, provisioning, or tenant-scoped data behavior.",
"exclusive": false
},
{
"name": "area/rbac",
"color": "c5def5",
"description": "Permissions, roles, delegation, or authorization policy.",
"exclusive": false
},
{
"name": "area/governance",
"color": "fef2c0",
"description": "Governance policy, audit, privacy, retention, or compliance behavior.",
"exclusive": false
},
{
"name": "area/module-system",
"color": "bfe5bf",
"description": "Module discovery, manifests, capabilities, routing, or optional integrations.",
"exclusive": false
},
{
"name": "area/migrations",
"color": "e4e669",
"description": "Alembic migrations, schema bootstrap, or persistence evolution.",
"exclusive": false
},
{
"name": "area/webui",
"color": "1d76db",
"description": "Shared WebUI shell, frontend components, routing, or frontend tests.",
"exclusive": false
},
{
"name": "area/api",
"color": "5319e7",
"description": "HTTP API contracts, routers, schemas, or API smoke behavior.",
"exclusive": false
},
{
"name": "area/db",
"color": "0e8a16",
"description": "Database sessions, models, transactions, or persistence primitives.",
"exclusive": false
},
{
"name": "area/devex",
"color": "bfd4f2",
"description": "Local developer workflow, scripts, tests, tooling, or release helpers.",
"exclusive": false
},
{
"name": "area/release",
"color": "c2e0c6",
"description": "Versioning, release locks, tags, packaging, or dependency pins.",
"exclusive": false
},
{
"name": "area/docs",
"color": "5319e7",
"description": "Durable documentation and project guidance.",
"exclusive": false
},
{
"name": "area/marketing",
"color": "f9d0c4",
"description": "Public website, product messaging, publication copy, or legal page content.",
"exclusive": false
},
{
"name": "source/todo-scan",
"color": "cccccc",
"description": "Imported from inline TODO/FIXME/HACK markers by the Gitea TODO importer.",
"exclusive": false
},
{
"name": "source/backlog-import",
"color": "bfdadc",
"description": "Imported from markdown backlog, roadmap, plan, or TODO files.",
"exclusive": false
},
{
"name": "codex/ready",
"color": "0e8a16",
"description": "Suitable for Codex to pick up with the existing issue context.",
"exclusive": false
},
{
"name": "codex/needs-human",
"color": "f9d0c4",
"description": "Needs an explicit human decision before Codex should implement.",
"exclusive": false
}
]

24
pyproject.toml Normal file
View File

@@ -0,0 +1,24 @@
[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "govoplan-organizations"
version = "0.1.6"
description = "GovOPlaN organizational model module."
readme = "README.md"
requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }]
dependencies = [
"govoplan-core>=0.1.6",
"govoplan-tenancy>=0.1.6",
]
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
govoplan_organizations = ["py.typed"]
[project.entry-points."govoplan.modules"]
organizations = "govoplan_organizations.backend.manifest:get_manifest"

View File

@@ -0,0 +1,3 @@
"""GovOPlaN organizations module."""
__version__ = "0.1.6"

View File

@@ -0,0 +1 @@
"""Backend integration for the GovOPlaN organizations module."""

View File

@@ -0,0 +1 @@
"""Organizations database models."""

View File

@@ -0,0 +1,80 @@
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import Boolean, DateTime, ForeignKey, JSON, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from govoplan_core.db.base import Base, TimestampMixin
def new_uuid() -> str:
return str(uuid.uuid4())
class OrganizationUnit(Base, TimestampMixin):
__tablename__ = "organizations_units"
__table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_organizations_units_tenant_slug"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
parent_id: Mapped[str | None] = mapped_column(ForeignKey("organizations_units.id", ondelete="SET NULL"), nullable=True, index=True)
slug: Mapped[str] = mapped_column(String(100), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
class OrganizationFunction(Base, TimestampMixin):
__tablename__ = "organizations_functions"
__table_args__ = (UniqueConstraint("tenant_id", "organization_unit_id", "slug", name="uq_organizations_functions_tenant_unit_slug"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
organization_unit_id: Mapped[str] = mapped_column(ForeignKey("organizations_units.id", ondelete="CASCADE"), nullable=False, index=True)
slug: Mapped[str] = mapped_column(String(100), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
delegable: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
act_in_place_allowed: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
class OrganizationFunctionAssignment(Base, TimestampMixin):
__tablename__ = "organizations_function_assignments"
__table_args__ = (
UniqueConstraint(
"tenant_id",
"account_id",
"function_id",
"organization_unit_id",
name="uq_organizations_function_assignments_account_scope",
),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
account_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
identity_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
function_id: Mapped[str] = mapped_column(ForeignKey("organizations_functions.id", ondelete="CASCADE"), nullable=False, index=True)
organization_unit_id: Mapped[str] = mapped_column(ForeignKey("organizations_units.id", ondelete="CASCADE"), nullable=False, index=True)
applies_to_subunits: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
source: Mapped[str] = mapped_column(String(50), default="direct", nullable=False)
delegated_from_assignment_id: Mapped[str | None] = mapped_column(ForeignKey("organizations_function_assignments.id", ondelete="SET NULL"), nullable=True, index=True)
acting_for_account_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
valid_from: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
valid_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
__all__ = [
"OrganizationFunction",
"OrganizationFunctionAssignment",
"OrganizationUnit",
"new_uuid",
]

View File

@@ -0,0 +1,146 @@
from __future__ import annotations
from sqlalchemy import or_
from govoplan_core.core.organizations import (
OrganizationDirectory,
OrganizationFunctionAssignmentRef,
OrganizationFunctionRef,
OrganizationUnitRef,
)
from govoplan_core.db.session import get_database
from govoplan_core.security.time import utc_now
from govoplan_organizations.backend.db.models import (
OrganizationFunction,
OrganizationFunctionAssignment,
OrganizationUnit,
)
def _status(active: bool) -> str:
return "active" if active else "inactive"
def _unit_ref(item: OrganizationUnit) -> OrganizationUnitRef:
return OrganizationUnitRef(
id=item.id,
tenant_id=item.tenant_id,
slug=item.slug,
name=item.name,
parent_id=item.parent_id,
description=item.description,
status=_status(item.is_active), # type: ignore[arg-type]
)
def _function_ref(item: OrganizationFunction) -> OrganizationFunctionRef:
return OrganizationFunctionRef(
id=item.id,
tenant_id=item.tenant_id,
organization_unit_id=item.organization_unit_id,
slug=item.slug,
name=item.name,
description=item.description,
delegable=item.delegable,
act_in_place_allowed=item.act_in_place_allowed,
status=_status(item.is_active), # type: ignore[arg-type]
)
def _assignment_ref(item: OrganizationFunctionAssignment) -> OrganizationFunctionAssignmentRef:
return OrganizationFunctionAssignmentRef(
id=item.id,
tenant_id=item.tenant_id,
account_id=item.account_id,
identity_id=item.identity_id,
function_id=item.function_id,
organization_unit_id=item.organization_unit_id,
applies_to_subunits=item.applies_to_subunits,
source=item.source, # type: ignore[arg-type]
delegated_from_assignment_id=item.delegated_from_assignment_id,
acting_for_account_id=item.acting_for_account_id,
valid_from=item.valid_from,
valid_until=item.valid_until,
status=_status(item.is_active), # type: ignore[arg-type]
)
class SqlOrganizationDirectory(OrganizationDirectory):
def get_organization_unit(self, organization_unit_id: str) -> OrganizationUnitRef | None:
with get_database().session() as session:
item = session.get(OrganizationUnit, organization_unit_id)
return _unit_ref(item) if item is not None else None
def organization_units_for_tenant(self, tenant_id: str) -> tuple[OrganizationUnitRef, ...]:
with get_database().session() as session:
rows = (
session.query(OrganizationUnit)
.filter(OrganizationUnit.tenant_id == tenant_id)
.order_by(OrganizationUnit.name.asc())
.all()
)
return tuple(_unit_ref(item) for item in rows)
def get_function(self, function_id: str) -> OrganizationFunctionRef | None:
with get_database().session() as session:
item = session.get(OrganizationFunction, function_id)
return _function_ref(item) if item is not None else None
def functions_for_organization_unit(
self,
organization_unit_id: str,
*,
include_subunits: bool = False,
) -> tuple[OrganizationFunctionRef, ...]:
with get_database().session() as session:
unit_ids = {organization_unit_id}
if include_subunits:
pending = [organization_unit_id]
while pending:
parent_id = pending.pop()
child_ids = [
row[0]
for row in session.query(OrganizationUnit.id)
.filter(OrganizationUnit.parent_id == parent_id)
.all()
]
for child_id in child_ids:
if child_id not in unit_ids:
unit_ids.add(child_id)
pending.append(child_id)
rows = (
session.query(OrganizationFunction)
.filter(OrganizationFunction.organization_unit_id.in_(unit_ids))
.order_by(OrganizationFunction.name.asc())
.all()
)
return tuple(_function_ref(item) for item in rows)
def get_function_assignment(self, assignment_id: str) -> OrganizationFunctionAssignmentRef | None:
with get_database().session() as session:
item = session.get(OrganizationFunctionAssignment, assignment_id)
return _assignment_ref(item) if item is not None else None
def function_assignments_for_account(
self,
account_id: str,
*,
tenant_id: str | None = None,
) -> tuple[OrganizationFunctionAssignmentRef, ...]:
now = utc_now()
with get_database().session() as session:
query = (
session.query(OrganizationFunctionAssignment)
.join(OrganizationFunction, OrganizationFunction.id == OrganizationFunctionAssignment.function_id)
.filter(
OrganizationFunctionAssignment.account_id == account_id,
OrganizationFunctionAssignment.is_active.is_(True),
OrganizationFunction.is_active.is_(True),
or_(OrganizationFunctionAssignment.valid_from.is_(None), OrganizationFunctionAssignment.valid_from <= now),
or_(OrganizationFunctionAssignment.valid_until.is_(None), OrganizationFunctionAssignment.valid_until > now),
)
.order_by(OrganizationFunctionAssignment.created_at.asc())
)
if tenant_id is not None:
query = query.filter(OrganizationFunctionAssignment.tenant_id == tenant_id)
return tuple(_assignment_ref(item) for item in query.all())

View File

@@ -0,0 +1,53 @@
from __future__ import annotations
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
from govoplan_core.core.modules import DocumentationTopic, MigrationSpec, ModuleContext, ModuleManifest
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY
from govoplan_core.db.base import Base
from govoplan_organizations.backend.db import models as organization_models # noqa: F401 - populate metadata
def _organization_directory(context: ModuleContext) -> object:
del context
from govoplan_organizations.backend.directory import SqlOrganizationDirectory
return SqlOrganizationDirectory()
manifest = ModuleManifest(
id="organizations",
name="Organizations",
version="0.1.6",
dependencies=("tenancy",),
migration_spec=MigrationSpec(module_id="organizations", metadata=Base.metadata),
uninstall_guard_providers=(
persistent_table_uninstall_guard(
organization_models.OrganizationUnit,
organization_models.OrganizationFunction,
organization_models.OrganizationFunctionAssignment,
label="Organizations",
),
),
capability_factories={
CAPABILITY_ORGANIZATION_DIRECTORY: _organization_directory,
},
documentation=(
DocumentationTopic(
id="organizations.model",
title="Organization model",
summary="Organizations owns units, hierarchy, functions, and function assignments. Access maps those facts to roles and rights.",
body=(
"Use organization functions to model responsibilities that outlive people. "
"A function assignment can apply to one organization unit or to that unit and all subunits."
),
layer="configured",
documentation_types=("admin", "user"),
audience=("tenant_admin", "access_admin", "operator"),
order=25,
),
),
)
def get_manifest() -> ModuleManifest:
return manifest

View File

@@ -0,0 +1 @@