Initialize organizations module
This commit is contained in:
35
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
35
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal 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:
|
||||
1
.gitea/ISSUE_TEMPLATE/config.yaml
Normal file
1
.gitea/ISSUE_TEMPLATE/config.yaml
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
27
.gitea/ISSUE_TEMPLATE/docs_workflow.md
Normal file
27
.gitea/ISSUE_TEMPLATE/docs_workflow.md
Normal 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?
|
||||
32
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
32
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal 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:
|
||||
28
.gitea/ISSUE_TEMPLATE/task.md
Normal file
28
.gitea/ISSUE_TEMPLATE/task.md
Normal 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:
|
||||
25
.gitea/ISSUE_TEMPLATE/tech_debt.md
Normal file
25
.gitea/ISSUE_TEMPLATE/tech_debt.md
Normal 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:
|
||||
15
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
15
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,15 @@
|
||||
## Issue
|
||||
|
||||
Closes #
|
||||
|
||||
## Summary
|
||||
|
||||
-
|
||||
|
||||
## Verification
|
||||
|
||||
-
|
||||
|
||||
## Notes
|
||||
|
||||
Follow-up issues:
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
build/
|
||||
dist/
|
||||
29
AGENTS.md
Normal file
29
AGENTS.md
Normal 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
44
README.md
Normal 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
|
||||
```
|
||||
36
docs/ORGANIZATION_MODEL.md
Normal file
36
docs/ORGANIZATION_MODEL.md
Normal 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
416
docs/gitea-labels.json
Normal 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
24
pyproject.toml
Normal 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"
|
||||
3
src/govoplan_organizations/__init__.py
Normal file
3
src/govoplan_organizations/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""GovOPlaN organizations module."""
|
||||
|
||||
__version__ = "0.1.6"
|
||||
1
src/govoplan_organizations/backend/__init__.py
Normal file
1
src/govoplan_organizations/backend/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Backend integration for the GovOPlaN organizations module."""
|
||||
1
src/govoplan_organizations/backend/db/__init__.py
Normal file
1
src/govoplan_organizations/backend/db/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Organizations database models."""
|
||||
80
src/govoplan_organizations/backend/db/models.py
Normal file
80
src/govoplan_organizations/backend/db/models.py
Normal 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",
|
||||
]
|
||||
146
src/govoplan_organizations/backend/directory.py
Normal file
146
src/govoplan_organizations/backend/directory.py
Normal 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())
|
||||
53
src/govoplan_organizations/backend/manifest.py
Normal file
53
src/govoplan_organizations/backend/manifest.py
Normal 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
|
||||
1
src/govoplan_organizations/py.typed
Normal file
1
src/govoplan_organizations/py.typed
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user