Start
This commit is contained in:
235
linedance-api/app/routers/alternatives.py
Normal file
235
linedance-api/app/routers/alternatives.py
Normal file
@@ -0,0 +1,235 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user