diff --git a/.idea/fastapi_gen.iml b/.idea/fastapi_gen.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/fastapi_gen.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..d46312a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..f649b90 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1676499669960 + + + + \ No newline at end of file diff --git a/backend/api/.idea/.gitignore b/backend/api/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/backend/api/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/backend/api/.idea/api.iml b/backend/api/.idea/api.iml new file mode 100644 index 0000000..2cdb1e3 --- /dev/null +++ b/backend/api/.idea/api.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/backend/api/.idea/codeStyles/codeStyleConfig.xml b/backend/api/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/backend/api/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/backend/api/.idea/dataSources.xml b/backend/api/.idea/dataSources.xml new file mode 100644 index 0000000..3ec678d --- /dev/null +++ b/backend/api/.idea/dataSources.xml @@ -0,0 +1,24 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/database.db + $ProjectFileDir$ + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/database7.db + $ProjectFileDir$ + + + file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.39.2/sqlite-jdbc-3.39.2.jar + + + + + \ No newline at end of file diff --git a/backend/api/.idea/dbnavigator.xml b/backend/api/.idea/dbnavigator.xml new file mode 100644 index 0000000..50a6953 --- /dev/null +++ b/backend/api/.idea/dbnavigator.xml @@ -0,0 +1,423 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/api/.idea/misc.xml b/backend/api/.idea/misc.xml new file mode 100644 index 0000000..fdbdefa --- /dev/null +++ b/backend/api/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backend/api/.idea/modules.xml b/backend/api/.idea/modules.xml new file mode 100644 index 0000000..d50cf45 --- /dev/null +++ b/backend/api/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/backend/api/.idea/vcs.xml b/backend/api/.idea/vcs.xml new file mode 100644 index 0000000..b2bdec2 --- /dev/null +++ b/backend/api/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backend/api/database/auth/crud.py b/backend/api/database/auth/crud.py index 02d7f04..43e40db 100644 --- a/backend/api/database/auth/crud.py +++ b/backend/api/database/auth/crud.py @@ -1,9 +1,14 @@ import uuid -from services.password import get_password_hash -from database.auth.models import User, UserEdit -from sqlmodel import Session, select + from jose import jwt, exceptions +from sqlmodel import Session, select + from config import SECRET_KEY, ALGORITHM +from database.auth.models import User, UserEdit +from database.room.models import Member +from services.password import get_password_hash + + def create_user_db(username:str , password: str, db: Session): user = User(username=username, hashed_password=password, clientId=uuid.uuid4()) db.add(user) @@ -84,3 +89,7 @@ def change_user_uuid(id: int, db: Session): db.refresh(user) return user.clientId + +def parse_user_rooms(user: User, db: Session): + members = db.exec(select(Member).where(Member.user_id == user.id)).all() + return [{"name": m.room.name, "id_code": m.room.id_code, "admin": m.is_admin} for m in members] diff --git a/backend/api/database/auth/models.py b/backend/api/database/auth/models.py index 5a37d66..cf22f63 100644 --- a/backend/api/database/auth/models.py +++ b/backend/api/database/auth/models.py @@ -1,9 +1,11 @@ -from typing import List, Optional +import uuid +from typing import List from typing import TYPE_CHECKING, Optional from uuid import UUID -import uuid -from sqlmodel import Field, SQLModel, Relationship + from pydantic import validator, BaseModel +from sqlmodel import Field, SQLModel, Relationship + from services.password import validate_password from services.schema import as_form @@ -29,9 +31,16 @@ class User(UserBase, table=True): @as_form class UserEdit(UserBase): pass - +class UsersRoom(BaseModel): + name: str + id_code: str + admin: bool = False class UserRead(UserBase): id: int + rooms: List[UsersRoom] = [] +class UserEditRead(UserBase): + id: int + #rooms: List[UsersRoom] = [] @as_form diff --git a/backend/api/database/db.py b/backend/api/database/db.py index ef4ef1b..c3c2a0d 100644 --- a/backend/api/database/db.py +++ b/backend/api/database/db.py @@ -2,7 +2,7 @@ import pydantic.json import json from sqlmodel import SQLModel, create_engine, Session, select -sqlite_file_name = "database.db" +sqlite_file_name = "database7.db" sqlite_url = f"sqlite:///{sqlite_file_name}" @@ -19,6 +19,6 @@ def create_db_and_tables(): SQLModel.metadata.create_all(engine) def get_session(): - with Session(engine) as s: + with Session(engine, expire_on_commit=False) as s: yield s diff --git a/backend/api/database/exercices/models.py b/backend/api/database/exercices/models.py index 388c2ce..6defdd9 100644 --- a/backend/api/database/exercices/models.py +++ b/backend/api/database/exercices/models.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from database.auth.models import User -class ExampleEnum(Enum): +class ExampleEnum(str, Enum): csv = 'csv' pdf = 'pdf' web = 'web' diff --git a/backend/api/database/room/crud.py b/backend/api/database/room/crud.py index e2594b6..933dcb4 100644 --- a/backend/api/database/room/crud.py +++ b/backend/api/database/room/crud.py @@ -1,17 +1,21 @@ -from services.auth import get_current_user_optional -from services.misc import noteOn20 -from copy import deepcopy -from typing import Dict, List import uuid -from fastapi import Body, Depends, HTTPException, status +from copy import deepcopy +from typing import List + +from fastapi import Depends, HTTPException, status, Query from pydantic import BaseModel -from sqlmodel import Session, delete, select, col, table -from database.db import get_session -from database.room.models import Anonymous, Challenge, Challenges, CorrigedGeneratorOut, Exercices, ExercicesCreate, Member, Note, Parcours, ParcoursCreate, ParcoursReadShort, ParsedGeneratorOut, Room, RoomCreate, RoomInfo, RoomRead, TmpCorrection, Waiter, MemberRead -from database.auth.models import User -from services.database import generate_unique_code +from sqlalchemy import func +from sqlmodel import Session, delete, select, col + from database.auth.crud import get_user_from_token +from database.auth.models import User +from database.db import get_session from database.exercices.models import Exercice +from database.room.models import Anonymous, Challenge, Challenges, CorrigedGeneratorOut, Exercices, ExercicesCreate, \ + Member, Note, Parcours, ParcoursCreate, ParcoursReadShort, ParsedGeneratorOut, Room, RoomCreate, RoomInfo, \ + TmpCorrection, Waiter, MemberRead, CorrigedData, CorrectionData, Challenger +from services.auth import get_current_user_optional +from services.database import generate_unique_code def create_room_db(*, room: RoomCreate, user: User | None = None, username: str | None = None, db: Session): @@ -38,12 +42,15 @@ def create_room_db(*, room: RoomCreate, user: User | None = None, username: str return {"room": room_obj, "member": member} + def change_room_name(room: Room, name: str, db: Session): room.name = name db.add(room) db.commit() db.refresh(room) return room + + def change_room_status(room: Room, public: bool, db: Session): room.public = public db.add(room) @@ -51,6 +58,7 @@ def change_room_status(room: Room, public: bool, db: Session): db.refresh(room) return room + def get_member_from_user(user_id: int, room_id: int, db: Session): member = db.exec(select(Member).where(Member.room_id == room_id, Member.user_id == user_id)).first() @@ -86,6 +94,7 @@ def get_anonymous_from_code(reconnect_code: str, db: Session): Anonymous.reconnect_code == reconnect_code)).first() return anonymous + def get_anonymous_from_clientId(clientId: str, db: Session): anonymous = db.exec(select(Anonymous).where( Anonymous.clientId == clientId)).first() @@ -102,20 +111,23 @@ def get_member_from_clientId(clientId: str, room_id: int, 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 = Member(room=room, user=user, anonymous=anonymous, waiting=waiting, + member = Member(room=room, user=user, anonymous=anonymous, waiting=waiting, id_code=member_id) + member.online = True db.add(member) db.commit() db.refresh(member) return member -def get_or_create_member(*, room: Room, user: User | None = None, anonymous: Anonymous | None = None, waiting: bool = False, db: Session): - member = user is not None and get_member_from_user(user.id, room.id, db) +def get_or_create_member(*, room: Room, user: User | None = None, anonymous: Anonymous | None = None, + waiting: bool = False, db: Session): + member = user is not None and get_member_from_user(user.id, room.id, db) if member is not None and member is not False: return member - member= create_member(room=room, user=user, anonymous=anonymous, waiting=waiting, db=db) - + member = create_member(room=room, user=user, + anonymous=anonymous, waiting=waiting, db=db) + def connect_member(member: Member, db: Session): member.online = True @@ -128,10 +140,10 @@ def connect_member(member: Member, db: Session): def disconnect_member(member: Member, db: Session): if member.waiting == False: member.online = False - + if member.anonymous is not None: - change_anonymous_clientId(member.anonymous,db) - + change_anonymous_clientId(member.anonymous, db) + db.add(member) db.commit() db.refresh(member) @@ -167,6 +179,7 @@ def create_anonymous_member(username: str, room: Room, db: Session): db.refresh(member) return member + def create_anonymous(username: str, room: Room, db: Session): username = validate_username(username, room, db) if username is None: @@ -179,10 +192,13 @@ def create_anonymous(username: str, room: Room, db: Session): db.refresh(anonymous) return anonymous + def check_user_in_room(user_id: int, room_id: int, db: Session): - user = db.exec(select(Member).where(Member.user_id==user_id, Member.room_id == room_id)).first() + user = db.exec(select(Member).where(Member.user_id == + user_id, Member.room_id == room_id)).first() return user + def create_user_member(user: User, room: Room, db: Session): member = get_member_from_user(user.id, room.id, db) if member is not None: @@ -194,6 +210,7 @@ def create_user_member(user: User, room: Room, db: Session): db.refresh(member) return member + def create_anonymous_waiter(username: str, room: Room, db: Session): username = validate_username(username, room, db) if username is None: @@ -210,6 +227,7 @@ def create_anonymous_waiter(username: str, room: Room, db: Session): db.refresh(member) return member + def create_user_waiter(user: User, room: Room, db: Session): member = get_member_from_user(user.id, room.id, db) if member is not None: @@ -219,6 +237,7 @@ def create_user_waiter(user: User, room: Room, db: Session): db=db) return member + def get_waiter(waiter_code: str, db: Session): return db.exec(select(Member).where(Member.id_code == waiter_code, Member.waiting == True)).first() @@ -236,6 +255,7 @@ def delete_member(member: Member, db: Session): def accept_waiter(member: Member, db: Session): member.waiting = False member.waiter_code = None + member.online = True db.add(member) db.commit() db.refresh(member) @@ -254,39 +274,173 @@ def leave_room(member: Member, db: Session): return None - -def serialize_member(member: Member) -> MemberRead | Waiter: +def serialize_member(member: Member, private: bool = False, admin: bool = False, + m2: Member | None = None) -> MemberRead | Waiter: member_obj = member.user or member.anonymous - if member.waiting == False: - return MemberRead(username=member_obj.username, reconnect_code=getattr(member_obj, "reconnect_code", ""), isUser=member.user_id != None, isAdmin=member.is_admin, id_code=member.id_code).dict() - if member.waiting == True: + print("OHLA", member_obj, private, member.user_id == None) + if not member.waiting: + return MemberRead(username=member_obj.username, online=member.online, + clientId=str(member_obj.clientId) if (private == True and member.user_id == None) else "", + reconnect_code=getattr(member_obj, "reconnect_code", "") if (admin or m2 == member) else "", + isUser=member.user_id != None, isAdmin=member.is_admin, id_code=member.id_code).dict() + if member.waiting: return Waiter(username=member_obj.username, waiter_id=member.id_code).dict() + def serialize_parcours_short(parcours: Parcours, member: Member, db: Session): - best_note = db.exec(select(Challenge.note, Challenge.time).where(Challenge.parcours_id == parcours.id, Challenge.challenger_id == member.id).order_by(col(Challenge.note).desc()).limit(1)).first() - note = None - if best_note is not None: - best_note=best_note[0] - note = Note(note=best_note[0], time=best_note[1]) - return ParcoursReadShort(**parcours.dict(exclude_unset=True), best_note=note) + challenger = getChallenger(parcours, member, db) + + return ParcoursReadShort(name=parcours.name, id_code=parcours.id_code, best_note=challenger.best, + validated=challenger.validated) + def serialize_challenge(challenge: Challenge): - return Challenges(name=challenge.challenger.user.username if challenge.challenger.user is not None else challenge.challenger.anonymous.username, value=Note(note=challenge.note, time=challenge.time), isCorriged=challenge.isCorriged, canCorrige=challenge.data is not None) + return Challenges( + name=challenge.challenger.user.username if challenge.challenger.user is not None else challenge.challenger.anonymous.username, + value=Note(note=challenge.note, time=challenge.time), isCorriged=challenge.isCorriged, + canCorrige=challenge.data is not None) -def serialize_parcours_short(parcours: Parcours, member: Member, db: Session): - if member.is_member == False: - challenges = db.exec(select(Challenge).where(Challenge.parcours_id == parcours.id, Challenge.challenger_id == member.id)).all() - else: - challenges = db.exec(select(Challenge).where( - Challenge.parcours_id == parcours.id)).all() - - challenges = [serialize_challenge(c) for c in challenges] - return Parcours(**parcours.dict(), challenges=challenges) - - def serialize_room(room: Room, member: Member, db: Session): - return RoomInfo(**room.dict(), parcours=[serialize_parcours_short(p, member, db) for p in room.parcours], members=[serialize_member(m) for m in room.members]) + return RoomInfo(**room.dict(), parcours=[serialize_parcours_short(p, member, db) for p in room.parcours], + members=[serialize_member(m, admin=member.is_admin, m2=member) for m in room.members]) + + +def getUsername(m: Member): + return m.user.username if m.user is not None else m.anonymous.username + + +def getChallengerInfo(c: Challenge, db: Session): + challenger = db.exec(select(Challenger).where(Challenger.member_id == + c.challenger_mid, Challenger.parcours_id == c.challenger_pid)).first() + if challenger is not None: + member = challenger.member + return {"name": getUsername(member), "id_code": member.id_code} + + +def getChallenges(c: Challenger, db: Session): + challenges = db.exec(select(Challenge).where(Challenge.challenger_mid == c.member_id, + Challenge.challenger_pid == c.parcours_id)).all() + return challenges + + +def getTops(p: Parcours, db: Session): + tops = db.exec(select(Challenge).where(Challenge.parcours_id == p.id_code).order_by( + col(Challenge.mistakes), col(Challenge.time)).limit(3)).all() + + tops = [{"challenger": getChallengerInfo( + t, db), "mistakes": t.mistakes, "time": t.time} for t in tops] + return tops + + +def getAvgTops(p: Parcours, db: Session): + avgTop = db.exec(select(Challenger).where(Challenger.parcours_id == + p.id).order_by(col(Challenger.avg)).limit(3)).all() + + avgTop = [{"id_code": t.member.id_code, "avg": t.avg, + "name": getUsername(t.member)} for t in avgTop] + return avgTop + + +def getRank(c: Challenger, p: Parcours, db: Session): + noteRank = db.exec(select([func.count(Challenge.id)]).where(Challenge.parcours_id == p.id_code).order_by( + col(Challenge.mistakes), col(Challenge.time)).where(Challenge.mistakes <= c.best, + Challenge.time < c.best_time)).one() + return noteRank + 1 + + +def getAvgRank(c: Challenger, p: Parcours, db: Session): + avgRank = db.exec(select([func.count(Challenger.member_id)]).where( + Challenger.parcours_id == p.id).order_by(col(Challenger.avg)).where(Challenger.avg < c.avg)).one() + return avgRank + 1 + + +def getMemberRank(m: Member, p: Parcours, db: Session): + challenger = db.exec(select(Challenger).where(Challenger.member_id == m.id)).first() + if challenger is None or challenger.best is None: + return None + return getRank(challenger, p, db) + + +def getMemberAvgRank(m: Member, p: Parcours, db: Session): + challenger = db.exec(select(Challenger).where(Challenger.member_id == m.id)).first() + print('CHALLE', challenger) + if challenger is None or challenger.avg is None: + return None + return getAvgRank(challenger, p, db) + + +def serialize_parcours(parcours: Parcours, member: Member, db: Session): + tops = getTops(parcours, db) + avgTop = getAvgTops(parcours, db) + + challenger = db.exec(select(Challenger).where( + Challenger.member_id == member.id, Challenger.parcours_id == parcours.id)).first() + + noteRank = None + avgRank = None + pb = None + if challenger is not None and challenger.avg is not None and challenger.best is not None: + noteRank = getRank(challenger, parcours, db) + avgRank = getAvgRank(challenger, parcours, db) + pb = {"mistakes": challenger.best, "time": challenger.best_time} + + statement = select(Challenger).where(Challenger.parcours_id == parcours.id) + if not member.is_admin: + statement = statement.where(Challenger.member_id == member.id) + + challengers = db.exec(statement).all() + + challs = {c.member.id_code: { + "challenger": {"id_code": c.member.id_code, "name": getUsername(c.member)}, + # 'validated': chall.mistakes <= parcours.max_mistakes + "challenges": [Challenges(**{**chall.dict(), "canCorrige": chall.data != []}) for chall in getChallenges(c, db)] + } for c in challengers} + + return {**parcours.dict(), "pb": pb, "tops": tops, "challenges": challs, "rank": noteRank, "memberRank": avgRank, + "validated": challenger.validated if challenger != None else False, "ranking": avgTop} + tops = [] + challs = {} + challenges = sorted(parcours.challenges, key=lambda x: ( + x.note['value'], x.time), reverse=True) + memberRank = None + rank = None + pb = None + validated = False + + total = 0 + + for i, chall in enumerate(challenges): + total += chall.note['value'] + id = chall.challenger.id_code + name = chall.challenger.user.username if chall.challenger.user_id != None else chall.challenger.anonymous.username + if i <= 2: + tops.append({"challenger": {"id_code": id, "name": name}, + "note": chall.note, "time": chall.time}) + + if id == member.id_code: + if challs.get(id) is None: + rank = i + 1 + memberRank = len(challs) + 1 + pb = {"note": chall.note, "time": chall.time} + if validated is False and chall.validated: + validated = True + + if member.is_admin or chall.challenger.id_code == member.id_code: + t = challs.get(id, {"total": 0})['total'] + challs[id] = {"challenger": {"id_code": id, "name": name + }, "challenges": [*challs.get(id, {'challenges': []})['challenges'], + Challenges( + **{**chall.dict(), "canCorrige": chall.data != []})], + "total": t + chall.note['value']} + + topMembers = [{**c['challenger'], "avg": c['total'] / + len(c['challenges'])} for id, c in challs.items()] + topMembers.sort(key=lambda x: x['avg'], reverse=True) + return {**parcours.dict(), "tops": tops, "challenges": challs, "rank": rank, "memberRank": memberRank, "pb": pb, + "validated": validated, + 'avg': None if len(parcours.challenges) == 0 else round(total / len(parcours.challenges), 2), + "ranking": topMembers} def change_anonymous_clientId(anonymous: Anonymous, db: Session): @@ -298,20 +452,49 @@ def change_anonymous_clientId(anonymous: Anonymous, db: Session): return anonymous -#Parcours -def validate_exercices(exos: List[ExercicesCreate], db: Session ): - exercices = db.exec(select(Exercice).where(Exercice.web == True).where(col(Exercice.id_code).in_([e.exercice_id for e in exos]))).all() - exos_id_list = [e.exercice_id for e in exos] - exercices.sort(key=lambda e: exos_id_list.index(e.id_code)) - return [Exercices(exercice_id=e.id_code, name=e.name, quantity=[ex for ex in exos if ex.exercice_id == e.id_code][0].quantity).dict() for e in exercices] +# Parcours +from services.io import add_fast_api_root +from generateur.generateur_main import generate_from_path, parseGeneratorOut -def create_parcours_db(parcours: ParcoursCreate,room_id: int, db: Session): + +def countInput(ex: Exercice, q: int): + exo = parseGeneratorOut(generate_from_path(add_fast_api_root( + ex.exo_source), 1, "web")) + return len(exo.inputs) * q + + +class ExoToCount(BaseModel): + ex: Exercice + q: int + + +def getTotal(exs: list[ExoToCount]): + total = 0 + for e in exs: + total += countInput(e.ex, e.q) + return total + + +def validate_exercices(exos: List[ExercicesCreate], db: Session): + exercices = db.exec(select(Exercice).where(Exercice.web == True).where( + col(Exercice.id_code).in_([e.exercice_id for e in exos]))).all() + exos_id_list = [e.exercice_id for e in exos] + # exoToCountList = [ExoToCount(ex=e, q=q) for e, q in zip(exercices, [c.quantity for c in exos])] + exercices.sort(key=lambda e: exos_id_list.index(e.id_code)) + return [Exercices(exercice_id=e.id_code, name=e.name, + quantity=[ex for ex in exos if ex.exercice_id == e.id_code][0].quantity, + examples=e.examples).dict() for e in exercices] + + +def create_parcours_db(parcours: ParcoursCreate, room_id: int, db: Session): exercices = validate_exercices(parcours.exercices, db) if len(exercices) == 0: return "Veuillez entrer au moins un exercice valide" id_code = generate_unique_code(Parcours, s=db) - parcours_obj = Parcours(**{**parcours.dict(), "exercices": exercices}, room_id=room_id, id_code=id_code) - print(parcours_obj) + + parcours_obj = Parcours( + **{**parcours.dict(), "exercices": exercices}, room_id=room_id, id_code=id_code) + db.add(parcours_obj) db.commit() db.refresh(parcours_obj) @@ -320,24 +503,80 @@ def create_parcours_db(parcours: ParcoursCreate,room_id: int, db: Session): def deleteParcoursRelated(parcours: Parcours, db: Session): db.exec(delete(Challenge).where(Challenge.parcours_id == parcours.id_code)) - db.exec(delete(TmpCorrection).where(TmpCorrection.parcours_id == parcours.id_code)) + db.exec(delete(TmpCorrection).where( + TmpCorrection.parcours_id == parcours.id_code)) + db.exec(delete(Challenger).where(Challenger.parcours_id == parcours.id)) + db.commit() - + + +def change_challengers_validation(p: Parcours, validation: int, db: Session): + challengers = db.exec(select(Challenger).where( + Challenger.parcours_id == p.id)).all() + challs = [] + for c in challengers: + validated = c.best <= validation + if validated != c.validated: + c.validated = validated + challs.append(c) + + db.bulk_save_objects(challs) + db.commit() + + +def change_challenges_validation(p: Parcours, validation: int, db: Session): + print('cHANGE') + challenges = db.exec(select(Challenge).where( + Challenge.parcours_id == p.id_code)).all() + print('CHALLS', challenges) + challs = [] + for c in challenges: + validated = c.mistakes <= validation + print('CHAL', validated, c.validated, c) + if validated != c.validated: + c.validated = validated + challs.append(c) + + db.bulk_save_objects(challs) + db.commit() + + +def changeValidation(p: Parcours, validation: int, db: Session): + change_challengers_validation(p, validation, db) + change_challenges_validation(p, validation, db) + + +def compareExercices(old: list[Exercices], new: list[ExercicesCreate]): + old = [{"id": o['exercice_id'], "q": o['quantity']} for o in old] + new = [{"id": n.exercice_id, "q": n.quantity} for n in new] + return old == new + + def update_parcours_db(parcours: ParcoursCreate, parcours_obj: Parcours, db: Session): - exercices = validate_exercices(parcours.exercices, db) - if len(exercices) == 0: - return "Veuillez entrer au moins un exercice valide" - - parcours_data = parcours.dict(exclude_unset=True) - for key, value in parcours_data.items(): - setattr(parcours_obj, key, value) - parcours_obj.exercices = exercices + update_challenges = False + + if not compareExercices(parcours_obj.exercices, parcours.exercices): + exercices = validate_exercices(parcours.exercices, db) + if len(exercices) == 0: + return "Veuillez entrer au moins un exercice valide" + deleteParcoursRelated(parcours_obj, db) + update_challenges = True + parcours_obj.exercices = exercices + + if parcours_obj.max_mistakes != parcours.max_mistakes: + changeValidation(parcours_obj, parcours.max_mistakes, db) + + parcours_obj.name = parcours.name + parcours_obj.time = parcours.time + parcours_obj.max_mistakes = parcours.max_mistakes + db.add(parcours_obj) db.commit() - deleteParcoursRelated(parcours_obj, db) + db.refresh(parcours_obj) - - return parcours_obj + + return parcours_obj, update_challenges + def delete_parcours_db(parcours: Parcours, db: Session): db.delete(parcours) @@ -347,138 +586,210 @@ def delete_parcours_db(parcours: Parcours, db: Session): class CorrigedChallenge(BaseModel): data: List[List[CorrigedGeneratorOut]] - note: Note + mistakes: int isCorriged: bool -def create_tmp_correction(data: List[List[CorrigedGeneratorOut]], parcours_id: str, member: Member, db: Session): +def create_tmp_correction(data: List[CorrigedData], parcours_id: str, member: Member, db: Session): code = generate_unique_code(TmpCorrection, s=db) tmpCorr = TmpCorrection(data=data, id_code=code, member=member, parcours_id=parcours_id) db.add(tmpCorr) db.commit() db.refresh(tmpCorr) + return tmpCorr -def change_challenge(challenge: Challenge, corriged: CorrigedChallenge, db: Session): - challenge.data = corriged['data'] - challenge.note = corriged['note'] - challenge.isCorriged = corriged['isCorriged'] - challenge.validated = noteOn20( - corriged['note']['value'], corriged['note']['total']) > challenge.parcours.validate_condition - db.add(challenge) - db.commit() - db.refresh(challenge) - - return challenge - - -def validate_challenge_input(obj: List[List[ParsedGeneratorOut]], corr: TmpCorrection): +def validate_challenge_input(obj: List[CorrectionData], corr: TmpCorrection): data = corr.data if len(obj) != len(data): return False for i in range(len(data)): exo_corr = data[i] exo = obj[i] - if len(exo) != len(exo_corr): + print('EXO', exo) + print('EXO', exo.data) + if len(exo.data) != len(exo_corr['data']): return - zipped = zip(exo_corr, exo) - same = all([e['calcul'] == f.calcul and len(e['inputs']) == len(f.inputs) for e,f in zipped]) + zipped = zip(exo_corr['data'], exo.data) + same = all([e['calcul'] == f.calcul and len(e['inputs']) + == len(f.inputs) for e, f in zipped]) if not same: return False return True -def validate_challenge_correction(obj: List[List[CorrigedGeneratorOut]], chall: Challenge): + +def validate_challenge_correction(obj: List[CorrigedData], chall: Challenge): data = chall.data if len(obj) != len(data): return False for i in range(len(data)): exo_corr = data[i] exo = obj[i] - if len(exo) != len(exo_corr): + if len(exo.data) != len(exo_corr['data']): return - zipped = zip(exo_corr, exo) - same = all([e['calcul'] == f.calcul and len(e['inputs']) == len(f.inputs) for e,f in zipped]) + zipped = zip(exo_corr['data'], exo.data) + same = all([e['calcul'] == f.calcul and len(e['inputs']) + == len(f.inputs) for e, f in zipped]) if not same: return False return True - def corrige_challenge(obj: List[List[ParsedGeneratorOut]], corr: TmpCorrection) -> CorrigedChallenge: - if validate_challenge_input(obj , corr) is False: + if validate_challenge_input(obj, corr) is False: return None + data = corr.data note = 0 total = 0 isCorriged = True + mistakes = 0 for i in range(len(data)): - exo_corr = data[i] - exo = obj[i] + exo_corr = data[i]["data"] + exo = obj[i].data if len(exo) != len(exo_corr): return zipped = zip(exo_corr, exo) for e, f in zipped: + print("HO\n\n") for k, l in zip(e['inputs'], f.inputs): k["value"] = str(l.value) total += 1 if k['correction'] is None: isCorriged = False - if str(k["correction"]) == str(l.value): + k['valid'] = None + + elif str(k["correction"]) == str(l.value): + k['valid'] = True note += 1 - - return {"data": data, "note": {"value": 1, "total": 3}, "isCorriged": isCorriged} - - - -def change_correction(obj: List[List[CorrigedGeneratorOut]], chall: Challenge) -> CorrigedChallenge: + else: + k['valid'] = False + mistakes += 1 + + return {"data": data, "mistakes": mistakes, "isCorriged": isCorriged} + return {"data": data, "mistakes": mistakes, "note": {"value": note, "total": total}, "isCorriged": isCorriged} + + +def change_correction(obj: List[CorrigedData], chall: Challenge) -> CorrigedChallenge: if validate_challenge_correction(obj, chall) is False: return None data = deepcopy(chall.data) note = 0 total = 0 isCorriged = True + mistakes = 0 for i in range(len(data)): - exo_corr = data[i] - exo = obj[i] + exo_corr = data[i]['data'] + exo = obj[i].data if len(exo) != len(exo_corr): return zipped = zip(exo_corr, exo) for e, f in zipped: for k, l in zip(e['inputs'], f.inputs): - k["correction"] = str(l.correction) + k["correction"] = l.correction + k["valid"] = l.valid total += 1 - if k['correction'] is None: + if k['correction'] is None and l.valid is None: isCorriged = False - if str(k["correction"]) == str(l.value): + if l.valid is True: note += 1 - + else: + mistakes += 1 + + return {"data": data, "mistakes": mistakes, "isCorriged": isCorriged} return {"data": data, "note": {"value": note, "total": total}, "isCorriged": isCorriged} - +def getChallenger(parcours: Parcours, member: Member, db: Session): + challenger = db.exec(select(Challenger).where( + Challenger.member_id == member.id, Challenger.parcours_id == parcours.id)).first() + if challenger is None: + return Challenger(parcours_id=parcours.id, member_id=member.id) + return challenger -def create_challenge(data: List[List[CorrigedGeneratorOut]], challenger: Member, parcours: Parcours, time: int, note: Note, isCorriged: bool, db: Session): - challenge = Challenge(data=data, challenger=challenger, parcours=parcours, time=time, note=note, validated=noteOn20(note["value"], note['total']) >= - parcours.validate_condition, isCorriged=isCorriged, id_code=generate_unique_code(Challenge, s=db)) + +def ChallengerFromChallenge(c: Challenge, db: Session): + challenger = db.exec(select(Challenger).where( + Challenger.member_id == c.challenger_mid, Challenger.parcours_id == c.challenger_pid)).first() + return challenger + + +def checkValidated(challenger: Challenger, db: Session, challenge: Challenge | None = None): + challenges = db.exec(select(Challenge).where(Challenge.challenger_mid == challenger.member_id, + Challenge.challenger_pid == challenger.parcours_id, + Challenge.validated == True)).all() + if challenge is not None: + challenges = [c for c in challenges if c.id != challenge.id] + return len(challenges) != 0 + + +def create_challenge(data: List[CorrigedData], challenger: Member, parcours: Parcours, time: int, mistakes: int, + isCorriged: bool, db: Session): + challenger_obj: Challenger = getChallenger(parcours, challenger, db) + validated = mistakes <= parcours.max_mistakes + challenge = Challenge(data=data, challenger_pid=challenger_obj.parcours_id, challenger_mid=challenger_obj.member_id, + parcours=parcours, time=time, mistakes=mistakes, isCorriged=isCorriged, + id_code=generate_unique_code(Challenge, s=db), validated=validated) + + if (challenger_obj.best is not None and challenger_obj.best > mistakes) or challenger_obj.best is None: + challenger_obj.best = mistakes + challenger_obj.best_time = time + + challenges = db.exec(select([func.count(Challenge.id)]).where( + Challenge.challenger_mid == challenger_obj.member_id, Challenge.challenger_pid == parcours.id)).one() + + if validated and challenger_obj.validated is False: + challenger_obj.validated = True + + avg = challenger_obj.avg + if avg is None: + avg = 0 + challenger_obj.avg = (avg * + (challenges - 1) + mistakes) / (challenges) db.add(challenge) + db.add(challenger_obj) db.commit() db.refresh(challenge) - return challenge + db.refresh(challenger_obj) + print('RETURN,', challenge, challenger_obj) + return challenge, challenger_obj + def change_challenge(challenge: Challenge, corriged: CorrigedChallenge, db: Session): + challenger = ChallengerFromChallenge(challenge, db) + + challengesCount = len(getChallenges(challenger, db)) + avg = challenger.avg * challengesCount - challenge.mistakes + parcours = challenge.parcours + if challenger.best > corriged['mistakes']: + challenger.best = corriged['mistakes'] + challenger.best_time = challenge.time + + validated = corriged['mistakes'] <= parcours.max_mistakes + challenge.validated = validated + + if challenger.validated == False and validated: + challenger.validated = True + + elif challenger.validated == True and not validated: + challenger.validated = checkValidated(challenger, db, challenge) + + challenger.avg = (avg + corriged['mistakes']) / challengesCount + challenge.data = corriged['data'] - challenge.note = corriged['note'] + challenge.mistakes = corriged['mistakes'] challenge.isCorriged = corriged['isCorriged'] - challenge.validated = noteOn20( - corriged['note']['value'], corriged['note']['total']) > challenge.parcours.validate_condition + # challenge.validated = corriged['mistakes'] <= parcours.max_mistakes db.add(challenge) + db.add(challenger) db.commit() db.refresh(challenge) + db.refresh(challenger) - return challenge + return challenge, challenger # Dependencies @@ -489,8 +800,6 @@ def check_room(room_id: str, db: Session = Depends(get_session)): return room - - def get_room(room_id, db: Session = Depends(get_session)): room = db.exec(select(Room).where(Room.id_code == room_id)).first() if room is None: @@ -499,13 +808,14 @@ def get_room(room_id, db: Session = Depends(get_session)): return room -def get_member_dep(room: Room = Depends(get_room), user: User = Depends(get_current_user_optional), clientId: str | None = Body(default=None), db: Session = Depends(get_session)): +def get_member_dep(room: Room = Depends(get_room), user: User = Depends(get_current_user_optional), + clientId: str | None = Query(default=None), db: Session = Depends(get_session)): if user is None and clientId is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") if user is not None: member = get_member_from_user(user.id, room.id, db) - if clientId is not None: + elif clientId is not None: member = get_member_from_clientId(clientId, room.id, db) if member is None: @@ -550,11 +860,9 @@ def get_correction(correction_id: str, parcours_id: str, member: Member = Depend return tmpCorr - -def get_challenge(challenge_id: str, parcours_id: str, db: Session = Depends(get_session)): - +def get_challenge(challenge_id: str, db: Session = Depends(get_session)): challenge = db.exec(select(Challenge).where( - Challenge.id_code == challenge_id, Challenge.parcours_id == parcours_id)).first() + Challenge.id_code == challenge_id)).first() if challenge is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Challenge introuvable") @@ -564,4 +872,3 @@ def get_challenge(challenge_id: str, parcours_id: str, db: Session = Depends(get status_code=status.HTTP_400_BAD_REQUEST, detail="Impossible de corriger ce challenge") return challenge - diff --git a/backend/api/database/room/models.py b/backend/api/database/room/models.py index 5f2cd5b..0685819 100644 --- a/backend/api/database/room/models.py +++ b/backend/api/database/room/models.py @@ -2,214 +2,325 @@ from uuid import UUID, uuid4 from pydantic import root_validator, BaseModel import pydantic.json from typing import List, Optional, TYPE_CHECKING -from sqlmodel import SQLModel, Field, Relationship, JSON, Column - +from sqlmodel import SQLModel, Field, Relationship, JSON, Column, ForeignKeyConstraint +from database.exercices.models import Example from database.auth.models import UserRead + if TYPE_CHECKING: from database.auth.models import User - - + + class RoomBase(SQLModel): - name: str = Field(max_length=20) - public: bool = Field(default=False) - global_results: bool = Field(default=False) + name: str = Field(max_length=20) + public: bool = Field(default=False) + global_results: bool = Field(default=False) + class RoomCreate(RoomBase): - pass + pass + class Room(RoomBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - id_code: str = Field(index=True) - - members: List['Member'] = Relationship(back_populates="room") - parcours: List['Parcours'] = Relationship(back_populates="room") - - + id: Optional[int] = Field(default=None, primary_key=True) + id_code: str = Field(index=True) + + members: List['Member'] = Relationship(back_populates="room") + parcours: List['Parcours'] = Relationship(back_populates="room") + + class AnonymousBase(SQLModel): - username: str = Field(max_length=20) - + username: str = Field(max_length=20) + + class AnonymousCreate(AnonymousBase): - pass + pass + class Anonymous(AnonymousBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - reconnect_code: str = Field(index=True) - - clientId: Optional[UUID] = Field(default=uuid4(), index=True) - member: 'Member' = Relationship(back_populates="anonymous") - - -class Member(SQLModel, table = True): - id: Optional[int] = Field(default=None, primary_key=True) - id_code: str = Field(index=True) - - user_id: Optional[int] = Field(foreign_key="user.id", default=None) - user: Optional["User"] = Relationship(back_populates='members') - - anonymous_id: Optional[int] = Field(foreign_key="anonymous.id", default=None) - anonymous: Optional[Anonymous] = Relationship(back_populates="member") - - room_id: int = Field(foreign_key="room.id") - room: Room = Relationship(back_populates='members') + id: Optional[int] = Field(default=None, primary_key=True) + reconnect_code: str = Field(index=True) + + clientId: Optional[UUID] = Field(default=uuid4(), index=True) + member: 'Member' = Relationship(back_populates="anonymous") + + +class Member(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + id_code: str = Field(index=True) + + user_id: Optional[int] = Field(foreign_key="user.id", default=None) + user: Optional["User"] = Relationship(back_populates='members') + + anonymous_id: Optional[int] = Field( + foreign_key="anonymous.id", default=None) + anonymous: Optional[Anonymous] = Relationship(back_populates="member") + + room_id: int = Field(foreign_key="room.id") + room: Room = Relationship(back_populates='members') + + challengers: List["Challenger"] = Relationship(back_populates="member") + + is_admin: bool = False + + waiting: bool = False + + online: bool = False + + waiter_code: Optional[str] = Field(default=None) + + corrections: List['TmpCorrection'] = Relationship(back_populates="member") - challenges: List["Challenge"] = Relationship(back_populates="challenger") - - is_admin: bool = False - - waiting: bool = False - online: bool = False - - waiter_code: Optional[str] = Field(default= None) - - corrections: List['TmpCorrection'] = Relationship(back_populates="member") - class ExercicesCreate(SQLModel): - exercice_id: str - quantity: int = 10 - -class Exercices(ExercicesCreate): - name: str + exercice_id: str + quantity: int = 10 + +class Exercices(ExercicesCreate): + name: str + examples: Example + +class Challenger(SQLModel, table=True): + member_id: int = Field(foreign_key="member.id", primary_key=True) + parcours_id: int = Field(foreign_key="parcours.id", primary_key=True) + + parcours: "Parcours" = Relationship(back_populates="challengers") + member: "Member" = Relationship(back_populates="challengers") + + #challenges: list["Challenge"] = Relationship(back_populates="challenger") + + avg: Optional[float] = Field(default=None) + best: Optional[int] = Field(default=None) + best_time: Optional[int] = Field(default=None) + validated: bool = Field(default=False) + class Parcours(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - id_code: str = Field(index=True, unique=True) - - room_id: int = Field(foreign_key="room.id") - room: Room = Relationship(back_populates='parcours') - - name: str - time: int - validate_condition: int - - exercices: List[Exercices] = Field(sa_column=Column(JSON)) - challenges: List["Challenge"] = Relationship(back_populates="parcours") - corrections: List["TmpCorrection"] = Relationship(back_populates="parcours") + id: Optional[int] = Field(default=None, primary_key=True) + id_code: str = Field(index=True, unique=True) + + room_id: int = Field(foreign_key="room.id") + room: Room = Relationship(back_populates='parcours') + + challengers: list[Challenger] = Relationship(back_populates="parcours") + + name: str + time: int + + + max_mistakes: int + + exercices: List[Exercices] = Field(sa_column=Column(JSON)) + challenges: List["Challenge"] = Relationship(back_populates="parcours") + corrections: List["TmpCorrection"] = Relationship( + back_populates="parcours") + + + class Note(BaseModel): - value: int - total: int - - + value: int + total: int + + class TimedNote(Note): - time: int - + time: int + + class ParcoursReadShort(SQLModel): - name: str - best_note: str | None = None - id_code: str - + name: str + best_note: str | None = None + validated: bool = False + id_code: str + +class ParcoursReadUpdate(SQLModel): + id_code: str + name: str + time: int + max_mistakes: int + exercices: List[Exercices] + update_challenges: bool = False + +class ChallengerInfo(BaseModel): + name: str + id_code: str + + +class ChallengerAverage(ChallengerInfo): + avg: float + + class Challenges(SQLModel): - id_code: str - challenger: str - note: Note - time: int - isCorriged: bool - canCorrige: bool - validated: bool + id_code: str + mistakes: int + time: int + isCorriged: bool + canCorrige: bool = True + validated: bool = False + + +class ChallengeInfo(BaseModel): + challenger: ChallengerInfo + challenges: list[Challenges] + #total: int + +class Tops(BaseModel): + challenger: ChallengerInfo + mistakes: int + time: int class ParcoursRead(SQLModel): - name: str - time: int - validate_condition: int - id_code: str - exercices: List[Exercices] - challenges: List[Challenges] - + tops: list[Tops] + name: str + time: int + max_mistakes: int + id_code: str + exercices: List[Exercices] + challenges: dict[str, ChallengeInfo] + rank: int | None + pb: dict[str, int] | None + memberRank: int | None + validated: bool + ranking: list[ChallengerAverage] + #avg: float | None + + class ParcoursCreate(SQLModel): - name: str - time: int - validate_condition: int - exercices: List[ExercicesCreate] + name: str + time: int + max_mistakes: int + exercices: List[ExercicesCreate] class NotCorrigedInput(BaseModel): - index: int - value: str - + index: int + value: str + + class CorrigedInput(NotCorrigedInput): - correction: str + correction: str | None + valid: bool | None + class ParsedGeneratorOut(BaseModel): - calcul: str - inputs: List[NotCorrigedInput] - + calcul: str + inputs: List[NotCorrigedInput] + + class CorrigedGeneratorOut(BaseModel): - calcul: str - inputs: List[CorrigedInput] + calcul: str + inputs: List[CorrigedInput] + class Challenge(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - id_code: str = Field(index=True, unique=True) - - challenger_id: int = Field(foreign_key="member.id") - challenger: Member = Relationship(back_populates="challenges") - - parcours_id: int = Field(foreign_key="parcours.id_code") - parcours: Parcours = Relationship(back_populates="challenges") - - data: Optional[List[List[CorrigedGeneratorOut]]] = Field(sa_column=Column(JSON), default=[]) - - time: int - note: Note = Field( - sa_column=Column(JSON)) - validated: bool - isCorriged: bool - - + id: Optional[int] = Field(default=None, primary_key=True) + id_code: str = Field(index=True, unique=True) + + ''' challenger_id: int = Field(foreign_key="member.id") + challenger: Member = Relationship(back_populates="challenges") ''' + + parcours_id: int = Field(foreign_key="parcours.id_code") + parcours: Parcours = Relationship(back_populates="challenges") + + challenger_pid: int + challenger_mid: int + __table_args__ = (ForeignKeyConstraint(["challenger_pid", "challenger_mid"], + ["challenger.parcours_id","challenger.member_id"]), + {}) + + #challenger: "Challenger" = Relationship(back_populates="challenges") + + data: Optional[List] = Field( + sa_column=Column(JSON), default=[]) + + time: int + ''' note: Note = Field( + sa_column=Column(JSON)) ''' + + mistakes: int + #note_value: int + validated: bool + + isCorriged: bool + + +class ExoInfo(BaseModel): + id_code: str + name: str + consigne: str | None + + +class CorrigedData(BaseModel): + exo: ExoInfo + data: List[CorrigedGeneratorOut] + + +class CorrectionData(BaseModel): + exo: ExoInfo + data: List[ParsedGeneratorOut] + + class ChallengeRead(SQLModel): id_code: str - data: Optional[List[List[CorrigedGeneratorOut]]] = [] + data: Optional[List[CorrigedData]] = [] time: int - note: Note + mistakes: int validated: bool isCorriged: bool + class TmpCorrection(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - id_code: str = Field(index=True) - - parcours_id: str = Field(foreign_key="parcours.id_code") - parcours: Parcours = Relationship(back_populates="corrections") - - member_id: int = Field(foreign_key="member.id") - member: Member = Relationship(back_populates="corrections") - - data: List[List[CorrigedGeneratorOut]] = Field(sa_column=Column(JSON)) - + id: Optional[int] = Field(default=None, primary_key=True) + id_code: str = Field(index=True) + + parcours_id: str = Field(foreign_key="parcours.id_code") + parcours: Parcours = Relationship(back_populates="corrections") + + member_id: int = Field(foreign_key="member.id") + member: Member = Relationship(back_populates="corrections") + + data: List = Field(sa_column=Column(JSON)) + + class AnonymousRead(AnonymousBase): - reconnect_code: str - + reconnect_code: str + + class Username(SQLModel): - username: str - - + username: str + + class MemberRead(SQLModel): - username: str - reconnect_code: str = '' - isUser: bool - isAdmin: bool - id_code: str + username: str + reconnect_code: str = '' + isUser: bool + isAdmin: bool + id_code: str + clientId: str = "" + online: bool class RoomRead(RoomBase): - id_code: str - + id_code: str + + class RoomAndMember(BaseModel): room: RoomRead member: MemberRead -class RoomInfo(RoomRead): - public: bool - name: str - members: List[MemberRead] - parcours: List[ParcoursReadShort] - + class Waiter(BaseModel): username: str waiter_id: str + +class RoomInfo(RoomRead): + public: bool + name: str + members: List[MemberRead | Waiter] + parcours: List[ParcoursReadShort] + + class RoomConnectionInfos(BaseModel): - room: str - member: str | None = None \ No newline at end of file + room: str + member: str | None = None diff --git a/backend/api/database2.db b/backend/api/database2.db new file mode 100644 index 0000000..74c4bbf Binary files /dev/null and b/backend/api/database2.db differ diff --git a/backend/api/database3.db b/backend/api/database3.db new file mode 100644 index 0000000..004a896 Binary files /dev/null and b/backend/api/database3.db differ diff --git a/backend/api/database4.db b/backend/api/database4.db new file mode 100644 index 0000000..4d2d574 Binary files /dev/null and b/backend/api/database4.db differ diff --git a/backend/api/database5.db b/backend/api/database5.db new file mode 100644 index 0000000..8e49e83 Binary files /dev/null and b/backend/api/database5.db differ diff --git a/backend/api/database6.db b/backend/api/database6.db new file mode 100644 index 0000000..470d788 Binary files /dev/null and b/backend/api/database6.db differ diff --git a/backend/api/database7.db b/backend/api/database7.db new file mode 100644 index 0000000..fbf27af Binary files /dev/null and b/backend/api/database7.db differ diff --git a/backend/api/faking/room.py b/backend/api/faking/room.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/generateur/generateur_csv.py b/backend/api/generateur/generateur_csv.py index f9bfdae..3950ab6 100644 --- a/backend/api/generateur/generateur_csv.py +++ b/backend/api/generateur/generateur_csv.py @@ -70,3 +70,4 @@ def Csv_generator(path, nb_in_serie, nb_page, police, consigne, writer): for r in range(rest_line): writer.writerow(['']) + diff --git a/backend/api/generateur/generateur_main.py b/backend/api/generateur/generateur_main.py index f602c1d..72ab2e8 100644 --- a/backend/api/generateur/generateur_main.py +++ b/backend/api/generateur/generateur_main.py @@ -1,41 +1,45 @@ import random import re -import importlib.util import string from typing import List -from pydantic import BaseModel + import sympy +from pydantic import BaseModel + class GeneratorOut(BaseModel): calcul: str correction: str | None = None -def parseOut(calcul): - """Fait en sorte de séparer la correction présente dans le calcul""" - regex = r"\[(.*?)\]" - calculEx = calcul['calcul'].replace('[', ' [').replace(']', '] ') - splitted = calculEx.split() - - if len(list(filter(lambda e: e.startswith("[") and e.endswith(']'), splitted))) == 0: - splitted.append('[]') - - inputs = [] - for i in range(len(splitted)): - c = splitted[i] - match = re.findall(regex, c) - if len(match) != 0: - correction = c[1:-1] - splitted[i] = f'[{len(inputs)}]' - inputs.append( - {'index': len(inputs), 'correction': correction, 'value': ""}) - - calculEx = ' '.join(splitted) - return {'calcul': calculEx, 'inputs': inputs} +def parseOut(calcul): + """Fait en sorte de séparer la correction présente dans le calcul""" + regex = r"\[(.*?)\]" + calculEx = calcul['calcul'].replace('[', ' [').replace(']', '] ') + splitted = calculEx.split() + + if len(list(filter(lambda e: e.startswith("[") and e.endswith(']'), splitted))) == 0: + splitted.append('[]') + + inputs = [] + for i in range(len(splitted)): + c = splitted[i] + match = re.findall(regex, c) + if len(match) != 0: + correction = c[1:-1] + if correction == "": + correction = None + splitted[i] = f'[{len(inputs)}]' + inputs.append( + {'index': len(inputs), 'correction': correction, 'value': ""}) + + calculEx = ' '.join(splitted) + return {'calcul': calculEx, 'inputs': inputs} def parseGeneratorOut(out: List[GeneratorOut]): - return [parseOut(c) for c in out] + return [parseOut(c) for c in out] + def getObjectKey(obj, key): if obj[key] == None: @@ -44,7 +48,9 @@ def getObjectKey(obj, key): def getCorrectionKey(obj, key): - return key if (obj[key] != False and obj['correction'] == False) else 'calcul' if(obj['calcul'] != False and obj['correction'] == False) else 'correction' if obj['correction'] != False else None + return key if (obj[key] != False and obj['correction'] == False) else 'calcul' if ( + obj['calcul'] != False and obj['correction'] == False) else 'correction' if obj[ + 'correction'] != False else None def parseCorrection(calc, replacer='...'): @@ -54,35 +60,37 @@ def parseCorrection(calc, replacer='...'): return calc -def generate_from_data(data, quantity, key, forcedCorrection=False): +def generate_from_data(data, quantity, key, forced_correction=False): locs = {} exec(data, {"random": random, "string": string, "sympy": sympy}, locs) try: main_func = locs['main'] - except: + except KeyError: return None main_result = main_func() default_object = {"calcul": False, 'pdf': False, 'csv': False, 'web': False, 'correction': False} # les valeurs par défaut # Si l'utilisateur n'a pas entré une valeur, elle est définie à False - + result_object = {**default_object, **main_result} object_key = getObjectKey(result_object, key) correction_key = getCorrectionKey(result_object, key) op_list = [] try: replacer = locs["CORRECTION_REPLACER"] - except: + except KeyError: replacer = '...' - + for i in range(quantity): main_result = main_func() main = {**default_object, **main_result} op_list.append({'calcul': parseCorrection(main[ - object_key], replacer) if (forcedCorrection or (key != 'web' and main['correction'] == False)) else main[object_key], "correction": main[correction_key]}) + object_key], replacer) if ( + forced_correction or (key != 'web' and main['correction'] == False)) else main[object_key], + "correction": main[correction_key]}) return op_list -def generate_from_path(path, quantity, key, forcedCorrection=False): +def generate_from_path(path, quantity, key, forced_correction=False): data = open(path, "r").read() - return generate_from_data(data, quantity, key, forcedCorrection) + return generate_from_data(data, quantity, key, forced_correction) diff --git a/backend/api/main.py b/backend/api/main.py index 53ff472..f932ee3 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -1,36 +1,29 @@ #import schemas.base -from services.database import generate_unique_code -from sqlmodel import SQLModel, Field, select -from services.password import get_password_hash -from sqlmodel import Session, select -from database.auth.crud import create_user_db -from services.auth import get_current_user_optional, jwt_required -from fastapi.openapi.utils import get_openapi -from database.auth.models import User, UserRead -from database.exercices.models import Exercice, ExerciceReadFull -from fastapi_pagination import add_pagination -from fastapi.responses import PlainTextResponse +from typing import List, Optional + +from fastapi import Depends, Request, status +from fastapi import FastAPI, HTTPException +from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError, ValidationError -from fastapi import FastAPI, HTTPException, Depends, Request, status, Header +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse from fastapi_jwt_auth import AuthJWT from fastapi_jwt_auth.exceptions import AuthJWTException -from fastapi.responses import JSONResponse -from typing import List, Optional, Sequence -from tortoise.contrib.pydantic import pydantic_model_creator -from fastapi import FastAPI, HTTPException, params -from tortoise import Tortoise -from fastapi.middleware.cors import CORSMiddleware -from tortoise.contrib.fastapi import register_tortoise -from pydantic import BaseModel, validator -from database.db import create_db_and_tables, get_session -from services.jwt import revoke_access, revoke_refresh -import routes.base -from redis import Redis -from fastapi.encoders import jsonable_encoder -import config +from fastapi_pagination import add_pagination from sqladmin import Admin, ModelView +from sqlmodel import SQLModel, Field +from sqlmodel import Session, select + +import config +import routes.base +from database.auth.crud import create_user_db +from database.auth.models import User, UserRead +from database.db import create_db_and_tables, get_session from database.db import engine -from fastapi.security import OAuth2PasswordBearer, HTTPBearer +from database.exercices.models import Exercice, ExerciceReadFull +from services.jwt import revoke_access, revoke_refresh +from services.password import get_password_hash + app = FastAPI(title="API Generateur d'exercices") origins = [ "http://localhost:8000", @@ -102,7 +95,7 @@ def test(test_1: str, test_2: str, test_3: str = Depends(t), test_4: str = Depen @app.exception_handler(RequestValidationError) @app.exception_handler(ValidationError) -async def validation_exception_handler(request, exc: RequestValidationError|ValidationError): +async def validation_exception_handler(request: Request, exc: RequestValidationError|ValidationError): errors = {} print(exc.errors()) for e in exc.errors(): diff --git a/backend/api/routes/auth/routes.py b/backend/api/routes/auth/routes.py index 7e55be9..feb8a89 100644 --- a/backend/api/routes/auth/routes.py +++ b/backend/api/routes/auth/routes.py @@ -1,15 +1,18 @@ from typing import List + from fastapi import APIRouter, Depends, HTTPException, status -from services.jwt import revoke_access -from services.password import get_password_hash, verify_password -from services.auth import get_current_clientId, get_current_user, get_current_user_optional, jwt_refresh_required -from database.auth.crud import change_user_uuid, check_unique_username, create_user_db, delete_user_db, update_password_db, update_user_db -from services.auth import authenticate_user -from database.auth.models import PasswordSet, User, UserEdit, UserRead, UserRegister -from pydantic import BaseModel from fastapi_jwt_auth import AuthJWT -from sqlmodel import Session,select +from pydantic import BaseModel +from sqlmodel import Session, select + +from database.auth.crud import change_user_uuid, check_unique_username, create_user_db, delete_user_db, \ + update_password_db, update_user_db, parse_user_rooms +from database.auth.models import PasswordSet, User, UserEdit, UserRead, UserRegister, UserEditRead from database.db import get_session +from services.auth import authenticate_user +from services.auth import get_current_clientId, get_current_user, get_current_user_optional, jwt_refresh_required +from services.password import get_password_hash, verify_password + router = APIRouter(tags=['Authentification']) @@ -22,8 +25,9 @@ class Token(BaseModel): def login_for_access_token(user: User = Depends(authenticate_user)): Authorize = AuthJWT() access_token = Authorize.create_access_token( - subject=str(user.clientId), fresh=True) - refresh_token = Authorize.create_refresh_token(subject=str(user.clientId)) + subject=str(user.clientId), fresh=True, user_claims={"username": user.username}) + refresh_token = Authorize.create_refresh_token(subject=str( + user.clientId), user_claims={"username": user.username}) return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"} @router.post('/register', response_model=Token) @@ -33,8 +37,9 @@ def register(user: UserRegister = Depends(UserRegister.as_form), Authorize: Auth raise HTTPException(status_code = status.HTTP_400_BAD_REQUEST,detail={'username_error': "Nom d'utilisateur indisponible"}) user = create_user_db(username, get_password_hash(user.password), db) access_token = Authorize.create_access_token( - subject=str(user.clientId)) - refresh_token = Authorize.create_refresh_token(subject=str(user.clientId)) + subject=str(user.clientId), user_claims={"username": user.username}) + refresh_token = Authorize.create_refresh_token(subject=str( + user.clientId), user_claims={"username": user.username}) return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"} @router.get('/users', response_model=List[UserRead]) @@ -42,7 +47,11 @@ def get_users(db: Session = Depends(get_session)): users = db.exec(select(User)).all() return users -@router.put('/user' , response_model=UserRead,) + +@router.get('/user', response_model=UserRead) +def get_user(user: User = Depends(get_current_user), db: Session = Depends(get_session)): + return {**user.dict(), "rooms": parse_user_rooms(user, db)} +@router.put('/user' , response_model=UserEditRead,) def update_user(user: UserEdit = Depends(UserEdit.as_form), clientId: str = Depends(get_current_clientId), db: Session = Depends(get_session)): user_obj = update_user_db(clientId, user, db) return user_obj @@ -58,8 +67,9 @@ def update_password(password: PasswordSet = Depends(PasswordSet.as_form), user: user_obj = change_user_uuid(user.id, db) access_token = Authorize.create_access_token( - subject=str(user_obj)) - refresh_token = Authorize.create_refresh_token(subject=str(user_obj)) + subject=str(user_obj), user_claims={"username": user.username}) + refresh_token = Authorize.create_refresh_token( + subject=str(user_obj), user_claims={"username": user.username}) return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"} @@ -82,5 +92,6 @@ def check_token(user: User = Depends(get_current_user_optional)): @router.post('/refresh') def refresh(Authorize: AuthJWT = Depends(jwt_refresh_required)): current_user = Authorize.get_jwt_subject() - new_access_token = Authorize.create_access_token(subject=current_user) + username = Authorize.get_raw_jwt()['username'] + new_access_token = Authorize.create_access_token(subject=current_user, user_claims={"username":username}) return {"access_token": new_access_token} diff --git a/backend/api/routes/exercices/routes.py b/backend/api/routes/exercices/routes.py index 5ece63b..dc2795a 100644 --- a/backend/api/routes/exercices/routes.py +++ b/backend/api/routes/exercices/routes.py @@ -1,28 +1,34 @@ -from pydantic import BaseModel +import csv +import io from enum import Enum from typing import List -from fastapi import APIRouter, Depends, Path, Query, UploadFile, HTTPException, status + +from fastapi import APIRouter, Depends, Query, UploadFile, HTTPException, status +from fastapi.responses import FileResponse, StreamingResponse +from fastapi_pagination.ext.sqlalchemy_future import paginate as p +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, ExerciceCreate, ExerciceEdit, ExerciceReadFull, ExercicesTagLink, Tag, TagCreate, TagRead, ExerciceRead +from database.exercices.crud import add_tags_db, check_exercice_author, check_private, check_tag_author, create_exo_db, \ + delete_exo_db, get_exo_dependency, clone_exo_db, remove_tag_db, serialize_exo, update_exo_db, get_tags_dependency +from database.exercices.models import Exercice, ExerciceCreate, ExerciceEdit, ExerciceReadFull, ExercicesTagLink, Tag, \ + TagCreate, TagRead, ExerciceRead +from generateur.generateur_csv import Csv_generator from services.auth import get_current_user, get_current_user_optional -from sqlmodel import Session, select, col -from database.exercices.crud import add_tags_db, check_exercice_author, check_private, check_tag_author, create_exo_db, delete_exo_db, get_exo_dependency, clone_exo_db, parse_exo_tags, remove_tag_db, serialize_exo, update_exo_db, get_tags_dependency from services.exoValidation import validate_file, validate_file_optionnal from services.io import add_fast_api_root, get_filename_from_path -from fastapi.responses import FileResponse -from sqlmodel import func -from fastapi_pagination import paginate ,Page from services.models import Page -from fastapi_pagination.ext.sqlalchemy_future import paginate as p router = APIRouter(tags=['exercices']) + class ExoType(str, Enum): - csv="csv" - pdf="pdf" - web="web" - + csv = "csv" + pdf = "pdf" + web = "web" + def filter_exo_by_tags(exos: List[tuple[Exercice, str]], tags: List[Tag]): valid_exos = [exo for exo, tag in exos if all( @@ -30,48 +36,53 @@ def filter_exo_by_tags(exos: List[tuple[Exercice, str]], tags: List[Tag]): return valid_exos -def queryFilters_dependency(search: str = "", tags: List[str] | None = Depends(get_tags_dependency), type: ExoType | None = Query(default = None)): +def queryFilters_dependency(search: str = "", tags: List[str] | None = Depends(get_tags_dependency), + type: ExoType | None = Query(default=None)): return search, tags, type @router.post('/exercices', response_model=ExerciceReadFull, status_code=status.HTTP_201_CREATED) -def create_exo(exercice: ExerciceCreate = Depends(ExerciceCreate.as_form), file: UploadFile = Depends(validate_file), user: User = Depends(get_current_user), db: Session = Depends(get_session)): +def create_exo(exercice: ExerciceCreate = Depends(ExerciceCreate.as_form), file: UploadFile = Depends(validate_file), + user: User = Depends(get_current_user), db: Session = Depends(get_session)): file_obj = file['file'].file._file file_obj.name = file['file'].filename exo_obj = create_exo_db(exercice=exercice, user=user, - exo_source=file_obj, supports=file['supports'],db=db) + exo_source=file_obj, supports=file['supports'], db=db) return serialize_exo(exo=exo_obj, user_id=user.id, db=db) @router.post('/clone/{id_code}', response_model=ExerciceReadFull) -def clone_exo(exercice: Exercice | None = Depends(check_private), user: User = Depends(get_current_user), db: Session = Depends(get_session)): +def clone_exo(exercice: Exercice | None = Depends(check_private), user: User = Depends(get_current_user), + db: Session = Depends(get_session)): if not exercice: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail={ - "Exercice introuvable"}) + "Exercice introuvable"}) exo_obj = clone_exo_db(exercice, user, db) if type(exo_obj) == str: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=exo_obj) return serialize_exo(exo=exo_obj, user_id=user.id, db=db) - -@router.get('/exercices/user', response_model=Page[ExerciceRead|ExerciceReadFull]) -def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tuple[str, List[int] | None, ExoType | None] = Depends(queryFilters_dependency), db: Session = Depends(get_session)): + +@router.get('/exercices/user', response_model=Page[ExerciceRead | ExerciceReadFull]) +def get_user_exercices(user: User = Depends(get_current_user), + queryFilters: tuple[str, List[int] | None, ExoType | None] = Depends(queryFilters_dependency), + db: Session = Depends(get_session)): search, tags, type = queryFilters - + statement = select(Exercice) statement = statement.where(Exercice.author_id == user.id) statement = statement.where(Exercice.name.startswith(search)) - + if type == ExoType.csv: statement = statement.where(Exercice.csv == True) if type == ExoType.pdf: statement = statement.where(Exercice.pdf == True) if type == ExoType.web: statement = statement.where(Exercice.web == True) - + for t in tags: - sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id==Exercice.id).where( + sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id == Exercice.id).where( ExercicesTagLink.tag_id == t).exists() statement = statement.where(sub) page = p(db, statement) @@ -81,14 +92,17 @@ def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tup return page -@router.get('/exercices/public', response_model=Page[ExerciceRead|ExerciceReadFull]) -def get_public_exercices(user: User | None = Depends(get_current_user_optional), queryFilters: tuple[str, List[int] | None] = Depends(queryFilters_dependency), db: Session = Depends(get_session)): +@router.get('/exercices/public', response_model=Page[ExerciceRead | ExerciceReadFull]) +def get_public_exercices(user: User | None = Depends(get_current_user_optional), + queryFilters: tuple[str, List[int] | None] = Depends(queryFilters_dependency), + db: Session = Depends(get_session)): search, tags, type = queryFilters if user is not None: statement = select(Exercice) statement = statement.where(Exercice.author_id != user.id) statement = statement.where(Exercice.private == False) + statement = statement.where(Exercice.origin_id == None) statement = statement.where(Exercice.name.startswith(search)) if type == ExoType.csv: @@ -97,19 +111,19 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional), statement = statement.where(Exercice.pdf == True) if type == ExoType.web: statement = statement.where(Exercice.web == True) - + for t in tags: - sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id==Exercice.id).where( - ExercicesTagLink.tag_id == t).exists() + sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id == Exercice.id).where( + ExercicesTagLink.tag_id == t).exists() statement = statement.where(sub) - + page = p(db, statement) print('¨PAGE', page) exercices = page.items page.items = [ serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices] return page - + else: statement = select(Exercice) statement = statement.where(Exercice.private == False) @@ -120,7 +134,7 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional), statement = statement.where(Exercice.pdf == True) if type == ExoType.web: statement = statement.where(Exercice.web == True) - + page = p(db, statement) exercices = page.items page.items = [ @@ -129,12 +143,14 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional), @router.get('/exercice/{id_code}', response_model=ExerciceReadFull) -def get_exercice(exo: Exercice = Depends(check_private), user: User | None = Depends(get_current_user_optional), db: Session = Depends(get_session)): +def get_exercice(exo: Exercice = Depends(check_private), user: User | None = Depends(get_current_user_optional), + db: Session = Depends(get_session)): return serialize_exo(exo=exo, user_id=getattr(user, 'id', None), db=db) @router.put('/exercice/{id_code}', response_model=ExerciceReadFull) -def update_exo(file: UploadFile = Depends(validate_file_optionnal), exo: Exercice = Depends(check_exercice_author), exercice: ExerciceEdit = Depends(ExerciceEdit.as_form), db: Session = Depends(get_session)): +def update_exo(file: UploadFile = Depends(validate_file_optionnal), exo: Exercice = Depends(check_exercice_author), + exercice: ExerciceEdit = Depends(ExerciceEdit.as_form), db: Session = Depends(get_session)): if exo is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable') @@ -151,7 +167,8 @@ def update_exo(file: UploadFile = Depends(validate_file_optionnal), exo: Exercic @router.delete('/exercice/{id_code}') -def delete_exercice(exercice: Exercice | bool | None = Depends(check_exercice_author), db: Session = Depends(get_session)): +def delete_exercice(exercice: Exercice | bool | None = Depends(check_exercice_author), + db: Session = Depends(get_session)): if exercice is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Exercice introuvable") @@ -162,23 +179,24 @@ def delete_exercice(exercice: Exercice | bool | None = Depends(check_exercice_au return {'detail': 'Exercice supprimé avec succès'} - class NewTags(BaseModel): exo: ExerciceReadFull tags: list[TagRead] - + @router.post('/exercice/{id_code}/tags', response_model=NewTags, tags=['tags']) -def add_tags(tags: List[TagCreate], exo: Exercice | None = Depends(get_exo_dependency), db: Session = Depends(get_session), user: User = Depends(get_current_user)): +def add_tags(tags: List[TagCreate], exo: Exercice | None = Depends(get_exo_dependency), + db: Session = Depends(get_session), user: User = Depends(get_current_user)): if exo is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable') exo_obj, new = add_tags_db(exo, tags, user, db) - return {"exo":serialize_exo(exo=exo_obj, user_id=user.id, db=db), "tags": new} + return {"exo": serialize_exo(exo=exo_obj, user_id=user.id, db=db), "tags": new} @router.delete('/exercice/{id_code}/tags/{tag_id}', response_model=ExerciceReadFull, tags=['tags']) -def remove_tag(exo: Exercice | None = Depends(get_exo_dependency), tag: Tag | None | bool = Depends(check_tag_author), db: Session = Depends(get_session)): +def remove_tag(exo: Exercice | None = Depends(get_exo_dependency), tag: Tag | None | bool = Depends(check_tag_author), + db: Session = Depends(get_session)): if exo is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable') @@ -194,7 +212,7 @@ def remove_tag(exo: Exercice | None = Depends(get_exo_dependency), tag: Tag | No @router.get('/exercice/{id_code}/exo_source') -async def get_exo_source(exo: Exercice = Depends(check_exercice_author), db: Session = Depends(get_session)): +async def get_exo_source(exo: Exercice = Depends(check_exercice_author)): if exo is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable') @@ -203,9 +221,30 @@ async def get_exo_source(exo: Exercice = Depends(check_exercice_author), db: Ses detail='Cet exercice ne vous appartient pas') path = add_fast_api_root(exo.exo_source) filename = get_filename_from_path(path) - return FileResponse(path, headers={'content-disposition': 'attachment;filename='+filename}) + return FileResponse(path, headers={'content-disposition': 'attachment;filename=' + filename}) @router.get('/tags', response_model=List[TagRead], tags=['tags']) def get_tags(user: User = Depends(get_current_user), db: Session = Depends(get_session)): return user.tags + + + + + +@router.get('/generator/csv/{id_code}') +async def generate_csv(*, exo: Exercice | None = Depends(get_exo_dependency), filename: str): + if exo.csv is False: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, + detail='Impossible de générer cet exercice sur dans ce format') + + source_path = add_fast_api_root(exo.exo_source) + consigne = exo.consigne + + buffer = io.StringIO() + writer = csv.writer(buffer, delimiter=',', + quotechar=',', quoting=csv.QUOTE_MINIMAL, dialect='excel') # mettre | comme sep un jour + Csv_generator(source_path, 10, 10, 12, consigne, writer) + + return StreamingResponse(iter([buffer.getvalue()]), + headers={"Content-Disposition": f'attachment;filename={filename}'}, media_type='text/csv') diff --git a/backend/api/routes/room/consumer.py b/backend/api/routes/room/consumer.py index c77183c..84eaf8f 100644 --- a/backend/api/routes/room/consumer.py +++ b/backend/api/routes/room/consumer.py @@ -1,29 +1,42 @@ +from typing import Any, TYPE_CHECKING, Callable + from fastapi.websockets import WebSocket -from services.websocket import Consumer -from typing import Any, TYPE_CHECKING -from database.room.models import Room, Member, MemberRead, Waiter from sqlmodel import Session -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, disconnect_member, create_anonymous_member, create_anonymous_waiter, create_user_member, create_user_waiter, get_or_create_member, get_waiter, accept_waiter, leave_room, refuse_waiter, check_room + 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, \ + 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 +from database.room.models import Room, Member, MemberRead, Waiter +from services.websocket import Consumer + if TYPE_CHECKING: - from routes.room.routes import RoomManager - + from routes.room.routes import RoomManager + + class RoomConsumer(Consumer): - + def __init__(self, ws: WebSocket, room: Room, manager: "RoomManager", db: Session): self.room = room self.ws = ws self.manager = manager self.db = db self.member = None - + # WS Utilities + async def send(self, payload: Any | Callable): + if callable(payload): + payload = payload(self.member) + return await super().send(payload) async def connect(self): await self.ws.accept() - async def direct_send(self, type: str, payload: Any): - await self.ws.send_json({'type': type, "data": payload}) + async def direct_send(self, type: str, payload: Any, code: int | None = None): + sending = {'type': type, "data": payload, } + if code != None: + sending["code"] = code + await self.ws.send_json({'type': type, "data": payload, }) async def send_to_admin(self, type: str, payload: Any, exclude: bool = False): await self.manager.send_to_admin(self.room.id_code, {'type': type, "data": payload}) @@ -32,7 +45,8 @@ class RoomConsumer(Consumer): await self.manager.send_to(self.room.id_code, member_id, {'type': type, "data": payload}) async def broadcast(self, type, payload, exclude=False): - await self.manager.broadcast({"type": type, "data": payload}, self.room.id_code, exclude=[exclude == True and self]) + await self.manager.broadcast({"type": type, "data": payload}, self.room.id_code, + exclude=[exclude == True and self]) def add_to_group(self): self.manager.add(self.room.id_code, self) @@ -40,13 +54,20 @@ class RoomConsumer(Consumer): async def connect_self(self): if isinstance(self.member, Member): connect_member(self.member, self.db) - await self.broadcast(type="connect", payload={"member": serialize_member(self.member)}, exclude=True) + await self.manager.broadcast(lambda m: {"type": "connect", "data": { + "member": serialize_member(self.member, admin=m.is_admin, m2=m)}}, self.room.id_code, exclude=[self]) + # await self.broadcast(type="connect", payload={"member": serialize_member(self.member)}, exclude=True) async def disconnect_self(self): if isinstance(self.member, Member): + ''' self.db.expire(self.member) + self.db.refresh(self.member) ''' disconnect_member(self.member, self.db) if self.member.waiting is False: - await self.broadcast(type="disconnect", payload={"member": serialize_member(self.member)}) + await self.manager.broadcast(lambda m: {"type": "disconnect", "data": { + "member": serialize_member(self.member, admin=m.is_admin, m2=m)}}, self.room.id_code, + exclude=[self]) + # await self.broadcast(type="disconnect", payload={"member": serialize_member(self.member)}) else: await self.send_to_admin(type="disconnect_waiter", payload={"waiter": serialize_member(self.member)}) @@ -55,11 +76,12 @@ class RoomConsumer(Consumer): self.member = member await self.connect_self() self.add_to_group() - clientId = self.member.anonymous.clientId if self.member.anonymous is not None else "" - await self.direct_send(type="loggedIn", payload={"member": {**serialize_member(self.member), 'clientId': str(clientId)}}) + await self.direct_send(type="loggedIn", + payload={"member": {**serialize_member(self.member, private=True, m2=self.member)}}) + + async def send_error(self, msg, code: int = 400): + await self.direct_send(type="error", payload={"msg": msg, "code": code}) - async def send_error(self, msg): - await self.direct_send(type="error", payload={"msg": msg}) # Conditions async def isAdminReceive(self): @@ -71,38 +93,40 @@ class RoomConsumer(Consumer): def isAdmin(self): return self.member is not None and self.member.is_admin == True - + async def isMember(self): + print('S', self.member, self.ws, self.ws.state, self.ws.application_state.__str__()) if self.member is None: await self.send_error("Vous n'êtes connecté à aucune salle") return self.member is not None and self.member.waiting == False - + def isWaiter(self): return self.member is not None and self.member.waiting == True + # Received Events - + @Consumer.event('login') async def login(self, token: str | None = None, reconnect_code: str | None = None): if reconnect_code is None and token is None: await self.direct_send(type="error", payload={"msg": "Veuillez spécifier une méthode de connection"}) return - + print("login", token) if token is not None: member = get_member_from_token(token, self.room.id, self.db) + print('MEMBER', member) if member == False: - await self.send_error("Token expired") + await self.send_error("Token expired", code=422) return if member is None: - await self.send_error("Utilisateur introuvable dans cette salle") + await self.send_error("Utilisateur introuvable dans cette salle", code=401) return elif reconnect_code is not None: member = get_member_from_reconnect_code( reconnect_code, self.room.id, db=self.db) if member is None: - await self.send_error("Utilisateur introuvable dans cette salle") + await self.send_error("Utilisateur introuvable dans cette salle", code=401) return - await self.loginMember(member) @Consumer.event('join') @@ -116,9 +140,9 @@ class RoomConsumer(Consumer): if user is False: await self.send_error("Token expired") return - + userInRoom = check_user_in_room(user.id, self.room.id, self.db) - + if userInRoom is not None: await self.loginMember(userInRoom) return @@ -127,6 +151,9 @@ class RoomConsumer(Consumer): user=user, room=self.room, waiting=self.room.public is False, db=self.db) elif username is not None: + if len(username) < 4 or len(username) > 15: + await self.send_error("Nom d'utilisateur invalide ou indisponible") + return anonymous = create_anonymous(username, self.room, self.db) if anonymous is None: await self.send_error("Nom d'utilisateur invalide ou indisponible") @@ -134,61 +161,69 @@ class RoomConsumer(Consumer): waiter = create_member( anonymous=anonymous, room=self.room, waiting=self.room.public is False, db=self.db) - + self.member = waiter self.add_to_group() - + if self.room.public is False: await self.direct_send(type="waiting", payload={"waiter": serialize_member(self.member)}) await self.send_to_admin(type="waiter", payload={"waiter": serialize_member(self.member)}) else: - await self.broadcast(type="joined", payload={"member": serialize_member(self.member)}, exclude=True) - await self.direct_send(type="accepted", payload={"member": serialize_member(self.member)}) - + await self.manager.broadcast( + lambda m: {"type": "joined", "data": {"member": serialize_member(self.member, admin=m.is_admin, m2=m)}}, + self.room.id_code) + # await self.broadcast(type="joined", payload={"member": serialize_member(self.member)}, exclude=True) + await self.direct_send(type="accepted", + payload={"member": serialize_member(self.member, private=True, m2=self.member)}) @Consumer.event('accept', conditions=[isAdminReceive]) async def accept(self, waiter_id: str): waiter = get_waiter(waiter_id, self.db) if waiter is None: - await self.send_error("Utilisateur en list d'attente introuvable") + await self.send_error("Utilisateur en liste d'attente introuvable") return member = accept_waiter(waiter, self.db) - await self.send_to(type="accepted", payload={"member": serialize_member(member)}, member_id=waiter_id) - await self.broadcast(type="joined", payload={"member": serialize_member(member)}) + await self.send_to(type="accepted", payload={"member": serialize_member(member, private=True, m2=member)}, + member_id=waiter_id) + await self.manager.broadcast( + lambda m: {"type": "joined", + "data": {"member": serialize_member(member, admin=m.is_admin, m2=m)}}, + self.room.id_code) + # await self.broadcast(type="joined", payload={"member": serialize_member(member)}) @Consumer.event('refuse', conditions=[isAdminReceive]) - async def accept(self, waiter_id: str): + async def refuse(self, waiter_id: str): waiter = get_waiter(waiter_id, self.db) - member = refuse_waiter(waiter, self.db) + refuse_waiter(waiter, self.db) await self.send_to(type="refused", payload={'waiter_id': waiter_id}, member_id=waiter_id) - await self.direct_send(type="successfullyRefused", payload= {"waiter_id": waiter_id}) - + await self.direct_send(type="successfullyRefused", payload={"waiter_id": waiter_id}) + @Consumer.event('ping_room') async def proom(self): await self.broadcast(type='ping', payload={}, exclude=True) - + @Consumer.event('sub_parcours') async def sub_parcours(self, parcours_id: str): if isinstance(self.member, Member) and self.member.waiting == False: self.manager.add(parcours_id, self) - + @Consumer.event('unsub_parcours') async def unsub_parcours(self, parcours_id: str): if isinstance(self.member, Member) and self.member.waiting == False: self.manager.remove(parcours_id, self) - + @Consumer.event('set_name', conditions=[isAdminReceive]) async def change_name(self, name: str): if len(name) < 20: - self.room = change_room_name(self.room,name, self.db) + self.room = change_room_name(self.room, name, self.db) print('SENDING') await self.broadcast(type="new_name", payload={"name": name}) - + return await self.send_error('Nom trop long (max 20 character)') - + @Consumer.event('set_visibility', conditions=[isAdminReceive]) - async def change_name(self, public: bool): + async def change_visibility(self, public: bool): self.room = change_room_status(self.room, public, self.db) await self.broadcast(type="new_visibility", payload={"public": public}) @@ -197,7 +232,6 @@ class RoomConsumer(Consumer): await self.direct_send(type="error", payload={"msg": "Vous n'êtes connecté à aucune salle"}) return self.member is not None - @Consumer.event('leave', conditions=[isMember]) async def leave(self): if self.member.is_admin is True: @@ -205,7 +239,7 @@ class RoomConsumer(Consumer): return member_obj = serialize_member(self.member) leave_room(self.member, self.db) - + await self.direct_send(type="successfully_leaved", payload={}) await self.broadcast(type='leaved', payload={"member": member_obj}) self.member = None @@ -219,7 +253,7 @@ class RoomConsumer(Consumer): if member.is_admin is True: await self.send_error("Vous ne pouvez pas bannir un administrateur") return - + member_serialized = serialize_member(member) leave_room(member, self.db) await self.send_to(type="banned", payload={}, member_id=member.id_code) @@ -227,10 +261,11 @@ class RoomConsumer(Consumer): # Sending Events - @Consumer.sending(['connect', "disconnect", "joined"], conditions=[isMember]) + @Consumer.sending(["joined"], conditions=[isMember]) def joined(self, member: MemberRead): + if self.member.id_code == member.id_code: - raise ValueError("") # Prevent from sending event + raise ValueError("") # Prevent from sending event if self.member.is_admin == False: member.reconnect_code = "" return {"member": member} @@ -239,6 +274,11 @@ class RoomConsumer(Consumer): def waiter(self, waiter: Waiter): return {"waiter": waiter} + @Consumer.sending('accepted') + def accepted(self, member: MemberRead): + self.db.refresh(self.member) + return {"member": member} + @Consumer.sending("refused", conditions=[isWaiter]) def refused(self, waiter_id: str): self.member = None @@ -249,6 +289,7 @@ class RoomConsumer(Consumer): def banned(self): self.member = None self.manager.remove(self.room.id, self) + self.ws.close() return {} @Consumer.sending('ping', conditions=[isMember]) @@ -256,6 +297,6 @@ class RoomConsumer(Consumer): return {} async def disconnect(self): + print('DISCONNECTED', self.member) self.manager.remove(self.room.id, self) await self.disconnect_self() - diff --git a/backend/api/routes/room/manager.py b/backend/api/routes/room/manager.py index 5545ca3..1f0dac7 100644 --- a/backend/api/routes/room/manager.py +++ b/backend/api/routes/room/manager.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Dict, List +from typing import TYPE_CHECKING, Dict, List, Callable, Any from starlette.websockets import WebSocketState if TYPE_CHECKING: from routes.room.consumer import RoomConsumer @@ -16,7 +16,8 @@ class RoomManager: self.active_connections[group].append(member) async def _send(self, connection: "RoomConsumer", message, group: str): - if connection.ws.application_state == WebSocketState.DISCONNECTED: + print("STATE", connection.ws.client_state.__str__()) + if connection.ws.application_state == WebSocketState.DISCONNECTED or connection.ws.client_state == WebSocketState.DISCONNECTED: self.remove(group, connection) elif connection.ws.application_state == WebSocketState.CONNECTED: await connection.send(message) @@ -24,14 +25,17 @@ class RoomManager: def remove(self, group: str, member: "RoomConsumer"): if group in self.active_connections: if member in self.active_connections[group]: + print("remoied") self.active_connections[group].remove(member) - async def broadcast(self, message, group: str, exclude: list["RoomConsumer"] = []): - print('BROADCaST', message) + async def broadcast(self, message: Any | Callable, group: str, conditions: list[Callable] = [], exclude: list["RoomConsumer"] = [], ): + print('BROADCaST', message, self.active_connections) + + if group in self.active_connections: for connection in list(set(self.active_connections[group])): - # print(connection) - if connection not in exclude: + print(connection, connection.ws.state, connection.ws.client_state, connection.ws.application_state) + if connection not in exclude and all(f(connection) for f in conditions ): await self._send(connection, message, group) async def send_to(self, group, id_code, msg): @@ -44,6 +48,6 @@ class RoomManager: async def send_to_admin(self, group, msg): if group in self.active_connections: members = [c for c in self.active_connections[group] - if c.member.is_admin == True] + if c.member is not None and c.member.is_admin == True] for m in members: await self._send(m, msg, group) diff --git a/backend/api/routes/room/routes.py b/backend/api/routes/room/routes.py index 4c2cf9a..c39a2c6 100644 --- a/backend/api/routes/room/routes.py +++ b/backend/api/routes/room/routes.py @@ -1,26 +1,52 @@ -from services.database import generate_unique_code -from services.io import add_fast_api_root -from generateur.generateur_main import generate_from_path, parseGeneratorOut, parseOut -from database.exercices.models import Exercice -from database.room.crud import CorrigedChallenge, change_correction, corrige_challenge, 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 +from typing import List, Optional -from pydantic import BaseModel -from typing import Any, Callable, Dict, List, Optional from fastapi import APIRouter, Depends, WebSocket, status, Query, Body -from config import ALGORITHM, SECRET_KEY -from database.auth.crud import get_user_from_clientId_db +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 sqlmodel import Session, col, select -from database.room.models import Challenge, ChallengeRead, Challenges, CorrigedGeneratorOut, Member, Note, Parcours, ParcoursCreate, ParcoursRead, ParcoursReadShort, ParsedGeneratorOut, Room, RoomConnectionInfos, RoomCreate, RoomAndMember, RoomInfo, TmpCorrection +from database.exercices.models import Exercice +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, \ + 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 database.auth.crud import get_user_from_token -from services.websocket import Consumer -from services.misc import noteOn20, stripKeyDict +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, \ + 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 + router = APIRouter(tags=["room"]) manager = RoomManager() @@ -28,6 +54,7 @@ manager = RoomManager() def get_manager(): return manager + @router.post('/room', response_model=RoomConnectionInfos) def create_room(room: RoomCreate, username: Optional[str] = Query(default=None, max_length=20), user: User | None = Depends(get_current_user_optional), db: Session = Depends(get_session)): room_obj = create_room_db(room=room, user=user, username=username, db=db) @@ -39,36 +66,42 @@ def get_room_route(room: Room = Depends(get_room), member: Member = Depends(get_ return serialize_room(room, member, db) - @router.post('/room/{room_id}/parcours', response_model=ParcoursRead) async def create_parcours(*, parcours: ParcoursCreate, room_id: str, member: Member = Depends(check_admin), m: RoomManager = Depends(get_manager), db: Session = Depends(get_session)): parcours_obj = create_parcours_db(parcours, member.room_id, db) if type(parcours_obj) == str: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=parcours_obj) - await m.broadcast({"type": "add_parcours", "data": {"parcours": ParcoursReadShort(**parcours_obj.dict(exclude_unset=True)).dict()}}, room_id) - return parcours_obj + await m.broadcast({"type": "add_parcours", + "data": {"parcours": ParcoursReadShort(**parcours_obj.dict(exclude_unset=True)).dict()}}, + room_id) + + return serialize_parcours(parcours_obj, member, db) @router.get('/room/{room_id}/parcours/{parcours_id}', response_model=ParcoursRead) async def get_parcours_route(*, parcours: Parcours = Depends(get_parcours), member: Member = Depends(get_member_dep), db: Session = Depends(get_session)): - if member.is_admin == False: - return {**parcours.dict(), "challenges": [Challenges(**{**chall.dict(), "challenger": member.id_code, "canCorrige": chall.data != []}) for chall in parcours.challenges if chall.challenger.id_code == member.id_code]} - - if member.is_admin == True: - return {**parcours.dict(), "challenges": [Challenges(**{**chall.dict(), "challenger": member.id_code, "canCorrige": chall.data != []}) for chall in parcours.challenges]} + return serialize_parcours(parcours, member, db) -@router.put('/room/{room_id}/parcours/{parcours_id}', response_model=ParcoursRead, dependencies=[Depends(check_admin)]) -async def update_parcours(*, room_id: str, parcours: ParcoursCreate, parcours_old: Parcours = Depends(get_parcours), m: RoomManager = Depends(get_manager), db: Session = Depends(get_session)): - parcours_obj = update_parcours_db(parcours, parcours_old, db) +@router.put('/room/{room_id}/parcours/{parcours_id}', response_model=ParcoursRead) +async def update_parcours(*, room_id: str, parcours: ParcoursCreate, member: Member = Depends(check_admin), + parcours_old: Parcours = Depends(get_parcours), m: RoomManager = Depends(get_manager), + db: Session = Depends(get_session)): + parcours_obj, update_challenges = update_parcours_db( + parcours, parcours_old, db) if type(parcours_obj) == str: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=parcours_obj) - short = ParcoursReadShort( - **parcours_obj.dict(exclude_unset=True), id_code=parcours_obj.id_code) - await m.broadcast({"type": "update_parcours", "data": {"parcours": short.dict()}}, room_id) - return parcours_obj + + await m.broadcast(lambda m: {"type": "update_parcours", + "data": {"parcours": serialize_parcours_short(parcours_obj, m, db).dict()}}, room_id) + await m.broadcast({"type": "edit_parcours", "data": { + "parcours": ParcoursReadUpdate(**parcours_obj.dict(), update_challenges=update_challenges).dict()}}, + parcours_old.id_code) + + return serialize_parcours(parcours_obj, member, db) + return {**parcours_obj.dict()} @router.delete('/room/{room_id}/parcours/{parcours_id}', dependencies=[Depends(check_admin)]) @@ -82,51 +115,133 @@ class Exos(BaseModel): exercice: Exercice quantity: int + @router.get('/room/{room_id}/challenge/{parcours_id}') -def challenge_route(parcours_id: str, exercices: List[Exos] = Depends(get_exercices), member: Member = Depends(get_member_dep), db: Session = Depends(get_session)): +def challenge_route(parcours: Parcours = Depends(get_parcours), exercices: List[Exos] = Depends(get_exercices), + member: Member = Depends(get_member_dep), db: Session = Depends(get_session)): + print('GENERATE', exercices) correction = [parseGeneratorOut(generate_from_path(add_fast_api_root( e['exercice'].exo_source), e['quantity'], "web")) for e in exercices] - sending = [[{**c, 'inputs': [stripKeyDict(i, "correction") - for i in c['inputs']]} for c in e] for e in correction] - tmpCorr = create_tmp_correction(correction, parcours_id, member, db) - return {'challenge': sending, "id_code": tmpCorr.id_code} + infos = [{"name": e["exercice"].name, "consigne": e['exercice'].consigne, "id_code": e['exercice'].id_code} + for e in exercices] + + sending = [{"exo": ei, "data": [{**c, 'inputs': [stripKeyDict(i, "correction") + for i in c['inputs']]} for c in e]} for e, ei in + zip(correction, infos)] + + corriged = [{"exo": ei, "data": e} for e, ei in zip(correction, infos)] + tmpCorr = create_tmp_correction(corriged, parcours.id_code, member, db) + + return {'challenge': sending, "id_code": tmpCorr.id_code, + 'parcours': {"name": parcours.name, 'time': parcours.time, "max_mistakes": parcours.max_mistakes, + 'id_code': parcours.id_code}} @router.post('/room/{room_id}/challenge/{parcours_id}/{correction_id}', response_model=ChallengeRead) -async def send_challenge(*, challenge: List[List[ParsedGeneratorOut]], correction: TmpCorrection = Depends(get_correction), time: int = Body(), db: Session = Depends(get_session), m: RoomManager = Depends(get_manager),): +async def send_challenge(*, challenge: List[CorrectionData], correction: TmpCorrection = Depends(get_correction), + time: int = Body(), db: Session = Depends(get_session), + m: RoomManager = Depends(get_manager), ): parcours = correction.parcours member = correction.member data = corrige_challenge(challenge, correction) + if data is None: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail={"challenge_error":"Object does not correspond to correction"}) - chall = create_challenge(**data, challenger=member, - parcours=parcours, time=time, db=db) - - await m.broadcast({"type": "challenge", "data": Challenges(**{**chall.dict(), "challenger": member.id_code, "canCorrige": chall.data != []}).dict()}, parcours.id_code) + status_code=status.HTTP_400_BAD_REQUEST, + detail={"challenge_error": "Object does not correspond to correction"}) + chall, challenger = create_challenge(**data, challenger=member, + parcours=parcours, time=time, db=db) + print('CHALLENGE', chall) + await m.broadcast({"type": "challenge", "data": ChallengeInfo( + challenger={"name": member.user.username if member.user_id != None else member.anonymous.username, + "id_code": member.id_code}, + challenges=[Challenges(**{**chall.dict(), "canCorrige": chall.data != [], })]).dict()}, parcours.id_code, + conditions=[lambda c: c.member.is_admin or c.member.id_code == member.id_code]) + #TODO : Envoyer que à ceux d'après + await m.broadcast(lambda m: {"type": "newRanks", "data": {"rank": getMemberRank(m, correction.parcours, db), + "avgRank": getMemberAvgRank(m, correction.parcours, db)}}, + parcours.id_code) + print('CHALLENGE', chall) + rank, avgRank = getRank( + challenger, parcours, db), getAvgRank(challenger, parcours, db) + print('RANKS', rank, avgRank) + if rank <= 3 or avgRank <= 3: + await m.broadcast({"type": "newTops", "data": { + "tops": getTops(correction.parcours, db), + "avgTops": getAvgTops(correction.parcours, db), + }}, parcours.id_code) + print('CHALLENGE', chall) db.delete(correction) + returnValue = {**chall.dict(), 'validated': chall.mistakes <= correction.parcours.max_mistakes} db.commit() - return chall + return returnValue + # return {**chall.dict(), 'validated': chall.mistakes <= correction.parcours.max_mistakes} -@router.get('/room/{room_id}/challenge/{parcours_id}/{challenge_id}', response_model=ChallengeRead, dependencies=[Depends(get_member_dep)]) -async def challenge_read(*, challenge: Challenge = Depends(get_challenge)): - return challenge +class ParcoursInfo(BaseModel): + name: str + time: int + # validate: int + id_code: str -@router.put('/room/{room_id}/challenge/{parcours_id}/{challenge_id}', response_model=ChallengeRead, dependencies=[Depends(check_admin)]) -async def corrige(*, correction: List[List[CorrigedGeneratorOut]], challenge: Challenge = Depends(get_challenge), db: Session = Depends(get_session), m: RoomManager = Depends(get_manager),): +class Chall(BaseModel): + challenge: Challenge + parcours: ParcoursInfo + + +# response_model=ChallengeRead +@router.get('/room/{room_id}/correction/{challenge_id}', dependencies=[Depends(get_member_dep)]) +async def challenge_read(*, challenge: Challenge = Depends(get_challenge), db: Session = Depends(get_session)): parcours = challenge.parcours - member = challenge.challenger + + member = db.exec(select(Member).where( + Member.id == challenge.challenger_mid)).first() + challenger = ChallengerFromChallenge(challenge, db) + obj = member.user if member.user_id is not None else member.anonymous + return {**ChallengeRead(**challenge.dict()).dict(), "challenger": {"name": obj.username}, + 'parcours': {"name": parcours.name, 'time': parcours.time, "max_mistakes": parcours.max_mistakes, + 'id_code': parcours.id_code}} + + +# response_model=ChallengeRead + + +@router.put('/room/{room_id}/correction/{challenge_id}', dependencies=[Depends(check_admin)]) +async def corrige(*, correction: List[CorrigedData] = Body(), challenge: Challenge = Depends(get_challenge), + db: Session = Depends(get_session), m: RoomManager = Depends(get_manager), ): data = change_correction(correction, challenge) if data is None: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail={"correction_error": "Object does not correspond to challenge"}) - challenge = change_challenge(challenge, data, db) + status_code=status.HTTP_400_BAD_REQUEST, + detail={"correction_error": "Object does not correspond to challenge"}) - await m.broadcast({"type": "challenge_change", "data": Challenges(**{**challenge.dict(), "challenger": member.id_code, "canCorrige": challenge.data != []}).dict()}, parcours.id_code) - return challenge + parcours = challenge.parcours + member = db.exec(select(Member).where( + Member.id == challenge.challenger_mid)).first() + challenge, challenger = change_challenge(challenge, data, db) + obj = member.user if member.user_id is not None else member.anonymous + + await m.broadcast(lambda m: {"type": "newRanks", "data": {"rank": getMemberRank(m, parcours, db), + "avgRank": getMemberAvgRank(m, parcours, db)}}, + parcours.id_code) + + rank, avgRank = getRank( + challenger, parcours, db), getAvgRank(challenger, parcours, db) + print('Rank', rank, avgRank) + if rank <= 3 or avgRank <= 3: + await m.broadcast({"type": "newTops", "data": { + "tops": getTops(parcours, db), + "avgTops": getAvgTops(parcours, db), + }}, parcours.id_code) + + await m.broadcast({"type": "challenge_change", + "data": {"challenge": Challenges(**challenge.dict()).dict(), "member": member.id_code}}, + parcours.id_code, conditions=[lambda m: m.member.is_admin or m.member.id_code == member.id_code]) + + return {**ChallengeRead(**challenge.dict()).dict(), "challenger": {"name": obj.username}} + return {**ChallengeRead.from_orm(challenge).dict(), "challenger": {"name": obj.username, }} @router.websocket('/ws/room/{room_id}') diff --git a/backend/api/services/websocket.py b/backend/api/services/websocket.py index 5ce825b..bb1a220 100644 --- a/backend/api/services/websocket.py +++ b/backend/api/services/websocket.py @@ -3,6 +3,7 @@ from pydantic import validate_arguments, BaseModel from fastapi.websockets import WebSocketDisconnect, WebSocket from pydantic.error_wrappers import ValidationError import inspect +from starlette.websockets import WebSocketState def make_event_decorator(eventsDict): def _(name: str | List, conditions: List[Callable | bool] = []): @@ -68,8 +69,11 @@ class Consumer: await self.ws.send_json({"type": "error", "data": {"detail": [{ers['loc'][-1]: ers['msg']} for ers in errors]}}) async def send(self, payload): + ''' if self.ws.state == WebSocketState.DISCONNECTED: + return ''' type = payload.get('type', None) - #print('TYPE', type, self.member) + print('TYPE', type, self.member) + if type is not None: event_wrapper = self.sendings.get(type, None) if event_wrapper is not None: @@ -85,6 +89,7 @@ class Consumer: try: validated_payload = model(self=self, **data) except ValidationError as e: + print("ERROR", e) await self.ws.send_json({"type": "error", "data": {"msg": "Oops there was an error"}}) return @@ -133,4 +138,5 @@ class Consumer: data = await self.ws.receive_json() await self.receive(data) except WebSocketDisconnect: + print('DISCONNECTION') await self.disconnect() diff --git a/backend/api/tests/test_exos.py b/backend/api/tests/test_exos.py index 4536983..c9c5a88 100644 --- a/backend/api/tests/test_exos.py +++ b/backend/api/tests/test_exos.py @@ -116,7 +116,7 @@ def test_clone(client: TestClient): print(rr.json()) assert rr.status_code == 200 assert 'id_code' in rr.json() - assert {**rr.json(), 'id_code': None} == {'name': 'test_exo', 'consigne': 'consigne', 'private': False, 'id_code': None, 'author': {'username': 'lilian2'}, 'original': {"id_code": id_code, "name": create['name']}, 'tags': [], 'exo_source': 'test.py', 'supports': { + assert {**rr.json(), 'id_code': None} == {'name': 'test_exo', 'consigne': 'consigne', 'private': False, 'id_code': None, 'author': {'username': 'lilian2'}, 'original': {"id_code": id_code, "name": create['name'], 'author': 'lilian'}, 'tags': [], 'exo_source': 'test.py', 'supports': { 'pdf': False, 'csv': True, 'web': True}, 'examples': {'type': 'csv', 'data': [{'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}]}, 'is_author': True} @@ -233,22 +233,25 @@ class Tags(BaseModel): def test_add_tags(client: TestClient, name='name', tags: List[Tags] = [{'label': "name", 'color': "#ff0000", - 'id_code': "tag_id"}], user=None): + 'id_code': None}], user=None, exo =None): if user == None: token = test_register(client, username="lilian")['access'] user = {"token": token, 'username': "lilian"} else: token = user['token'] - - exo = test_create(client, name=name, user=user) - id_code = exo['id_code'] + if exo == None: + exo = test_create(client, name=name, user=user) + id_code = exo['id_code'] + else: + id_code = exo['id_code'] r = client.post(f'/exercice/{id_code}/tags', json=tags, headers={'Authorization': 'Bearer ' + token}) - print(r.json()) + print("DATA", tags, "\n\n",r.json()) data = r.json() + labels = [l['label'] for l in tags] assert r.status_code == 200 - assert {**data, "tags": [{**t, "id_code": None} - for t in data['tags']]} == {**exo, 'tags': [*exo['tags'], *[{**t, 'id_code': None} for t in tags]]} + assert {'exo': {**data['exo'], 'tags': [{**t, "id_code": "test"} for t in data['exo']['tags'] if t['id_code'] != None]}, "tags": [{**t, "id_code": "test"} + for t in data['tags'] if t['id_code'] != None]} == {"exo": {**exo, 'tags': [{**t, "id_code": "test"} for t in tags]}, 'tags': [*exo['tags'], *[{**t, 'id_code': "test"} for t in tags if t['id_code'] == None]]} return r.json() @@ -281,19 +284,19 @@ def test_add_tags_too_long(client: TestClient): def test_remove_tag(client: TestClient): token = test_register(client, username="lilian")['access'] exo = test_add_tags(client, user={"token": token, 'username': "lilian"}) - id_code = exo['id_code'] + id_code = exo['exo']['id_code'] tag_id = exo["tags"][0]["id_code"] r = client.delete(f'/exercice/{id_code}/tags/{tag_id}', headers={'Authorization': 'Bearer ' + token}) print(r.json()) assert r.json() == { - **exo, 'tags': exo['tags'][1:]} + **exo['exo'], 'tags': exo['tags'][1:]} def test_remove_tag_not_found(client: TestClient): token = test_register(client, username="lilian")['access'] exo = test_add_tags(client, user={"token": token, 'username': "lilian"}) - id_code = exo['id_code'] + id_code = exo['exo']['id_code'] tag_id = "none" r = client.delete(f'/exercice/{id_code}/tags/{tag_id}', headers={'Authorization': 'Bearer ' + token}) @@ -316,7 +319,7 @@ def test_remove_tag_not_owner(client: TestClient): token = test_register(client, username="lilian")['access'] token2 = test_register(client, username="lilian2")['access'] exo = test_add_tags(client, user={"token": token, 'username': "lilian"}) - id_code = exo['id_code'] + id_code = exo['exo']['id_code'] tag_id = exo['tags'][0]['id_code'] r = client.delete(f'/exercice/{id_code}/tags/{tag_id}', headers={'Authorization': 'Bearer ' + token2}) @@ -481,29 +484,31 @@ def test_get_user_with_tags(client: TestClient): token2 = test_register(client, username="lilian2")['access'] tags1 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None}] - tags2 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None}, + tags2 = [ {'label': "tag2", 'color': "#ff0000", 'id_code': None}] - tags3 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None}, - {'label': "tag2", 'color': "#ff0000", 'id_code': None}, {'label': "tag3", 'color': "#ff0000", 'id_code': None}] + + tags3 = [{'label': "tag3", 'color': "#ff0000", 'id_code': None}] exo_other_user = test_create( client, user={'token': token2, 'username': "lilian2"}) exo1 = test_add_tags(client, user={ 'token': token1, 'username': "lilian"}, tags=tags1) - + tags2 = [*exo1['tags'], *tags2] exo2 = test_add_tags(client, user={ 'token': token1, 'username': "lilian"}, tags=tags2) + + tags3 = [*exo2['tags'], *tags3] exo3 = test_add_tags(client, user={ 'token': token1, 'username': "lilian"}, tags=tags3) tags1 = exo1['tags'] tags2 = exo2['tags'] tags3 = exo3['tags'] - r = client.get('/exercices/user', params={'tags': [*[t['id_code'] for t in tags2], 'notexist']}, + r = client.get('/exercices/user', params={'tags': [*[t['id_code'] for t in tags2], 'notexisting']}, headers={'Authorization': 'Bearer ' + token1}) - print(r.json()) - assert r.json()['items'] == [exo2, exo3] + print("DATA", r.json()) + assert r.json()['items'] == [exo2['exo'], exo3['exo']] def test_get_user_with_tags_and_search(client: TestClient): @@ -511,20 +516,18 @@ def test_get_user_with_tags_and_search(client: TestClient): token2 = test_register(client, username="lilian2")['access'] tags1 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None}] - tags2 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None}, - {'label': "tag2", 'color': "#ff0000", 'id_code': None}] - tags3 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None}, - {'label': "tag2", 'color': "#ff0000", 'id_code': None}, {'label': "tag3", 'color': "#ff0000", 'id_code': None}] + tags2 = [{'label': "tag2", 'color': "#ff0000", 'id_code': None}] + tags3 = [{'label': "tag3", 'color': "#ff0000", 'id_code': None}] exo_other_user = test_create( client, user={'token': token2, 'username': "lilian2"}) exo1 = test_add_tags(client, user={ 'token': token1, 'username': "lilian"}, tags=tags1, name="yes") - + tags2 = [*exo1['tags'], *tags2] exo2 = test_add_tags(client, user={ 'token': token1, 'username': "lilian"}, tags=tags2, name="no") - + tags3 = [*exo2['tags'], *tags3] exo3 = test_add_tags(client, user={ 'token': token1, 'username': "lilian"}, tags=tags3, name="yes") @@ -534,7 +537,7 @@ def test_get_user_with_tags_and_search(client: TestClient): r = client.get('/exercices/user', params={"search": "yes", 'tags': [t['id_code'] for t in tags2]}, headers={'Authorization': 'Bearer ' + token1}) print(r.json()) - assert r.json()['items'] == [exo3] + assert r.json()['items'] == [exo3['exo']] def test_get_public_auth(client: TestClient): @@ -595,8 +598,8 @@ def test_get_exo_no_auth(client: TestClient): token = test_register(client, username="lilian")['access'] exo = test_add_tags(client, user={'token': token, 'username': "lilian"}) - r = client.get('/exercice/' + exo['id_code']) - assert r.json() == {**exo, "tags": [], 'is_author': False} + r = client.get('/exercice/' + exo['exo']['id_code']) + assert r.json() == {**exo['exo'], "tags": [], 'is_author': False} def test_get_exo_no_auth_private(client: TestClient): @@ -613,20 +616,24 @@ def test_get_exo_auth(client: TestClient): token2 = test_register(client, username="lilian2")['access'] exo = test_add_tags(client, user={'token': token, 'username': "lilian"}) - r = client.get('/exercice/' + exo['id_code'], + r = client.get('/exercice/' + exo['exo']['id_code'], headers={'Authorization': 'Bearer ' + token2}) print(r.json(), exo) - assert r.json() == {**exo, "tags": [], 'is_author': False} + assert r.json() == {**exo['exo'], "tags": [], 'is_author': False} def test_get_exo_auth_with_tags(client: TestClient): token = test_register(client, username="lilian")['access'] + token2 = test_register(client, username="lilian2")['access'] + exo = test_add_tags(client, user={'token': token, 'username': "lilian"}) - - r = client.get('/exercice/' + exo['id_code'], + test_add_tags(client, user={'token': token2, 'username': "lilian2"}, exo={**exo['exo'], "is_author": False, "tags": []}) + + r = client.get('/exercice/' + exo['exo']['id_code'], headers={'Authorization': 'Bearer ' + token}) + print(r.json(), exo) - assert r.json() == {**exo} + assert r.json() == {**exo['exo']} def test_get_exo_auth_private(client: TestClient): @@ -668,7 +675,7 @@ def test_get_csv(client: TestClient): assert r.json()['items'] == [{**exoCsv.json(), 'is_author': False}] -def test_get_pdf(client: TestClient): +''' def test_get_pdf(client: TestClient): token = test_register(client)['access'] exoCsv = client.post('/exercices', data={"name": "name", "consigne": "consigne", "private": False}, files={ 'file': ('test.py', open('tests/testing_exo_source/exo_source_csv_only.py', 'rb'))}, headers={"Authorization": "Bearer " + token}) @@ -680,7 +687,7 @@ def test_get_pdf(client: TestClient): r = client.get('/exercices/public', params={"type": "pdf"}) assert r.json()['items'] == [{**exoPdf.json(), 'is_author': False}] - + ''' def test_get_web(client: TestClient): token = test_register(client)['access'] @@ -693,7 +700,7 @@ def test_get_web(client: TestClient): r = client.get('/exercices/public', params={"type": "web"}) - assert r.json() == [{**exoWeb.json(), 'is_author': False}] + assert r.json()['items'] == [{**exoWeb.json(), 'is_author': False}] def test_get_invalid_type(client: TestClient): diff --git a/backend/api/tests/test_room.py b/backend/api/tests/test_room.py index cc1e9dd..cce6589 100644 --- a/backend/api/tests/test_room.py +++ b/backend/api/tests/test_room.py @@ -1,13 +1,13 @@ -import random from fastapi import HTTPException from fastapi.testclient import TestClient + from tests.test_auth import test_register from tests.test_exos import test_create def test_create_room_no_auth(client: TestClient, public=False): r = client.post('/room', json={"name": "test_room", - "public": public}, params={'username': "lilian"}) + "public": public}, params={'username': "lilian"}) print(r.json()) assert "room" in r.json() assert "member" in r.json() @@ -16,8 +16,8 @@ def test_create_room_no_auth(client: TestClient, public=False): def test_create_room_no_auth_invalid(client: TestClient): - r = client.post('/room', json={"name": "test_room"*21, - "public": False}, params={'username': "lilian"*21}) + r = client.post('/room', json={"name": "test_room" * 21, + "public": False}, params={'username': "lilian" * 21}) print(r.json()) assert r.json() == {'detail': {'username_error': 'ensure this value has at most 20 characters', 'name_error': 'ensure this value has at most 20 characters'}} @@ -50,12 +50,13 @@ def test_login_no_auth(client: TestClient): member = room['member'] with client.websocket_connect(f"/ws/room/" + room['room']) as ws: ws.send_json({"type": "login", "data": { - "reconnect_code": member}}) + "reconnect_code": member}}) data = ws.receive_json() assert "id_code" in data['data']['member'] assert "clientId" in data['data']['member'] assert data == {'type': "loggedIn", "data": {"member": {**data['data']['member'], - "reconnect_code": member, "isAdmin": True, "isUser": False}}} + "reconnect_code": member, "isAdmin": True, + "isUser": False}}} def test_login_no_auth_different_clientId(client: TestClient): @@ -63,13 +64,13 @@ def test_login_no_auth_different_clientId(client: TestClient): member = room['member'] with client.websocket_connect(f"/ws/room/" + room['room']) as ws: ws.send_json({"type": "login", "data": { - "reconnect_code": member}}) + "reconnect_code": member}}) data = ws.receive_json() clientId = data['data']['member']['clientId'] ws.close() with client.websocket_connect(f"/ws/room/" + room['room']) as ws: ws.send_json({"type": "login", "data": { - "reconnect_code": member}}) + "reconnect_code": member}}) data = ws.receive_json() assert data['data']['member']['clientId'] != clientId @@ -78,11 +79,11 @@ def test_login_no_auth_not_in_room(client: TestClient): room = test_create_room_no_auth(client=client) with client.websocket_connect(f"/ws/room/" + room['room']) as ws: ws.send_json({"type": "login", "data": { - "reconnect_code": "lol"}}) + "reconnect_code": "lol"}}) data = ws.receive_json() print(data) - assert data == {'type': "error", "data": { - "msg": "Utilisateur introuvable dans cette salle"}} + assert data == {'type': "error", "data": {"code": 401, + "msg": "Utilisateur introuvable dans cette salle"}} def test_login_auth(client: TestClient): @@ -95,7 +96,8 @@ def test_login_auth(client: TestClient): print(data) assert "id_code" in data["data"]['member'] assert data == {'type': "loggedIn", "data": {"member": {**data["data"]['member'], - "username": "lilian", "isAdmin": True, "isUser": True, 'reconnect_code': "", "clientId": ""}}} + "username": "lilian", "isAdmin": True, "isUser": True, + 'reconnect_code': "", "clientId": ""}}} def test_login_auth_not_in_room(client: TestClient): @@ -106,8 +108,8 @@ def test_login_auth_not_in_room(client: TestClient): ws.send_json({"type": "login", "data": {"token": token}}) data = ws.receive_json() print(data) - assert data == {'type': "error", "data": { - "msg": "Utilisateur introuvable dans cette salle"}} + assert data == {'type': "error", "data": {"code": 401, + "msg": "Utilisateur introuvable dans cette salle"}} def test_join_auth(client: TestClient): @@ -116,7 +118,7 @@ def test_join_auth(client: TestClient): member = room['member'] with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { - "reconnect_code": member}}) + "reconnect_code": member}}) admin.receive_json() with client.websocket_connect(f"/ws/room/" + room['room']) as member: member.send_json({"type": "join", "data": {"token": token}}) @@ -132,14 +134,17 @@ def test_join_auth(client: TestClient): "waiter": {"waiter_id": mdata['data']['waiter']['waiter_id'], "username": "lilian2"}}} admin.send_json({"type": "accept", "data": { - "waiter_id": mdata['data']['waiter']['waiter_id']}}) + "waiter_id": mdata['data']['waiter']['waiter_id']}}) mdata = member.receive_json() - print('MDATA', mdata) assert mdata == {"type": "accepted", "data": {"member": { - "username": "lilian2", "isUser": True, "isAdmin": False, "reconnect_code": "", "id_code": mdata['data']["member"]["id_code"]}}} + "username": "lilian2", "isUser": True, "isAdmin": False, "reconnect_code": "", + "id_code": mdata['data']["member"]["id_code"], "clientId": "", "online": True}}} + print("MDATABUS", mdata) adata = admin.receive_json() + assert adata == {'type': "joined", 'data': { - "member": {"reconnect_code": "", "username": "lilian2", "isUser": True, "isAdmin": False, "id_code": adata['data']["member"]["id_code"]}}} + "member": {"reconnect_code": "", "clientId": "", "online": True, "username": "lilian2", "isUser": True, + "isAdmin": False, "id_code": adata['data']["member"]["id_code"]}}} admin.send_json({"type": "ping_room"}) mdata = member.receive_json() assert mdata == {"type": "ping", "data": {}} @@ -151,20 +156,22 @@ def test_join_waiter_not_found(client: TestClient): member = room['member'] with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { - "reconnect_code": member}}) + "reconnect_code": member}}) admin.receive_json() admin.send_json({"type": "accept", "data": {"waiter_id": "OOOO"}}) data = admin.receive_json() - assert data == {"type": "error", "data": { - "msg": "Utilisateur en list d'attente introuvable"}} + assert data == {"type": "error", "data": {"code": 400, + "msg": "Utilisateur en liste d'attente introuvable"}} +# TODO = Maybe check the joined event + def test_join_no_auth(client: TestClient): room = test_create_room_no_auth(client=client) member = room['member'] with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { - "reconnect_code": member}}) + "reconnect_code": member}}) admin.receive_json() with client.websocket_connect(f"/ws/room/" + room['room']) as memberws: memberws.send_json( @@ -179,15 +186,19 @@ def test_join_no_auth(client: TestClient): "waiter": {"waiter_id": mdata['data']['waiter']['waiter_id'], "username": "member"}}} admin.send_json({"type": "accept", "data": { - "waiter_id": mdata['data']['waiter']['waiter_id']}}) + "waiter_id": mdata['data']['waiter']['waiter_id']}}) mdata = memberws.receive_json() new_reconnect = mdata['data']['member']['reconnect_code'] + clientId = mdata['data']['member']['clientId'] + assert clientId != "" assert 'reconnect_code' in mdata['data']['member'] assert mdata == {"type": "accepted", "data": {"member": { - "username": "member", "reconnect_code": new_reconnect, "isUser": False, "isAdmin": False, "id_code": mdata['data']["member"]["id_code"]}}} + "username": "member", "reconnect_code": new_reconnect, "isUser": False, "isAdmin": False, + "id_code": mdata['data']["member"]["id_code"], "clientId": clientId, "online": True}}} adata = admin.receive_json() assert adata == {'type': "joined", 'data': { - "member": {"reconnect_code": new_reconnect, "username": "member", "isUser": False, "isAdmin": False, "id_code": adata['data']["member"]["id_code"]}}} + "member": {"reconnect_code": new_reconnect, "clientId": "", "online": True, "username": "member", + "isUser": False, "isAdmin": False, "id_code": adata['data']["member"]["id_code"]}}} admin.send_json({"type": "ping_room"}) mdata = memberws.receive_json() assert mdata == {"type": "ping", "data": {}} @@ -202,8 +213,8 @@ def test_join_no_auth_username_error(client: TestClient): with client.websocket_connect(f"/ws/room/" + room['room']) as member: member.send_json({"type": "join", "data": {"username": "lilian"}}) mdata = member.receive_json() - assert mdata == {"type": "error", "data": { - "msg": "Nom d'utilisateur invalide ou indisponible"}} + assert mdata == {"type": "error", "data": {"code": 400, + "msg": "Nom d'utilisateur invalide ou indisponible"}} def test_join_no_auth_username_too_long(client: TestClient): @@ -211,10 +222,10 @@ def test_join_no_auth_username_too_long(client: TestClient): member = room['member'] with client.websocket_connect(f"/ws/room/" + room['room']) as member: - member.send_json({"type": "join", "data": {"username": "lilian"*21}}) + member.send_json({"type": "join", "data": {"username": "lilian" * 21}}) mdata = member.receive_json() - assert mdata == {"type": "error", "data": { - "msg": "Nom d'utilisateur invalide ou indisponible"}} + assert mdata == {"type": "error", "data": {"code": 400, + "msg": "Nom d'utilisateur invalide ou indisponible"}} def test_join_auth_refused(client: TestClient): @@ -223,7 +234,7 @@ def test_join_auth_refused(client: TestClient): member = room['member'] with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { - "reconnect_code": member}}) + "reconnect_code": member}}) admin.receive_json() with client.websocket_connect(f"/ws/room/" + room['room']) as member: member.send_json({"type": "join", "data": {"token": token}}) @@ -236,7 +247,7 @@ def test_join_auth_refused(client: TestClient): assert adata == {'type': "waiter", 'data': { "waiter": {"waiter_id": waiter_id, "username": "lilian2"}}} admin.send_json({"type": "refuse", "data": { - "waiter_id": waiter_id}}) + "waiter_id": waiter_id}}) adata = admin.receive_json() assert adata == {"type": "successfullyRefused", "data": {"waiter_id": waiter_id}} @@ -256,7 +267,8 @@ def test_join_auth_in_room_yet(client: TestClient): assert "id_code" in mdata['data']['member'] assert "clientId" in mdata['data']['member'] assert mdata == {"type": "loggedIn", "data": {"member": {**mdata['data']['member'], - "username": "lilian", "isAdmin": True, "isUser": True, 'reconnect_code': ""}}} + "username": "lilian", "isAdmin": True, "isUser": True, + 'reconnect_code': ""}}} def test_join_auth_public(client: TestClient): @@ -265,17 +277,20 @@ def test_join_auth_public(client: TestClient): member = room['member'] with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { - "reconnect_code": member}}) + "reconnect_code": member}}) admin.receive_json() with client.websocket_connect(f"/ws/room/" + room['room']) as member: member.send_json({"type": "join", "data": {"token": token}}) mdata = member.receive_json() assert mdata == {"type": "accepted", "data": {"member": { - "username": "lilian2", "isUser": True, "isAdmin": False, "reconnect_code": "", "id_code": mdata['data']["member"]["id_code"]}}} + "username": "lilian2", "isUser": True, "isAdmin": False, "reconnect_code": "", + "id_code": mdata['data']["member"]["id_code"], "clientId": "", "online": True}}} adata = admin.receive_json() assert adata == {'type': "joined", 'data': { - "member": {"reconnect_code": "", "username": "lilian2", "isUser": True, "isAdmin": False, "id_code": mdata['data']["member"]["id_code"]}}} + "member": {"reconnect_code": "", "username": "lilian2", "isUser": True, "isAdmin": False, + "id_code": mdata['data']["member"]["id_code"], + "clientId": "", "online": True}}} admin.send_json({"type": "ping_room"}) mdata = member.receive_json() assert mdata == {"type": "ping", "data": {}} @@ -286,19 +301,24 @@ def test_join_no_auth_public(client: TestClient): member = room['member'] with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { - "reconnect_code": member}}) + "reconnect_code": member}}) admin.receive_json() with client.websocket_connect(f"/ws/room/" + room['room']) as member: member.send_json({"type": "join", "data": {"username": "member"}}) mdata = member.receive_json() assert 'reconnect_code' in mdata['data']['member'] + assert "clientId" in mdata['data']['member'] and mdata['data']['member']['clientId'] != "" assert mdata == {"type": "accepted", "data": {"member": { - "username": "member", "reconnect_code": mdata['data']['member']['reconnect_code'], "isUser": False, "isAdmin": False, "id_code": mdata['data']["member"]["id_code"]}}} + "username": "member", "reconnect_code": mdata['data']['member']['reconnect_code'], "isUser": False, + "isAdmin": False, "id_code": mdata['data']["member"]["id_code"], + "clientId": mdata['data']['member']['clientId'], "online": True}}} adata = admin.receive_json() assert adata == {'type': "joined", 'data': { - "member": {"reconnect_code": mdata['data']['member']['reconnect_code'], "username": "member", "isUser": False, "isAdmin": False, "id_code": mdata['data']["member"]["id_code"]}}} + "member": {"reconnect_code": mdata['data']['member']['reconnect_code'], "username": "member", + "isUser": False, "isAdmin": False, "id_code": mdata['data']["member"]["id_code"], + "clientId": "", "online": True}}} member.send_json({"type": "update_groups"}) admin.send_json({"type": "ping_room"}) @@ -312,7 +332,7 @@ def test_join_auth_unauthorized(client: TestClient): member = room['member'] with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { - "reconnect_code": member}}) + "reconnect_code": member}}) admin.receive_json() with client.websocket_connect(f"/ws/room/" + room['room']) as member: member.send_json({"type": "join", "data": {"token": token}}) @@ -338,15 +358,16 @@ def test_connect_admin(client: TestClient): members = room['members'] with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { - "reconnect_code": members[0]}}) + "reconnect_code": members[0]}}) admin.receive_json() with client.websocket_connect(f"/ws/room/" + room['room']) as member: member.send_json({'type': "login", "data": { - "reconnect_code": members[1]}}) + "reconnect_code": members[1]}}) adata = admin.receive_json() assert adata == {"type": "connect", "data": {"member": { - "username": "member", "reconnect_code": members[1], "isAdmin": False, "isUser": False, "id_code": adata['data']["member"]["id_code"]}}} + "username": "member", "reconnect_code": members[1], "isAdmin": False, "isUser": False, + "id_code": adata['data']["member"]["id_code"], "clientId": "", "online": True}}} def test_connect_member(client: TestClient): @@ -358,11 +379,12 @@ def test_connect_member(client: TestClient): memberws.receive_json() with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({'type': "login", "data": { - "reconnect_code": members[0]}}) + "reconnect_code": members[0]}}) mdata = memberws.receive_json() assert mdata == {"type": "connect", "data": {"member": { - "username": "lilian", "reconnect_code": "", "isAdmin": True, "isUser": False, "id_code": mdata['data']["member"]["id_code"]}}} + "username": "lilian", "reconnect_code": "", "isAdmin": True, "isUser": False, + "id_code": mdata['data']["member"]["id_code"], "clientId": "", "online": True}}} def test_disconnect(client: TestClient): @@ -374,13 +396,33 @@ def test_disconnect(client: TestClient): memberws.receive_json() with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({'type': "login", "data": { - "reconnect_code": members[0]}}) + "reconnect_code": members[0]}}) memberws.receive_json() admin.close() mdata = memberws.receive_json() assert mdata == {"type": "disconnect", "data": {"member": { - "username": "lilian", "reconnect_code": "", "isAdmin": True, "isUser": False, "id_code": mdata['data']["member"]["id_code"]}}} + "username": "lilian", "reconnect_code": "", "isAdmin": True, "isUser": False, + "id_code": mdata['data']["member"]["id_code"], "clientId": "", "online": False}}} + + +def test_disconnect_admin(client: TestClient): + room = test_join_no_auth(client=client) + members = room['members'] + with client.websocket_connect(f"/ws/room/" + room['room']) as admin: + admin.send_json({"type": "login", "data": { + "reconnect_code": members[0]}}) + admin.receive_json() + with client.websocket_connect(f"/ws/room/" + room['room']) as memberws: + memberws.send_json({'type': "login", "data": { + "reconnect_code": members[1]}}) + memberws.receive_json() + memberws.close() + admin.receive_json() + adata = admin.receive_json() + assert adata == {"type": "disconnect", "data": {"member": { + "username": "member", "reconnect_code": members[1], "isAdmin": False, "isUser": False, + "id_code": adata['data']["member"]["id_code"], "clientId": "", "online": False}}} def test_disconnect_waiter(client: TestClient): @@ -421,7 +463,8 @@ def test_leave(client: TestClient): adata = admin.receive_json() assert adata == {"type": "leaved", "data": {"member": { - "username": "test", "reconnect_code": m["data"]['member']["reconnect_code"], "isAdmin": False, "isUser": False, "id_code": adata['data']["member"]["id_code"]}}} + "username": "test", "reconnect_code": "", "isAdmin": False, + "isUser": False, "id_code": adata['data']["member"]["id_code"], "online": True, "clientId": ""}}} def test_leave_not_connected(client: TestClient): @@ -431,8 +474,8 @@ def test_leave_not_connected(client: TestClient): with client.websocket_connect(f"/ws/room/" + room['room']) as memberws: memberws.send_json({"type": "leave"}) data = memberws.receive_json() - assert data == {"type": "error", "data": { - "msg": "Vous n'êtes connecté à aucune salle"}} + assert data == {"type": "error", "data": {"code": 400, + "msg": "Vous n'êtes connecté à aucune salle"}} def test_leave_admin(client: TestClient): @@ -445,10 +488,11 @@ def test_leave_admin(client: TestClient): admin.receive_json() admin.send_json({"type": "leave"}) data = admin.receive_json() - assert data == {"type": "error", "data": { - "msg": "Vous ne pouvez pas quitter une salle dont vous êtes l'administrateur"}} + assert data == {"type": "error", "data": {"code": 400, + "msg": "Vous ne pouvez pas quitter une salle dont vous êtes l'administrateur"}} +# TODO : Change type for ban and leave def test_ban_anonymous(client: TestClient): room = test_create_room_no_auth(client=client, public=True) member = room['member'] @@ -470,7 +514,8 @@ def test_ban_anonymous(client: TestClient): adata = admin.receive_json() assert adata == {"type": "leaved", "data": {"member": { - "username": "test", "reconnect_code": reconnect_code, "isUser": False, "isAdmin": False, "id_code": id_code}}} + "username": "test", "reconnect_code": "", "isUser": False, "isAdmin": False, + "id_code": id_code, "clientId": "", "online": True}}} def test_ban_anonymous_unauthorized(client: TestClient): @@ -500,8 +545,8 @@ def test_ban_admin(client: TestClient): admin.send_json( {"type": "ban", "data": {"member_id": data['data']['member']['id_code']}}) a = admin.receive_json() - assert a == {'type': "error", "data": { - "msg": "Vous ne pouvez pas bannir un administrateur"}} + assert a == {'type': "error", "data": {"code": 400, + "msg": "Vous ne pouvez pas bannir un administrateur"}} def test_ban_user(client: TestClient): @@ -518,13 +563,14 @@ def test_ban_user(client: TestClient): admin.receive_json() admin.send_json( - {"type": "ban", "data": {"member_id": m['data']['member']['id_code']}}) + {"type": "ban", "data": {"member_id": m['data']['member']['id_code']}}) mdata = memberws.receive_json() assert mdata == {"type": "banned", "data": {}} adata = admin.receive_json() assert adata == {"type": "leaved", "data": {"member": { - "username": "lilian2", "reconnect_code": "", "isUser": True, "isAdmin": False, "id_code": m['data']['member']['id_code']}}} + "username": "lilian2", "reconnect_code": "", "isUser": True, "isAdmin": False, + "id_code": m['data']['member']['id_code'], "clientId": "", "online": True}}} def test_create_parcours_no_auth(client: TestClient): @@ -533,26 +579,44 @@ def test_create_parcours_no_auth(client: TestClient): token = test_register(client, username="lilian") exo1 = test_create(name="test1", client=client, user={ - "token": token['access'], "username": "lilian"}) + "token": token['access'], "username": "lilian"}) exo2 = test_create(name="test2", client=client, user={ - "token": token['access'], "username": "lilian"}) + "token": token['access'], "username": "lilian"}) exo3 = test_create(name="test3", client=client, user={ - "token": token['access'], "username": "lilian"}) + "token": token['access'], "username": "lilian"}) exo4 = test_create(name="test4", client=client, user={ - "token": token['access'], "username": "lilian"}) + "token": token['access'], "username": "lilian"}) with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { "reconnect_code": member}}) adata = admin.receive_json() - r = client.post(f'/room/{room["room"]}/parcours', json={"clientId": adata['data']['member']['clientId'], "parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, "exercices": [ - {"exercice_id": "lolilol", "quantity": 12}, {"exercice_id": exo1['id_code'], "quantity": 10}, {"exercice_id": exo2['id_code'], "quantity": 5}, {"exercice_id": exo3['id_code'], "quantity": 12}]}}) - print('resonse', r.json()) - assert r.json() == {"id_code": r.json()['id_code'], "challenges": [], "name": "test_parcours", "time": 10*60, "validate_condition": 10, "exercices": [{ - "exercice_id": exo1['id_code'], "name": exo1['name'], "quantity": 10}, {"exercice_id": exo2['id_code'], "name": exo2['name'], "quantity": 5}, {"exercice_id": exo3['id_code'], "name": exo3['name'], "quantity": 12}]} + r = client.post(f'/room/{room["room"]}/parcours', params={"clientId": adata['data']['member']['clientId']}, + json={ + "name": "test_parcours", "time": 10 * 60, + "max_mistakes": 10, "exercices": [ + {"exercice_id": "lolilol", "quantity": 12}, + {"exercice_id": exo1['id_code'], + "quantity": 10}, + {"exercice_id": exo2['id_code'], "quantity": 5}, + {"exercice_id": exo3['id_code'], + "quantity": 12}]}) + print('resonse', r.json(), exo3) + assert r.json() == {"id_code": r.json()['id_code'], "memberRank": None, "name": "test_parcours", "pb": None, + "rank": None, "ranking": [], "tops": [], "validated": False, "challenges": {}, + "name": "test_parcours", "time": 10 * 60, + "max_mistakes": 10, "exercices": [{"examples": exo1['examples'], + "exercice_id": exo1['id_code'], "name": exo1['name'], + "quantity": 10}, + {"examples": exo2['examples'], + "exercice_id": exo2['id_code'], "name": exo2['name'], + "quantity": 5}, + {"examples": exo3['examples'], + "exercice_id": exo3['id_code'], "name": exo3['name'], + "quantity": 12}]} adata = admin.receive_json() assert adata == {"type": "add_parcours", "data": {"parcours": { - "name": "test_parcours", "id_code": r.json()['id_code'], "best_note": None}}} + "name": "test_parcours", "id_code": r.json()['id_code'], "best_note": None, "validated": False, }}} return r.json(), member, room @@ -560,25 +624,42 @@ def test_create_parcours_auth(client: TestClient): token = test_register(client, username="lilian")['access'] room = test_create_room_auth(client=client, token=token) exo1 = test_create(name="test1", client=client, user={ - "token": token, "username": "lilian"}) + "token": token, "username": "lilian"}) exo2 = test_create(name="test2", client=client, user={ - "token": token, "username": "lilian"}) + "token": token, "username": "lilian"}) exo3 = test_create(name="test3", client=client, user={ - "token": token, "username": "lilian"}) + "token": token, "username": "lilian"}) exo4 = test_create(name="test4", client=client, user={ - "token": token, "username": "lilian"}) + "token": token, "username": "lilian"}) with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { "token": token}}) adata = admin.receive_json() - r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={"parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, "exercices": [ - {"exercice_id": "lolilol", "quantity": 12}, {"exercice_id": exo1['id_code'], "quantity": 10}, {"exercice_id": exo2['id_code'], "quantity": 5}, {"exercice_id": exo3['id_code'], "quantity": 12}]}}) + r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={ + "name": "test_parcours", "time": 10 * 60, + "max_mistakes": 10, "exercices": [ + {"exercice_id": "lolilol", "quantity": 12}, + {"exercice_id": exo1['id_code'], + "quantity": 10}, + {"exercice_id": exo2['id_code'], "quantity": 5}, + {"exercice_id": exo3['id_code'], + "quantity": 12}]}) - assert r.json() == {"id_code": r.json()['id_code'], "challenges": [], "name": "test_parcours", "time": 10*60, "validate_condition": 10, "exercices": [{ - "exercice_id": exo1['id_code'], "name": exo1['name'], "quantity": 10}, {"exercice_id": exo2['id_code'], "name": exo2['name'], "quantity": 5}, {"exercice_id": exo3['id_code'], "name": exo3['name'], "quantity": 12}]} + assert r.json() == {"id_code": r.json()['id_code'], "memberRank": None, "name": "test_parcours", "pb": None, + "rank": None, "ranking": [], "tops": [], "validated": False, "challenges": {}, + "name": "test_parcours", "time": 10 * 60, + "max_mistakes": 10, "exercices": [{"examples": exo1['examples'], + "exercice_id": exo1['id_code'], "name": exo1['name'], + "quantity": 10}, + {"examples": exo2['examples'], + "exercice_id": exo2['id_code'], "name": exo2['name'], + "quantity": 5}, + {"examples": exo3['examples'], + "exercice_id": exo3['id_code'], "name": exo3['name'], + "quantity": 12}]} adata = admin.receive_json() assert adata == {"type": "add_parcours", "data": {"parcours": { - "name": "test_parcours", "id_code": r.json()['id_code'], "best_note": None}}} + "name": "test_parcours", "id_code": r.json()['id_code'], "best_note": None, "validated": False, }}} return r.json(), token, room @@ -596,8 +677,9 @@ def test_create_parcours_unauthorized(client: TestClient): mdata = memberws.send_json( {"type": "join", "data": {"token": token}}) - r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={"parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, - "exercices": [{"exercice_id": "lolilol", "quantity": 12}]}}) + r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, + json={"parcours": {"name": "test_parcours", "time": 10 * 60, "validate_condition": 10, + "exercices": [{"exercice_id": "lolilol", "quantity": 12}]}}) assert r.status_code == 401 assert r.json() == { "detail": "Vous devez être administrateur pour faire cela"} @@ -613,8 +695,9 @@ def test_create_parcours_not_in_room(client: TestClient): "reconnect_code": member}}) adata = admin.receive_json() - r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={"parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, - "exercices": [{"exercice_id": "lolilol", "quantity": 12}]}}) + r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, + json={"parcours": {"name": "test_parcours", "time": 10 * 60, "validate_condition": 10, + "exercices": [{"exercice_id": "lolilol", "quantity": 12}]}}) assert r.status_code == 401 assert r.json() == { "detail": "Vous n'êtes pas dans cette salle"} @@ -625,15 +708,23 @@ def test_create_parcours_no_valid_exo(client: TestClient): member = room['member'] token = test_register(client, username="lilian")['access'] exoCsv = client.post('/exercices', data={"name": "name", "consigne": "consigne", "private": False}, files={ - 'file': ('test.py', open('tests/testing_exo_source/exo_source_csv_only.py', 'rb'))}, headers={"Authorization": "Bearer " + token}) + 'file': ('test.py', open('tests/testing_exo_source/exo_source_csv_only.py', 'rb'))}, + headers={"Authorization": "Bearer " + token}) with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { "reconnect_code": member}}) adata = admin.receive_json() - r = client.post(f'/room/{room["room"]}/parcours', json={"clientId": adata['data']['member']['clientId'], "parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, - "exercices": [{"exercice_id": "lolilol", "quantity": 12}, {"exercice_id": exoCsv.json()['id_code'], "quantity": 10}]}}) + r = client.post(f'/room/{room["room"]}/parcours', params={"clientId": adata['data']['member']['clientId']}, + json={"name": "test_parcours", "time": 10 * 60, + "max_mistakes": 10, + "exercices": [{"exercice_id": "lolilol", + "quantity": 12}, { + "exercice_id": + exoCsv.json()[ + 'id_code'], + "quantity": 10}]}) assert r.status_code == 400 assert r.json() == { "detail": "Veuillez entrer au moins un exercice valide"} @@ -648,16 +739,18 @@ def test_create_parcours_not_authenticated(client: TestClient): "reconnect_code": member}}) admin.receive_json() - r = client.post(f'/room/{room["room"]}/parcours', json={"parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, - "exercices": [{"exercice_id": "lolilol", "quantity": 12}]}}) + r = client.post(f'/room/{room["room"]}/parcours', + json={"parcours": {"name": "test_parcours", "time": 10 * 60, "validate_condition": 10, + "exercices": [{"exercice_id": "lolilol", "quantity": 12}]}}) assert r.status_code == 401 assert r.json() == { "detail": "Not authenticated"} def test_create_parcours_room_not_found(client: TestClient): - r = client.post(f'/room/test/parcours', json={"parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, - "exercices": [{"exercice_id": "lolilol", "quantity": 12}]}}) + r = client.post(f'/room/test/parcours', + json={"parcours": {"name": "test_parcours", "time": 10 * 60, "validate_condition": 10, + "exercices": [{"exercice_id": "lolilol", "quantity": 12}]}}) assert r.status_code == 404 assert r.json() == {"detail": "Salle introuvable"} @@ -669,7 +762,7 @@ def test_delete_parcours_auth(client: TestClient): "token": token}}) adata = admin.receive_json() r = client.delete(f"/room/{room['room']}/parcours/{parcours['id_code']}", headers={ - "Authorization": "Bearer " + token}) + "Authorization": "Bearer " + token}) assert r.json() == "ok" adata = admin.receive_json() assert adata == {'type': "del_parcours", @@ -682,8 +775,8 @@ def test_delete_parcours_no_auth(client: TestClient): admin.send_json({"type": "login", "data": { "reconnect_code": reconnect}}) adata = admin.receive_json() - r = client.delete(f"/room/{room['room']}/parcours/{parcours['id_code']}", json={ - "clientId": adata['data']['member']['clientId']}) + r = client.delete(f"/room/{room['room']}/parcours/{parcours['id_code']}", params={ + "clientId": adata['data']['member']['clientId']}) assert r.json() == "ok" adata = admin.receive_json() assert adata == {'type': "del_parcours", @@ -701,7 +794,7 @@ def test_delete_parcours_unauthorized(client: TestClient): {"type": "join", "data": {"token": token}}) adata = admin.receive_json() r = client.delete(f"/room/{room['room']}/parcours/{parcours['id_code']}", headers={ - "Authorization": "Bearer " + token}) + "Authorization": "Bearer " + token}) assert r.status_code == 401 assert r.json() == { "detail": "Vous devez être administrateur pour faire cela"} @@ -724,12 +817,22 @@ def validate_entry(out: str): return out[:-1] == "1 + [0] = " and int(out[-1]) <= 9 and int(out[-1]) >= 1 +def fill_challenge(c): + for ex in c: + ex['data'] = [{**e, "inputs": [{**i, "value": str(id)} for i in e['inputs']]} for id,e in enumerate(ex['data'])] + return c + +def corrige_challenge(c, corr): + for ex in c: + ex['data'] = [{**e, "inputs": [{**i, "correction": corr, "valid": i['value'] == corr} for i in e['inputs']]} for id,e in enumerate(ex['data'])] + return c + def test_challenge_auth(client: TestClient): token = test_register(client, username="lilian")['access'] token2 = test_register(client, username="lilian2")['access'] room = test_create_room_auth(client=client, token=token, public=True) exo1 = test_create(name="test1", client=client, user={ - "token": token, "username": "lilian"}) + "token": token, "username": "lilian"}) with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { @@ -740,35 +843,65 @@ def test_challenge_auth(client: TestClient): memberws.send_json( {"type": "join", "data": {"token": token2}}) mdata = memberws.receive_json() + id_code = mdata['data']['member']['id_code'] - r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={"parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, "exercices": [ - {"exercice_id": exo1['id_code'], "quantity": 3}]}}) + r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, + json={"name": "test_parcours", "time": 10 * 60, "max_mistakes": 10, "exercices": [ + {"exercice_id": exo1['id_code'], "quantity": 3}]}) - parcours_id = r.json()['id_code'] - memberws.receive_json() - admin.receive_json() # new parcours - admin.receive_json() # disconnect - admin.receive_json() - admin.send_json({"type": "sub_parcours", "data": { - "parcours_id": parcours_id}}) - r = client.get(url=f'/room/{room["room"]}/challenge/{parcours_id}', - headers={"Authorization": "Bearer " + token2}) - data = r.json()['challenge'] + parcours_id = r.json()['id_code'] - data = r.json()['challenge'] - assert "id_code" in r.json() - assert len(data) == 1 and len(data[0]) == 3 and all([e['inputs'] == [ - {'index': 0, "value": ""}] and validate_entry(e['calcul']) for e in data[0]]) - filled_obj = [ - [{**e, 'inputs': [{"index": 0, "value": str(data[0].index(e))}]} for e in data[0]]] - r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token2}) - assert r.json() == {"id_code": r.json()['id_code'], "note": {"value": 1, "total": 3}, "time": 162, "validated": False, "isCorriged": True, "data": [ - [{**e, 'inputs': [{**e['inputs'][0], "correction": '1'}]} for e in filled_obj[0]]]} + # memberws.receive_json() + admin.receive_json() # new parcours + memberws.receive_json() + admin.receive_json() - adata = admin.receive_json() - assert adata == {'type': 'challenge', 'data': {'challenger': mdata['data']['member']['id_code'], 'note': { - 'value': 1, 'total': 3}, 'time': 162,"validated": False, 'isCorriged': True, 'canCorrige': True, "id_code": r.json()['id_code']}} + admin.send_json({"type": "sub_parcours", "data": { + "parcours_id": parcours_id}}) + memberws.send_json({"type": "sub_parcours", "data": { + "parcours_id": parcours_id}}) + + r = client.get(url=f'/room/{room["room"]}/challenge/{parcours_id}', + headers={"Authorization": "Bearer " + token2}) + + data = r.json()['challenge'] + assert "id_code" in r.json() and r.json()['id_code'] != "" + assert len(data) == 1 and len(data[0]['data']) == 3 and all([e['inputs'] == [ + {'index': 0, "value": ""}] and validate_entry(e['calcul']) for e in data[0]['data']]) + + filled_obj = [ + [{**e, 'inputs': [{"index": 0, "value": str(i)}]} for i, e in enumerate(data[0]['data'])]] + + filled_obj = fill_challenge(data) + + r = client.post( + f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', + json={'challenge': filled_obj, "time": 162}, + headers={"Authorization": "Bearer " + token2}) + + assert r.json()['id_code'] != "" + assert r.json() == {"id_code": r.json()['id_code'], "mistakes": 2, "time": 162, + "validated": True, "isCorriged": True, "data": corrige_challenge(filled_obj, "1")} + + adata = admin.receive_json() + assert adata == {'type': 'challenge', 'data': { + 'challenger': {"id_code": mdata['data']['member']['id_code'], + "name": mdata['data']['member']['username']}, + 'challenges': [ + {"canCorrige": True, "id_code": r.json()['id_code'], "mistakes": 2, "time": 162, "isCorriged": True, + "validated": True}]}} + + admin.receive_json() + adata = admin.receive_json() + + memberws.receive_json() + mdata = memberws.receive_json() + assert mdata == {'type': "newRanks", 'data': {"rank": 1, "avgRank": 1}} + mdata = memberws.receive_json() + + assert mdata == adata == {'type': 'newTops', 'data': { + 'tops': [{'challenger': {'name': 'lilian2', 'id_code': id_code}, 'mistakes': 2, 'time': 162}], + 'avgTops': [{'id_code': id_code, 'avg': 2.0, 'name': 'lilian2'}]}} def test_corrige_auth(client: TestClient): @@ -776,7 +909,7 @@ def test_corrige_auth(client: TestClient): token2 = test_register(client, username="lilian2")['access'] room = test_create_room_auth(client=client, token=token, public=True) exo1 = test_create(name="test1", client=client, user={ - "token": token, "username": "lilian"}) + "token": token, "username": "lilian"}) with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { @@ -787,59 +920,81 @@ def test_corrige_auth(client: TestClient): memberws.send_json( {"type": "join", "data": {"token": token2}}) mdata = memberws.receive_json() + id_code = mdata['data']['member']['id_code'] - r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={"parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, "exercices": [ - {"exercice_id": exo1['id_code'], "quantity": 3}]}}) + r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, + json={"name": "test_parcours", "time": 10 * 60, "max_mistakes": 10, "exercices": [ + {"exercice_id": exo1['id_code'], "quantity": 3}]}) + print(r.json()) + parcours_id = r.json()['id_code'] + memberws.receive_json() + admin.receive_json() # new parcours + # admin.receive_json() # disconnect + admin.receive_json() + memberws.send_json({"type": "sub_parcours", "data": { + "parcours_id": parcours_id}}) + admin.send_json({"type": "sub_parcours", "data": { + "parcours_id": parcours_id}}) + r = client.get(url=f'/room/{room["room"]}/challenge/{parcours_id}', + headers={"Authorization": "Bearer " + token2}) - parcours_id = r.json()['id_code'] - memberws.receive_json() - admin.receive_json() # new parcours - admin.receive_json() # disconnect - admin.receive_json() - admin.send_json({"type": "sub_parcours", "data": { - "parcours_id": parcours_id}}) - r = client.get(url=f'/room/{room["room"]}/challenge/{parcours_id}', - headers={"Authorization": "Bearer " + token2}) - data = r.json()['challenge'] + data = r.json()['challenge'] + assert "id_code" in r.json() + assert len(data) == 1 and len(data[0]['data']) == 3 and all([e['inputs'] == [ + {'index': 0, "value": ""}] and validate_entry(e['calcul']) for e in data[0]['data']]) + filled_obj = fill_challenge(data) + r = client.post( + f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', + json={'challenge': filled_obj, "time": 162}, + headers={"Authorization": "Bearer " + token2}) - data = r.json()['challenge'] - assert "id_code" in r.json() - assert len(data) == 1 and len(data[0]) == 3 and all([e['inputs'] == [ - {'index': 0, "value": ""}] and validate_entry(e['calcul']) for e in data[0]]) - filled_obj = [ - [{**e, 'inputs': [{"index": 0, "value": str(data[0].index(e))}]} for e in data[0]]] - r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token2}) - - assert r.json() == {"id_code": r.json()['id_code'], "note": {"value": 1, "total": 3}, "time": 162, "validated": False, "isCorriged": True, "data": [ - [{**e, 'inputs': [{**e['inputs'][0], "correction": '1'}]} for e in filled_obj[0]]]} + assert r.json()['id_code'] != "" + assert r.json() == {"id_code": r.json()['id_code'], "mistakes": 2, "time": 162, + "validated": True, "isCorriged": True, "data": corrige_challenge(filled_obj, "1")} + adata = admin.receive_json() + assert adata == {'type': 'challenge', 'data': { + 'challenger': {"id_code": mdata['data']['member']['id_code'], + "name": mdata['data']['member']['username']}, + 'challenges': [ + {"canCorrige": True, "id_code": r.json()['id_code'], "mistakes": 2, "time": 162, "isCorriged": True, + "validated": True}]}} - adata = admin.receive_json() - assert adata == {'type': 'challenge', 'data': {'challenger': mdata['data']['member']['id_code'], 'note': { - 'value': 1, 'total': 3}, 'time': 162, 'isCorriged': True,"validated": False, 'canCorrige': True, "id_code": r.json()['id_code']}} - - - rr = client.get( - f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', headers={"Authorization": "Bearer " + token2}) - - assert rr.json() == r.json() + rr = client.get( + f'/room/{room["room"]}/correction/{r.json()["id_code"]}', + headers={"Authorization": "Bearer " + token2}) + assert rr.json() == r.json() - data = r.json()['data'] - corriged_obj = [ - [{**e, 'inputs': [{**e['inputs'][0], "correction": str(data[0].index(e))}]} for e in data[0]]] - r = client.put( - f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', json={'correction': corriged_obj}, headers={"Authorization": "Bearer " + token}) - assert r.json() == {"id_code": r.json()['id_code'], "note": {"value": 3, "total": 3}, "time": 162, "validated": True, "isCorriged": True, "data": corriged_obj} - - adata = admin.receive_json() - assert adata == {'type': 'challenge_change', 'data': {'challenger': mdata['data']['member']['id_code'], 'note': { - 'value': 3, 'total': 3}, 'time': 162,"validated": True, 'isCorriged': True, 'canCorrige': True, "id_code": r.json()['id_code']}} - rr = client.get( - f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', headers={"Authorization": "Bearer " + token2}) + data = r.json()['data'] + corriged_obj = [ + [{**e, 'inputs': [{**e['inputs'][0], "correction": str(i)}]} for i, e in enumerate(data[0]['data'])]] + corriged_obj = corrige_challenge(filled_obj, "0") + r = client.put( + f'/room/{room["room"]}/challenge/{r.json()["id_code"]}', + json={'correction': corriged_obj}, + headers={"Authorization": "Bearer " + token}) - assert rr.json() == r.json() + assert r.json() == {"id_code": r.json()['id_code'], "mistakes": 3, "time": 162, + "validated": False, "isCorriged": True, "data": corriged_obj} + + adata = admin.receive_json() + assert adata == {'type': 'challenge', 'data': { + 'challenger': {"id_code": mdata['data']['member']['id_code'], + "name": mdata['data']['member']['username']}, + 'challenges': [ + {"canCorrige": True, "id_code": r.json()['id_code'], "mistakes": 2, "time": 162, "isCorriged": True, + "validated": True}]}} + + assert adata == {'type': 'challenge_change', + 'data': {'challenger': mdata['data']['member']['id_code'], 'note': { + 'value': 3, 'total': 3}, 'time': 162, "validated": True, 'isCorriged': True, + 'canCorrige': True, "id_code": r.json()['id_code']}} + rr = client.get( + f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', + headers={"Authorization": "Bearer " + token2}) + + assert rr.json() == r.json() def test_get_challenge_auth(client: TestClient): @@ -847,7 +1002,7 @@ def test_get_challenge_auth(client: TestClient): token2 = test_register(client, username="lilian2")['access'] room = test_create_room_auth(client=client, token=token, public=True) exo1 = test_create(name="test1", client=client, user={ - "token": token, "username": "lilian"}) + "token": token, "username": "lilian"}) with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { @@ -859,8 +1014,9 @@ def test_get_challenge_auth(client: TestClient): {"type": "join", "data": {"token": token2}}) mdata = memberws.receive_json() - r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={"parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, "exercices": [ - {"exercice_id": exo1['id_code'], "quantity": 3}]}}) + r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={ + "parcours": {"name": "test_parcours", "time": 10 * 60, "validate_condition": 10, "exercices": [ + {"exercice_id": exo1['id_code'], "quantity": 3}]}}) parcours_id = r.json()['id_code'] memberws.receive_json() @@ -868,7 +1024,7 @@ def test_get_challenge_auth(client: TestClient): admin.receive_json() # disconnect admin.receive_json() admin.send_json({"type": "sub_parcours", "data": { - "parcours_id": parcours_id}}) + "parcours_id": parcours_id}}) r = client.get(url=f'/room/{room["room"]}/challenge/{parcours_id}', headers={"Authorization": "Bearer " + token2}) @@ -879,22 +1035,26 @@ def test_get_challenge_auth(client: TestClient): filled_obj = [ [{**e, 'inputs': [{"index": 0, "value": str(data[0].index(e))}]} for e in data[0]]] r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token2}) + f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', + json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token2}) + + assert r.json() == {"id_code": r.json()['id_code'], "note": {"value": 1, "total": 3}, "time": 162, + "validated": False, "isCorriged": True, "data": [ + [{**e, 'inputs': [{**e['inputs'][0], "correction": '1'}]} for e in filled_obj[0]]]} - assert r.json() == {"id_code": r.json()['id_code'], "note": {"value": 1, "total": 3}, "time": 162, "validated": False, "isCorriged": True, "data": [ - [{**e, 'inputs': [{**e['inputs'][0], "correction": '1'}]} for e in filled_obj[0]]]} - rr = client.get( - f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', headers={"Authorization": "Bearer " + token2}) + f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', + headers={"Authorization": "Bearer " + token2}) assert rr.json() == r.json() + def test_challenge_auth_bad_obj(client: TestClient): token = test_register(client, username="lilian")['access'] token2 = test_register(client, username="lilian2")['access'] room = test_create_room_auth(client=client, token=token, public=True) exo1 = test_create(name="test1", client=client, user={ - "token": token, "username": "lilian"}) + "token": token, "username": "lilian"}) with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { @@ -906,50 +1066,63 @@ def test_challenge_auth_bad_obj(client: TestClient): {"type": "join", "data": {"token": token2}}) mdata = memberws.receive_json() - r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={"parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, "exercices": [ - {"exercice_id": exo1['id_code'], "quantity": 3}]}}) + r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={ + "parcours": {"name": "test_parcours", "time": 10 * 60, "validate_condition": 10, "exercices": [ + {"exercice_id": exo1['id_code'], "quantity": 3}]}}) parcours_id = r.json()['id_code'] memberws.receive_json() - - + r = client.get(url=f'/room/{room["room"]}/challenge/{parcours_id}', headers={"Authorization": "Bearer " + token2}) - + data = r.json()['challenge'] - id_code = r.json()['id_code'] + id_code = r.json()['id_code'] filled_obj = [ [{**e, 'inputs': [{"index": 0, "value": str(data[0].index(e))}]} for e in data[0]]] r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{id_code}', json={'challenge': [*filled_obj, "test"], "time": 162}, headers={"Authorization": "Bearer " + token2}) + f'/room/{room["room"]}/challenge/{parcours_id}/{id_code}', + json={'challenge': [*filled_obj, "test"], "time": 162}, headers={"Authorization": "Bearer " + token2}) assert r.json() == {'detail': { 'challenge_error': 'value is not a valid list'}} - + r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{id_code}', json={'challenge': [*filled_obj, ["test"]], "time": 162}, headers={"Authorization": "Bearer " + token2}) + f'/room/{room["room"]}/challenge/{parcours_id}/{id_code}', + json={'challenge': [*filled_obj, ["test"]], "time": 162}, headers={"Authorization": "Bearer " + token2}) assert r.json() == {'detail': { 'challenge_error': 'value is not a valid dict'}} r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{id_code}', json={'challenge': [*filled_obj, [{"test": "test"}]], "time": 162}, headers={"Authorization": "Bearer " + token2}) + f'/room/{room["room"]}/challenge/{parcours_id}/{id_code}', + json={'challenge': [*filled_obj, [{"test": "test"}]], "time": 162}, + headers={"Authorization": "Bearer " + token2}) assert r.json() == {'detail': { 'calcul_error': 'field required', 'inputs_error': 'field required'}} r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{id_code}', json={'challenge': [*filled_obj, filled_obj[0]], "time": 162}, headers={"Authorization": "Bearer " + token2}) - assert r.json() == {'detail': {"challenge_error": "Object does not correspond to correction"}} - + f'/room/{room["room"]}/challenge/{parcours_id}/{id_code}', + json={'challenge': [*filled_obj, filled_obj[0]], "time": 162}, + headers={"Authorization": "Bearer " + token2}) + assert r.json() == {'detail': { + "challenge_error": "Object does not correspond to correction"}} + r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{id_code}', json={'challenge': [*filled_obj, filled_obj[0]], "time": 162}, headers={"Authorization": "Bearer " + token2}) - assert r.json() == {'detail': {"challenge_error": "Object does not correspond to correction"}} + f'/room/{room["room"]}/challenge/{parcours_id}/{id_code}', + json={'challenge': [*filled_obj, filled_obj[0]], "time": 162}, + headers={"Authorization": "Bearer " + token2}) + assert r.json() == {'detail': { + "challenge_error": "Object does not correspond to correction"}} + def stripKey(dict, key): - return {k:v for k,v in dict.items() if k != key} + return {k: v for k, v in dict.items() if k != key} + + def test_get_parcours_member(client: TestClient): token = test_register(client, username="lilian")['access'] token2 = test_register(client, username="lilian2")['access'] room = test_create_room_auth(client=client, token=token, public=True) exo1 = test_create(name="test1", client=client, user={ - "token": token, "username": "lilian"}) + "token": token, "username": "lilian"}) with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { @@ -961,53 +1134,61 @@ def test_get_parcours_member(client: TestClient): {"type": "join", "data": {"token": token2}}) mdata = memberws.receive_json() - r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={"parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, "exercices": [ - {"exercice_id": exo1['id_code'], "quantity": 3}]}}) + r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={ + "parcours": {"name": "test_parcours", "time": 10 * 60, "validate_condition": 10, "exercices": [ + {"exercice_id": exo1['id_code'], "quantity": 3}]}}) parcours_id = r.json()['id_code'] memberws.receive_json() - - + r = client.get(url=f'/room/{room["room"]}/challenge/{parcours_id}', headers={"Authorization": "Bearer " + token2}) data = r.json()['challenge'] filled_obj = [ [{**e, 'inputs': [{"index": 0, "value": str(data[0].index(e))}]} for e in data[0]]] r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token2}) - challenge1 = {**stripKey(r.json(), "data"), "challenger": mdata['data']['member']['id_code'], "canCorrige": True} - + f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', + json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token2}) + challenge1 = {**stripKey(r.json(), "data"), + "challenger": mdata['data']['member']['id_code'], "canCorrige": True} + r = client.get(url=f'/room/{room["room"]}/challenge/{parcours_id}', headers={"Authorization": "Bearer " + token2}) data = r.json()['challenge'] filled_obj = [ [{**e, 'inputs': [{"index": 0, "value": str(data[0].index(e))}]} for e in data[0]]] r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token2}) - challenge2 ={**stripKey(r.json(), "data"), "challenger": mdata['data']['member']['id_code'], "canCorrige": True} - - + f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', + json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token2}) + challenge2 = {**stripKey(r.json(), "data"), + "challenger": mdata['data']['member']['id_code'], "canCorrige": True} + r = client.get(url=f'/room/{room["room"]}/challenge/{parcours_id}', headers={"Authorization": "Bearer " + token}) data = r.json()['challenge'] filled_obj = [ [{**e, 'inputs': [{"index": 0, "value": str(data[0].index(e))}]} for e in data[0]]] r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token}) - challenge3 ={**stripKey(r.json(), "data"), "challenger": mdata['data']['member']['id_code'], "canCorrige": True} - - + f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', + json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token}) + challenge3 = {**stripKey(r.json(), "data"), + "challenger": mdata['data']['member']['id_code'], "canCorrige": True} + r = client.get(url=f'/room/{room["room"]}/parcours/{parcours_id}', headers={"Authorization": "Bearer " + token2}) - assert r.json() == {'name': 'test_parcours', 'time': 600, 'validate_condition': 10, 'id_code': parcours_id, 'exercices': [{'exercice_id': exo1['id_code'], 'quantity': 3, 'name': 'test1'}], 'challenges': [challenge1, challenge2]} - + assert r.json() == {'name': 'test_parcours', 'time': 600, 'validate_condition': 10, 'id_code': parcours_id, + 'exercices': [ + {'exercice_id': exo1['id_code'], 'quantity': 3, 'name': 'test1'}], + 'challenges': [challenge1, challenge2]} + + def test_get_parcours_admin(client: TestClient): token = test_register(client, username="lilian")['access'] token2 = test_register(client, username="lilian2")['access'] room = test_create_room_auth(client=client, token=token, public=True) exo1 = test_create(name="test1", client=client, user={ - "token": token, "username": "lilian"}) + "token": token, "username": "lilian"}) with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { @@ -1019,60 +1200,67 @@ def test_get_parcours_admin(client: TestClient): {"type": "join", "data": {"token": token2}}) mdata = memberws.receive_json() - r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={"parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, "exercices": [ - {"exercice_id": exo1['id_code'], "quantity": 3}]}}) + r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={ + "parcours": {"name": "test_parcours", "time": 10 * 60, "validate_condition": 10, "exercices": [ + {"exercice_id": exo1['id_code'], "quantity": 3}]}}) parcours_id = r.json()['id_code'] memberws.receive_json() - - + r = client.get(url=f'/room/{room["room"]}/challenge/{parcours_id}', headers={"Authorization": "Bearer " + token2}) data = r.json()['challenge'] filled_obj = [ [{**e, 'inputs': [{"index": 0, "value": str(data[0].index(e))}]} for e in data[0]]] r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token2}) - challenge1 = {**stripKey(r.json(), "data"), "challenger": mdata['data']['member']['id_code'], "canCorrige": True} - + f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', + json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token2}) + challenge1 = {**stripKey(r.json(), "data"), + "challenger": mdata['data']['member']['id_code'], "canCorrige": True} + r = client.get(url=f'/room/{room["room"]}/challenge/{parcours_id}', headers={"Authorization": "Bearer " + token2}) data = r.json()['challenge'] filled_obj = [ [{**e, 'inputs': [{"index": 0, "value": str(data[0].index(e))}]} for e in data[0]]] r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token2}) - challenge2 ={**stripKey(r.json(), "data"), "challenger": mdata['data']['member']['id_code'], "canCorrige": True} - - + f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', + json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token2}) + challenge2 = {**stripKey(r.json(), "data"), + "challenger": mdata['data']['member']['id_code'], "canCorrige": True} + r = client.get(url=f'/room/{room["room"]}/challenge/{parcours_id}', headers={"Authorization": "Bearer " + token}) data = r.json()['challenge'] filled_obj = [ [{**e, 'inputs': [{"index": 0, "value": str(data[0].index(e))}]} for e in data[0]]] r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token}) - challenge3 ={**stripKey(r.json(), "data"), "challenger": adata['data']['member']['id_code'], "canCorrige": True} - - + f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', + json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token}) + challenge3 = {**stripKey(r.json(), "data"), + "challenger": adata['data']['member']['id_code'], "canCorrige": True} + r = client.get(url=f'/room/{room["room"]}/parcours/{parcours_id}', headers={"Authorization": "Bearer " + token}) - assert {**r.json(), "challenges": r.json()['challenges'].sort(key=lambda d: d['id_code']) }== {'name': 'test_parcours', 'time': 600, 'validate_condition': 10, 'id_code': parcours_id, 'exercices': [{'exercice_id': exo1['id_code'], 'quantity': 3, 'name': 'test1'}], 'challenges': [challenge1, challenge2, challenge3].sort(key = lambda d: d['id_code'])} - - + assert {**r.json(), "challenges": r.json()['challenges'].sort(key=lambda d: d['id_code'])} == { + 'name': 'test_parcours', 'time': 600, 'validate_condition': 10, 'id_code': parcours_id, 'exercices': [ + {'exercice_id': exo1['id_code'], 'quantity': 3, 'name': 'test1'}], + 'challenges': [challenge1, challenge2, challenge3].sort(key=lambda d: d['id_code'])} + + def test_update_parcours_auth(client: TestClient): token = test_register(client, username="lilian")['access'] token2 = test_register(client, username="lilian2")['access'] room = test_create_room_auth(client=client, token=token, public=True) exo1 = test_create(name="test1", client=client, user={ - "token": token, "username": "lilian"}) + "token": token, "username": "lilian"}) exo2 = test_create(name="test2", client=client, user={ - "token": token, "username": "lilian"}) + "token": token, "username": "lilian"}) exo3 = test_create(name="test3", client=client, user={ - "token": token, "username": "lilian"}) + "token": token, "username": "lilian"}) exo4 = test_create(name="test4", client=client, user={ - "token": token, "username": "lilian"}) - + "token": token, "username": "lilian"}) + with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { "token": token}}) @@ -1083,44 +1271,55 @@ def test_update_parcours_auth(client: TestClient): {"type": "join", "data": {"token": token2}}) mdata = memberws.receive_json() - r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={"parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, "exercices": [ - {"exercice_id": exo1['id_code'], "quantity": 3}]}}) - + r = client.post(f'/room/{room["room"]}/parcours', headers={"Authorization": "Bearer " + token}, json={ + "parcours": {"name": "test_parcours", "time": 10 * 60, "validate_condition": 10, "exercices": [ + {"exercice_id": exo1['id_code'], "quantity": 3}]}}) + parcours_id = r.json()['id_code'] memberws.receive_json() - - + r = client.get(url=f'/room/{room["room"]}/challenge/{parcours_id}', headers={"Authorization": "Bearer " + token2}) data = r.json()['challenge'] filled_obj = [ [{**e, 'inputs': [{"index": 0, "value": str(data[0].index(e))}]} for e in data[0]]] r = client.post( - f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token2}) - challenge1 = {**stripKey(r.json(), "data"), "challenger": mdata['data']['member']['id_code'], "canCorrige": True} - - + f'/room/{room["room"]}/challenge/{parcours_id}/{r.json()["id_code"]}', + json={'challenge': filled_obj, "time": 162}, headers={"Authorization": "Bearer " + token2}) + challenge1 = {**stripKey(r.json(), "data"), + "challenger": mdata['data']['member']['id_code'], "canCorrige": True} + r = client.get(url=f'/room/{room["room"]}/parcours/{parcours_id}', headers={"Authorization": "Bearer " + token}) - assert {**r.json(), "challenges": r.json()['challenges'].sort(key=lambda d: d['id_code']) }== {'name': 'test_parcours', 'time': 600, 'validate_condition': 10, 'id_code': parcours_id, 'exercices': [{'exercice_id': exo1['id_code'], 'quantity': 3, 'name': 'test1'}], 'challenges': [challenge1].sort(key = lambda d: d['id_code'])} - - r = client.put(url=f'/room/{room["room"]}/parcours/{parcours_id}', json={"parcours": {"name": "test_parcours", "time": 10*60, "validate_condition": 10, "exercices": [ - {"exercice_id": "lolilol", "quantity": 12}, {"exercice_id": exo1['id_code'], "quantity": 10}, {"exercice_id": exo2['id_code'], "quantity": 5}, {"exercice_id": exo3['id_code'], "quantity": 12}]}}, headers={'Authorization': "Bearer " + token}) - - assert r.json() == {"id_code": parcours_id, "challenges": [], "name": "test_parcours", "time": 10*60, "validate_condition": 10, "exercices": [{ - "exercice_id": exo1['id_code'], "name": exo1['name'], "quantity": 10}, {"exercice_id": exo2['id_code'], "name": exo2['name'], "quantity": 5}, {"exercice_id": exo3['id_code'], "name": exo3['name'], "quantity": 12}]} + assert {**r.json(), "challenges": r.json()['challenges'].sort(key=lambda d: d['id_code'])} == { + 'name': 'test_parcours', 'time': 600, 'validate_condition': 10, + 'id_code': parcours_id, 'exercices': [{'exercice_id': exo1['id_code'], 'quantity': 3, 'name': 'test1'}], + 'challenges': [challenge1].sort(key=lambda d: d['id_code'])} + + r = client.put(url=f'/room/{room["room"]}/parcours/{parcours_id}', json={ + "parcours": {"name": "test_parcours", "time": 10 * 60, "validate_condition": 10, "exercices": [ + {"exercice_id": "lolilol", "quantity": 12}, {"exercice_id": exo1['id_code'], "quantity": 10}, + {"exercice_id": exo2['id_code'], "quantity": 5}, {"exercice_id": exo3['id_code'], "quantity": 12}]}}, + headers={'Authorization': "Bearer " + token}) + + assert r.json() == {"id_code": parcours_id, "challenges": [], "name": "test_parcours", "time": 10 * 60, + "validate_condition": 10, "exercices": [{ + "exercice_id": exo1['id_code'], "name": exo1['name'], "quantity": 10}, + {"exercice_id": exo2['id_code'], "name": exo2['name'], "quantity": 5}, + {"exercice_id": exo3['id_code'], "name": exo3['name'], "quantity": 12}]} admin.receive_json() admin.receive_json() admin.receive_json() adata = admin.receive_json() assert adata == {"type": "update_parcours", "data": {"parcours": { "name": "test_parcours", "id_code": parcours_id, "best_note": None}}} - + + def test_change_name(client: TestClient): token = test_register(client, username="lilian")['access'] token2 = test_register(client, username="lilian2")['access'] room = test_create_room_auth(client=client, token=token, public=True) - + with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { "token": token}}) @@ -1130,19 +1329,20 @@ def test_change_name(client: TestClient): memberws.send_json( {"type": "join", "data": {"token": token2}}) mdata = memberws.receive_json() - + admin.receive_json() admin.send_json({"type": "set_name", "data": {"name": "new_name"}}) adata = admin.receive_json() assert adata == {'type': "new_name", "data": {"name": "new_name"}} mdata = memberws.receive_json() assert mdata == {'type': "new_name", "data": {"name": "new_name"}} - + + def test_change_name_too_long(client: TestClient): token = test_register(client, username="lilian")['access'] token2 = test_register(client, username="lilian2")['access'] room = test_create_room_auth(client=client, token=token, public=True) - + with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { "token": token}}) @@ -1152,20 +1352,20 @@ def test_change_name_too_long(client: TestClient): memberws.send_json( {"type": "join", "data": {"token": token2}}) mdata = memberws.receive_json() - + admin.receive_json() - admin.send_json({"type": "set_name", "data": {"name": "new_name"*20}}) + admin.send_json( + {"type": "set_name", "data": {"name": "new_name" * 20}}) adata = admin.receive_json() assert adata == {'type': "error", "data": { "msg": "Nom trop long (max 20 character)"}} - - - + + def test_change_status(client: TestClient): token = test_register(client, username="lilian")['access'] token2 = test_register(client, username="lilian2")['access'] room = test_create_room_auth(client=client, token=token, public=True) - + with client.websocket_connect(f"/ws/room/" + room['room']) as admin: admin.send_json({"type": "login", "data": { "token": token}}) @@ -1175,13 +1375,16 @@ def test_change_status(client: TestClient): memberws.send_json( {"type": "join", "data": {"token": token2}}) mdata = memberws.receive_json() - + admin.receive_json() - admin.send_json({"type": "set_visibility", "data": {"public": False}}) + admin.send_json({"type": "set_visibility", + "data": {"public": False}}) adata = admin.receive_json() - assert adata == {'type': "new_visibility", "data": {"public": False}} + assert adata == {'type': "new_visibility", + "data": {"public": False}} mdata = memberws.receive_json() - assert mdata == {'type': "new_visibility", "data": {"public": False}} + assert mdata == {'type': "new_visibility", + "data": {"public": False}} with client.websocket_connect(f"/ws/room/" + room['room']) as m2: m2.send_json( {"type": "join", "data": {"username": "okok"}}) @@ -1189,12 +1392,7 @@ def test_change_status(client: TestClient): assert "waiter_id" in mdata['data']['waiter'] assert mdata == {"type": "waiting", "data": {"waiter": { "username": "okok", "waiter_id": mdata['data']['waiter']['waiter_id']}}} - + adata = admin.receive_json() assert adata == {'type': "waiter", 'data': { "waiter": {"waiter_id": mdata['data']['waiter']['waiter_id'], "username": "okok"}}} - - - - - \ No newline at end of file diff --git a/backend/api/tests/testing_exo_source/exo_source_web_only.py b/backend/api/tests/testing_exo_source/exo_source_web_only.py index b28179d..2d46948 100644 --- a/backend/api/tests/testing_exo_source/exo_source_web_only.py +++ b/backend/api/tests/testing_exo_source/exo_source_web_only.py @@ -8,4 +8,4 @@ Fonction main() qui doit renvoyer un objet avec: def main(): t = random.randint(1, 10) - return {"csv": None, 'web': "None","pdf": None, "calcul": "1+1=2"} + return {"csv": None, 'web': "1 + [] = 2","pdf": None, "calcul": "1+1=2"} diff --git a/frontend/.idea/.gitignore b/frontend/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/frontend/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/frontend/.idea/dbnavigator.xml b/frontend/.idea/dbnavigator.xml new file mode 100644 index 0000000..9601314 --- /dev/null +++ b/frontend/.idea/dbnavigator.xml @@ -0,0 +1,420 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/.idea/frontend.iml b/frontend/.idea/frontend.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/frontend/.idea/frontend.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/.idea/inspectionProfiles/Project_Default.xml b/frontend/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/frontend/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/frontend/.idea/misc.xml b/frontend/.idea/misc.xml new file mode 100644 index 0000000..639900d --- /dev/null +++ b/frontend/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/.idea/modules.xml b/frontend/.idea/modules.xml new file mode 100644 index 0000000..f3d93d7 --- /dev/null +++ b/frontend/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/.idea/vcs.xml b/frontend/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/frontend/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts new file mode 100644 index 0000000..17161e3 --- /dev/null +++ b/frontend/cypress.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, +}); diff --git a/frontend/cypress/e2e/exos.cy.ts b/frontend/cypress/e2e/exos.cy.ts new file mode 100644 index 0000000..d64d400 --- /dev/null +++ b/frontend/cypress/e2e/exos.cy.ts @@ -0,0 +1,7 @@ +// type definitions for Cypress object "cy" +/// +describe('test exo', ()=>{ + it('test exo', ()=>{ + cy.visit('localhost:5173') + }) +}) \ No newline at end of file diff --git a/frontend/cypress/fixtures/example.json b/frontend/cypress/fixtures/example.json new file mode 100644 index 0000000..02e4254 --- /dev/null +++ b/frontend/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/frontend/cypress/support/commands.ts b/frontend/cypress/support/commands.ts new file mode 100644 index 0000000..698b01a --- /dev/null +++ b/frontend/cypress/support/commands.ts @@ -0,0 +1,37 @@ +/// +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable +// drag(subject: string, options?: Partial): Chainable +// dismiss(subject: string, options?: Partial): Chainable +// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable +// } +// } +// } \ No newline at end of file diff --git a/frontend/cypress/support/e2e.ts b/frontend/cypress/support/e2e.ts new file mode 100644 index 0000000..f80f74f --- /dev/null +++ b/frontend/cypress/support/e2e.ts @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index e90595c..ff47b1f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,6 +3,7 @@ "version": "0.0.1", "private": true, "scripts": { + "test": "npx vitest run --reporter verbose", "dev": "vite dev", "build": "vite build", "preview": "vite preview", @@ -14,12 +15,17 @@ "devDependencies": { "@sveltejs/adapter-auto": "^1.0.0", "@sveltejs/kit": "^1.0.0", + "@testing-library/svelte": "^3.2.2", "@types/chroma-js": "^2.1.4", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", + "@vitest/browser": "^0.28.4", + "@vitest/ui": "^0.28.4", + "cypress": "^12.6.0", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte3": "^4.0.0", + "jsdom": "^21.1.0", "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.8.1", "sass": "^1.53.0", @@ -28,21 +34,27 @@ "svelte-preprocess": "^4.10.7", "tslib": "^2.4.1", "typescript": "^4.9.3", - "vite": "^4.0.0" + "vite": "^4.0.0", + "vitest": "^0.28.4" }, "type": "module", "dependencies": { "@sveltestack/svelte-query": "^1.6.0", + "@testing-library/jest-dom": "^5.16.5", "@types/qs": "^6.9.7", "axios": "^1.2.2", "chroma-js": "^2.4.2", "jwt-decode": "^3.1.2", "qs": "^6.11.0", + "reconnecting-websocket": "^4.4.0", "svelecte": "^3.13.0", "svelte-forms": "^2.3.1", + "svelte-htm": "^1.2.0", "svelte-icons": "^2.1.0", + "svelte-markdown": "^0.2.3", "svelte-multiselect": "^8.2.3", "svelte-navigator": "^3.2.2", - "svelte-routing": "^1.6.0" + "svelte-routing": "^1.6.0", + "svelte-websocket-store": "^1.1.33" } } \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5fb01a1..f01752d 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -4,56 +4,77 @@ specifiers: '@sveltejs/adapter-auto': ^1.0.0 '@sveltejs/kit': ^1.0.0 '@sveltestack/svelte-query': ^1.6.0 + '@testing-library/jest-dom': ^5.16.5 + '@testing-library/svelte': ^3.2.2 '@types/chroma-js': ^2.1.4 '@types/qs': ^6.9.7 '@typescript-eslint/eslint-plugin': ^5.45.0 '@typescript-eslint/parser': ^5.45.0 + '@vitest/browser': ^0.28.4 + '@vitest/ui': ^0.28.4 axios: ^1.2.2 chroma-js: ^2.4.2 + cypress: ^12.6.0 eslint: ^8.28.0 eslint-config-prettier: ^8.5.0 eslint-plugin-svelte3: ^4.0.0 + jsdom: ^21.1.0 jwt-decode: ^3.1.2 prettier: ^2.8.0 prettier-plugin-svelte: ^2.8.1 qs: ^6.11.0 + reconnecting-websocket: ^4.4.0 sass: ^1.53.0 svelecte: ^3.13.0 svelte: ^3.54.0 svelte-check: ^2.9.2 svelte-forms: ^2.3.1 + svelte-htm: ^1.2.0 svelte-icons: ^2.1.0 + svelte-markdown: ^0.2.3 svelte-multiselect: ^8.2.3 svelte-navigator: ^3.2.2 svelte-preprocess: ^4.10.7 svelte-routing: ^1.6.0 + svelte-websocket-store: ^1.1.33 tslib: ^2.4.1 typescript: ^4.9.3 vite: ^4.0.0 + vitest: ^0.28.4 dependencies: '@sveltestack/svelte-query': 1.6.0 + '@testing-library/jest-dom': 5.16.5 '@types/qs': 6.9.7 axios: 1.2.2 chroma-js: 2.4.2 jwt-decode: 3.1.2 qs: 6.11.0 + reconnecting-websocket: 4.4.0 svelecte: 3.13.0 svelte-forms: 2.3.1 + svelte-htm: 1.2.0_svelte@3.55.0 svelte-icons: 2.1.0 + svelte-markdown: 0.2.3_svelte@3.55.0 svelte-multiselect: 8.2.3 svelte-navigator: 3.2.2_niwyv7xychq2ag6arq5eqxbomm svelte-routing: 1.6.0_niwyv7xychq2ag6arq5eqxbomm + svelte-websocket-store: 1.1.33 devDependencies: '@sveltejs/adapter-auto': 1.0.0_@sveltejs+kit@1.0.1 '@sveltejs/kit': 1.0.1_svelte@3.55.0+vite@4.0.3 + '@testing-library/svelte': 3.2.2_svelte@3.55.0 '@types/chroma-js': 2.1.4 '@typescript-eslint/eslint-plugin': 5.47.1_txmweb6yn7coi7nfrp22gpyqmy '@typescript-eslint/parser': 5.47.1_lzzuuodtsqwxnvqeq4g4likcqa + '@vitest/browser': 0.28.4 + '@vitest/ui': 0.28.4 + cypress: 12.6.0 eslint: 8.30.0 eslint-config-prettier: 8.5.0_eslint@8.30.0 eslint-plugin-svelte3: 4.0.0_khrjkzzv5v2x7orkj5o7sxbz3a + jsdom: 21.1.0 prettier: 2.8.1 prettier-plugin-svelte: 2.9.0_ajxj753sv7dbwexjherrch25ta sass: 1.57.1 @@ -63,9 +84,86 @@ devDependencies: tslib: 2.4.1 typescript: 4.9.4 vite: 4.0.3_sass@1.57.1 + vitest: 0.28.4_x3jjsgulbh7575qaqkjiilw4p4 packages: + /@adobe/css-tools/4.1.0: + resolution: {integrity: sha512-mMVJ/j/GbZ/De4ZHWbQAQO1J6iVnjtZLc9WEdkUQb8S/Bu2cAF2bETXUgMAdvMG3/ngtKmcNBe+Zms9bg6jnQQ==} + dev: false + + /@babel/code-frame/7.18.6: + resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.18.6 + + /@babel/helper-validator-identifier/7.19.1: + resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} + engines: {node: '>=6.9.0'} + + /@babel/highlight/7.18.6: + resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.19.1 + chalk: 2.4.2 + js-tokens: 4.0.0 + + /@babel/runtime-corejs3/7.20.13: + resolution: {integrity: sha512-p39/6rmY9uvlzRiLZBIB3G9/EBr66LBMcYm7fIDeSBNdRjF2AGD3rFZucUyAgGHC2N+7DdLvVi33uTjSE44FIw==} + engines: {node: '>=6.9.0'} + dependencies: + core-js-pure: 3.28.0 + regenerator-runtime: 0.13.11 + dev: false + + /@babel/runtime/7.20.13: + resolution: {integrity: sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + + /@colors/colors/1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + requiresBuild: true + dev: true + optional: true + + /@cypress/request/2.88.11: + resolution: {integrity: sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==} + engines: {node: '>= 6'} + dependencies: + aws-sign2: 0.7.0 + aws4: 1.12.0 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + http-signature: 1.3.6 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + performance-now: 2.1.0 + qs: 6.10.4 + safe-buffer: 5.2.1 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 8.3.2 + dev: true + + /@cypress/xvfb/1.2.4_supports-color@8.1.1: + resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} + dependencies: + debug: 3.2.7_supports-color@8.1.1 + lodash.once: 4.1.1 + transitivePeerDependencies: + - supports-color + dev: true + /@esbuild/android-arm/0.16.11: resolution: {integrity: sha512-j2xsG1OETgCe+OBA54DG5vLuGjmMZtQvyxt+rTw2aYK/RqjcG/F+UDdj43uoUOv8lSRC3lM4XpKLOVZfY/82yA==} engines: {node: '>=12'} @@ -156,6 +254,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64/0.14.54: + resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64/0.16.11: resolution: {integrity: sha512-2MCYdDBh9R+R1xuBFiApgkbp/tW1uV+aVeefKYqWSEk3o6MHzWo1FxEGA4dSnC+kThSBOMVpCV9z4/DPouA3bQ==} engines: {node: '>=12'} @@ -301,6 +408,32 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@jest/expect-utils/29.4.2: + resolution: {integrity: sha512-Dd3ilDJpBnqa0GiPN7QrudVs0cczMMHtehSo2CSTjm3zdHx0RcpmhFNVEltuEFeqfLIyWKFI224FsMSQ/nsJQA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.4.2 + dev: false + + /@jest/schemas/29.4.2: + resolution: {integrity: sha512-ZrGzGfh31NtdVH8tn0mgJw4khQuNHiKqdzJAFbCaERbyCP9tHlxWuL/mnMu8P7e/+k4puWjI1NOzi/sFsjce/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.25.21 + dev: false + + /@jest/types/29.4.2: + resolution: {integrity: sha512-CKlngyGP0fwlgC1BRUtPZSiWLBhyS9dKwKmyGxk8Z6M82LBEGB2aLQSg+U1MyLsU+M7UjnlLllBM2BLWKVm/Uw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.4.2 + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 18.11.18 + '@types/yargs': 17.0.22 + chalk: 4.1.2 + dev: false + /@jridgewell/resolve-uri/3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} @@ -317,6 +450,10 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true + /@jspm/core/2.0.0-beta.24: + resolution: {integrity: sha512-a4Bo/80Z6CoJNor5ldgs6002utmmbttP4JYd/FJ0Ob2fVdf6O6ha5SORBCqrnDnBvMc1TlrHY7dCfat5+H0a6A==} + dev: true + /@nodelib/fs.scandir/2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -342,6 +479,33 @@ packages: resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} dev: true + /@rollup/plugin-inject/4.0.4_rollup@2.79.1: + resolution: {integrity: sha512-4pbcU4J/nS+zuHk+c+OL3WtmEQhqxlZ9uqfjQMQDOHOPld7PsCd8k5LWs8h5wjwJN7MgnAn768F2sDxEP4eNFQ==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0_rollup@2.79.1 + estree-walker: 2.0.2 + magic-string: 0.25.9 + rollup: 2.79.1 + dev: true + + /@rollup/pluginutils/3.1.0_rollup@2.79.1: + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.1 + dev: true + + /@sinclair/typebox/0.25.21: + resolution: {integrity: sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g==} + dev: false + /@sveltejs/adapter-auto/1.0.0_@sveltejs+kit@1.0.1: resolution: {integrity: sha512-yKyPvlLVua1bJ/42FrR3X041mFGdB4GzTZOAEoHUcNBRE5Mhx94+eqHpC3hNvAOiLEDcKfVO0ObyKSu7qldU+w==} peerDependencies: @@ -407,6 +571,64 @@ packages: optional: true dev: false + /@testing-library/dom/8.20.0: + resolution: {integrity: sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==} + engines: {node: '>=12'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/runtime': 7.20.13 + '@types/aria-query': 5.0.1 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.4.4 + pretty-format: 27.5.1 + dev: true + + /@testing-library/jest-dom/5.16.5: + resolution: {integrity: sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==} + engines: {node: '>=8', npm: '>=6', yarn: '>=1'} + dependencies: + '@adobe/css-tools': 4.1.0 + '@babel/runtime': 7.20.13 + '@types/testing-library__jest-dom': 5.14.5 + aria-query: 5.1.3 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.5.16 + lodash: 4.17.21 + redent: 3.0.0 + dev: false + + /@testing-library/svelte/3.2.2_svelte@3.55.0: + resolution: {integrity: sha512-IKwZgqbekC3LpoRhSwhd0JswRGxKdAGkf39UiDXTywK61YyLXbCYoR831e/UUC6EeNW4hiHPY+2WuovxOgI5sw==} + engines: {node: '>= 10'} + peerDependencies: + svelte: 3.x + dependencies: + '@testing-library/dom': 8.20.0 + svelte: 3.55.0 + dev: true + + /@tootallnate/once/2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + dev: true + + /@types/aria-query/5.0.1: + resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} + dev: true + + /@types/chai-subset/1.3.3: + resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} + dependencies: + '@types/chai': 4.3.4 + dev: true + + /@types/chai/4.3.4: + resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} + dev: true + /@types/chroma-js/2.1.4: resolution: {integrity: sha512-l9hWzP7cp7yleJUI7P2acmpllTJNYf5uU6wh50JzSIZt3fFHe+w2FM6w9oZGBTYzjjm2qHdnQvI+fF/JF/E5jQ==} dev: true @@ -415,13 +637,47 @@ packages: resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==} dev: true + /@types/estree/0.0.39: + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + dev: true + + /@types/istanbul-lib-coverage/2.0.4: + resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + dev: false + + /@types/istanbul-lib-report/3.0.0: + resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.4 + dev: false + + /@types/istanbul-reports/3.0.1: + resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} + dependencies: + '@types/istanbul-lib-report': 3.0.0 + dev: false + + /@types/jest/29.4.0: + resolution: {integrity: sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==} + dependencies: + expect: 29.4.2 + pretty-format: 29.4.2 + dev: false + /@types/json-schema/7.0.11: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} dev: true + /@types/marked/4.0.8: + resolution: {integrity: sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw==} + dev: false + + /@types/node/14.18.36: + resolution: {integrity: sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==} + dev: true + /@types/node/18.11.18: resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==} - dev: true /@types/pug/2.0.6: resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==} @@ -441,6 +697,42 @@ packages: resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} dev: true + /@types/sinonjs__fake-timers/8.1.1: + resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} + dev: true + + /@types/sizzle/2.3.3: + resolution: {integrity: sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==} + dev: true + + /@types/stack-utils/2.0.1: + resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} + dev: false + + /@types/testing-library__jest-dom/5.14.5: + resolution: {integrity: sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==} + dependencies: + '@types/jest': 29.4.0 + dev: false + + /@types/yargs-parser/21.0.0: + resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} + dev: false + + /@types/yargs/17.0.22: + resolution: {integrity: sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g==} + dependencies: + '@types/yargs-parser': 21.0.0 + dev: false + + /@types/yauzl/2.10.0: + resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} + requiresBuild: true + dependencies: + '@types/node': 18.11.18 + dev: true + optional: true + /@typescript-eslint/eslint-plugin/5.47.1_txmweb6yn7coi7nfrp22gpyqmy: resolution: {integrity: sha512-r4RZ2Jl9kcQN7K/dcOT+J7NAimbiis4sSM9spvWimsBvDegMhKLA5vri2jG19PmIPbDjPeWzfUPQ2hjEzA4Nmg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -570,6 +862,70 @@ packages: eslint-visitor-keys: 3.3.0 dev: true + /@vitest/browser/0.28.4: + resolution: {integrity: sha512-jIbiVf2VBIvOx1kkLoTU+ehqNM0m932lO3ZvbOqqR3FHbbxUCWTrkOyFwQRFMfbpVJ9oVAsK8naccIGj1aPeMQ==} + dependencies: + '@vitest/runner': 0.28.4 + local-pkg: 0.4.3 + mlly: 1.1.0 + modern-node-polyfills: 0.0.9 + rollup-plugin-node-polyfills: 0.2.1 + sirv: 2.0.2 + dev: true + + /@vitest/expect/0.28.4: + resolution: {integrity: sha512-JqK0NZ4brjvOSL8hXAnIsfi+jxDF7rH/ZWCGCt0FAqRnVFc1hXsfwXksQvEnKqD84avRt3gmeXoK4tNbmkoVsQ==} + dependencies: + '@vitest/spy': 0.28.4 + '@vitest/utils': 0.28.4 + chai: 4.3.7 + dev: true + + /@vitest/runner/0.28.4: + resolution: {integrity: sha512-Q8UV6GjDvBSTfUoq0QXVCNpNOUrWu4P2qvRq7ssJWzn0+S0ojbVOxEjMt+8a32X6SdkhF8ak+2nkppsqV0JyNQ==} + dependencies: + '@vitest/utils': 0.28.4 + p-limit: 4.0.0 + pathe: 1.1.0 + dev: true + + /@vitest/spy/0.28.4: + resolution: {integrity: sha512-8WuhfXLlvCXpNXEGJW6Gc+IKWI32435fQJLh43u70HnZ1otJOa2Cmg2Wy2Aym47ZnNCP4NolF+8cUPwd0MigKQ==} + dependencies: + tinyspy: 1.1.1 + dev: true + + /@vitest/ui/0.28.4: + resolution: {integrity: sha512-LQfCCFc17n49mwtraV9/NAWl2DUqJS/9ZEa3fqJjoYO+HowdseQ5jvWflpzliCyfrIAh6cXVo1bNzHnDXe0cbw==} + dependencies: + fast-glob: 3.2.12 + flatted: 3.2.7 + pathe: 1.1.0 + picocolors: 1.0.0 + sirv: 2.0.2 + dev: true + + /@vitest/utils/0.28.4: + resolution: {integrity: sha512-l2QztOLdc2LkR+w/lP52RGh8hW+Ul4KESmCAgVE8q737I7e7bQoAfkARKpkPJ4JQtGpwW4deqlj1732VZD7TFw==} + dependencies: + cli-truncate: 3.1.0 + diff: 5.1.0 + loupe: 2.3.6 + picocolors: 1.0.0 + pretty-format: 27.5.1 + dev: true + + /abab/2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + dev: true + + /acorn-globals/7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + dependencies: + acorn: 8.8.2 + acorn-walk: 8.2.0 + dev: true + /acorn-jsx/5.3.2_acorn@8.8.1: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -578,12 +934,40 @@ packages: acorn: 8.8.1 dev: true + /acorn-walk/8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: true + /acorn/8.8.1: resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} engines: {node: '>=0.4.0'} hasBin: true dev: true + /acorn/8.8.2: + resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /agent-base/6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /aggregate-error/3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + dev: true + /ajv/6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: @@ -593,16 +977,47 @@ packages: uri-js: 4.4.1 dev: true + /ansi-colors/4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + dev: true + + /ansi-escapes/4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: true + /ansi-regex/5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} dev: true + /ansi-regex/6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + + /ansi-styles/3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + /ansi-styles/4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} dependencies: color-convert: 2.0.1 + + /ansi-styles/5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + /ansi-styles/6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} dev: true /anymatch/3.1.3: @@ -613,18 +1028,67 @@ packages: picomatch: 2.3.1 dev: true + /arch/2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + dev: true + /argparse/2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true + /aria-query/5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + dependencies: + deep-equal: 2.2.0 + /array-union/2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} dev: true + /asn1/0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /assert-plus/1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + dev: true + + /assertion-error/1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + + /astral-regex/2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + dev: true + + /async/3.2.4: + resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} + dev: true + /asynckit/0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: false + + /at-least-node/1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + dev: true + + /available-typed-arrays/1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + + /aws-sign2/0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + dev: true + + /aws4/1.12.0: + resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==} + dev: true /axios/1.2.2: resolution: {integrity: sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==} @@ -640,11 +1104,29 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /base64-js/1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /bcrypt-pbkdf/1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + dependencies: + tweetnacl: 0.14.5 + dev: true + /binary-extensions/2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} dev: true + /blob-util/2.0.2: + resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} + dev: true + + /bluebird/3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + dev: true + /brace-expansion/1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -657,12 +1139,22 @@ packages: engines: {node: '>=8'} dependencies: fill-range: 7.0.1 - dev: true /buffer-crc32/0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} dev: true + /buffer-from/1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /buffer/5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + /busboy/1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -670,24 +1162,74 @@ packages: streamsearch: 1.1.0 dev: true + /cac/6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + + /cachedir/2.3.0: + resolution: {integrity: sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==} + engines: {node: '>=6'} + dev: true + /call-bind/1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: function-bind: 1.1.1 - get-intrinsic: 1.1.3 - dev: false + get-intrinsic: 1.2.0 /callsites/3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} dev: true + /caseless/0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + dev: true + + /chai/4.3.7: + resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 4.1.3 + get-func-name: 2.0.0 + loupe: 2.3.6 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + + /chalk/2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + /chalk/3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: false + /chalk/4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + + /check-error/1.0.2: + resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + dev: true + + /check-more-types/2.24.0: + resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} + engines: {node: '>= 0.8.0'} dev: true /chokidar/3.5.3: @@ -709,15 +1251,66 @@ packages: resolution: {integrity: sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==} dev: false + /ci-info/3.8.0: + resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} + engines: {node: '>=8'} + + /clean-stack/2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + dev: true + + /cli-cursor/3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + dependencies: + restore-cursor: 3.1.0 + dev: true + + /cli-table3/0.6.3: + resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==} + engines: {node: 10.* || >= 12.*} + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + dev: true + + /cli-truncate/2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + dev: true + + /cli-truncate/3.1.0: + resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + slice-ansi: 5.0.0 + string-width: 5.1.2 + dev: true + + /color-convert/1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + /color-convert/2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} dependencies: color-name: 1.1.4 - dev: true + + /color-name/1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} /color-name/1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /colorette/2.0.19: + resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} dev: true /combined-stream/1.0.8: @@ -725,7 +1318,16 @@ packages: engines: {node: '>= 0.8'} dependencies: delayed-stream: 1.0.0 - dev: false + + /commander/5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + dev: true + + /common-tags/1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + dev: true /concat-map/0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -736,6 +1338,20 @@ packages: engines: {node: '>= 0.6'} dev: true + /core-js-pure/3.28.0: + resolution: {integrity: sha512-DSOVleA9/v3LNj/vFxAPfUHttKTzrB2RXhAPvR5TPXn4vrra3Z2ssytvRyt8eruJwAfwAiFADEbrjcRdcvPLQQ==} + requiresBuild: true + dev: false + + /core-js/3.28.0: + resolution: {integrity: sha512-GiZn9D4Z/rSYvTeg1ljAIsEqFm0LaN9gVtwDCrKL80zHtS31p9BAjmTxVqTQDMpwlMolJZOFntUG2uwyj7DAqw==} + requiresBuild: true + dev: false + + /core-util-is/1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + dev: true + /cross-spawn/7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -745,6 +1361,107 @@ packages: which: 2.0.2 dev: true + /css.escape/1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + dev: false + + /cssom/0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + dev: true + + /cssom/0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + dev: true + + /cssstyle/2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + dependencies: + cssom: 0.3.8 + dev: true + + /cypress/12.6.0: + resolution: {integrity: sha512-WdHSVaS1lumSd5XpVTslZd8ui9GIGphrzvXq9+3DtVhqjRZC5M70gu5SW/Y/SLPq3D1wiXGZoHC6HJ7ESVE2lw==} + engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0} + hasBin: true + requiresBuild: true + dependencies: + '@cypress/request': 2.88.11 + '@cypress/xvfb': 1.2.4_supports-color@8.1.1 + '@types/node': 14.18.36 + '@types/sinonjs__fake-timers': 8.1.1 + '@types/sizzle': 2.3.3 + arch: 2.2.0 + blob-util: 2.0.2 + bluebird: 3.7.2 + buffer: 5.7.1 + cachedir: 2.3.0 + chalk: 4.1.2 + check-more-types: 2.24.0 + cli-cursor: 3.1.0 + cli-table3: 0.6.3 + commander: 5.1.0 + common-tags: 1.8.2 + dayjs: 1.11.7 + debug: 4.3.4_supports-color@8.1.1 + enquirer: 2.3.6 + eventemitter2: 6.4.7 + execa: 4.1.0 + executable: 4.1.1 + extract-zip: 2.0.1_supports-color@8.1.1 + figures: 3.2.0 + fs-extra: 9.1.0 + getos: 3.2.1 + is-ci: 3.0.1 + is-installed-globally: 0.4.0 + lazy-ass: 1.6.0 + listr2: 3.14.0_enquirer@2.3.6 + lodash: 4.17.21 + log-symbols: 4.1.0 + minimist: 1.2.7 + ospath: 1.2.2 + pretty-bytes: 5.6.0 + proxy-from-env: 1.0.0 + request-progress: 3.0.0 + semver: 7.3.8 + supports-color: 8.1.1 + tmp: 0.2.1 + untildify: 4.0.0 + yauzl: 2.10.0 + dev: true + + /dashdash/1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + dependencies: + assert-plus: 1.0.0 + dev: true + + /data-urls/3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + dev: true + + /dayjs/1.11.7: + resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==} + dev: true + + /debug/3.2.7_supports-color@8.1.1: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 8.1.1 + dev: true + /debug/4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -757,10 +1474,55 @@ packages: ms: 2.1.2 dev: true + /debug/4.3.4_supports-color@8.1.1: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 8.1.1 + dev: true + + /decimal.js/10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: true + /dedent-js/1.0.1: resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} dev: false + /deep-eql/4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + + /deep-equal/2.2.0: + resolution: {integrity: sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==} + dependencies: + call-bind: 1.0.2 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.0 + is-arguments: 1.1.1 + is-array-buffer: 3.0.1 + is-date-object: 1.0.5 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + isarray: 2.0.5 + object-is: 1.1.5 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.4.3 + side-channel: 1.0.4 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.1 + which-typed-array: 1.1.9 + /deep-is/0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -770,10 +1532,16 @@ packages: engines: {node: '>=0.10.0'} dev: true + /define-properties/1.2.0: + resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + /delayed-stream/1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - dev: false /detect-indent/6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} @@ -784,6 +1552,16 @@ packages: resolution: {integrity: sha512-mbjoAaCL2qogBKgeFxFPOXAUsZchircF+B/79LD4sHH0+NHfYm8gZpQrskKDn5gENGt35+5OI1GUF7hLVnkPDw==} dev: true + /diff-sequences/29.4.2: + resolution: {integrity: sha512-R6P0Y6PrsH3n4hUXxL3nns0rbRk6Q33js3ygJBeEpbzLzgcNuJ61+u0RXasFpTKISw99TxUzFnumSnRLsjhLaw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false + + /diff/5.1.0: + resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} + engines: {node: '>=0.3.1'} + dev: true + /dir-glob/3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -798,10 +1576,279 @@ packages: esutils: 2.0.3 dev: true + /dom-accessibility-api/0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + /domexception/4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + dependencies: + webidl-conversions: 7.0.0 + dev: true + + /eastasianwidth/0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + + /ecc-jsbn/0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + dev: true + + /emoji-regex/8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /emoji-regex/9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + + /end-of-stream/1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: true + + /enquirer/2.3.6: + resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} + engines: {node: '>=8.6'} + dependencies: + ansi-colors: 4.1.3 + dev: true + + /entities/4.4.0: + resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} + engines: {node: '>=0.12'} + dev: true + + /es-get-iterator/1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.2 + is-set: 2.0.2 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + /es6-promise/3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} dev: true + /esbuild-android-64/0.14.54: + resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-android-arm64/0.14.54: + resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-64/0.14.54: + resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-arm64/0.14.54: + resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-64/0.14.54: + resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-arm64/0.14.54: + resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-32/0.14.54: + resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-64/0.14.54: + resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm/0.14.54: + resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm64/0.14.54: + resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-mips64le/0.14.54: + resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-ppc64le/0.14.54: + resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-riscv64/0.14.54: + resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-s390x/0.14.54: + resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64/0.14.54: + resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-openbsd-64/0.14.54: + resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-sunos-64/0.14.54: + resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32/0.14.54: + resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64/0.14.54: + resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64/0.14.54: + resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild/0.14.54: + resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/linux-loong64': 0.14.54 + esbuild-android-64: 0.14.54 + esbuild-android-arm64: 0.14.54 + esbuild-darwin-64: 0.14.54 + esbuild-darwin-arm64: 0.14.54 + esbuild-freebsd-64: 0.14.54 + esbuild-freebsd-arm64: 0.14.54 + esbuild-linux-32: 0.14.54 + esbuild-linux-64: 0.14.54 + esbuild-linux-arm: 0.14.54 + esbuild-linux-arm64: 0.14.54 + esbuild-linux-mips64le: 0.14.54 + esbuild-linux-ppc64le: 0.14.54 + esbuild-linux-riscv64: 0.14.54 + esbuild-linux-s390x: 0.14.54 + esbuild-netbsd-64: 0.14.54 + esbuild-openbsd-64: 0.14.54 + esbuild-sunos-64: 0.14.54 + esbuild-windows-32: 0.14.54 + esbuild-windows-64: 0.14.54 + esbuild-windows-arm64: 0.14.54 + dev: true + /esbuild/0.16.11: resolution: {integrity: sha512-Al0hhRUz/cCDvDp9VZp1L500HZZQ/HLjgTnQTmnW97+PoLmw+PuvB3e19JHYZtWnrxoh3qYrN/0tiRIbrE2oVQ==} engines: {node: '>=12'} @@ -832,11 +1879,33 @@ packages: '@esbuild/win32-x64': 0.16.11 dev: true + /escape-string-regexp/1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + /escape-string-regexp/2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + dev: false + /escape-string-regexp/4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} dev: true + /escodegen/2.0.0: + resolution: {integrity: sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + dev: true + /eslint-config-prettier/8.5.0_eslint@8.30.0: resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==} hasBin: true @@ -953,6 +2022,12 @@ packages: eslint-visitor-keys: 3.3.0 dev: true + /esprima/4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + /esquery/1.4.0: resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} engines: {node: '>=0.10'} @@ -977,11 +2052,83 @@ packages: engines: {node: '>=4.0'} dev: true + /estree-walker/0.6.1: + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + dev: true + + /estree-walker/1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + dev: true + + /estree-walker/2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + /esutils/2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} dev: true + /eventemitter2/6.4.7: + resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} + dev: true + + /execa/4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /executable/4.1.1: + resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} + engines: {node: '>=4'} + dependencies: + pify: 2.3.0 + dev: true + + /expect/29.4.2: + resolution: {integrity: sha512-+JHYg9O3hd3RlICG90OPVjRkPBoiUH7PxvDVMnRiaq1g6JUgZStX514erMl0v2Dc5SkfVbm7ztqbd6qHHPn+mQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.4.2 + jest-get-type: 29.4.2 + jest-matcher-utils: 29.4.2 + jest-message-util: 29.4.2 + jest-util: 29.4.2 + dev: false + + /extend/3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + dev: true + + /extract-zip/2.0.1_supports-color@8.1.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + dependencies: + debug: 4.3.4_supports-color@8.1.1 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.0 + transitivePeerDependencies: + - supports-color + dev: true + + /extsprintf/1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + dev: true + /fast-deep-equal/3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true @@ -1011,6 +2158,19 @@ packages: reusify: 1.0.4 dev: true + /fd-slicer/1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + dependencies: + pend: 1.2.0 + dev: true + + /figures/3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + dependencies: + escape-string-regexp: 1.0.5 + dev: true + /file-entry-cache/6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -1023,7 +2183,6 @@ packages: engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 - dev: true /find-up/5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} @@ -1055,6 +2214,24 @@ packages: optional: true dev: false + /for-each/0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + + /forever-agent/0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + dev: true + + /form-data/2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + /form-data/4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -1062,7 +2239,16 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: false + + /fs-extra/9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.10 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true /fs.realpath/1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1079,13 +2265,38 @@ packages: /function-bind/1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - /get-intrinsic/1.1.3: - resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==} + /functions-have-names/1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + /get-func-name/2.0.0: + resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + dev: true + + /get-intrinsic/1.2.0: + resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} dependencies: function-bind: 1.1.1 has: 1.0.3 has-symbols: 1.0.3 - dev: false + + /get-stream/5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.0 + dev: true + + /getos/3.2.1: + resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==} + dependencies: + async: 3.2.4 + dev: true + + /getpass/0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + dependencies: + assert-plus: 1.0.0 + dev: true /glob-parent/5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -1112,6 +2323,13 @@ packages: path-is-absolute: 1.0.1 dev: true + /global-dirs/3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + dependencies: + ini: 2.0.0 + dev: true + /globals/13.19.0: resolution: {integrity: sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==} engines: {node: '>=8'} @@ -1139,23 +2357,43 @@ packages: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} dev: true + /gopd/1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.0 + /graceful-fs/4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} - dev: true /grapheme-splitter/1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} dev: true + /has-bigints/1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + + /has-flag/3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + /has-flag/4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - dev: true + + /has-property-descriptors/1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.0 /has-symbols/1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} - dev: false + + /has-tostringtag/1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 /has/1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} @@ -1163,6 +2401,63 @@ packages: dependencies: function-bind: 1.1.1 + /htm/3.1.1: + resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==} + dev: false + + /html-encoding-sniffer/3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + dependencies: + whatwg-encoding: 2.0.0 + dev: true + + /http-proxy-agent/5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /http-signature/1.3.6: + resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==} + engines: {node: '>=0.10'} + dependencies: + assert-plus: 1.0.0 + jsprim: 2.0.2 + sshpk: 1.17.0 + dev: true + + /https-proxy-agent/5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /human-signals/1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + dev: true + + /iconv-lite/0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /ieee754/1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + /ignore/5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -1189,6 +2484,10 @@ packages: engines: {node: '>=0.8.19'} dev: true + /indent-string/4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + /inflight/1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: @@ -1200,6 +2499,38 @@ packages: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: true + /ini/2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + dev: true + + /internal-slot/1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.0 + has: 1.0.3 + side-channel: 1.0.4 + + /is-arguments/1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + + /is-array-buffer/3.0.1: + resolution: {integrity: sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + is-typed-array: 1.1.10 + + /is-bigint/1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + /is-binary-path/2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1207,17 +2538,51 @@ packages: binary-extensions: 2.2.0 dev: true + /is-boolean-object/1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + + /is-callable/1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + /is-ci/3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + dependencies: + ci-info: 3.8.0 + dev: true + /is-core-module/2.11.0: resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} dependencies: has: 1.0.3 dev: true + /is-date-object/1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + /is-extglob/2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} dev: true + /is-fullwidth-code-point/3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-fullwidth-code-point/4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + dev: true + /is-glob/4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1225,28 +2590,170 @@ packages: is-extglob: 2.1.1 dev: true + /is-installed-globally/0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + dev: true + + /is-map/2.0.2: + resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + + /is-number-object/1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + /is-number/7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - dev: true /is-path-inside/3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} dev: true + /is-potential-custom-element-name/1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: true + /is-promise/4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} dev: false + /is-regex/1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + + /is-set/2.0.2: + resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} + + /is-shared-array-buffer/1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + + /is-stream/2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: true + + /is-string/1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + + /is-symbol/1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + + /is-typed-array/1.1.10: + resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + + /is-typedarray/1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + dev: true + + /is-unicode-supported/0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: true + + /is-weakmap/2.0.1: + resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} + + /is-weakset/2.0.2: + resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + + /isarray/2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /isstream/0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + dev: true + + /jest-diff/29.4.2: + resolution: {integrity: sha512-EK8DSajVtnjx9sa1BkjZq3mqChm2Cd8rIzdXkQMA8e0wuXq53ypz6s5o5V8HRZkoEt2ywJ3eeNWFKWeYr8HK4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.4.2 + jest-get-type: 29.4.2 + pretty-format: 29.4.2 + dev: false + + /jest-get-type/29.4.2: + resolution: {integrity: sha512-vERN30V5i2N6lqlFu4ljdTqQAgrkTFMC9xaIIfOPYBw04pufjXRty5RuXBiB1d72tGbURa/UgoiHB90ruOSivg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false + + /jest-matcher-utils/29.4.2: + resolution: {integrity: sha512-EZaAQy2je6Uqkrm6frnxBIdaWtSYFoR8SVb2sNLAtldswlR/29JAgx+hy67llT3+hXBaLB0zAm5UfeqerioZyg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.4.2 + jest-get-type: 29.4.2 + pretty-format: 29.4.2 + dev: false + + /jest-message-util/29.4.2: + resolution: {integrity: sha512-SElcuN4s6PNKpOEtTInjOAA8QvItu0iugkXqhYyguRvQoXapg5gN+9RQxLAkakChZA7Y26j6yUCsFWN+hlKD6g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.18.6 + '@jest/types': 29.4.2 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.10 + micromatch: 4.0.5 + pretty-format: 29.4.2 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: false + + /jest-util/29.4.2: + resolution: {integrity: sha512-wKnm6XpJgzMUSRFB7YF48CuwdzuDIHenVuoIb1PLuJ6F+uErZsuDkU+EiExkChf6473XcawBrSfDSnXl+/YG4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.4.2 + '@types/node': 18.11.18 + chalk: 4.1.2 + ci-info: 3.8.0 + graceful-fs: 4.2.10 + picomatch: 2.3.1 + dev: false + /js-sdsl/4.2.0: resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==} dev: true + /js-tokens/4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + /js-yaml/4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1254,14 +2761,89 @@ packages: argparse: 2.0.1 dev: true + /jsbn/0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + dev: true + + /jsdom/21.1.0: + resolution: {integrity: sha512-m0lzlP7qOtthD918nenK3hdItSd2I+V3W9IrBcB36sqDwG+KnUs66IF5GY7laGWUnlM9vTsD0W1QwSEBYWWcJg==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + acorn: 8.8.2 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.4.3 + domexception: 4.0.0 + escodegen: 2.0.0 + form-data: 4.0.0 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.2 + parse5: 7.1.2 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.2 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.12.0 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + /json-schema-traverse/0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true + /json-schema/0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + dev: true + /json-stable-stringify-without-jsonify/1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true + /json-stringify-safe/5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: true + + /jsonc-parser/3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + + /jsonfile/6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.10 + dev: true + + /jsprim/2.0.2: + resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} + engines: {'0': node >=0.6.0} + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + dev: true + /jwt-decode/3.1.2: resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} dev: false @@ -1271,6 +2853,19 @@ packages: engines: {node: '>=6'} dev: true + /lazy-ass/1.6.0: + resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} + engines: {node: '> 0.8'} + dev: true + + /levn/0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + dev: true + /levn/0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1279,6 +2874,31 @@ packages: type-check: 0.4.0 dev: true + /listr2/3.14.0_enquirer@2.3.6: + resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==} + engines: {node: '>=10.0.0'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.19 + enquirer: 2.3.6 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.3.0 + rxjs: 7.8.0 + through: 2.3.8 + wrap-ansi: 7.0.0 + dev: true + + /local-pkg/0.4.3: + resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + engines: {node: '>=14'} + dev: true + /locate-path/6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1290,6 +2910,37 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash.once/4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: true + + /lodash/4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + /log-symbols/4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: true + + /log-update/4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + dev: true + + /loupe/2.3.6: + resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} + dependencies: + get-func-name: 2.0.0 + dev: true + /lower-case/2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: @@ -1303,6 +2954,11 @@ packages: yallist: 4.0.0 dev: true + /lz-string/1.4.4: + resolution: {integrity: sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==} + hasBin: true + dev: true + /magic-string/0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} dependencies: @@ -1316,6 +2972,16 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true + /marked/4.2.12: + resolution: {integrity: sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==} + engines: {node: '>= 12'} + hasBin: true + dev: false + + /merge-stream/2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + /merge2/1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1327,19 +2993,16 @@ packages: dependencies: braces: 3.0.2 picomatch: 2.3.1 - dev: true /mime-db/1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - dev: false /mime-types/2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} dependencies: mime-db: 1.52.0 - dev: false /mime/3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} @@ -1347,10 +3010,14 @@ packages: hasBin: true dev: true + /mimic-fn/2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + /min-indent/1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - dev: true /minimatch/3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1369,6 +3036,26 @@ packages: minimist: 1.2.7 dev: true + /mlly/1.1.0: + resolution: {integrity: sha512-cwzBrBfwGC1gYJyfcy8TcZU1f+dbH/T+TuOhtYP2wLv/Fb51/uV7HJQfBPtEupZ2ORLRU1EKFS/QfS3eo9+kBQ==} + dependencies: + acorn: 8.8.2 + pathe: 1.1.0 + pkg-types: 1.0.1 + ufo: 1.0.1 + dev: true + + /modern-node-polyfills/0.0.9: + resolution: {integrity: sha512-Z7sFaWAHCEAw44Ww1L0JEt4BaQ7/LVTbbqTtm3bNSfdWs0ZW7QwRN7Xy8RjSPOlZ9ZSkoXAa54neuvAC6KGRFg==} + dependencies: + '@jspm/core': 2.0.0-beta.24 + '@rollup/plugin-inject': 4.0.4_rollup@2.79.1 + acorn: 8.8.2 + esbuild: 0.14.54 + local-pkg: 0.4.3 + rollup: 2.79.1 + dev: true + /mri/1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -1409,9 +3096,39 @@ packages: engines: {node: '>=0.10.0'} dev: true + /npm-run-path/4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: true + + /nwsapi/2.2.2: + resolution: {integrity: sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==} + dev: true + /object-inspect/1.12.2: resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} - dev: false + + /object-is/1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + + /object-keys/1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + /object.assign/4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + has-symbols: 1.0.3 + object-keys: 1.1.1 /once/1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1419,6 +3136,25 @@ packages: wrappy: 1.0.2 dev: true + /onetime/5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + + /optionator/0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.3 + dev: true + /optionator/0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} engines: {node: '>= 0.8.0'} @@ -1431,6 +3167,10 @@ packages: word-wrap: 1.2.3 dev: true + /ospath/1.2.2: + resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} + dev: true + /p-limit/3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1438,6 +3178,13 @@ packages: yocto-queue: 0.1.0 dev: true + /p-limit/4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.0.0 + dev: true + /p-locate/5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -1445,6 +3192,13 @@ packages: p-limit: 3.1.0 dev: true + /p-map/4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + dependencies: + aggregate-error: 3.1.0 + dev: true + /parent-module/1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1452,6 +3206,12 @@ packages: callsites: 3.1.0 dev: true + /parse5/7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.4.0 + dev: true + /pascal-case/3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} dependencies: @@ -1483,6 +3243,22 @@ packages: engines: {node: '>=8'} dev: true + /pathe/1.1.0: + resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==} + dev: true + + /pathval/1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + + /pend/1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + dev: true + + /performance-now/2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + dev: true + /picocolors/1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: true @@ -1490,6 +3266,18 @@ packages: /picomatch/2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + + /pify/2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pkg-types/1.0.1: + resolution: {integrity: sha512-jHv9HB+Ho7dj6ItwppRDDl0iZRYBD0jsakHXtFgoLr+cHSF6xC+QL54sJmWxyGxOLYSHm0afhXhXcQDQqH9z8g==} + dependencies: + jsonc-parser: 3.2.0 + mlly: 1.1.0 + pathe: 1.1.0 dev: true /postcss/8.4.20: @@ -1501,6 +3289,11 @@ packages: source-map-js: 1.0.2 dev: true + /prelude-ls/1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + dev: true + /prelude-ls/1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -1522,15 +3315,60 @@ packages: hasBin: true dev: true + /pretty-bytes/5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + dev: true + + /pretty-format/27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: true + + /pretty-format/29.4.2: + resolution: {integrity: sha512-qKlHR8yFVCbcEWba0H0TOC8dnLlO4vPlyEjRPw31FZ2Rupy9nLa8ZLbYny8gWEl8CkEhJqAE6IzdNELTBVcBEg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.4.2 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: false + + /proxy-from-env/1.0.0: + resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} + dev: true + /proxy-from-env/1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false + /psl/1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: true + + /pump/3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + /punycode/2.1.1: resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} engines: {node: '>=6'} dev: true + /qs/6.10.4: + resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: true + /qs/6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} @@ -1538,10 +3376,22 @@ packages: side-channel: 1.0.4 dev: false + /querystringify/2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: true + /queue-microtask/1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /react-is/17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: true + + /react-is/18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: false + /readdirp/3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -1549,11 +3399,44 @@ packages: picomatch: 2.3.1 dev: true + /reconnecting-websocket/4.4.0: + resolution: {integrity: sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==} + dev: false + + /redent/3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: false + + /regenerator-runtime/0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + /regexp.prototype.flags/1.4.3: + resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + functions-have-names: 1.2.3 + /regexpp/3.2.0: resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} engines: {node: '>=8'} dev: true + /request-progress/3.0.0: + resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} + dependencies: + throttleit: 1.0.0 + dev: true + + /requires-port/1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: true + /resolve-from/4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1568,11 +3451,23 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /restore-cursor/3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: true + /reusify/1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} dev: true + /rfdc/1.3.0: + resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} + dev: true + /rimraf/2.7.1: resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} hasBin: true @@ -1587,6 +3482,35 @@ packages: glob: 7.2.3 dev: true + /rollup-plugin-inject/3.0.2: + resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. + dependencies: + estree-walker: 0.6.1 + magic-string: 0.25.9 + rollup-pluginutils: 2.8.2 + dev: true + + /rollup-plugin-node-polyfills/0.2.1: + resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} + dependencies: + rollup-plugin-inject: 3.0.2 + dev: true + + /rollup-pluginutils/2.8.2: + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} + dependencies: + estree-walker: 0.6.1 + dev: true + + /rollup/2.79.1: + resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + /rollup/3.8.1: resolution: {integrity: sha512-4yh9eMW7byOroYcN8DlF9P/2jCpu6txVIHjEqquQVSx7DI0RgyCCN3tjrcy4ra6yVtV336aLBB3v2AarYAxePQ==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -1601,6 +3525,12 @@ packages: queue-microtask: 1.2.3 dev: true + /rxjs/7.8.0: + resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} + dependencies: + tslib: 2.4.1 + dev: true + /sade/1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -1608,6 +3538,14 @@ packages: mri: 1.2.0 dev: true + /safe-buffer/5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /safer-buffer/2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + /sander/0.5.1: resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} dependencies: @@ -1627,6 +3565,13 @@ packages: source-map-js: 1.0.2 dev: true + /saxes/6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: true + /semver/7.3.8: resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} engines: {node: '>=10'} @@ -1655,9 +3600,16 @@ packages: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: call-bind: 1.0.2 - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.0 object-inspect: 1.12.2 - dev: false + + /siginfo/2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + + /signal-exit/3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true /sirv/2.0.2: resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==} @@ -1671,6 +3623,31 @@ packages: /slash/3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + + /slice-ansi/3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: true + + /slice-ansi/4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: true + + /slice-ansi/5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 dev: true /sorcery/0.10.0: @@ -1688,16 +3665,83 @@ packages: engines: {node: '>=0.10.0'} dev: true + /source-map-support/0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map/0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + /sourcemap-codec/1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead dev: true + /sshpk/1.17.0: + resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + dev: true + + /stack-utils/2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + dev: false + + /stackback/0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + + /std-env/3.3.2: + resolution: {integrity: sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==} + dev: true + + /stop-iteration-iterator/1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + dependencies: + internal-slot: 1.0.5 + /streamsearch/1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} dev: true + /string-width/4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string-width/5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.0.1 + dev: true + /strip-ansi/6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1705,23 +3749,52 @@ packages: ansi-regex: 5.0.1 dev: true + /strip-ansi/7.0.1: + resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + + /strip-final-newline/2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: true + /strip-indent/3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} dependencies: min-indent: 1.0.1 - dev: true /strip-json-comments/3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} dev: true + /strip-literal/1.0.1: + resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==} + dependencies: + acorn: 8.8.2 + dev: true + + /supports-color/5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + /supports-color/7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} dependencies: has-flag: 4.0.0 + + /supports-color/8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 dev: true /supports-preserve-symlinks-flag/1.0.0: @@ -1769,6 +3842,15 @@ packages: is-promise: 4.0.0 dev: false + /svelte-fragment-component/1.2.0_svelte@3.55.0: + resolution: {integrity: sha512-rRstmz2oAy2Y/7X57tRaIAJdMYsa2K/MOx/YJN/ETb7Bj9U3vjgioz27dMG1hl2vAKFTtQpxDhC31ur7ECwpog==} + engines: {node: '>=10'} + peerDependencies: + svelte: 3.x + dependencies: + svelte: 3.55.0 + dev: false + /svelte-hmr/0.15.1_svelte@3.55.0: resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==} engines: {node: ^12.20 || ^14.13.1 || >= 16} @@ -1778,10 +3860,45 @@ packages: svelte: 3.55.0 dev: true + /svelte-htm/1.2.0_svelte@3.55.0: + resolution: {integrity: sha512-6YFNncbyXbCa3PSoMQc/JR6O/Yg1OgNioH5rMgE88RVPzU8714y2Urfenlqs+ryRS+Inv+m6TJ9jH3W7wDCS1A==} + engines: {node: '>=10'} + peerDependencies: + svelte: 3.x + dependencies: + '@babel/runtime-corejs3': 7.20.13 + core-js: 3.28.0 + htm: 3.1.1 + svelte: 3.55.0 + svelte-fragment-component: 1.2.0_svelte@3.55.0 + svelte-hyperscript: 1.2.1_svelte@3.55.0 + dev: false + + /svelte-hyperscript/1.2.1_svelte@3.55.0: + resolution: {integrity: sha512-IVk91VDSOrhOUe0KI+Jfu7yCruUTOXgr4WysrK7hBNbFMDjeBOcPhk+LMYjUjdWxJx3+QSt3bUXj09YrUfRkjg==} + engines: {node: '>=10'} + peerDependencies: + svelte: 3.x + dependencies: + '@babel/runtime-corejs3': 7.20.13 + core-js: 3.28.0 + svelte: 3.55.0 + dev: false + /svelte-icons/2.1.0: resolution: {integrity: sha512-rHPQjweEc9fGSnvM0/4gA3pDHwyZyYsC5KhttCZRhSMJfLttJST5Uq0B16Czhw+HQ+HbSOk8kLigMlPs7gZtfg==} dev: false + /svelte-markdown/0.2.3_svelte@3.55.0: + resolution: {integrity: sha512-2h680NzTXnAD0CXhxe3GeHl6W+ayG4iKQRl+BIDRw+R0mUE0OiNxP1Vt8Rn+aWevB/LBiBIPCAwvL+0BkG057A==} + peerDependencies: + svelte: ^3.0.0 + dependencies: + '@types/marked': 4.0.8 + marked: 4.2.12 + svelte: 3.55.0 + dev: false + /svelte-multiselect/8.2.3: resolution: {integrity: sha512-cCnPFkG+0i2eBDaYUOgmQVa2TaJ6Xdjly0/tpch0XCfu4Rs0whbnEXP4QfKVloaAxEDUXwiIq/FHEYZ61xAklg==} dev: false @@ -1864,6 +3981,10 @@ packages: resolution: {integrity: sha512-xg9ckb8UeeIme4/5qlwCrl2QNmUZ8SCQYZn3Ji83cUsoASqRNy3KWjpmNmzYvPDqCHSZjruBBsoB7t5hwuzw5g==} dev: false + /svelte-websocket-store/1.1.33: + resolution: {integrity: sha512-E11veDt/iWHRmXAT+cKJdIPt5urM2c19HQpMQtzlNNpEP4U/OzspS+7tquKyxA9DZNtHL3d27tscQ5IDf5vAAg==} + dev: false + /svelte/3.55.0: resolution: {integrity: sha512-uGu2FVMlOuey4JoKHKrpZFkoYyj0VLjJdz47zX5+gVK5odxHM40RVhar9/iK2YFRVxvfg9FkhfVlR0sjeIrOiA==} engines: {node: '>= 8'} @@ -1880,10 +4001,22 @@ packages: typescript: 4.9.4 dev: false + /symbol-tree/3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: true + /text-table/0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /throttleit/1.0.0: + resolution: {integrity: sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==} + dev: true + + /through/2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + dev: true + /tiny-glob/0.2.9: resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} dependencies: @@ -1891,18 +4024,63 @@ packages: globrex: 0.1.2 dev: true + /tinybench/2.3.1: + resolution: {integrity: sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==} + dev: true + + /tinypool/0.3.1: + resolution: {integrity: sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy/1.1.1: + resolution: {integrity: sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g==} + engines: {node: '>=14.0.0'} + dev: true + + /tmp/0.2.1: + resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} + engines: {node: '>=8.17.0'} + dependencies: + rimraf: 3.0.2 + dev: true + /to-regex-range/5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 - dev: true /totalist/3.0.0: resolution: {integrity: sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==} engines: {node: '>=6'} dev: true + /tough-cookie/2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + dependencies: + psl: 1.9.0 + punycode: 2.1.1 + dev: true + + /tough-cookie/4.1.2: + resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.1.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: true + + /tr46/3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + dependencies: + punycode: 2.1.1 + dev: true + /tslib/1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true @@ -1920,6 +4098,23 @@ packages: typescript: 4.9.4 dev: true + /tunnel-agent/0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /tweetnacl/0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + dev: true + + /type-check/0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + dev: true + /type-check/0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1927,16 +4122,30 @@ packages: prelude-ls: 1.2.1 dev: true + /type-detect/4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + /type-fest/0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} dev: true + /type-fest/0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + /typescript/4.9.4: resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==} engines: {node: '>=4.2.0'} hasBin: true + /ufo/1.0.1: + resolution: {integrity: sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==} + dev: true + /undici/5.14.0: resolution: {integrity: sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==} engines: {node: '>=12.18'} @@ -1944,12 +4153,106 @@ packages: busboy: 1.6.0 dev: true + /universalify/0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: true + + /universalify/2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: true + + /untildify/4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + dev: true + /uri-js/4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: punycode: 2.1.1 dev: true + /url-parse/1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: true + + /uuid/8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: true + + /verror/1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + dev: true + + /vite-node/0.28.4_ovmyjmuuyckt3r3gpaexj2onji: + resolution: {integrity: sha512-KM0Q0uSG/xHHKOJvVHc5xDBabgt0l70y7/lWTR7Q0pR5/MrYxadT+y32cJOE65FfjGmJgxpVEEY+69btJgcXOQ==} + engines: {node: '>=v14.16.0'} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + mlly: 1.1.0 + pathe: 1.1.0 + picocolors: 1.0.0 + source-map: 0.6.1 + source-map-support: 0.5.21 + vite: 4.0.3_ovmyjmuuyckt3r3gpaexj2onji + transitivePeerDependencies: + - '@types/node' + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vite/4.0.3_ovmyjmuuyckt3r3gpaexj2onji: + resolution: {integrity: sha512-HvuNv1RdE7deIfQb8mPk51UKjqptO/4RXZ5yXSAvurd5xOckwS/gg8h9Tky3uSbnjYTgUm0hVCet1cyhKd73ZA==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 18.11.18 + esbuild: 0.16.11 + postcss: 8.4.20 + resolve: 1.22.1 + rollup: 3.8.1 + sass: 1.57.1 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /vite/4.0.3_sass@1.57.1: resolution: {integrity: sha512-HvuNv1RdE7deIfQb8mPk51UKjqptO/4RXZ5yXSAvurd5xOckwS/gg8h9Tky3uSbnjYTgUm0hVCet1cyhKd73ZA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1995,6 +4298,124 @@ packages: vite: 4.0.3_sass@1.57.1 dev: true + /vitest/0.28.4_x3jjsgulbh7575qaqkjiilw4p4: + resolution: {integrity: sha512-sfWIy0AdlbyGRhunm+TLQEJrFH9XuRPdApfubsyLcDbCRrUX717BRQKInTgzEfyl2Ipi1HWoHB84Nqtcwxogcg==} + engines: {node: '>=v14.16.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/chai': 4.3.4 + '@types/chai-subset': 1.3.3 + '@types/node': 18.11.18 + '@vitest/browser': 0.28.4 + '@vitest/expect': 0.28.4 + '@vitest/runner': 0.28.4 + '@vitest/spy': 0.28.4 + '@vitest/ui': 0.28.4 + '@vitest/utils': 0.28.4 + acorn: 8.8.1 + acorn-walk: 8.2.0 + cac: 6.7.14 + chai: 4.3.7 + debug: 4.3.4 + jsdom: 21.1.0 + local-pkg: 0.4.3 + pathe: 1.1.0 + picocolors: 1.0.0 + source-map: 0.6.1 + std-env: 3.3.2 + strip-literal: 1.0.1 + tinybench: 2.3.1 + tinypool: 0.3.1 + tinyspy: 1.1.1 + vite: 4.0.3_ovmyjmuuyckt3r3gpaexj2onji + vite-node: 0.28.4_ovmyjmuuyckt3r3gpaexj2onji + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /w3c-xmlserializer/4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + dependencies: + xml-name-validator: 4.0.0 + dev: true + + /webidl-conversions/7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: true + + /whatwg-encoding/2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: true + + /whatwg-mimetype/3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: true + + /whatwg-url/11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + dev: true + + /which-boxed-primitive/1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + + /which-collection/1.0.1: + resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} + dependencies: + is-map: 2.0.2 + is-set: 2.0.2 + is-weakmap: 2.0.1 + is-weakset: 2.0.2 + + /which-typed-array/1.1.9: + resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + is-typed-array: 1.1.10 + /which/2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2003,20 +4424,81 @@ packages: isexe: 2.0.0 dev: true + /why-is-node-running/2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + /word-wrap/1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} dev: true + /wrap-ansi/6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi/7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + /wrappy/1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true + /ws/8.12.0: + resolution: {integrity: sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xml-name-validator/4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: true + + /xmlchars/2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: true + /yallist/4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true + /yauzl/2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + dev: true + /yocto-queue/0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + /yocto-queue/1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true diff --git a/frontend/src/apis/auth.api.ts b/frontend/src/apis/auth.api.ts new file mode 100644 index 0000000..790f70f --- /dev/null +++ b/frontend/src/apis/auth.api.ts @@ -0,0 +1,15 @@ +import axios from 'axios'; +import { autoRefresh } from '../utils/utils'; +export const authInstance = axios.create({ + baseURL: `http://127.0.0.1:8002`, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'Access-Control-Allow-Origin': '*', + //'X-CSRFToken': csrftoken != undefined ? csrftoken : '', + } +}); + +authInstance.interceptors.request.use(autoRefresh, (error) => { + Promise.reject(error); +}); diff --git a/frontend/src/apis/exo.api.ts b/frontend/src/apis/exo.api.ts index e13b439..cb51362 100644 --- a/frontend/src/apis/exo.api.ts +++ b/frontend/src/apis/exo.api.ts @@ -1,6 +1,7 @@ -import { browser } from '$app/environment'; import axios from 'axios'; import { parse, stringify } from 'qs' +import { autoRefresh } from '../utils/utils'; + export const exoInstance = axios.create({ paramsSerializer:{encode:(params)=> {return parse(params, {arrayFormat:"brackets"})}, serialize: (p)=>{return stringify(p, {arrayFormat: "repeat"})}}, baseURL: `http://127.0.0.1:8002`, @@ -9,7 +10,9 @@ export const exoInstance = axios.create({ Accept: 'application/json', 'Access-Control-Allow-Origin': '*', //'X-CSRFToken': csrftoken != undefined ? csrftoken : '', - ...(browser && - localStorage.getItem('token') && { Authorization: `Bearer ${localStorage.getItem('token')}` }) } }); + +exoInstance.interceptors.request.use(autoRefresh, (error) => { + Promise.reject(error); +}); diff --git a/frontend/src/apis/room.api.ts b/frontend/src/apis/room.api.ts new file mode 100644 index 0000000..0bced10 --- /dev/null +++ b/frontend/src/apis/room.api.ts @@ -0,0 +1,19 @@ +import axios from 'axios'; +import { autoRefresh } from '../utils/utils'; + +export const roomInstance = axios.create({ + baseURL: `http://127.0.0.1:8002/room`, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'Access-Control-Allow-Origin': '*', + //'X-CSRFToken': csrftoken != undefined ? csrftoken : '', + } +}); + +roomInstance.interceptors.request.use( + autoRefresh, + (error) => { + Promise.reject(error); + } +); diff --git a/frontend/src/app.scss b/frontend/src/app.scss index 6b2d9d1..2ee2503 100644 --- a/frontend/src/app.scss +++ b/frontend/src/app.scss @@ -4,6 +4,44 @@ box-sizing: border-box; margin: 0; } +.spinner { + width: 30px; + height:30px; + border: 3px solid $contrast; + border-bottom-color: transparent; + border-radius: 50%; + animation: rotation 1s infinite linear; + + display: inline-block; + box-sizing: border-box; + +} + +.italic { + font-style: italic; +} +.underline { + text-decoration: underline; +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.container { + height: calc(100vh - 100px); // 100% - nav +} + + +*{ + scrollbar-width: auto!important; + scrollbar-color: $contrast transparent; +} .btn { border: none; @@ -13,9 +51,12 @@ transition: 0.3s; margin-bottom: 10px; margin-right: 7px; - padding: 0 10%; + padding: 0 50px; width: max-content; cursor: pointer; + &:disabled{ + cursor: not-allowed + } } .primary-btn { @@ -61,7 +102,7 @@ margin: 0; &:focus { outline: none; - border-bottom-color: red; + border-bottom-color: $contrast; } } @@ -79,4 +120,9 @@ .sv-dropdown{ z-index: 10!important; -} \ No newline at end of file +} + +.strong{ + font-weight: 900; +} + diff --git a/frontend/src/components/NavBar.svelte b/frontend/src/components/NavBar.svelte new file mode 100644 index 0000000..af1aac1 --- /dev/null +++ b/frontend/src/components/NavBar.svelte @@ -0,0 +1,207 @@ + + + + \ No newline at end of file diff --git a/frontend/src/components/NavLink.svelte b/frontend/src/components/NavLink.svelte index 1c92d6a..2d9c0ad 100644 --- a/frontend/src/components/NavLink.svelte +++ b/frontend/src/components/NavLink.svelte @@ -2,15 +2,16 @@ import { page } from '$app/stores'; export let href = "/"; export let exact = false; + export let no_hover = false; - + diff --git a/frontend/src/components/auth/PasswordForm.svelte b/frontend/src/components/auth/PasswordForm.svelte new file mode 100644 index 0000000..64f7cc8 --- /dev/null +++ b/frontend/src/components/auth/PasswordForm.svelte @@ -0,0 +1,31 @@ + + +{#if !!$myForm} +
+ + +
+{/if} + \ No newline at end of file diff --git a/frontend/src/components/auth/RoomList.svelte b/frontend/src/components/auth/RoomList.svelte new file mode 100644 index 0000000..6372d5a --- /dev/null +++ b/frontend/src/components/auth/RoomList.svelte @@ -0,0 +1,40 @@ + + +
+ +
+ + \ No newline at end of file diff --git a/frontend/src/components/auth/Section.svelte b/frontend/src/components/auth/Section.svelte new file mode 100644 index 0000000..cd4ea28 --- /dev/null +++ b/frontend/src/components/auth/Section.svelte @@ -0,0 +1,53 @@ + + +
+

