diff --git a/backend/app/db/redis.py b/backend/app/db/redis.py new file mode 100644 index 0000000..d20bf08 --- /dev/null +++ b/backend/app/db/redis.py @@ -0,0 +1,10 @@ +import redis +import os + +REDIS_HOST = os.environ.get("REDIS_HOST", "redis") +REDIS_PORT = int(os.environ.get("REDIS_PORT", "6379")) + + +async def create_redis() -> redis.Redis: + redis_connection = await redis.asyncio.Redis(host=REDIS_HOST, port=REDIS_PORT, db=0) + return redis_connection diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index 254bf63..df9e3f9 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -1,4 +1,9 @@ +from typing import Annotated +from fastapi import Depends +import redis + from .db.database import SessionLocal +from .db.redis import create_redis def get_db(): @@ -7,3 +12,19 @@ def get_db(): yield db finally: db.close() + + +async def get_redis(): + r = await create_redis() + try: + yield r + finally: + r.close() + + +async def get_pubsub(r: Annotated[redis.Redis, Depends(get_redis)]): + ps = r.pubsub() + try: + yield ps + finally: + ps.close() diff --git a/backend/app/main.py b/backend/app/main.py index 288d2a9..59e7f18 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,8 +1,10 @@ -from typing import Union +from typing import Union, Annotated from fastapi import FastAPI, Depends +import redis from .db import models from .db.database import SessionLocal, engine +from .db.redis import create_redis from .dependencies import get_db from .views.auth.api import router as auth_router @@ -17,6 +19,12 @@ app.include_router(auth_router) app.include_router(news_router) +@app.on_event("startup") +async def startup_event(): + r = await create_redis() + await r.flushall() + + @app.get("/") async def read_root(): return {"message": "OK"} diff --git a/backend/app/views/queue/api.py b/backend/app/views/queue/api.py index 419f8e3..a4bdcfc 100644 --- a/backend/app/views/queue/api.py +++ b/backend/app/views/queue/api.py @@ -53,3 +53,10 @@ async def join_queue( queue_user: Annotated[schemas.QueueUser, Depends(services.join_queue)] ) -> schemas.QueueUser: return queue_user + + +@router.post("/{queue_id}/listen") +async def listen_queue( + updated_queue: Annotated[schemas.QueueDetail, Depends(services.set_queue_listener)] +) -> schemas.QueueDetail: + return updated_queue diff --git a/backend/app/views/queue/schemas.py b/backend/app/views/queue/schemas.py index e4479d4..1da71c6 100644 --- a/backend/app/views/queue/schemas.py +++ b/backend/app/views/queue/schemas.py @@ -1,11 +1,23 @@ -from typing import Union +from typing import Union, List from pydantic import BaseModel from uuid import UUID +from ..auth import schemas as auth_schemas + + +class QueueUser(BaseModel): + id: UUID + position: int + passed: bool + user: auth_schemas.AnonUser + + class Config: + from_attributes = True class ParticipantInfo(BaseModel): total: int remaining: int + users_list: List[QueueUser] class Queue(BaseModel): @@ -30,12 +42,3 @@ class QueueInDb(Queue): class QueueDetail(Queue): id: UUID participants: ParticipantInfo - - -class QueueUser(BaseModel): - id: UUID - position: int - passed: bool - - class Config: - from_attributes = True diff --git a/backend/app/views/queue/services.py b/backend/app/views/queue/services.py index 0e01883..a51fbc8 100644 --- a/backend/app/views/queue/services.py +++ b/backend/app/views/queue/services.py @@ -2,8 +2,10 @@ from fastapi import Depends, HTTPException, status from typing import Annotated from sqlalchemy.orm import Session from uuid import UUID +import redis +import asyncio -from ...dependencies import get_db +from ...dependencies import get_db, get_pubsub from ...db import models from ..auth import services as auth_services @@ -50,6 +52,9 @@ def get_detailed_queue( participants=schemas.ParticipantInfo( total=q.users.count(), remaining=q.users.filter(models.QueueUser.passed == False).count(), + users_list=q.users.filter(models.QueueUser.passed == False).order_by( + models.QueueUser.position.asc() + ), ), ) raise HTTPException( @@ -82,3 +87,18 @@ def join_queue( status_code=status.HTTP_404_NOT_FOUND, detail="Not Found", ) + + +async def set_queue_listener( + queue_id: UUID, + db: Annotated[Session, Depends(get_db)], + ps: Annotated[redis.client.PubSub, Depends(get_pubsub)], +) -> schemas.QueueDetail: + await ps.subscribe(str(queue_id)) + async for m in ps.listen(): + print(m, flush=True) + if m.get("data", None) == b"updated": + print("UPDATED", flush=True) + break + new_queue = get_detailed_queue(queue_id=queue_id, db=db) + return new_queue diff --git a/backend/requirements.txt b/backend/requirements.txt index 7563861..88c65dc 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,4 +5,5 @@ sqlalchemy psycopg2-binary python-jose[cryptography] passlib[all] -captcha \ No newline at end of file +captcha +redis[hiredis] \ No newline at end of file diff --git a/dev.yml b/dev.yml index f793d30..67a2c3d 100644 --- a/dev.yml +++ b/dev.yml @@ -13,6 +13,8 @@ services: depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy frontend: build: context: frontend @@ -51,3 +53,13 @@ services: interval: 2s timeout: 2s retries: 5 + redis: + image: redis:7.2.4-alpine + restart: unless-stopped + ports: + - 6379 + healthcheck: + test: ["CMD", "redis-cli", "--raw", "incr", "ping"] + interval: 2s + timeout: 2s + retries: 5 diff --git a/env/backend/dev.env b/env/backend/dev.env index b9ede68..804212e 100644 --- a/env/backend/dev.env +++ b/env/backend/dev.env @@ -1,4 +1,6 @@ POSTGRES_USER=user POSTGRES_PASSWORD=password POSTGRES_DB=db -DEBUG=1 \ No newline at end of file +DEBUG=1 +REDIS_HOST=redis +REDIS_PORT=6379 \ No newline at end of file diff --git a/frontend/app/src/components/user/AnonUserCard.tsx b/frontend/app/src/components/user/AnonUserCard.tsx new file mode 100644 index 0000000..11212b0 --- /dev/null +++ b/frontend/app/src/components/user/AnonUserCard.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const UUIDToColor = (uuid: string): string => {}; + +const AnonUserCard = (): JSX.Element => { + return; +};