from __future__ import annotations from datetime import timedelta from sqlalchemy import select from app.core.config import get_settings from app.core.security import hash_token, token_urlsafe, utc_now import app.db.base as db_base from app.db.base import Base, init_db from app.models import ( Announcement, ConnectionToken, Event, FileAsset, Group, HomeProfile, Member, MemberInvite, Message, MigrationState, Notification, NotificationPreference, Poll, PollOption, Task, Thread, ) DEMO_INVITE_TOKEN = "demo-fc-invite" DEMO_REMOTE_CONNECTION_CODE = "demo-remote-sync-code" def _add_member(db, group: Group, name: str, role: str = "member", profile: HomeProfile | None = None, status: str = "joined") -> Member: member = Member( group_id=group.id, home_profile_id=profile.id if profile else None, display_name=name, role=role, status=status, joined_at=utc_now() - timedelta(days=9) if status in {"joined", "verified"} else None, last_seen_at=utc_now() - timedelta(days=5), notification_enabled_at=utc_now() - timedelta(days=3) if name == "Anna Müller" else None, ) db.add(member) db.flush() return member def _add_file(db, group: Group, member: Member, filename: str, description: str, requires_ack: bool = False) -> None: db.add( FileAsset( group_id=group.id, uploaded_by_member_id=member.id, filename_original=filename, filename_stored=f"seed-{filename.replace(' ', '_')}", content_type="text/plain", size_bytes=len(description.encode("utf-8")), storage_path=f"seed://{filename}", description=description, requires_ack=requires_ack, ) ) def seed() -> None: settings = get_settings() Base.metadata.drop_all(bind=db_base.engine) init_db() db = db_base.SessionLocal() try: anna = HomeProfile(primary_display_name="Anna Müller", last_seen_at=utc_now() - timedelta(days=5)) db.add(anna) db.flush() groups = [ Group( server_origin=settings.server_origin, name="FC Kreuzberg U12 Parents", description="Planning, matches, drivers, files, and official team announcements.", visibility="private", legacy_channel_status="transition", transition_deadline=(utc_now() + timedelta(days=21)).date(), ), Group( server_origin=settings.server_origin, name="Class 4B Parents", description="School forms, parent evenings, votes, and classroom coordination.", visibility="private", legacy_channel_status="transition", transition_deadline=(utc_now() + timedelta(days=28)).date(), ), Group( server_origin=settings.server_origin, name="Tenant Association", description="Building updates, maintenance actions, meeting minutes, and votes.", visibility="private", legacy_channel_status="transition", transition_deadline=(utc_now() + timedelta(days=14)).date(), ), Group( server_origin=settings.server_origin, name="Food Bank Volunteers", description="Volunteer shifts, supply lists, files, and announcements.", visibility="private", legacy_channel_status="transition", transition_deadline=(utc_now() + timedelta(days=30)).date(), ), ] db.add_all(groups) db.flush() fc, school, tenants, food = groups anna_fc = _add_member(db, fc, "Anna Müller", "admin", anna) coach = _add_member(db, fc, "Coach Mark", "owner") lisa_fc = _add_member(db, fc, "Lisa Becker") samir_fc = _add_member(db, fc, "Samir Khan") _add_member(db, fc, "Priya N.", status="invited") anna_school = _add_member(db, school, "Anna Müller", "member", anna) lisa_school = _add_member(db, school, "Lisa Becker", "admin") samir_school = _add_member(db, school, "Samir Khan") anna_tenant = _add_member(db, tenants, "Anna Müller", "member", anna) tenant_admin = _add_member(db, tenants, "Tenant admin", "owner") _add_member(db, tenants, "Priya N.", "moderator") anna_food = _add_member(db, food, "Anna Müller", "member", anna) priya_food = _add_member(db, food, "Priya N.", "admin") _add_member(db, food, "Samir Khan") db.add( MemberInvite( group_id=fc.id, created_by_member_id=anna_fc.id, label="Parent open invite", scope="open_seat", permission_role="member", token_hash=hash_token(DEMO_INVITE_TOKEN), max_uses=100, ) ) now = utc_now() db.add_all( [ Announcement( group_id=fc.id, author_member_id=coach.id, title="Official move: match details live here", body="Schedules, RSVP, driver planning, and files are now handled in GroupHome.", priority="urgent", official=True, requires_ack=True, ), Announcement( group_id=school.id, author_member_id=lisa_school.id, title="School form due Friday", body="Please download the excursion form and return it by Friday morning.", official=True, requires_ack=False, ), Announcement( group_id=food.id, author_member_id=priya_food.id, title="Saturday shift checklist", body="Bring gloves if you have them. New shelf labels are attached in files.", official=True, ), ] ) db.add_all( [ Event( group_id=fc.id, created_by_member_id=coach.id, title="League match vs. Neukölln", description="Please RSVP and add a note if you can drive.", starts_at=now + timedelta(days=2, hours=3), ends_at=now + timedelta(days=2, hours=5), location_name="Willi-Boos-Sportplatz", location_address="Gneisenaustr. 36, Berlin", rsvp_required=True, ), Event( group_id=fc.id, created_by_member_id=coach.id, title="Training moved to Pitch 2", description="The usual pitch is closed for maintenance.", starts_at=now + timedelta(days=1, hours=2), ends_at=now + timedelta(days=1, hours=4), location_name="Pitch 2", rsvp_required=False, changed_at=now - timedelta(days=1), ), Event( group_id=school.id, created_by_member_id=lisa_school.id, title="Parent evening", description="Agenda: class trip, reading groups, and summer project.", starts_at=now + timedelta(days=5, hours=1), location_name="Classroom 4B", rsvp_required=True, ), Event( group_id=food.id, created_by_member_id=priya_food.id, title="Volunteer shift", description="Sorting and front desk support.", starts_at=now + timedelta(days=3, hours=4), location_name="Food Bank Hall", rsvp_required=True, ), ] ) db.add_all( [ Task( group_id=fc.id, created_by_member_id=coach.id, assigned_to_member_id=anna_fc.id, title="Confirm one more driver", description="We need one extra car for Saturday.", due_at=now + timedelta(days=1), ), Task( group_id=food.id, created_by_member_id=priya_food.id, assigned_to_member_id=anna_food.id, title="Bring label printer", description="Use it for shelf relabeling before the morning shift.", due_at=now + timedelta(days=2), ), ] ) poll = Poll( group_id=tenants.id, created_by_member_id=tenant_admin.id, title="Vote: courtyard repair appointment", description="Choose the appointment that works for your household.", closes_at=now + timedelta(days=4), status="open", ) db.add(poll) db.flush() db.add_all( [ PollOption(poll_id=poll.id, label="Tuesday morning", position=1), PollOption(poll_id=poll.id, label="Thursday afternoon", position=2), PollOption(poll_id=poll.id, label="Either is fine", position=3), ] ) _add_file(db, fc, coach, "Season schedule.txt", "Full U12 season schedule and match locations.", True) _add_file(db, fc, coach, "Emergency contacts.txt", "Emergency contacts and consent notes.", False) _add_file(db, tenants, tenant_admin, "Meeting minutes.txt", "Tenant association meeting notes.", False) _add_file(db, school, lisa_school, "School excursion form.txt", "Please print and return this form.", True) for group, creator, title, body in [ (fc, lisa_fc, "Snack coordination", "I can bring oranges. Who can bring water?"), (fc, samir_fc, "Carpool planning", "I have two seats from Kottbusser Tor."), (tenants, tenant_admin, "Maintenance issue", "Elevator inspection is scheduled next week."), (food, priya_food, "Volunteer supplies", "We are low on tape and shelf labels."), ]: thread = Thread(group_id=group.id, created_by_member_id=creator.id, title=title, kind="discussion") db.add(thread) db.flush() db.add(Message(thread_id=thread.id, author_member_id=creator.id, body=body)) db.add( Notification( home_profile_id=anna.id, member_id=anna_fc.id, title="Coach Mark mentioned you", body="Can you confirm the last driver for Saturday?", category="direct_mentions", ) ) for owner_member in [anna_fc, anna_school, anna_tenant, anna_food]: for category, delivery in [ ("direct_mentions", "immediate"), ("event_changes", "immediate"), ("urgent_announcements", "immediate"), ("tasks_assigned", "immediate"), ("discussions", "digest"), ("new_files", "digest"), ("general_chatter", "digest"), ("reactions", "muted"), ("off_topic", "muted"), ]: db.add(NotificationPreference(home_profile_id=anna.id, member_id=owner_member.id, category=category, delivery=delivery)) for group in groups: db.add(MigrationState(group_id=group.id)) db.add( ConnectionToken( created_by_member_id=anna_fc.id, label="Demo remote sync code", token_hash=hash_token(DEMO_REMOTE_CONNECTION_CODE), scopes_json=["sync:read"], ) ) db.commit() print("Seed complete.") print(f"Demo invite URL: {settings.frontend_origin}/join/{DEMO_INVITE_TOKEN}") print(f"Demo remote connection code: {DEMO_REMOTE_CONNECTION_CODE}") finally: db.close() if __name__ == "__main__": seed()