first commit

This commit is contained in:
Kilton937342 2022-09-16 21:50:55 +02:00
commit 55e1a1baef
167 changed files with 12601 additions and 0 deletions

6
.gitignore vendored Normal file
View 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
View 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
View 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()

View 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

View 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

View 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

View 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

View 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)

View 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

View 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

View 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')

View 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

View 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

Binary file not shown.

View 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

View 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']

View 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

View 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)

View 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)')

View 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
"""

View 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([''])

View 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
View 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)

View 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}

View 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)

View 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

View 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')

View File

@ -0,0 +1,7 @@
from fastapi import APIRouter
router = APIRouter(tags=["room"])
@router.post('/room')
def create_room():
return

View 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)

View 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

View 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

View 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)

View 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

View 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

View 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

View 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)

View 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]

View 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"

View 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

View 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

View 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
View 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"

View 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

View 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

View 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"}

View 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}

View File

View 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 ""

View 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)

View 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

View 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"

View 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
View 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

View File

View 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

View 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"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

View 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

View 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

View 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"]

View 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)')

View 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

View 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

View 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

Binary file not shown.

View 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([''])

View 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
View 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
View 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,
)

View File

@ -0,0 +1,4 @@
[tool.aerich]
tortoise_orm = "main.TORTOISE_ORM"
location = "./migrations"
src_folder = "./."

View 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

View 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

View 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]

View 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

View 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

View 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)

View File

View 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

View 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

View 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"}

View 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"}

View 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"}

View 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"}

View 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"}

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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
View 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;

View 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 : "",
},
});

View 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 : "",
},
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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