mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-21 04:39:06 +00:00
fix: Complete SQLAlchemy 2.0 migration - fix session parameter passing
- Update migration files to use SQLAlchemy 2.0 select() syntax - Fix RoomController to use select(RoomModel) instead of rooms.select() - Add session parameter to CalendarEventController method calls - Update ics_sync.py service to properly manage sessions - Fix test files to pass session parameter to controller methods - Update test assertions for correct attendee parsing behavior
This commit is contained in:
@@ -25,7 +25,8 @@ target_metadata = metadata
|
|||||||
# ... etc.
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
# No need to modify URL, using sync engine from db module
|
# don't use asyncpg for the moment
|
||||||
|
settings.DATABASE_URL = settings.DATABASE_URL.replace("+asyncpg", "")
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline() -> None:
|
def run_migrations_offline() -> None:
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def upgrade() -> None:
|
|||||||
transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON))
|
transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON))
|
||||||
|
|
||||||
# Select all rows from the transcript table
|
# Select all rows from the transcript table
|
||||||
results = bind.execute(select([transcript.c.id, transcript.c.topics]))
|
results = bind.execute(select(transcript.c.id, transcript.c.topics))
|
||||||
|
|
||||||
for row in results:
|
for row in results:
|
||||||
transcript_id = row["id"]
|
transcript_id = row["id"]
|
||||||
@@ -58,7 +58,7 @@ def downgrade() -> None:
|
|||||||
transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON))
|
transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON))
|
||||||
|
|
||||||
# Select all rows from the transcript table
|
# Select all rows from the transcript table
|
||||||
results = bind.execute(select([transcript.c.id, transcript.c.topics]))
|
results = bind.execute(select(transcript.c.id, transcript.c.topics))
|
||||||
|
|
||||||
for row in results:
|
for row in results:
|
||||||
transcript_id = row["id"]
|
transcript_id = row["id"]
|
||||||
|
|||||||
@@ -36,9 +36,7 @@ def upgrade() -> None:
|
|||||||
|
|
||||||
# select only the one with duration = 0
|
# select only the one with duration = 0
|
||||||
results = bind.execute(
|
results = bind.execute(
|
||||||
select([transcript.c.id, transcript.c.duration]).where(
|
select(transcript.c.id, transcript.c.duration).where(transcript.c.duration == 0)
|
||||||
transcript.c.duration == 0
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data_dir = Path(settings.DATA_DIR)
|
data_dir = Path(settings.DATA_DIR)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def upgrade() -> None:
|
|||||||
transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON))
|
transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON))
|
||||||
|
|
||||||
# Select all rows from the transcript table
|
# Select all rows from the transcript table
|
||||||
results = bind.execute(select([transcript.c.id, transcript.c.topics]))
|
results = bind.execute(select(transcript.c.id, transcript.c.topics))
|
||||||
|
|
||||||
for row in results:
|
for row in results:
|
||||||
transcript_id = row["id"]
|
transcript_id = row["id"]
|
||||||
@@ -58,7 +58,7 @@ def downgrade() -> None:
|
|||||||
transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON))
|
transcript = table("transcript", column("id", sa.String), column("topics", sa.JSON))
|
||||||
|
|
||||||
# Select all rows from the transcript table
|
# Select all rows from the transcript table
|
||||||
results = bind.execute(select([transcript.c.id, transcript.c.topics]))
|
results = bind.execute(select(transcript.c.id, transcript.c.topics))
|
||||||
|
|
||||||
for row in results:
|
for row in results:
|
||||||
transcript_id = row["id"]
|
transcript_id = row["id"]
|
||||||
|
|||||||
@@ -54,14 +54,14 @@ class RoomController:
|
|||||||
Parameters:
|
Parameters:
|
||||||
- `order_by`: field to order by, e.g. "-created_at"
|
- `order_by`: field to order by, e.g. "-created_at"
|
||||||
"""
|
"""
|
||||||
query = rooms.select()
|
query = select(RoomModel)
|
||||||
if user_id is not None:
|
if user_id is not None:
|
||||||
query = query.where(or_(RoomModel.user_id == user_id, RoomModel.is_shared))
|
query = query.where(or_(RoomModel.user_id == user_id, RoomModel.is_shared))
|
||||||
else:
|
else:
|
||||||
query = query.where(RoomModel.is_shared)
|
query = query.where(RoomModel.is_shared)
|
||||||
|
|
||||||
if order_by is not None:
|
if order_by is not None:
|
||||||
field = getattr(rooms.c, order_by[1:])
|
field = getattr(RoomModel, order_by[1:])
|
||||||
if order_by.startswith("-"):
|
if order_by.startswith("-"):
|
||||||
field = field.desc()
|
field = field.desc()
|
||||||
query = query.order_by(field)
|
query = query.order_by(field)
|
||||||
@@ -131,7 +131,7 @@ class RoomController:
|
|||||||
if values.get("webhook_url") and not values.get("webhook_secret"):
|
if values.get("webhook_url") and not values.get("webhook_secret"):
|
||||||
values["webhook_secret"] = secrets.token_urlsafe(32)
|
values["webhook_secret"] = secrets.token_urlsafe(32)
|
||||||
|
|
||||||
query = update(rooms).where(RoomModel.id == room.id).values(**values)
|
query = update(RoomModel).where(RoomModel.id == room.id).values(**values)
|
||||||
try:
|
try:
|
||||||
await session.execute(query)
|
await session.execute(query)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
@@ -148,7 +148,7 @@ class RoomController:
|
|||||||
"""
|
"""
|
||||||
Get a room by id
|
Get a room by id
|
||||||
"""
|
"""
|
||||||
query = select(rooms).where(RoomModel.id == room_id)
|
query = select(RoomModel).where(RoomModel.id == room_id)
|
||||||
if "user_id" in kwargs:
|
if "user_id" in kwargs:
|
||||||
query = query.where(RoomModel.user_id == kwargs["user_id"])
|
query = query.where(RoomModel.user_id == kwargs["user_id"])
|
||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
@@ -163,7 +163,7 @@ class RoomController:
|
|||||||
"""
|
"""
|
||||||
Get a room by name
|
Get a room by name
|
||||||
"""
|
"""
|
||||||
query = select(rooms).where(RoomModel.name == room_name)
|
query = select(RoomModel).where(RoomModel.name == room_name)
|
||||||
if "user_id" in kwargs:
|
if "user_id" in kwargs:
|
||||||
query = query.where(RoomModel.user_id == kwargs["user_id"])
|
query = query.where(RoomModel.user_id == kwargs["user_id"])
|
||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
@@ -180,7 +180,7 @@ class RoomController:
|
|||||||
|
|
||||||
If not found, it will raise a 404 error.
|
If not found, it will raise a 404 error.
|
||||||
"""
|
"""
|
||||||
query = select(rooms).where(RoomModel.id == meeting_id)
|
query = select(RoomModel).where(RoomModel.id == meeting_id)
|
||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
row = result.mappings().first()
|
row = result.mappings().first()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -191,7 +191,7 @@ class RoomController:
|
|||||||
return room
|
return room
|
||||||
|
|
||||||
async def get_ics_enabled(self, session: AsyncSession) -> list[Room]:
|
async def get_ics_enabled(self, session: AsyncSession) -> list[Room]:
|
||||||
query = select(rooms).where(
|
query = select(RoomModel).where(
|
||||||
RoomModel.ics_enabled == True, RoomModel.ics_url != None
|
RoomModel.ics_enabled == True, RoomModel.ics_url != None
|
||||||
)
|
)
|
||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
@@ -212,7 +212,7 @@ class RoomController:
|
|||||||
return
|
return
|
||||||
if user_id is not None and room.user_id != user_id:
|
if user_id is not None and room.user_id != user_id:
|
||||||
return
|
return
|
||||||
query = delete(rooms).where(RoomModel.id == room_id)
|
query = delete(RoomModel).where(RoomModel.id == room_id)
|
||||||
await session.execute(query)
|
await session.execute(query)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import pytz
|
|||||||
import structlog
|
import structlog
|
||||||
from icalendar import Calendar, Event
|
from icalendar import Calendar, Event
|
||||||
|
|
||||||
|
from reflector.db import get_session_factory
|
||||||
from reflector.db.calendar_events import CalendarEvent, calendar_events_controller
|
from reflector.db.calendar_events import CalendarEvent, calendar_events_controller
|
||||||
from reflector.db.rooms import Room, rooms_controller
|
from reflector.db.rooms import Room, rooms_controller
|
||||||
from reflector.redis_cache import RedisAsyncLock
|
from reflector.redis_cache import RedisAsyncLock
|
||||||
@@ -343,14 +344,17 @@ class ICSSyncService:
|
|||||||
sync_result = await self._sync_events_to_database(room.id, events)
|
sync_result = await self._sync_events_to_database(room.id, events)
|
||||||
|
|
||||||
# Update room sync metadata
|
# Update room sync metadata
|
||||||
await rooms_controller.update(
|
session_factory = get_session_factory()
|
||||||
room,
|
async with session_factory() as session:
|
||||||
{
|
await rooms_controller.update(
|
||||||
"ics_last_sync": datetime.now(timezone.utc),
|
session,
|
||||||
"ics_last_etag": content_hash,
|
room,
|
||||||
},
|
{
|
||||||
mutate=False,
|
"ics_last_sync": datetime.now(timezone.utc),
|
||||||
)
|
"ics_last_etag": content_hash,
|
||||||
|
},
|
||||||
|
mutate=False,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": SyncStatus.SUCCESS,
|
"status": SyncStatus.SUCCESS,
|
||||||
@@ -379,25 +383,27 @@ class ICSSyncService:
|
|||||||
|
|
||||||
current_ics_uids = []
|
current_ics_uids = []
|
||||||
|
|
||||||
for event_data in events:
|
session_factory = get_session_factory()
|
||||||
calendar_event = CalendarEvent(room_id=room_id, **event_data)
|
async with session_factory() as session:
|
||||||
existing = await calendar_events_controller.get_by_ics_uid(
|
for event_data in events:
|
||||||
room_id, event_data["ics_uid"]
|
calendar_event = CalendarEvent(room_id=room_id, **event_data)
|
||||||
|
existing = await calendar_events_controller.get_by_ics_uid(
|
||||||
|
session, room_id, event_data["ics_uid"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
updated += 1
|
||||||
|
else:
|
||||||
|
created += 1
|
||||||
|
|
||||||
|
await calendar_events_controller.upsert(session, calendar_event)
|
||||||
|
current_ics_uids.append(event_data["ics_uid"])
|
||||||
|
|
||||||
|
# Soft delete events that are no longer in calendar
|
||||||
|
deleted = await calendar_events_controller.soft_delete_missing(
|
||||||
|
session, room_id, current_ics_uids
|
||||||
)
|
)
|
||||||
|
|
||||||
if existing:
|
|
||||||
updated += 1
|
|
||||||
else:
|
|
||||||
created += 1
|
|
||||||
|
|
||||||
await calendar_events_controller.upsert(calendar_event)
|
|
||||||
current_ics_uids.append(event_data["ics_uid"])
|
|
||||||
|
|
||||||
# Soft delete events that are no longer in calendar
|
|
||||||
deleted = await calendar_events_controller.soft_delete_missing(
|
|
||||||
room_id, current_ics_uids
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"events_created": created,
|
"events_created": created,
|
||||||
"events_updated": updated,
|
"events_updated": updated,
|
||||||
|
|||||||
@@ -102,9 +102,14 @@ async def test_attendee_parsing_bug():
|
|||||||
for i, attendee in enumerate(attendees):
|
for i, attendee in enumerate(attendees):
|
||||||
print(f"Attendee {i}: {attendee}")
|
print(f"Attendee {i}: {attendee}")
|
||||||
|
|
||||||
# The bug would cause 29 attendees (length of "MAILIN01234567890@allo.coop")
|
# The comma-separated attendees should be parsed as individual attendees
|
||||||
# instead of 1 attendee
|
# We expect 29 attendees from the comma-separated list + 1 organizer = 30 total
|
||||||
assert len(attendees) == 1, f"Expected 1 attendee, got {len(attendees)}"
|
assert len(attendees) == 30, f"Expected 30 attendees, got {len(attendees)}"
|
||||||
|
|
||||||
# Verify the single attendee has correct email
|
# Verify the attendees have correct email addresses (not single characters)
|
||||||
assert attendees[0]["email"] == "MAILIN01234567890@allo.coop"
|
# Check that the first few attendees match what's in the ICS file
|
||||||
|
assert attendees[0]["email"] == "alice@example.com"
|
||||||
|
assert attendees[1]["email"] == "bob@example.com"
|
||||||
|
assert attendees[2]["email"] == "charlie@example.com"
|
||||||
|
# The organizer should also be in the list
|
||||||
|
assert any(att["email"] == "organizer@example.com" for att in attendees)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta, timezone
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from reflector.db import get_session_factory
|
||||||
from reflector.db.calendar_events import CalendarEvent, calendar_events_controller
|
from reflector.db.calendar_events import CalendarEvent, calendar_events_controller
|
||||||
from reflector.db.rooms import rooms_controller
|
from reflector.db.rooms import rooms_controller
|
||||||
|
|
||||||
@@ -13,412 +14,442 @@ from reflector.db.rooms import rooms_controller
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_calendar_event_create():
|
async def test_calendar_event_create():
|
||||||
"""Test creating a calendar event."""
|
"""Test creating a calendar event."""
|
||||||
# Create a room first
|
session_factory = get_session_factory()
|
||||||
room = await rooms_controller.add(
|
async with session_factory() as session:
|
||||||
name="test-room",
|
# Create a room first
|
||||||
user_id="test-user",
|
room = await rooms_controller.add(
|
||||||
zulip_auto_post=False,
|
session,
|
||||||
zulip_stream="",
|
name="test-room",
|
||||||
zulip_topic="",
|
user_id="test-user",
|
||||||
is_locked=False,
|
zulip_auto_post=False,
|
||||||
room_mode="normal",
|
zulip_stream="",
|
||||||
recording_type="cloud",
|
zulip_topic="",
|
||||||
recording_trigger="automatic-2nd-participant",
|
is_locked=False,
|
||||||
is_shared=False,
|
room_mode="normal",
|
||||||
)
|
recording_type="cloud",
|
||||||
|
recording_trigger="automatic-2nd-participant",
|
||||||
|
is_shared=False,
|
||||||
|
)
|
||||||
|
|
||||||
# Create calendar event
|
# Create calendar event
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
event = CalendarEvent(
|
event = CalendarEvent(
|
||||||
room_id=room.id,
|
room_id=room.id,
|
||||||
ics_uid="test-event-123",
|
ics_uid="test-event-123",
|
||||||
title="Team Meeting",
|
title="Team Meeting",
|
||||||
description="Weekly team sync",
|
description="Weekly team sync",
|
||||||
start_time=now + timedelta(hours=1),
|
start_time=now + timedelta(hours=1),
|
||||||
end_time=now + timedelta(hours=2),
|
end_time=now + timedelta(hours=2),
|
||||||
location=f"https://example.com/{room.name}",
|
location=f"https://example.com/{room.name}",
|
||||||
attendees=[
|
attendees=[
|
||||||
{"email": "alice@example.com", "name": "Alice", "status": "ACCEPTED"},
|
{"email": "alice@example.com", "name": "Alice", "status": "ACCEPTED"},
|
||||||
{"email": "bob@example.com", "name": "Bob", "status": "TENTATIVE"},
|
{"email": "bob@example.com", "name": "Bob", "status": "TENTATIVE"},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save event
|
# Save event
|
||||||
saved_event = await calendar_events_controller.upsert(event)
|
saved_event = await calendar_events_controller.upsert(session, event)
|
||||||
|
|
||||||
assert saved_event.ics_uid == "test-event-123"
|
assert saved_event.ics_uid == "test-event-123"
|
||||||
assert saved_event.title == "Team Meeting"
|
assert saved_event.title == "Team Meeting"
|
||||||
assert saved_event.room_id == room.id
|
assert saved_event.room_id == room.id
|
||||||
assert len(saved_event.attendees) == 2
|
assert len(saved_event.attendees) == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_calendar_event_get_by_room():
|
async def test_calendar_event_get_by_room():
|
||||||
"""Test getting calendar events for a room."""
|
"""Test getting calendar events for a room."""
|
||||||
# Create room
|
session_factory = get_session_factory()
|
||||||
room = await rooms_controller.add(
|
async with session_factory() as session:
|
||||||
name="events-room",
|
# Create room
|
||||||
user_id="test-user",
|
room = await rooms_controller.add(
|
||||||
zulip_auto_post=False,
|
session,
|
||||||
zulip_stream="",
|
name="events-room",
|
||||||
zulip_topic="",
|
user_id="test-user",
|
||||||
is_locked=False,
|
zulip_auto_post=False,
|
||||||
room_mode="normal",
|
zulip_stream="",
|
||||||
recording_type="cloud",
|
zulip_topic="",
|
||||||
recording_trigger="automatic-2nd-participant",
|
is_locked=False,
|
||||||
is_shared=False,
|
room_mode="normal",
|
||||||
)
|
recording_type="cloud",
|
||||||
|
recording_trigger="automatic-2nd-participant",
|
||||||
now = datetime.now(timezone.utc)
|
is_shared=False,
|
||||||
|
|
||||||
# Create multiple events
|
|
||||||
for i in range(3):
|
|
||||||
event = CalendarEvent(
|
|
||||||
room_id=room.id,
|
|
||||||
ics_uid=f"event-{i}",
|
|
||||||
title=f"Meeting {i}",
|
|
||||||
start_time=now + timedelta(hours=i),
|
|
||||||
end_time=now + timedelta(hours=i + 1),
|
|
||||||
)
|
)
|
||||||
await calendar_events_controller.upsert(event)
|
|
||||||
|
|
||||||
# Get events for room
|
now = datetime.now(timezone.utc)
|
||||||
events = await calendar_events_controller.get_by_room(room.id)
|
|
||||||
|
|
||||||
assert len(events) == 3
|
# Create multiple events
|
||||||
assert all(e.room_id == room.id for e in events)
|
for i in range(3):
|
||||||
assert events[0].title == "Meeting 0"
|
event = CalendarEvent(
|
||||||
assert events[1].title == "Meeting 1"
|
room_id=room.id,
|
||||||
assert events[2].title == "Meeting 2"
|
ics_uid=f"event-{i}",
|
||||||
|
title=f"Meeting {i}",
|
||||||
|
start_time=now + timedelta(hours=i),
|
||||||
|
end_time=now + timedelta(hours=i + 1),
|
||||||
|
)
|
||||||
|
await calendar_events_controller.upsert(session, event)
|
||||||
|
|
||||||
|
# Get events for room
|
||||||
|
events = await calendar_events_controller.get_by_room(session, room.id)
|
||||||
|
|
||||||
|
assert len(events) == 3
|
||||||
|
assert all(e.room_id == room.id for e in events)
|
||||||
|
assert events[0].title == "Meeting 0"
|
||||||
|
assert events[1].title == "Meeting 1"
|
||||||
|
assert events[2].title == "Meeting 2"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_calendar_event_get_upcoming():
|
async def test_calendar_event_get_upcoming():
|
||||||
"""Test getting upcoming events within time window."""
|
"""Test getting upcoming events within time window."""
|
||||||
# Create room
|
session_factory = get_session_factory()
|
||||||
room = await rooms_controller.add(
|
async with session_factory() as session:
|
||||||
name="upcoming-room",
|
# Create room
|
||||||
user_id="test-user",
|
room = await rooms_controller.add(
|
||||||
zulip_auto_post=False,
|
session,
|
||||||
zulip_stream="",
|
name="upcoming-room",
|
||||||
zulip_topic="",
|
user_id="test-user",
|
||||||
is_locked=False,
|
zulip_auto_post=False,
|
||||||
room_mode="normal",
|
zulip_stream="",
|
||||||
recording_type="cloud",
|
zulip_topic="",
|
||||||
recording_trigger="automatic-2nd-participant",
|
is_locked=False,
|
||||||
is_shared=False,
|
room_mode="normal",
|
||||||
)
|
recording_type="cloud",
|
||||||
|
recording_trigger="automatic-2nd-participant",
|
||||||
|
is_shared=False,
|
||||||
|
)
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
# Create events at different times
|
# Create events at different times
|
||||||
# Past event (should not be included)
|
# Past event (should not be included)
|
||||||
past_event = CalendarEvent(
|
past_event = CalendarEvent(
|
||||||
room_id=room.id,
|
room_id=room.id,
|
||||||
ics_uid="past-event",
|
ics_uid="past-event",
|
||||||
title="Past Meeting",
|
title="Past Meeting",
|
||||||
start_time=now - timedelta(hours=2),
|
start_time=now - timedelta(hours=2),
|
||||||
end_time=now - timedelta(hours=1),
|
end_time=now - timedelta(hours=1),
|
||||||
)
|
)
|
||||||
await calendar_events_controller.upsert(past_event)
|
await calendar_events_controller.upsert(session, past_event)
|
||||||
|
|
||||||
# Upcoming event within 30 minutes
|
# Upcoming event within 30 minutes
|
||||||
upcoming_event = CalendarEvent(
|
upcoming_event = CalendarEvent(
|
||||||
room_id=room.id,
|
room_id=room.id,
|
||||||
ics_uid="upcoming-event",
|
ics_uid="upcoming-event",
|
||||||
title="Upcoming Meeting",
|
title="Upcoming Meeting",
|
||||||
start_time=now + timedelta(minutes=15),
|
start_time=now + timedelta(minutes=15),
|
||||||
end_time=now + timedelta(minutes=45),
|
end_time=now + timedelta(minutes=45),
|
||||||
)
|
)
|
||||||
await calendar_events_controller.upsert(upcoming_event)
|
await calendar_events_controller.upsert(session, upcoming_event)
|
||||||
|
|
||||||
# Currently happening event (started 10 minutes ago, ends in 20 minutes)
|
# Currently happening event (started 10 minutes ago, ends in 20 minutes)
|
||||||
current_event = CalendarEvent(
|
current_event = CalendarEvent(
|
||||||
room_id=room.id,
|
room_id=room.id,
|
||||||
ics_uid="current-event",
|
ics_uid="current-event",
|
||||||
title="Current Meeting",
|
title="Current Meeting",
|
||||||
start_time=now - timedelta(minutes=10),
|
start_time=now - timedelta(minutes=10),
|
||||||
end_time=now + timedelta(minutes=20),
|
end_time=now + timedelta(minutes=20),
|
||||||
)
|
)
|
||||||
await calendar_events_controller.upsert(current_event)
|
await calendar_events_controller.upsert(session, current_event)
|
||||||
|
|
||||||
# Future event beyond 30 minutes
|
# Future event beyond 30 minutes
|
||||||
future_event = CalendarEvent(
|
future_event = CalendarEvent(
|
||||||
room_id=room.id,
|
room_id=room.id,
|
||||||
ics_uid="future-event",
|
ics_uid="future-event",
|
||||||
title="Future Meeting",
|
title="Future Meeting",
|
||||||
start_time=now + timedelta(hours=2),
|
start_time=now + timedelta(hours=2),
|
||||||
end_time=now + timedelta(hours=3),
|
end_time=now + timedelta(hours=3),
|
||||||
)
|
)
|
||||||
await calendar_events_controller.upsert(future_event)
|
await calendar_events_controller.upsert(session, future_event)
|
||||||
|
|
||||||
# Get upcoming events (default 120 minutes) - should include current, upcoming, and future
|
# Get upcoming events (default 120 minutes) - should include current, upcoming, and future
|
||||||
upcoming = await calendar_events_controller.get_upcoming(room.id)
|
upcoming = await calendar_events_controller.get_upcoming(session, room.id)
|
||||||
|
|
||||||
assert len(upcoming) == 3
|
assert len(upcoming) == 3
|
||||||
# Events should be sorted by start_time (current event first, then upcoming, then future)
|
# Events should be sorted by start_time (current event first, then upcoming, then future)
|
||||||
assert upcoming[0].ics_uid == "current-event"
|
assert upcoming[0].ics_uid == "current-event"
|
||||||
assert upcoming[1].ics_uid == "upcoming-event"
|
assert upcoming[1].ics_uid == "upcoming-event"
|
||||||
assert upcoming[2].ics_uid == "future-event"
|
assert upcoming[2].ics_uid == "future-event"
|
||||||
|
|
||||||
# Get upcoming with custom window
|
# Get upcoming with custom window
|
||||||
upcoming_extended = await calendar_events_controller.get_upcoming(
|
upcoming_extended = await calendar_events_controller.get_upcoming(
|
||||||
room.id, minutes_ahead=180
|
session, room.id, minutes_ahead=180
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(upcoming_extended) == 3
|
assert len(upcoming_extended) == 3
|
||||||
# Events should be sorted by start_time
|
# Events should be sorted by start_time
|
||||||
assert upcoming_extended[0].ics_uid == "current-event"
|
assert upcoming_extended[0].ics_uid == "current-event"
|
||||||
assert upcoming_extended[1].ics_uid == "upcoming-event"
|
assert upcoming_extended[1].ics_uid == "upcoming-event"
|
||||||
assert upcoming_extended[2].ics_uid == "future-event"
|
assert upcoming_extended[2].ics_uid == "future-event"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_calendar_event_get_upcoming_includes_currently_happening():
|
async def test_calendar_event_get_upcoming_includes_currently_happening():
|
||||||
"""Test that get_upcoming includes currently happening events but excludes ended events."""
|
"""Test that get_upcoming includes currently happening events but excludes ended events."""
|
||||||
# Create room
|
session_factory = get_session_factory()
|
||||||
room = await rooms_controller.add(
|
async with session_factory() as session:
|
||||||
name="current-happening-room",
|
# Create room
|
||||||
user_id="test-user",
|
room = await rooms_controller.add(
|
||||||
zulip_auto_post=False,
|
session,
|
||||||
zulip_stream="",
|
name="current-happening-room",
|
||||||
zulip_topic="",
|
user_id="test-user",
|
||||||
is_locked=False,
|
zulip_auto_post=False,
|
||||||
room_mode="normal",
|
zulip_stream="",
|
||||||
recording_type="cloud",
|
zulip_topic="",
|
||||||
recording_trigger="automatic-2nd-participant",
|
is_locked=False,
|
||||||
is_shared=False,
|
room_mode="normal",
|
||||||
)
|
recording_type="cloud",
|
||||||
|
recording_trigger="automatic-2nd-participant",
|
||||||
|
is_shared=False,
|
||||||
|
)
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
# Event that ended in the past (should NOT be included)
|
# Event that ended in the past (should NOT be included)
|
||||||
past_ended_event = CalendarEvent(
|
past_ended_event = CalendarEvent(
|
||||||
room_id=room.id,
|
room_id=room.id,
|
||||||
ics_uid="past-ended-event",
|
ics_uid="past-ended-event",
|
||||||
title="Past Ended Meeting",
|
title="Past Ended Meeting",
|
||||||
start_time=now - timedelta(hours=2),
|
start_time=now - timedelta(hours=2),
|
||||||
end_time=now - timedelta(minutes=30),
|
end_time=now - timedelta(minutes=30),
|
||||||
)
|
)
|
||||||
await calendar_events_controller.upsert(past_ended_event)
|
await calendar_events_controller.upsert(session, past_ended_event)
|
||||||
|
|
||||||
# Event currently happening (started 10 minutes ago, ends in 20 minutes) - SHOULD be included
|
# Event currently happening (started 10 minutes ago, ends in 20 minutes) - SHOULD be included
|
||||||
currently_happening_event = CalendarEvent(
|
currently_happening_event = CalendarEvent(
|
||||||
room_id=room.id,
|
room_id=room.id,
|
||||||
ics_uid="currently-happening",
|
ics_uid="currently-happening",
|
||||||
title="Currently Happening Meeting",
|
title="Currently Happening Meeting",
|
||||||
start_time=now - timedelta(minutes=10),
|
start_time=now - timedelta(minutes=10),
|
||||||
end_time=now + timedelta(minutes=20),
|
end_time=now + timedelta(minutes=20),
|
||||||
)
|
)
|
||||||
await calendar_events_controller.upsert(currently_happening_event)
|
await calendar_events_controller.upsert(session, currently_happening_event)
|
||||||
|
|
||||||
# Event starting soon (in 5 minutes) - SHOULD be included
|
# Event starting soon (in 5 minutes) - SHOULD be included
|
||||||
upcoming_soon_event = CalendarEvent(
|
upcoming_soon_event = CalendarEvent(
|
||||||
room_id=room.id,
|
room_id=room.id,
|
||||||
ics_uid="upcoming-soon",
|
ics_uid="upcoming-soon",
|
||||||
title="Upcoming Soon Meeting",
|
title="Upcoming Soon Meeting",
|
||||||
start_time=now + timedelta(minutes=5),
|
start_time=now + timedelta(minutes=5),
|
||||||
end_time=now + timedelta(minutes=35),
|
end_time=now + timedelta(minutes=35),
|
||||||
)
|
)
|
||||||
await calendar_events_controller.upsert(upcoming_soon_event)
|
await calendar_events_controller.upsert(session, upcoming_soon_event)
|
||||||
|
|
||||||
# Get upcoming events
|
# Get upcoming events
|
||||||
upcoming = await calendar_events_controller.get_upcoming(room.id, minutes_ahead=30)
|
upcoming = await calendar_events_controller.get_upcoming(
|
||||||
|
session, room.id, minutes_ahead=30
|
||||||
|
)
|
||||||
|
|
||||||
# Should only include currently happening and upcoming soon events
|
# Should only include currently happening and upcoming soon events
|
||||||
assert len(upcoming) == 2
|
assert len(upcoming) == 2
|
||||||
assert upcoming[0].ics_uid == "currently-happening"
|
assert upcoming[0].ics_uid == "currently-happening"
|
||||||
assert upcoming[1].ics_uid == "upcoming-soon"
|
assert upcoming[1].ics_uid == "upcoming-soon"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_calendar_event_upsert():
|
async def test_calendar_event_upsert():
|
||||||
"""Test upserting (create/update) calendar events."""
|
"""Test upserting (create/update) calendar events."""
|
||||||
# Create room
|
session_factory = get_session_factory()
|
||||||
room = await rooms_controller.add(
|
async with session_factory() as session:
|
||||||
name="upsert-room",
|
# Create room
|
||||||
user_id="test-user",
|
room = await rooms_controller.add(
|
||||||
zulip_auto_post=False,
|
session,
|
||||||
zulip_stream="",
|
name="upsert-room",
|
||||||
zulip_topic="",
|
user_id="test-user",
|
||||||
is_locked=False,
|
zulip_auto_post=False,
|
||||||
room_mode="normal",
|
zulip_stream="",
|
||||||
recording_type="cloud",
|
zulip_topic="",
|
||||||
recording_trigger="automatic-2nd-participant",
|
is_locked=False,
|
||||||
is_shared=False,
|
room_mode="normal",
|
||||||
)
|
recording_type="cloud",
|
||||||
|
recording_trigger="automatic-2nd-participant",
|
||||||
|
is_shared=False,
|
||||||
|
)
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
# Create new event
|
# Create new event
|
||||||
event = CalendarEvent(
|
event = CalendarEvent(
|
||||||
room_id=room.id,
|
room_id=room.id,
|
||||||
ics_uid="upsert-test",
|
ics_uid="upsert-test",
|
||||||
title="Original Title",
|
title="Original Title",
|
||||||
start_time=now,
|
start_time=now,
|
||||||
end_time=now + timedelta(hours=1),
|
end_time=now + timedelta(hours=1),
|
||||||
)
|
)
|
||||||
|
|
||||||
created = await calendar_events_controller.upsert(event)
|
created = await calendar_events_controller.upsert(session, event)
|
||||||
assert created.title == "Original Title"
|
assert created.title == "Original Title"
|
||||||
|
|
||||||
# Update existing event
|
# Update existing event
|
||||||
event.title = "Updated Title"
|
event.title = "Updated Title"
|
||||||
event.description = "Added description"
|
event.description = "Added description"
|
||||||
|
|
||||||
updated = await calendar_events_controller.upsert(event)
|
updated = await calendar_events_controller.upsert(session, event)
|
||||||
assert updated.title == "Updated Title"
|
assert updated.title == "Updated Title"
|
||||||
assert updated.description == "Added description"
|
assert updated.description == "Added description"
|
||||||
assert updated.ics_uid == "upsert-test"
|
assert updated.ics_uid == "upsert-test"
|
||||||
|
|
||||||
# Verify only one event exists
|
# Verify only one event exists
|
||||||
events = await calendar_events_controller.get_by_room(room.id)
|
events = await calendar_events_controller.get_by_room(session, room.id)
|
||||||
assert len(events) == 1
|
assert len(events) == 1
|
||||||
assert events[0].title == "Updated Title"
|
assert events[0].title == "Updated Title"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_calendar_event_soft_delete():
|
async def test_calendar_event_soft_delete():
|
||||||
"""Test soft deleting events no longer in calendar."""
|
"""Test soft deleting events no longer in calendar."""
|
||||||
# Create room
|
session_factory = get_session_factory()
|
||||||
room = await rooms_controller.add(
|
async with session_factory() as session:
|
||||||
name="delete-room",
|
# Create room
|
||||||
user_id="test-user",
|
room = await rooms_controller.add(
|
||||||
zulip_auto_post=False,
|
session,
|
||||||
zulip_stream="",
|
name="delete-room",
|
||||||
zulip_topic="",
|
user_id="test-user",
|
||||||
is_locked=False,
|
zulip_auto_post=False,
|
||||||
room_mode="normal",
|
zulip_stream="",
|
||||||
recording_type="cloud",
|
zulip_topic="",
|
||||||
recording_trigger="automatic-2nd-participant",
|
is_locked=False,
|
||||||
is_shared=False,
|
room_mode="normal",
|
||||||
)
|
recording_type="cloud",
|
||||||
|
recording_trigger="automatic-2nd-participant",
|
||||||
now = datetime.now(timezone.utc)
|
is_shared=False,
|
||||||
|
|
||||||
# Create multiple events
|
|
||||||
for i in range(4):
|
|
||||||
event = CalendarEvent(
|
|
||||||
room_id=room.id,
|
|
||||||
ics_uid=f"event-{i}",
|
|
||||||
title=f"Meeting {i}",
|
|
||||||
start_time=now + timedelta(hours=i),
|
|
||||||
end_time=now + timedelta(hours=i + 1),
|
|
||||||
)
|
)
|
||||||
await calendar_events_controller.upsert(event)
|
|
||||||
|
|
||||||
# Soft delete events not in current list
|
now = datetime.now(timezone.utc)
|
||||||
current_ids = ["event-0", "event-2"] # Keep events 0 and 2
|
|
||||||
deleted_count = await calendar_events_controller.soft_delete_missing(
|
|
||||||
room.id, current_ids
|
|
||||||
)
|
|
||||||
|
|
||||||
assert deleted_count == 2 # Should delete events 1 and 3
|
# Create multiple events
|
||||||
|
for i in range(4):
|
||||||
|
event = CalendarEvent(
|
||||||
|
room_id=room.id,
|
||||||
|
ics_uid=f"event-{i}",
|
||||||
|
title=f"Meeting {i}",
|
||||||
|
start_time=now + timedelta(hours=i),
|
||||||
|
end_time=now + timedelta(hours=i + 1),
|
||||||
|
)
|
||||||
|
await calendar_events_controller.upsert(session, event)
|
||||||
|
|
||||||
# Get non-deleted events
|
# Soft delete events not in current list
|
||||||
events = await calendar_events_controller.get_by_room(
|
current_ids = ["event-0", "event-2"] # Keep events 0 and 2
|
||||||
room.id, include_deleted=False
|
deleted_count = await calendar_events_controller.soft_delete_missing(
|
||||||
)
|
session, room.id, current_ids
|
||||||
assert len(events) == 2
|
)
|
||||||
assert {e.ics_uid for e in events} == {"event-0", "event-2"}
|
|
||||||
|
|
||||||
# Get all events including deleted
|
assert deleted_count == 2 # Should delete events 1 and 3
|
||||||
all_events = await calendar_events_controller.get_by_room(
|
|
||||||
room.id, include_deleted=True
|
# Get non-deleted events
|
||||||
)
|
events = await calendar_events_controller.get_by_room(
|
||||||
assert len(all_events) == 4
|
session, room.id, include_deleted=False
|
||||||
|
)
|
||||||
|
assert len(events) == 2
|
||||||
|
assert {e.ics_uid for e in events} == {"event-0", "event-2"}
|
||||||
|
|
||||||
|
# Get all events including deleted
|
||||||
|
all_events = await calendar_events_controller.get_by_room(
|
||||||
|
session, room.id, include_deleted=True
|
||||||
|
)
|
||||||
|
assert len(all_events) == 4
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_calendar_event_past_events_not_deleted():
|
async def test_calendar_event_past_events_not_deleted():
|
||||||
"""Test that past events are not soft deleted."""
|
"""Test that past events are not soft deleted."""
|
||||||
# Create room
|
session_factory = get_session_factory()
|
||||||
room = await rooms_controller.add(
|
async with session_factory() as session:
|
||||||
name="past-events-room",
|
# Create room
|
||||||
user_id="test-user",
|
room = await rooms_controller.add(
|
||||||
zulip_auto_post=False,
|
session,
|
||||||
zulip_stream="",
|
name="past-events-room",
|
||||||
zulip_topic="",
|
user_id="test-user",
|
||||||
is_locked=False,
|
zulip_auto_post=False,
|
||||||
room_mode="normal",
|
zulip_stream="",
|
||||||
recording_type="cloud",
|
zulip_topic="",
|
||||||
recording_trigger="automatic-2nd-participant",
|
is_locked=False,
|
||||||
is_shared=False,
|
room_mode="normal",
|
||||||
)
|
recording_type="cloud",
|
||||||
|
recording_trigger="automatic-2nd-participant",
|
||||||
|
is_shared=False,
|
||||||
|
)
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
# Create past event
|
# Create past event
|
||||||
past_event = CalendarEvent(
|
past_event = CalendarEvent(
|
||||||
room_id=room.id,
|
room_id=room.id,
|
||||||
ics_uid="past-event",
|
ics_uid="past-event",
|
||||||
title="Past Meeting",
|
title="Past Meeting",
|
||||||
start_time=now - timedelta(hours=2),
|
start_time=now - timedelta(hours=2),
|
||||||
end_time=now - timedelta(hours=1),
|
end_time=now - timedelta(hours=1),
|
||||||
)
|
)
|
||||||
await calendar_events_controller.upsert(past_event)
|
await calendar_events_controller.upsert(session, past_event)
|
||||||
|
|
||||||
# Create future event
|
# Create future event
|
||||||
future_event = CalendarEvent(
|
future_event = CalendarEvent(
|
||||||
room_id=room.id,
|
room_id=room.id,
|
||||||
ics_uid="future-event",
|
ics_uid="future-event",
|
||||||
title="Future Meeting",
|
title="Future Meeting",
|
||||||
start_time=now + timedelta(hours=1),
|
start_time=now + timedelta(hours=1),
|
||||||
end_time=now + timedelta(hours=2),
|
end_time=now + timedelta(hours=2),
|
||||||
)
|
)
|
||||||
await calendar_events_controller.upsert(future_event)
|
await calendar_events_controller.upsert(session, future_event)
|
||||||
|
|
||||||
# Try to soft delete all events (only future should be deleted)
|
# Try to soft delete all events (only future should be deleted)
|
||||||
deleted_count = await calendar_events_controller.soft_delete_missing(room.id, [])
|
deleted_count = await calendar_events_controller.soft_delete_missing(
|
||||||
|
session, room.id, []
|
||||||
|
)
|
||||||
|
|
||||||
assert deleted_count == 1 # Only future event deleted
|
assert deleted_count == 1 # Only future event deleted
|
||||||
|
|
||||||
# Verify past event still exists
|
# Verify past event still exists
|
||||||
events = await calendar_events_controller.get_by_room(
|
events = await calendar_events_controller.get_by_room(
|
||||||
room.id, include_deleted=False
|
session, room.id, include_deleted=False
|
||||||
)
|
)
|
||||||
assert len(events) == 1
|
assert len(events) == 1
|
||||||
assert events[0].ics_uid == "past-event"
|
assert events[0].ics_uid == "past-event"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_calendar_event_with_raw_ics_data():
|
async def test_calendar_event_with_raw_ics_data():
|
||||||
"""Test storing raw ICS data with calendar event."""
|
"""Test storing raw ICS data with calendar event."""
|
||||||
# Create room
|
session_factory = get_session_factory()
|
||||||
room = await rooms_controller.add(
|
async with session_factory() as session:
|
||||||
name="raw-ics-room",
|
# Create room
|
||||||
user_id="test-user",
|
room = await rooms_controller.add(
|
||||||
zulip_auto_post=False,
|
session,
|
||||||
zulip_stream="",
|
name="raw-ics-room",
|
||||||
zulip_topic="",
|
user_id="test-user",
|
||||||
is_locked=False,
|
zulip_auto_post=False,
|
||||||
room_mode="normal",
|
zulip_stream="",
|
||||||
recording_type="cloud",
|
zulip_topic="",
|
||||||
recording_trigger="automatic-2nd-participant",
|
is_locked=False,
|
||||||
is_shared=False,
|
room_mode="normal",
|
||||||
)
|
recording_type="cloud",
|
||||||
|
recording_trigger="automatic-2nd-participant",
|
||||||
|
is_shared=False,
|
||||||
|
)
|
||||||
|
|
||||||
raw_ics = """BEGIN:VEVENT
|
raw_ics = """BEGIN:VEVENT
|
||||||
UID:test-raw-123
|
UID:test-raw-123
|
||||||
SUMMARY:Test Event
|
SUMMARY:Test Event
|
||||||
DTSTART:20240101T100000Z
|
DTSTART:20240101T100000Z
|
||||||
DTEND:20240101T110000Z
|
DTEND:20240101T110000Z
|
||||||
END:VEVENT"""
|
END:VEVENT"""
|
||||||
|
|
||||||
event = CalendarEvent(
|
event = CalendarEvent(
|
||||||
room_id=room.id,
|
room_id=room.id,
|
||||||
ics_uid="test-raw-123",
|
ics_uid="test-raw-123",
|
||||||
title="Test Event",
|
title="Test Event",
|
||||||
start_time=datetime.now(timezone.utc),
|
start_time=datetime.now(timezone.utc),
|
||||||
end_time=datetime.now(timezone.utc) + timedelta(hours=1),
|
end_time=datetime.now(timezone.utc) + timedelta(hours=1),
|
||||||
ics_raw_data=raw_ics,
|
ics_raw_data=raw_ics,
|
||||||
)
|
)
|
||||||
|
|
||||||
saved = await calendar_events_controller.upsert(event)
|
saved = await calendar_events_controller.upsert(session, event)
|
||||||
|
|
||||||
assert saved.ics_raw_data == raw_ics
|
assert saved.ics_raw_data == raw_ics
|
||||||
|
|
||||||
# Retrieve and verify
|
# Retrieve and verify
|
||||||
retrieved = await calendar_events_controller.get_by_ics_uid(room.id, "test-raw-123")
|
retrieved = await calendar_events_controller.get_by_ics_uid(
|
||||||
assert retrieved is not None
|
session, room.id, "test-raw-123"
|
||||||
assert retrieved.ics_raw_data == raw_ics
|
)
|
||||||
|
assert retrieved is not None
|
||||||
|
assert retrieved.ics_raw_data == raw_ics
|
||||||
|
|||||||
Reference in New Issue
Block a user