661 lines
31 KiB
Python
661 lines
31 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
|
|
from sqlalchemy import inspect
|
|
|
|
from govoplan_calendar.backend.db import models as calendar_models # noqa: F401 - populate Calendar ORM metadata
|
|
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
|
|
from govoplan_core.core.calendar import (
|
|
CALENDAR_AVAILABILITY_READ_SCOPE,
|
|
CALENDAR_EVENT_WRITE_SCOPE,
|
|
CAPABILITY_CALENDAR_EXTERNAL_PROFILES,
|
|
CAPABILITY_CALENDAR_INVITATIONS,
|
|
CAPABILITY_CALENDAR_OUTBOX,
|
|
CAPABILITY_CALENDAR_SCHEDULING,
|
|
)
|
|
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
|
|
from govoplan_core.core.modules import (
|
|
DocumentationCondition,
|
|
DocumentationTopic,
|
|
FrontendModule,
|
|
FrontendRoute,
|
|
MigrationSpec,
|
|
ModuleContext,
|
|
ModuleInterfaceProvider,
|
|
ModuleManifest,
|
|
NavItem,
|
|
PermissionDefinition,
|
|
RoleTemplate,
|
|
)
|
|
from govoplan_core.core.provider_governance import (
|
|
ExternalProviderDeclaration,
|
|
ExternalProviderStateProviderRegistration,
|
|
ModuleArchitectureDeclaration,
|
|
ModuleArchitectureDocumentation,
|
|
ModuleMaturityEvidence,
|
|
ProviderBehaviorDeclaration,
|
|
ProviderObjectDeclaration,
|
|
)
|
|
from govoplan_core.core.views import ViewSurface
|
|
from govoplan_core.db.base import Base
|
|
|
|
|
|
CALDAV_PROVIDER_ID = "calendar.caldav_sync"
|
|
ICS_PROVIDER_ID = "calendar.ics_subscription"
|
|
GRAPH_PROVIDER_ID = "calendar.microsoft_graph"
|
|
EWS_PROVIDER_ID = "calendar.exchange_ews"
|
|
OPEN_XCHANGE_PROVIDER_ID = "calendar.open_xchange"
|
|
|
|
CALENDAR_ARCHITECTURE = ModuleArchitectureDeclaration(
|
|
layer="communication_participation",
|
|
kind="domain",
|
|
maturity="vertical_slice",
|
|
evidence=(
|
|
ModuleMaturityEvidence(
|
|
kind="test",
|
|
reference="tests/test_caldav.py",
|
|
summary="Exercises CalDAV discovery, pull, push, deletion, and reconciliation behavior.",
|
|
),
|
|
ModuleMaturityEvidence(
|
|
kind="test",
|
|
reference="tests/test_outbox.py",
|
|
summary="Exercises durable calendar effects, retries, terminal states, and cleanup.",
|
|
),
|
|
ModuleMaturityEvidence(
|
|
kind="documentation",
|
|
reference="docs/CALENDAR_INTEGRATION_CONCEPT.md",
|
|
summary="Documents Calendar ownership and optional integration boundaries.",
|
|
),
|
|
),
|
|
known_limits=(
|
|
"Provider target-environment and recovery-drill evidence is not yet packaged as a reference package.",
|
|
"Microsoft Graph and EWS adapters do not yet have the same write/reconciliation coverage as CalDAV.",
|
|
),
|
|
supported_authority_modes=(
|
|
"native_authoritative",
|
|
"external_authoritative",
|
|
"external_mirror",
|
|
"governed_sync",
|
|
"linked_reference",
|
|
),
|
|
owned_concepts=(
|
|
"calendar collections",
|
|
"VEVENT lifecycle",
|
|
"recurrence and availability",
|
|
"calendar synchronization state",
|
|
),
|
|
non_owned_concepts=(
|
|
"meeting polls",
|
|
"mail delivery",
|
|
"task lifecycle",
|
|
"external identity and credential secrets",
|
|
),
|
|
target_tested_providers=(CALDAV_PROVIDER_ID,),
|
|
documentation=ModuleArchitectureDocumentation(
|
|
migration=("src/govoplan_calendar/backend/migrations/versions",),
|
|
upgrade=("docs/CALENDAR_INTEGRATION_CONCEPT.md",),
|
|
recovery=("docs/CALENDAR_INTEGRATION_CONCEPT.md",),
|
|
security=("tests/test_caldav_security.py", "tests/test_http_security.py"),
|
|
operations=("docs/CALENDAR_INTEGRATION_CONCEPT.md",),
|
|
),
|
|
)
|
|
|
|
def _read_only_calendar_provider(
|
|
*,
|
|
provider_id: str,
|
|
label: str,
|
|
source_name: str,
|
|
) -> ExternalProviderDeclaration:
|
|
return ExternalProviderDeclaration(
|
|
id=provider_id,
|
|
module_id="calendar",
|
|
label=label,
|
|
maturity="read",
|
|
operations=("read", "preview"),
|
|
objects=(
|
|
ProviderObjectDeclaration(
|
|
object_type="calendar_collection",
|
|
field_groups=("identity", "display", "sync_state"),
|
|
authority_modes=("external_authoritative", "external_mirror"),
|
|
default_authority_mode="external_mirror",
|
|
),
|
|
ProviderObjectDeclaration(
|
|
object_type="calendar_event",
|
|
field_groups=(
|
|
"identity",
|
|
"schedule",
|
|
"recurrence",
|
|
"participants",
|
|
"content",
|
|
),
|
|
authority_modes=("external_authoritative", "external_mirror"),
|
|
default_authority_mode="external_mirror",
|
|
),
|
|
),
|
|
behavior=ProviderBehaviorDeclaration(
|
|
revision_tokens=f"{source_name} object identity, remote revision, and content fingerprint are retained.",
|
|
concurrency="The source is inbound-only; refresh compares remote identity and revision before replacing the derived projection.",
|
|
freshness="Last attempt, last success, and source status are retained.",
|
|
health="Authentication, transport, parsing, and source failures are recorded without exposing credentials.",
|
|
max_read_items=5000,
|
|
evidence=f"Imported events retain the {source_name} source, remote identity, revision, and normalized content fingerprint.",
|
|
audit_event_types=("calendar.sync.requested", "calendar.sync.completed"),
|
|
correction="A later successful refresh creates the corrected local projection while source authority remains external.",
|
|
reconciliation="Re-read the bounded source and compare remote identity, revision, and normalized event content.",
|
|
outage="The last local projection remains available with stale or unknown freshness.",
|
|
classifications=("internal", "confidential", "restricted"),
|
|
purposes=("calendar subscription", "availability", "meeting coordination"),
|
|
retention="Calendar projection, sync evidence, and credential retention are independent policies.",
|
|
secret_handling="Authentication material remains in Calendar credential records or deployment-owned secret references.",
|
|
),
|
|
documentation_topic_ids=("calendar.external-sources-and-sync",),
|
|
)
|
|
|
|
|
|
CALENDAR_EXTERNAL_PROVIDERS = (
|
|
ExternalProviderDeclaration(
|
|
id=CALDAV_PROVIDER_ID,
|
|
module_id="calendar",
|
|
label="CalDAV calendar synchronization",
|
|
maturity="synchronize",
|
|
operations=(
|
|
"discover",
|
|
"read",
|
|
"write",
|
|
"delete",
|
|
"synchronize",
|
|
"preview",
|
|
),
|
|
objects=(
|
|
ProviderObjectDeclaration(
|
|
object_type="calendar_collection",
|
|
field_groups=("identity", "display", "sync_state"),
|
|
authority_modes=(
|
|
"native_authoritative",
|
|
"external_authoritative",
|
|
"external_mirror",
|
|
"governed_sync",
|
|
),
|
|
default_authority_mode="governed_sync",
|
|
),
|
|
ProviderObjectDeclaration(
|
|
object_type="calendar_event",
|
|
field_groups=(
|
|
"identity",
|
|
"schedule",
|
|
"recurrence",
|
|
"participants",
|
|
"content",
|
|
),
|
|
authority_modes=(
|
|
"native_authoritative",
|
|
"external_authoritative",
|
|
"external_mirror",
|
|
"governed_sync",
|
|
),
|
|
default_authority_mode="governed_sync",
|
|
),
|
|
),
|
|
behavior=ProviderBehaviorDeclaration(
|
|
revision_tokens="CalDAV sync tokens and per-resource ETags are retained.",
|
|
concurrency="Conditional requests reject stale ETags; conflicts remain explicit.",
|
|
freshness="Last attempt, last success, sync token, and pending outbox work are visible.",
|
|
health="Discovery, authentication, transport, collection, and reconciliation failures are recorded.",
|
|
max_read_items=5000,
|
|
idempotency="Stable VEVENT UID, source id, and durable operation keys suppress duplicate effects.",
|
|
retry="Only classified transient failures receive bounded backoff retries.",
|
|
timeout_seconds=30,
|
|
conflicts="Stale revisions and concurrent remote changes enter conflict/reconciliation state.",
|
|
outcome_unknown="A timed-out remote write is outcome-unknown and is not blindly repeated.",
|
|
outcome_unknown_supported=True,
|
|
evidence="Operation id, request intent, ETag/sync token, response classification, and reconciliation result are retained.",
|
|
audit_event_types=(
|
|
"calendar.sync.requested",
|
|
"calendar.sync.completed",
|
|
"calendar.sync.conflict",
|
|
"calendar.sync.reconciled",
|
|
),
|
|
correction="A later conditional update or tombstone corrects external state after reconciliation.",
|
|
rollback="Remote effects are not treated as transactionally rollback-safe.",
|
|
compensation="A compensating event update/delete may be queued after the remote state is known.",
|
|
reconciliation="Read the resource by href/UID, compare ETag and content, then classify applied, retryable, conflict, or manual.",
|
|
outage="Local calendars remain usable with stale markers; pending writes stay durable and bounded.",
|
|
classifications=("internal", "confidential", "restricted"),
|
|
purposes=("calendar collaboration", "availability", "meeting coordination"),
|
|
retention="Calendar event, outbox terminal-state, audit, and credential retention are independent policies.",
|
|
secret_handling="Authentication material is stored through Calendar credential records or external secret references and is never emitted in provider metadata.",
|
|
),
|
|
capability_names=(CAPABILITY_CALENDAR_OUTBOX,),
|
|
interface_names=("calendar.outbox",),
|
|
documentation_topic_ids=("calendar.external-sources-and-sync",),
|
|
),
|
|
ExternalProviderDeclaration(
|
|
id=OPEN_XCHANGE_PROVIDER_ID,
|
|
module_id="calendar",
|
|
label="Open-Xchange calendar synchronization",
|
|
maturity="synchronize",
|
|
operations=("discover", "read", "write", "delete", "synchronize", "preview"),
|
|
objects=(
|
|
ProviderObjectDeclaration(
|
|
object_type="calendar_collection",
|
|
field_groups=("identity", "display", "sync_state", "resource_mapping"),
|
|
authority_modes=("external_authoritative", "external_mirror", "governed_sync"),
|
|
default_authority_mode="governed_sync",
|
|
),
|
|
ProviderObjectDeclaration(
|
|
object_type="calendar_event",
|
|
field_groups=("identity", "schedule", "recurrence", "participants", "content"),
|
|
authority_modes=("external_authoritative", "external_mirror", "governed_sync"),
|
|
default_authority_mode="governed_sync",
|
|
),
|
|
),
|
|
behavior=ProviderBehaviorDeclaration(
|
|
revision_tokens="Open-Xchange CalDAV sync tokens, collection tags, and per-resource ETags are retained.",
|
|
concurrency="Conditional CalDAV requests reject stale ETags and expose conflicts for reconciliation.",
|
|
freshness="Last attempt, last success, sync token, and pending outbox work are visible.",
|
|
health="Discovery, authentication, transport, collection, and reconciliation failures are recorded.",
|
|
max_read_items=5000,
|
|
idempotency="Stable VEVENT UID, source id, and durable operation keys suppress duplicate effects.",
|
|
retry="Only classified transient failures receive bounded backoff retries.",
|
|
timeout_seconds=30,
|
|
conflicts="Stale revisions and concurrent Open-Xchange changes enter conflict or reconciliation state.",
|
|
outcome_unknown="A timed-out remote write is outcome-unknown and is reconciled before retry.",
|
|
outcome_unknown_supported=True,
|
|
evidence="The Open-Xchange profile reference, mapping references, operation intent, ETag, and reconciliation result are retained.",
|
|
audit_event_types=("calendar.sync.requested", "calendar.sync.completed", "calendar.sync.conflict", "calendar.sync.reconciled"),
|
|
correction="A later conditional update or tombstone corrects external state after reconciliation.",
|
|
rollback="Remote effects are not treated as transactionally rollback-safe.",
|
|
compensation="A compensating event update or delete may be queued after remote state is known.",
|
|
reconciliation="Read by CalDAV href and VEVENT UID, compare ETag and content, then classify the result.",
|
|
outage="The local projection remains available with stale markers and durable pending writes.",
|
|
classifications=("internal", "confidential", "restricted"),
|
|
purposes=("calendar collaboration", "availability", "resource booking", "meeting coordination"),
|
|
retention="Calendar event, outbox, audit, profile-binding, and credential retention remain independent policies.",
|
|
secret_handling="Calendar stores only scoped credential references or encrypted Calendar-owned credentials.",
|
|
),
|
|
capability_names=(CAPABILITY_CALENDAR_EXTERNAL_PROFILES, CAPABILITY_CALENDAR_OUTBOX, CAPABILITY_CALENDAR_SCHEDULING),
|
|
interface_names=("calendar.external_profiles", "calendar.outbox", "calendar.scheduling"),
|
|
documentation_topic_ids=("calendar.external-sources-and-sync",),
|
|
),
|
|
_read_only_calendar_provider(
|
|
provider_id=ICS_PROVIDER_ID,
|
|
label="ICS and webcal subscription",
|
|
source_name="ICS/webcal",
|
|
),
|
|
_read_only_calendar_provider(
|
|
provider_id=GRAPH_PROVIDER_ID,
|
|
label="Microsoft Graph calendar projection",
|
|
source_name="Microsoft Graph",
|
|
),
|
|
_read_only_calendar_provider(
|
|
provider_id=EWS_PROVIDER_ID,
|
|
label="Exchange Web Services calendar projection",
|
|
source_name="Exchange Web Services",
|
|
),
|
|
)
|
|
|
|
|
|
_calendar_table_retirement_provider = drop_table_retirement_provider(
|
|
calendar_models.CalendarCollection,
|
|
calendar_models.CalendarEvent,
|
|
calendar_models.CalendarMigrationBatch,
|
|
calendar_models.CalendarMigrationResource,
|
|
calendar_models.CalendarOutboxOperation,
|
|
calendar_models.CalendarSyncCredential,
|
|
calendar_models.CalendarSyncSource,
|
|
calendar_models.CalendarViewPreference,
|
|
label="Calendar",
|
|
)
|
|
|
|
|
|
def _calendar_retirement_provider(session: object | None, module_id: str):
|
|
plan = _calendar_table_retirement_provider(session, module_id)
|
|
base_executor = plan.destroy_data_executor
|
|
if base_executor is None:
|
|
return plan
|
|
|
|
def executor(execute_session: object, execute_module_id: str) -> None:
|
|
if not hasattr(execute_session, "get_bind") or not hasattr(execute_session, "query"):
|
|
raise RuntimeError("No database session is available for Calendar credential retirement.")
|
|
if inspect(execute_session.get_bind()).has_table(calendar_models.CalendarSyncCredential.__tablename__):
|
|
from govoplan_calendar.backend.service import delete_calendar_credentials_for_retirement
|
|
|
|
delete_calendar_credentials_for_retirement(execute_session)
|
|
base_executor(execute_session, execute_module_id)
|
|
|
|
return replace(
|
|
plan,
|
|
destroy_data_warnings=(
|
|
*plan.destroy_data_warnings,
|
|
"Calendar-owned credentials are deleted immediately before tables are dropped; retirement fails if an external secret provider is unavailable.",
|
|
),
|
|
destroy_data_executor=executor,
|
|
)
|
|
|
|
|
|
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
|
|
module_id, resource, action = scope.split(":", 2)
|
|
return PermissionDefinition(
|
|
scope=scope,
|
|
label=label,
|
|
description=description,
|
|
category="Calendar",
|
|
level="tenant",
|
|
module_id=module_id,
|
|
resource=resource,
|
|
action=action,
|
|
)
|
|
|
|
|
|
PERMISSIONS = (
|
|
_permission("calendar:calendar:read", "View calendars", "List tenant calendar collections and metadata."),
|
|
_permission("calendar:calendar:write", "Manage calendars", "Create and edit tenant calendar collections."),
|
|
_permission("calendar:calendar:admin", "Administer calendars", "Delete calendars and manage tenant-level calendar settings."),
|
|
_permission("calendar:event:read", "View calendar events", "List and inspect calendar events."),
|
|
_permission(CALENDAR_EVENT_WRITE_SCOPE, "Manage calendar events", "Create and edit calendar events."),
|
|
_permission("calendar:event:delete", "Delete calendar events", "Delete or cancel calendar events where policy allows it."),
|
|
_permission("calendar:event:import", "Import iCalendar events", "Import VEVENT data from iCalendar sources."),
|
|
_permission("calendar:event:export", "Export iCalendar events", "Export events as text/calendar VEVENT data."),
|
|
_permission(CALENDAR_AVAILABILITY_READ_SCOPE, "Read availability", "Read free/busy and availability data for integrations."),
|
|
)
|
|
|
|
ROLE_TEMPLATES = (
|
|
RoleTemplate(
|
|
slug="calendar_manager",
|
|
name="Calendar manager",
|
|
description="Manage tenant calendars and events.",
|
|
permissions=(
|
|
"calendar:calendar:read",
|
|
"calendar:calendar:write",
|
|
"calendar:event:read",
|
|
CALENDAR_EVENT_WRITE_SCOPE,
|
|
"calendar:event:delete",
|
|
"calendar:event:import",
|
|
"calendar:event:export",
|
|
CALENDAR_AVAILABILITY_READ_SCOPE,
|
|
),
|
|
),
|
|
RoleTemplate(
|
|
slug="calendar_viewer",
|
|
name="Calendar viewer",
|
|
description="Read calendars, events, exports, and availability.",
|
|
permissions=(
|
|
"calendar:calendar:read",
|
|
"calendar:event:read",
|
|
"calendar:event:export",
|
|
CALENDAR_AVAILABILITY_READ_SCOPE,
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
def _tenant_summary(session, tenant_id: str) -> dict[str, int]:
|
|
from govoplan_calendar.backend.db.models import (
|
|
CalendarCollection,
|
|
CalendarEvent,
|
|
CalendarOutboxOperation,
|
|
CalendarSyncCredential,
|
|
CalendarSyncSource,
|
|
CalendarViewPreference,
|
|
)
|
|
|
|
return {
|
|
"calendars": session.query(CalendarCollection).filter(CalendarCollection.tenant_id == tenant_id, CalendarCollection.deleted_at.is_(None)).count(),
|
|
"calendar_events": session.query(CalendarEvent).filter(CalendarEvent.tenant_id == tenant_id, CalendarEvent.deleted_at.is_(None)).count(),
|
|
"calendar_sync_sources": session.query(CalendarSyncSource).filter(CalendarSyncSource.tenant_id == tenant_id, CalendarSyncSource.deleted_at.is_(None)).count(),
|
|
"calendar_sync_credentials": session.query(CalendarSyncCredential).filter(CalendarSyncCredential.tenant_id == tenant_id, CalendarSyncCredential.deleted_at.is_(None)).count(),
|
|
"calendar_view_preferences": session.query(CalendarViewPreference).filter(CalendarViewPreference.tenant_id == tenant_id).count(),
|
|
"calendar_outbox_pending": session.query(CalendarOutboxOperation)
|
|
.filter(
|
|
CalendarOutboxOperation.tenant_id == tenant_id,
|
|
CalendarOutboxOperation.status.in_(("pending", "retry", "in_progress")),
|
|
)
|
|
.count(),
|
|
}
|
|
|
|
|
|
def _calendar_router(context: ModuleContext):
|
|
from govoplan_calendar.backend.runtime import configure_runtime
|
|
|
|
configure_runtime(registry=context.registry, settings=context.settings)
|
|
from govoplan_calendar.backend.router import router
|
|
|
|
return router
|
|
|
|
|
|
def _calendar_scheduling_provider(context: ModuleContext) -> object:
|
|
del context
|
|
from govoplan_calendar.backend.capabilities import SqlCalendarSchedulingProvider
|
|
|
|
return SqlCalendarSchedulingProvider()
|
|
|
|
|
|
def _calendar_invitation_provider(context: ModuleContext) -> object:
|
|
del context
|
|
from govoplan_calendar.backend.capabilities import SqlCalendarInvitationProvider
|
|
|
|
return SqlCalendarInvitationProvider()
|
|
|
|
|
|
def _calendar_outbox_provider(context: ModuleContext) -> object:
|
|
from govoplan_calendar.backend.outbox import (
|
|
OUTBOX_DEFAULT_TERMINAL_RETENTION_DAYS,
|
|
SqlCalendarOutboxProvider,
|
|
)
|
|
|
|
return SqlCalendarOutboxProvider(
|
|
terminal_retention_days=getattr(
|
|
context.settings,
|
|
"calendar_outbox_terminal_retention_days",
|
|
OUTBOX_DEFAULT_TERMINAL_RETENTION_DAYS,
|
|
)
|
|
)
|
|
|
|
|
|
def _calendar_external_profile_provider(context: ModuleContext) -> object:
|
|
del context
|
|
from govoplan_calendar.backend.capabilities import SqlCalendarExternalProfileProvider
|
|
|
|
return SqlCalendarExternalProfileProvider()
|
|
|
|
|
|
def _caldav_provider_states(context):
|
|
from govoplan_calendar.backend.provider_state import caldav_provider_states
|
|
|
|
return caldav_provider_states(context)
|
|
|
|
|
|
def _ics_provider_states(context):
|
|
from govoplan_calendar.backend.provider_state import ics_provider_states
|
|
|
|
return ics_provider_states(context)
|
|
|
|
|
|
def _graph_provider_states(context):
|
|
from govoplan_calendar.backend.provider_state import graph_provider_states
|
|
|
|
return graph_provider_states(context)
|
|
|
|
|
|
def _ews_provider_states(context):
|
|
from govoplan_calendar.backend.provider_state import ews_provider_states
|
|
|
|
return ews_provider_states(context)
|
|
|
|
|
|
def _open_xchange_provider_states(context):
|
|
from govoplan_calendar.backend.provider_state import open_xchange_provider_states
|
|
|
|
return open_xchange_provider_states(context)
|
|
|
|
|
|
manifest = ModuleManifest(
|
|
id="calendar",
|
|
name="Calendar",
|
|
version="0.1.8",
|
|
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
|
optional_dependencies=("mail", "tasks", "scheduling", "appointments", "workflow_engine", "notifications", "dms", "connectors"),
|
|
provides_interfaces=(
|
|
ModuleInterfaceProvider(name="calendar.outbox", version="0.1.8"),
|
|
ModuleInterfaceProvider(name="calendar.scheduling", version="0.1.8"),
|
|
ModuleInterfaceProvider(name="calendar.invitations", version="0.2.0"),
|
|
ModuleInterfaceProvider(name="calendar.external_profiles", version="0.1.0"),
|
|
),
|
|
permissions=PERMISSIONS,
|
|
route_factory=_calendar_router,
|
|
role_templates=ROLE_TEMPLATES,
|
|
tenant_summary_providers=(_tenant_summary,),
|
|
architecture=CALENDAR_ARCHITECTURE,
|
|
external_providers=CALENDAR_EXTERNAL_PROVIDERS,
|
|
external_provider_state_providers=(
|
|
ExternalProviderStateProviderRegistration(
|
|
module_id="calendar",
|
|
provider_id=CALDAV_PROVIDER_ID,
|
|
provider=_caldav_provider_states,
|
|
),
|
|
ExternalProviderStateProviderRegistration(
|
|
module_id="calendar",
|
|
provider_id=ICS_PROVIDER_ID,
|
|
provider=_ics_provider_states,
|
|
),
|
|
ExternalProviderStateProviderRegistration(
|
|
module_id="calendar",
|
|
provider_id=GRAPH_PROVIDER_ID,
|
|
provider=_graph_provider_states,
|
|
),
|
|
ExternalProviderStateProviderRegistration(
|
|
module_id="calendar",
|
|
provider_id=EWS_PROVIDER_ID,
|
|
provider=_ews_provider_states,
|
|
),
|
|
ExternalProviderStateProviderRegistration(
|
|
module_id="calendar",
|
|
provider_id=OPEN_XCHANGE_PROVIDER_ID,
|
|
provider=_open_xchange_provider_states,
|
|
),
|
|
),
|
|
documentation=(
|
|
DocumentationTopic(
|
|
id="calendar.manage-calendars-and-events",
|
|
title="Use calendars and events",
|
|
summary="Create calendar collections and work with all-day or timed events in continuous, month, week, workweek, and day views.",
|
|
body="Calendar remembers the selected view and preferences. Events can be created, edited, moved, resized, repeated, imported, exported, or deleted when the current account has the corresponding permission. All-day events use dates rather than local clock times; timed events retain their timezone-aware start and end values.",
|
|
documentation_types=("user",),
|
|
audience=("user", "calendar_manager"),
|
|
related_modules=("scheduling", "notifications"),
|
|
metadata={"kind": "reference"},
|
|
),
|
|
DocumentationTopic(
|
|
id="calendar.external-sources-and-sync",
|
|
title="Connect and synchronize external calendars",
|
|
summary="Calendar supports local collections, two-way CalDAV and Open-Xchange profiles, and read-only ICS/webcal, Microsoft Graph, and Exchange Web Services sources.",
|
|
body="Each external source keeps its URL, synchronization direction, status, and credential reference with the Calendar collection. Open-Xchange uses the proven CalDAV transport while retaining connector-profile, identity/group-mapping, and resource-calendar references. Manual or scheduled synchronization records bounded outcomes. CalDAV writes use conditional requests and durable outbox state; conflicts and unknown outcomes require synchronization or explicit reconciliation instead of blind repetition. Moving between two-way CalDAV calendars is an administrator-authorized migration batch: all destination resources must be copied before any source resource is conditionally deleted with its recorded ETag. Calendar and event changes remain locked while progress, conflicts, cancellation eligibility, and evidence are visible. Removing an external source removes the connection, while deleting a local calendar deletes its owned events after confirmation or transfer.",
|
|
documentation_types=("admin", "user"),
|
|
audience=("user", "calendar_manager", "operator"),
|
|
related_modules=("connectors", "audit", "ops"),
|
|
metadata={"kind": "reference"},
|
|
),
|
|
DocumentationTopic(
|
|
id="calendar.campaign-invitations-and-replies",
|
|
title="Track Campaign invitations and attendee replies",
|
|
summary="Mirror accepted Campaign invitation deliveries as correlated VEVENTs and keep attendee participation state authoritative in Calendar.",
|
|
body="Campaign can render a METHOD:REQUEST attachment before delivery and create or update the correlated Calendar event only after delivery acceptance. Calendar stores correlation, attendee PARTSTAT, response time, bounded evidence, synchronization state, and any degraded behavior. When Mail watches an authorized IMAP folder, METHOD:REPLY parts are forwarded idempotently to Calendar. Campaign reports query the current state in batches instead of copying it into Campaign records. Recurring Campaign invitation series remain a separate workflow.",
|
|
documentation_types=("admin", "user"),
|
|
audience=("calendar_manager", "campaign_manager", "operator"),
|
|
conditions=(
|
|
DocumentationCondition(
|
|
required_modules=("calendar", "campaigns"),
|
|
required_capabilities=(CAPABILITY_CALENDAR_INVITATIONS,),
|
|
any_scopes=(
|
|
"calendar:calendar:read",
|
|
"calendar:calendar:write",
|
|
"calendar:calendar:admin",
|
|
),
|
|
),
|
|
),
|
|
related_modules=("campaigns", "mail", "audit"),
|
|
metadata={"kind": "workflow"},
|
|
),
|
|
DocumentationTopic(
|
|
id="calendar.outbound-change-recovery",
|
|
title="Recover synchronized calendar writes",
|
|
summary="Inspect unresolved CalDAV writes and recover only the latest safe resource generation.",
|
|
body=(
|
|
"Calendar administrators open Outbound changes from a synchronized calendar's settings. "
|
|
"The bounded history explains attempts, conflicts, dead work, stale generations, disabled sources, "
|
|
"and active worker leases. Retry schedules a failed desired state, reconcile compares it with the "
|
|
"remote resource, and discard abandons the local desired state after a separate warning so the next "
|
|
"full sync can accept remote. Automatic dispatch and due-source execution remain service-account-only "
|
|
"worker operations and are not exposed as interactive controls."
|
|
),
|
|
documentation_types=("admin", "user"),
|
|
audience=("calendar_manager", "operator", "tenant_admin"),
|
|
related_modules=("ops", "audit"),
|
|
metadata={"kind": "runbook"},
|
|
),
|
|
),
|
|
capability_factories={
|
|
CAPABILITY_CALENDAR_OUTBOX: _calendar_outbox_provider,
|
|
CAPABILITY_CALENDAR_SCHEDULING: _calendar_scheduling_provider,
|
|
CAPABILITY_CALENDAR_INVITATIONS: _calendar_invitation_provider,
|
|
CAPABILITY_CALENDAR_EXTERNAL_PROFILES: _calendar_external_profile_provider,
|
|
},
|
|
nav_items=(NavItem(path="/calendar", label="Calendar", icon="calendar", required_any=("calendar:event:read",), order=55),),
|
|
frontend=FrontendModule(
|
|
module_id="calendar",
|
|
package_name="@govoplan/calendar-webui",
|
|
routes=(
|
|
FrontendRoute(
|
|
path="/calendar",
|
|
component="CalendarPage",
|
|
required_any=("calendar:event:read",),
|
|
order=55,
|
|
),
|
|
),
|
|
nav_items=(NavItem(path="/calendar", label="Calendar", icon="calendar", required_any=("calendar:event:read",), order=55),),
|
|
view_surfaces=(
|
|
ViewSurface(
|
|
id="calendar.widget.upcoming",
|
|
module_id="calendar",
|
|
kind="section",
|
|
label="Upcoming events widget",
|
|
order=40,
|
|
),
|
|
ViewSurface(
|
|
id="calendar.settings.preferences",
|
|
module_id="calendar",
|
|
kind="section",
|
|
label="Calendar preferences",
|
|
order=45,
|
|
),
|
|
),
|
|
),
|
|
migration_spec=MigrationSpec(
|
|
module_id="calendar",
|
|
metadata=Base.metadata,
|
|
script_location=str(Path(__file__).with_name("migrations") / "versions"),
|
|
retirement_supported=True,
|
|
retirement_provider=_calendar_retirement_provider,
|
|
retirement_notes="Destructive retirement drops calendar-owned database tables after the installer captures a database snapshot.",
|
|
),
|
|
uninstall_guard_providers=(
|
|
persistent_table_uninstall_guard(
|
|
calendar_models.CalendarCollection,
|
|
calendar_models.CalendarEvent,
|
|
calendar_models.CalendarMigrationBatch,
|
|
calendar_models.CalendarMigrationResource,
|
|
calendar_models.CalendarOutboxOperation,
|
|
calendar_models.CalendarSyncCredential,
|
|
calendar_models.CalendarSyncSource,
|
|
calendar_models.CalendarViewPreference,
|
|
label="Calendar",
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
def get_manifest() -> ModuleManifest:
|
|
return manifest
|