+
+
+ {title}

+
+ +
+ {#if !!onValidate} +
+ +
+ + {/if} +
+ + \ No newline at end of file diff --git a/frontend/src/components/auth/UserConfirm.svelte b/frontend/src/components/auth/UserConfirm.svelte new file mode 100644 index 0000000..224673d --- /dev/null +++ b/frontend/src/components/auth/UserConfirm.svelte @@ -0,0 +1,35 @@ + + +
+

Veuillez confirmer votre identité

+ +
+ + +
+
+ \ No newline at end of file diff --git a/frontend/src/components/exos/Card.svelte b/frontend/src/components/exos/Card.svelte index c2b3281..2c507eb 100644 --- a/frontend/src/components/exos/Card.svelte +++ b/frontend/src/components/exos/Card.svelte @@ -7,11 +7,13 @@ import TagContainer from './TagContainer.svelte'; import PrivacyIndicator from './PrivacyIndicator.svelte'; import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte'; + import type { Writable } from 'svelte/store'; + export let exo: Exercice; const { show } = getContext<{ show: Function }>('modal'); const { navigate } = getContext<{ navigate: Function }>('navigation'); - const { isAuth } = getContext<{ isAuth: boolean }>('auth'); + const { isAuth } = getContext<{ isAuth: Writable }>('auth'); const exerciceStore = getContext('exos'); const tagsStore = getContext('tags'); @@ -19,10 +21,6 @@ const handleClick = () => { opened = true; navigate(`/exercices/${exo.id_code}`); - }; - - let tg = false; - $: !!opened && show( ModalCard, { @@ -35,36 +33,36 @@ opened = false; } ); + }; -
{}} - on:keypress={() => {}} -> +
{}} on:keypress={() => {}}>

