This page explains the cross-cutting rules of the intranet module. src/cluster_erp/modules/intranet/access.py scopes personal records and resolves the content viewer, src/cluster_erp/modules/intranet/context.py resolves the acting person, src/cluster_erp/modules/intranet/retention_handlers.py ages out analytics events, and src/cluster_erp/modules/intranet/validation.py normalises form input. Access grants and audience segments live in src/cluster_erp/modules/intranet/content/services.py.
In short¶
- Access and audience are two separate axes. Access grants decide who may see content; segments decide who is targeted (
src/cluster_erp/modules/intranet/models.py:8-11). - Personal records (leave, requests, deliveries, the inbox) use three tiers:
read_allsees everything, others see their own records and their team's, and a caller without a party sees nothing (src/cluster_erp/modules/intranet/access.py:1-21). - The acting person is the party of the signed-in user's employee link (
src/cluster_erp/modules/intranet/context.py:1-30). - Read analytics of a post are shown only in aggregate, with unique counts below a minimum group size suppressed (
src/cluster_erp/modules/intranet/content/services.py:1327-1355).
How it works¶
Acting person¶
resolve_party_for_user reads employee_id from the signed-in user and asks GetPartyForEmployee for the party. A login without an employee link resolves to no party (src/cluster_erp/modules/intranet/context.py:20-30). For personal writes, effective_party_guarded uses the caller's own party; naming another party needs the privileged permission the route passes, for example intranet_leave_request.read_all (src/cluster_erp/modules/intranet/access.py:147-170). A named party must also be an active employee (src/cluster_erp/modules/intranet/access.py:124-144).
Row scope for personal records¶
resolve_scope returns a scope from the core row-scope helpers (src/cluster_erp/modules/intranet/access.py:189-220):
| Caller | Sees | Source |
|---|---|---|
holds <entity>.read_all |
every record | src/cluster_erp/modules/intranet/access.py:209-210 |
| has a party | own records and the team, read live through GetTeamPartyIds |
src/cluster_erp/modules/intranet/access.py:173-186, src/cluster_erp/modules/intranet/access.py:211-218 |
| has no party | nothing | src/cluster_erp/modules/intranet/access.py:213-216 |
The resolved scope is also published to the database row-level security backstop (src/cluster_erp/modules/intranet/access.py:202-219). The mechanism is described on field and row access and multi-tenancy. Official deliveries add one rule: a confidential delivery is visible only to its recipient and to read_all holders (src/cluster_erp/modules/intranet/delivery/services.py:40-64).
Access grants for content¶
An access grant (IntranetAclGrant) attaches to a space or a post and names a realm: everyone, role, party, or segment, with can_view, can_edit, and can_publish flags (src/cluster_erp/modules/intranet/models.py:78-79, src/cluster_erp/modules/intranet/models.py:379-408).
- Listing joins the grant table as an SQL predicate. A post with no view grants on itself or its space is visible to every reader; otherwise the reader must match a grant by party, role, or segment (
src/cluster_erp/modules/intranet/content/services.py:585-626). resolve_acl_viewercollects the reader's party, role ids, and segment ids; holders ofintranet_content.read_allskip the trimming (src/cluster_erp/modules/intranet/access.py:78-121).- Publish grants narrow the editorial permission: with no publish grant on the space or post, the permission alone decides; with grants, the actor must match one (
src/cluster_erp/modules/intranet/content/services.py:628-660). - Grants are set by an upsert per subject, realm, and id, and removed by setting
deleted_at(src/cluster_erp/modules/intranet/content/services.py:544-583). The routes areGET,POST /acl/andDELETE /acl/{grant_id}(src/cluster_erp/modules/intranet/content/routes.py:515-546).
Audience segments¶
flowchart LR F["audience builder fields"] --> A["_assemble_rule"] A --> R["rule include and exclude"] R --> B["rebuild_members"] P["DirectoryService._live_people"] --> B Q["GetProjectMemberPartyIds"] --> B B --> M["IntranetSegmentMember rows"] M --> U["publication recipients, ACL segment realm, event audience"]
In prose: the audience builder stores a rule with include and exclude parts over departments, organisations, named parties, projects, job titles, and locations (src/cluster_erp/modules/intranet/content/services.py:185-205). A person matches when any filled criterion matches; titles and locations compare without case and surrounding spaces; an empty rule matches nobody (src/cluster_erp/modules/intranet/content/services.py:135-179). rebuild_members retires the dynamic members, reads the live employees, resolves project teams through GetProjectMemberPartyIds, and inserts one member row per match. Manual members are kept (src/cluster_erp/modules/intranet/content/services.py:426-506). Publishing a post rebuilds its segment first (src/cluster_erp/modules/intranet/content/services.py:1022-1035). POST /segments/{id}/rebuild and POST /segments/rebuild-all rebuild on demand (src/cluster_erp/modules/intranet/content/routes.py:604-633).
Analytics floor and retention¶
IntranetSettings.min_group_size(default 5) is the floor below which unique-reader counts are suppressed; totals are always shown (src/cluster_erp/modules/intranet/models.py:155-200,src/cluster_erp/modules/intranet/content/services.py:1327-1355).- On each
RetentionSweep,sweep_retentiondeletesintranet_analytics_eventsrows older thananalytics_retention_days(default 730). A window of zero or less keeps everything (src/cluster_erp/modules/intranet/retention_handlers.py:1-55). The general retention mechanism is on records module.
Input normalisation¶
scrub_blank_strings turns blank strings into empty values before validation, so that untouched form fields do not collide with live-row unique indexes (src/cluster_erp/modules/intranet/validation.py:1-25).
Why it is built this way¶
- The model docstring calls the merging of access and targeting the most repeated bug of the field, and states that the acknowledgement store is kept apart from analytics (
src/cluster_erp/modules/intranet/models.py:8-19). - The
access.pydocstring states that a coarse read permission alone would let anyone read every payslip (src/cluster_erp/modules/intranet/access.py:3-5). - The retention docstring states that analytics events have no
deleted_at, so the generic purge never saw them (src/cluster_erp/modules/intranet/retention_handlers.py:1-8).
Rules and boundaries¶
- Physical deletion in this module is limited to analytics events past their window (
src/cluster_erp/modules/intranet/retention_handlers.py:44-52). The project's written engineering rules allow retention purges of event rows whose retention period has ended. - Decision actions need elevated permissions on top of
.update(src/cluster_erp/modules/intranet/access.py:66-75).
Interaction with other parts¶
| Contract | Purpose | Source |
|---|---|---|
GetPartyForEmployee |
acting person | src/cluster_erp/modules/intranet/context.py:25-27 |
GetTeamPartyIds |
live team closure for the scope | src/cluster_erp/modules/intranet/access.py:181-186 |
GetProjectMemberPartyIds |
project teams in segment rules | src/cluster_erp/modules/intranet/content/services.py:494-502 |
RetentionSweep |
daily retention pass | src/cluster_erp/modules/intranet/module.py:224-229 |
Limits and open points¶
- Segment membership is a snapshot; it changes on a rebuild or on publish, not on every employee change (
src/cluster_erp/modules/intranet/content/services.py:426-433,src/cluster_erp/modules/intranet/content/services.py:1027-1035). - Directory names come from one party search with a limit of 10000 (
src/cluster_erp/modules/intranet/directory/services.py:74-80).
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| access grant | IntranetAclGrant |
A view, edit, or publish right on a space or post for everyone, a role, a party, or a segment. | Erişim izni | src/cluster_erp/modules/intranet/models.py:379 |
| row scope | resolve_scope |
The set of parties whose records a caller may see. | satır kapsamı | src/cluster_erp/modules/intranet/access.py:189 |
| minimum group size | min_group_size |
The floor below which unique reader counts are hidden. | Asgari grup büyüklüğü | src/cluster_erp/modules/intranet/models.py:155-200 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.