inital commit, very early alpha stage
This commit is contained in:
2
backend/app/db/__init__.py
Normal file
2
backend/app/db/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Database helpers."""
|
||||
|
||||
31
backend/app/db/base.py
Normal file
31
backend/app/db/base.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from collections.abc import Generator
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
connect_args = {"check_same_thread": False} if settings.database_url.startswith("sqlite") else {}
|
||||
engine = create_engine(settings.database_url, connect_args=connect_args, future=True)
|
||||
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=False, future=True)
|
||||
|
||||
|
||||
def get_db() -> Generator[Session, None, None]:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
import app.models # noqa: F401
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
320
backend/app/db/seed.py
Normal file
320
backend/app/db/seed.py
Normal file
@@ -0,0 +1,320 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user