From c90159d9da64bf52c39359d101d20289b1f5a43f Mon Sep 17 00:00:00 2001 From: Carsten Kvist Date: Sat, 11 Apr 2026 00:32:36 +0200 Subject: [PATCH] 2 --- .../__pycache__/alternatives.cpython-312.pyc | Bin 11132 -> 0 bytes app/routers/__pycache__/auth.cpython-312.pyc | Bin 2862 -> 0 bytes .../__pycache__/projects.cpython-312.pyc | Bin 13281 -> 0 bytes app/routers/__pycache__/songs.cpython-312.pyc | Bin 8360 -> 0 bytes app/routers/alternatives.py | 235 ------------------ app/routers/auth.py | 39 --- app/routers/projects.py | 190 -------------- app/routers/songs.py | 109 -------- app/schemas/__init__.py | 115 --------- .../__pycache__/__init__.cpython-312.pyc | Bin 4854 -> 0 bytes .../__pycache__/manager.cpython-312.pyc | Bin 4831 -> 0 bytes app/websocket/manager.py | 78 ------ 12 files changed, 766 deletions(-) delete mode 100644 app/routers/__pycache__/alternatives.cpython-312.pyc delete mode 100644 app/routers/__pycache__/auth.cpython-312.pyc delete mode 100644 app/routers/__pycache__/projects.cpython-312.pyc delete mode 100644 app/routers/__pycache__/songs.cpython-312.pyc delete mode 100644 app/routers/alternatives.py delete mode 100644 app/routers/auth.py delete mode 100644 app/routers/projects.py delete mode 100644 app/routers/songs.py delete mode 100644 app/schemas/__init__.py delete mode 100644 app/schemas/__pycache__/__init__.cpython-312.pyc delete mode 100644 app/websocket/__pycache__/manager.cpython-312.pyc delete mode 100644 app/websocket/manager.py diff --git a/app/routers/__pycache__/alternatives.cpython-312.pyc b/app/routers/__pycache__/alternatives.cpython-312.pyc deleted file mode 100644 index fbf4b0700d1e5174736ee789d7e44a2f54b43836..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11132 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 GmB!!p`)L>e diff --git a/app/routers/__pycache__/projects.cpython-312.pyc b/app/routers/__pycache__/projects.cpython-312.pyc deleted file mode 100644 index 404e032a49186e82357700670e3d7e4cdf31490f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13281 zcmd5?X>1$UouA=3;;GA$Bl)0h$rdd~@_i*fWJ$Jc%Ti*=al$5%Y0gL@#Y3KJqDW>PnXEfK5FO6sOC6DjePkh(c+i`aekh{NZIIDO8D z%jb%?eQxs35-yFD`N|^YzVb+guOd?EtBfr3EsIq7sv_0CYML@o9L-u;W`Mp`a?_y9 z$+w)faZj;!&dfSEN56%2UNZS=*sZLKbFgmCPTrJqPxUiw*(I~DmRla$qb@67vgO=L zuHq?ZxK*K2hS3b1^(L+KB2OjiQ@?@Gt>oQ?nl zV*$0rX)ou>H2N0ysRH_*Mdn|_RXp*`)&0WRTiDtH>T7^{yE^B5$$nev%eiuG{tUaK zV1#ur!qGfswZ_)4D{rlO!ku3@|J4Q5>lfL55A6N7p}w4}ESUeAf)Um)vj1yUWIQoy z))i3Qu*kgYo{(yNjsuhvU0*=CVUhjb@HbIzD4^_JWGB49N!8zqlNO%m#)2_6F0wCP zuKHVW6VhMKEh{+D#)2^#VGKRYZ{n)I6-PZyMyb+9v9V1BqikB_9Gbr+?r{{9b$HSY zySZS5r$@JVw>?0H@*3rmmgkP1jE%%OUbePz!(5aVWM@Zj?{n?12e{#QFc$UFvbBd3 z1f^;j;NpGkS!gW9T403dxoEs^MBsR@SvH-5Qno(F$3k2nE<1CT6I|phwA*u!J+bJ3 zY{J}Y)%vvZoIlQKkEe!Ne6;00=^ly8j-%1@!8n&A<|-i8!m@I8!E;aCA0H8NBnmqM znX!`QVP$%P%&0Z5LADI@Tz~L&*%bE=2(m5rrZ33K2KKCM;koz-9}VPm36Vx7$Wcl^ zPC+(BCFoF21CY15EUtZ_@=*vhV{yv&3R#NZQB;B&YM?0X`;u?=P_@dqAz=_4al6`s zrCEb2E8n6Mh8{Tbt_Rm(o?{hczlP$T;Ls4~=^u%*T-bG;@tmR_?*V)ka3gK6fV#2iYt%-bDbx@xCe z#Wg!6*DjISb30F=|`x5Nl6g2>r%|FxQ zrUVsIdlZy_1?}AP&3KADJxV1Ex){(-eyNj&@pLHbkH;Er8C5$Xq+udI9}P9;gjrYx zlvV33--Y5W2_tJ#XC8N}uUIo{DL|GfRHnYkmkD#IJpZKD)YT>QzcH%shhUw&W5I~l z{D`BJm+1oeWjP<@UV}rkaBMIh<7IQ~wJ4sUKY-$kV{>4Yt$4J^%@%f{mg0SVLPd>H$=QInO}wJeTVZVj&(J806Ue z7happkV(L3JlMw+zY;Utf4M~AsJ^qKyoWpog}&jDv*BQXaF%Qi0vm!1LiBkpd_8`+ z9y8=Megl4D#&?2ji4d78AZUR3An|MQwWH53z)Ir;!2B*~S_VJieaMpV)|+g^!E35y}RqKb?rUIbczWjcHDE6=XmUh*|^M4;!yHrUs?8Fzl{e-{X?2Q)D``MvB?Z*TFo$rkg>%xOh$&jztBo zFA`(9uxyBM3#6|VdJ=v-3pfGkOZ}foUpmCVSVd`pL|%%w42#T6<24oUA&s~4Ljhmw zjLp)wG^xH6U$;?Li~+*{6)?O4LUY`hFosI=wAI>G!#kxWYTN|x|2})DGviQ&I$FL= znADZ4DX55!t3NHwX}5bOB@&$MS2Si-4df%&xM# z6RD|MQsCR+J+hFHfhUX5;cg&T4vh3q!>H7VW z<3PsIns&5Gj&_g@4I4jVZZN-CDK#9(G#pJg92HOYW=@_?pFAy|35g9yrG}wQLoD48 zlN!$bzKoyQGP!1I(~mZZWqk4&WU3e$kesVB&ib^o{)2%JL)SweMz2R_ebV}alCve_ zY)?DeCFhak;VrKBe-7@|BWZ6{Z)ym0pSF7)qZpoDHPM7YMO7}|kXGGI8 ze|co4$_4r_e=6M~Am{v>tD9hTnZJU?}pF@M!!0&-VwkzVjF91SAHh~T451^D# z6N*bBHGTn=DJl-~UrDTi1^Y-(w+3n83)KEaNyW_v^bcv^9Xf=EQ+}KMOfC;M zqFe+Mv>~7r<>3ktBV5H3-wkgO^gbnyCb4&Z_4zM`-WR;*aMBp zBlk>}w+~-DJhAIiH&%~cJbLLkR*qdfHu2gQrfN_Y$u1(_$DGX!cq%&z+PZ< z@;%UL2mA!&JyOE`f^9W~oUn|dlo#UA!vyd&b@3aaPcLY22lsj)Ji>B)Q84{M(rG8bABP@b>%_T; zAX^0v46-=SpMXZb3o__R{EH&I;vt|L!i8Dc2;%db!q7EBoJI}4KKSBU_z8oMEqQ)b zHB+&xvDxjDF{x^IrfPq>YQI!<@DW9uS6{YGm?wlU8TTYTxnp|W@0e9tBQ)PH#`w8Q zC-N|O@EaJ>OY=vdSb%Rt8<9ULs1!k>0Nf`7Q))e3g{6Bq)Kc-moPmQWnhWLV6Zx;Z|dZ=wELal`@@a_QV zny|_*F0Of#+TLh3VZIlfm+m}nG3re9d{u4rU_~zACJU?GAiPD#H9K~(QDCR4@^evZ ztnHRv)llPBSRMLvMJb_0`H8TtEqV=)>Kx$?#Ifj!IQ3`nW9p~IAPoZAmE+~=B3@Ql zg6H@ZFw&Uq5I-_N_-PY4Ra9)#Xtly2!X+zvPKI0cFfS=mO+xl{lV~9 zxd$C^!o=W#dxTiT<8|rD9>qXMED-};W-ts8^+(`yY_nrV40JL^CqAN-@-mC7AWPiO zgOwmN-0NU_!(92D@P)uYhL#j2#hP*|6YNurdA=Vy5aH*64SXW0wpVgJBQnq2&-v!; z4_?hU*QT9oKiK}^zU%vD+kScCXD8C@S|sP8jI$%{?2w$th+j^G;>_L@`%&aZB-J-} zLR@o9avc|$_fq<~mqhnw$?eOy&!pXFB=^~5$Gp{@`@BnXb&E{*SI(N5(A@e@ydQh- z)cw}`xmSFd`@-3uJdCm!O~aj+x^iL}OhZTIbme<1-(5NF7B}pftHZD6YQ?H{(SBGo z9ag0_*+M^tp&>e`Kx07^0C%0}Q*hZppMDn)7Wf7YwxYDa*Vd6qDouB;G>pTPBmO@_{l!0+?y%imM-5mcj%M$kK6Bf#qw=Zd2got#dP_LQn@dAJZqsU zSO2%!=&;;(xFKFtyXw95@2;OU{?hd`*B#?;na>&VOdxY6nm!YinAjsDbj(slv*j<3 zD5#Q9*{8;xEzg?9)+rW14p>pd`Ao@ z?%YIRp^6f#yrO-hXxgaYpkkpHA;1l$u;BQS>xeq)g399VU4h516TZ+hmwPDwc_0b5 z8JZU@JJlcxnFp`qSd|w>2m6$6=Yh=I@DtWUw$KduMo{C*fy)O(Qw>?XzO(e!lFm}W zI=>m&_zss~aZS4Es#01{l`e39mVh>Ke;n~L*d3A}Fjyd4fpIh^MD3x3o|ET5qU(eS zp}-26hBZi=vcZ)HxAH2d#~wl-Eu`VNKCXcw9h%0jI{QM4c4hrL+fc@CvWB zX=2Dc025KYyoRt7FfV9(_zyr#2$KIfl;wF#v4~JaDsg>c5sl)b2Qxfoehf3T*@)2u zcYZtzH;iXTA?nNt0bcVZl$ch@;tqugr$WfV<$xb92Moi4fTkw#CG!461;lj;6Zgeq zTC5}q{eB2P;UQ#8vGaYq^UChayRWtVV1LH$N!vXiRDM`}y*kzQQRj`$^qSq0eNV=I zAZ?uskpm&cQEr{@-})%S9)f?aagW!z0^chlTP(cL7u z+cWO2w7cs!7c$*}bax=rJ(%tul)6Kbdnn^Rmv*0%+`@!ueq+;Q+ti7xCuUmiR@Tq9 zOfX-SRL{PJE1@)34*2|#ukKUjk+wai6<54dd6G=?iHTw9uIG}z}(X` zHVY9MHJ0%fJqRY;`!tCv==cf=>v@`Y7kmqGmV_0e0a{E8{}-a?Wh5+pT1*lA5KSPe zyHOaAfcIKtB>x^l%*b+(fYB|$ri{X-s+~!^3u0%kC}vqtP`!BP;`>cN0sfy8_K(6n zm>(ak_yDdqu<=lEco>?`K=TOR!JQfy4U;$ox#n;3yztq-ga7tJKR}lATqJg$Q#3_CAB$+Z;s-GLzrydY--rB- zsLO$9;w;>SCa;c&-U=a?ko9c!x5npk3hLFNneTD36RM+ps^m3Ke+Npvn!Y9czToQH08N z>?Ghwet}1k$>UM;rI?|VbK>;s0;>qmzvg;NTBnuXgNZ0y`p3 zZcNF&At)SwJTS1WIb#N0!(|@FPq^TW$pw|9@s`~@g^hfIO@2JCnHW+EcpDD;5lFUZg zDyAD#zEtNIC3}*Vti?n-XIirq6sgmsn5%w_PY+!-`UE|_AxmMIs#MC^T}pX7@R+n` zS5XG$?V5`<6Rk6*8DHA7IawpoTjo6*QkM7KA0H9hdZlMiJp_W!P7WaXXJ>mKVe!~V z8MbB(P|LcIMD4}eiJqC7xr$`1MDLs5xo@uj=dtw3mqcGkIyv-^GSd~)E3;Unj*#Nc z_DA@HboOTrP|KF8Q|V3}!&&T_Z{9h3{^pyX?-F|g(s33yz&Q9qwap-~+NIayhKt;%6k$RdGb3hE9 z#Fx)J!V;>*W7AEBoMSyHA}DCHcS z6}F4MS09lI(%hLfU|V)$ZnH$kT8Umazj4d#`kT$44v1Z^N{9P!T9z3yAIlt>j|IQ2 z!}(a0eJ1nSD$(2KcOSTI`IS4}`#tgHu+$rQNLlFSnVu{bDL8T{?!b{lfh4wP4N%LL z=k`T(KP}Ns^Lq~7uKLyL^r@G{S0d7>D3WNLfz?2fdW{r!SR~PiB$|;#W420Lz?#{* z**DT#59661`6hCVbnVPGJndRU4itzSD3DeMji+9#oH|+3+S&bg1jJy^{E1%i)c1bf zmkthRf&J)1hQ}2AFRB$7sf>0Z&p?rSgA})6tKmsJ+y4klB;5<7p_*;Lc>^rIYWwFqx*=x&Yj>I*62l3J9Y^GrrYaDtrL#Sxn1cg?C-D_OBA0;f Mz(YW8DG@pU3yn&86aWAK diff --git a/app/routers/__pycache__/songs.cpython-312.pyc b/app/routers/__pycache__/songs.cpython-312.pyc deleted file mode 100644 index 53965bdc28be27eba3f3333de00b284b5c7b73b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8360 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 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() diff --git a/app/routers/auth.py b/app/routers/auth.py deleted file mode 100644 index 0e16aaac..00000000 --- a/app/routers/auth.py +++ /dev/null @@ -1,39 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from fastapi.security import OAuth2PasswordRequestForm -from sqlalchemy.orm import Session -from app.core.database import get_db -from app.core.security import hash_password, verify_password, create_access_token -from app.models import User -from app.schemas import UserCreate, UserOut, Token - -router = APIRouter(prefix="/auth", tags=["auth"]) - - -@router.post("/register", response_model=UserOut, status_code=201) -def register(data: UserCreate, db: Session = Depends(get_db)): - if db.query(User).filter(User.username == data.username).first(): - raise HTTPException(400, "Brugernavnet er allerede i brug") - if db.query(User).filter(User.email == data.email).first(): - raise HTTPException(400, "E-mailen er allerede i brug") - - user = User( - username=data.username, - email=data.email, - password_hash=hash_password(data.password), - ) - db.add(user) - db.commit() - db.refresh(user) - return user - - -@router.post("/login", response_model=Token) -def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): - user = db.query(User).filter(User.username == form.username).first() - if not user or not verify_password(form.password, user.password_hash): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Forkert brugernavn eller kodeord", - ) - token = create_access_token({"sub": user.id}) - return {"access_token": token} diff --git a/app/routers/projects.py b/app/routers/projects.py deleted file mode 100644 index 1d341b0e..00000000 --- a/app/routers/projects.py +++ /dev/null @@ -1,190 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session -from app.core.database import get_db -from app.core.security import get_current_user -from app.models import User, Project, ProjectMember, ProjectSong, Song -from app.schemas import ( - ProjectCreate, ProjectUpdate, ProjectOut, - InviteMember, ProjectSongAdd, ProjectSongStatusUpdate, ProjectSongOut, -) - -router = APIRouter(prefix="/projects", tags=["projects"]) - - -def _get_project_or_404(project_id: str, db: Session) -> Project: - p = db.query(Project).filter(Project.id == project_id).first() - if not p: - raise HTTPException(404, "Projekt ikke fundet") - return p - - -def _assert_role(project: Project, user: User, db: Session, min_role: str = "viewer"): - roles = ["viewer", "editor", "owner"] - if project.owner_id == user.id: - return # ejer har altid adgang - member = db.query(ProjectMember).filter_by(project_id=project.id, user_id=user.id, status="accepted").first() - if not member: - if project.is_public and min_role == "viewer": - return - raise HTTPException(403, "Du har ikke adgang til dette projekt") - if roles.index(member.role) < roles.index(min_role): - raise HTTPException(403, "Din rolle giver ikke rettighed til dette") - - -# ── CRUD ────────────────────────────────────────────────────────────────────── - -@router.get("/", response_model=list[ProjectOut]) -def list_projects(db: Session = Depends(get_db), me: User = Depends(get_current_user)): - owned = db.query(Project).filter(Project.owner_id == me.id).all() - member_ids = [m.project_id for m in db.query(ProjectMember).filter_by(user_id=me.id, status="accepted").all()] - shared = db.query(Project).filter(Project.id.in_(member_ids)).all() - return list({p.id: p for p in owned + shared}.values()) - - -@router.post("/", response_model=ProjectOut, status_code=201) -def create_project(data: ProjectCreate, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - project = Project(owner_id=me.id, **data.model_dump()) - db.add(project) - db.commit() - db.refresh(project) - return project - - -@router.get("/{project_id}", response_model=ProjectOut) -def get_project(project_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - p = _get_project_or_404(project_id, db) - _assert_role(p, me, db, "viewer") - return p - - -@router.patch("/{project_id}", response_model=ProjectOut) -def update_project(project_id: str, data: ProjectUpdate, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - p = _get_project_or_404(project_id, db) - _assert_role(p, me, db, "editor") - for field, val in data.model_dump(exclude_none=True).items(): - setattr(p, field, val) - db.commit() - db.refresh(p) - return p - - -@router.delete("/{project_id}", status_code=204) -def delete_project(project_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - p = _get_project_or_404(project_id, db) - if p.owner_id != me.id: - raise HTTPException(403, "Kun ejeren kan slette projektet") - db.delete(p) - db.commit() - - -# ── Invitationer ────────────────────────────────────────────────────────────── - -@router.post("/{project_id}/invite", status_code=201) -def invite_member(project_id: str, data: InviteMember, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - p = _get_project_or_404(project_id, db) - if p.owner_id != me.id: - raise HTTPException(403, "Kun ejeren kan invitere") - - target = db.query(User).filter(User.username == data.username).first() - if not target: - raise HTTPException(404, f"Brugeren '{data.username}' findes ikke") - if target.id == me.id: - raise HTTPException(400, "Du kan ikke invitere dig selv") - - existing = db.query(ProjectMember).filter_by(project_id=project_id, user_id=target.id).first() - if existing: - raise HTTPException(400, "Brugeren er allerede inviteret eller medlem") - - member = ProjectMember(project_id=project_id, user_id=target.id, role=data.role, status="pending") - db.add(member) - db.commit() - return {"detail": f"{data.username} er inviteret som {data.role}"} - - -@router.get("/invitations/pending") -def get_pending_invitations(db: Session = Depends(get_db), me: User = Depends(get_current_user)): - invitations = db.query(ProjectMember).filter_by(user_id=me.id, status="pending").all() - return [ - {"invitation_id": inv.id, "project_id": inv.project_id, "role": inv.role, "invited_at": inv.invited_at} - for inv in invitations - ] - - -@router.post("/invitations/{invitation_id}/accept") -def accept_invitation(invitation_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - inv = db.query(ProjectMember).filter_by(id=invitation_id, user_id=me.id).first() - if not inv: - raise HTTPException(404, "Invitation ikke fundet") - inv.status = "accepted" - db.commit() - return {"detail": "Invitation accepteret"} - - -@router.delete("/invitations/{invitation_id}") -def decline_invitation(invitation_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - inv = db.query(ProjectMember).filter_by(id=invitation_id, user_id=me.id).first() - if not inv: - raise HTTPException(404, "Invitation ikke fundet") - db.delete(inv) - db.commit() - return {"detail": "Invitation afvist"} - - -# ── Danseliste (ProjectSongs) ───────────────────────────────────────────────── - -@router.get("/{project_id}/songs", response_model=list[ProjectSongOut]) -def list_project_songs(project_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - p = _get_project_or_404(project_id, db) - _assert_role(p, me, db, "viewer") - return p.project_songs - - -@router.post("/{project_id}/songs", response_model=ProjectSongOut, status_code=201) -def add_song_to_project(project_id: str, data: ProjectSongAdd, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - p = _get_project_or_404(project_id, db) - _assert_role(p, me, db, "editor") - - song = db.query(Song).filter(Song.id == data.song_id).first() - if not song: - raise HTTPException(404, "Sang ikke fundet") - - position = data.position - if position is None: - last = db.query(ProjectSong).filter_by(project_id=project_id).order_by(ProjectSong.position.desc()).first() - position = (last.position + 1) if last else 1 - - ps = ProjectSong(project_id=project_id, song_id=data.song_id, position=position) - db.add(ps) - db.commit() - db.refresh(ps) - return ps - - -@router.patch("/{project_id}/songs/{ps_id}/status", response_model=ProjectSongOut) -def update_song_status(project_id: str, ps_id: str, data: ProjectSongStatusUpdate, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - p = _get_project_or_404(project_id, db) - _assert_role(p, me, db, "editor") - - ps = db.query(ProjectSong).filter_by(id=ps_id, project_id=project_id).first() - if not ps: - raise HTTPException(404, "Sang ikke fundet i projektet") - - valid = {"pending", "playing", "played", "skipped"} - if data.status not in valid: - raise HTTPException(400, f"Ugyldig status. Vælg én af: {valid}") - - ps.status = data.status - db.commit() - db.refresh(ps) - return ps - - -@router.delete("/{project_id}/songs/{ps_id}", status_code=204) -def remove_song_from_project(project_id: str, ps_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - p = _get_project_or_404(project_id, db) - _assert_role(p, me, db, "editor") - ps = db.query(ProjectSong).filter_by(id=ps_id, project_id=project_id).first() - if not ps: - raise HTTPException(404, "Sang ikke fundet i projektet") - db.delete(ps) - db.commit() diff --git a/app/routers/songs.py b/app/routers/songs.py deleted file mode 100644 index 3e10b145..00000000 --- a/app/routers/songs.py +++ /dev/null @@ -1,109 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session -from app.core.database import get_db -from app.core.security import get_current_user -from app.models import User, Song, SongDance, DanceAlternative -from app.schemas import ( - SongCreate, SongOut, - SongDanceCreate, SongDanceOut, - DanceAlternativeCreate, DanceAlternativeOut, -) - -router = APIRouter(prefix="/songs", tags=["songs"]) - - -# ── Sange ───────────────────────────────────────────────────────────────────── - -@router.get("/", response_model=list[SongOut]) -def list_songs(db: Session = Depends(get_db), me: User = Depends(get_current_user)): - return db.query(Song).filter(Song.owner_id == me.id).all() - - -@router.post("/", response_model=SongOut, status_code=201) -def create_song(data: SongCreate, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - song = Song(owner_id=me.id, **data.model_dump()) - db.add(song) - db.commit() - db.refresh(song) - return song - - -@router.get("/{song_id}", response_model=SongOut) -def get_song(song_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - song = db.query(Song).filter(Song.id == song_id, Song.owner_id == me.id).first() - if not song: - raise HTTPException(404, "Sang ikke fundet") - return song - - -@router.delete("/{song_id}", status_code=204) -def delete_song(song_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - song = db.query(Song).filter(Song.id == song_id, Song.owner_id == me.id).first() - if not song: - raise HTTPException(404, "Sang ikke fundet") - db.delete(song) - db.commit() - - -# ── Danse på en sang ────────────────────────────────────────────────────────── - -@router.post("/{song_id}/dances", response_model=SongDanceOut, status_code=201) -def add_dance(song_id: str, data: SongDanceCreate, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - song = db.query(Song).filter(Song.id == song_id, Song.owner_id == me.id).first() - if not song: - raise HTTPException(404, "Sang ikke fundet") - dance = SongDance(song_id=song_id, **data.model_dump()) - db.add(dance) - db.commit() - db.refresh(dance) - return dance - - -@router.delete("/{song_id}/dances/{dance_id}", status_code=204) -def remove_dance(song_id: str, dance_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - song = db.query(Song).filter(Song.id == song_id, Song.owner_id == me.id).first() - if not song: - raise HTTPException(404, "Sang ikke fundet") - dance = db.query(SongDance).filter(SongDance.id == dance_id, SongDance.song_id == song_id).first() - if not dance: - raise HTTPException(404, "Dans ikke fundet") - db.delete(dance) - db.commit() - - -# ── Alternativ-danse ────────────────────────────────────────────────────────── - -@router.post("/{song_id}/dances/{dance_id}/alternatives", response_model=DanceAlternativeOut, status_code=201) -def add_alternative(song_id: str, dance_id: str, data: DanceAlternativeCreate, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - song = db.query(Song).filter(Song.id == song_id, Song.owner_id == me.id).first() - if not song: - raise HTTPException(404, "Sang ikke fundet") - dance = db.query(SongDance).filter(SongDance.id == dance_id, SongDance.song_id == song_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") - - alt = DanceAlternative(song_dance_id=dance_id, **data.model_dump()) - db.add(alt) - db.commit() - db.refresh(alt) - return alt - - -@router.get("/{song_id}/dances/{dance_id}/alternatives", response_model=list[DanceAlternativeOut]) -def list_alternatives(song_id: str, dance_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - dance = db.query(SongDance).filter(SongDance.id == dance_id, SongDance.song_id == song_id).first() - if not dance: - raise HTTPException(404, "Dans ikke fundet") - return dance.alternatives - - -@router.delete("/{song_id}/dances/{dance_id}/alternatives/{alt_id}", status_code=204) -def remove_alternative(song_id: str, dance_id: str, alt_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)): - alt = db.query(DanceAlternative).filter(DanceAlternative.id == alt_id, DanceAlternative.song_dance_id == dance_id).first() - if not alt: - raise HTTPException(404, "Alternativ ikke fundet") - db.delete(alt) - db.commit() diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py deleted file mode 100644 index 21227c3c..00000000 --- a/app/schemas/__init__.py +++ /dev/null @@ -1,115 +0,0 @@ -from __future__ import annotations -from datetime import datetime -from pydantic import BaseModel, EmailStr - - -# ── Auth ────────────────────────────────────────────────────────────────────── - -class UserCreate(BaseModel): - username: str - email: EmailStr - password: str - -class UserOut(BaseModel): - id: str - username: str - email: str - created_at: datetime - model_config = {"from_attributes": True} - -class Token(BaseModel): - access_token: str - token_type: str = "bearer" - - -# ── Project ─────────────────────────────────────────────────────────────────── - -class ProjectCreate(BaseModel): - name: str - description: str = "" - is_public: bool = False - -class ProjectUpdate(BaseModel): - name: str | None = None - description: str | None = None - is_public: bool | None = None - -class ProjectOut(BaseModel): - id: str - owner_id: str - name: str - description: str - is_public: bool - updated_at: datetime - model_config = {"from_attributes": True} - -class InviteMember(BaseModel): - username: str - role: str = "viewer" # editor | viewer - - -# ── Song ────────────────────────────────────────────────────────────────────── - -class SongCreate(BaseModel): - title: str - artist: str = "" - local_path: str = "" - bpm: int = 0 - duration_sec: int = 0 - -class SongOut(BaseModel): - id: str - owner_id: str - title: str - artist: str - local_path: str - bpm: int - duration_sec: int - synced_at: datetime - dances: list[SongDanceOut] = [] - model_config = {"from_attributes": True} - - -# ── Dance ───────────────────────────────────────────────────────────────────── - -class SongDanceCreate(BaseModel): - dance_name: str - dance_order: int = 1 - -class SongDanceOut(BaseModel): - id: str - dance_name: str - dance_order: int - model_config = {"from_attributes": True} - -class DanceAlternativeCreate(BaseModel): - alt_song_dance_id: str - note: str = "" - -class DanceAlternativeOut(BaseModel): - id: str - song_dance_id: str - alt_song_dance_id: str - note: str - model_config = {"from_attributes": True} - - -# ── ProjectSong ─────────────────────────────────────────────────────────────── - -class ProjectSongAdd(BaseModel): - song_id: str - position: int | None = None # None = tilføj sidst - -class ProjectSongStatusUpdate(BaseModel): - status: str # pending | playing | played | skipped - -class ProjectSongOut(BaseModel): - id: str - song_id: str - position: int - status: str - song: SongOut - model_config = {"from_attributes": True} - - -SongOut.model_rebuild() diff --git a/app/schemas/__pycache__/__init__.cpython-312.pyc b/app/schemas/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 1a72197cbfd7c13e390d5524c5ca5e4001b84fae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4854 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 liste af aktive forbindelser - self.rooms: dict[str, list[WebSocket]] = {} - - async def connect(self, project_id: str, ws: WebSocket): - await ws.accept() - self.rooms.setdefault(project_id, []).append(ws) - - def disconnect(self, project_id: str, ws: WebSocket): - if project_id in self.rooms: - self.rooms[project_id].discard(ws) if hasattr(self.rooms[project_id], 'discard') else None - try: - self.rooms[project_id].remove(ws) - except ValueError: - pass - - async def broadcast(self, project_id: str, message: dict): - dead = [] - for ws in self.rooms.get(project_id, []): - try: - await ws.send_text(json.dumps(message)) - except Exception: - dead.append(ws) - for ws in dead: - self.disconnect(project_id, ws) - - -manager = ConnectionManager() - - -@router.websocket("/{project_id}") -async def project_live( - project_id: str, - websocket: WebSocket, - db: Session = Depends(get_db), -): - project = db.query(Project).filter(Project.id == project_id).first() - if not project: - await websocket.close(code=4004) - return - - await manager.connect(project_id, websocket) - - # Send nuværende tilstand med det samme ved opkobling - songs = db.query(ProjectSong).filter_by(project_id=project_id).order_by(ProjectSong.position).all() - await websocket.send_text(json.dumps({ - "event": "state", - "project_id": project_id, - "songs": [ - {"id": ps.id, "position": ps.position, "status": ps.status, "song_id": ps.song_id} - for ps in songs - ], - })) - - try: - while True: - await websocket.receive_text() # hold forbindelsen åben - except WebSocketDisconnect: - manager.disconnect(project_id, websocket) - - -async def notify_status_change(project_id: str, project_song_id: str, new_status: str): - """Kaldes fra projects-router når en sangs status ændres.""" - await manager.broadcast(project_id, { - "event": "status_update", - "project_song_id": project_song_id, - "status": new_status, - })