first commit
This commit is contained in:
commit
55e1a1baef
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
frontend/node_modules
|
||||
frontend/dist
|
||||
**/__pycache__/**
|
||||
backend/env
|
||||
backend/api/uploads
|
||||
backend/api/database.db
|
20
backend/api/config.py
Normal file
20
backend/api/config.py
Normal file
@ -0,0 +1,20 @@
|
||||
from datetime import timedelta
|
||||
from redis import Redis
|
||||
from pydantic import BaseModel
|
||||
SECRET_KEY = "6323081020d8939e6385dd688a26cbca0bb34ed91997959167637319ba4f6f3e"
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||
|
||||
|
||||
redis_conn = Redis(host='localhost', port=6379, db=0, decode_responses=True)
|
||||
|
||||
|
||||
class Settings(BaseModel):
|
||||
authjwt_secret_key: str = SECRET_KEY
|
||||
authjwt_denylist_enabled: bool = False
|
||||
authjwt_denylist_token_checks: set = {"access", "refresh"}
|
||||
access_expires: int = timedelta(minutes=15)
|
||||
refresh_expires: int = timedelta(days=30)
|
||||
|
||||
|
||||
settings = Settings()
|
43
backend/api/conftest.py
Normal file
43
backend/api/conftest.py
Normal file
@ -0,0 +1,43 @@
|
||||
import os
|
||||
import shutil
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
from sqlmodel.pool import StaticPool
|
||||
from database.exercices.models import Exercice
|
||||
from main import app
|
||||
|
||||
from database.db import get_session
|
||||
|
||||
|
||||
@pytest.fixture(name="session")
|
||||
def session_fixture():
|
||||
engine = create_engine(
|
||||
"sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool, echo=False
|
||||
)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
#cleanup uploads
|
||||
exos = session.exec(select(Exercice)).all()
|
||||
for e in exos:
|
||||
try:
|
||||
shutil.rmtree(os.path.join('uploads', e.id_code))
|
||||
except:
|
||||
pass
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def client_fixture(session: Session):
|
||||
|
||||
def get_session_override():
|
||||
return session
|
||||
|
||||
app.dependency_overrides[get_session] = get_session_override
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
yield client
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
74
backend/api/database/auth/crud.py
Normal file
74
backend/api/database/auth/crud.py
Normal file
@ -0,0 +1,74 @@
|
||||
import uuid
|
||||
from services.password import get_password_hash
|
||||
from database.auth.models import User, UserEdit
|
||||
from sqlmodel import Session, select
|
||||
|
||||
def create_user_db(username:str , password: str, db: Session):
|
||||
user = User(username=username, hashed_password=password, clientId=uuid.uuid4())
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
def get_user_from_username_db(username, db: Session):
|
||||
user = db.exec(select(User).where(User.username == username)).first()
|
||||
return user
|
||||
|
||||
def get_user_from_clientId_db(clientId: str , db: Session):
|
||||
user = db.exec(select(User).where(User.clientId == clientId)).first()
|
||||
return user
|
||||
|
||||
def update_user_db(clientId: str, user: UserEdit, db: Session):
|
||||
db_user = get_user_from_clientId_db(clientId, db)
|
||||
|
||||
if not db_user:
|
||||
return None
|
||||
|
||||
user_data = user.dict(exclude_unset=True)
|
||||
|
||||
for key, value in user_data.items():
|
||||
setattr(db_user, key, value)
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
def update_password_db(id: int, password: str, db: Session):
|
||||
user = db.get(User, id)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
user.hashed_password = get_password_hash(password)
|
||||
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
return user
|
||||
|
||||
def delete_user_db(id: int, db: Session):
|
||||
user = db.get(User, id)
|
||||
if not user:
|
||||
return False
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def check_unique_username(username: str, db: Session):
|
||||
user = db.exec(select(User).where(User.username == username)).first()
|
||||
if not user:
|
||||
return username
|
||||
return None
|
||||
|
||||
def change_user_uuid(id: int, db: Session):
|
||||
user = db.get(User, id)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
user.clientId = uuid.uuid4()
|
||||
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user.clientId
|
||||
|
76
backend/api/database/auth/models.py
Normal file
76
backend/api/database/auth/models.py
Normal file
@ -0,0 +1,76 @@
|
||||
from typing import List, Optional
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from uuid import UUID
|
||||
import uuid
|
||||
from sqlmodel import Field, SQLModel, Relationship
|
||||
from pydantic import validator, BaseModel
|
||||
from database.db import get_session, get_session
|
||||
from services.password import validate_password
|
||||
from services.schema import as_form
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from database.exercices.models import Exercice, Tag
|
||||
from database.room.models import Member
|
||||
|
||||
class UserBase(SQLModel):
|
||||
username: str = Field(max_length=20, index= True)
|
||||
firstname: Optional[str]
|
||||
name: Optional[str]
|
||||
email: Optional[str]
|
||||
|
||||
class User(UserBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
clientId: Optional[UUID] = Field(default=uuid.uuid4(), index=True)
|
||||
hashed_password: str
|
||||
|
||||
exercices: List['Exercice'] = Relationship(back_populates='author')
|
||||
tags: List['Tag'] = Relationship(back_populates='author')
|
||||
members: List['Member'] = Relationship(back_populates='user')
|
||||
|
||||
@as_form
|
||||
class UserEdit(UserBase):
|
||||
pass
|
||||
|
||||
class UserRead(UserBase):
|
||||
id: int
|
||||
|
||||
|
||||
@as_form
|
||||
class UserRegister(BaseModel):
|
||||
username: str = Field(max_length=20)
|
||||
password: str
|
||||
password_confirm: str
|
||||
|
||||
|
||||
|
||||
@validator('password')
|
||||
def password_validation(cls, v):
|
||||
is_valid = validate_password(v)
|
||||
if is_valid != True:
|
||||
raise ValueError(is_valid)
|
||||
return v
|
||||
|
||||
@validator('password_confirm')
|
||||
def password_match(cls, v, values):
|
||||
if 'password' in values and v != values['password']:
|
||||
raise ValueError('Les mots de passe ne correspondent pas')
|
||||
return v
|
||||
|
||||
@as_form
|
||||
class PasswordSet(BaseModel):
|
||||
old_password: str
|
||||
password: str
|
||||
password_confirm: str
|
||||
|
||||
@validator('password')
|
||||
def password_validation(cls, v):
|
||||
is_valid = validate_password(v)
|
||||
if is_valid != True:
|
||||
raise ValueError(is_valid)
|
||||
return v
|
||||
|
||||
@validator('password_confirm')
|
||||
def password_match(cls, v, values):
|
||||
if 'password' in values and v != values['password']:
|
||||
raise ValueError('Les mots de passe ne correspondent pas')
|
||||
return v
|
17
backend/api/database/db.py
Normal file
17
backend/api/database/db.py
Normal file
@ -0,0 +1,17 @@
|
||||
import random
|
||||
import string
|
||||
from sqlmodel import SQLModel, create_engine, Session, select
|
||||
|
||||
sqlite_file_name = "database.db"
|
||||
sqlite_url = f"sqlite:///{sqlite_file_name}"
|
||||
|
||||
engine = create_engine(sqlite_url, echo=False, connect_args={"check_same_thread": False})
|
||||
|
||||
|
||||
def create_db_and_tables():
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
def get_session():
|
||||
with Session(engine) as s:
|
||||
yield s
|
||||
|
46
backend/api/database/exercices/FileField.py
Normal file
46
backend/api/database/exercices/FileField.py
Normal file
@ -0,0 +1,46 @@
|
||||
|
||||
import io
|
||||
import os
|
||||
from typing import IO, Type
|
||||
|
||||
from services.io import add_fast_api_root, get_filename, get_or_create_dir, remove_fastapi_root
|
||||
|
||||
|
||||
class FileFieldMeta(type):
|
||||
def __getitem__(self, upload_root: str,) -> Type['FileField']:
|
||||
return type('MyFieldValue', (FileField,), {'upload_root': upload_root})
|
||||
|
||||
|
||||
class FileField(str, metaclass=FileFieldMeta):
|
||||
upload_root: str
|
||||
|
||||
@classmethod
|
||||
def __get_validators__(cls):
|
||||
yield cls.validate
|
||||
|
||||
@classmethod
|
||||
def validate(cls, value: str | IO, values):
|
||||
upload_root = get_or_create_dir(
|
||||
add_fast_api_root(cls.upload_root))
|
||||
if not isinstance(value, str):
|
||||
value.seek(0)
|
||||
|
||||
is_binary = isinstance(value, io.BytesIO)
|
||||
|
||||
name = get_filename(value, 'exo_source.py')
|
||||
|
||||
parent = get_or_create_dir(os.path.join(
|
||||
upload_root, values['id_code']))
|
||||
|
||||
mode = 'w+' if not is_binary else 'wb+'
|
||||
|
||||
path = os.path.join(parent, name)
|
||||
with open(path, mode) as f:
|
||||
f.write(value.read())
|
||||
|
||||
return remove_fastapi_root(path)
|
||||
|
||||
else:
|
||||
if not os.path.exists(add_fast_api_root(value)):
|
||||
raise ValueError('File does not exist')
|
||||
return value
|
172
backend/api/database/exercices/crud.py
Normal file
172
backend/api/database/exercices/crud.py
Normal file
@ -0,0 +1,172 @@
|
||||
from fastapi import Query
|
||||
import os
|
||||
import shutil
|
||||
from typing import IO, List
|
||||
from sqlmodel import Session, select, or_
|
||||
from database.auth.models import User
|
||||
from database.db import get_session
|
||||
from fastapi import Depends
|
||||
from database.exercices.models import ExerciceCreate, Exercice, ExerciceEdit, ExerciceRead, ExercicesTagLink, Tag, TagCreate
|
||||
from services.auth import get_current_user
|
||||
from services.database import generate_unique_code
|
||||
from services.io import add_fast_api_root, get_ancestor, get_or_create_dir
|
||||
|
||||
|
||||
def create_exo_db(exercice: ExerciceCreate, user: User, exo_source: IO, db: Session):
|
||||
|
||||
exo_db = Exercice(**exercice.dict(exclude_unset=True),
|
||||
author_id=user.id, id_code=generate_unique_code(Exercice, db), exo_source=exo_source)
|
||||
|
||||
db.add(exo_db)
|
||||
db.commit()
|
||||
db.refresh(exo_db)
|
||||
return exo_db
|
||||
|
||||
|
||||
def clone_exo_source(path: str, id_code: str):
|
||||
if not os.path.exists(add_fast_api_root(path)):
|
||||
return None
|
||||
upload_root = get_ancestor(path, 2)
|
||||
path = add_fast_api_root(path)
|
||||
new_path = add_fast_api_root(
|
||||
os.path.join(upload_root, id_code))
|
||||
dir_path = get_or_create_dir(new_path)
|
||||
final_path = shutil.copy(path, dir_path)
|
||||
return final_path
|
||||
|
||||
|
||||
def clone_exo_db(exercice: Exercice, user: User, db: Session):
|
||||
if exercice.author_id == user.id:
|
||||
return 'Vous ne pouvez pas copier un de vos exercices'
|
||||
|
||||
new_exo = Exercice.from_orm(exercice)
|
||||
new_exo.id = None
|
||||
new_exo.author_id = user.id
|
||||
new_exo.origin_id = exercice.id
|
||||
|
||||
new_id_code = generate_unique_code(Exercice, db)
|
||||
new_exo.id_code = new_id_code
|
||||
|
||||
new_exo_source = clone_exo_source(exercice.exo_source, new_id_code)
|
||||
if not new_exo_source:
|
||||
return "Erreur lors de la copie de l'exercice, fichier source introuvable"
|
||||
new_exo.exo_source = new_exo_source
|
||||
|
||||
db.add(new_exo)
|
||||
db.commit()
|
||||
db.refresh(new_exo)
|
||||
return new_exo
|
||||
|
||||
|
||||
def update_exo_db(old_exo: Exercice, new_exo: ExerciceEdit, exo_source: IO | None, db: Session):
|
||||
|
||||
exo_data = new_exo.dict(exclude_unset=True, exclude_none=True)
|
||||
|
||||
for key, value in exo_data.items():
|
||||
setattr(old_exo, key, value)
|
||||
|
||||
if exo_source:
|
||||
os.remove(add_fast_api_root(old_exo.exo_source))
|
||||
old_exo.exo_source = exo_source
|
||||
db.add(old_exo)
|
||||
db.commit()
|
||||
db.refresh(old_exo)
|
||||
return old_exo
|
||||
|
||||
|
||||
def delete_exo_db(exo: Exercice, db: Session):
|
||||
db.delete(exo)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
def get_or_create_tag(tag: TagCreate, user: User, db: Session):
|
||||
tag_db = db.exec(select(Tag).where(Tag.author_id == user.id).where(or_(
|
||||
Tag.id_code == tag.id_code, Tag.label == tag.label))).first()
|
||||
if tag_db is not None:
|
||||
return tag_db
|
||||
id_code = generate_unique_code(Tag, db)
|
||||
tag_db = Tag(**{**tag.dict(exclude_unset=True),
|
||||
'id_code': id_code, 'author_id': user.id})
|
||||
db.add(tag_db)
|
||||
db.commit()
|
||||
db.refresh(tag_db)
|
||||
return tag_db
|
||||
|
||||
|
||||
def add_tags_db(exo: Exercice, tags: List[TagCreate], user: User, db: Session):
|
||||
for tag in tags:
|
||||
tag_db = get_or_create_tag(tag, user, db)
|
||||
exo.tags.append(tag_db)
|
||||
db.add(exo)
|
||||
db.commit()
|
||||
db.refresh(exo)
|
||||
return exo
|
||||
|
||||
|
||||
def remove_tag_db(exo: Exercice, tag: Tag, db: Session):
|
||||
exo.tags.remove(tag)
|
||||
db.add(tag)
|
||||
db.commit()
|
||||
return exo
|
||||
|
||||
|
||||
def parse_exo_tags(exo_id: int, user_id: int, db: Session):
|
||||
|
||||
exo_tags = db.exec(select(Tag, ExercicesTagLink).where(ExercicesTagLink.exercice_id ==
|
||||
exo_id, Tag.id == ExercicesTagLink.tag_id, Tag.author_id == user_id)).all() # select -> (Exercice, ExerciceLink)
|
||||
exo_tags = [t[0] for t in exo_tags]
|
||||
return exo_tags
|
||||
|
||||
|
||||
|
||||
# Dependencies
|
||||
|
||||
def get_exo_dependency(id_code: str, db: Session = Depends(get_session)):
|
||||
with db.no_autoflush:
|
||||
exo = db.exec(select(Exercice).where(
|
||||
Exercice.id_code == id_code)).first()
|
||||
return exo
|
||||
|
||||
|
||||
def check_exercice_author(exo: Exercice | None = Depends(get_exo_dependency), user: User = Depends(get_current_user)):
|
||||
if not exo:
|
||||
return None
|
||||
if exo.author_id != user.id:
|
||||
return False
|
||||
return exo
|
||||
|
||||
|
||||
def check_tag_author(tag_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_session)):
|
||||
tag = db.exec(select(Tag).where(Tag.id_code == tag_id)).first()
|
||||
if not tag:
|
||||
return None
|
||||
|
||||
is_owner = tag.author_id == user.id
|
||||
if is_owner == False:
|
||||
return False
|
||||
|
||||
return tag
|
||||
|
||||
|
||||
def get_tags_dependency(tags: List[str] | None = Query(None), db: Session = Depends(get_session)):
|
||||
if tags is None:
|
||||
return None
|
||||
validated_tags = []
|
||||
for t in tags:
|
||||
tag = db.exec(select(Tag.id).where(Tag.id_code == t)).first()
|
||||
if tag is not None:
|
||||
validated_tags.append(tag)
|
||||
return validated_tags
|
||||
|
||||
|
||||
#Serialize
|
||||
|
||||
def check_author(exo: Exercice, user_id:int):
|
||||
return exo.author_id == user_id
|
||||
|
||||
def serialize_exo(*, exo: Exercice, user_id: User = None, db: Session):
|
||||
tags = parse_exo_tags(exo_id=exo.id, user_id=user_id,
|
||||
db=db) if user_id is not None else []
|
||||
is_author = user_id is not None and check_author(exo=exo, user_id=user_id)
|
||||
return ExerciceRead(**exo.dict(), author=exo.author, original=exo.original, tags=tags, is_author=is_author)
|
184
backend/api/database/exercices/models.py
Normal file
184
backend/api/database/exercices/models.py
Normal file
@ -0,0 +1,184 @@
|
||||
from sqlmodel import select
|
||||
from sqlalchemy.inspection import inspect
|
||||
from enum import Enum
|
||||
import os
|
||||
from pydantic import BaseModel
|
||||
from pydantic import validator, root_validator
|
||||
from generateur.generateur_main import Generateur
|
||||
from services.exoValidation import get_support_from_path
|
||||
from services.io import add_fast_api_root, get_filename_from_path
|
||||
from services.schema import as_form
|
||||
from typing import Any, List, Optional
|
||||
from sqlmodel import SQLModel, Field, Relationship, Session
|
||||
from database.auth.models import User, UserRead
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from .FileField import FileField
|
||||
from database.db import get_session
|
||||
if TYPE_CHECKING:
|
||||
from database.auth.models import User
|
||||
|
||||
|
||||
class ExercicesTagLink(SQLModel, table=True):
|
||||
exercice_id: Optional[int] = Field(
|
||||
default=None, foreign_key='exercice.id', primary_key=True)
|
||||
tag_id: Optional[int] = Field(
|
||||
default=None, foreign_key='tag.id', primary_key=True)
|
||||
|
||||
|
||||
class ExerciceBase(SQLModel):
|
||||
name: str = Field(max_length=50, index=True)
|
||||
consigne: Optional[str] = Field(max_length=200, default=None)
|
||||
private: Optional[bool] = Field(default=False)
|
||||
|
||||
|
||||
class Exercice(ExerciceBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code: str = Field(unique=True, index=True)
|
||||
|
||||
exo_source: FileField['/uploads']
|
||||
|
||||
author_id: int = Field(foreign_key='user.id')
|
||||
author: "User" = Relationship(back_populates='exercices')
|
||||
|
||||
origin_id: Optional[int] = Field(
|
||||
foreign_key='exercice.id', default=None, nullable=True)
|
||||
original: Optional['Exercice'] = Relationship(back_populates='duplicated', sa_relationship_kwargs=dict(
|
||||
remote_side='Exercice.id'
|
||||
))
|
||||
duplicated: List['Exercice'] = Relationship(back_populates='original')
|
||||
|
||||
tags: List['Tag'] = Relationship(
|
||||
back_populates='exercices', link_model=ExercicesTagLink)
|
||||
|
||||
|
||||
def dict_relationship(self, *, exclude: List[str], include: List[str]):
|
||||
relationships = inspect(self.__class__).relationships.items()
|
||||
relationsData = {rname: getattr(self, rname) for rname, rp in relationships if rname not in exclude and (rname in include if len(include) != 0 else True)}
|
||||
return {**self.dict(), **relationsData}
|
||||
|
||||
class Config:
|
||||
validate_assignment = True
|
||||
extra='allow'
|
||||
|
||||
|
||||
|
||||
class ColorEnum(Enum):
|
||||
green='#00ff00'
|
||||
red="#ff0000"
|
||||
blue="#0000ff"
|
||||
string="string"
|
||||
|
||||
class TagBase(SQLModel):
|
||||
label: str = Field(max_length=20)
|
||||
color: ColorEnum
|
||||
|
||||
|
||||
class Tag(TagBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code: str = Field(unique=True, index=True)
|
||||
|
||||
author_id: int = Field(foreign_key='user.id')
|
||||
author: "User" = Relationship(back_populates='tags')
|
||||
|
||||
exercices: List["Exercice"] = Relationship(
|
||||
back_populates='tags', link_model=ExercicesTagLink)
|
||||
|
||||
|
||||
class TagCreate(TagBase):
|
||||
id_code: str = None
|
||||
|
||||
|
||||
class TagRead(TagBase):
|
||||
id_code: str
|
||||
|
||||
|
||||
@as_form
|
||||
class ExerciceCreate(ExerciceBase):
|
||||
pass
|
||||
|
||||
|
||||
class ExerciceEdit(ExerciceCreate):
|
||||
pass
|
||||
|
||||
|
||||
class ExerciceOrigin(SQLModel):
|
||||
#id: int
|
||||
id_code: str
|
||||
name: str
|
||||
|
||||
|
||||
class ExoSupport(BaseModel):
|
||||
pdf: bool
|
||||
csv: bool
|
||||
web: bool
|
||||
|
||||
|
||||
class ExampleEnum(Enum):
|
||||
csv = 'csv'
|
||||
pdf = 'pdf'
|
||||
web = 'web'
|
||||
undisponible = 'undisponible'
|
||||
|
||||
|
||||
class GeneratorOutput(BaseModel):
|
||||
calcul: str
|
||||
correction: str | None = None
|
||||
|
||||
|
||||
class Example(BaseModel):
|
||||
type: ExampleEnum = ExampleEnum.undisponible
|
||||
examples: List[GeneratorOutput] | None = None
|
||||
|
||||
|
||||
class Author(SQLModel):
|
||||
username: str
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_source_path_from_name_and_id(name: str, id_code: str):
|
||||
return f'/uploads/{id_code}/{name}'
|
||||
|
||||
class ExerciceRead(ExerciceBase):
|
||||
id_code: str
|
||||
author: Author
|
||||
original: ExerciceOrigin | None
|
||||
tags: List[TagRead] = None
|
||||
exo_source: str
|
||||
|
||||
supports: ExoSupport = None
|
||||
|
||||
examples: Example = None
|
||||
|
||||
is_author: bool = None
|
||||
|
||||
@validator('supports', always=True, pre=True)
|
||||
def get_supports(cls, value, values):
|
||||
exo_source = get_source_path_from_name_and_id(values['exo_source'], values['id_code'])
|
||||
exo_source = add_fast_api_root(exo_source)
|
||||
if os.path.exists(exo_source):
|
||||
support_compatibility = get_support_from_path(
|
||||
exo_source)
|
||||
return support_compatibility
|
||||
return None
|
||||
|
||||
@validator('examples', always=True)
|
||||
def get_examples(cls, value, values):
|
||||
if values.get('supports') == None:
|
||||
return {}
|
||||
supports = values.get('supports').dict()
|
||||
exo_source = get_source_path_from_name_and_id(
|
||||
values['exo_source'], values['id_code'])
|
||||
return {
|
||||
"type": ExampleEnum.csv if supports['csv'] == True else ExampleEnum.web if supports['web'] == True else None,
|
||||
"data": Generateur(add_fast_api_root(exo_source), 3, "csv" if supports['csv'] == True else "web" if supports['web'] == True else None, True) if supports['csv'] == True == True or supports['web'] == True == True else None
|
||||
}
|
||||
|
||||
@validator('exo_source')
|
||||
def get_exo_source_name(cls, value, values):
|
||||
if value is not None:
|
||||
return get_filename_from_path(value)
|
||||
return value
|
||||
|
||||
|
6
backend/api/database/room/crud.py
Normal file
6
backend/api/database/room/crud.py
Normal file
@ -0,0 +1,6 @@
|
||||
from sqlmodel import Session
|
||||
from database.room.models import RoomCreate
|
||||
from database.auth.models import User
|
||||
|
||||
def create_room_db(*,room: RoomCreate, user: User | None = None,username: str, db: Session):
|
||||
return
|
49
backend/api/database/room/models.py
Normal file
49
backend/api/database/room/models.py
Normal file
@ -0,0 +1,49 @@
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
from sqlmodel import SQLModel, Field, Relationship
|
||||
if TYPE_CHECKING:
|
||||
from database.auth.models import User
|
||||
|
||||
|
||||
class RoomBase(SQLModel):
|
||||
name: str = Field(max_length=20)
|
||||
public: bool = Field(default=False)
|
||||
|
||||
class RoomCreate(RoomBase):
|
||||
pass
|
||||
|
||||
class Room(RoomBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code: str
|
||||
|
||||
members: List['Member'] = Relationship(back_populates="room")
|
||||
|
||||
class RoomRead(RoomBase):
|
||||
id_code: str
|
||||
#members: List[]
|
||||
|
||||
class AnonymousBase(SQLModel):
|
||||
username: str = Field(max_length=20)
|
||||
|
||||
class AnonymousCreate(AnonymousBase):
|
||||
pass
|
||||
|
||||
class Anonymous(AnonymousBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
reconnect_code: str
|
||||
|
||||
member: 'Member' = Relationship(back_populates="anonymous")
|
||||
|
||||
|
||||
class Member(SQLModel, table = True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
|
||||
user_id: Optional[int] = Field(foreign_key="user.id", default=None)
|
||||
anonymous_id: Optional[int] = Field(foreign_key="anonymous.id", default=None)
|
||||
|
||||
anonymous: Optional[Anonymous] = Relationship(back_populates="member")
|
||||
user: Optional['User'] = Relationship(back_populates='members')
|
||||
|
||||
room_id: int = Field(foreign_key="room.id")
|
||||
room: Room = Relationship(back_populates='members')
|
||||
|
||||
|
30
backend/api/dbo/auth/crud.py
Normal file
30
backend/api/dbo/auth/crud.py
Normal file
@ -0,0 +1,30 @@
|
||||
from services.password import get_password_hash
|
||||
from database.auth.models import User
|
||||
|
||||
async def create_user_db(username, password):
|
||||
#id_code = generate_unique_code(UserModel)
|
||||
return await User.create(username=username, hashed_password=password)
|
||||
|
||||
async def get_user_from_username_db(username):
|
||||
return await User.get_or_none(username=username)
|
||||
|
||||
|
||||
async def get_user_from_clientId_db(clientId):
|
||||
return await User.get_or_none(clientId=clientId)
|
||||
|
||||
|
||||
async def update_user_db(username: str, **kwargs):
|
||||
user = await get_user_from_username_db(username)
|
||||
await user.update_from_dict({**kwargs}).save()
|
||||
return user
|
||||
|
||||
async def delete_user_db(username: str):
|
||||
user = await get_user_from_username_db(username)
|
||||
await user.delete()
|
||||
|
||||
|
||||
async def update_password_db(username: str, password: str):
|
||||
user = await get_user_from_username_db(username)
|
||||
|
||||
await user.update_from_dict({'hashed_password': get_password_hash(password)}).save(update_fields=["hashed_password"])
|
||||
return user
|
22
backend/api/dbo/auth/models.py
Normal file
22
backend/api/dbo/auth/models.py
Normal file
@ -0,0 +1,22 @@
|
||||
import uuid
|
||||
from tortoise.models import Model
|
||||
from tortoise import fields
|
||||
|
||||
|
||||
class User(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
clientId = fields.UUIDField(unique=True, default=uuid.uuid4)
|
||||
username = fields.CharField(max_length=100, unique=True)
|
||||
hashed_password = fields.CharField(max_length=255)
|
||||
email = fields.CharField(null=True, max_length=255)
|
||||
|
||||
name = fields.CharField(null=True, max_length=255)
|
||||
firstname = fields.CharField(null=True, max_length=255)
|
||||
|
||||
disabled = fields.BooleanField(default=False)
|
||||
|
||||
class PydanticMeta:
|
||||
exclude = ['hashed_password']
|
||||
|
||||
class Meta:
|
||||
table = "users"
|
BIN
backend/api/dbo/db.sqlite3
Normal file
BIN
backend/api/dbo/db.sqlite3
Normal file
Binary file not shown.
106
backend/api/dbo/exercices/crud.py
Normal file
106
backend/api/dbo/exercices/crud.py
Normal file
@ -0,0 +1,106 @@
|
||||
import os
|
||||
import shutil
|
||||
from typing import BinaryIO, List
|
||||
from schemas.exercices import TagIn, TagIn_schema
|
||||
from services.database import generate_unique_code
|
||||
from services.io import add_fast_api_root, get_ancestor, get_filename_from_path, get_parent_dir, remove_fastapi_root, remove_if_exists, get_or_create_dir
|
||||
|
||||
from database.exercices.models import Exercice, Tag
|
||||
from schemas.exercices import Exercice_schema
|
||||
|
||||
|
||||
|
||||
async def create_exo_db(*args, **kwargs) -> Exercice:
|
||||
code = await generate_unique_code(Exercice)
|
||||
return await Exercice.create(*args, **{**kwargs, 'id_code': code})
|
||||
|
||||
|
||||
async def update_exo_db(id_code: str,exo_source: BinaryIO | None = None, **kwargs) -> Exercice:
|
||||
exo = await Exercice.get(id_code=id_code)
|
||||
if exo_source != None:
|
||||
path = add_fast_api_root(exo.exo_source)
|
||||
|
||||
remove_if_exists(path)
|
||||
kwargs['exo_source'] = exo_source
|
||||
|
||||
await exo.update_from_dict({**kwargs, 'origin_id': exo.origin_id, 'isOriginal': exo.isOriginal}).save()
|
||||
return exo
|
||||
|
||||
|
||||
async def delete_exo_db(id_code: str):
|
||||
exo = await Exercice.get(id_code=id_code)
|
||||
path = add_fast_api_root(exo.exo_source)
|
||||
|
||||
parent = get_parent_dir(path)
|
||||
|
||||
shutil.rmtree(parent)
|
||||
|
||||
return await exo.delete()
|
||||
|
||||
|
||||
def clone_exo_source(path, id_code):
|
||||
upload_root = get_ancestor(path, 2)
|
||||
path = add_fast_api_root(path)
|
||||
new_path = add_fast_api_root(
|
||||
os.path.join(upload_root, id_code))
|
||||
dir_path = get_or_create_dir(new_path)
|
||||
final_path = shutil.copy(path, dir_path)
|
||||
return final_path
|
||||
|
||||
def exclude_dict_key(dict, key):
|
||||
return {k: v for k, v in dict.items() if k != key}
|
||||
|
||||
|
||||
async def clone_exo_db(id_code: str, user_id):
|
||||
exo = await Exercice.get(id_code=id_code)
|
||||
new_id_code = await generate_unique_code(Exercice)
|
||||
|
||||
exo_obj = await Exercice_schema.from_tortoise_orm(exo)
|
||||
exo_obj = exo_obj.dict(exclude_unset=True)
|
||||
print("eaoiuezaoiueza", exo_obj)
|
||||
exo_obj.pop('tags')
|
||||
exo_obj = exclude_dict_key(exo_obj, 'tags')
|
||||
exo_obj = exclude_dict_key(exo_obj, 'author')
|
||||
#exo_obj.pop('exercices')
|
||||
|
||||
path = clone_exo_source(exo.exo_source, new_id_code)
|
||||
print('\n\npatiuar', path, exo_obj, '\n',
|
||||
exclude_dict_key(exo_obj, 'tags'))
|
||||
new_exo = Exercice(**{**exo_obj, 'id_code': new_id_code, 'exo_source': path,
|
||||
"isOriginal": False, 'author_id': user_id}, origin_id=exo.id)
|
||||
|
||||
await new_exo.save()
|
||||
return new_exo
|
||||
|
||||
|
||||
async def get_exo_source_path(id_code: str):
|
||||
exo = await Exercice.get(id_code=id_code)
|
||||
path = add_fast_api_root(exo.exo_source)
|
||||
return path
|
||||
|
||||
#tags
|
||||
async def get_or_create_tag(id_code: str, data: List[TagIn_schema]):
|
||||
tag = await Tag.get_or_none(id_code=id_code)
|
||||
if tag == None:
|
||||
code = await generate_unique_code(Tag)
|
||||
return await Tag.create(**{**data, 'id_code': code})
|
||||
return tag
|
||||
|
||||
|
||||
async def add_tag_db(exo: Exercice, tags_data: List[TagIn], user_id: int) -> Exercice:
|
||||
for t in tags_data:
|
||||
tag = await get_or_create_tag(t.id_code, {**t.dict(exclude_unset=True), 'owner_id': user_id})
|
||||
await exo.tags.add(tag)
|
||||
return exo
|
||||
|
||||
'''
|
||||
async def add_tag_db(id_code: str, tag: Tag) -> Exercice:
|
||||
exo = await Exercice.get(id_code=id_code)
|
||||
await exo.tags.add(tag)
|
||||
return exo
|
||||
'''
|
||||
async def delete_tag_db(exo_id: str, tag_id: str) -> Exercice:
|
||||
exo = await Exercice.get(id_code=exo_id)
|
||||
tag = await exo.tags.all().get(id_code=tag_id)
|
||||
await exo.tags.remove(tag)
|
||||
return exo
|
105
backend/api/dbo/exercices/models.py
Normal file
105
backend/api/dbo/exercices/models.py
Normal file
@ -0,0 +1,105 @@
|
||||
import asyncio
|
||||
from io import BytesIO
|
||||
import io
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
|
||||
from tortoise.models import Model
|
||||
from tortoise import fields
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
import async_to_sync as sync
|
||||
from tortoise.manager import Manager
|
||||
from generateur.generateur_main import Generateur
|
||||
|
||||
from services.io import add_fast_api_root
|
||||
|
||||
from database.utils.ExoSourceValidator import ExoSourceValidator
|
||||
from services.exoValidation import get_support_from_path, get_support_from_data
|
||||
|
||||
from database.utils.FileField import FileField
|
||||
|
||||
|
||||
class Tag(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
id_code = fields.CharField(unique=True, default="", max_length=255)
|
||||
|
||||
label = fields.CharField(max_length=35)
|
||||
color = fields.CharField(max_length=100)
|
||||
|
||||
owner = fields.ForeignKeyField('models.User')
|
||||
|
||||
|
||||
class Exercice(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
id_code = fields.CharField(default="", max_length=10, unique=True)
|
||||
|
||||
name = fields.CharField(max_length=50)
|
||||
|
||||
consigne = fields.CharField(max_length=200, null=True, default="")
|
||||
|
||||
exo_source = FileField(upload_root="/uploads",
|
||||
validators=[ExoSourceValidator()])
|
||||
|
||||
updated_at = fields.DatetimeField(auto_now=True)
|
||||
|
||||
private = fields.BooleanField(default=False)
|
||||
|
||||
tags = fields.ManyToManyField('models.Tag', null=True)
|
||||
|
||||
origin = fields.ForeignKeyField('models.Exercice', null=True)
|
||||
isOriginal = fields.BooleanField(default=True)
|
||||
|
||||
author = fields.ForeignKeyField('models.User')
|
||||
|
||||
def pdfSupport(self) -> bool:
|
||||
if not isinstance(self.exo_source, io.BytesIO) and not isinstance(self.exo_source, io.StringIO):
|
||||
if os.path.exists(add_fast_api_root(self.exo_source)):
|
||||
support_compatibility = get_support_from_path(
|
||||
add_fast_api_root(self.exo_source))
|
||||
return support_compatibility['isPdf']
|
||||
return False
|
||||
else:
|
||||
self.exo_source.seek(0)
|
||||
support_compatibility = get_support_from_data(
|
||||
self.exo_source.read())
|
||||
return support_compatibility['isPdf']
|
||||
|
||||
def csvSupport(self) -> bool:
|
||||
if not isinstance(self.exo_source, io.BytesIO) and not isinstance(self.exo_source, io.StringIO):
|
||||
if os.path.exists(add_fast_api_root(self.exo_source)):
|
||||
support_compatibility = get_support_from_path(
|
||||
add_fast_api_root(self.exo_source))
|
||||
return support_compatibility['isCsv']
|
||||
return False
|
||||
else:
|
||||
self.exo_source.seek(0)
|
||||
support_compatibility = get_support_from_data(
|
||||
self.exo_source.read())
|
||||
return support_compatibility['isCsv']
|
||||
|
||||
def webSupport(self) -> bool:
|
||||
|
||||
if not isinstance(self.exo_source, io.BytesIO) and not isinstance(self.exo_source, io.StringIO):
|
||||
if os.path.exists(add_fast_api_root(self.exo_source)):
|
||||
support_compatibility = get_support_from_path(
|
||||
add_fast_api_root(self.exo_source))
|
||||
return support_compatibility['isWeb']
|
||||
return False
|
||||
else:
|
||||
self.exo_source.seek(0)
|
||||
support_compatibility = get_support_from_data(
|
||||
self.exo_source.read())
|
||||
return support_compatibility['isWeb']
|
||||
|
||||
def examples(self) -> dict:
|
||||
if not isinstance(self.exo_source, io.BytesIO) and not isinstance(self.exo_source, io.StringIO):
|
||||
return {
|
||||
"type": "Csv" if self.csvSupport() else "web" if self.webSupport() else None,
|
||||
"data": Generateur(add_fast_api_root(self.exo_source), 3, "csv" if self.csvSupport() else "web" if self.pdfSupport() else None, True) if self.csvSupport() == True or self.webSupport() == True else None
|
||||
}
|
||||
return {}
|
||||
|
||||
class PydanticMeta:
|
||||
computed = ["pdfSupport", "csvSupport", "webSupport", 'examples']
|
||||
exclude = ["exo_source", "exercices", 'id']
|
106
backend/api/dbo/rooms/crud.py
Normal file
106
backend/api/dbo/rooms/crud.py
Normal file
@ -0,0 +1,106 @@
|
||||
from schemas.rooms import AnonymousIn_schema, RoomIn_schema
|
||||
from schemas.users import User_schema
|
||||
from database.auth.models import User
|
||||
from database.rooms.models import AnonymousMember, Room, RoomOwner, Waiter
|
||||
from services.database import generate_unique_code
|
||||
|
||||
|
||||
async def create_room_with_user_db(room: RoomIn_schema, user: User_schema):
|
||||
code = await generate_unique_code(Room)
|
||||
|
||||
room_obj = await Room.create(**room.dict(exclude_unset=True), id_code=code)
|
||||
|
||||
user = await User.get(id=user.id)
|
||||
await room_obj.users.add(user)
|
||||
await RoomOwner.create(room_id=room_obj.id, user_id=user.id)
|
||||
return room_obj
|
||||
|
||||
|
||||
async def create_room_anonymous_db(room: RoomIn_schema, anonymous: AnonymousIn_schema):
|
||||
code = await generate_unique_code(Room)
|
||||
|
||||
room_obj = await Room.create(**room.dict(exclude_unset=True), id_code=code)
|
||||
|
||||
anonymous_code = await generate_unique_code(AnonymousMember)
|
||||
anonymous = await AnonymousMember.create(**anonymous.dict(exclude_unset=True), id_code=anonymous_code, room_id=room_obj.id)
|
||||
|
||||
await RoomOwner.create(room_id=room_obj.id, anonymous_id=anonymous.id)
|
||||
return room_obj
|
||||
|
||||
|
||||
async def get_room_db(room_id: str):
|
||||
room = await Room.get_or_none(id_code=room_id)
|
||||
return room
|
||||
|
||||
|
||||
async def check_user_in_room(room: Room, user: User):
|
||||
return await room.users.filter(id=user.id).count() != 0
|
||||
|
||||
|
||||
async def get_member_by_code(room: Room, code: str):
|
||||
anonymous = await room.anonymousmembers
|
||||
filtered_anonymous = [
|
||||
m for m in anonymous if m.id_code == code]
|
||||
if len(filtered_anonymous) == 0:
|
||||
return None
|
||||
return filtered_anonymous[0]
|
||||
|
||||
|
||||
async def check_user_owner(room: Room, user: User):
|
||||
room_owner = await room.room_owner
|
||||
user_owner = await room_owner.user
|
||||
if user_owner == None:
|
||||
return False
|
||||
return user_owner.id == user.id
|
||||
|
||||
|
||||
async def check_anonymous_owner(room: Room, anonymous: AnonymousMember):
|
||||
room_owner = await room.room_owner
|
||||
anonymous_owner = await room_owner.anonymous
|
||||
if anonymous_owner == None:
|
||||
return False
|
||||
return anonymous_owner.id_code == anonymous.id_code
|
||||
|
||||
|
||||
async def create_waiter_by_user(room: Room, user: User):
|
||||
code = await generate_unique_code(Waiter)
|
||||
return await Waiter.create(room_id=room.id, user_id=user.id, name=user.username, id_code=code)
|
||||
|
||||
|
||||
async def create_waiter_anonymous(room: Room, name: str):
|
||||
code = await generate_unique_code(Waiter)
|
||||
return await Waiter.create(name=name, id_code=code, room_id=room.id)
|
||||
|
||||
|
||||
async def connect_room(room: Room, code):
|
||||
online = room.online
|
||||
await room.update_from_dict({'online': [*online, code]}).save(update_fields=['online'])
|
||||
return
|
||||
|
||||
|
||||
async def disconnect_room(room: Room, code):
|
||||
online = room.online
|
||||
await room.update_from_dict({'online': [o for o in online if o != code]}).save(update_fields=['online'])
|
||||
return
|
||||
|
||||
|
||||
async def validate_name_in_room(room: Room, name):
|
||||
anonymous = await room.anonymousmembers
|
||||
if len([a for a in anonymous if a == name]) != 0:
|
||||
return "Pseudo déjà utilisé"
|
||||
if len(name) < 3:
|
||||
return "Pseudo trop court"
|
||||
if len(name) > 30:
|
||||
return "Pseudo trop long"
|
||||
return True
|
||||
|
||||
|
||||
async def check_user_in_room(room_id: str , user: User):
|
||||
room = await Room.get(id_code = room_id)
|
||||
users = await room.users.filter(id=user.id)
|
||||
return users.count() != 0
|
||||
|
||||
async def check_user_in_room(room_id: str , anonymous: AnonymousMember):
|
||||
room = await Room.get(id_code = room_id)
|
||||
members = await room.anonymousmembers.filter(id=anonymous.id)
|
||||
return members.count() != 0
|
74
backend/api/dbo/rooms/models.py
Normal file
74
backend/api/dbo/rooms/models.py
Normal file
@ -0,0 +1,74 @@
|
||||
from tortoise.models import Model
|
||||
from tortoise import fields
|
||||
|
||||
|
||||
class Room(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
id_code = fields.CharField(max_length=30, unique=True)
|
||||
|
||||
name = fields.CharField(max_length=255)
|
||||
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
|
||||
public_result = fields.BooleanField(default=False)
|
||||
private = fields.BooleanField(default=True)
|
||||
|
||||
online = fields.JSONField(default=list)
|
||||
|
||||
users = fields.ManyToManyField('models.UserModel')
|
||||
|
||||
class PydanticMeta:
|
||||
exlude = ('users__email')
|
||||
|
||||
|
||||
class AnonymousMember(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
id_code = fields.CharField(max_length=30, unique=True)
|
||||
|
||||
name = fields.CharField(max_length=255)
|
||||
|
||||
room = fields.ForeignKeyField('models.Room')
|
||||
|
||||
class PydanticMeta:
|
||||
exclude = ['room_owner', "id_code", 'id', 'challenger']
|
||||
|
||||
|
||||
class Waiter(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
name = fields.CharField(max_length=255)
|
||||
id_code = fields.CharField(max_length=30, unique=True)
|
||||
room = fields.ForeignKeyField('models.Room')
|
||||
user = fields.ForeignKeyField("models.UserModel", null=True)
|
||||
|
||||
|
||||
class RoomOwner(Model):
|
||||
user = fields.ForeignKeyField('models.UserModel', null=True)
|
||||
anonymous = fields.OneToOneField('models.AnonymousMember', null=True)
|
||||
room = fields.OneToOneField('models.Room', null=True)
|
||||
|
||||
class Meta:
|
||||
table = 'room_owner'
|
||||
|
||||
|
||||
class Parcours(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
id_code = fields.CharField(max_length=30, unique=True)
|
||||
|
||||
name = fields.CharField(max_length=255)
|
||||
created_at = fields.DateField(auto_now_add=True)
|
||||
|
||||
room = fields.ForeignKeyField('models.Room')
|
||||
|
||||
timer = fields.IntField(default=10)
|
||||
|
||||
exercices = fields.JSONField(default=list)
|
||||
|
||||
success_condition = fields.IntField(default=10)
|
||||
|
||||
|
||||
class Challenger(Model):
|
||||
parcours = fields.ForeignKeyField('models.Parcours')
|
||||
anonymous = fields.OneToOneField('models.AnonymousMember', null=True)
|
||||
user = fields.OneToOneField("models.UserModel", null=True)
|
||||
|
||||
challenges = fields.JSONField(default=list)
|
19
backend/api/dbo/utils/ExoSourceValidator.py
Normal file
19
backend/api/dbo/utils/ExoSourceValidator.py
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
from tortoise.exceptions import ValidationError
|
||||
from tortoise.validators import Validator
|
||||
import typing
|
||||
from services.exoValidation import get_support_from_data
|
||||
|
||||
class ExoSourceValidator(Validator):
|
||||
"""
|
||||
A validator to validate ...
|
||||
"""
|
||||
|
||||
def __call__(self, value: typing.IO):
|
||||
exo_supports_compatibility = get_support_from_data(
|
||||
value.read())
|
||||
if not exo_supports_compatibility['isPdf'] and not exo_supports_compatibility['isCsv'] and not exo_supports_compatibility['isWeb']:
|
||||
raise ValidationError(
|
||||
'[Error] : Exercice non valide (compatible avec aucun support)')
|
||||
|
||||
|
61
backend/api/dbo/utils/FileField.py
Normal file
61
backend/api/dbo/utils/FileField.py
Normal file
@ -0,0 +1,61 @@
|
||||
import os
|
||||
import typing
|
||||
import uuid
|
||||
from fastapi import UploadFile
|
||||
from tortoise.fields import TextField
|
||||
from tortoise import ConfigurationError
|
||||
import io
|
||||
from services.io import delete_root_slash, add_fast_api_root, get_filename_from_path, get_or_create_dir, is_binary_file, get_filename, remove_fastapi_root
|
||||
|
||||
|
||||
class FileField(TextField):
|
||||
def __init__(self, *, upload_root: str, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.upload_root = delete_root_slash(upload_root)
|
||||
|
||||
self.upload_root = get_or_create_dir(os.path.join(
|
||||
os.environ.get('FASTAPI_ROOT_URL'), self.upload_root))
|
||||
|
||||
def _is_binary(self, file: UploadFile):
|
||||
return isinstance(file, io.BytesIO)
|
||||
|
||||
def to_db_value(self, value: typing.IO, instance):
|
||||
if not isinstance(value, str):
|
||||
super().validate(value)
|
||||
value.seek(0)
|
||||
is_binary = self._is_binary(value)
|
||||
name = get_filename(value)
|
||||
|
||||
parent = get_or_create_dir(os.path.join(
|
||||
self.upload_root, instance.id_code))
|
||||
|
||||
mode = 'w+' if not is_binary else 'wb+'
|
||||
|
||||
path = os.path.join(self.upload_root, parent, name)
|
||||
|
||||
with open(path, mode) as f:
|
||||
f.write(value.read())
|
||||
|
||||
return remove_fastapi_root(path)
|
||||
return value
|
||||
|
||||
def to_python_value(self, value: str):
|
||||
return value
|
||||
|
||||
"""
|
||||
abs_path = get_abs_path_from_relative_to_root(value)
|
||||
if is_binary_file(abs_path):
|
||||
mode = 'rb'
|
||||
buffer = io.BytesIO()
|
||||
else:
|
||||
mode = 'r'
|
||||
buffer = io.StringIO()
|
||||
|
||||
buffer.name = get_filename_from_path(value)
|
||||
|
||||
with open(abs_path, mode) as f:
|
||||
buffer.write(f.read())
|
||||
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
"""
|
72
backend/api/generateur/generateur_csv.py
Normal file
72
backend/api/generateur/generateur_csv.py
Normal file
@ -0,0 +1,72 @@
|
||||
from .generateur_main import Generateur
|
||||
PAGE_LINES = {
|
||||
10: 53,
|
||||
12: 49,
|
||||
14: 39,
|
||||
16: 34,
|
||||
18: 31
|
||||
}
|
||||
MAX_LENGTH = {
|
||||
10: 38,
|
||||
12: 32,
|
||||
14: 25,
|
||||
16: 23,
|
||||
18: 20
|
||||
}
|
||||
|
||||
|
||||
def Csv_generator(path, nb_in_serie, nb_page, police, consigne, writer):
|
||||
exo_exemple = Generateur(path, 1, 'csv')
|
||||
if len(consigne) < MAX_LENGTH[police] and len(consigne) > len(exo_exemple):
|
||||
longueur_max = len(consigne) + 5
|
||||
elif len(consigne) > MAX_LENGTH[police] and len(consigne) > len(exo_exemple):
|
||||
longueur_max = MAX_LENGTH[police]
|
||||
elif len(consigne) > MAX_LENGTH[police] and len(consigne) < len(exo_exemple):
|
||||
longueur_max = len(exo_exemple)
|
||||
elif len(consigne) < MAX_LENGTH[police] and len(consigne) < len(exo_exemple):
|
||||
longueur_max = len(exo_exemple)
|
||||
else:
|
||||
longueur_max = len(exo_exemple)
|
||||
|
||||
consigne_lines = []
|
||||
if len(consigne) > 30:
|
||||
cons = consigne.replace(',', ' ').split(' ')
|
||||
text_longueur = ''
|
||||
for i in cons:
|
||||
text_longueur = text_longueur + i + ' '
|
||||
if len(text_longueur) > longueur_max:
|
||||
consigne_lines.append(text_longueur)
|
||||
text_longueur = ''
|
||||
# print(text_longueur)
|
||||
else:
|
||||
consigne_lines.append(consigne)
|
||||
serie_page_vertical = int(PAGE_LINES[police] /
|
||||
(nb_in_serie + 1 + len(consigne_lines)))
|
||||
|
||||
rest_line = PAGE_LINES[police] - (serie_page_vertical * nb_in_serie +
|
||||
serie_page_vertical * len(consigne_lines) + serie_page_vertical)
|
||||
|
||||
max_length = len(exo_exemple) if len(
|
||||
exo_exemple) > longueur_max else longueur_max
|
||||
max_in_line = 2 * MAX_LENGTH[police]
|
||||
space = max_in_line / 8
|
||||
|
||||
nb_in_line = int(max_in_line / (max_length + space)) + 1
|
||||
|
||||
for p in range(nb_page):
|
||||
for c in range(serie_page_vertical):
|
||||
|
||||
for w in consigne_lines:
|
||||
writer.writerow([*[w, ""] * nb_in_line])
|
||||
|
||||
for k in range(nb_in_serie):
|
||||
calcul_list = list(
|
||||
map(lambda calc: calc['calcul'], Generateur(path, nb_in_line, 'csv')))
|
||||
n = 1
|
||||
for i in range(n, len(calcul_list) + n + 1, n+1):
|
||||
calcul_list.insert(i, '')
|
||||
writer.writerow(calcul_list)
|
||||
writer.writerow([''])
|
||||
|
||||
for r in range(rest_line):
|
||||
writer.writerow([''])
|
49
backend/api/generateur/generateur_main.py
Normal file
49
backend/api/generateur/generateur_main.py
Normal file
@ -0,0 +1,49 @@
|
||||
import re
|
||||
import importlib.util
|
||||
|
||||
|
||||
def getObjectKey(obj, key):
|
||||
if obj[key] == None:
|
||||
return None
|
||||
return key if obj[key] != False else 'calcul' if obj['calcul'] != False else None
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def parseCorrection(calc, replacer='...'):
|
||||
exp_list = re.findall(r"\[([A-Za-z0-9_]+)\]", calc)
|
||||
for exp in exp_list:
|
||||
calc = calc.replace(f'[{exp}]', replacer)
|
||||
return calc
|
||||
|
||||
|
||||
def Generateur(path, quantity, key, forcedCorrection=False):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"tmp", path)
|
||||
tmp = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(tmp)
|
||||
try:
|
||||
main_func = tmp.main
|
||||
except:
|
||||
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 = tmp.CORRECTION_REPLACER
|
||||
except:
|
||||
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]})
|
||||
return op_list
|
138
backend/api/main.py
Normal file
138
backend/api/main.py
Normal file
@ -0,0 +1,138 @@
|
||||
#import schemas.base
|
||||
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, UserBase, UserRead
|
||||
from database.exercices.models import Exercice, ExerciceRead
|
||||
import database.db
|
||||
from fastapi_pagination import add_pagination
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from fastapi.exceptions import RequestValidationError, ValidationError
|
||||
from fastapi import FastAPI, HTTPException, Depends, Request, status, Header
|
||||
from fastapi_jwt_auth import AuthJWT
|
||||
from fastapi_jwt_auth.exceptions import AuthJWTException
|
||||
from fastapi.responses import JSONResponse
|
||||
from typing import List, 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
|
||||
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 sqladmin import Admin, ModelView
|
||||
from database.db import engine
|
||||
from fastapi.security import OAuth2PasswordBearer, HTTPBearer
|
||||
from pydantic import Field
|
||||
app = FastAPI(title="API Generateur d'exercices")
|
||||
origins = [
|
||||
"http://localhost:8000",
|
||||
"https://localhost:8001",
|
||||
"http://localhost",
|
||||
"http://localhost:8080",
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=['*'],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
admin = Admin(app, engine)
|
||||
|
||||
|
||||
class UserAdmin(ModelView, model=User):
|
||||
column_list = [User.id, User.username]
|
||||
|
||||
|
||||
admin.add_view(UserAdmin)
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
create_db_and_tables()
|
||||
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
@app.exception_handler(ValidationError)
|
||||
async def validation_exception_handler(request, exc: RequestValidationError|ValidationError):
|
||||
errors = {}
|
||||
print(exc.errors())
|
||||
for e in exc.errors():
|
||||
errors[e['loc'][-1] + "_error"] = e['msg']
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content=jsonable_encoder({"detail": errors}),
|
||||
)
|
||||
|
||||
|
||||
#JWT AUTH
|
||||
|
||||
|
||||
|
||||
|
||||
@AuthJWT.load_config
|
||||
def get_config():
|
||||
return config.settings
|
||||
|
||||
# exception handler for authjwt
|
||||
# in production, you can tweak performance using orjson response
|
||||
@app.exception_handler(AuthJWTException)
|
||||
def authjwt_exception_handler(request: Request, exc: AuthJWTException):
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"detail": exc.message}
|
||||
)
|
||||
|
||||
|
||||
#REDIS
|
||||
|
||||
|
||||
'''
|
||||
@AuthJWT.token_in_denylist_loader
|
||||
def check_if_token_in_denylist(decrypted_token):
|
||||
jti = decrypted_token['jti']
|
||||
entry = config.redis_conn.get(jti)
|
||||
return entry and entry == 'true'
|
||||
'''
|
||||
|
||||
#ROUTES
|
||||
|
||||
app.include_router(routes.base.api_router)
|
||||
|
||||
|
||||
@app.delete('/access-revoke')
|
||||
def access_revoke(Authorize: AuthJWT = Depends()):
|
||||
Authorize.jwt_required()
|
||||
revoke_access(Authorize.get_raw_jwt())
|
||||
return {"detail": "Access token has been revoke"}
|
||||
|
||||
|
||||
@app.delete('/refresh-revoke')
|
||||
def refresh_revoke(Authorize: AuthJWT = Depends()):
|
||||
Authorize.jwt_refresh_token_required()
|
||||
revoke_refresh(Authorize.get_raw_jwt())
|
||||
return {"detail": "Refresh token has been revoke"}
|
||||
|
||||
|
||||
class user(UserRead):
|
||||
exercices: List[ExerciceRead] = None
|
||||
|
||||
@app.post('/test', response_model=List[ExerciceRead] )
|
||||
def test(db:Session= Depends(get_session)):
|
||||
#create_user_db('lilian', get_password_hash('Pomme937342'), db)
|
||||
create_user_db('lilian2', get_password_hash('Pomme937342'), db)
|
||||
exos = db.exec(select(Exercice)).all()
|
||||
return exos
|
||||
|
||||
|
||||
add_pagination(app)
|
87
backend/api/routes/auth/routes.py
Normal file
87
backend/api/routes/auth/routes.py
Normal file
@ -0,0 +1,87 @@
|
||||
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 database.db import get_session
|
||||
router = APIRouter(tags=['Authentification'])
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
refresh_token: str
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
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))
|
||||
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
|
||||
|
||||
@router.post('/register', response_model=Token)
|
||||
def register(user: UserRegister = Depends(UserRegister.as_form), Authorize: AuthJWT = Depends(), db: Session = Depends(get_session)):
|
||||
username = check_unique_username(user.username, db)
|
||||
if not username:
|
||||
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))
|
||||
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
|
||||
|
||||
@router.get('/users', response_model=List[UserRead])
|
||||
def get_users(db: Session = Depends(get_session)):
|
||||
users = db.exec(select(User)).all()
|
||||
return users
|
||||
|
||||
@router.put('/user' , response_model=UserRead,)
|
||||
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
|
||||
|
||||
|
||||
@router.put('/user/password')
|
||||
def update_password(password: PasswordSet = Depends(PasswordSet.as_form), user: User = Depends(get_current_user), db: Session = Depends(get_session), Authorize: AuthJWT = Depends()):
|
||||
isValid = verify_password(password.old_password, user.hashed_password)
|
||||
if not isValid:
|
||||
raise HTTPException(status_code=401, detail={'old_password_error': 'Mot de passe invalide'})
|
||||
|
||||
user_obj = update_password_db(user.id, password.password, db)
|
||||
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))
|
||||
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.post('/logout')
|
||||
def logout(user: User = Depends(get_current_user), db: Session = Depends(get_session),):
|
||||
change_user_uuid(user.id, db)
|
||||
return {'ok': True}
|
||||
|
||||
|
||||
@router.delete('/user')
|
||||
def delete_user(user: User = Depends(authenticate_user), db: Session = Depends(get_session)):
|
||||
delete_user_db(user.id, db)
|
||||
return {'ok': True}
|
||||
|
||||
|
||||
@router.post('/check-access',)
|
||||
def check_token(user: User = Depends(get_current_user_optional)):
|
||||
return {'username': user.username} if user != None else False
|
||||
|
||||
@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)
|
||||
return {"access_token": new_access_token}
|
10
backend/api/routes/base.py
Normal file
10
backend/api/routes/base.py
Normal file
@ -0,0 +1,10 @@
|
||||
from fastapi import APIRouter
|
||||
import routes.auth.routes
|
||||
import routes.exercices.routes
|
||||
import routes.room.routes
|
||||
api_router = APIRouter()
|
||||
|
||||
|
||||
api_router.include_router(routes.auth.routes.router)
|
||||
api_router.include_router(routes.exercices.routes.router)
|
||||
api_router.include_router(routes.room.routes.router)
|
183
backend/api/routes/exercices/routes.py
Normal file
183
backend/api/routes/exercices/routes.py
Normal file
@ -0,0 +1,183 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, UploadFile, HTTPException, status
|
||||
from database.auth.models import User
|
||||
from database.db import get_session
|
||||
from database.exercices.models import Exercice, ExerciceCreate, ExerciceEdit, ExerciceRead, ExercicesTagLink, Tag, TagCreate, TagRead
|
||||
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_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
|
||||
|
||||
router = APIRouter(tags=['exercices'])
|
||||
|
||||
def filter_exo_by_tags(exos: List[tuple[Exercice, str]], tags: List[Tag]):
|
||||
valid_exos = [exo for exo, tag in exos if all(
|
||||
str(t) in tag.split(',') for t in tags)]
|
||||
return valid_exos
|
||||
|
||||
|
||||
def queryFilters_dependency(search: str = "", tags: List[int] | None = Depends(get_tags_dependency)):
|
||||
return search, tags
|
||||
|
||||
|
||||
@router.post('/exercices', response_model=ExerciceRead, 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)):
|
||||
file_obj = file.file._file
|
||||
file_obj.name = file.filename
|
||||
exo_obj = create_exo_db(exercice=exercice, user=user,
|
||||
exo_source=file_obj, db=db)
|
||||
return serialize_exo(exo=exo_obj, user_id=user.id, db=db)
|
||||
|
||||
|
||||
@router.post('/clone/{id_code}', response_model=ExerciceRead)
|
||||
def clone_exo(exercice: Exercice | None = Depends(get_exo_dependency), 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"})
|
||||
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=List[ExerciceRead])
|
||||
def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tuple[str, List[int] | None] = Depends(queryFilters_dependency), db: Session = Depends(get_session)):
|
||||
search, tags = queryFilters
|
||||
|
||||
if tags is not None and len(tags) != 0:
|
||||
statement = select(Exercice, func.group_concat(
|
||||
ExercicesTagLink.tag_id))
|
||||
else:
|
||||
statement = select(Exercice)
|
||||
|
||||
statement = statement.where(Exercice.author_id == user.id)
|
||||
statement = statement.where(Exercice.name.startswith(search))
|
||||
|
||||
if tags is not None and len(tags) != 0:
|
||||
statement = statement.join(ExercicesTagLink).where(
|
||||
col(ExercicesTagLink.tag_id).in_(tags)).group_by(ExercicesTagLink.exercice_id)
|
||||
|
||||
exercices = db.exec(statement).all()
|
||||
if tags is not None and len(tags) != 0:
|
||||
exercices = filter_exo_by_tags(exercices, tags)
|
||||
|
||||
exercices = [
|
||||
serialize_exo(exo=e, user_id=user.id, db= db) for e in exercices]
|
||||
return exercices
|
||||
|
||||
|
||||
@router.get('/exercices/public', response_model=List[ExerciceRead])
|
||||
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 = queryFilters
|
||||
|
||||
if user is not None:
|
||||
if tags is not None and len(tags) != 0:
|
||||
statement = select(Exercice, func.group_concat(
|
||||
ExercicesTagLink.tag_id))
|
||||
else:
|
||||
statement = select(Exercice)
|
||||
|
||||
statement = statement.where(Exercice.author_id != user.id)
|
||||
statement = statement.where(Exercice.private == False)
|
||||
statement = statement.where(Exercice.name.startswith(search))
|
||||
|
||||
if tags is not None and len(tags) != 0:
|
||||
statement = statement.join(ExercicesTagLink).where(
|
||||
col(ExercicesTagLink.tag_id).in_(tags)).group_by(ExercicesTagLink.exercice_id)
|
||||
|
||||
exercices = db.exec(statement).all()
|
||||
|
||||
if tags is not None and len(tags) != 0:
|
||||
exercices = filter_exo_by_tags(exercices, tags)
|
||||
|
||||
exercices = [
|
||||
serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices]
|
||||
return exercices
|
||||
else:
|
||||
statement = select(Exercice)
|
||||
statement = statement.where(Exercice.private == False)
|
||||
statement = statement.where(Exercice.name.startswith(search))
|
||||
exercices = db.exec(statement).all()
|
||||
return [serialize_exo(exo=e, user_id=None, db=db) for e in exercices]
|
||||
|
||||
|
||||
@router.get('/exercice/{id_code}', response_model=ExerciceRead)
|
||||
def get_exercice(exo: Exercice = Depends(get_exo_dependency), 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=ExerciceRead)
|
||||
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')
|
||||
if exo == False:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Cet exercice ne vous appartient pas')
|
||||
|
||||
file_obj = None
|
||||
if file:
|
||||
file_obj = file.file._file
|
||||
file_obj.name = file.filename
|
||||
exo_obj = update_exo_db(exo, exercice, file_obj, db)
|
||||
return serialize_exo(exo=exo_obj, user_id=exo_obj.author_id, db=db)
|
||||
|
||||
|
||||
@router.delete('/exercice/{id_code}')
|
||||
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")
|
||||
if exercice == False:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Cet exercice ne vous appartient pas')
|
||||
delete_exo_db(exercice, db)
|
||||
return {'detail': 'Exercice supprimé avec succès'}
|
||||
|
||||
|
||||
@router.post('/exercice/{id_code}/tags', response_model=ExerciceRead, 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)):
|
||||
if exo is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable')
|
||||
exo_obj = add_tags_db(exo, tags, user, db)
|
||||
return serialize_exo(exo=exo_obj, user_id=user.id, db=db)
|
||||
|
||||
|
||||
@router.delete('/exercice/{id_code}/tags/{tag_id}', response_model=ExerciceRead, 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)):
|
||||
if exo is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail='Exercice introuvable')
|
||||
if tag is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail='Tag introuvable')
|
||||
if tag == False:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Vous n'êtes pas le propriétaire du tag")
|
||||
|
||||
exo_obj = remove_tag_db(exo, tag, db)
|
||||
return serialize_exo(exo=exo_obj, user_id=tag.author_id, db=db)
|
||||
|
||||
|
||||
@router.get('/exercice/{id_code}/exo_source')
|
||||
async def get_exo_source(exo: Exercice = Depends(check_exercice_author), db: Session = Depends(get_session)):
|
||||
if exo is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail='Exercice introuvable')
|
||||
if exo == False:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
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})
|
||||
|
||||
|
||||
@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
|
247
backend/api/routes/exercices/routes_old.py
Normal file
247
backend/api/routes/exercices/routes_old.py
Normal file
@ -0,0 +1,247 @@
|
||||
from pydantic import BaseModel
|
||||
import csv
|
||||
import io
|
||||
from typing import List
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from fastapi import APIRouter, Depends, UploadFile, status, Query
|
||||
from services.database import get_exo
|
||||
from services.io import add_fast_api_root, get_filename_from_path
|
||||
from database.exercices.crud import add_tag_db, clone_exo_db, create_exo_db, delete_exo_db, get_exo_source_path, update_exo_db
|
||||
from services.exoValidation import validate_file
|
||||
from database.auth.models import User
|
||||
from services.auth import check_author_exo, get_current_user, get_current_user_optional
|
||||
from database.exercices.models import Exercice, Tag
|
||||
from fastapi_jwt_auth import AuthJWT
|
||||
|
||||
from schemas.exercices import ExerciceIn_form, ExerciceSchema, Exercices_schema, Exercice_schema, Exercices_withoutTags, Tag_schema, TagFull_schema, TagIn
|
||||
from fastapi_pagination import paginate, Page
|
||||
|
||||
from generateur.generateur_csv import Csv_generator
|
||||
|
||||
router = APIRouter(tags=['exercices'])
|
||||
|
||||
|
||||
|
||||
def get_exercice_data(exo: Exercice, user: User | None = None):
|
||||
return {}
|
||||
|
||||
# Exercices
|
||||
|
||||
|
||||
class Exo(BaseModel):
|
||||
name: str
|
||||
id_code: str
|
||||
tags: List[TagFull_schema]
|
||||
|
||||
async def get_tags(tags: List[str] | None=Query(None)):
|
||||
if tags is None:
|
||||
return None
|
||||
validated_tags = []
|
||||
for t in tags:
|
||||
tag = await Tag.get_or_none(id_code=t)
|
||||
if tag is not None:
|
||||
validated_tags.append(tag)
|
||||
return validated_tags
|
||||
|
||||
|
||||
async def get_exo_by_tags(exos: Query, tags: List[Tag]):
|
||||
valid_exos = []
|
||||
for e in exos:
|
||||
exo_tags = await e.tags.all()
|
||||
if (all(t in exo_tags for t in tags)):
|
||||
valid_exos.append(e)
|
||||
return valid_exos
|
||||
|
||||
@router.get("/exercices", response_model=Page[Exo])
|
||||
async def get_exercices(search: str = "", tags: List[Tag] | None = Depends(get_tags)):
|
||||
exos = Exercice.all()
|
||||
print(Exo.schema_json(indent=4))
|
||||
print(Exercice_schema.schema_json(indent=4))
|
||||
if tags != None:
|
||||
exos = await exos.filter(tags__id_code__in=[t.id_code for t in tags]).distinct()
|
||||
exos = await get_exo_by_tags(exos, tags)
|
||||
print(exos, [e.id_code for e in exos])
|
||||
exos = Exercice.filter(id_code__in = [e.id_code for e in exos])
|
||||
print( await exos)
|
||||
exos = exos.filter(name__icontains= search)
|
||||
exo_list = await Exercices_schema.from_queryset(exos)
|
||||
return paginate(exo_list)
|
||||
|
||||
async def get_exo_with_user_tags(exo: Exercice, user: User) -> Exercices_schema:
|
||||
exo_data = await Exercice_schema.from_tortoise_orm(exo)
|
||||
|
||||
exo_tags = await exo.tags.all()
|
||||
exo_data = {**exo_data.dict(), 'tags': await TagFull_schema.from_queryset(exo.tags.filter(owner_id=user.id))}
|
||||
|
||||
return exo_data
|
||||
|
||||
|
||||
|
||||
@router.get('/exercices/user', response_model=Page[Exercices_schema])
|
||||
async def get_user_exercices(user: User = Depends(get_current_user), search: str = "", tags: List[Tag] | None = Depends(get_tags)):
|
||||
|
||||
exos = Exercice.filter(author_id=user.id)
|
||||
print('tatgs', tags)
|
||||
if tags != None:
|
||||
print('lolilol')
|
||||
exos = await exos.filter(tags__id_code__in=[t.id_code for t in tags]).distinct()
|
||||
exos = await get_exo_by_tags(exos, tags)
|
||||
exos = Exercice.filter(id_code__in=[e.id_code for e in exos])
|
||||
exos = await exos.filter(name__icontains=search)
|
||||
|
||||
exo_list = [await get_exo_with_user_tags(e, user) for e in exos]
|
||||
print(len(exo_list))
|
||||
return paginate(exo_list)
|
||||
|
||||
|
||||
def exclude_dict_key(dict, key):
|
||||
return {k: v for k, v in dict.items() if k != key}
|
||||
|
||||
@router.get('/exercices/public', response_model=Page[Exercices_schema | Exercices_withoutTags])
|
||||
async def get_public_exercices(user: User | None = Depends(get_current_user_optional), search: str = "", tags: List[Tag] | None = Depends(get_tags)):
|
||||
is_authenticated = user != None
|
||||
if is_authenticated:
|
||||
exos = Exercice.filter(author_id__not=user.id, private = False, isOriginal=True)
|
||||
if tags != None:
|
||||
exos = await exos.filter(tags__id_code__in=[t.id_code for t in tags]).distinct()
|
||||
exos = await get_exo_by_tags(exos, tags)
|
||||
exos = Exercice.filter(id_code__in=[e.id_code for e in exos])
|
||||
else:
|
||||
exos = Exercice.filter(private=False, isOriginal=True)
|
||||
|
||||
exos = await exos.filter(name__icontains=search)
|
||||
|
||||
if is_authenticated:
|
||||
exo_list = [await get_exo_with_user_tags(e, user) for e in exos]
|
||||
else:
|
||||
exo_list = await Exercices_withoutTags.from_queryset(Exercice.all())
|
||||
|
||||
return paginate(exo_list)
|
||||
|
||||
async def get_exo_tags(exo: Exercice, user: User) -> Exercice_schema:
|
||||
exo_data = await Exercice_schema.from_tortoise_orm(exo)
|
||||
|
||||
exo_tags = await exo.tags.all()
|
||||
exo_data = {**exo_data.dict(), 'tags': await TagFull_schema.from_queryset(exo.tags.filter(owner_id=user.id))}
|
||||
|
||||
return exo_data
|
||||
|
||||
@router.get('/exercice/{id_code}', response_model=ExerciceSchema)
|
||||
async def get_exercice(id_code: str, user: User = Depends(get_current_user_optional)):
|
||||
is_authenticated = user != None
|
||||
print(TagFull_schema.schema_json(indent=4), '\n\n', ExerciceSchema.schema_json(indent=4))
|
||||
exo = await Exercice.get_or_none(id_code=id_code)
|
||||
if exo is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="exercice not found")
|
||||
|
||||
if is_authenticated:
|
||||
|
||||
author = await exo.author
|
||||
is_author = author.id == user.id
|
||||
|
||||
exo_obj = await Exercice_schema.from_tortoise_orm(exo)
|
||||
exo_obj = await get_exo_tags(exo, user)
|
||||
print(exo_obj)
|
||||
exo_dict = exo_obj
|
||||
exo_dict['is_author'] = is_author
|
||||
exo_dict['exo_source_name'] = get_filename_from_path(exo.exo_source)
|
||||
|
||||
return exo_dict
|
||||
|
||||
exo_obj = await Exercice_schema.from_tortoise_orm(exo)
|
||||
exo_dict = exo_obj.dict()
|
||||
exo_dict['is_author'] = False
|
||||
exo_dict['exo_source_name'] = get_filename_from_path(exo.exo_source)
|
||||
exo_dict['tags'] = []
|
||||
return exo_dict
|
||||
|
||||
|
||||
@router.post("/exercices", response_model=Exercice_schema)
|
||||
async def create_exercice(file: UploadFile = Depends(validate_file), exo: ExerciceIn_form = Depends(ExerciceIn_form.as_form), user: User = Depends(get_current_user)):
|
||||
file_obj = file.file._file
|
||||
file_obj.name = file.filename
|
||||
exo_obj = await create_exo_db(**{**exo.dict(exclude_unset=True)}, exo_source=file_obj, author_id=user.id)
|
||||
return await Exercice_schema.from_tortoise_orm(exo_obj)
|
||||
|
||||
|
||||
@router.delete("/exercice/{id_code}")
|
||||
async def delete_exercice(id_code: str, author: User = Depends(check_author_exo)):
|
||||
await delete_exo_db(id_code)
|
||||
return {'detail': "Exercice successfully deleted"}
|
||||
|
||||
|
||||
|
||||
@router.put("/exercice/{id_code}", response_model=Exercice_schema)
|
||||
async def update_exercice(id_code: str, file: UploadFile = None, exo: ExerciceIn_form = Depends(ExerciceIn_form.as_form), author: User = Depends(check_author_exo)):
|
||||
file_obj = None
|
||||
if file != None:
|
||||
file_obj = file.file._file
|
||||
file_obj.name = file.filename
|
||||
exo_obj = await update_exo_db(id_code, exo_source=file_obj, ** {**exo.dict(exclude_unset=True)})
|
||||
return await Exercice_schema.from_tortoise_orm(exo_obj)
|
||||
|
||||
|
||||
@router.post('/exercices/{id_code}/clone', response_model=Exercice_schema)
|
||||
async def clone_exercice(id_code: str, user: User = Depends(get_current_user)):
|
||||
exo_obj = await clone_exo_db(id_code, user.id)
|
||||
return await Exercice_schema.from_tortoise_orm(exo_obj)
|
||||
|
||||
|
||||
@router.get('/exercices/{id_code}/exo_source')
|
||||
async def get_exo_source(id_code: str, author: User = Depends(check_author_exo)):
|
||||
path = await get_exo_source_path(id_code)
|
||||
filename = get_filename_from_path(path)
|
||||
return FileResponse(path, headers={'content-disposition': 'attachment;filename='+filename})
|
||||
|
||||
|
||||
# Tags
|
||||
@router.get('/tags')
|
||||
async def get_tags(user: User = Depends(get_current_user)):
|
||||
return await Tag_schema.from_queryset(user.tags.all())
|
||||
|
||||
|
||||
|
||||
@router.post('/tags/{id_code}')
|
||||
async def add_tags(id_code: str, tags: List[TagIn], user: User = Depends(get_current_user)):
|
||||
exo = await Exercice.get(id_code = id_code)
|
||||
exercice = await add_tag_db(exo, tags, user.id)
|
||||
|
||||
return {'exo': await get_exo_with_user_tags(exo, user), 'tags': await TagFull_schema.from_queryset(user.tags.all())}
|
||||
|
||||
|
||||
async def check_tag_owner(tag_id: str, user: User):
|
||||
tag = await Tag.get(id_code=tag_id)
|
||||
owner = await tag.owner
|
||||
if owner.id != user.id:
|
||||
raise HTTPException(status_code = status.HTTP_401_UNAUTHORIZED, detail="Vous n'êtes pas le créateur du tag")
|
||||
return tag
|
||||
|
||||
@router.delete('/tags/{id_code}/{tag_id}')
|
||||
async def delete_tag(id_code:str,tag_id: str, user: User = Depends(get_current_user)):
|
||||
tag = await check_tag_owner(tag_id, user)
|
||||
exo = await Exercice.get(id_code=id_code)
|
||||
await exo.tags.remove(tag)
|
||||
return await get_exo_with_user_tags(exo, user)
|
||||
|
||||
|
||||
# Generation
|
||||
|
||||
@router.get('/generator/csv/{exo_id}')
|
||||
async def generate_csv(exo_id: str, filename: str):
|
||||
exo = await Exercice.get(id_code=exo_id)
|
||||
|
||||
if exo.csvSupport == False:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail='Impossible de générer cet exercice sur le support csv')
|
||||
|
||||
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')
|
7
backend/api/routes/room/routes.py
Normal file
7
backend/api/routes/room/routes.py
Normal file
@ -0,0 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(tags=["room"])
|
||||
|
||||
@router.post('/room')
|
||||
def create_room():
|
||||
return
|
24
backend/api/routes/rooms/routes.py
Normal file
24
backend/api/routes/rooms/routes.py
Normal file
@ -0,0 +1,24 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from services.auth import get_current_user_optional
|
||||
from database.rooms.crud import create_room_with_user_db, create_room_anonymous_db, get_room_db
|
||||
from schemas.rooms import AnonymousIn_schema, RoomIn_schema, Room_schema
|
||||
from database.auth.models import User
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post('/rooms', response_model=Room_schema)
|
||||
async def create_room(roomData: RoomIn_schema, anonymous: AnonymousIn_schema = None, user: User = Depends(get_current_user_optional)):
|
||||
if user is not None:
|
||||
room = await create_room_with_user_db(room=roomData, user=user)
|
||||
return await Room_schema.from_tortoise_orm(room)
|
||||
else:
|
||||
room = await create_room_anonymous_db(room=roomData, anonymous=anonymous)
|
||||
return await Room_schema.from_tortoise_orm(room)
|
||||
|
||||
|
||||
@router.get('/room/{room_id}')
|
||||
async def get_room(room_id: str):
|
||||
room = await get_room_db(room_id)
|
||||
if room is None:
|
||||
return None
|
||||
return await Room_schema.from_tortoise_orm(room)
|
7
backend/api/schemas/base.py
Normal file
7
backend/api/schemas/base.py
Normal file
@ -0,0 +1,7 @@
|
||||
from tortoise import Tortoise
|
||||
|
||||
|
||||
Tortoise.init_models(['database.exercices.models',
|
||||
'database.auth.models'], "models") # "database.room.models"
|
||||
|
||||
from schemas import exercices, rooms, users
|
33
backend/api/schemas/exercices.py
Normal file
33
backend/api/schemas/exercices.py
Normal file
@ -0,0 +1,33 @@
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
from services.schema import as_form
|
||||
from database.exercices.models import Exercice, Tag
|
||||
from pydantic import BaseModel
|
||||
|
||||
Exercice_schema = pydantic_model_creator(Exercice, name="Exercice", exclude=['tags.owner','author.clientId', 'created_at', 'updated_at'])
|
||||
|
||||
ExerciceIn_schema = pydantic_model_creator(
|
||||
Exercice, name="ExerciceIn", exclude_readonly=True, exclude=['id_code', 'exo_source', "tags_id", 'author_id', 'origin'])
|
||||
|
||||
|
||||
Exercices_schema = pydantic_model_creator(Exercice, name="exercices", include=[
|
||||
'name', 'id_code', 'tags'], exclude=['tags.owner'])
|
||||
|
||||
Exercices_withoutTags = pydantic_model_creator(Exercice, name = 'exercices_wTags', include=['name', 'id_code'])
|
||||
@as_form
|
||||
class ExerciceIn_form(ExerciceIn_schema):
|
||||
pass
|
||||
|
||||
class ExerciceSchema(Exercice_schema):
|
||||
is_author: bool
|
||||
exo_source_name: str
|
||||
|
||||
|
||||
Tag_schema = pydantic_model_creator(Tag, name="tag", include=['label', "id_code", "color"])
|
||||
TagFull_schema = pydantic_model_creator(Tag, name='tagFull', exclude=['owner', 'exercices'])
|
||||
TagIn_schema = pydantic_model_creator(
|
||||
Tag, name="tagIn", exclude_readonly=True, include=['label', 'id_code', 'color'])
|
||||
|
||||
class TagIn(BaseModel):
|
||||
label: str
|
||||
id_code: str
|
||||
color: str
|
17
backend/api/schemas/rooms.py
Normal file
17
backend/api/schemas/rooms.py
Normal file
@ -0,0 +1,17 @@
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
from database.rooms.models import Room, AnonymousMember, Parcours
|
||||
Room_schema = pydantic_model_creator(
|
||||
Room, name='room', include=["id", 'name', 'id_code'])
|
||||
|
||||
RoomIn_schema = pydantic_model_creator(Room, name='roomIn', exclude_readonly=True, exclude=[
|
||||
'created_at', 'online', 'id_code', 'users_waiters'])
|
||||
|
||||
|
||||
Anonymous_schema = pydantic_model_creator(
|
||||
AnonymousMember, name='anonymousMember')
|
||||
AnonymousIn_schema = pydantic_model_creator(
|
||||
AnonymousMember, name='anonymousMemberIn', exclude_readonly=True, exclude=['id_code', 'room_id'])
|
||||
|
||||
Parcours_schema = pydantic_model_creator(Parcours, name='parcours')
|
||||
ParcoursIn_schema = pydantic_model_creator(
|
||||
Parcours, name='parcoursIn', exclude_readonly=True)
|
58
backend/api/schemas/users.py
Normal file
58
backend/api/schemas/users.py
Normal file
@ -0,0 +1,58 @@
|
||||
from typing import Optional
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
from services.schema import as_form
|
||||
from database.auth.models import User
|
||||
from pydantic import BaseModel, validator
|
||||
from services.password import validate_password
|
||||
User_schema = pydantic_model_creator(User, name='users', include=[
|
||||
'username', 'email', "name", "firstname"])
|
||||
UserIn_schema = pydantic_model_creator(
|
||||
User, name='usersIn', exclude_readonly=True)
|
||||
|
||||
|
||||
@as_form
|
||||
class UserForm(BaseModel):
|
||||
username: str
|
||||
firstname: Optional[str]
|
||||
name: Optional[str]
|
||||
email: Optional[str]
|
||||
|
||||
|
||||
@as_form
|
||||
class UserIn_Form(UserIn_schema):
|
||||
pass
|
||||
|
||||
|
||||
@as_form
|
||||
class UserLog(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
@as_form
|
||||
class UserRegister(UserLog):
|
||||
password_confirm: str
|
||||
|
||||
@validator('username')
|
||||
def username_alphanumeric(cls, v):
|
||||
assert v.isalnum(), 'must be alphanumeric'
|
||||
return v
|
||||
|
||||
@validator('password')
|
||||
def password_validation(cls, v):
|
||||
is_valid = validate_password(v)
|
||||
if is_valid != True:
|
||||
raise ValueError(is_valid)
|
||||
return v
|
||||
|
||||
@validator('password_confirm')
|
||||
def password_match(cls, v, values):
|
||||
if 'password' in values and v != values['password']:
|
||||
raise ValueError('Les mots de passe ne correspondent pas')
|
||||
return v
|
||||
|
||||
|
||||
@as_form
|
||||
class PasswordSet(BaseModel):
|
||||
password: str
|
||||
password_confirm: str
|
63
backend/api/services/auth.py
Normal file
63
backend/api/services/auth.py
Normal file
@ -0,0 +1,63 @@
|
||||
from services.password import verify_password
|
||||
from fastapi_jwt_auth import AuthJWT
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from database.auth.crud import get_user_from_clientId_db, get_user_from_username_db
|
||||
from sqlmodel import Session
|
||||
from database.db import get_session
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
|
||||
bearer = OAuth2PasswordBearer(tokenUrl='/login')
|
||||
|
||||
|
||||
def authenticate_user(user: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_session)):
|
||||
user_db = get_user_from_username_db(user.username, db)
|
||||
if not user_db:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"username_error": "Utilisateur introuvable"},
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
if not verify_password(user.password, user_db.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"password_error": "Mot de passe invalide"},
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user_db
|
||||
|
||||
def jwt_required(Authorize: AuthJWT = Depends(), token: str = Depends(bearer)):
|
||||
Authorize.jwt_required()
|
||||
return Authorize
|
||||
|
||||
|
||||
def jwt_optional(Authorize: AuthJWT = Depends()):
|
||||
Authorize.jwt_optional()
|
||||
return Authorize
|
||||
|
||||
|
||||
def jwt_refresh_required(Authorize: AuthJWT = Depends(), token: str = Depends(bearer)):
|
||||
Authorize.jwt_refresh_token_required()
|
||||
return Authorize
|
||||
|
||||
|
||||
def fresh_jwt_required(Authorize: AuthJWT = Depends(), token: str = Depends(bearer)):
|
||||
Authorize.fresh_jwt_required()
|
||||
return Authorize
|
||||
|
||||
def get_current_clientId(Authorize: AuthJWT = Depends(jwt_required)):
|
||||
return Authorize.get_jwt_subject()
|
||||
|
||||
|
||||
def get_current_user(clientId: str = Depends(get_current_clientId), db: Session = Depends(get_session)):
|
||||
user = get_user_from_clientId_db(clientId, db)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail='Utilisateur introuvable')
|
||||
return user
|
||||
|
||||
def get_current_user_optional(Authorize: AuthJWT = Depends(jwt_optional), db: Session = Depends(get_session)):
|
||||
clientId = Authorize.get_jwt_subject()
|
||||
if clientId:
|
||||
return get_user_from_clientId_db(clientId, db)
|
||||
return None
|
||||
|
15
backend/api/services/database.py
Normal file
15
backend/api/services/database.py
Normal file
@ -0,0 +1,15 @@
|
||||
import random
|
||||
import string
|
||||
from sqlmodel import select, Session
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
|
||||
def generate_unique_code(model: SQLModel, s: Session, length: int = 6):
|
||||
while True:
|
||||
code = ''.join(random.choices(string.ascii_uppercase, k=length))
|
||||
is_unique = s.exec(select(model).where(
|
||||
model.id_code == code)).first() == None
|
||||
if is_unique:
|
||||
break
|
||||
return code
|
||||
|
101
backend/api/services/exoValidation.py
Normal file
101
backend/api/services/exoValidation.py
Normal file
@ -0,0 +1,101 @@
|
||||
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi import UploadFile, status
|
||||
from tortoise.validators import Validator
|
||||
from tortoise.exceptions import ValidationError
|
||||
import types
|
||||
from services.timeout import timeout
|
||||
from services.io import is_binary_file
|
||||
import os
|
||||
|
||||
def checkExoSupportCompatibility(obj):
|
||||
isPdf = False if (obj['pdf'] == None or (
|
||||
obj['calcul'] == False and obj['pdf'] == False)) else True
|
||||
|
||||
isCsv = False if (obj['csv'] == None or (
|
||||
obj['calcul'] == False and obj['csv'] == False)) else True
|
||||
|
||||
isWeb = False if (obj['web'] == None or (
|
||||
obj['calcul'] == False and obj['web'] == False)) else True
|
||||
|
||||
return {
|
||||
'pdf': isPdf, 'csv': isCsv, 'web': isWeb}
|
||||
|
||||
|
||||
def get_module_from_string(value: str, *args, **kwargs):
|
||||
locs = {}
|
||||
try:
|
||||
exec(value, dict(), locs)
|
||||
except Exception as err:
|
||||
raise ValueError(err)
|
||||
return locs
|
||||
|
||||
|
||||
def execute_main_if_avalaible(spec, *args, **kwargs):
|
||||
try:
|
||||
return spec["main"]()
|
||||
except KeyError as atrerror:
|
||||
raise ValueError(f"Fonction 'main' introuvable")
|
||||
except Exception as e:
|
||||
raise ValueError(f'[Error] : {e}')
|
||||
|
||||
|
||||
def get_spec_with_timeout(data, time):
|
||||
return get_module_from_string(data)
|
||||
with timeout(time, ValidationError('[Error] : Script took too long')):
|
||||
return get_module_from_string(data)
|
||||
|
||||
|
||||
def fill_empty_values(object):
|
||||
default_object = {"calcul": False, 'pdf': False, 'csv': False,
|
||||
'web': False, 'correction': False}
|
||||
return {**default_object, **object}
|
||||
|
||||
|
||||
def get_support_from_data(data: str):
|
||||
|
||||
locs = get_spec_with_timeout(data, 5)
|
||||
result = execute_main_if_avalaible(locs)
|
||||
result = fill_empty_values(result)
|
||||
|
||||
exo_supports_compatibility = checkExoSupportCompatibility(result)
|
||||
return exo_supports_compatibility
|
||||
|
||||
|
||||
def get_support_from_path(path: str):
|
||||
if not os.path.exists(path):
|
||||
raise ValidationError('[Error] : No such file or directory')
|
||||
is_binary = is_binary_file(path)
|
||||
|
||||
if is_binary:
|
||||
mode = 'rb'
|
||||
else:
|
||||
mode = 'r'
|
||||
|
||||
with open(path, mode) as f:
|
||||
data = f.read() if mode == "r" else f.read().decode('utf8')
|
||||
return get_support_from_data(data)
|
||||
|
||||
|
||||
async def validate_file(file: UploadFile):
|
||||
data = await file.read()
|
||||
try:
|
||||
exo_supports_compatibility = get_support_from_data(
|
||||
data)
|
||||
if not exo_supports_compatibility['pdf'] and not exo_supports_compatibility['csv'] and not exo_supports_compatibility['web']:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail={"exo_source_error":
|
||||
'Exercice non valide (compatible avec aucun support)'})
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail={
|
||||
"exo_source_error": e.args[0]})
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
await file.seek(0)
|
||||
return file
|
||||
|
||||
async def validate_file_optionnal(file: UploadFile = None):
|
||||
if not file:
|
||||
return None
|
||||
return await validate_file(file)
|
||||
|
||||
|
62
backend/api/services/io.py
Normal file
62
backend/api/services/io.py
Normal file
@ -0,0 +1,62 @@
|
||||
|
||||
import os
|
||||
import typing
|
||||
import uuid
|
||||
|
||||
TEXTCHARS = bytearray({7, 8, 9, 10, 12, 13, 27} |
|
||||
set(range(0x20, 0x100)) - {0x7f})
|
||||
|
||||
|
||||
def delete_root_slash(path: str) -> str:
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
return path
|
||||
|
||||
|
||||
def add_fast_api_root(path):
|
||||
fast_api_root = os.environ.get('FASTAPI_ROOT_URL')
|
||||
if path.startswith(fast_api_root):
|
||||
return path
|
||||
return os.path.join(os.environ.get('FASTAPI_ROOT_URL'), delete_root_slash(path))
|
||||
|
||||
def remove_fastapi_root(path):
|
||||
return path.replace(os.environ.get('FASTAPI_ROOT_URL'), "")
|
||||
|
||||
def is_binary_file(file_path: str):
|
||||
with open(file_path, 'rb') as f:
|
||||
content = f.read(1024)
|
||||
return bool(content.translate(None, TEXTCHARS))
|
||||
|
||||
|
||||
def get_or_create_dir(path: str) -> str:
|
||||
if not os.path.exists(path):
|
||||
os.mkdir(path)
|
||||
return path
|
||||
|
||||
|
||||
def get_filename(file: typing.IO, default: str = uuid.uuid4()) -> str:
|
||||
if hasattr(file, 'name'):
|
||||
return file.name
|
||||
elif hasattr(file, 'filename'):
|
||||
return file.filename
|
||||
else:
|
||||
return f"{default}.py"
|
||||
|
||||
|
||||
def remove_if_exists(path):
|
||||
if os.path.exists(path) and os.path.isfile(path):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def get_parent_dir(path):
|
||||
return os.path.abspath(os.path.join(path, os.pardir))
|
||||
|
||||
|
||||
def get_ancestor(path: str, levels: int = 1):
|
||||
for i in range(levels):
|
||||
path = get_parent_dir(path)
|
||||
return path
|
||||
|
||||
|
||||
def get_filename_from_path(path):
|
||||
return os.path.split(path)[-1]
|
15
backend/api/services/jwt.py
Normal file
15
backend/api/services/jwt.py
Normal file
@ -0,0 +1,15 @@
|
||||
import config
|
||||
|
||||
def revoke_access(decrypted_token):
|
||||
if decrypted_token['type'] == 'access':
|
||||
jti= decrypted_token['jti']
|
||||
config.redis_conn.setex(jti, config.settings.access_expires, 'true')
|
||||
return {"detail": "Access token has been revoke"}
|
||||
raise "Access token required"
|
||||
|
||||
def revoke_refresh(decrypted_token):
|
||||
if decrypted_token['type'] == 'refresh':
|
||||
jti= decrypted_token['jti']
|
||||
config.redis_conn.setex(jti, config.settings.refresh_expires, 'true')
|
||||
return {"detail": "Refresh token has been revoke"}
|
||||
raise "Refresh token required"
|
23
backend/api/services/password.py
Normal file
23
backend/api/services/password.py
Normal file
@ -0,0 +1,23 @@
|
||||
import re
|
||||
from passlib.context import CryptContext
|
||||
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def validate_password(password):
|
||||
if len(password) < 8:
|
||||
return "Le mot de passe est trop court (8 caractères minimum)"
|
||||
elif re.search('[0-9]', password) is None:
|
||||
return 'Le mot de passe doit contenir au moins un chiffre'
|
||||
elif re.search('[A-Z]', password) is None:
|
||||
return "Le mot de passe doit contenir au moins une majuscule"
|
||||
return True
|
32
backend/api/services/schema.py
Normal file
32
backend/api/services/schema.py
Normal file
@ -0,0 +1,32 @@
|
||||
import inspect
|
||||
from typing import Type
|
||||
|
||||
from fastapi import Form
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import ModelField
|
||||
|
||||
|
||||
def as_form(cls: Type[BaseModel]):
|
||||
new_parameters = []
|
||||
|
||||
for field_name, model_field in cls.__fields__.items():
|
||||
model_field: ModelField # type: ignore
|
||||
|
||||
new_parameters.append(
|
||||
inspect.Parameter(
|
||||
model_field.alias,
|
||||
inspect.Parameter.POSITIONAL_ONLY,
|
||||
default=Form(...) if model_field.required else Form(
|
||||
model_field.default),
|
||||
annotation=model_field.outer_type_,
|
||||
)
|
||||
)
|
||||
|
||||
async def as_form_func(**data):
|
||||
return cls(**data)
|
||||
|
||||
sig = inspect.signature(as_form_func)
|
||||
sig = sig.replace(parameters=new_parameters)
|
||||
as_form_func.__signature__ = sig # type: ignore
|
||||
setattr(cls, 'as_form', as_form_func)
|
||||
return cls
|
21
backend/api/services/timeout.py
Normal file
21
backend/api/services/timeout.py
Normal file
@ -0,0 +1,21 @@
|
||||
from contextlib import contextmanager
|
||||
import signal
|
||||
def raise_timeout(signum, frame):
|
||||
raise TimeoutError
|
||||
|
||||
@contextmanager
|
||||
def timeout(time:int, exception = TimeoutError):
|
||||
# Register a function to raise a TimeoutError on the signal.
|
||||
signal.signal(signal.SIGALRM, raise_timeout)
|
||||
# Schedule the signal to be sent after ``time``.
|
||||
signal.alarm(time)
|
||||
|
||||
try:
|
||||
yield
|
||||
except TimeoutError:
|
||||
print('TIMED OUT')
|
||||
raise exception
|
||||
finally:
|
||||
# Unregister the signal so it won't be triggered
|
||||
# if the timeout is not reached.
|
||||
signal.signal(signal.SIGALRM, signal.SIG_IGN)
|
108
backend/api/testing.py
Normal file
108
backend/api/testing.py
Normal file
@ -0,0 +1,108 @@
|
||||
import uuid
|
||||
from pydantic import validator
|
||||
import io
|
||||
import os
|
||||
from fastapi import UploadFile, Form
|
||||
from pathlib import Path
|
||||
from typing import IO, Any, List, Optional, Type
|
||||
|
||||
from fastapi import FastAPI
|
||||
from sqlmodel import Field, Session, SQLModel, create_engine, select
|
||||
from services.exoValidation import get_support_from_data
|
||||
|
||||
from services.io import add_fast_api_root, get_filename, get_or_create_dir, remove_fastapi_root
|
||||
|
||||
|
||||
class FileFieldMeta(type):
|
||||
def __getitem__(self, upload_root: str,) -> Type['FileField']:
|
||||
return type('MyFieldValue', (FileField,), {'upload_root': upload_root})
|
||||
|
||||
|
||||
class FileField(str, metaclass=FileFieldMeta):
|
||||
upload_root: str
|
||||
@classmethod
|
||||
def __get_validators__(cls):
|
||||
yield cls.validate
|
||||
|
||||
@classmethod
|
||||
def validate(cls, value: str | IO, values):
|
||||
print(cls.upload_root, cls.default_naming_field)
|
||||
upload_root = get_or_create_dir(
|
||||
add_fast_api_root(cls.upload_root))
|
||||
|
||||
if not isinstance(value, str):
|
||||
value.seek(0)
|
||||
|
||||
is_binary = isinstance(value, io.BytesIO)
|
||||
|
||||
name = get_filename(value, 'exo_source.py')
|
||||
|
||||
parent = get_or_create_dir(os.path.join(
|
||||
upload_root, values['id_code']))
|
||||
|
||||
mode = 'w+' if not is_binary else 'wb+'
|
||||
|
||||
path = os.path.join(parent, name)
|
||||
with open(path, mode) as f:
|
||||
f.write(value.read())
|
||||
|
||||
return remove_fastapi_root(path)
|
||||
|
||||
else:
|
||||
if not os.path.exists(value):
|
||||
raise ValueError('File does not exist')
|
||||
return value
|
||||
|
||||
class Hero(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code : str
|
||||
path: FileField['/testing', 'id_code']
|
||||
|
||||
|
||||
|
||||
|
||||
class HeroCreate(SQLModel):
|
||||
path: FileField[42]
|
||||
|
||||
class HeroRead(SQLModel):
|
||||
id: int
|
||||
id_code: str
|
||||
path: str
|
||||
|
||||
|
||||
sqlite_file_name = "testing.db"
|
||||
sqlite_url = f"sqlite:///{sqlite_file_name}"
|
||||
|
||||
connect_args = {"check_same_thread": False}
|
||||
engine = create_engine(sqlite_url, echo=True, connect_args=connect_args)
|
||||
|
||||
|
||||
def create_db_and_tables():
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
create_db_and_tables()
|
||||
|
||||
|
||||
@app.get("/heroes/", response_model=List[HeroRead])
|
||||
def read_heroes():
|
||||
with Session(engine) as session:
|
||||
heroes = session.exec(select(Hero)).all()
|
||||
return heroes
|
||||
|
||||
@app.post("/heroes/", )
|
||||
def create_hero(file: UploadFile, name: str = Form()):
|
||||
with Session(engine) as session:
|
||||
file_obj = file.file._file
|
||||
db_hero = Hero(path=file_obj, id_code=name)
|
||||
session.add(db_hero)
|
||||
session.commit()
|
||||
session.refresh(db_hero)
|
||||
return "db_hero"
|
||||
|
||||
|
249
backend/api/tests/test_auth.py
Normal file
249
backend/api/tests/test_auth.py
Normal file
@ -0,0 +1,249 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
VALID_USERNAME = 'lilian'
|
||||
VALID_PASSWORD = 'Test12345'
|
||||
|
||||
def test_register(client: TestClient, username = VALID_USERNAME):
|
||||
print('usernae')
|
||||
r = client.post('/register', data={"username": username, 'password': VALID_PASSWORD, 'password_confirm': VALID_PASSWORD})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 200
|
||||
assert 'access_token' in data
|
||||
assert 'refresh_token' in data
|
||||
return {'access': data['access_token'], 'refresh': data['refresh_token']}
|
||||
|
||||
def test_register_username_too_long(client: TestClient):
|
||||
r = client.post('/register', data={"username": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
'password':VALID_PASSWORD, 'password_confirm':VALID_PASSWORD})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 422
|
||||
assert data['detail']['username_error'] == 'ensure this value has at most 20 characters'
|
||||
|
||||
def test_register_mdp_not_corresponding(client: TestClient):
|
||||
r = client.post('/register', data={"username": VALID_USERNAME,
|
||||
'password': "Test12345", 'password_confirm': 'Test1234'})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 422
|
||||
assert data['detail']['password_confirm_error'] == 'Les mots de passe ne correspondent pas'
|
||||
|
||||
def test_register_mdp_missing_number(client: TestClient):
|
||||
r = client.post('/register', data={"username": "lilian",
|
||||
'password': "Testttttt", 'password_confirm': 'Testttttt'})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 422
|
||||
assert data['detail']['password_error'] == 'Le mot de passe doit contenir au moins un chiffre'
|
||||
|
||||
def test_register_mdp_missing_maj(client: TestClient):
|
||||
r = client.post('/register', data={"username":VALID_USERNAME,
|
||||
'password': "testttttt1", 'password_confirm': 'testttttt1'})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 422
|
||||
assert data['detail']['password_error'] == 'Le mot de passe doit contenir au moins une majuscule'
|
||||
|
||||
def test_register_mdp_too_short(client: TestClient):
|
||||
r = client.post('/register', data={"username": VALID_USERNAME,
|
||||
'password': "t", 'password_confirm': 't'})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 422
|
||||
assert data['detail'][
|
||||
'password_error'] == 'Le mot de passe est trop court (8 caractères minimum)'
|
||||
|
||||
|
||||
def test_register_username_indisponible(client: TestClient):
|
||||
r = client.post('/register', data={"username": VALID_USERNAME,
|
||||
'password':VALID_PASSWORD, 'password_confirm':VALID_PASSWORD})
|
||||
rr = client.post('/register', data={"username": VALID_USERNAME,
|
||||
'password':VALID_PASSWORD, 'password_confirm':VALID_PASSWORD})
|
||||
data = rr.json()
|
||||
print(data)
|
||||
assert rr.status_code == 400
|
||||
assert data['detail'][
|
||||
'username_error'] == "Nom d'utilisateur indisponible"
|
||||
|
||||
def test_login(client: TestClient):
|
||||
test_register(client)
|
||||
|
||||
r = client.post('/login', data={"username": VALID_USERNAME, 'password': VALID_PASSWORD})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 200
|
||||
assert 'access_token' in data
|
||||
assert 'refresh_token' in data
|
||||
return data['refresh_token']
|
||||
|
||||
def test_login_invalid_password(client: TestClient):
|
||||
test_register(client)
|
||||
|
||||
r = client.post('/login', data={"username": VALID_USERNAME, 'password': 'Test1234'})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 401
|
||||
assert data['detail'][
|
||||
'password_error'] == "Mot de passe invalide"
|
||||
|
||||
def test_login_user_not_found(client: TestClient):
|
||||
r = client.post('/login', data={"username": VALID_USERNAME, 'password': VALID_PASSWORD})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 401
|
||||
assert data['detail'][
|
||||
'username_error'] == "Utilisateur introuvable"
|
||||
|
||||
|
||||
def test_check_token(client: TestClient):
|
||||
token = test_register(client)['access']
|
||||
|
||||
r = client.post(
|
||||
'/check-access', headers={'Authorization': 'Bearer ' + token})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 200
|
||||
assert data['username'] == 'lilian'
|
||||
|
||||
def test_refresh(client: TestClient):
|
||||
refresh = test_login(client)
|
||||
|
||||
r = client.post(
|
||||
'/refresh', headers={'Authorization': 'Bearer ' + refresh})
|
||||
data = r.json()
|
||||
assert r.status_code == 200
|
||||
assert 'access_token' in data
|
||||
|
||||
#TODO : token invalid
|
||||
|
||||
def test_update_user(client: TestClient):
|
||||
token = test_register(client)['access']
|
||||
|
||||
r = client.put(
|
||||
'/user', headers={'Authorization': 'Bearer ' + token}, data= {'username': 'lilian2', 'email': 'example@example.com', 'firstname': 'test', 'name': "test"})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 200
|
||||
assert data['username'] == 'lilian2'
|
||||
assert data['email'] == 'example@example.com'
|
||||
assert data['firstname'] == 'test'
|
||||
assert data['name'] == 'test'
|
||||
|
||||
def test_update_user_invalid(client: TestClient):
|
||||
token = test_register(client)['access']
|
||||
|
||||
r = client.put(
|
||||
'/user', headers={'Authorization': 'Bearer ' + token}, data={'username': 'lilian222222222222222', 'email': 'example@example.com', 'firstname': 'test', 'name': "test"})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 422
|
||||
assert data['detail']['username_error'] == 'ensure this value has at most 20 characters'
|
||||
|
||||
def test_update_username_missing(client: TestClient):
|
||||
token = test_register(client)['access']
|
||||
|
||||
r = client.put(
|
||||
'/user', headers={'Authorization': 'Bearer ' + token}, data={ 'email': 'example@example.com', 'firstname': 'test', 'name': "test"})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 422
|
||||
assert data['detail']['username_error'] == 'field required'
|
||||
def test_update_username_missing(client: TestClient):
|
||||
|
||||
r = client.put(
|
||||
'/user', data={ 'email': 'example@example.com', 'firstname': 'test', 'name': "test"})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 401
|
||||
assert data['detail'] == 'Not authenticated'
|
||||
|
||||
|
||||
#TODO invalid jwt
|
||||
|
||||
#Validation for delete user request work as same as login request so no need to test it
|
||||
def test_delete_user(client: TestClient):
|
||||
test_register(client)
|
||||
|
||||
r = client.delete(
|
||||
'/user', data={'username': VALID_USERNAME, 'password': VALID_PASSWORD})
|
||||
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 200
|
||||
assert data['ok'] == True
|
||||
|
||||
|
||||
def test_delete_invalid_password(client: TestClient):
|
||||
test_register(client)
|
||||
|
||||
r = client.delete(
|
||||
'/user', data={"username": VALID_USERNAME, 'password': 'Test1234'})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 401
|
||||
assert data['detail'][
|
||||
'password_error'] == "Mot de passe invalide"
|
||||
|
||||
|
||||
def test_delete_user_not_found(client: TestClient):
|
||||
r = client.delete(
|
||||
'/user', data={"username": VALID_USERNAME, 'password': VALID_PASSWORD})
|
||||
data = r.json()
|
||||
print(data)
|
||||
assert r.status_code == 401
|
||||
assert data['detail'][
|
||||
'username_error'] == "Utilisateur introuvable"
|
||||
|
||||
|
||||
def test_update_password(client: TestClient):
|
||||
tokens = test_register(client)
|
||||
token = tokens['access']
|
||||
|
||||
new_password = "12345Test"
|
||||
r = client.put(
|
||||
'/user/password', data={'password': new_password, 'password_confirm': new_password, 'old_password': VALID_PASSWORD}, headers={'Authorization': 'Bearer ' + token})
|
||||
|
||||
data = r.json()
|
||||
assert r.status_code == 200
|
||||
assert 'access_token' in data
|
||||
assert 'refresh_token' in data
|
||||
|
||||
new_token = data['access_token']
|
||||
|
||||
check_access = client.post('/check-access', headers = {'Authorization': 'Bearer ' + token})
|
||||
assert check_access.json() == False
|
||||
|
||||
check_access = client.post(
|
||||
'/check-access', headers={'Authorization': 'Bearer ' + new_token})
|
||||
|
||||
assert check_access.json()['username'] == VALID_USERNAME
|
||||
|
||||
log = client.post("/login", data={'username': VALID_USERNAME, 'password': new_password})
|
||||
data = log.json()
|
||||
assert log.status_code == 200
|
||||
assert 'access_token' in data
|
||||
assert 'refresh_token' in data
|
||||
|
||||
log = client.post("/login", data={'username': VALID_USERNAME, 'password': VALID_PASSWORD})
|
||||
data = log.json()
|
||||
assert log.status_code == 401
|
||||
|
||||
|
||||
def test_logout(client: TestClient):
|
||||
tokens = test_register(client)
|
||||
token = tokens['access']
|
||||
|
||||
r = client.post('/logout', headers={'Authorization': 'Bearer ' + token})
|
||||
data = r.json()
|
||||
|
||||
assert r.status_code == 200
|
||||
assert data['ok'] == True
|
||||
|
||||
check_access = client.post(
|
||||
'/check-access', headers={'Authorization': 'Bearer ' + token})
|
||||
assert check_access.json() == False
|
||||
|
||||
|
||||
|
||||
|
481
backend/api/tests/test_exos.py
Normal file
481
backend/api/tests/test_exos.py
Normal file
@ -0,0 +1,481 @@
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
import time
|
||||
from fastapi.testclient import TestClient
|
||||
from tests.test_auth import test_register
|
||||
|
||||
|
||||
def test_create(client: TestClient, name="test_exo", consigne="consigne", private=False, user=None):
|
||||
|
||||
if user == None:
|
||||
token = test_register(client, username="lilian")['access']
|
||||
username = 'lilian'
|
||||
else:
|
||||
token = user['token']
|
||||
username = user['username']
|
||||
r = client.post('/exercices', data={"name": name, "consigne": consigne, "private": private}, files={
|
||||
'file': ('test.py', open('tests/testing_exo_source/exo_source.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
||||
print(r.json())
|
||||
assert r.status_code == 201
|
||||
assert 'id_code' in r.json()
|
||||
assert {**r.json(), 'id_code': None} == {'name': name, 'consigne': consigne, 'private': private, 'id_code': None, 'author': {'username': username}, 'original': None, 'tags': [], 'exo_source': 'test.py', 'supports': {
|
||||
'pdf': True, 'csv': True, 'web': False}, 'examples': {'type': 'csv', 'data': [{'calcul': 'None ++', 'correction': 'None ++'}, {'calcul': 'None ++', 'correction': 'None ++'}, {'calcul': 'None ++', 'correction': 'None ++'}]}, 'is_author': True}
|
||||
return r.json()
|
||||
|
||||
|
||||
def test_create_too_long(client: TestClient):
|
||||
|
||||
token = test_register(client)['access']
|
||||
|
||||
r = client.post('/exercices', data={"name": "e"*51, "consigne": "e"*201, "private": False}, files={
|
||||
'file': ('test.py', open('tests/testing_exo_source/exo_source.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
||||
print('RESP', r.json())
|
||||
assert r.status_code == 422
|
||||
assert r.json()['detail'] == {'name_error': 'ensure this value has at most 50 characters',
|
||||
'consigne_error': 'ensure this value has at most 200 characters'}
|
||||
|
||||
|
||||
def test_create_name_missing(client: TestClient):
|
||||
token = test_register(client)['access']
|
||||
|
||||
r = client.post('/exercices', headers={"Authorization": "Bearer " + token})
|
||||
print(r.json())
|
||||
assert r.status_code == 422
|
||||
assert r.json()['detail'] == {
|
||||
'name_error': 'field required', 'file_error': 'field required'}
|
||||
|
||||
|
||||
def test_create_missing_main(client: TestClient):
|
||||
token = test_register(client)['access']
|
||||
|
||||
r = client.post('/exercices', data={"name": "test_exo", "consigne": "consigne", "private": False}, files={
|
||||
'file': ('test.py', open('tests/testing_exo_source/exo_source_missing_main.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
||||
print(r.json())
|
||||
assert r.status_code == 422
|
||||
assert r.json()['detail'] == {
|
||||
'exo_source_error': "Fonction 'main' introuvable"}
|
||||
|
||||
|
||||
def test_create_invalid_source(client: TestClient):
|
||||
token = test_register(client)['access']
|
||||
|
||||
r = client.post('/exercices', data={"name": "test_exo", "consigne": "consigne", "private": False}, files={
|
||||
'file': ('test.py', open('tests/testing_exo_source/exo_source_invalid.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
||||
print(r.json())
|
||||
assert r.status_code == 422
|
||||
assert r.json()['detail'] == {
|
||||
'exo_source_error': "Exercice non valide (compatible avec aucun support)"}
|
||||
|
||||
|
||||
def test_clone(client: TestClient):
|
||||
create = test_create(client)
|
||||
id_code = create['id_code']
|
||||
token = test_register(client, username="lilian2")['access']
|
||||
rr = client.post('/clone/' + id_code,
|
||||
headers={'Authorization': 'Bearer ' + token})
|
||||
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': {
|
||||
'pdf': True, 'csv': True, 'web': False}, 'examples': {'type': 'csv', 'data': [{'calcul': 'None ++', 'correction': 'None ++'}, {'calcul': 'None ++', 'correction': 'None ++'}, {'calcul': 'None ++', 'correction': 'None ++'}]}, 'is_author': True}
|
||||
|
||||
|
||||
def test_update(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
id_code = test_create(client, user={'token': token, 'username': "lilian"},
|
||||
consigne="testconsigne")['id_code']
|
||||
r = client.put('/exercice/' + id_code, data={"name": "name", "private": True}, files={
|
||||
'file': ('test2.py', open('tests/testing_exo_source/exo_source.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
||||
print(r.json())
|
||||
assert r.status_code == 200
|
||||
assert 'id_code' in r.json()
|
||||
assert r.json() == {'name': "name", 'consigne': "testconsigne", 'private': True, 'id_code': id_code, 'author': {'username': 'lilian'}, 'original': None, 'tags': [], 'exo_source': 'test2.py', 'supports': {
|
||||
'pdf': True, 'csv': True, 'web': False}, 'examples': {'type': 'csv', 'data': [{'calcul': 'None ++', 'correction': 'None ++'}, {'calcul': 'None ++', 'correction': 'None ++'}, {'calcul': 'None ++', 'correction': 'None ++'}]}, 'is_author': True}
|
||||
return r.json()
|
||||
|
||||
|
||||
def test_update_missing_name(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
id_code = test_create(client, user={'token': token, 'username': "lilian"})[
|
||||
'id_code']
|
||||
r = client.put('/exercice/' + id_code, data={"consigne": "consigne", "private": False}, files={
|
||||
'file': ('test2.py', open('tests/testing_exo_source/exo_source.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
||||
print(r.json())
|
||||
assert r.status_code == 422
|
||||
assert r.json()['detail'] == {'name_error': 'field required'}
|
||||
|
||||
|
||||
def test_update_missing_main(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
id_code = test_create(client, user={'token': token, 'username': "lilian"})[
|
||||
'id_code']
|
||||
r = client.put('/exercice/' + id_code, data={"consigne": "consigne", "private": False}, files={
|
||||
'file': ('test2.py', open('tests/testing_exo_source/exo_source_missing_main.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
||||
print(r.json())
|
||||
assert r.status_code == 422
|
||||
assert r.json()['detail'] == {
|
||||
'exo_source_error': "Fonction 'main' introuvable"}
|
||||
|
||||
|
||||
def test_update_invalid(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
id_code = test_create(client, user={'token': token, 'username': "lilian"})[
|
||||
'id_code']
|
||||
r = client.put('/exercice/' + id_code, data={"consigne": "consigne", "private": False}, files={
|
||||
'file': ('test2.py', open('tests/testing_exo_source/exo_source_invalid.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
||||
print(r.json())
|
||||
assert r.status_code == 422
|
||||
assert r.json()['detail'] == {
|
||||
'exo_source_error': "Exercice non valide (compatible avec aucun support)"}
|
||||
|
||||
|
||||
def test_update_too_long(client: TestClient):
|
||||
|
||||
token = test_register(client, username="lilian")['access']
|
||||
id_code = test_create(client, user={'token': token, 'username': "lilian"})[
|
||||
'id_code']
|
||||
r = client.put('/exercice/' + id_code, data={'name': 'e'*51, "consigne": "e"*201, "private": False}, files={
|
||||
'file': ('test2.py', open('tests/testing_exo_source/exo_source.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
||||
print(r.json())
|
||||
assert r.status_code == 422
|
||||
assert r.json()['detail'] == {'name_error': 'ensure this value has at most 50 characters',
|
||||
'consigne_error': 'ensure this value has at most 200 characters'}
|
||||
|
||||
|
||||
def test_delete(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
id_code = test_create(client, user={'token': token, 'username': "lilian"})[
|
||||
'id_code']
|
||||
r = client.delete('/exercice/' + id_code,
|
||||
headers={'Authorization': 'Bearer ' + token})
|
||||
print(r.json())
|
||||
assert r.status_code == 200
|
||||
assert r.json()['detail'] == 'Exercice supprimé avec succès'
|
||||
|
||||
|
||||
def test_delete_not_found(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
r = client.delete('/exercice/' + "test",
|
||||
headers={'Authorization': 'Bearer ' + token})
|
||||
print(r.json())
|
||||
assert r.status_code == 404
|
||||
assert r.json()['detail'] == 'Exercice introuvable'
|
||||
|
||||
|
||||
# TAGS
|
||||
class Tags(BaseModel):
|
||||
id_code: str | None
|
||||
color: str
|
||||
label: str
|
||||
|
||||
|
||||
def test_add_tags(client: TestClient, name='name', tags: List[Tags] = [{'label': "name", 'color': "#ff0000",
|
||||
'id_code': "tag_id"}], user=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']
|
||||
r = client.post(f'/exercice/{id_code}/tags', json=tags,
|
||||
headers={'Authorization': 'Bearer ' + token})
|
||||
print(r.json())
|
||||
data = r.json()
|
||||
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]]}
|
||||
return r.json()
|
||||
|
||||
|
||||
def test_add_tags_invalid_color(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
exo = test_create(client, user={'token': token, 'username': "lilian"})
|
||||
id_code = exo['id_code']
|
||||
r = client.post(f'/exercice/{id_code}/tags', json=[
|
||||
{'label': "name", 'color': "color", 'id_code': "id_code"}], headers={'Authorization': 'Bearer ' + token})
|
||||
print(r.json())
|
||||
data = r.json()
|
||||
assert r.status_code == 422
|
||||
assert data['detail'] == {
|
||||
'color_error': "value is not a valid enumeration member; permitted: '#00ff00', '#ff0000', '#0000ff', 'string'"}
|
||||
|
||||
|
||||
def test_add_tags_too_long(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
exo = test_create(client, user={'token': token, 'username': "lilian"})
|
||||
id_code = exo['id_code']
|
||||
r = client.post(f'/exercice/{id_code}/tags', json=[
|
||||
{'label': "n"*21, 'color': "#ff0000", 'id_code': "id_code"}], headers={'Authorization': 'Bearer ' + token})
|
||||
print(r.json())
|
||||
data = r.json()
|
||||
assert r.status_code == 422
|
||||
assert data['detail'] == {
|
||||
'label_error': "ensure this value has at most 20 characters"}
|
||||
|
||||
|
||||
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']
|
||||
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:]}
|
||||
|
||||
|
||||
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']
|
||||
tag_id = "none"
|
||||
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
|
||||
headers={'Authorization': 'Bearer ' + token})
|
||||
print(r.json())
|
||||
assert r.json()['detail'] == 'Tag introuvable'
|
||||
|
||||
|
||||
def test_remove_tag_exo_not_found(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
|
||||
tag_id = "none"
|
||||
id_code = "tets"
|
||||
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
|
||||
headers={'Authorization': 'Bearer ' + token})
|
||||
print(r.json())
|
||||
assert r.json()['detail'] == 'Exercice introuvable'
|
||||
|
||||
|
||||
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']
|
||||
tag_id = exo['tags'][0]['id_code']
|
||||
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
|
||||
headers={'Authorization': 'Bearer ' + token2})
|
||||
print(r.json())
|
||||
assert r.json()['detail'] == "Vous n'êtes pas le propriétaire du tag"
|
||||
|
||||
|
||||
def test_exo_exo_source(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
exo = test_create(client, user={'token': token, 'username': "lilian"})
|
||||
id_code = exo['id_code']
|
||||
r = client.get(f"/exercice/{id_code}/exo_source",
|
||||
headers={'Authorization': 'Bearer ' + token})
|
||||
print(r.text)
|
||||
print(r.headers)
|
||||
assert r.text == open('tests/testing_exo_source/exo_source.py', 'r').read()
|
||||
assert r.headers['content-disposition'].split('filename=')[-1] == 'test.py'
|
||||
|
||||
|
||||
def test_get_users_exos(client: TestClient):
|
||||
token1 = test_register(client, username="lilian")['access']
|
||||
token2 = test_register(client, username="lilian2")['access']
|
||||
|
||||
prv = test_create(client, private=True, user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
exo_other_user = test_create(
|
||||
client, user={'token': token2, 'username': "lilian2"})
|
||||
|
||||
r = client.get('/exercices/user',
|
||||
headers={'Authorization': 'Bearer ' + token1})
|
||||
print(r.json())
|
||||
assert r.json() == [prv]
|
||||
|
||||
|
||||
def test_get_user_with_search(client: TestClient):
|
||||
token1 = test_register(client, username="lilian")['access']
|
||||
token2 = test_register(client, username="lilian2")['access']
|
||||
|
||||
exo_other_user = test_create(
|
||||
client, user={'token': token2, 'username': "lilian2"})
|
||||
|
||||
exo1 = test_create(client, name='test1', user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
exo2 = test_create(client, name='test2', user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
exo3 = test_create(client, name='text', user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
exo4 = test_create(client, name='autre', user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
exo5 = test_create(client, name='tes', user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
|
||||
r = client.get('/exercices/user', params={"search": "test"},
|
||||
headers={'Authorization': 'Bearer ' + token1})
|
||||
print(r.json())
|
||||
assert r.json() == [exo1, exo2]
|
||||
|
||||
|
||||
def test_get_user_with_search(client: TestClient):
|
||||
token1 = test_register(client, username="lilian")['access']
|
||||
token2 = test_register(client, username="lilian2")['access']
|
||||
|
||||
exo_other_user = test_create(
|
||||
client, user={'token': token2, 'username': "lilian2"})
|
||||
|
||||
exo1 = test_create(client, name='test1', user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
exo2 = test_create(client, name='test2', user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
exo3 = test_create(client, name='text', user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
exo4 = test_create(client, name='autre', user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
exo5 = test_create(client, name='tes', user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
|
||||
r = client.get('/exercices/user', params={"search": "test"},
|
||||
headers={'Authorization': 'Bearer ' + token1})
|
||||
print(r.json())
|
||||
assert r.json() == [exo1, exo2]
|
||||
|
||||
|
||||
def test_get_user_with_tags(client: TestClient):
|
||||
token1 = test_register(client, username="lilian")['access']
|
||||
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}]
|
||||
|
||||
exo_other_user = test_create(
|
||||
client, user={'token': token2, 'username': "lilian2"})
|
||||
|
||||
exo1 = test_add_tags(client, user={
|
||||
'token': token1, 'username': "lilian"}, tags=tags1)
|
||||
|
||||
exo2 = test_add_tags(client, user={
|
||||
'token': token1, 'username': "lilian"}, tags=tags2)
|
||||
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']},
|
||||
headers={'Authorization': 'Bearer ' + token1})
|
||||
print(r.json())
|
||||
assert r.json() == [exo2, exo3]
|
||||
|
||||
|
||||
def test_get_user_with_tags_and_search(client: TestClient):
|
||||
token1 = test_register(client, username="lilian")['access']
|
||||
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}]
|
||||
|
||||
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")
|
||||
|
||||
exo2 = test_add_tags(client, user={
|
||||
'token': token1, 'username': "lilian"}, tags=tags2, name="no")
|
||||
|
||||
exo3 = test_add_tags(client, user={
|
||||
'token': token1, 'username': "lilian"}, tags=tags3, name="yes")
|
||||
|
||||
tags1 = exo1['tags']
|
||||
tags2 = exo2['tags']
|
||||
tags3 = exo3['tags']
|
||||
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() == [exo3]
|
||||
|
||||
|
||||
def test_get_public_auth(client: TestClient):
|
||||
token1 = test_register(client, username="lilian")['access']
|
||||
token2 = test_register(client, username="lilian2")['access']
|
||||
|
||||
prv = test_create(client, private=True, user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
public1 = test_create(
|
||||
client, user={'token': token2, 'username': "lilian2"})
|
||||
public2 = test_create(
|
||||
client, user={'token': token1, 'username': "lilian"})
|
||||
|
||||
r = client.get('/exercices/public',
|
||||
headers={'Authorization': 'Bearer ' + token2})
|
||||
print(r.json())
|
||||
assert r.json() == [{**public2, 'is_author': False}]
|
||||
|
||||
|
||||
def test_get_public_auth_with_search(client: TestClient):
|
||||
token1 = test_register(client, username="lilian")['access']
|
||||
token2 = test_register(client, username="lilian2")['access']
|
||||
|
||||
prv = test_create(client, private=True, user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
public1 = test_create(
|
||||
client, user={'token': token2, 'username': "lilian2"})
|
||||
|
||||
public2 = test_create(
|
||||
client, user={'token': token1, 'username': "lilian"}, name="yes")
|
||||
public3 = test_create(
|
||||
client, user={'token': token1, 'username': "lilian"}, name="no")
|
||||
|
||||
r = client.get('/exercices/public',
|
||||
params={'search': "yes"}, headers={'Authorization': 'Bearer ' + token2})
|
||||
print(r.json())
|
||||
assert r.json() == [{**public2, 'is_author': False}]
|
||||
|
||||
def test_get_public_no_auth(client: TestClient):
|
||||
token1 = test_register(client, username="lilian")['access']
|
||||
token2 = test_register(client, username="lilian2")['access']
|
||||
|
||||
prv = test_create(client, private=True, user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
public1 = test_create(
|
||||
client, user={'token': token2, 'username': "lilian2"})
|
||||
public2 = test_create(
|
||||
client, user={'token': token1, 'username': "lilian"})
|
||||
|
||||
r = client.get('/exercices/public')
|
||||
print(r.json())
|
||||
assert r.json() == [{**public1, 'is_author': False},
|
||||
{**public2, 'is_author': False}]
|
||||
|
||||
|
||||
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}
|
||||
|
||||
def test_get_exo_auth(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'],
|
||||
headers={'Authorization': 'Bearer ' + token2})
|
||||
print(r.json(), exo)
|
||||
assert r.json() == {**exo, "tags": [], 'is_author': False}
|
||||
|
||||
def test_get_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"})
|
||||
exo2 = test_add_tags(client, user={'token': token2, 'username': "lilian2"})
|
||||
|
||||
tags1 = exo['tags']
|
||||
tags2 = exo2['tags']
|
||||
|
||||
r = client.get('/tags', headers={'Authorization': 'Bearer ' + token2})
|
||||
print(r.json())
|
||||
assert r.json() == tags2
|
11
backend/api/tests/testing_exo_source/exo_source.py
Normal file
11
backend/api/tests/testing_exo_source/exo_source.py
Normal file
@ -0,0 +1,11 @@
|
||||
import random
|
||||
|
||||
"""
|
||||
Fonction main() qui doit renvoyer un objet avec:
|
||||
calcul: le calcul a afficher
|
||||
result: la correction du calcul (pas de correction -> mettre None)
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
return {"csv": "None ++", 'web': None, "calcul": "1+1=2"}
|
11
backend/api/tests/testing_exo_source/exo_source_invalid.py
Normal file
11
backend/api/tests/testing_exo_source/exo_source_invalid.py
Normal file
@ -0,0 +1,11 @@
|
||||
import random
|
||||
|
||||
"""
|
||||
Fonction main() qui doit renvoyer un objet avec:
|
||||
calcul: le calcul a afficher
|
||||
result: la correction du calcul (pas de correction -> mettre None)
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
return {"csv": None, 'web': None, "web": None}
|
@ -0,0 +1 @@
|
||||
|
0
backend/api_old/apis/__init__.py
Normal file
0
backend/api_old/apis/__init__.py
Normal file
140
backend/api_old/apis/auth/route_auth.py
Normal file
140
backend/api_old/apis/auth/route_auth.py
Normal file
@ -0,0 +1,140 @@
|
||||
from typing import List, Optional
|
||||
from fastapi_jwt_auth import AuthJWT
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, FastAPI, Form, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from pydantic import BaseModel
|
||||
from api_old.schema.user import UserForm
|
||||
from config import Room_schema, User_schema, UserIn_Form, UserIn_schema
|
||||
from database.auth.crud import create_user_db, disable_user_db, delete_user_db, get_user_db, update_password_db, update_user_db
|
||||
from services.auth import PasswordSet, check_unique_user, create_access_token, authenticate_user, fresh_jwt_required, get_current_clientId, get_current_user, jwt_refresh_required, jwt_required, User, UserRegister, validate_passwords, validate_register_user
|
||||
from services.password import get_password_hash, validate_password
|
||||
from database.auth.models import UserModel
|
||||
from database.decorators import as_form
|
||||
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
refresh_token: str
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login_for_access_token(user: User = Depends(authenticate_user), Authorize: AuthJWT = Depends()):
|
||||
access_token = Authorize.create_access_token(
|
||||
subject=user.username, fresh=True)
|
||||
refresh_token = Authorize.create_refresh_token(subject=user.clientId)
|
||||
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
class Room(BaseModel):
|
||||
name: str
|
||||
id_code: str
|
||||
owner: bool
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str = None
|
||||
firstname: str = None
|
||||
name: str = None
|
||||
email: str = None
|
||||
rooms: list[Room] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
@router.get("/user", response_model=User)
|
||||
async def read_users_me(Authorize: AuthJWT = Depends(get_current_user)):
|
||||
Authorize.jwt_required()
|
||||
clientId = Authorize.get_jwt_subject()
|
||||
user = await get_user_db(clientId)
|
||||
if user is not None and user.disabled == False:
|
||||
print(user.room_owners)
|
||||
sc = await User_schema.from_tortoise_orm(user)
|
||||
sc = sc.dict()
|
||||
# sc['rooms'] = await Room_schema.from_queryset(await user.rooms.all())
|
||||
f = await Room_schema.from_queryset(user.rooms.all())
|
||||
ro = await user.room_owners.all().values('room_id')
|
||||
|
||||
rr = [r['room_id'] for r in ro]
|
||||
ff = [r.id for r in f]
|
||||
rooms = [{**r.dict(), "owner": r.id in ff} for r in f]
|
||||
print(rooms)
|
||||
sc = await User_schema.from_tortoise_orm(user)
|
||||
sc = sc.dict()
|
||||
|
||||
sc['rooms'] = rooms
|
||||
return sc
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail='User disabled')
|
||||
|
||||
|
||||
@router.delete('/user')
|
||||
async def delete_user(user: UserModel = Depends(authenticate_user)):
|
||||
await delete_user_db(user.username)
|
||||
return 'success'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@router.put('/user')
|
||||
async def update_user(user: UserForm = Depends(UserForm.as_form), username: str = Depends(get_current_clientId), Authorize: AuthJWT = Depends()):
|
||||
user_obj = await update_user_db(username, **user.dict(exclude_unset=True))
|
||||
access_token = Authorize.create_access_token(
|
||||
subject=user.clientId)
|
||||
refresh_token = Authorize.create_refresh_token(subject=user.clientId)
|
||||
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.post('/register', response_model=Token)
|
||||
async def register(user: UserRegister = Depends(UserRegister.as_form), Authorize: AuthJWT = Depends()):
|
||||
username = await check_unique_user(user.username)
|
||||
|
||||
user = await create_user_db(user.username, get_password_hash(user.password))
|
||||
|
||||
access_token = Authorize.create_access_token(
|
||||
subject=user.username)
|
||||
refresh_token = Authorize.create_refresh_token(subject=user.username)
|
||||
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.post('/user/disable')
|
||||
async def disable_user(user: UserModel = Depends(authenticate_user)):
|
||||
await disable_user_db(user.username)
|
||||
return 'success'
|
||||
|
||||
|
||||
@router.put('/user/password')
|
||||
async def update_password(passwords: PasswordSet = Depends(validate_passwords),Authorize: AuthJWT=Depends(fresh_jwt_required)):
|
||||
username = Authorize.get_jwt_subject()
|
||||
user = await update_password_db(username, passwords.password)
|
||||
return await User_schema.from_tortoise_orm(user)
|
||||
|
||||
|
||||
@router.get('/users')
|
||||
async def get_users():
|
||||
return await User_schema.from_queryset(UserModel.all())
|
||||
|
||||
|
||||
@router.post('/refresh')
|
||||
async def refresh(Authorize: AuthJWT = Depends(jwt_refresh_required)):
|
||||
current_user = Authorize.get_jwt_subject()
|
||||
new_access_token = Authorize.create_access_token(subject=current_user)
|
||||
return {"access_token": new_access_token}
|
||||
|
||||
|
||||
|
||||
@router.post('/check-access')
|
||||
async def check_token(Authorize: AuthJWT = Depends(jwt_required)):
|
||||
return ""
|
13
backend/api_old/apis/base.py
Normal file
13
backend/api_old/apis/base.py
Normal file
@ -0,0 +1,13 @@
|
||||
from fastapi import APIRouter
|
||||
import apis.exercices.route_exercices
|
||||
import apis.auth.route_auth
|
||||
import apis.room.route_room
|
||||
import apis.room.websocket
|
||||
api_router = APIRouter()
|
||||
|
||||
api_router.include_router(apis.exercices.route_exercices.router)
|
||||
api_router.include_router(apis.auth.route_auth.router)
|
||||
api_router.include_router(apis.room.route_room.router)
|
||||
api_router.include_router(apis.room.websocket.router)
|
||||
|
||||
|
0
backend/api_old/apis/exercices/__init__.py
Normal file
0
backend/api_old/apis/exercices/__init__.py
Normal file
165
backend/api_old/apis/exercices/route_exercices.py
Normal file
165
backend/api_old/apis/exercices/route_exercices.py
Normal file
@ -0,0 +1,165 @@
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, Form, UploadFile, status
|
||||
from api.schemas.exercices import ExerciceSchema
|
||||
from database.exercices.validators import get_support_compatibility_for_exo_source_from_data
|
||||
from database.auth.models import UserModel
|
||||
from generateur.generateur_csv import Csv_generator
|
||||
from database.decorators import as_form
|
||||
from database.exercices.models import Exercice
|
||||
from fastapi.exceptions import HTTPException
|
||||
from database.exercices.crud import add_tag_db, create_exo_db, delete_tag_db, get_exo_source_path, update_exo_db, delete_exo_db, clone_exo_db
|
||||
from config import Exercice_schema, ExerciceIn_form, ExerciceIn_schema, Exo_schema, TagIn_schema, User_schema
|
||||
from services.auth import check_author_exo, get_current_clientId, get_current_user, jwt_optional, jwt_required
|
||||
from services.io import get_abs_path_from_relative_to_root, get_filename_from_path
|
||||
from fastapi.responses import FileResponse, Response, StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from fastapi_jwt_auth import AuthJWT
|
||||
from fastapi_pagination import paginate, Page
|
||||
router = APIRouter()
|
||||
|
||||
# Exercices
|
||||
|
||||
|
||||
@router.get("/exercices", response_model=Page[Exo_schema])
|
||||
async def get_exercices():
|
||||
exo_list = await Exo_schema.from_queryset(Exercice.all())
|
||||
return paginate(exo_list)
|
||||
|
||||
|
||||
@router.get('/exercices/user', response_model=Page[Exo_schema])
|
||||
async def get_exercices(Authorize: AuthJWT=Depends(jwt_required)):
|
||||
username = Authorize.get_jwt_subject()
|
||||
user = await UserModel.get(username=username)
|
||||
|
||||
exo_list = await Exo_schema.from_queryset(Exercice.filter(author_id=user.id))
|
||||
return paginate(exo_list)
|
||||
|
||||
@router.get('/exercices/public', response_model=Page[Exo_schema])
|
||||
async def get_exercices(Authorize: AuthJWT=Depends(jwt_optional)):
|
||||
username = Authorize.get_jwt_subject()
|
||||
is_authenticated = username != None
|
||||
if is_authenticated:
|
||||
user = await UserModel.get(username=username)
|
||||
exo_list = Exercice.filter(author_id__not = user.id)
|
||||
return paginate(exo_list)
|
||||
exo_list = await Exo_schema.from_queryset(Exercice.all())
|
||||
return paginate(exo_list)
|
||||
|
||||
|
||||
@router.get('/exercice/{id_code}', response_model=ExerciceSchema)
|
||||
async def get_exercice(id_code: str, Authorize: AuthJWT= Depends(jwt_optional)):
|
||||
username = Authorize.get_jwt_subject()
|
||||
is_authenticated = username != None
|
||||
|
||||
exo = await Exercice.get(id_code=id_code)
|
||||
if is_authenticated:
|
||||
user = await UserModel.get(username=username)
|
||||
author = await exo.author
|
||||
is_author = author.id == user.id
|
||||
exo_obj = await Exercice_schema.from_tortoise_orm(exo)
|
||||
exo_dict = exo_obj.dict()
|
||||
exo_dict['is_author'] = is_author
|
||||
return exo_dict
|
||||
|
||||
exo_obj = await Exercice_schema.from_tortoise_orm(exo)
|
||||
exo_dict =exo_obj.dict()
|
||||
exo_dict['is_author'] = False
|
||||
return exo_dict
|
||||
|
||||
|
||||
async def validate_file(file: UploadFile):
|
||||
data = await file.read()
|
||||
try:
|
||||
exo_supports_compatibility = get_support_compatibility_for_exo_source_from_data(
|
||||
data)
|
||||
if not exo_supports_compatibility['isPdf'] and not exo_supports_compatibility['isCsv'] and not exo_supports_compatibility['isWeb']:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail={"exo_source":
|
||||
'[Error] : Exercice non valide (compatible avec aucun support)'})
|
||||
|
||||
except Exception as e:
|
||||
msg = e.args[0]
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail={"exo_source": msg})
|
||||
await file.seek(0)
|
||||
return file
|
||||
|
||||
@router.post("/exercices", response_model=Exercice_schema)
|
||||
async def create_exercice(file: UploadFile = Depends(validate_file), exo: ExerciceIn_schema = Depends(ExerciceIn_form.as_form), current_user: UserModel = Depends(get_current_user)):
|
||||
file_obj = file.file._file
|
||||
file_obj.name = file.filename
|
||||
exo_obj = await create_exo_db(**{**exo.dict(exclude_unset=True)}, exo_source=file_obj, author_id=current_user.id)
|
||||
return await Exercice_schema.from_tortoise_orm(exo_obj)
|
||||
|
||||
|
||||
@router.delete("/exercices/{id_code}", response_model=str)
|
||||
async def delete_exercice(id_code: str, author: User_schema = Depends(check_author_exo)):
|
||||
await delete_exo_db(id_code)
|
||||
return "success"
|
||||
|
||||
|
||||
@router.put("/exercices/{id_code}", response_model=Exercice_schema)
|
||||
async def update_exercice(id_code: str, file: UploadFile, exo: ExerciceIn_form = Depends(ExerciceIn_form.as_form), author: User_schema = Depends(check_author_exo)):
|
||||
file_obj = file.file._file
|
||||
file_obj.name = file.filename
|
||||
exo_obj = await update_exo_db(id_code, **{**exo.dict(exclude_unset=True)}, exo_source=file_obj)
|
||||
return await Exercice_schema.from_tortoise_orm(exo_obj)
|
||||
|
||||
|
||||
@router.post('/exercices/{id_code}/clone', response_model=Exercice_schema)
|
||||
async def clone_exercice(id_code: str, user: User_schema = Depends(get_current_user)):
|
||||
exo_obj = await clone_exo_db(id_code, user.id)
|
||||
return await Exercice_schema.from_tortoise_orm(exo_obj)
|
||||
|
||||
|
||||
@router.get('/exercices/{id_code}/exo_source')
|
||||
async def get_exo_source(id_code: str, author: User_schema = Depends(check_author_exo)):
|
||||
path = await get_exo_source_path(id_code)
|
||||
filename = get_filename_from_path(path)
|
||||
return FileResponse(path, filename=filename)
|
||||
|
||||
# Tags
|
||||
|
||||
|
||||
@router.post('/exercices/{id_code}/tags', response_model=Exercice_schema)
|
||||
async def update_tag(id_code: str, tags_data: List[TagIn_schema], current_user: User_schema = Depends(get_current_user)):
|
||||
exo_obj = await add_tag_db(id_code, tags_data, current_user.id)
|
||||
return await Exercice_schema.from_tortoise_orm(exo_obj)
|
||||
|
||||
|
||||
@router.delete('/exercices/{exo_id}/tags/{tags_id}', response_model=Exercice_schema)
|
||||
async def remove_tag(exo_id: str, tag_id: str, owner: User_schema = Depends(check_author_exo)):
|
||||
exo_obj = await delete_tag_db(exo_id, tag_id)
|
||||
return await Exercice_schema.from_tortoise_orm(exo_obj)
|
||||
|
||||
|
||||
@router.get('/generator/csv/{exo_id}')
|
||||
async def generate_csv(exo_id: str, filename: str):
|
||||
exo = await Exercice.get(id_code=exo_id)
|
||||
|
||||
if exo.csvSupport == False:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail='Impossible de générer cet exercice sur le support csv')
|
||||
|
||||
source_path = get_abs_path_from_relative_to_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')
|
||||
|
||||
|
||||
class ExoOption(BaseModel):
|
||||
id: str
|
||||
nbInExo: int
|
||||
nbOfExo: int
|
||||
|
||||
|
||||
@router.post('/generator/pdf')
|
||||
async def generate_pdf(exos_list: List[ExoOption]):
|
||||
return
|
64
backend/api_old/apis/room/route_room.py
Normal file
64
backend/api_old/apis/room/route_room.py
Normal file
@ -0,0 +1,64 @@
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from config import AnonymousIn_schema, Room_schema, RoomIn_schema, User_schema
|
||||
from database.auth.models import UserModel
|
||||
from database.room.crud import create_room_anonymous_db, create_room_with_user_db, get_room_db
|
||||
from database.room.models import Room
|
||||
from services.auth import get_current_user_optional
|
||||
router = APIRouter()
|
||||
|
||||
@router.post('/rooms')
|
||||
async def create_room(roomData: RoomIn_schema, anonymous: AnonymousIn_schema = None, user: User_schema = Depends(get_current_user_optional)):
|
||||
if user is not None:
|
||||
room= await create_room_with_user_db(room = roomData, user=user)
|
||||
return await Room_schema.from_tortoise_orm(room)
|
||||
else:
|
||||
room = await create_room_anonymous_db(room = roomData, anonymous = anonymous)
|
||||
return await Room_schema.from_tortoise_orm(room)
|
||||
|
||||
@router.get('/room/{room_id}')
|
||||
async def get_room(room_id: str):
|
||||
room = await get_room_db(room_id)
|
||||
if room is None:
|
||||
return None
|
||||
return await Room_schema.from_tortoise_orm(room)
|
||||
|
||||
@router.get('/room/check/{room_id}')
|
||||
async def check_room(room_id:str):
|
||||
room = await get_room_db(room_id)
|
||||
if room is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
@router.post('/room/{room_id}/join')
|
||||
async def join_room(room_id: str, anonymous: AnonymousIn_schema = None, user: User_schema = Depends(get_current_user_optional)):
|
||||
room = await Room.get(id_code=room_id)
|
||||
user = await UserModel.get(id=user.id)
|
||||
if room.private == True:
|
||||
if user is not None:
|
||||
await room.users_waiters.add(user)
|
||||
return 'waiting'
|
||||
else:
|
||||
return
|
||||
else:
|
||||
if user is not None:
|
||||
await room.users.add(user)
|
||||
return 'logged in'
|
||||
|
||||
|
||||
@router.delete('/room/{room_id}')
|
||||
async def delete_room(room_id):
|
||||
return
|
||||
|
||||
|
||||
@router.get('/rooms')
|
||||
async def get_rooms():
|
||||
rooms = Room.all()
|
||||
return await Room_schema.from_queryset(rooms)
|
||||
|
||||
|
||||
@router.get('/test/{room_id}')
|
||||
async def test(room_id):
|
||||
room = await Room.get(id_code=room_id)
|
||||
ano = await room.anonymousmembers
|
||||
print(await ano.get(id_code='JTSGUC'))
|
||||
return "user"
|
315
backend/api_old/apis/room/websocket.py
Normal file
315
backend/api_old/apis/room/websocket.py
Normal file
@ -0,0 +1,315 @@
|
||||
import json
|
||||
from typing import Dict, List, Union
|
||||
from fastapi import Cookie, Depends, FastAPI, HTTPException, Query, WebSocket, status, APIRouter, WebSocketDisconnect, status
|
||||
from fastapi.responses import HTMLResponse
|
||||
from config import User_schema
|
||||
from database.auth.models import UserModel
|
||||
from database.exercices.crud import generate_unique_code
|
||||
from database.room.crud import check_anonymous_owner, check_user_in_room, check_user_owner, connect_room, create_waiter_anonymous, create_waiter_by_user, disconnect_room, get_member_by_code, validate_name_in_room
|
||||
from database.room.models import AnonymousMember, Room, RoomOwner, Waiter
|
||||
from services.auth import get_user_from_token
|
||||
from services.io import get_abs_path_from_relative_to_root
|
||||
import secrets
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get('/')
|
||||
def index():
|
||||
return HTMLResponse(open(get_abs_path_from_relative_to_root('/index.html'), 'r').read())
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: Dict[str,List[WebSocket]] = {}
|
||||
|
||||
async def connect(self, websocket: WebSocket, room_id):
|
||||
await websocket.accept()
|
||||
|
||||
if room_id not in self.active_connections:
|
||||
self.active_connections[room_id] = []
|
||||
|
||||
self.active_connections[room_id].append(websocket)
|
||||
|
||||
async def add(self, room_id, ws):
|
||||
if room_id not in self.active_connections:
|
||||
self.active_connections[room_id] = []
|
||||
|
||||
self.active_connections[room_id].append(ws)
|
||||
|
||||
def remove(self, websocket: WebSocket, room_id):
|
||||
if room_id in self.active_connections:
|
||||
try:
|
||||
self.active_connections[room_id].remove(websocket)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def send_personal_message(self, message: str, websocket: WebSocket):
|
||||
await websocket.send_text(message)
|
||||
|
||||
async def broadcast(self, message: str, room_id):
|
||||
if room_id in self.active_connections:
|
||||
for connection in self.active_connections[room_id]:
|
||||
await connection.send_json(message)
|
||||
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
class Consumer():
|
||||
def __init__(self, ws: WebSocket):
|
||||
self.ws : WebSocket = ws
|
||||
|
||||
async def connect(self):
|
||||
pass
|
||||
|
||||
async def receive(self):
|
||||
pass
|
||||
|
||||
async def disconnect(self):
|
||||
pass
|
||||
|
||||
async def run(self):
|
||||
await self.connect()
|
||||
try:
|
||||
while True:
|
||||
data = await self.ws.receive_text()
|
||||
await self.receive(data)
|
||||
except WebSocketDisconnect:
|
||||
await self.disconnect()
|
||||
|
||||
|
||||
class RoomConsumer(Consumer):
|
||||
def __init__(self, ws:WebSocket, room_id, manager:ConnectionManager):
|
||||
self.ws:WebSocket = ws
|
||||
self.room_id = room_id
|
||||
self.manager:ConnectionManager = manager
|
||||
self.owner = False
|
||||
|
||||
async def connect(self):
|
||||
await self.ws.accept()
|
||||
self.clientId = secrets.token_hex(32)
|
||||
await self.manager.add(self.ws, self.room_id)
|
||||
self.room : Room = await Room.get(id_code=self.room_id)
|
||||
self.status = None
|
||||
self.waiter = None
|
||||
self.user = None
|
||||
await self.ws.send_json({'type': 'accept'})
|
||||
|
||||
|
||||
|
||||
async def receive(self, data):
|
||||
json_data = json.loads(data)
|
||||
payload = json_data['data']
|
||||
type = json_data['type']
|
||||
|
||||
if type == 'auth':
|
||||
token = payload['token']
|
||||
self.user = await get_user_from_token(token)
|
||||
|
||||
if self.user is not None:
|
||||
await self.ws.send_json({'type': 'auth_success'})
|
||||
else:
|
||||
await self.ws.send_json({'type': 'auth_failed'})
|
||||
|
||||
|
||||
if type == "login" and self.room.private == True and self.user is not None:
|
||||
if await check_user_in_room(self.room, self.user):
|
||||
if await check_user_owner(self.room, self.user):
|
||||
self.owner = True
|
||||
await self.manager.add(f'{self.room_id}__owner', self.ws)
|
||||
else:
|
||||
await self.manager.add(self.room_id, self.ws)
|
||||
await connect_room(self.room, self.user.id)
|
||||
await self.manager.broadcast({'type': 'joined', 'data': {"name": self.user.username}}, self.room_id)
|
||||
await self.manager.broadcast({'type': 'joined', 'data': {"name": self.user.username}}, f'{self.room_id}__owner')
|
||||
|
||||
else:
|
||||
self.waiter = await create_waiter_by_user(self.room, self.user)
|
||||
|
||||
await self.ws.send_json({'data': "waiting"})
|
||||
await self.manager.add(f'{self.room_id}__waiting__{self.user.id}', self.ws)
|
||||
await self.manager.broadcast({'type': 'add_waiter', 'data': {"name": self.user.username, 'id': self.user.id}}, f'{self.room_id}__owner')
|
||||
|
||||
|
||||
if type == "login" and self.room.private == True and self.user is None:
|
||||
if 'relogin_code' in payload:
|
||||
|
||||
anonymous = await get_member_by_code(self.room, payload['relogin_code'])
|
||||
if anonymous is not None:
|
||||
if await check_anonymous_owner(self.room, anonymous):
|
||||
self.owner = True
|
||||
await self.manager.add(f'{self.room_id}__owner', self.ws)
|
||||
else:
|
||||
await self.manager.add(self.room_id, self.ws)
|
||||
self.anonymous = anonymous
|
||||
await connect_room(self.room, self.anonymous.id_code)
|
||||
await self.manager.broadcast(
|
||||
{'type': 'joined', 'data': {"name": anonymous.name}}, self.room_id)
|
||||
|
||||
else:
|
||||
valid_username = await validate_name_in_room(payload['name'])
|
||||
if valid_username == True:
|
||||
self.waiter = await create_waiter_anonymous(self.room, payload['name'])
|
||||
|
||||
await self.ws.send_json({'type': "waiting"})
|
||||
await self.manager.add(f'{self.room_id}__waiting__{self.waiter.id_code}', self.ws)
|
||||
await self.manager.broadcast({'type': 'add_waiter', 'data': {
|
||||
"name": self.waiter.name, 'id': self.waiter.id_code}}, f'{self.room_id}__owner')
|
||||
else:
|
||||
await self.ws.send_json({"type": "error", 'data': {"user_input": valid_username}})
|
||||
|
||||
|
||||
if type == "login" and self.room.private == False and self.user is not None:
|
||||
if await check_user_in_room(self.room, self.user):
|
||||
if await check_user_owner(self.room, self.user):
|
||||
self.owner = True
|
||||
await self.manager.add(f'{self.room_id}__owner', self.ws)
|
||||
else:
|
||||
await self.manager.add(self.room_id, self.ws)
|
||||
await connect_room(self.room, self.user.id)
|
||||
await self.manager.broadcast({'type': 'joined', 'data': {"name": self.user.username}}, self.room_id)
|
||||
await self.manager.broadcast({'type': 'joined', 'data': {"name": self.user.username}}, f'{self.room_id}__owner')
|
||||
else:
|
||||
await self.room.users.add(self.user)
|
||||
await self.manager.add(self.room_id, self.ws)
|
||||
await connect_room(self.room, self.user.id)
|
||||
await self.manager.broadcast(
|
||||
{'type': 'joined', 'data': {"name": self.user.username}}, self.room_id)
|
||||
|
||||
await self.manager.broadcast(
|
||||
{'type': 'joined', 'data': {"name": self.user.username}}, f'{self.room_id}__owner')
|
||||
|
||||
|
||||
if type == 'login' and self.room.private == False and self.user is None:
|
||||
if 'relogin_code' in payload:
|
||||
|
||||
anonymous = await get_member_by_code(self.room, payload['relogin_code'])
|
||||
if anonymous is not None:
|
||||
|
||||
if await check_anonymous_owner(self.room, anonymous):
|
||||
self.owner = True
|
||||
await self.manager.add(f'{self.room_id}__owner', self.ws)
|
||||
else:
|
||||
await self.manager.add(self.room_id, self.ws)
|
||||
self.anonymous = anonymous
|
||||
await connect_room(self.room, self.anonymous.id_code)
|
||||
await self.manager.broadcast(
|
||||
{'type': 'joined', 'data': {"name": anonymous.name}}, self.room_id)
|
||||
else:
|
||||
valid_username = await validate_name_in_room(self.room, payload['name'])
|
||||
if valid_username == True:
|
||||
code = await generate_unique_code(AnonymousMember)
|
||||
self.anonymous = await AnonymousMember.create(name=payload['name'], id_code=code, room_id=self.room.id)
|
||||
await self.manager.add(self.room_id, self.ws)
|
||||
self.owner = False
|
||||
await connect_room(self.room, self.anonymous.id_code)
|
||||
await self.manager.broadcast({'type': 'joined', 'data': {'name': self.anonymous.name}}, self.room_id)
|
||||
await self.manager.broadcast({'type': 'joined', 'data': {'name': self.anonymous.name, 'code': self.anonymous.id_code}}, f'{self.room_id}__owner')
|
||||
else:
|
||||
await self.ws.send({'type': "error", "data": {"user_input": valid_username}})
|
||||
|
||||
|
||||
|
||||
if type == 'accept_waiter':
|
||||
if self.owner == True:
|
||||
id = payload['id']
|
||||
await self.manager.broadcast({'type': 'log_waiter', 'data': {}}, f'{self.room_id}__waiting__{id}')
|
||||
|
||||
if type == 'refuse_waiter':
|
||||
if self.owner == True:
|
||||
id = payload['id']
|
||||
await self.manager.broadcast({'type': 'reject_waiter', 'data': {}}, f'{self.room_id}__waiting__{id}')
|
||||
|
||||
if type == 'log_waiter':
|
||||
if self.user is not None:
|
||||
await self.room.users.add(self.user)
|
||||
await self.waiter.delete()
|
||||
self.manager.remove('f{self.room_id}__waiting__{self.user.id}', self.ws)
|
||||
await self.manager.add(self.room_id, self.ws)
|
||||
|
||||
await connect_room(self.room, self.user.id)
|
||||
await self.manager.broadcast(
|
||||
{'type': 'joined', 'data': {"name": self.user.username}}, self.room_id)
|
||||
|
||||
await self.manager.broadcast(
|
||||
{'type': 'joined', 'data': {"name": self.user.username}}, f'{self.room_id}__owner')
|
||||
|
||||
else:
|
||||
code = await generate_unique_code(AnonymousMember)
|
||||
self.anonymous = await AnonymousMember.create(name=self.waiter.name, id_code=code, room_id=self.room.id)
|
||||
|
||||
self.manager.remove(self.ws, f'{self.room_id}__waiting__{self.waiter.id_code}')
|
||||
await self.waiter.delete()
|
||||
self.waiter = None
|
||||
await self.manager.add(self.room_id, self.ws)
|
||||
self.owner = False
|
||||
|
||||
await connect_room(self.room, self.anonymous.id_code)
|
||||
await self.manager.broadcast({'type': 'joined', 'data': {'name': self.anonymous.name}}, self.room_id)
|
||||
await self.manager.broadcast({'type': 'joined', 'data': {'name': self.anonymous.name, 'code': self.anonymous.id_code}}, f'{self.room_id}__owner')
|
||||
elif type == 'ban':
|
||||
if self.owner == True:
|
||||
status = payload['status']
|
||||
name = ""
|
||||
if status == 'user':
|
||||
user = await UserModel.get(id=payload['id'])
|
||||
await disconnect_room(self.room, user.id)
|
||||
name = user.username
|
||||
await self.room.users.remove(user)
|
||||
elif status == 'anonymous':
|
||||
anonymous = await AnonymousMember.get(id_code=payload['id'])
|
||||
name = anonymous.name
|
||||
await disconnect_room(self.room, anonymous.id_code)
|
||||
await anonymous.delete()
|
||||
|
||||
await self.manager.broadcast({'type': 'leave', 'data': {"name": name}}, self.room_id)
|
||||
await self.manager.broadcast({'type': 'leave', 'data': {"name": name}},f'{self.room_id}__owner')
|
||||
|
||||
elif type == "leave":
|
||||
name = ""
|
||||
if self.user is not None:
|
||||
name = self.user.username
|
||||
await self.room.users.remove(self.user)
|
||||
|
||||
else:
|
||||
name = self.anonymous.name
|
||||
await self.anonymous.delete()
|
||||
|
||||
await self.manager.broadcast({'type': 'leave', 'data': {"name": name}}, self.room_id)
|
||||
await self.manager.broadcast({'type': 'leave', 'data': {"name": name}}, f'{self.room_id}__owner')
|
||||
|
||||
async def disconnect(self):
|
||||
if self.waiter != None:
|
||||
self.manager.remove(self.ws, f'{self.room_id}__waiting__{self.waiter.id_code}')
|
||||
await self.manager.broadcast({'type': "disconnect_waiter", 'data': {'name': self.waiter.name}}, self.room_id)
|
||||
await self.manager.broadcast({'type': "disconnect_waiter", 'data': {'name': self.waiter.name}}, f'{self.room_id}__owner')
|
||||
await self.waiter.delete()
|
||||
if self.owner == True:
|
||||
if self.user is not None:
|
||||
disconnect_room(self.room, self.user.id)
|
||||
else:
|
||||
disconnect_room(self.room, self.anonymous.id_code)
|
||||
self.manager.remove(self.ws, f'{self.room_id}__owner')
|
||||
else:
|
||||
self.manager.remove(self.ws, self.room_id)
|
||||
if self.user is not None:
|
||||
disconnect_room(self.room, self.user.id)
|
||||
await self.manager.broadcast({'type': "disconnect", 'data': {'name': self.user.username}}, self.room_id)
|
||||
await self.manager.broadcast({'type': "disconnect", 'data': {'name': self.user.username}}, f'{self.room_id}__owner')
|
||||
elif self.anonymous is not None:
|
||||
disconnect_room(self.room, self.anonymous.id_code)
|
||||
await self.manager.broadcast({'type': "disconnect_waiter", 'data': {'name': self.anonymous.name}}, self.room_id)
|
||||
await self.manager.broadcast({'type': "disconnect_waiter", 'data': {'name': self.anonymous.name}}, f'{self.room_id}__owner')
|
||||
await self.manager.broadcast({'type': "disconnect", 'data': {'name': self}}, self.room_id)
|
||||
await self.manager.broadcast({'type': f"Client left the chat"}, f'{self.room_id}__owner')
|
||||
|
||||
|
||||
async def check_room(room_id):
|
||||
room = await Room.get_or_none(id_code = room_id)
|
||||
if room == None:
|
||||
raise HTTPException(status_code = status.HTTP_404_NOT_FOUND, detail = 'Room does not exist ')
|
||||
return room_id
|
||||
|
||||
@router.websocket('/ws/{room_id}')
|
||||
async def room_ws(ws: WebSocket, room_id:str = Depends(check_room), ):
|
||||
consumer = RoomConsumer(ws, room_id, manager)
|
||||
await consumer.run()
|
58
backend/api_old/config.py
Normal file
58
backend/api_old/config.py
Normal file
@ -0,0 +1,58 @@
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
from database.decorators import as_form
|
||||
from database.auth.models import UserModel
|
||||
from database.exercices.models import Exercice, Tag
|
||||
from database.room.models import Room, Parcours, AnonymousMember
|
||||
from tortoise import Tortoise
|
||||
|
||||
|
||||
Tortoise.init_models(['database.exercices.models',
|
||||
'database.auth.models', "database.room.models"], "models")
|
||||
|
||||
Exercice_schema = pydantic_model_creator(Exercice, name="exercice", include=[
|
||||
"name", "tags", 'id_code', "consigne", 'pdfSupport', "csvSupport", 'examples'])
|
||||
|
||||
ExerciceIn_schema = pydantic_model_creator(
|
||||
Exercice, name="exerciceIn", exclude_readonly=True, exclude=['id_code', 'exo_source', "tags_id", 'author_id', 'origin'])
|
||||
|
||||
Exo_schema = pydantic_model_creator(Exercice, name="exerciceszzz", include=[
|
||||
'name', 'id_code', 'tags'])
|
||||
@as_form
|
||||
class ExerciceIn_form(ExerciceIn_schema):
|
||||
pass
|
||||
|
||||
|
||||
Tag_schema = pydantic_model_creator(Tag, name="tag", exclude=['owner', "id", ])
|
||||
TagIn_schema = pydantic_model_creator(
|
||||
Tag, name="tagIn", exclude_readonly=True, exclude=['owner_id'])
|
||||
|
||||
User_schema = pydantic_model_creator(UserModel, name='users', include=[
|
||||
'username', 'email', "name", "firstname"])
|
||||
UserIn_schema = pydantic_model_creator(
|
||||
UserModel, name='usersIn', exclude_readonly=True)
|
||||
|
||||
|
||||
@as_form
|
||||
class UserIn_Form(UserIn_schema):
|
||||
pass
|
||||
|
||||
|
||||
Room_schema = pydantic_model_creator(
|
||||
Room, name='room', include=["id", 'name', 'id_code'])
|
||||
|
||||
RoomIn_schema = pydantic_model_creator(Room, name='roomIn', exclude_readonly=True, exclude=[
|
||||
'created_at', 'online', 'id_code', 'users_waiters'])
|
||||
|
||||
Anonymous_schema = pydantic_model_creator(
|
||||
AnonymousMember, name='anonymousMember')
|
||||
AnonymousIn_schema = pydantic_model_creator(
|
||||
AnonymousMember, name='anonymousMemberIn', exclude_readonly=True, exclude=['id_code', 'room_id'])
|
||||
|
||||
Parcours_schema = pydantic_model_creator(Parcours, name='parcours')
|
||||
ParcoursIn_schema = pydantic_model_creator(
|
||||
Parcours, name='parcoursIn', exclude_readonly=True)
|
||||
|
||||
|
||||
SECRET_KEY = "6323081020d8939e6385dd688a26cbca0bb34ed91997959167637319ba4f6f3e"
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
0
backend/api_old/database/__init__.py
Normal file
0
backend/api_old/database/__init__.py
Normal file
40
backend/api_old/database/auth/crud.py
Normal file
40
backend/api_old/database/auth/crud.py
Normal file
@ -0,0 +1,40 @@
|
||||
from config import User_schema
|
||||
from database.exercices.crud import generate_unique_code
|
||||
from services.password import get_password_hash
|
||||
from .models import UserModel
|
||||
|
||||
|
||||
async def get_user_db(username):
|
||||
return await UserModel.get_or_none(username=username)
|
||||
|
||||
async def get_user_from_clientId_db(clientId):
|
||||
return await UserModel.get_or_none(clientId=clientId)
|
||||
|
||||
|
||||
async def create_user_db(username, password):
|
||||
#id_code = generate_unique_code(UserModel)
|
||||
return await UserModel.create(username=username, hashed_password=password)
|
||||
|
||||
async def disable_user_db(username):
|
||||
user =await UserModel.get(username=username)
|
||||
user_obj = await User_schema.from_tortoise_orm(user)
|
||||
await user.update_from_dict({**user_obj.dict(exclude_unset=True), 'disabled': True}).save()
|
||||
|
||||
return user
|
||||
|
||||
async def delete_user_db(username):
|
||||
user = await UserModel.get(username=username)
|
||||
await user.delete()
|
||||
|
||||
|
||||
async def update_user_db(username_id: str, **kwargs):
|
||||
user = await UserModel.get(username=username_id)
|
||||
await user.update_from_dict({**kwargs}).save()
|
||||
return user
|
||||
|
||||
async def update_password_db(username, password):
|
||||
print(username)
|
||||
user = await UserModel.get(username=username)
|
||||
|
||||
await user.update_from_dict({'hashed_password': get_password_hash(password)}).save(update_fields=["hashed_password"])
|
||||
return user
|
22
backend/api_old/database/auth/models.py
Normal file
22
backend/api_old/database/auth/models.py
Normal file
@ -0,0 +1,22 @@
|
||||
import uuid
|
||||
from tortoise.models import Model
|
||||
from tortoise import fields
|
||||
|
||||
|
||||
class UserModel(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
clientId = fields.UUIDField(unique=True, default = uuid.uuid4)
|
||||
username = fields.CharField(max_length = 100, unique=True)
|
||||
hashed_password = fields.CharField(max_length = 255)
|
||||
email = fields.CharField(null=True, max_length=255)
|
||||
|
||||
name = fields.CharField(null=True, max_length=255)
|
||||
firstname=fields.CharField(null=True, max_length=255)
|
||||
|
||||
disabled = fields.BooleanField(default=False)
|
||||
|
||||
class PydanticMeta:
|
||||
exclude=['hashed_password']
|
||||
|
||||
class Meta:
|
||||
table = "users"
|
BIN
backend/api_old/database/db.sqlite3
Normal file
BIN
backend/api_old/database/db.sqlite3
Normal file
Binary file not shown.
BIN
backend/api_old/database/db.sqlite3-shm
Normal file
BIN
backend/api_old/database/db.sqlite3-shm
Normal file
Binary file not shown.
BIN
backend/api_old/database/db.sqlite3-wal
Normal file
BIN
backend/api_old/database/db.sqlite3-wal
Normal file
Binary file not shown.
32
backend/api_old/database/decorators.py
Normal file
32
backend/api_old/database/decorators.py
Normal file
@ -0,0 +1,32 @@
|
||||
import inspect
|
||||
from typing import Type
|
||||
|
||||
from fastapi import Form
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import ModelField
|
||||
|
||||
def as_form(cls: Type[BaseModel]):
|
||||
new_parameters = []
|
||||
|
||||
for field_name, model_field in cls.__fields__.items():
|
||||
model_field: ModelField # type: ignore
|
||||
|
||||
new_parameters.append(
|
||||
inspect.Parameter(
|
||||
model_field.alias,
|
||||
inspect.Parameter.POSITIONAL_ONLY,
|
||||
default=Form(...) if model_field.required else Form(
|
||||
model_field.default),
|
||||
annotation=model_field.outer_type_,
|
||||
)
|
||||
)
|
||||
|
||||
async def as_form_func(**data):
|
||||
return cls(**data)
|
||||
|
||||
sig = inspect.signature(as_form_func)
|
||||
sig = sig.replace(parameters=new_parameters)
|
||||
as_form_func.__signature__ = sig # type: ignore
|
||||
setattr(cls, 'as_form', as_form_func)
|
||||
return cls
|
||||
|
102
backend/api_old/database/exercices/crud.py
Normal file
102
backend/api_old/database/exercices/crud.py
Normal file
@ -0,0 +1,102 @@
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import string
|
||||
from typing import List
|
||||
from config import Exercice_schema, TagIn_schema
|
||||
from services.io import get_abs_path_from_relative_to_root, get_ancestor, get_parent_dir, remove_fastapi_root, remove_if_exists, get_or_create_dir
|
||||
from tortoise import Model
|
||||
from generateur.generateur_main import Generateur
|
||||
from .models import Exercice, Tag
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async def create_exo_db(*args, **kwargs) -> Exercice:
|
||||
code = await generate_unique_code(Exercice)
|
||||
return await Exercice.create(*args, **{**kwargs, 'id_code': code})
|
||||
|
||||
|
||||
async def delete_exo_db(id_code: str):
|
||||
exo = await Exercice.get(id_code=id_code)
|
||||
path = get_abs_path_from_relative_to_root( exo.exo_source)
|
||||
|
||||
parent = get_parent_dir(path)
|
||||
|
||||
shutil.rmtree(parent)
|
||||
|
||||
return await exo.delete()
|
||||
|
||||
async def update_exo_db(id_code: str, **kwargs) -> Exercice:
|
||||
exo = await Exercice.get(id_code=id_code)
|
||||
path = get_abs_path_from_relative_to_root(exo.exo_source)
|
||||
|
||||
remove_if_exists(path)
|
||||
|
||||
await exo.update_from_dict({**kwargs, 'origin_id': exo.origin_id, 'isOriginal': exo.isOriginal}).save()
|
||||
return exo
|
||||
|
||||
|
||||
#flag
|
||||
async def get_or_create_tag(id_code: str, data: List[TagIn_schema]):
|
||||
tag = await Tag.get_or_none(id_code=id_code)
|
||||
if tag == None:
|
||||
code = await generate_unique_code(Tag)
|
||||
return await Tag.create(**{**data,'id_code': code})
|
||||
return tag
|
||||
|
||||
async def add_or_remove_tag(exo_id_code: str, tag: Tag):
|
||||
exo = await Exercice.get(id_code = exo_id_code)
|
||||
is_present = await exo.tags.all().get_or_none(id_code=tag.id_code)
|
||||
if is_present == None:
|
||||
await exo.tags.add(tag)
|
||||
else:
|
||||
await exo.tags.remove(tag)
|
||||
return exo
|
||||
|
||||
async def add_tag_db(id_code: str, tags_data: List[TagIn_schema], user_id:int) -> Exercice:
|
||||
exo = await Exercice.get(id_code = id_code)
|
||||
for t in tags_data:
|
||||
tag = await get_or_create_tag(t.id_code, {**t.dict(exclude_unset=True), 'owner_id': user_id})
|
||||
await exo.tags.add(tag)
|
||||
return exo
|
||||
|
||||
async def delete_tag_db(exo_id: str, tag_id: str) -> Exercice:
|
||||
exo = await Exercice.get(id_code=exo_id)
|
||||
tag = await exo.tags.all().get(id_code=tag_id)
|
||||
await exo.tags.remove(tag)
|
||||
return exo
|
||||
|
||||
|
||||
def clone_exo_source(path, id_code):
|
||||
upload_root = get_ancestor(path, 2)
|
||||
path = get_abs_path_from_relative_to_root(path)
|
||||
new_path = get_abs_path_from_relative_to_root(os.path.join(upload_root, id_code))
|
||||
get_or_create_dir((new_path))
|
||||
|
||||
return remove_fastapi_root(shutil.copy(path, new_path))
|
||||
|
||||
|
||||
async def clone_exo_db(id_code:str, user_id):
|
||||
exo = await Exercice.get(id_code=id_code)
|
||||
new_id_code = await generate_unique_code(Exercice)
|
||||
|
||||
exo_obj = await Exercice_schema.from_tortoise_orm(exo)
|
||||
exo_obj = exo_obj.dict(exclude_unset=True)
|
||||
exo_obj.pop('tags')
|
||||
exo_obj.pop('exercices')
|
||||
|
||||
path = clone_exo_source(exo.exo_source, new_id_code)
|
||||
|
||||
new_exo = Exercice(**{**exo_obj, 'id_code': new_id_code, 'exo_source': path,
|
||||
"isOriginal": False, 'author_id': user_id}, origin_id=exo.id)
|
||||
|
||||
await new_exo.save()
|
||||
return new_exo
|
||||
|
||||
async def get_exo_source_path(id_code: str):
|
||||
exo = await Exercice.get(id_code=id_code)
|
||||
path = get_abs_path_from_relative_to_root(exo.exo_source)
|
||||
return path
|
62
backend/api_old/database/exercices/customField.py
Normal file
62
backend/api_old/database/exercices/customField.py
Normal file
@ -0,0 +1,62 @@
|
||||
import os
|
||||
import typing
|
||||
import uuid
|
||||
from fastapi import UploadFile
|
||||
from tortoise.fields import TextField
|
||||
from tortoise import ConfigurationError
|
||||
import io
|
||||
from services.io import delete_root_slash, get_abs_path_from_relative_to_root, get_filename_from_path, get_or_create_dir, is_binary_file, get_filename, remove_fastapi_root
|
||||
|
||||
|
||||
|
||||
|
||||
class FileField(TextField):
|
||||
def __init__(self, *, upload_root: str, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.upload_root = delete_root_slash(upload_root)
|
||||
|
||||
self.upload_root = get_or_create_dir(os.path.join(os.environ.get('FASTAPI_ROOT_URL'), self.upload_root))
|
||||
|
||||
|
||||
def _is_binary(self, file: UploadFile):
|
||||
return isinstance(file, io.BytesIO)
|
||||
|
||||
def to_db_value(self, value: typing.IO, instance):
|
||||
if not isinstance(value, str):
|
||||
print(instance)
|
||||
super().validate(value)
|
||||
value.seek(0)
|
||||
is_binary = self._is_binary(value)
|
||||
name = get_filename(value)
|
||||
|
||||
parent = get_or_create_dir(os.path.join(self.upload_root, instance.id_code))
|
||||
|
||||
mode = 'w+' if not is_binary else 'wb+'
|
||||
|
||||
path = os.path.join(self.upload_root, parent, name)
|
||||
|
||||
with open(path, mode) as f:
|
||||
f.write(value.read())
|
||||
|
||||
return remove_fastapi_root(path)
|
||||
return value
|
||||
|
||||
def to_python_value(self, value: str):
|
||||
|
||||
if not self._is_binary:
|
||||
if is_binary_file(value):
|
||||
mode = 'rb'
|
||||
buffer = io.BytesIO()
|
||||
else:
|
||||
mode = 'r'
|
||||
buffer = io.StringIO()
|
||||
|
||||
buffer.name =get_filename_from_path(value)
|
||||
|
||||
with open(get_abs_path_from_relative_to_root(value), mode) as f:
|
||||
buffer.write(f.read())
|
||||
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
return value
|
||||
|
106
backend/api_old/database/exercices/models.py
Normal file
106
backend/api_old/database/exercices/models.py
Normal file
@ -0,0 +1,106 @@
|
||||
import asyncio
|
||||
from io import BytesIO
|
||||
import io
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
|
||||
from tortoise.models import Model
|
||||
from tortoise import fields
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
import async_to_sync as sync
|
||||
from tortoise.manager import Manager
|
||||
from generateur.generateur_main import Generateur
|
||||
|
||||
from services.io import get_abs_path_from_relative_to_root
|
||||
|
||||
from .validators import ExoSourceValidator, get_support_compatibility_for_exo_source_from_path, get_support_compatibility_for_exo_source_from_data
|
||||
|
||||
from .customField import FileField
|
||||
|
||||
|
||||
class Tag(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
id_code = fields.CharField(unique=True, default="", max_length=15)
|
||||
|
||||
name = fields.CharField(max_length=35)
|
||||
color = fields.CharField(max_length=100)
|
||||
|
||||
owner = fields.ForeignKeyField('models.UserModel')
|
||||
|
||||
|
||||
class Exercice(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
id_code = fields.CharField(default="", max_length=10, unique=True)
|
||||
|
||||
name = fields.CharField(max_length=50)
|
||||
|
||||
consigne = fields.CharField(max_length=200, null=True, default="")
|
||||
|
||||
exo_source = FileField(upload_root="/uploads",
|
||||
validators=[ExoSourceValidator()])
|
||||
|
||||
updated_at = fields.DatetimeField(auto_now=True)
|
||||
|
||||
private = fields.BooleanField(default=False)
|
||||
|
||||
tags = fields.ManyToManyField('models.Tag', null=True)
|
||||
|
||||
origin = fields.ForeignKeyField('models.Exercice', null = True)
|
||||
isOriginal = fields.BooleanField(default = True)
|
||||
|
||||
author = fields.ForeignKeyField('models.UserModel')
|
||||
|
||||
def pdfSupport(self) -> bool:
|
||||
if not isinstance(self.exo_source, io.BytesIO):
|
||||
if os.path.exists(get_abs_path_from_relative_to_root(self.exo_source)):
|
||||
support_compatibility = get_support_compatibility_for_exo_source_from_path(
|
||||
get_abs_path_from_relative_to_root(self.exo_source))
|
||||
return support_compatibility['isPdf']
|
||||
return False
|
||||
else:
|
||||
self.exo_source.seek(0)
|
||||
support_compatibility = get_support_compatibility_for_exo_source_from_data(
|
||||
self.exo_source.read())
|
||||
return support_compatibility['isPdf']
|
||||
|
||||
def csvSupport(self) -> bool:
|
||||
if not isinstance(self.exo_source, io.BytesIO):
|
||||
if os.path.exists(get_abs_path_from_relative_to_root(self.exo_source)):
|
||||
support_compatibility = get_support_compatibility_for_exo_source_from_path(
|
||||
get_abs_path_from_relative_to_root(self.exo_source))
|
||||
return support_compatibility['isCsv']
|
||||
return False
|
||||
else:
|
||||
self.exo_source.seek(0)
|
||||
support_compatibility = get_support_compatibility_for_exo_source_from_data(
|
||||
self.exo_source.read())
|
||||
return support_compatibility['isCsv']
|
||||
|
||||
def webSupport(self) -> bool:
|
||||
|
||||
if not isinstance(self.exo_source, io.BytesIO):
|
||||
if os.path.exists(get_abs_path_from_relative_to_root(self.exo_source)):
|
||||
support_compatibility = get_support_compatibility_for_exo_source_from_path(
|
||||
get_abs_path_from_relative_to_root(self.exo_source))
|
||||
return support_compatibility['isWeb']
|
||||
return False
|
||||
else:
|
||||
self.exo_source.seek(0)
|
||||
support_compatibility = get_support_compatibility_for_exo_source_from_data(
|
||||
self.exo_source.read())
|
||||
return support_compatibility['isWeb']
|
||||
|
||||
def examples(self) -> dict:
|
||||
if not isinstance(self.exo_source, io.BytesIO):
|
||||
return {
|
||||
"type": "Csv" if self.csvSupport() else "web" if self.webSupport() else None,
|
||||
"data": Generateur(get_abs_path_from_relative_to_root(self.exo_source), 3, "csv" if self.csvSupport() else "web" if self.pdfSupport() else None, True) if self.csvSupport() == True or self.webSupport() == True else None
|
||||
}
|
||||
return {}
|
||||
|
||||
class PydanticMeta:
|
||||
computed = ["pdfSupport", "csvSupport", "webSupport", 'examples']
|
||||
exclude = ["exo_source", 'id', "exercices"]
|
||||
|
||||
|
93
backend/api_old/database/exercices/validators.py
Normal file
93
backend/api_old/database/exercices/validators.py
Normal file
@ -0,0 +1,93 @@
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
import typing
|
||||
from tortoise.validators import Validator
|
||||
from tortoise.exceptions import ValidationError
|
||||
import importlib.util
|
||||
import types
|
||||
from services.timeout import timeout
|
||||
|
||||
from .customField import is_binary_file
|
||||
|
||||
|
||||
def checkExoSupportCompatibility(obj):
|
||||
isPdf = False if (obj['pdf'] == None or (
|
||||
obj['calcul'] == False and obj['pdf'] == False)) else True
|
||||
|
||||
isCsv = False if (obj['csv'] == None or (
|
||||
obj['calcul'] == False and obj['csv'] == False)) else True
|
||||
|
||||
isWeb = False if (obj['web'] == None or (
|
||||
obj['calcul'] == False and obj['web'] == False)) else True
|
||||
|
||||
return {
|
||||
'isPdf': isPdf, 'isCsv': isCsv, 'isWeb': isWeb}
|
||||
|
||||
def get_module_from_string(value: str) -> types.ModuleType:
|
||||
spec = types.ModuleType('exo')
|
||||
try:
|
||||
exec(value, spec.__dict__)
|
||||
except Exception as err:
|
||||
raise ValidationError(f'[Error] : {err}')
|
||||
|
||||
return spec
|
||||
|
||||
|
||||
|
||||
def execute_main_if_present(spec):
|
||||
try:
|
||||
return spec.main()
|
||||
except AttributeError as atrerror:
|
||||
raise ValidationError(f"[Error] : function 'main' is missing")
|
||||
except Exception as e:
|
||||
raise ValidationError(f'[Error] : {e}')
|
||||
|
||||
def get_spec_with_timeout(data, time):
|
||||
with timeout(time, ValidationError('[Error] : Script took too long')):
|
||||
return get_module_from_string(data)
|
||||
|
||||
def fill_empty_values(object):
|
||||
default_object = {"calcul": False, 'pdf': False, 'csv': False,
|
||||
'web': False, 'correction': False}
|
||||
return {**default_object, **object}
|
||||
|
||||
|
||||
|
||||
def get_support_compatibility_for_exo_source_from_data(data: str):
|
||||
|
||||
spec = get_spec_with_timeout(data, 5)
|
||||
result = execute_main_if_present(spec)
|
||||
|
||||
|
||||
result = fill_empty_values(result)
|
||||
|
||||
exo_supports_compatibility = checkExoSupportCompatibility(result)
|
||||
return exo_supports_compatibility
|
||||
|
||||
|
||||
def get_support_compatibility_for_exo_source_from_path(path):
|
||||
if not os.path.exists(path):
|
||||
raise ValidationError('[Error] : No such file or directory')
|
||||
is_binary = is_binary_file(path)
|
||||
|
||||
if is_binary:
|
||||
mode = 'rb'
|
||||
else:
|
||||
mode = 'r'
|
||||
with open(path, mode) as f:
|
||||
data = f.read() if mode == "r" else f.read().decode('utf8')
|
||||
return get_support_compatibility_for_exo_source_from_data(data)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ExoSourceValidator(Validator):
|
||||
"""
|
||||
A validator to validate ...
|
||||
"""
|
||||
def __call__(self, value: typing.IO):
|
||||
exo_supports_compatibility = get_support_compatibility_for_exo_source_from_data(
|
||||
value.read())
|
||||
if not exo_supports_compatibility['isPdf'] and not exo_supports_compatibility['isCsv'] and not exo_supports_compatibility['isWeb']:
|
||||
raise ValidationError('[Error] : Exercice non valide (compatible avec aucun support)')
|
79
backend/api_old/database/main.py
Normal file
79
backend/api_old/database/main.py
Normal file
@ -0,0 +1,79 @@
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException
|
||||
|
||||
import backend.api.database.exercices.crud as crud, database, backend.api.database.exercices.models as models, schemas
|
||||
from database import db_state_default
|
||||
|
||||
database.db.connect()
|
||||
database.db.create_tables([models.User, models.Item])
|
||||
database.db.close()
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
sleep_time = 10
|
||||
|
||||
|
||||
async def reset_db_state():
|
||||
database.db._state._state.set(db_state_default.copy())
|
||||
database.db._state.reset()
|
||||
|
||||
|
||||
def get_db(db_state=Depends(reset_db_state)):
|
||||
try:
|
||||
database.db.connect()
|
||||
yield
|
||||
finally:
|
||||
if not database.db.is_closed():
|
||||
database.db.close()
|
||||
|
||||
|
||||
@app.post("/users/", response_model=schemas.User, dependencies=[Depends(get_db)])
|
||||
def create_user(user: schemas.UserCreate):
|
||||
db_user = crud.get_user_by_email(email=user.email)
|
||||
if db_user:
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
return crud.create_user(user=user)
|
||||
|
||||
|
||||
@app.get("/users/", response_model=List[schemas.User], dependencies=[Depends(get_db)])
|
||||
def read_users(skip: int = 0, limit: int = 100):
|
||||
users = crud.get_users(skip=skip, limit=limit)
|
||||
return users
|
||||
|
||||
|
||||
@app.get(
|
||||
"/users/{user_id}", response_model=schemas.User, dependencies=[Depends(get_db)]
|
||||
)
|
||||
def read_user(user_id: int):
|
||||
db_user = crud.get_user(user_id=user_id)
|
||||
if db_user is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return db_user
|
||||
|
||||
|
||||
@app.post(
|
||||
"/users/{user_id}/items/",
|
||||
response_model=schemas.Item,
|
||||
dependencies=[Depends(get_db)],
|
||||
)
|
||||
def create_item_for_user(user_id: int, item: schemas.ItemCreate):
|
||||
return crud.create_user_item(item=item, user_id=user_id)
|
||||
|
||||
|
||||
@app.get("/items/", response_model=List[schemas.Item], dependencies=[Depends(get_db)])
|
||||
def read_items(skip: int = 0, limit: int = 100):
|
||||
items = crud.get_items(skip=skip, limit=limit)
|
||||
return items
|
||||
|
||||
|
||||
@app.get(
|
||||
"/slowusers/", response_model=List[schemas.User], dependencies=[Depends(get_db)]
|
||||
)
|
||||
def read_slow_users(skip: int = 0, limit: int = 100):
|
||||
global sleep_time
|
||||
sleep_time = max(0, sleep_time - 1)
|
||||
time.sleep(sleep_time) # Fake long processing request
|
||||
users = crud.get_users(skip=skip, limit=limit)
|
||||
return users
|
90
backend/api_old/database/room/crud.py
Normal file
90
backend/api_old/database/room/crud.py
Normal file
@ -0,0 +1,90 @@
|
||||
from config import AnonymousIn_schema, RoomIn_schema, User_schema
|
||||
from database.auth.models import UserModel
|
||||
from .models import AnonymousMember, Room, RoomOwner, Waiter
|
||||
from database.exercices.crud import generate_unique_code
|
||||
|
||||
|
||||
|
||||
async def create_room_with_user_db(room: RoomIn_schema, user: User_schema):
|
||||
code = await generate_unique_code(Room)
|
||||
|
||||
room_obj = await Room.create(**room.dict(exclude_unset=True), id_code=code)
|
||||
|
||||
user = await UserModel.get(id=user.id)
|
||||
await room_obj.users.add(user)
|
||||
await RoomOwner.create(room_id=room_obj.id, user_id=user.id)
|
||||
return room_obj
|
||||
|
||||
async def create_room_anonymous_db(room: RoomIn_schema, anonymous: AnonymousIn_schema):
|
||||
code = await generate_unique_code(Room)
|
||||
|
||||
room_obj = await Room.create(**room.dict(exclude_unset=True), id_code=code)
|
||||
|
||||
anonymous_code = await generate_unique_code(AnonymousMember)
|
||||
anonymous = await AnonymousMember.create(**anonymous.dict(exclude_unset=True), id_code=anonymous_code, room_id=room_obj.id)
|
||||
|
||||
await RoomOwner.create(room_id=room_obj.id, anonymous_id=anonymous.id)
|
||||
return room_obj
|
||||
|
||||
|
||||
async def get_room_db(room_id:str):
|
||||
room = await Room.get_or_none(id_code=room_id)
|
||||
return room
|
||||
|
||||
|
||||
|
||||
async def check_user_in_room(room: Room, user: UserModel):
|
||||
return await room.users.filter(id=user.id).count() != 0
|
||||
|
||||
async def get_member_by_code(room: Room, code: str):
|
||||
anonymous = await room.anonymousmembers
|
||||
filtered_anonymous = [
|
||||
m for m in anonymous if m.id_code == code]
|
||||
if len(filtered_anonymous) == 0:
|
||||
return None
|
||||
return filtered_anonymous[0]
|
||||
|
||||
async def check_user_owner(room: Room, user: UserModel):
|
||||
room_owner = await room.room_owner
|
||||
user_owner = await room_owner.user
|
||||
if user_owner == None:
|
||||
return False
|
||||
return user_owner.id == user.id
|
||||
|
||||
async def check_anonymous_owner(room: Room, anonymous: AnonymousMember):
|
||||
room_owner = await room.room_owner
|
||||
anonymous_owner = await room_owner.anonymous
|
||||
if anonymous_owner == None:
|
||||
return False
|
||||
return anonymous_owner.id_code == anonymous.id_code
|
||||
|
||||
|
||||
|
||||
async def create_waiter_by_user(room: Room, user: UserModel ):
|
||||
code = await generate_unique_code(Waiter)
|
||||
return await Waiter.create(room_id=room.id, user_id=user.id, name=user.username, id_code=code)
|
||||
|
||||
async def create_waiter_anonymous(room: Room, name: str):
|
||||
code = await generate_unique_code(Waiter)
|
||||
return await Waiter.create(name=name, id_code=code, room_id=room.id)
|
||||
|
||||
async def connect_room(room: Room, code):
|
||||
online = room.online
|
||||
await room.update_from_dict({'online': [*online, code]}).save(update_fields=['online'])
|
||||
return
|
||||
async def disconnect_room(room: Room, code):
|
||||
online = room.online
|
||||
await room.update_from_dict({'online': [o for o in online if o!=code]}).save(update_fields=['online'])
|
||||
return
|
||||
|
||||
async def validate_name_in_room(room: Room, name):
|
||||
anonymous = await room.anonymousmembers
|
||||
if len([a for a in anonymous if a == name]) != 0:
|
||||
return "Pseudo déjà utilisé"
|
||||
if len(name) < 3:
|
||||
return "Pseudo trop court"
|
||||
if len(name) > 30:
|
||||
return "Pseudo trop long"
|
||||
return True
|
||||
|
||||
|
72
backend/api_old/database/room/models.py
Normal file
72
backend/api_old/database/room/models.py
Normal file
@ -0,0 +1,72 @@
|
||||
from email.policy import default
|
||||
from tortoise.models import Model
|
||||
from tortoise import fields
|
||||
|
||||
|
||||
class Room(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
id_code = fields.CharField(max_length=30, unique=True)
|
||||
|
||||
name = fields.CharField(max_length=255)
|
||||
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
|
||||
public_result = fields.BooleanField(default=False)
|
||||
private = fields.BooleanField(default = True)
|
||||
|
||||
online = fields.JSONField(default = list)
|
||||
|
||||
users = fields.ManyToManyField('models.UserModel')
|
||||
|
||||
class PydanticMeta:
|
||||
exlude=('users__email')
|
||||
|
||||
class AnonymousMember(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
id_code = fields.CharField(max_length=30, unique=True)
|
||||
|
||||
name = fields.CharField(max_length=255)
|
||||
|
||||
room = fields.ForeignKeyField('models.Room')
|
||||
|
||||
class PydanticMeta:
|
||||
exclude=['room_owner', "id_code", 'id', 'challenger']
|
||||
|
||||
|
||||
class Waiter(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
name = fields.CharField(max_length=255)
|
||||
id_code = fields.CharField(max_length=30, unique=True)
|
||||
room = fields.ForeignKeyField('models.Room')
|
||||
user = fields.ForeignKeyField("models.UserModel", null=True)
|
||||
|
||||
class RoomOwner(Model):
|
||||
user = fields.ForeignKeyField('models.UserModel', null = True)
|
||||
anonymous = fields.OneToOneField('models.AnonymousMember', null=True)
|
||||
room = fields.OneToOneField('models.Room', null=True)
|
||||
class Meta:
|
||||
table='room_owner'
|
||||
|
||||
|
||||
class Parcours(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
id_code = fields.CharField(max_length=30, unique=True)
|
||||
|
||||
name = fields.CharField(max_length=255)
|
||||
created_at = fields.DateField(auto_now_add=True)
|
||||
|
||||
room = fields.ForeignKeyField('models.Room')
|
||||
|
||||
timer = fields.IntField(default=10)
|
||||
|
||||
exercices = fields.JSONField(default=list)
|
||||
|
||||
success_condition = fields.IntField(default = 10)
|
||||
|
||||
|
||||
class Challenger(Model):
|
||||
parcours = fields.ForeignKeyField('models.Parcours')
|
||||
anonymous = fields.OneToOneField('models.AnonymousMember', null=True)
|
||||
user = fields.OneToOneField("models.UserModel", null=True)
|
||||
|
||||
challenges = fields.JSONField(default=list)
|
BIN
backend/api_old/db.sqlite3
Normal file
BIN
backend/api_old/db.sqlite3
Normal file
Binary file not shown.
72
backend/api_old/generateur/generateur_csv.py
Normal file
72
backend/api_old/generateur/generateur_csv.py
Normal file
@ -0,0 +1,72 @@
|
||||
from .generateur_main import Generateur
|
||||
PAGE_LINES = {
|
||||
10: 53,
|
||||
12: 49,
|
||||
14: 39,
|
||||
16: 34,
|
||||
18: 31
|
||||
}
|
||||
MAX_LENGTH = {
|
||||
10: 38,
|
||||
12: 32,
|
||||
14: 25,
|
||||
16: 23,
|
||||
18: 20
|
||||
}
|
||||
|
||||
|
||||
def Csv_generator(path, nb_in_serie, nb_page, police, consigne, writer):
|
||||
exo_exemple = Generateur(path, 1, 'csv')
|
||||
if len(consigne) < MAX_LENGTH[police] and len(consigne) > len(exo_exemple):
|
||||
longueur_max = len(consigne) + 5
|
||||
elif len(consigne) > MAX_LENGTH[police] and len(consigne) > len(exo_exemple):
|
||||
longueur_max = MAX_LENGTH[police]
|
||||
elif len(consigne) > MAX_LENGTH[police] and len(consigne) < len(exo_exemple):
|
||||
longueur_max = len(exo_exemple)
|
||||
elif len(consigne) < MAX_LENGTH[police] and len(consigne) < len(exo_exemple):
|
||||
longueur_max = len(exo_exemple)
|
||||
else:
|
||||
longueur_max = len(exo_exemple)
|
||||
|
||||
consigne_lines = []
|
||||
if len(consigne) > 30:
|
||||
cons = consigne.replace(',', ' ').split(' ')
|
||||
text_longueur = ''
|
||||
for i in cons:
|
||||
text_longueur = text_longueur + i + ' '
|
||||
if len(text_longueur) > longueur_max:
|
||||
consigne_lines.append(text_longueur)
|
||||
text_longueur = ''
|
||||
# print(text_longueur)
|
||||
else:
|
||||
consigne_lines.append(consigne)
|
||||
serie_page_vertical = int(PAGE_LINES[police] /
|
||||
(nb_in_serie + 1 + len(consigne_lines)))
|
||||
|
||||
rest_line = PAGE_LINES[police] - (serie_page_vertical * nb_in_serie +
|
||||
serie_page_vertical * len(consigne_lines) + serie_page_vertical)
|
||||
|
||||
max_length = len(exo_exemple) if len(
|
||||
exo_exemple) > longueur_max else longueur_max
|
||||
max_in_line = 2 * MAX_LENGTH[police]
|
||||
space = max_in_line / 8
|
||||
|
||||
nb_in_line = int(max_in_line / (max_length + space)) + 1
|
||||
|
||||
for p in range(nb_page):
|
||||
for c in range(serie_page_vertical):
|
||||
|
||||
for w in consigne_lines:
|
||||
writer.writerow([*[w, ""] * nb_in_line])
|
||||
|
||||
for k in range(nb_in_serie):
|
||||
calcul_list = list(
|
||||
map(lambda calc: calc['calcul'], Generateur(path, nb_in_line, 'csv')))
|
||||
n = 1
|
||||
for i in range(n, len(calcul_list) + n + 1, n+1):
|
||||
calcul_list.insert(i, '')
|
||||
writer.writerow(calcul_list)
|
||||
writer.writerow([''])
|
||||
|
||||
for r in range(rest_line):
|
||||
writer.writerow([''])
|
49
backend/api_old/generateur/generateur_main.py
Normal file
49
backend/api_old/generateur/generateur_main.py
Normal file
@ -0,0 +1,49 @@
|
||||
import re
|
||||
import importlib.util
|
||||
|
||||
|
||||
def getObjectKey(obj, key):
|
||||
if obj[key] == None:
|
||||
return None
|
||||
return key if obj[key] != False else 'calcul' if obj['calcul'] != False else None
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def parseCorrection(calc, replacer='...'):
|
||||
exp_list = re.findall(r"\[([A-Za-z0-9_]+)\]", calc)
|
||||
for exp in exp_list:
|
||||
calc = calc.replace(f'[{exp}]', replacer)
|
||||
return calc
|
||||
|
||||
|
||||
def Generateur(path, quantity, key, forcedCorrection=False):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"tmp", path)
|
||||
tmp = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(tmp)
|
||||
try:
|
||||
main_func = tmp.main
|
||||
except:
|
||||
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
|
||||
print(main_result)
|
||||
result_object = {**default_object, **main_result}
|
||||
object_key = getObjectKey(result_object, key)
|
||||
correction_key = getCorrectionKey(result_object, key)
|
||||
op_list = []
|
||||
try:
|
||||
replacer = tmp.CORRECTION_REPLACER
|
||||
except:
|
||||
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]})
|
||||
return op_list
|
135
backend/api_old/index.html
Normal file
135
backend/api_old/index.html
Normal file
@ -0,0 +1,135 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Chat</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebSocket Room</h1>
|
||||
|
||||
<section id="connection">
|
||||
<h2>Connection</h2>
|
||||
<form action="" onsubmit="login(event)">
|
||||
<input type="text" placeholder="Username..." id="username" />
|
||||
<input type="text" placeholder="Password..." id="password" />
|
||||
<button>Se connecter</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Room</h2>
|
||||
|
||||
<section>
|
||||
<h3>Create or Join</h3>
|
||||
|
||||
<form action="" onsubmit="join(event)">
|
||||
<input type="text" id="room_code" />
|
||||
<input type="text" id="name" />
|
||||
<input type="text" id="reco" />
|
||||
<button>join</button>
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<p>Members</p>
|
||||
<u id="members"></u>
|
||||
|
||||
<p>Waiters</p>
|
||||
<u id="waiters"></u>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
let token = null;
|
||||
async function login(e) {
|
||||
e.preventDefault();
|
||||
var username = document.getElementById("username").value;
|
||||
var password = document.getElementById("password").value;
|
||||
var form = new FormData();
|
||||
form.append("username", username);
|
||||
form.append("password", password);
|
||||
var data = new URLSearchParams(form);
|
||||
|
||||
token = await fetch("http://localhost:8001/login", {
|
||||
method: "post",
|
||||
body: data,
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
})
|
||||
.then((res) => {
|
||||
return res.json();
|
||||
})
|
||||
.then((r) => {
|
||||
var token = r["access_token"];
|
||||
var connect_section = document.getElementById("connection");
|
||||
var p = document.createElement("p");
|
||||
var name = JSON.parse(atob(r["access_token"].split(".")[1]))["sub"];
|
||||
name = document.createTextNode(`Connected as ${name}`);
|
||||
|
||||
connect_section.appendChild(p);
|
||||
connect_section.appendChild(name);
|
||||
return token;
|
||||
});
|
||||
}
|
||||
|
||||
function join(e) {
|
||||
e.preventDefault();
|
||||
var room_code = document.getElementById("room_code").value;
|
||||
ws = new WebSocket(`ws://localhost:8001/ws/${room_code}`, [], {
|
||||
headers: { Authorization: "Bearer " + token },
|
||||
});
|
||||
|
||||
ws.onmessage = (msg) => {
|
||||
var type = JSON.parse(msg.data)["type"];
|
||||
var data = JSON.parse(msg.data)["data"];
|
||||
console.log("TYPE", type, type == "add_waiter");
|
||||
if (type == "accept") {
|
||||
if (token == null) {
|
||||
var name = document.getElementById("name").value;
|
||||
var reco = document.getElementById("reco").value;
|
||||
if (name == "") {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "login",
|
||||
data: { relogin_code: reco },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
ws.send(
|
||||
JSON.stringify({ type: "login", data: { name: name } })
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: "auth", data: { token: token } }));
|
||||
}
|
||||
}
|
||||
if (type == "auth_success") {
|
||||
ws.send(JSON.stringify({ type: "login", data: {} }));
|
||||
}
|
||||
if (type == "auth_failed") {
|
||||
ws.send(
|
||||
JSON.stringify({ type: "login", data: { name: "test_name" } })
|
||||
);
|
||||
}
|
||||
if (type == "add_waiter") {
|
||||
var name = data["name"];
|
||||
var id = data["id"];
|
||||
name = document.createTextNode(name);
|
||||
|
||||
var waiter = document.getElementById("waiters");
|
||||
var li = document.createElement("li");
|
||||
var btn = document.createElement("button");
|
||||
waiter.appendChild(li);
|
||||
li.appendChild(name);
|
||||
li.addEventListener("click", () => {
|
||||
console.log("TET");
|
||||
ws.send(
|
||||
JSON.stringify({ type: "accept_waiter", data: { id: id } })
|
||||
);
|
||||
});
|
||||
}
|
||||
if (type == "log_waiter") {
|
||||
ws.send(JSON.stringify({ type: "log_waiter", data: {} }));
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
127
backend/api_old/main.py
Normal file
127
backend/api_old/main.py
Normal file
@ -0,0 +1,127 @@
|
||||
from fastapi_pagination import add_pagination
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from fastapi.exceptions import RequestValidationError, ValidationError
|
||||
from datetime import timedelta
|
||||
from fastapi import FastAPI, HTTPException, Depends, Request, status
|
||||
from fastapi_jwt_auth import AuthJWT
|
||||
from fastapi_jwt_auth.exceptions import AuthJWTException
|
||||
from fastapi.responses import JSONResponse
|
||||
from typing import List
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from tortoise import Tortoise
|
||||
from database.exercices.models import Exercice
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from tortoise.contrib.fastapi import register_tortoise
|
||||
from pydantic import BaseModel
|
||||
import apis.base
|
||||
import config
|
||||
from redis import Redis
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
app = FastAPI(title="Tortoise ORM FastAPI example")
|
||||
origins = [
|
||||
"http://localhost:8000",
|
||||
"https://localhost:8001",
|
||||
"http://localhost",
|
||||
"http://localhost:8080",
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=['*'],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
@app.exception_handler(ValidationError)
|
||||
async def validation_exception_handler(request, exc: RequestValidationError):
|
||||
errors = {}
|
||||
for e in exc.errors():
|
||||
errors[e['loc'][-1] + "_error"] = e['msg']
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content=jsonable_encoder({"detail": errors}),
|
||||
)
|
||||
|
||||
|
||||
class Settings(BaseModel):
|
||||
authjwt_secret_key: str = config.SECRET_KEY
|
||||
authjwt_denylist_enabled: bool = True
|
||||
authjwt_denylist_token_checks: set = {"access", "refresh"}
|
||||
access_expires: int = timedelta(minutes=15 )
|
||||
refresh_expires: int = timedelta(days=30)
|
||||
|
||||
# callback to get your configuration
|
||||
|
||||
settings = Settings()
|
||||
@AuthJWT.load_config
|
||||
def get_config():
|
||||
return settings
|
||||
|
||||
|
||||
# exception handler for authjwt
|
||||
# in production, you can tweak performance using orjson response
|
||||
@app.exception_handler(AuthJWTException)
|
||||
def authjwt_exception_handler(request: Request, exc: AuthJWTException):
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"detail": exc.message}
|
||||
)
|
||||
|
||||
|
||||
redis_conn = Redis(host='localhost', port=6379, db=0, decode_responses=True)
|
||||
|
||||
|
||||
@AuthJWT.token_in_denylist_loader
|
||||
def check_if_token_in_denylist(decrypted_token):
|
||||
jti = decrypted_token['jti']
|
||||
entry = redis_conn.get(jti)
|
||||
return entry and entry == 'true'
|
||||
|
||||
app.include_router(apis.base.api_router)
|
||||
|
||||
|
||||
@app.delete('/access-revoke')
|
||||
def access_revoke(Authorize: AuthJWT = Depends()):
|
||||
Authorize.jwt_required()
|
||||
|
||||
# Store the tokens in redis with the value true for revoked.
|
||||
# We can also set an expires time on these tokens in redis,
|
||||
# so they will get automatically removed after they expired.
|
||||
jti = Authorize.get_raw_jwt()['jti']
|
||||
redis_conn.setex(jti, settings.access_expires, 'true')
|
||||
return {"detail": "Access token has been revoke"}
|
||||
|
||||
|
||||
@app.delete('/refresh-revoke')
|
||||
def refresh_revoke(Authorize: AuthJWT = Depends()):
|
||||
Authorize.jwt_refresh_token_required()
|
||||
|
||||
jti = Authorize.get_raw_jwt()['jti']
|
||||
redis_conn.setex(jti, settings.refresh_expires, 'true')
|
||||
return {"detail": "Refresh token has been revoke"}
|
||||
add_pagination(app)
|
||||
|
||||
TORTOISE_ORM = {
|
||||
"connections": {"default": "sqlite://database/db.sqlite3"},
|
||||
"apps": {
|
||||
"models": {
|
||||
"models": ["database.exercices.models", 'database.auth.models', "database.room.models","aerich.models"],
|
||||
"default_connection": "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
register_tortoise(
|
||||
app,
|
||||
config=TORTOISE_ORM,
|
||||
#db_url="sqlite://database/db.sqlite3",
|
||||
modules={"models": ["database.exercices.models", 'database.auth.models']},
|
||||
generate_schemas=True,
|
||||
add_exception_handlers=True,
|
||||
)
|
4
backend/api_old/pyproject.toml
Normal file
4
backend/api_old/pyproject.toml
Normal file
@ -0,0 +1,4 @@
|
||||
[tool.aerich]
|
||||
tortoise_orm = "main.TORTOISE_ORM"
|
||||
location = "./migrations"
|
||||
src_folder = "./."
|
47
backend/api_old/schema/user.py
Normal file
47
backend/api_old/schema/user.py
Normal file
@ -0,0 +1,47 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, validator
|
||||
from services.password import validate_password
|
||||
|
||||
from database.decorators import as_form
|
||||
|
||||
|
||||
@as_form
|
||||
class UserForm(BaseModel):
|
||||
username: str
|
||||
firstname: Optional[str]
|
||||
name: Optional[str]
|
||||
email: Optional[str]
|
||||
|
||||
@as_form
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
@as_form
|
||||
class UserRegister(User):
|
||||
password_confirm: str
|
||||
|
||||
@validator('username')
|
||||
def username_alphanumeric(cls, v):
|
||||
assert v.isalnum(), 'must be alphanumeric'
|
||||
return v
|
||||
|
||||
@validator('password')
|
||||
def password_validation(cls, v):
|
||||
is_valid = validate_password(v)
|
||||
if is_valid != True:
|
||||
raise ValueError(is_valid)
|
||||
return v
|
||||
|
||||
@validator('password_confirm')
|
||||
def password_match(cls, v, values):
|
||||
if 'password' in values and v != values['password']:
|
||||
raise ValueError('Les mots de passe ne correspondent pas')
|
||||
return v
|
||||
|
||||
|
||||
@as_form
|
||||
class PasswordSet(BaseModel):
|
||||
password: str
|
||||
password_confirm: str
|
138
backend/api_old/services/auth.py
Normal file
138
backend/api_old/services/auth.py
Normal file
@ -0,0 +1,138 @@
|
||||
from uuid import UUID
|
||||
from database.decorators import as_form
|
||||
from services.password import get_password_hash, validate_password
|
||||
from fastapi_jwt_auth import AuthJWT
|
||||
from datetime import datetime, timedelta
|
||||
from jose import jwt, JWTError
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from config import SECRET_KEY, ALGORITHM, ExerciceIn_schema, User_schema
|
||||
from database.auth.crud import get_user_db
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from pydantic import BaseModel
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
|
||||
from database.auth.models import UserModel
|
||||
from .jwt import create_access_token
|
||||
from passlib.context import CryptContext
|
||||
from .password import verify_password
|
||||
from database.exercices.models import Exercice, Tag
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from schema.user import User, UserRegister, PasswordSet
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login", auto_error=False)
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
clientId: str | None = None
|
||||
|
||||
|
||||
|
||||
async def authenticate_user(user: User = Depends(User.as_form)):
|
||||
user_db = await get_user_db(user.username)
|
||||
if not user_db:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"username_error":"Utilisateur introuvable"},
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
if not verify_password(user.password, user_db.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={"password_error": "Mot de passe invalide"},
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user_db
|
||||
|
||||
|
||||
async def get_user(username):
|
||||
user = await get_user_db(username)
|
||||
if user:
|
||||
return await User_schema.from_tortoise_orm(user)
|
||||
|
||||
|
||||
async def get_user_from_token(token: str):
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
clientId: str = payload.get("sub")
|
||||
if clientId is None:
|
||||
return None
|
||||
user = await UserModel.get_or_none(clientId=clientId)
|
||||
return user
|
||||
|
||||
|
||||
|
||||
''' async def check_tag_owner(exo_id: str, user: User_schema = Depends(get_current_active_user), ):
|
||||
tag = await Tag.get(id_code=exo_id)
|
||||
if tag.owner_id != user.id:
|
||||
raise HTTPException(status_code=401, detail="Non autorisé")
|
||||
return user '''
|
||||
|
||||
|
||||
def jwt_required(Authorize: AuthJWT = Depends()):
|
||||
Authorize.jwt_required()
|
||||
return Authorize
|
||||
|
||||
|
||||
def jwt_optional(Authorize: AuthJWT = Depends()):
|
||||
Authorize.jwt_optional()
|
||||
return Authorize
|
||||
|
||||
|
||||
def jwt_refresh_required(Authorize: AuthJWT = Depends()):
|
||||
Authorize.jwt_refresh_token_required()
|
||||
return Authorize
|
||||
|
||||
|
||||
def fresh_jwt_required(Authorize: AuthJWT = Depends()):
|
||||
Authorize.fresh_jwt_required()
|
||||
return Authorize
|
||||
|
||||
|
||||
|
||||
|
||||
def get_current_clientId(Authorize: AuthJWT = Depends(jwt_required)):
|
||||
return Authorize.get_jwt_subject()
|
||||
|
||||
async def get_current_user(clientId: str = Depends(get_current_clientId)):
|
||||
user = await UserModel.get_or_none(clientId=clientId)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='User not found')
|
||||
return user
|
||||
|
||||
async def get_current_user_optional(Authorize: AuthJWT = Depends(jwt_optional)):
|
||||
clientId = Authorize.get_jwt_subject()
|
||||
if clientId:
|
||||
return await UserModel.get_or_none(clientId=clientId)
|
||||
return None
|
||||
|
||||
async def check_unique_user(username: str):
|
||||
user = await UserModel.get_or_none(username=username)
|
||||
if user is not None:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail={"username_error": "Déjà pris "})
|
||||
return username
|
||||
|
||||
|
||||
def validate_passwords(passwords: PasswordSet = Depends(PasswordSet.as_form)):
|
||||
if passwords.password != passwords.password_confirm:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Les mots de passe ne correspondent pas !')
|
||||
is_valid = validate_password(passwords.password)
|
||||
if is_valid is not True:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f'Invalid password : {is_valid}')
|
||||
return passwords.password
|
||||
|
||||
|
||||
async def validate_register_user(user: UserRegister = Depends(UserRegister.as_form)):
|
||||
username = await check_unique_user(user.username)
|
||||
password_set = PasswordSet(
|
||||
password=user.password, password_confirm=user.password_confirm)
|
||||
validate_passwords(password_set)
|
||||
return user
|
||||
|
||||
|
||||
async def check_author_exo(id_code: str, user: UserModel = Depends(get_current_user)):
|
||||
exo = await Exercice.get(id_code=id_code)
|
||||
if exo.author_id != user.id:
|
||||
raise HTTPException(status_code=401, detail="Non autorisé")
|
||||
return user
|
56
backend/api_old/services/io.py
Normal file
56
backend/api_old/services/io.py
Normal file
@ -0,0 +1,56 @@
|
||||
|
||||
|
||||
import os
|
||||
import typing
|
||||
import uuid
|
||||
|
||||
TEXTCHARS = bytearray({7, 8, 9, 10, 12, 13, 27} |
|
||||
set(range(0x20, 0x100)) - {0x7f})
|
||||
def delete_root_slash(path: str) -> str:
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
return path
|
||||
|
||||
|
||||
def get_abs_path_from_relative_to_root(path):
|
||||
return os.path.join(os.environ.get('FASTAPI_ROOT_URL'), delete_root_slash(path))
|
||||
|
||||
|
||||
def remove_fastapi_root(path):
|
||||
return path.replace(os.environ.get('FASTAPI_ROOT_URL'), "")
|
||||
|
||||
def is_binary_file(file_path: str):
|
||||
with open(file_path, 'rb') as f:
|
||||
content = f.read(1024)
|
||||
return bool(content.translate(None, TEXTCHARS))
|
||||
def get_or_create_dir(path: str) -> str:
|
||||
if not os.path.exists(path):
|
||||
os.mkdir(path)
|
||||
return path
|
||||
|
||||
|
||||
def get_filename(file: typing.IO, default: str = uuid.uuid4()) -> str:
|
||||
if hasattr(file, 'name'):
|
||||
return file.name
|
||||
elif hasattr(file, 'filename'):
|
||||
return file.filename
|
||||
else:
|
||||
return f"{default.id_code}.py"
|
||||
|
||||
def remove_if_exists(path):
|
||||
if os.path.exists(path) and os.path.isfile(path):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def get_parent_dir(path):
|
||||
return os.path.abspath(os.path.join(path, os.pardir))
|
||||
|
||||
|
||||
def get_ancestor(path:str, levels: int = 1):
|
||||
for i in range(levels):
|
||||
path = get_parent_dir(path)
|
||||
return path
|
||||
|
||||
|
||||
def get_filename_from_path(path):
|
||||
return os.path.split(path)[-1]
|
14
backend/api_old/services/jwt.py
Normal file
14
backend/api_old/services/jwt.py
Normal file
@ -0,0 +1,14 @@
|
||||
from datetime import datetime, timedelta
|
||||
from jose import jwt, JWTError
|
||||
from config import SECRET_KEY, ALGORITHM
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
24
backend/api_old/services/password.py
Normal file
24
backend/api_old/services/password.py
Normal file
@ -0,0 +1,24 @@
|
||||
import re
|
||||
from passlib.context import CryptContext
|
||||
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def validate_password(password):
|
||||
if len(password) < 8:
|
||||
return "Le mot de passe est trop court (8 caractères minimum)"
|
||||
elif re.search('[0-9]', password) is None:
|
||||
return 'Le mot de passe doit contenir au moins un chiffre'
|
||||
elif re.search('[A-Z]', password) is None:
|
||||
return "Le mot de passe doit contenir au moins une majuscule"
|
||||
return True
|
||||
|
21
backend/api_old/services/timeout.py
Normal file
21
backend/api_old/services/timeout.py
Normal file
@ -0,0 +1,21 @@
|
||||
from contextlib import contextmanager
|
||||
import signal
|
||||
def raise_timeout(signum, frame):
|
||||
raise TimeoutError
|
||||
|
||||
@contextmanager
|
||||
def timeout(time:int, exception = TimeoutError):
|
||||
# Register a function to raise a TimeoutError on the signal.
|
||||
signal.signal(signal.SIGALRM, raise_timeout)
|
||||
# Schedule the signal to be sent after ``time``.
|
||||
signal.alarm(time)
|
||||
|
||||
try:
|
||||
yield
|
||||
except TimeoutError:
|
||||
print('TIMED OUT')
|
||||
raise exception
|
||||
finally:
|
||||
# Unregister the signal so it won't be triggered
|
||||
# if the timeout is not reached.
|
||||
signal.signal(signal.SIGALRM, signal.SIG_IGN)
|
0
backend/api_old/tests/__init__.py
Normal file
0
backend/api_old/tests/__init__.py
Normal file
49
backend/api_old/tests/conftest.py
Normal file
49
backend/api_old/tests/conftest.py
Normal file
@ -0,0 +1,49 @@
|
||||
from typing import Any
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from apis.base import api_router
|
||||
from tortoise.contrib.fastapi import register_tortoise
|
||||
|
||||
|
||||
TORTOISE_ORM = {
|
||||
"connections": {"default": "sqlite://database/db.sqlite3"},
|
||||
"apps": {
|
||||
"models": {
|
||||
"models": ["database.models", "aerich.models"],
|
||||
"default_connection": "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def start_application():
|
||||
app = FastAPI()
|
||||
app.include_router(api_router)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def app() -> Generator[FastAPI, Any, None]:
|
||||
_app = start_application()
|
||||
register_tortoise(
|
||||
app,
|
||||
config=TORTOISE_ORM,
|
||||
#db_url="sqlite://database/db.sqlite3",
|
||||
modules={"models": ["database.models"]},
|
||||
generate_schemas=True,
|
||||
add_exception_handlers=True,
|
||||
)
|
||||
yield _app
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client(
|
||||
app: FastAPI
|
||||
) -> Generator[TestClient, Any, None]:
|
||||
with TestClient(app) as client:
|
||||
yield client
|
105
backend/api_old/tests/test_exercices.py
Normal file
105
backend/api_old/tests/test_exercices.py
Normal file
@ -0,0 +1,105 @@
|
||||
|
||||
from tortoise.contrib.fastapi import register_tortoise
|
||||
import datetime
|
||||
from fastapi.testclient import TestClient
|
||||
import sys, os
|
||||
|
||||
import requests
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from faker import Faker
|
||||
from main import app
|
||||
|
||||
fake = Faker()
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
fake_exercices = []
|
||||
|
||||
token = ''
|
||||
|
||||
def test_register():
|
||||
global token
|
||||
fail_response = requests.post('http://localhost:8001/register', data={
|
||||
'username': "", 'password': 't', "password_confirm": "t"})
|
||||
|
||||
assert fail_response.status_code == 422
|
||||
assert fail_response.json()['detail'][0]['msg'] == 'field required'
|
||||
|
||||
fail_response = requests.post('http://localhost:8001/register', data={
|
||||
'username': "test", 'password': 'tt', "password_confirm": "t"})
|
||||
assert fail_response.status_code == 400
|
||||
assert fail_response.json()['detail'] == 'Les mots de passe ne correspondent pas !'
|
||||
|
||||
fail_response = requests.post('http://localhost:8001/register', data={
|
||||
'username': "test", 'password': 'tt', "password_confirm": "tt"})
|
||||
assert fail_response.status_code == 400
|
||||
assert fail_response.json(
|
||||
)['detail'] == 'Invalid password : Password too short'
|
||||
|
||||
fail_response = requests.post('http://localhost:8001/register', data={
|
||||
'username': "test", 'password': 'testtest', "password_confirm": "testtest"})
|
||||
assert fail_response.status_code == 400
|
||||
assert fail_response.json(
|
||||
)['detail'] == 'Invalid password : Password must have a figure'
|
||||
fail_response = requests.post('http://localhost:8001/register', data={
|
||||
'username': "test", 'password': 'testtest1', "password_confirm": "testtest1"})
|
||||
assert fail_response.status_code == 400
|
||||
assert fail_response.json(
|
||||
)['detail'] == 'Invalid password : Password must have capital letter'
|
||||
|
||||
success_response = requests.post('http://localhost:8001/register', data={'username': "test", 'password': 'Testtest1', "password_confirm": "Testtest1"})
|
||||
print(success_response.json())
|
||||
assert success_response.status_code == 200
|
||||
assert 'access_token' in success_response.json()
|
||||
assert success_response.json()['token_type'] == 'bearer'
|
||||
|
||||
fail_response = requests.post('http://localhost:8001/register', data={
|
||||
'username': "test", 'password': 'Testtest1', "password_confirm": "Testtest1"})
|
||||
assert fail_response.status_code == 422
|
||||
assert 'UNIQUE constraint failed' in fail_response.json()['detail'][0]['msg']
|
||||
token = success_response.json()['access_token']
|
||||
|
||||
def test_login():
|
||||
global token
|
||||
r = requests.post('http://localhost:8001/login', data = {"username": "teste", 'password': 'Testtest1'})
|
||||
assert r.status_code == 401
|
||||
assert r.json()['detail'] == 'Incorrect username or password'
|
||||
r = requests.post('http://localhost:8001/login', data = {"username": "test", 'password': 'Testtest'})
|
||||
assert r.status_code == 401
|
||||
assert r.json()['detail'] == 'Incorrect username or password'
|
||||
|
||||
r = requests.post('http://localhost:8001/login', data = {"username": "test", 'password': 'Testtest1'})
|
||||
assert r.status_code == 200
|
||||
assert 'access_token' in r.json()
|
||||
assert r.json()['token_type'] == 'bearer'
|
||||
token = r.json()['access_token']
|
||||
|
||||
def test_delete_user():
|
||||
r = requests.delete('http://localhost:8001/user', headers={'Authorization': f"Bearer {token}"})
|
||||
print(r.json())
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_create_exo():
|
||||
print('TOKEN', token)
|
||||
headers = {'Authorization': f'Bearer ${token}'}
|
||||
response = requests.post('http://localhost:8001/exercices/', params= {"name": "test", "consigne": "test", 'private': False}, files = {'file': ('1test_model.py', open("/home/lilian/1test_model.py", 'rb'), "text/x-python")}, headers=headers)
|
||||
data = response.json()
|
||||
fake_exercices.append(data)
|
||||
assert 'id_code' in data
|
||||
assert 'updated_at' in data
|
||||
id_code = data.pop('id_code')
|
||||
data.pop('updated_at')
|
||||
assert response.status_code == 200
|
||||
assert data == {'name': 'test', 'consigne': 'test', 'private': False,
|
||||
'tags': [], 'origin': None, 'isOriginal': True, 'pdfSupport': True, 'csvSupport': False, 'webSupport': True}
|
||||
|
||||
def delete_exo():
|
||||
response = requests.delete('http://localhost:8001/exercices/DATEMY')
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_ws():
|
||||
pass
|
||||
|
||||
|
||||
|
10
backend/api_old/uploads/ERSIWQ/1test_model.py
Normal file
10
backend/api_old/uploads/ERSIWQ/1test_model.py
Normal file
@ -0,0 +1,10 @@
|
||||
import random
|
||||
|
||||
"""
|
||||
Fonction main() qui doit renvoyer un objet avec:
|
||||
calcul: le calcul a afficher
|
||||
result: la correction du calcul (pas de correction -> mettre None)
|
||||
"""
|
||||
|
||||
def main():
|
||||
return {"csv": "None", 'pdf': "", "calcul": "1+1=2"}
|
10
backend/api_old/uploads/JZJGJR/1test_model.py
Normal file
10
backend/api_old/uploads/JZJGJR/1test_model.py
Normal file
@ -0,0 +1,10 @@
|
||||
import random
|
||||
|
||||
"""
|
||||
Fonction main() qui doit renvoyer un objet avec:
|
||||
calcul: le calcul a afficher
|
||||
result: la correction du calcul (pas de correction -> mettre None)
|
||||
"""
|
||||
|
||||
def main():
|
||||
return {"csv": "1+1", 'pdf': "", "calcul": "1+1=2"}
|
10
backend/api_old/uploads/NAZABB/1test_model.py
Normal file
10
backend/api_old/uploads/NAZABB/1test_model.py
Normal file
@ -0,0 +1,10 @@
|
||||
import random
|
||||
|
||||
"""
|
||||
Fonction main() qui doit renvoyer un objet avec:
|
||||
calcul: le calcul a afficher
|
||||
result: la correction du calcul (pas de correction -> mettre None)
|
||||
"""
|
||||
|
||||
def main():
|
||||
return {"csv": None, 'pdf': "", "calcul": "1+1=2"}
|
10
backend/api_old/uploads/SSJCKT/1test_model.py
Normal file
10
backend/api_old/uploads/SSJCKT/1test_model.py
Normal file
@ -0,0 +1,10 @@
|
||||
import random
|
||||
|
||||
"""
|
||||
Fonction main() qui doit renvoyer un objet avec:
|
||||
calcul: le calcul a afficher
|
||||
result: la correction du calcul (pas de correction -> mettre None)
|
||||
"""
|
||||
|
||||
def main():
|
||||
return {"csv": "None", 'pdf': "", "calcul": "1+1=2"}
|
10
backend/api_old/uploads/TVLLES/1test_model.py
Normal file
10
backend/api_old/uploads/TVLLES/1test_model.py
Normal file
@ -0,0 +1,10 @@
|
||||
import random
|
||||
|
||||
"""
|
||||
Fonction main() qui doit renvoyer un objet avec:
|
||||
calcul: le calcul a afficher
|
||||
result: la correction du calcul (pas de correction -> mettre None)
|
||||
"""
|
||||
|
||||
def main():
|
||||
return {"csv": "None", 'pdf': "", "calcul": "1+1=2"}
|
10
backend/api_old/uploads/UAPHYF/1test_model.py
Normal file
10
backend/api_old/uploads/UAPHYF/1test_model.py
Normal file
@ -0,0 +1,10 @@
|
||||
import random
|
||||
|
||||
"""
|
||||
Fonction main() qui doit renvoyer un objet avec:
|
||||
calcul: le calcul a afficher
|
||||
result: la correction du calcul (pas de correction -> mettre None)
|
||||
"""
|
||||
|
||||
def main():
|
||||
return {"csv": None, 'web': None, "calcul": "1+1=2"}
|
34
frontend/README.md
Normal file
34
frontend/README.md
Normal file
@ -0,0 +1,34 @@
|
||||
## Usage
|
||||
|
||||
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
|
||||
|
||||
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
|
||||
|
||||
```bash
|
||||
$ npm install # or pnpm install or yarn install
|
||||
```
|
||||
|
||||
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm dev` or `npm start`
|
||||
|
||||
Runs the app in the development mode.<br>
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br>
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `dist` folder.<br>
|
||||
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
|
||||
## Deployment
|
||||
|
||||
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
|
15
frontend/index.html
Normal file
15
frontend/index.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
33
frontend/package.json
Normal file
33
frontend/package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "vite-template-solid",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"sass": "^1.54.3",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.0",
|
||||
"vite-plugin-solid": "^2.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solidjs/meta": "^0.28.0",
|
||||
"@solidjs/router": "^0.4.2",
|
||||
"axios": "^0.27.2",
|
||||
"chroma-js": "^2.4.2",
|
||||
"emotion-solid": "^1.1.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"solid-forms": "^0.4.5",
|
||||
"solid-icons": "^1.0.1",
|
||||
"solid-js": "^1.4.7",
|
||||
"solid-styled-components": "^0.28.4",
|
||||
"solid-styled-jsx": "^0.27.1",
|
||||
"solid-toast": "^0.3.4",
|
||||
"styled-jsx": "^3.4.4"
|
||||
}
|
||||
}
|
1336
frontend/pnpm-lock.yaml
Normal file
1336
frontend/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
33
frontend/src/App.module.css
Normal file
33
frontend/src/App.module.css
Normal file
@ -0,0 +1,33 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #b318f0;
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
65
frontend/src/App.tsx
Normal file
65
frontend/src/App.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { Component, createEffect, createSignal } from "solid-js";
|
||||
|
||||
import logo from "./logo.svg";
|
||||
import styles from "./App.module.css";
|
||||
import { MetaProvider } from "@solidjs/meta";
|
||||
import Layout from "./components/Layout";
|
||||
import { Route, Routes } from "@solidjs/router";
|
||||
import Test from "./components/test";
|
||||
import Home from "./components/Home";
|
||||
import Routing from "./components/Routing";
|
||||
import { AuthProvider } from "./context/auth.context.jsx";
|
||||
import { LoginPopUpProvider } from "./context/loginPopUp.context.jsx";
|
||||
import LoginPopup from "./components/LoginPopup";
|
||||
import { Toaster } from "solid-toast";
|
||||
import { NotificationProvider } from "./context/notification.context.jsx";
|
||||
import { NavigateProvider } from "./context/navigate.context.jsx";
|
||||
const App: Component = () => {
|
||||
const [count, setCount] = createSignal(0);
|
||||
createEffect(() => {
|
||||
setInterval(() => setCount((c) => c + 1), 1000);
|
||||
});
|
||||
const [popup, setPopup] = createSignal({ active: false, next: () => {} });
|
||||
return (
|
||||
<MetaProvider>
|
||||
<NotificationProvider>
|
||||
<NavigateProvider>
|
||||
<LoginPopUpProvider
|
||||
popup={(next: () => void) => {
|
||||
setPopup({ active: true, next: next });
|
||||
}}
|
||||
active={popup().active}
|
||||
next={() => {
|
||||
popup().next();
|
||||
setPopup({ active: false, next: () => {} });
|
||||
}}
|
||||
>
|
||||
<AuthProvider>
|
||||
<Routing />
|
||||
|
||||
<LoginPopup
|
||||
active={popup().active}
|
||||
close={() => {
|
||||
setPopup({ active: false, next: () => {} });
|
||||
}}
|
||||
/>
|
||||
|
||||
<Toaster />
|
||||
</AuthProvider>
|
||||
</LoginPopUpProvider>{" "}
|
||||
</NavigateProvider>
|
||||
</NotificationProvider>
|
||||
</MetaProvider>
|
||||
);
|
||||
};
|
||||
|
||||
type countModel = {
|
||||
count: number;
|
||||
};
|
||||
|
||||
const Counter: Component<countModel> = (props: countModel) => {
|
||||
var c = props.count;
|
||||
return <p>{props.count}</p>;
|
||||
};
|
||||
|
||||
export default App;
|
12
frontend/src/apis/auth.instance.js
Normal file
12
frontend/src/apis/auth.instance.js
Normal file
@ -0,0 +1,12 @@
|
||||
import axios from "axios";
|
||||
import { csrftoken, isBrowser } from "../utils/utils.js";
|
||||
|
||||
export const authInstance = axios.create({
|
||||
baseURL: `http://localhost:8002/`,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"X-CSRFToken": csrftoken != undefined ? csrftoken : "",
|
||||
},
|
||||
});
|
27
frontend/src/apis/exoInstance.instance.js
Normal file
27
frontend/src/apis/exoInstance.instance.js
Normal file
@ -0,0 +1,27 @@
|
||||
import axios from "axios";
|
||||
import { csrftoken, isBrowser } from "../utils/utils.js";
|
||||
|
||||
|
||||
export const exoInstance = axios.create({
|
||||
paramsSerializer: (params)=> {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const key of Object.keys(params)) {
|
||||
const param = params[key];
|
||||
if (Array.isArray(param)) {
|
||||
for (const p of param) {
|
||||
searchParams.append(key, p);
|
||||
}
|
||||
} else {
|
||||
searchParams.append(key, param);
|
||||
}
|
||||
}
|
||||
return searchParams.toString();
|
||||
},
|
||||
baseURL: `http://localhost:8002/`,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"X-CSRFToken": csrftoken != undefined ? csrftoken : "",
|
||||
},
|
||||
});
|
BIN
frontend/src/assets/favicon.ico
Normal file
BIN
frontend/src/assets/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
18
frontend/src/components/DelayedShow.tsx
Normal file
18
frontend/src/components/DelayedShow.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { Component, createEffect, createSignal, JSX, Show } from "solid-js";
|
||||
|
||||
export const DelayedShow: Component<{ children: JSX.Element, when: boolean, mountDelay: number, unMountDelay: number, fallback?: JSX.Element }> = (props) => {
|
||||
var initial = true
|
||||
const [condition, setCondition] = createSignal(props.when)
|
||||
createEffect(() => {
|
||||
console.log('chaneg')
|
||||
var delay = !!props.when ? props.mountDelay : props.unMountDelay;
|
||||
if (!initial) {
|
||||
console.log('change')
|
||||
setTimeout(() => {
|
||||
setCondition(props.when)
|
||||
}, delay * 1000)
|
||||
}
|
||||
initial = false
|
||||
})
|
||||
return <Show when={condition()} fallback={props.fallback}>{props.children}</Show>
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user