{exo.name}

-

Exemples

- {#if !!exo.consigne}

{exo.consigne}

{/if} - {#each exo.examples.data.slice(0, 3) as ex} -

{ex.calcul}

- {/each} + {#if exo.examples != null} +

Exemples

+ {#if !!exo.consigne}

{exo.consigne}

{/if} + {#each exo.examples.data.slice(0, 3) as ex} +

{ex.calcul}

+ {/each} + {:else} +

Aucun exemple disponible

+ {/if}
- {#if !!isAuth} - {#if exo.is_author && exo.original == null} -
- - {exo.private == true ? 'Privé' : 'Public'} -
- {:else if !exo.is_author} -
- - Par {exo.author.username} - + {#if !!$isAuth && exo.is_author && exo.original == null } +
+ + {exo.private == true ? 'Privé' : 'Public'} +
+ {:else if !exo.is_author} +
+ + Par {exo.author.username} + + {#if !!$isAuth}
{}} on:click|stopPropagation={() => { @@ -78,15 +76,15 @@ >
-
- {:else if exo.is_author && exo.original != null} -
- Par {exo.original?.author} -
- {/if}{/if} + {/if} +
+ {:else if exo.is_author && exo.original != null} +
+ Par {exo.original?.author} +
+ {/if}
- {#if !!isAuth} + {#if !!$isAuth} {/if} @@ -188,7 +186,7 @@ background-color: $background; min-height: 250px; max-height: 300px; - &:not(.tagMode):hover { + &:hover { transform: translateX(10px) translateY(-10px); } } diff --git a/frontend/src/components/exos/CreateCard.svelte b/frontend/src/components/exos/CreateCard.svelte index 057e7e1..3ae680f 100644 --- a/frontend/src/components/exos/CreateCard.svelte +++ b/frontend/src/components/exos/CreateCard.svelte @@ -17,9 +17,10 @@
- diff --git a/frontend/src/components/exos/ExoList.svelte b/frontend/src/components/exos/ExoList.svelte new file mode 100644 index 0000000..f64a820 --- /dev/null +++ b/frontend/src/components/exos/ExoList.svelte @@ -0,0 +1,165 @@ + + +
{ + if ( + more != undefined && + $exos.hasMore && + more.offsetHeight + more.scrollTop >= more.scrollHeight + ) { + fetchNextPage(); + } + }} +> +
+ { + if (updateText != null) { + clearTimeout(updateText); + } + + updateText = window.setTimeout(() => { + search = e.currentTarget.value; + }, 500); + }} + /> + {#if !!$isAuth} +
+ + +
{/if} +
+
+ {#each $exos.data as e (e.id_code)} +
{ + if ($selectedExos.map((s) => s.exercice_id).includes(e.id_code)) { + selectedExos.update((n) => { + return [...n.filter((s) => s.exercice_id != e.id_code)]; + }); + } else { + selectedExos.update((n) => { + return [...n, { quantity: 10, exercice_id: e.id_code, name: e.name }]; + }); + } + }} + class:selected={$selectedExos.map((s) => s.exercice_id).includes(e.id_code)} + on:keydown={() => {}} + > +

{e.name}

+ +
+ {/each} + {#if $exos.hasMore} +

+ {/if} +
+
+ + diff --git a/frontend/src/components/exos/Feed.svelte b/frontend/src/components/exos/Feed.svelte index 77064fa..c4ef081 100644 --- a/frontend/src/components/exos/Feed.svelte +++ b/frontend/src/components/exos/Feed.svelte @@ -3,96 +3,140 @@ import Card from './Card.svelte'; import Head from './Head.svelte'; import ModalCard from './ModalCard.svelte'; - import { Query, useQueryClient, type QueryOptions } from '@sveltestack/svelte-query'; import { getExo, getExos, getTags } from '../../requests/exo.request'; import { page } from '$app/stores'; import { onMount } from 'svelte'; import Pagination from './Pagination.svelte'; import { goto } from '$app/navigation'; - import { writable } from 'svelte/store'; + import {type Writable, writable} from 'svelte/store'; import { setContext } from 'svelte'; import type { Page, Tag } from '../../types/exo.type'; import type { Store } from '../../types/api.type'; + import { page as p } from '$app/stores'; const { show } = getContext<{ show: Function }>('modal'); - const { navigate } = getContext<{ navigate: Function }>('navigation'); + const { navigate, insertUrl } = getContext<{ navigate: Function; insertUrl: Function }>( + 'navigation' + ); + const { isAuth } = getContext<{ isAuth: Writable }>('auth'); + let filter = $isAuth ? 'user' : 'public'; - let filter = 'user'; - - const exerciceStore = writable>({ + const exerciceStore = writable>({ isLoading: false, isFetching: false, isSuccess: false, data: undefined }); - const tagStore = writable>({ + const tagStore = writable>({ isLoading: false, isFetching: false, isSuccess: false, - data: undefined + data: [] }); setContext('exos', exerciceStore); setContext('tags', tagStore); + onMount(() => { + let page = $page.url.searchParams.get('page') + if(page == null){ + page = '1' + } if ($page.params.slug != undefined && !['user', 'public'].includes($page.params.slug)) { getExo($page.params.slug).then((r) => { - show(ModalCard, { exo: r, exos: exerciceStore, tags: tagStore }, () => navigate('/exercices/' + filter)); + insertUrl('exercices/' + filter); + show(ModalCard, { exo: r, exos: exerciceStore, tags: tagStore }, () => navigate(-1)); }); - } else if ($page.params.slug == undefined) { - goto('/exercices/public'); + } else if ($page.params.slug == undefined || $page.params.slug == "user") { + filter = $isAuth ? 'user' : 'public'; + goto(`/exercices/${filter}?${new URLSearchParams({page}).toString()}`) + } else if($page.params.slug == "public"){ + filter = 'public'; + goto(`/exercices/${filter}?${new URLSearchParams({page}).toString()}`) } }); $: filter = ['user', 'public'].includes($page.params.slug) ? $page.params.slug : filter; + /*$: if(['user', 'public'].includes($page.params.slug) && !$isAuth){ + filter = "public" + //goto('/exercices/' + filter) + }*/ + const size = 15; + $: activePage = parseInt($page.url.searchParams.get('page')!) || 1; + let search = ''; + let selected: Tag[] = []; + $: { + if(!$isAuth){ + filter = 'public' + } exerciceStore.update((s) => { return { ...s, isFetching: true }; }); getExos(filter as 'public' | 'user', { - page: activePage, + page: activePage == 0 ? 1: activePage, search, + size, tags: [...selected.map((t) => t.id_code)] - }).then((r) => { - exerciceStore.update((e) => { - return { ...e, isSuccess: true, isFetching: false, data: r }; - }); - }); + }) + .then((r) => { + console.log('R', r); + if (activePage > r.totalPage && r.total != 0 && r.totalPage != 0) { + activePage = r.totalPage; + //$p.url.searchParams.set('page', String(activePage)); + goto(`?${new URLSearchParams({page: activePage}).toString()}`); + return; + } + exerciceStore.update((e) => { + return { ...e, isSuccess: true, isFetching: false, data: r }; + }); + }) + .catch(console.log); } $: { - tagStore.update((s)=>{return {...s, isFetching: true}}); - getTags().then(r=>{ - tagStore.update((e) => { - return { ...e, isSuccess: true, isFetching: false, data: r }; + if($isAuth) { + tagStore.update((s) => { + return {...s, isFetching: true}; }); - }) + getTags().then((r) => { + tagStore.update((e) => { + return {...e, isSuccess: true, isFetching: false, data: r}; + }); + }); + } } - let activePage = parseInt($page.url.searchParams.get('page')!) || 1; - let search = ''; - let selected: Tag[] = []; + + + - -{#if $tagStore.isSuccess == true && $tagStore.data != undefined} +{#if $tagStore.data != undefined} {/if} {#if $tagStore.isFetching == true} Fetching -{/if} +{/if}
-

- Tous les exercices -

-

- Vous retrouverez ici tous les exercices que vous avez créé ou copié depuis les exercices - publics -

+ {#if filter == 'user'} +

+ Vos exercices +

+

+ Vous retrouverez ici tous les exercices que vous avez créé ou copié depuis les exercices + publics +

+ {:else} +

+ Tous les exercices +

+

Vous retrouverez ici tous les exercices créés par les autres utilisateurs

+ {/if}
{#if $exerciceStore.data != undefined} {#each $exerciceStore.data.items.filter((e) => e != null && selected.every((t) => e.tags @@ -100,11 +144,63 @@ .includes(t.id_code))) as e} {/each} - + {:else} + {#each Array(10) as i} +
+ {/each} {/if}
+{#if $exerciceStore.data != undefined} + +{/if} diff --git a/frontend/src/components/exos/Head.svelte b/frontend/src/components/exos/Head.svelte index 0f3fe91..fd5a545 100644 --- a/frontend/src/components/exos/Head.svelte +++ b/frontend/src/components/exos/Head.svelte @@ -7,20 +7,31 @@ import type { Writable } from 'svelte/store'; import type { Store } from '../../types/api.type'; - const { navigate } = getContext<{navigate: Function}>('navigation'); - const { show, close } = getContext<{show: Function, close: Function}>('modal'); + const { navigate } = getContext<{ navigate: Function }>('navigation'); + const { show, close } = getContext<{ show: Function; close: Function }>('modal'); + export let location = 'public'; export let search = ''; export let selected: Tag[] = []; - const { isAuth } = getContext<{ isAuth: boolean }>('auth'); + const { isAuth } = getContext<{ isAuth: Writable }>('auth'); - const tags: Writable> = getContext('tags') + const tags: Writable> = getContext('tags'); const exerciceStore: Writable> = getContext('exos'); + let updateText = null; + let tmp ="" + $: { + if(updateText != null){ + clearTimeout(updateText); + } + updateText = window.setTimeout(() => { + search = tmp; + }, 500); + }
- {#if !!isAuth} + {#if !!$isAuth}