from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from sqlalchemy import func from pydantic import BaseModel from app.core.database import get_db from app.core.security import get_current_user from app.models import User, SongDance, DanceAlternative, DanceAlternativeRating router = APIRouter(prefix="/alternatives", tags=["alternatives"]) # Bayesiansk minimum — alternativer med færre ratings trækkes mod gennemsnittet BAYESIAN_MIN_VOTES = 5 # ── Schemas ─────────────────────────────────────────────────────────────────── class AlternativeCreate(BaseModel): song_dance_id: str # dans der foreslås alternativ TIL alt_song_dance_id: str # den alternative dans note: str = "" class AlternativeOut(BaseModel): id: str song_dance_id: str alt_song_dance_id: str alt_dance_name: str alt_song_title: str created_by_username: str note: str my_score: int | None # den indloggede brugers egen rating avg_score: float | None # simpelt gennemsnit (til visning) bayesian_score: float # bruges til sortering rating_count: int model_config = {"from_attributes": True} class RatingUpsert(BaseModel): score: int # 1-5 # ── Hjælpefunktion: genberegn bayesian score ───────────────────────────────── def _recalculate_bayesian(alternative: DanceAlternative, db: Session): """ Bayesiansk score: vægter gennemsnittet mod et globalt gennemsnit når der er få ratings, så nye alternativer ikke dominerer listen. Formel: (n × avg + m × global_avg) / (n + m) n = antal ratings på dette alternativ avg = gennemsnit for dette alternativ m = BAYESIAN_MIN_VOTES (tillid-konstant) global_avg = gennemsnit på tværs af ALLE ratings """ # Beregn stats for dette alternativ result = db.query( func.count(DanceAlternativeRating.id), func.avg(DanceAlternativeRating.score), ).filter(DanceAlternativeRating.alternative_id == alternative.id).one() n = result[0] or 0 avg = float(result[1]) if result[1] else 0.0 # Globalt gennemsnit på tværs af alle ratings global_avg_result = db.query(func.avg(DanceAlternativeRating.score)).scalar() global_avg = float(global_avg_result) if global_avg_result else 3.0 # 3.0 som neutral fallback m = BAYESIAN_MIN_VOTES bayesian = (n * avg + m * global_avg) / (n + m) if (n + m) > 0 else global_avg alternative.bayesian_score = round(bayesian, 4) db.flush() # ── Endpoints ───────────────────────────────────────────────────────────────── @router.post("/", status_code=201) def create_alternative( data: AlternativeCreate, db: Session = Depends(get_db), me: User = Depends(get_current_user), ): """Opret et nyt alternativ-dans forslag. Alle registrerede brugere kan bidrage.""" dance = db.query(SongDance).filter(SongDance.id == data.song_dance_id).first() if not dance: raise HTTPException(404, "Dans ikke fundet") alt_dance = db.query(SongDance).filter(SongDance.id == data.alt_song_dance_id).first() if not alt_dance: raise HTTPException(404, "Alternativ-dans ikke fundet") if data.song_dance_id == data.alt_song_dance_id: raise HTTPException(400, "En dans kan ikke være sit eget alternativ") # Undgå dubletter fra samme bruger existing = db.query(DanceAlternative).filter_by( song_dance_id=data.song_dance_id, alt_song_dance_id=data.alt_song_dance_id, created_by=me.id, ).first() if existing: raise HTTPException(400, "Du har allerede foreslået dette alternativ") alt = DanceAlternative( song_dance_id=data.song_dance_id, alt_song_dance_id=data.alt_song_dance_id, created_by=me.id, note=data.note, bayesian_score=3.0, # starter på globalt neutral ) db.add(alt) db.commit() db.refresh(alt) return {"id": alt.id, "detail": "Alternativ oprettet"} @router.get("/for-dance/{song_dance_id}", response_model=list[AlternativeOut]) def list_alternatives_for_dance( song_dance_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user), ): """ Hent alle alternativer til en given dans, sorteret efter bayesiansk score. Viser din egen rating og gennemsnittet. """ alternatives = ( db.query(DanceAlternative) .filter(DanceAlternative.song_dance_id == song_dance_id) .order_by(DanceAlternative.bayesian_score.desc()) .all() ) result = [] for alt in alternatives: # Din egen rating my_rating = db.query(DanceAlternativeRating).filter_by( alternative_id=alt.id, user_id=me.id ).first() # Aggregeret stats stats = db.query( func.count(DanceAlternativeRating.id), func.avg(DanceAlternativeRating.score), ).filter(DanceAlternativeRating.alternative_id == alt.id).one() result.append(AlternativeOut( id=alt.id, song_dance_id=alt.song_dance_id, alt_song_dance_id=alt.alt_song_dance_id, alt_dance_name=alt.alt_song_dance.dance_name, alt_song_title=alt.alt_song_dance.song.title, created_by_username=alt.creator.username, note=alt.note, my_score=my_rating.score if my_rating else None, avg_score=round(float(stats[1]), 1) if stats[1] else None, bayesian_score=alt.bayesian_score, rating_count=stats[0] or 0, )) return result @router.put("/{alternative_id}/rate") def rate_alternative( alternative_id: str, data: RatingUpsert, db: Session = Depends(get_db), me: User = Depends(get_current_user), ): """Sæt eller opdater din rating (1-5) på et alternativ.""" if not 1 <= data.score <= 5: raise HTTPException(400, "Score skal være mellem 1 og 5") alt = db.query(DanceAlternative).filter(DanceAlternative.id == alternative_id).first() if not alt: raise HTTPException(404, "Alternativ ikke fundet") # Upsert — opdater eksisterende rating eller opret ny existing = db.query(DanceAlternativeRating).filter_by( alternative_id=alternative_id, user_id=me.id ).first() if existing: existing.score = data.score else: db.add(DanceAlternativeRating( alternative_id=alternative_id, user_id=me.id, score=data.score, )) db.flush() _recalculate_bayesian(alt, db) db.commit() return { "detail": "Rating gemt", "my_score": data.score, "bayesian_score": alt.bayesian_score, } @router.delete("/{alternative_id}/rate", status_code=204) def remove_rating( alternative_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user), ): """Fjern din rating fra et alternativ.""" rating = db.query(DanceAlternativeRating).filter_by( alternative_id=alternative_id, user_id=me.id ).first() if not rating: raise HTTPException(404, "Du har ikke rated dette alternativ") alt = db.query(DanceAlternative).filter(DanceAlternative.id == alternative_id).first() db.delete(rating) db.flush() _recalculate_bayesian(alt, db) db.commit() @router.delete("/{alternative_id}", status_code=204) def delete_alternative( alternative_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user), ): """Slet et alternativ — kun den der oprettede det.""" alt = db.query(DanceAlternative).filter(DanceAlternative.id == alternative_id).first() if not alt: raise HTTPException(404, "Alternativ ikke fundet") if alt.created_by != me.id: raise HTTPException(403, "Du kan kun slette dine egne forslag") db.delete(alt) db.commit()