This commit is contained in:
Lilian 2023-02-26 11:35:37 +01:00 committed by Kilton937342
parent 90b48e710f
commit 9393c88f8e
20 changed files with 589 additions and 414 deletions

View File

@ -59,9 +59,15 @@ def change_room_status(room: Room, public: bool, db: Session):
return room return room
def delete_room_db(room: Room, db: Session):
db.delete(room)
db.commit()
return True
def get_member_from_user(user_id: int, room_id: int, db: Session): def get_member_from_user(user_id: int, room_id: int, db: Session):
member = db.exec(select(Member).where(Member.room_id == member = db.exec(select(Member).where(Member.room_id ==
room_id, Member.user_id == user_id)).first() room_id, Member.user_id == user_id)).first()
return member return member
@ -77,7 +83,7 @@ def get_member_from_token(token: str, room_id: int, db: Session):
def get_member_from_anonymous(anonymous_id: int, room_id: int, db: Session): def get_member_from_anonymous(anonymous_id: int, room_id: int, db: Session):
member = db.exec(select(Member).where(Member.room_id == member = db.exec(select(Member).where(Member.room_id ==
room_id, Member.anonymous_id == anonymous_id)).first() room_id, Member.anonymous_id == anonymous_id)).first()
return member return member
@ -109,7 +115,8 @@ def get_member_from_clientId(clientId: str, room_id: int, db: Session):
return member return member
def create_member(*, room: Room, user: User | None = None, anonymous: Anonymous | None = None, waiting: bool = False, db: Session): def create_member(*, room: Room, user: User | None = None, anonymous: Anonymous | None = None, waiting: bool = False,
db: Session):
member_id = generate_unique_code(Member, s=db) member_id = generate_unique_code(Member, s=db)
member = Member(room=room, user=user, anonymous=anonymous, waiting=waiting, member = Member(room=room, user=user, anonymous=anonymous, waiting=waiting,
id_code=member_id) id_code=member_id)
@ -154,7 +161,7 @@ def disconnect_member(member: Member, db: Session):
return member return member
def validate_username(username: str, room: Room, db: Session = Depends(get_session)): def validate_username(username: str, room: Room, db: Session = Depends(get_session)):
if len(username) > 20: if len(username) > 20:
return None return None
members = select(Member.anonymous_id).where( members = select(Member.anonymous_id).where(
@ -269,6 +276,8 @@ def refuse_waiter(member: Member, db: Session):
def leave_room(member: Member, db: Session): def leave_room(member: Member, db: Session):
#db.execute(delete(Challenger).where(col(Challenger.member_id) == member.id))
#db.execute(delete(TmpCorrection).where(col(TmpCorrection.member_id) == member.id))
db.delete(member) db.delete(member)
db.commit() db.commit()
return None return None
@ -525,7 +534,6 @@ def change_challengers_validation(p: Parcours, validation: int, db: Session):
def change_challenges_validation(p: Parcours, validation: int, db: Session): def change_challenges_validation(p: Parcours, validation: int, db: Session):
challenges = db.exec(select(Challenge).where( challenges = db.exec(select(Challenge).where(
Challenge.parcours_id == p.id_code)).all() Challenge.parcours_id == p.id_code)).all()
print('CHALLS', challenges) print('CHALLS', challenges)
@ -833,7 +841,7 @@ def check_admin(member: Member = Depends(get_member_dep)):
def get_parcours(parcours_id: str, room: Room = Depends(get_room), db: Session = Depends(get_session)): def get_parcours(parcours_id: str, room: Room = Depends(get_room), db: Session = Depends(get_session)):
room = db.exec(select(Parcours).where(Parcours.id_code == room = db.exec(select(Parcours).where(Parcours.id_code ==
parcours_id, Parcours.room_id == room.id)).first() parcours_id, Parcours.room_id == room.id)).first()
if room is None: if room is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Parcours introuvable") status_code=status.HTTP_404_NOT_FOUND, detail="Parcours introuvable")
@ -844,10 +852,12 @@ def get_exercices(parcours: Parcours = Depends(get_parcours), db: Session = Depe
exercices = db.exec(select(Exercice).where(col(Exercice.id_code).in_( exercices = db.exec(select(Exercice).where(col(Exercice.id_code).in_(
[e['exercice_id'] for e in parcours.exercices]))).all() [e['exercice_id'] for e in parcours.exercices]))).all()
return [{"exercice": e, "quantity": [q for q in parcours.exercices if q['exercice_id'] == e.id_code][0]['quantity']} for e in exercices] return [{"exercice": e, "quantity": [q for q in parcours.exercices if q['exercice_id'] == e.id_code][0]['quantity']}
for e in exercices]
def get_correction(correction_id: str, parcours_id: str, member: Member = Depends(get_member_dep), db: Session = Depends(get_session)): def get_correction(correction_id: str, parcours_id: str, member: Member = Depends(get_member_dep),
db: Session = Depends(get_session)):
tmpCorr = db.exec(select(TmpCorrection).where( tmpCorr = db.exec(select(TmpCorrection).where(
TmpCorrection.id_code == correction_id, TmpCorrection.parcours_id == parcours_id)).first() TmpCorrection.id_code == correction_id, TmpCorrection.parcours_id == parcours_id)).first()
if tmpCorr is None: if tmpCorr is None:

View File

@ -24,8 +24,8 @@ class Room(RoomBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
id_code: str = Field(index=True) id_code: str = Field(index=True)
members: List['Member'] = Relationship(back_populates="room") members: List['Member'] = Relationship(back_populates="room",sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
parcours: List['Parcours'] = Relationship(back_populates="room") parcours: List['Parcours'] = Relationship(back_populates="room",sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
class AnonymousBase(SQLModel): class AnonymousBase(SQLModel):
@ -58,7 +58,9 @@ class Member(SQLModel, table=True):
room_id: int = Field(foreign_key="room.id") room_id: int = Field(foreign_key="room.id")
room: Room = Relationship(back_populates='members') room: Room = Relationship(back_populates='members')
challengers: List["Challenger"] = Relationship(back_populates="member") challengers: List["Challenger"] = Relationship(back_populates="member", sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
corrections: List["TmpCorrection"] = Relationship(back_populates="member", sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
is_admin: bool = False is_admin: bool = False
@ -68,7 +70,6 @@ class Member(SQLModel, table=True):
waiter_code: Optional[str] = Field(default=None) waiter_code: Optional[str] = Field(default=None)
corrections: List['TmpCorrection'] = Relationship(back_populates="member")
class ExercicesCreate(SQLModel): class ExercicesCreate(SQLModel):
@ -101,7 +102,7 @@ class Parcours(SQLModel, table=True):
room_id: int = Field(foreign_key="room.id") room_id: int = Field(foreign_key="room.id")
room: Room = Relationship(back_populates='parcours') room: Room = Relationship(back_populates='parcours')
challengers: list[Challenger] = Relationship(back_populates="parcours") challengers: list[Challenger] = Relationship(back_populates="parcours", sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
name: str name: str
time: int time: int
@ -110,9 +111,9 @@ class Parcours(SQLModel, table=True):
max_mistakes: int max_mistakes: int
exercices: List[Exercices] = Field(sa_column=Column(JSON)) exercices: List[Exercices] = Field(sa_column=Column(JSON))
challenges: List["Challenge"] = Relationship(back_populates="parcours") challenges: List["Challenge"] = Relationship(back_populates="parcours", sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
corrections: List["TmpCorrection"] = Relationship( corrections: List["TmpCorrection"] = Relationship(
back_populates="parcours") back_populates="parcours", sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})

Binary file not shown.

View File

@ -8,7 +8,7 @@ from database.auth.crud import get_user_from_token
from database.room.crud import change_room_name, change_room_status, serialize_member, check_user_in_room, \ from database.room.crud import change_room_name, change_room_status, serialize_member, check_user_in_room, \
create_anonymous, create_member, get_member, get_member_from_token, get_member_from_reconnect_code, connect_member, \ create_anonymous, create_member, get_member, get_member_from_token, get_member_from_reconnect_code, connect_member, \
disconnect_member, get_waiter, accept_waiter, leave_room, refuse_waiter disconnect_member, get_waiter, accept_waiter, leave_room, refuse_waiter
from database.room.models import Room, Member, MemberRead, Waiter from database.room.models import Room, Member, MemberRead, Waiter, Challenger
from services.websocket import Consumer from services.websocket import Consumer
if TYPE_CHECKING: if TYPE_CHECKING:
@ -17,7 +17,7 @@ if TYPE_CHECKING:
class RoomConsumer(Consumer): class RoomConsumer(Consumer):
def __init__(self, ws: WebSocket, room: Room, manager: "RoomManager", db: Session): def __init__(self, ws: WebSocket, room: Room | None, manager: "RoomManager", db: Session):
self.room = room self.room = room
self.ws = ws self.ws = ws
self.manager = manager self.manager = manager
@ -25,14 +25,22 @@ class RoomConsumer(Consumer):
self.member = None self.member = None
self.banned = False self.banned = False
async def connect(self):
await self.ws.accept()
if self.room is None:
await self.send_error("Salle introuvable", code=404)
await self.ws.close()
return False
return True
# WS Utilities # WS Utilities
async def send(self, payload: Any | Callable): async def send(self, payload: Any | Callable):
if callable(payload): if callable(payload):
payload = payload(self.member) payload = payload(self.member)
return await super().send(payload) return await super().send(payload)
async def connect(self):
await self.ws.accept()
async def direct_send(self, type: str, payload: Any, code: int | None = None): async def direct_send(self, type: str, payload: Any, code: int | None = None):
sending = {'type': type, "data": payload, } sending = {'type': type, "data": payload, }
@ -243,12 +251,13 @@ class RoomConsumer(Consumer):
@Consumer.event('leave', conditions=[isMember]) @Consumer.event('leave', conditions=[isMember])
async def leave(self): async def leave(self):
print('LEAVED', self.member, isinstance(self.member, Member), isinstance(self.member, Challenger))
if self.member.is_admin is True: if self.member.is_admin is True:
await self.send_error("Vous ne pouvez pas quitter une salle dont vous êtes l'administrateur") await self.send_error("Vous ne pouvez pas quitter une salle dont vous êtes l'administrateur")
return return
member_obj = serialize_member(self.member) member_obj = serialize_member(self.member)
leave_room(self.member, self.db) leave_room(self.member, self.db)
self.member = None
await self.direct_send(type="successfully_leaved", payload={}) await self.direct_send(type="successfully_leaved", payload={})
await self.broadcast(type='leaved', payload={"member": member_obj}) await self.broadcast(type='leaved', payload={"member": member_obj})
self.member = None self.member = None
@ -294,13 +303,13 @@ class RoomConsumer(Consumer):
self.manager.remove(self.room.id, self) self.manager.remove(self.room.id, self)
return {"waiter_id": waiter_id} return {"waiter_id": waiter_id}
@Consumer.sending("banned", conditions=[isMember]) # @Consumer.sending("banned", conditions=[isMember])
async def banned(self): # def banned(self):
self.member = None # self.member = None
self.manager.remove(self.room.id_code, self) # self.manager.remove(self.room.id_code, self)
self.banned = True # self.banned = True
#await self.ws.close() # #await self.ws.close()
return {} # return {}
@Consumer.sending('ping', conditions=[isMember]) @Consumer.sending('ping', conditions=[isMember])
def ping(self): def ping(self):

View File

@ -8,30 +8,7 @@ from sqlmodel import Session, select
from database.auth.models import User from database.auth.models import User
from database.db import get_session from database.db import get_session
from database.exercices.models import Exercice from database.exercices.models import Exercice
from database.room.crud import serialize_parcours_short, change_correction, corrige_challenge, \ from database.room.crud import delete_room_db
create_parcours_db, delete_parcours_db, create_room_db, get_member_dep, check_room, serialize_room, \
update_parcours_db, get_parcours, get_room, check_admin, get_exercices, get_challenge, get_correction, \
create_tmp_correction, create_challenge, change_challenge, serialize_parcours, getTops, getAvgRank, getRank, \
getAvgTops, ChallengerFromChallenge, getMemberAvgRank, getMemberRank
from database.room.models import Challenge, ChallengeRead, Challenges, ParcoursReadUpdate, ChallengeInfo, Member, \
Parcours, ParcoursCreate, ParcoursRead, ParcoursReadShort, Room, RoomConnectionInfos, \
RoomCreate, RoomInfo, TmpCorrection, CorrigedData, CorrectionData
from generateur.generateur_main import generate_from_path, parseGeneratorOut
from routes.room.consumer import RoomConsumer
from routes.room.manager import RoomManager
from services.auth import get_current_user_optional
from services.io import add_fast_api_root
from services.misc import stripKeyDict
from typing import List, Optional
from fastapi import APIRouter, Depends, WebSocket, status, Query, Body
from fastapi.exceptions import HTTPException
from pydantic import BaseModel
from sqlmodel import Session, select
from database.auth.models import User
from database.db import get_session
from database.exercices.models import Exercice
from database.room.crud import serialize_parcours_short, change_correction, corrige_challenge, \ from database.room.crud import serialize_parcours_short, change_correction, corrige_challenge, \
create_parcours_db, delete_parcours_db, create_room_db, get_member_dep, check_room, serialize_room, \ create_parcours_db, delete_parcours_db, create_room_db, get_member_dep, check_room, serialize_room, \
update_parcours_db, get_parcours, get_room, check_admin, get_exercices, get_challenge, get_correction, \ update_parcours_db, get_parcours, get_room, check_admin, get_exercices, get_challenge, get_correction, \
@ -68,6 +45,14 @@ def get_room_route(room: Room = Depends(get_room), member: Member = Depends(get_
return serialize_room(room, member, db) return serialize_room(room, member, db)
@router.delete('/room/{room_id}', dependencies=[ Depends(check_admin) ])
async def delete_room(room: Room = Depends(get_room), m: RoomManager = Depends(get_manager),
db: Session = Depends(get_session)):
delete_room_db(room, db)
await m.broadcast({"type": "deleted"}, room.id_code)
return {"message": "ok"}
@router.post('/room/{room_id}/parcours', response_model=ParcoursRead) @router.post('/room/{room_id}/parcours', response_model=ParcoursRead)
async def create_parcours(*, parcours: ParcoursCreate, room_id: str, member: Member = Depends(check_admin), async def create_parcours(*, parcours: ParcoursCreate, room_id: str, member: Member = Depends(check_admin),
m: RoomManager = Depends(get_manager), db: Session = Depends(get_session)): m: RoomManager = Depends(get_manager), db: Session = Depends(get_session)):
@ -258,8 +243,7 @@ async def corrige(*, correction: List[CorrigedData] = Body(), challenge: Challen
@router.websocket('/ws/room/{room_id}') @router.websocket('/ws/room/{room_id}')
async def room_ws(ws: WebSocket, room: Room | None = Depends(check_room), db: Session = Depends(get_session), async def room_ws(ws: WebSocket, room: Room | None = Depends(check_room), db: Session = Depends(get_session),
m: RoomManager = Depends(get_manager)): m: RoomManager = Depends(get_manager)):
if room is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail='Room not found')
consumer = RoomConsumer(ws=ws, room=room, manager=m, db=db) consumer = RoomConsumer(ws=ws, room=room, manager=m, db=db)
await consumer.run() await consumer.run()

View File

@ -1,9 +1,10 @@
from typing import List, Callable, Any, Dict
from pydantic import validate_arguments, BaseModel
from fastapi.websockets import WebSocketDisconnect, WebSocket
from pydantic.error_wrappers import ValidationError
import inspect import inspect
from starlette.websockets import WebSocketState from typing import List, Callable, Any, Dict
from fastapi.websockets import WebSocketDisconnect, WebSocket
from pydantic import validate_arguments, BaseModel
from pydantic.error_wrappers import ValidationError
def make_event_decorator(eventsDict): def make_event_decorator(eventsDict):
def _(name: str | List, conditions: List[Callable | bool] = []): def _(name: str | List, conditions: List[Callable | bool] = []):
@ -62,7 +63,8 @@ class Consumer:
#self.events: Dict[str, Callable] = {} #self.events: Dict[str, Callable] = {}
async def connect(self): async def connect(self):
pass await self.ws.accept()
return True
async def validation_error_handler(self, e: ValidationError): async def validation_error_handler(self, e: ValidationError):
errors = e.errors() errors = e.errors()
@ -132,7 +134,9 @@ class Consumer:
pass pass
async def run(self): async def run(self):
await self.connect() accepted = await self.connect()
if accepted is False:
return
try: try:
while True: while True:
data = await self.ws.receive_json() data = await self.ws.receive_json()

View File

@ -1,17 +1,17 @@
import axios from 'axios'; import axios from "axios";
import { autoRefresh } from '../utils/utils'; import { autoRefresh } from "../utils/utils";
import {env} from '$env/dynamic/private'; import { env } from "$env/dynamic/public";
export const authInstance = axios.create({ export const authInstance = axios.create({
baseURL: `${env.API_BASE}`, baseURL: `${env.PUBLIC_API_BASE}`,
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
Accept: 'application/json', Accept: "application/json",
'Access-Control-Allow-Origin': '*', "Access-Control-Allow-Origin": "*"
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '', //'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
} }
}); });
authInstance.interceptors.request.use(autoRefresh, (error) => { authInstance.interceptors.request.use(autoRefresh, (error) => {
Promise.reject(error); Promise.reject(error);
}); });

View File

@ -1,10 +1,10 @@
import axios from 'axios'; import axios from 'axios';
import { parse, stringify } from 'qs' import { parse, stringify } from 'qs'
import { autoRefresh } from '../utils/utils'; import { autoRefresh } from '../utils/utils';
import {env} from '$env/dynamic/private'; import { env } from "$env/dynamic/public";
export const exoInstance = axios.create({ export const exoInstance = axios.create({
paramsSerializer:{encode:(params)=> {return parse(params, {arrayFormat:"brackets"})}, serialize: (p)=>{return stringify(p, {arrayFormat: "repeat"})}}, paramsSerializer:{encode:(params)=> {return parse(params, {arrayFormat:"brackets"})}, serialize: (p)=>{return stringify(p, {arrayFormat: "repeat"})}},
baseURL: `${env.API_BASE}`, baseURL: `${env.PUBLIC_API_BASE}`,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Accept: 'application/json', Accept: 'application/json',

View File

@ -1,20 +1,20 @@
import axios from 'axios'; import axios from "axios";
import { autoRefresh } from '../utils/utils'; import { autoRefresh } from "../utils/utils";
import {env} from '$env/dynamic/private'; import { env } from "$env/dynamic/public";
export const roomInstance = axios.create({ export const roomInstance = axios.create({
baseURL: `${env.API_BASE}/room`, baseURL: `${env.PUBLIC_API_BASE}room`,
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
Accept: 'application/json', Accept: "application/json",
'Access-Control-Allow-Origin': '*', "Access-Control-Allow-Origin": "*"
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '', //'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
} }
}); });
roomInstance.interceptors.request.use( roomInstance.interceptors.request.use(
autoRefresh, autoRefresh,
(error) => { (error) => {
Promise.reject(error); Promise.reject(error);
} }
); );

View File

@ -16,6 +16,7 @@
afterNavigate(() => { afterNavigate(() => {
open = false; open = false;
}); });
$: console.log("USERNAME", $username);
</script> </script>
<nav data-sveltekit-preload-data="hover" class:open> <nav data-sveltekit-preload-data="hover" class:open>
@ -37,7 +38,8 @@
<div class="icon"> <div class="icon">
<FaUser /> <FaUser />
</div> </div>
{$username}</div> {$username}
</div>
</NavLink> </NavLink>
<div class="icon signout" title="Se déconnecter" on:click={()=>{ <div class="icon signout" title="Se déconnecter" on:click={()=>{
logout() logout()

View File

@ -10,7 +10,7 @@
<ul> <ul>
{#each rooms as room} {#each rooms as room}
<li> <li>
<a href="/room/{room.id_code}">{room.name} ({room.admin ? "Administrateur" : "Member"})</a> <a href="/room/{room.id_code}">{room.name} ({room.admin ? "Administrateur" : "Membre"})</a>
</li> </li>
{/each} {/each}
</ul> </ul>

View File

@ -4,6 +4,10 @@
import FaLock from "svelte-icons/fa/FaLock.svelte"; import FaLock from "svelte-icons/fa/FaLock.svelte";
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import FaTimes from "svelte-icons/fa/FaTimes.svelte";
import FaCheck from "svelte-icons/fa/FaCheck.svelte";
import MdCheck from "svelte-icons/md/MdCheck.svelte";
import MdClose from "svelte-icons/md/MdClose.svelte";
const room: Writable<Room> = getContext("room"); const room: Writable<Room> = getContext("room");
const member: Writable<Member> = getContext("member"); const member: Writable<Member> = getContext("member");
@ -111,14 +115,20 @@
class="accept" class="accept"
on:click={() => { on:click={() => {
send('accept', { waiter_id: m.waiter_id }); send('accept', { waiter_id: m.waiter_id });
}}>Accept }}
title="Accepter"
>
<MdCheck />
</button </button
> >
<button <button
class="refuse" class="refuse"
on:click={() => { on:click={() => {
send('refuse', { waiter_id: m.waiter_id }); send('refuse', { waiter_id: m.waiter_id });
}}>Refuse }}
title="Refuser"
>
<MdClose />
</button </button
> >
</p> </p>
@ -171,6 +181,9 @@
font-size: 1em; font-size: 1em;
width: max-content; width: max-content;
display: flex;
align-items: center;
span { span {
color: grey; color: grey;
font-size: 0.9em; font-size: 0.9em;
@ -203,4 +216,27 @@
font-weight: 500; font-weight: 500;
font-size: 0.8em !important; font-size: 0.8em !important;
} }
.accept, .refuse {
border: none;
background-color: transparent;
cursor: pointer;
width: 30px;
height: 30px;
transition: .3s;
margin: 0;
&:hover {
transform: scale(1.1);
}
}
.accept {
margin-left: 5px;
color: $green;
}
.refuse {
color: $red;
}
</style> </style>

View File

@ -13,6 +13,8 @@
import { messages, handlers } from "../../store/ws"; import { messages, handlers } from "../../store/ws";
import Stats from "./Stats.svelte"; import Stats from "./Stats.svelte";
import ChallengesList from "./ChallengesList.svelte"; import ChallengesList from "./ChallengesList.svelte";
import { delParcours } from "../../requests/room.request.js";
import FaRegTrashAlt from "svelte-icons/fa/FaRegTrashAlt.svelte";
export let id_code: string; export let id_code: string;
@ -20,7 +22,7 @@
const member: Writable<Member> = getContext("member"); const member: Writable<Member> = getContext("member");
const { send } = getContext<{ send: Function }>("ws"); const { send } = getContext<{ send: Function }>("ws");
const {alert} = getContext<{alert: Function}>("alert");
const parcours: Writable<ParcoursRead | null> = getContext("parcours"); const parcours: Writable<ParcoursRead | null> = getContext("parcours");
let open = ""; let open = "";
@ -53,6 +55,27 @@
> >
<FaEdit /> <FaEdit />
</div> </div>
<div
class="icon back"
on:keydown={() => {}}
on:click|stopPropagation={() => {
alert({
title: 'Supprimer ?',
description: 'Voulez vous supprimer ce parcours ?',
validate: () => {
delParcours(
$room?.id_code,
$parcours.id_code,
!$member.isUser ? $member?.clientId : null
).then(()=>{
goto(`?${new URLSearchParams().toString()}`);
});
}
});
}}
>
<FaRegTrashAlt />
</div>
{/if} {/if}
<div <div
@ -187,6 +210,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
width: 20px; width: 20px;
height: 20px;
} }
@ -235,6 +259,7 @@
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
align-items: stretch; align-items: stretch;
padding: 3px 15px;
} }
.btn { .btn {
@ -269,4 +294,7 @@
.edit { .edit {
color: $green; color: $green;
} }
h1{
word-break: break-word;
}
</style> </style>

View File

@ -1,128 +1,150 @@
<script lang="ts"> <script lang="ts">
import { delParcours } from '../../requests/room.request'; import { getContext } from "svelte";
import { getContext } from 'svelte'; import type { Writable } from "svelte/store";
import FaRegTrashAlt from 'svelte-icons/fa/FaRegTrashAlt.svelte'; import type { Member, Room } from "../../types/room.type";
import type { Writable } from 'svelte/store'; import { goto } from "$app/navigation";
import type { Member, Room } from '../../types/room.type';
import { goto } from '$app/navigation'; const room: Writable<Room> = getContext("room");
import { page } from '$app/stores'; const member: Writable<Member> = getContext("member");
const room: Writable<Room> = getContext('room');
const member: Writable<Member> = getContext('member');
const { alert } = getContext<{ alert: Function }>('alert');
</script> </script>
<div class="parcours"> <div class="parcours">
<div class="head"> <div class="head">
<h2>Parcours</h2> <h2>Parcours</h2>
{#if $member.isAdmin} {#if $member.isAdmin}
<button <button
class="primary-btn" class="primary-btn"
on:click={() => { on:click={() => {
goto(`?${new URLSearchParams({ p: 'new' }).toString()}`); goto(`?${new URLSearchParams({ p: 'new' }).toString()}`);
}}>Nouveau</button }}>Nouveau
> </button
{/if} >
</div> {/if}
</div>
<div class="list"> <div class="list">
{#if $room.parcours.length == 0} {#if $room.parcours.length == 0}
<p class="empty">Aucun parcours pour le moment</p> <p class="empty">Aucun parcours pour le moment</p>
{/if} {/if}
{#each $room.parcours as p} {#each $room.parcours as p}
<div <div
on:click={() => { on:click={() => {
goto(`?${new URLSearchParams({ p: p.id_code }).toString()}`); goto(`?${new URLSearchParams({ p: p.id_code }).toString()}`);
}} }}
on:keydown={() => {}} on:keydown={() => {}}
> >
<p>{p.name}</p> <p class="parcours-name">{p.name}</p>
{#if p.best_note}
<p>Record : {p.best_note} fautes</p>
{:else}
Aucun essai effectué
{/if}
{#if p.validated}
<p data-testid="valid">Parcours validé</p>
{/if}
{#if $member.isAdmin}
<div <div class="stats">
class="icon delete" <p class="stat">
on:keydown={() => {}} {#if p.best_note}
on:click|stopPropagation={() => { <strong>Record :</strong> {p.best_note} faute{p.best_note > 1 ? "s" : ""}
alert({ {:else}
title: 'Supprimer ?', Aucun essai effectué
description: 'Voulez vous supprimer ce parcours ?', {/if}
validate: () => { </p>
delParcours( <p class="stat valid" data-testid="valid"
$room?.id_code, class:validated={p.validated}>{ p.validated ? "Parcours validé" : "Parcours non validé"}</p>
p.id_code,
!$member.isUser ? $member?.clientId : null </div>
);
}
}); </div>
}} {/each}
> </div>
<FaRegTrashAlt />
</div>
{/if}
</div>
{/each}
</div>
</div> </div>
<style lang="scss"> <style lang="scss">
.empty { .empty {
text-align: center; text-align: center;
font-style: italic; font-style: italic;
margin: 30px 0; margin: 30px 0;
} }
.parcours {
background-color: rgba($background, 0.4);
border: 1px solid $border;
padding: 20px; .parcours {
width: 100%; background-color: rgba($background, 0.4);
height: 100%; border: 1px solid $border;
display: flex; padding: 20px;
flex-direction: column; width: 100%;
border-radius: 5px; height: 100%;
} display: flex;
.head { flex-direction: column;
display: flex; border-radius: 5px;
align-items: center; }
justify-content: space-between;
border-bottom: 1px solid $border; .head {
padding: 10px 0; display: flex;
button { align-items: center;
width: max-content; justify-content: space-between;
} border-bottom: 1px solid $border;
} padding: 10px 0;
.icon {
width: 18px; button {
height: 18px; width: max-content;
color: $red; }
transition: 0.4s; }
opacity: 0;
&:hover { .icon {
transform: scale(1.1); width: 18px;
} height: 18px;
} color: $red;
.list { transition: 0.4s;
overflow: auto; opacity: 0;
> div {
padding: 30px 10px; &:hover {
border-bottom: 1px solid $border-light; transform: scale(1.1);
cursor: pointer; }
transition: 0.3s; }
display: flex;
align-items: center; .list {
justify-content: space-between; overflow: auto;
&:hover {
background-color: lighten($color: $background, $amount: 10); > div {
.icon { padding: 30px 10px;
opacity: 1; border-bottom: 1px solid $border-light;
} cursor: pointer;
} transition: 0.3s;
} display: flex;
} align-items: center;
justify-content: space-between;
&:hover {
background-color: lighten($color: $background, $amount: 10);
.icon {
opacity: 1;
}
}
}
}
.valid {
font-weight: 600;
color: $red;
}
.validated {
color: $green;
}
.stat{
text-align: justify;
height: 1em;
margin: 5px 0;
width: max-content;
&::after{
content: "";
display: inline-block;
width: 100%;
}
}
.parcours-name {
font-weight: 500;
margin: 0;
word-break: break-word;
}
</style> </style>

View File

@ -1,47 +1,51 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext } from "svelte";
import FaRegTrashAlt from 'svelte-icons/fa/FaRegTrashAlt.svelte'; import FaRegTrashAlt from "svelte-icons/fa/FaRegTrashAlt.svelte";
import FaUndo from 'svelte-icons/fa/FaUndo.svelte'; import FaUndo from "svelte-icons/fa/FaUndo.svelte";
import FaTimes from 'svelte-icons/fa/FaTimes.svelte'; import FaTimes from "svelte-icons/fa/FaTimes.svelte";
import { goto } from '$app/navigation'; import FaSignOutAlt from "svelte-icons/fa/FaSignOutAlt.svelte";
import type { Writable } from 'svelte/store';
import type { Room, Member } from '../../types/room.type'; import { goto } from "$app/navigation";
import type ReconnectingWebSocket from 'reconnecting-websocket'; import type { Writable } from "svelte/store";
const room: Writable<Room> = getContext('room'); import type { Member, Room } from "../../types/room.type";
const member: Writable<Member>= getContext('member'); import type ReconnectingWebSocket from "reconnecting-websocket";
const { send, ws } = getContext<{send: Function, ws: ReconnectingWebSocket}>('ws'); import { deleteRoom } from "../../requests/room.request.js";
let editing = false;
let name = $room.name; const room: Writable<Room> = getContext("room");
let r: HTMLInputElement; const member: Writable<Member> = getContext("member");
const { send, ws } = getContext<{ send: Function, ws: ReconnectingWebSocket }>("ws");
const { alert } = getContext<{ alert: Function }>("alert");
let editing = false;
let name = $room.name;
let r: HTMLInputElement;
</script> </script>
<div class="head"> <div class="head">
<div class="title"> <div class="title">
{#if !editing} {#if !editing}
<h1 <h1
on:dblclick={() => { on:dblclick={() => {
if(!$member.isAdmin) return if(!$member.isAdmin) return
editing = true; editing = true;
name = $room.name; name = $room.name;
r.focus(); r.focus();
console.log("OPENED") console.log("OPENED")
}} }}
> >
{$room.name}<span on:dblclick|stopPropagation>#{$room.id_code}</span> {$room.name}<span on:dblclick|stopPropagation>#{$room.id_code}</span>
</h1> </h1>
{/if} {/if}
<input <input
type="text" bind:this={r}
class="input" bind:value={name}
class:hide={!editing} class="input"
on:focusout={() => { class:hide={!editing}
on:focusout={() => {
editing = false; editing = false;
}} }}
bind:value={name} on:keydown={(e) => {
bind:this={r}
on:keydown={(e) => {
if (e.key == 'Escape') { if (e.key == 'Escape') {
editing = false; editing = false;
} else if (e.key == 'Enter') { } else if (e.key == 'Enter') {
@ -49,102 +53,134 @@
editing = false; editing = false;
} }
}} }}
/> type="text"
</div> />
</div>
<div class="icons"> <div class="icons">
{#if $member.isAdmin} {#if $member.isAdmin}
<div class="icon trash" data-testid="delete"><FaRegTrashAlt /></div> <div class="icon trash" data-testid="delete" on:click={()=>{
{/if} alert({title:"Voulez vous vraiment supprimer la salle ?",
description:"L'intégralité des données de la salle sera supprimée et vous ne pourrez plus y accéder",
validate: ()=>{
deleteRoom($room.id_code, !$member.isUser ? $member.clientId: null)
},
validateButton: "Supprimer la salle"})
<div //goto('/room/join')
class="icon refresh" }}>
on:click={() => { <FaRegTrashAlt />
console.log(ws) </div>
{:else}
<div class="icon trash" data-testid="leave" on:click={()=>{
alert({title:"Voulez vous vraiment quitter la salle ?",
description:"Vous perdrez l'intégralité de vos résultats et ne pourrez plus accéder à cette salle sans y avoir été accepté à nouveau",
validate: ()=>{
send('leave')
},
validateButton: "Quitter la salle"})
//goto('/room/join')
}}
on:keypress={()=>{}}
>
<FaSignOutAlt />
</div>
{/if}
<div
class="icon refresh"
data-testid="refresh"
on:click={() => {
ws.reconnect(); ws.reconnect();
}} }}
on:keydown={() => {}} on:keydown={() => {}}
data-testid="refresh" >
> <FaUndo />
<FaUndo /> </div>
</div> <div
<div class="icon trash"
data-testid="leave" data-testid="leave"
class="icon trash" on:click={() => {
on:click={() => {
ws.close(); ws.close();
goto('/room/join') goto('/room/join')
}} }}
on:keydown={() => {}} on:keydown={() => {}}
> >
<FaTimes /> <FaTimes />
</div> </div>
</div> </div>
</div> </div>
<style lang="scss"> <style lang="scss">
.hide { .hide {
width: 0; width: 0;
height: 0; height: 0;
padding: 0; padding: 0;
border: none; border: none;
opacity: 0; opacity: 0;
} }
.title {
max-width: 50%;
margin: 10px 0;
font-size: 2em;
padding: 0 10px;
// height: 50px;
input {
font-size: inherit;
font-weight: 700;
transition: all 0s;
transition: border 0.3s;
padding-left: 0;
padding-top: 0;
}
h1 {
margin: 0;
font-weight: 700;
font-size: inherit;
span {
font-size: 0.5em;
color: grey;
}
}
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
}
.icon { .title {
width: 20px; max-width: 50%;
height: 20px; margin: 10px 0;
transition: 0.2s; font-size: 2em;
cursor: pointer; padding: 0 10px;
display: flex; // height: 50px;
&:hover { input {
transform: scale(1.1); font-size: inherit;
} font-weight: 700;
} transition: all 0s;
transition: border 0.3s;
padding-left: 0;
padding-top: 0;
}
.trash { h1 {
color: $red; margin: 0;
} font-weight: 700;
font-size: inherit;
.refresh { span {
color: $contrast; font-size: 0.5em;
&:hover { color: grey;
transform: rotate(-360deg); }
} }
} }
.icons { .head {
display: flex; display: flex;
gap: 20px; align-items: center;
align-items: center; justify-content: space-between;
} }
.icon {
width: 20px;
height: 20px;
transition: 0.2s;
cursor: pointer;
display: flex;
&:hover {
transform: scale(1.1);
}
}
.trash {
color: $red;
}
.refresh {
color: $contrast;
&:hover {
transform: rotate(-360deg);
}
}
.icons {
display: flex;
gap: 20px;
align-items: center;
}
</style> </style>

View File

@ -1,118 +1,137 @@
<script lang="ts"> <script lang="ts">
import type { ParcoursRead, Member } from '../../types/room.type'; import type { ParcoursRead, Member } from "../../types/room.type";
import { parseTimer, statsCalculator } from '../../utils/utils'; import { parseTimer, statsCalculator } from "../../utils/utils";
import { getContext } from 'svelte'; import { getContext } from "svelte";
import type { Writable } from 'svelte/store'; import type { Writable } from "svelte/store";
import Classement from './Classement.svelte'; import Classement from "./Classement.svelte";
const parcours: Writable<ParcoursRead | null> = getContext('parcours'); const parcours: Writable<ParcoursRead | null> = getContext("parcours");
const member: Writable<Member | null> = getContext('member'); const member: Writable<Member | null> = getContext("member");
$: stats = $: stats =
$parcours != null && $member != null && !!$parcours.challenges[$member.id_code] $parcours != null && $member != null && !!$parcours.challenges[$member.id_code]
? statsCalculator($parcours.challenges[$member.id_code].challenges.map((c) => c.mistakes)) ? statsCalculator($parcours.challenges[$member.id_code].challenges.map((c) => c.mistakes))
: null; : null;
</script> </script>
{#if $parcours != null} {#if $parcours != null}
<div class="stats"> <div class="stats">
<h1 class:validated={$parcours.validated}> <h1 class:validated={$parcours.validated}>
{$parcours.validated ? 'Parcours validé !' : 'Parcours non validé !'} {$parcours.validated ? 'Parcours validé !' : 'Parcours non validé !'}
</h1> </h1>
<div class="statistics"> <div class="statistics">
<p data-testid="avg"> <p data-testid="avg">
<strong>Moyenne</strong> <strong>Moyenne</strong>
{stats?.avg.toFixed(2) !== null && stats?.avg.toFixed(2) !== undefined {stats?.avg.toFixed(2) !== null && stats?.avg.toFixed(2) !== undefined
? `${stats?.avg.toFixed(2)} fautes` ? `${stats?.avg.toFixed(2)} fautes`
: '-'} : '-'}
</p> </p>
<p data-testid="max"> <p data-testid="max">
<strong>Pire</strong> <strong>Pire</strong>
{stats?.max !== null && stats?.max !== undefined ? `${stats?.max} fautes` : '-'} {stats?.max !== null && stats?.max !== undefined ? `${stats?.max} fautes` : '-'}
</p> </p>
<p data-testid="min"> <p data-testid="min">
<strong>Meilleur</strong> <strong>Meilleur</strong>
{stats?.min !== null && stats?.min !== undefined ? `${stats?.min} fautes` : '-'} {stats?.min !== null && stats?.min !== undefined ? `${stats?.min} fautes` : '-'}
</p> </p>
</div> </div>
<div class="classement"> <div class="classement">
<h2>Classements</h2> <h2>Classements</h2>
<div class="ranks"> <div class="ranks">
<div> <div>
<h3>Top élèves</h3> <h3>Top élèves</h3>
<Classement <Classement
tops={$parcours.ranking.map((t) => { tops={$parcours.ranking.map((t) => {
return { name: t.name, value: `Moyenne : ${t.avg.toFixed(2)} fautes` }; return { name: t.name, value: `Moyenne : ${t.avg.toFixed(2)} fautes` };
})} })}
rank={$parcours.memberRank != null && $parcours.memberRank > 3 && $member != null rank={$parcours.memberRank != null && $parcours.memberRank > 3 && $member != null
? { ? {
name: $member.username, name: $member.username,
rank: $parcours.memberRank, rank: $parcours.memberRank,
value: `Moyenne : ${stats?.avg.toFixed(2)} fautes` value: `Moyenne : ${stats?.avg.toFixed(2)} fautes`
} }
: null} : null}
/> />
</div> </div>
<div> <div>
<h3 style:text-align="right">Top essais</h3> <h3 style:text-align="right">Top essais</h3>
<Classement <Classement
tops={$parcours.tops.map((t) => { tops={$parcours.tops.map((t) => {
return { return {
name: t.challenger.name, name: t.challenger.name,
value: `${t.mistakes} fautes en ${parseTimer(t.time)}` value: `${t.mistakes} fautes en ${parseTimer(t.time)}`
}; };
})} })}
rank={$parcours.rank != null && $parcours.rank > 3 && $member != null rank={$parcours.rank != null && $parcours.rank > 3 && $member != null
? { ? {
name: $member.username, name: $member.username,
rank: $parcours.rank, rank: $parcours.rank,
value: `${$parcours.pb.mistakes} fautes en ${parseTimer($parcours.pb.time)}` value: `${$parcours.pb.mistakes} fautes en ${parseTimer($parcours.pb.time)}`
} }
: null} : null}
/> />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/if} {/if}
<style lang="scss"> <style lang="scss">
.stats { @import "../../mixins.scss";
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
gap: 20px;
}
.statistics {
display: flex;
gap: 20px;
p {
display: flex;
flex-direction: column;
align-items: center;
}
}
h1 { .stats {
font-size: 3em; display: flex;
color: $red; flex-direction: column;
text-align: center; align-items: center;
} width: 100%;
.validated { gap: 20px;
color: $green; }
}
.ranks { .statistics {
display: flex; display: flex;
justify-content: space-evenly; gap: 20px;
}
.classement { p {
width: 100%; display: flex;
h2{ flex-direction: column;
text-align: center; align-items: center;
}
} }
}
h1 {
font-size: 3em;
color: $red;
text-align: center;
}
.validated {
color: $green;
}
.ranks {
display: flex;
justify-content: space-evenly;
@include down(750) {
flex-direction: column;
align-items: center;
gap: 10px;
& > div {
display: flex;
flex-direction: column;
align-items: center;
gap: 7px;
}
}
}
.classement {
width: 100%;
h2 {
text-align: center;
}
}
</style> </style>

View File

@ -4,7 +4,7 @@ import {authInstance} from '../apis/auth.api';
export const loginRequest = (data: { username: string; password: string }) => { export const loginRequest = (data: { username: string; password: string }) => {
return authInstance return authInstance
.request({ .request({
url: 'http://localhost:8002/login', url: '/login',
method: 'POST', method: 'POST',
data, data,
headers: { headers: {
@ -23,7 +23,7 @@ export const registerRequest = (data: {
}) => { }) => {
return authInstance return authInstance
.request({ .request({
url: 'http://localhost:8002/register', url: '/register',
method: 'POST', method: 'POST',
data, data,
headers: { headers: {
@ -39,7 +39,7 @@ export const registerRequest = (data: {
export const refreshRequest = (token: string) => { export const refreshRequest = (token: string) => {
return authInstance return authInstance
.request({ .request({
url: 'http://localhost:8002/refresh', url: '/refresh',
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',

View File

@ -13,7 +13,6 @@ export const createRoom = (data: { name: string }, username: string | null = nul
}; };
export const getRoom = (id_code: string, clientId: string | null = null) => { export const getRoom = (id_code: string, clientId: string | null = null) => {
console.log('GETROOM', clientId, { ...(clientId != null && { clientId }) });
return roomInstance return roomInstance
.request({ .request({
url: '/' + id_code, url: '/' + id_code,
@ -23,6 +22,16 @@ export const getRoom = (id_code: string, clientId: string | null = null) => {
.then((r) => r.data); .then((r) => r.data);
}; };
export const deleteRoom = (id_code: string, clientId: string | null = null) => {
return roomInstance
.request({
url: '/' + id_code,
method: 'DELETE',
params: { ...(clientId != null && { clientId }) }
})
.then((r) => r.data);
};
export const createParcours = ( export const createParcours = (
id_code: string, id_code: string,
parcours: { time: number; name: string; max_mistakes: number; exercices: {exercice_id: string, quantity:number}[] }, parcours: { time: number; name: string; max_mistakes: number; exercices: {exercice_id: string, quantity:number}[] },

View File

@ -21,6 +21,7 @@
<Auth> <Auth>
<Alert> <Alert>
<Modal> <Modal>
<main> <main>
<NavBar/> <NavBar/>
<slot/> <slot/>

View File

@ -26,13 +26,13 @@
const room = writable<Room | null>(null); const room = writable<Room | null>(null);
const member = writable<Member | null>(null); const member = writable<Member | null>(null);
const parcours = writable<ParcoursRead | null>(null); const parcours = writable<ParcoursRead | null>(null);
const ws = connect("ws://127.0.0.1:8002/ws/room/" + $page.params.slug); const ws = connect("ws://127.0.0.1:8002/api/ws/room/" + $page.params.slug);
setContext("room", room); setContext("room", room);
setContext("member", member); setContext("member", member);
setContext("parcours", parcours); setContext("parcours", parcours);
setContext("ws", { send: (type: string, data: Object) => ws.send({ type, data }), ws: ws.ws }); setContext("ws", { send: (type: string, data: Object) => ws.send({ type, data }), ws: ws.ws });
const { error } = getContext("notif"); const { error, info } = getContext("notif");
const onMessage = (payload: { type: string; data: any }) => { const onMessage = (payload: { type: string; data: any }) => {
if (payload == undefined) return; if (payload == undefined) return;
const { type, data } = payload; const { type, data } = payload;
@ -83,11 +83,13 @@
case "waiting": case "waiting":
close()
$member = { ...data.waiter, room: data.room }; $member = { ...data.waiter, room: data.room };
goto(`?${new URLSearchParams({ a: "waiting" })}`); goto(`?${new URLSearchParams({ a: "waiting" })}`);
return; return;
case "refused": case "refused":
error("Refusé", "L'administrateur a refusé votre demande");
close(); close();
ws?.close(1000); ws?.close(1000);
goto("/room/join"); goto("/room/join");
@ -107,11 +109,18 @@
} }
return; return;
case "banned": case "banned":
error("Ban", "Vous avez été banni de la salle par l'administrateur");
ws?.close(1000); ws?.close(1000);
goto("/room/join"); goto("/room/join");
sessionStorage.removeItem("reconnect"); sessionStorage.removeItem("reconnect");
return; return;
case "deleted":
info("Suppression", "La salle a été supprimée par l'administrateur");
ws.close(1000)
goto("/room/join")
return
case "error": case "error":
const { code, msg } = data; const { code, msg } = data;
if (code == 401) { if (code == 401) {
@ -148,6 +157,10 @@
} else { } else {
error("Erreur", "Message : " + msg); error("Erreur", "Message : " + msg);
} }
if(code == 404){
ws.close(1000)
goto("/room/join")
}
return; return;
case "waiter": case "waiter":
@ -507,6 +520,7 @@
column-gap: 50px; column-gap: 50px;
row-gap: 10px; row-gap: 10px;
height: 100%; height: 100%;
padding: 7px 15px;
@include down(800) { @include down(800) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;