Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bcf41bb1f | |||
| 39d2c28ab1 |
@@ -1,5 +1,9 @@
|
|||||||
# govoplan-calendar
|
# govoplan-calendar
|
||||||
|
|
||||||
|
<!-- govoplan-repository-type:start -->
|
||||||
|
**Repository type:** module (domain).
|
||||||
|
<!-- govoplan-repository-type:end -->
|
||||||
|
|
||||||
GovOPlaN Calendar is the standalone calendar module. It provides tenant calendar collections, iCalendar/VEVENT event storage, a calendar WebUI, and integration boundaries for scheduling, tasks, mail, appointments, workflow, notifications, and external groupware.
|
GovOPlaN Calendar is the standalone calendar module. It provides tenant calendar collections, iCalendar/VEVENT event storage, a calendar WebUI, and integration boundaries for scheduling, tasks, mail, appointments, workflow, notifications, and external groupware.
|
||||||
|
|
||||||
## Ownership
|
## Ownership
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/calendar-webui",
|
"name": "@govoplan/calendar-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "webui/src/index.ts",
|
"main": "webui/src/index.ts",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.8",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-calendar"
|
name = "govoplan-calendar"
|
||||||
version = "0.1.6"
|
version = "0.1.8"
|
||||||
description = "GovOPlaN calendar module with VEVENT storage and WebUI integration."
|
description = "GovOPlaN calendar module with VEVENT storage and WebUI integration."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
authors = [{ name = "GovOPlaN" }]
|
authors = [{ name = "GovOPlaN" }]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"govoplan-core>=0.1.6",
|
"govoplan-core>=0.1.8",
|
||||||
"govoplan-access>=0.1.6",
|
"govoplan-access>=0.1.8",
|
||||||
|
"defusedxml>=0.7,<1",
|
||||||
"icalendar>=7.2",
|
"icalendar>=7.2",
|
||||||
"python-dateutil>=2.9",
|
"python-dateutil>=2.9",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from dataclasses import dataclass, field
|
|||||||
from typing import Mapping, Protocol
|
from typing import Mapping, Protocol
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
|
from defusedxml import ElementTree as SafeElementTree
|
||||||
|
|
||||||
|
|
||||||
class CalDAVError(RuntimeError):
|
class CalDAVError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
@@ -322,8 +324,8 @@ def urllib_transport(method: str, url: str, headers: Mapping[str, str], body: by
|
|||||||
|
|
||||||
def parse_multistatus(payload: bytes) -> CalDAVReportResult:
|
def parse_multistatus(payload: bytes) -> CalDAVReportResult:
|
||||||
try:
|
try:
|
||||||
root = ElementTree.fromstring(payload)
|
root = SafeElementTree.fromstring(payload)
|
||||||
except ElementTree.ParseError as exc:
|
except SafeElementTree.ParseError as exc:
|
||||||
raise CalDAVError(f"Invalid CalDAV XML response: {exc}") from exc
|
raise CalDAVError(f"Invalid CalDAV XML response: {exc}") from exc
|
||||||
objects: list[CalDAVObject] = []
|
objects: list[CalDAVObject] = []
|
||||||
sync_token = first_child_text(root, "sync-token")
|
sync_token = first_child_text(root, "sync-token")
|
||||||
@@ -355,8 +357,8 @@ def parse_multistatus(payload: bytes) -> CalDAVReportResult:
|
|||||||
|
|
||||||
def parse_discovery_multistatus(payload: bytes) -> list[_DAVDiscoveryResponse]:
|
def parse_discovery_multistatus(payload: bytes) -> list[_DAVDiscoveryResponse]:
|
||||||
try:
|
try:
|
||||||
root = ElementTree.fromstring(payload)
|
root = SafeElementTree.fromstring(payload)
|
||||||
except ElementTree.ParseError as exc:
|
except SafeElementTree.ParseError as exc:
|
||||||
raise CalDAVError(f"Invalid CalDAV XML response: {exc}") from exc
|
raise CalDAVError(f"Invalid CalDAV XML response: {exc}") from exc
|
||||||
responses: list[_DAVDiscoveryResponse] = []
|
responses: list[_DAVDiscoveryResponse] = []
|
||||||
for response in child_elements(root, "response"):
|
for response in child_elements(root, "response"):
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ def _calendar_router(context: ModuleContext):
|
|||||||
manifest = ModuleManifest(
|
manifest = ModuleManifest(
|
||||||
id="calendar",
|
id="calendar",
|
||||||
name="Calendar",
|
name="Calendar",
|
||||||
version="0.1.6",
|
version="0.1.8",
|
||||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||||
optional_dependencies=("mail", "tasks", "scheduling", "appointments", "workflow", "notifications", "dms", "connectors"),
|
optional_dependencies=("mail", "tasks", "scheduling", "appointments", "workflow", "notifications", "dms", "connectors"),
|
||||||
permissions=PERMISSIONS,
|
permissions=PERMISSIONS,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Calendar Alembic revisions."""
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
"""v0.1.7 calendar baseline
|
||||||
|
|
||||||
|
Revision ID: 9e0f1a2b3c4d
|
||||||
|
Revises: None
|
||||||
|
Create Date: 2026-07-11 00:00:00.000000
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = '9e0f1a2b3c4d'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = '4f2a9c8e7b6d'
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table('calendar_collections',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('slug', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('timezone', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('color', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('owner_type', sa.String(length=20), nullable=False),
|
||||||
|
sa.Column('owner_id', sa.String(length=36), nullable=True),
|
||||||
|
sa.Column('visibility', sa.String(length=20), nullable=False),
|
||||||
|
sa.Column('is_default', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('created_by_user_id', sa.String(length=36), nullable=True),
|
||||||
|
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('metadata', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_user_id'], ['access_users.id'], name=op.f('fk_calendar_collections_created_by_user_id_access_users'), ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['tenant_id'], ['core_scopes.id'], name=op.f('fk_calendar_collections_tenant_id_scopes'), ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_calendar_collections'))
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_calendar_collections_created_by_user_id'), 'calendar_collections', ['created_by_user_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_collections_deleted_at'), 'calendar_collections', ['deleted_at'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_collections_is_default'), 'calendar_collections', ['is_default'], unique=False)
|
||||||
|
op.create_index('ix_calendar_collections_owner', 'calendar_collections', ['tenant_id', 'owner_type', 'owner_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_collections_owner_id'), 'calendar_collections', ['owner_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_collections_owner_type'), 'calendar_collections', ['owner_type'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_collections_tenant_id'), 'calendar_collections', ['tenant_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_collections_visibility'), 'calendar_collections', ['visibility'], unique=False)
|
||||||
|
op.create_index('uq_calendar_collections_active_slug', 'calendar_collections', ['tenant_id', 'slug'], unique=True, sqlite_where=sa.text('deleted_at IS NULL'), postgresql_where=sa.text('deleted_at IS NULL'))
|
||||||
|
op.create_table('calendar_sync_credentials',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('credential_kind', sa.String(length=30), nullable=False),
|
||||||
|
sa.Column('label', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('secret_encrypted', sa.Text(), nullable=True),
|
||||||
|
sa.Column('created_by_user_id', sa.String(length=36), nullable=True),
|
||||||
|
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('metadata', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_user_id'], ['access_users.id'], name=op.f('fk_calendar_sync_credentials_created_by_user_id_access_users'), ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['tenant_id'], ['core_scopes.id'], name=op.f('fk_calendar_sync_credentials_tenant_id_scopes'), ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_calendar_sync_credentials'))
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_calendar_sync_credentials_created_by_user_id'), 'calendar_sync_credentials', ['created_by_user_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_sync_credentials_credential_kind'), 'calendar_sync_credentials', ['credential_kind'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_sync_credentials_deleted_at'), 'calendar_sync_credentials', ['deleted_at'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_sync_credentials_tenant_id'), 'calendar_sync_credentials', ['tenant_id'], unique=False)
|
||||||
|
op.create_index('ix_calendar_sync_credentials_tenant_kind', 'calendar_sync_credentials', ['tenant_id', 'credential_kind'], unique=False)
|
||||||
|
op.create_table('calendar_events',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('calendar_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('uid', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('recurrence_id', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('sequence', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('summary', sa.String(length=500), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('location', sa.String(length=500), nullable=True),
|
||||||
|
sa.Column('status', sa.String(length=40), nullable=False),
|
||||||
|
sa.Column('transparency', sa.String(length=20), nullable=False),
|
||||||
|
sa.Column('classification', sa.String(length=20), nullable=False),
|
||||||
|
sa.Column('start_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('end_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('duration_seconds', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('all_day', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('timezone', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('organizer', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('attendees', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('categories', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('rrule', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('rdate', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('exdate', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('reminders', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('attachments', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('related_to', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('source_kind', sa.String(length=30), nullable=False),
|
||||||
|
sa.Column('source_href', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('etag', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('icalendar', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('raw_ics', sa.Text(), nullable=True),
|
||||||
|
sa.Column('created_by_user_id', sa.String(length=36), nullable=True),
|
||||||
|
sa.Column('updated_by_user_id', sa.String(length=36), nullable=True),
|
||||||
|
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('metadata', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['calendar_id'], ['calendar_collections.id'], name=op.f('fk_calendar_events_calendar_id_calendar_collections'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_user_id'], ['access_users.id'], name=op.f('fk_calendar_events_created_by_user_id_access_users'), ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['tenant_id'], ['core_scopes.id'], name=op.f('fk_calendar_events_tenant_id_scopes'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['updated_by_user_id'], ['access_users.id'], name=op.f('fk_calendar_events_updated_by_user_id_access_users'), ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_calendar_events')),
|
||||||
|
sa.UniqueConstraint('calendar_id', 'uid', 'recurrence_id', name='uq_calendar_events_calendar_uid_recurrence')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_calendar_events_all_day'), 'calendar_events', ['all_day'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_events_calendar_id'), 'calendar_events', ['calendar_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_events_created_by_user_id'), 'calendar_events', ['created_by_user_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_events_deleted_at'), 'calendar_events', ['deleted_at'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_events_end_at'), 'calendar_events', ['end_at'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_events_etag'), 'calendar_events', ['etag'], unique=False)
|
||||||
|
op.create_index('ix_calendar_events_range', 'calendar_events', ['tenant_id', 'calendar_id', 'start_at', 'end_at'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_events_recurrence_id'), 'calendar_events', ['recurrence_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_events_source_kind'), 'calendar_events', ['source_kind'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_events_start_at'), 'calendar_events', ['start_at'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_events_status'), 'calendar_events', ['status'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_events_tenant_id'), 'calendar_events', ['tenant_id'], unique=False)
|
||||||
|
op.create_index('ix_calendar_events_tenant_status', 'calendar_events', ['tenant_id', 'status'], unique=False)
|
||||||
|
op.create_index('ix_calendar_events_tenant_uid', 'calendar_events', ['tenant_id', 'uid'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_events_uid'), 'calendar_events', ['uid'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_events_updated_by_user_id'), 'calendar_events', ['updated_by_user_id'], unique=False)
|
||||||
|
op.create_table('calendar_sync_sources',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('calendar_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('source_kind', sa.String(length=30), nullable=False),
|
||||||
|
sa.Column('collection_url', sa.String(length=1000), nullable=False),
|
||||||
|
sa.Column('display_name', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('auth_type', sa.String(length=30), nullable=False),
|
||||||
|
sa.Column('username', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('credential_ref', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('sync_enabled', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('sync_interval_seconds', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('sync_direction', sa.String(length=30), nullable=False),
|
||||||
|
sa.Column('conflict_policy', sa.String(length=30), nullable=False),
|
||||||
|
sa.Column('sync_token', sa.Text(), nullable=True),
|
||||||
|
sa.Column('ctag', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('last_attempt_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('last_synced_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('next_sync_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('last_status', sa.String(length=30), nullable=True),
|
||||||
|
sa.Column('last_error', sa.Text(), nullable=True),
|
||||||
|
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('metadata', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['calendar_id'], ['calendar_collections.id'], name=op.f('fk_calendar_sync_sources_calendar_id_calendar_collections'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['tenant_id'], ['core_scopes.id'], name=op.f('fk_calendar_sync_sources_tenant_id_scopes'), ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_calendar_sync_sources'))
|
||||||
|
)
|
||||||
|
op.create_index('ix_calendar_sync_sources_calendar', 'calendar_sync_sources', ['tenant_id', 'calendar_id', 'source_kind'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_sync_sources_calendar_id'), 'calendar_sync_sources', ['calendar_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_sync_sources_deleted_at'), 'calendar_sync_sources', ['deleted_at'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_sync_sources_next_sync_at'), 'calendar_sync_sources', ['next_sync_at'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_sync_sources_source_kind'), 'calendar_sync_sources', ['source_kind'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_sync_sources_sync_enabled'), 'calendar_sync_sources', ['sync_enabled'], unique=False)
|
||||||
|
op.create_index(op.f('ix_calendar_sync_sources_tenant_id'), 'calendar_sync_sources', ['tenant_id'], unique=False)
|
||||||
|
op.create_index('uq_calendar_sync_sources_active_url', 'calendar_sync_sources', ['tenant_id', 'source_kind', 'collection_url'], unique=True, sqlite_where=sa.text('deleted_at IS NULL'), postgresql_where=sa.text('deleted_at IS NULL'))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table('calendar_sync_sources')
|
||||||
|
op.drop_table('calendar_events')
|
||||||
|
op.drop_table('calendar_sync_credentials')
|
||||||
|
op.drop_table('calendar_collections')
|
||||||
@@ -13,6 +13,7 @@ from datetime import datetime, timedelta, timezone
|
|||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
|
|
||||||
|
from defusedxml import ElementTree as SafeElementTree
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@@ -1375,8 +1376,8 @@ def ews_find_item_body(*, start: datetime, end: datetime, mailbox: Any | None =
|
|||||||
|
|
||||||
def parse_ews_calendar_items(xml_text: str) -> list[dict[str, Any]]:
|
def parse_ews_calendar_items(xml_text: str) -> list[dict[str, Any]]:
|
||||||
try:
|
try:
|
||||||
root = ET.fromstring(xml_text)
|
root = SafeElementTree.fromstring(xml_text)
|
||||||
except ET.ParseError as exc:
|
except SafeElementTree.ParseError as exc:
|
||||||
raise CalendarError(f"Invalid EWS response XML: {exc}") from exc
|
raise CalendarError(f"Invalid EWS response XML: {exc}") from exc
|
||||||
items: list[dict[str, Any]] = []
|
items: list[dict[str, Any]] = []
|
||||||
for item in root.findall(f".//{{{EWS_TYPES_NS}}}CalendarItem"):
|
for item in root.findall(f".//{{{EWS_TYPES_NS}}}CalendarItem"):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/calendar-webui",
|
"name": "@govoplan/calendar-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"./styles/calendar.css": "./src/styles/calendar.css"
|
"./styles/calendar.css": "./src/styles/calendar.css"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.8",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user