before style

This commit is contained in:
Kilton937342 2023-01-27 21:41:08 +01:00
parent ccd047dbb0
commit 43031d22fb
44 changed files with 2730 additions and 294 deletions

View File

@ -2,7 +2,7 @@ from fastapi import Query, HTTPException, status
import os import os
import shutil import shutil
from typing import IO, List from typing import IO, List
from sqlmodel import Session, select, or_ from sqlmodel import Session, select, or_, col
from generateur.generateur_main import generate_from_data, generate_from_path from generateur.generateur_main import generate_from_data, generate_from_path
from database.auth.models import User from database.auth.models import User
from database.db import get_session from database.db import get_session
@ -62,13 +62,14 @@ def clone_exo_db(exercice: Exercice, user: User, db: Session):
db.refresh(new_exo) db.refresh(new_exo)
return new_exo return new_exo
def update_exo_db(old_exo: Exercice, new_exo: ExerciceEdit,supports: Supports, exo_source: IO | None, db: Session): def update_exo_db(old_exo: Exercice, new_exo: ExerciceEdit, supports: Supports | None, exo_source: IO | None, db: Session):
exo_data = new_exo.dict(exclude_unset=True, exclude_none=True) exo_data = new_exo.dict(exclude_unset=True, exclude_none=True)
for key, value in exo_data.items(): for key, value in exo_data.items():
setattr(old_exo, key, value) setattr(old_exo, key, value)
if supports is not None:
old_exo.csv = supports['csv'] old_exo.csv = supports['csv']
old_exo.pdf = supports['pdf'] old_exo.pdf = supports['pdf']
old_exo.web = supports['web'] old_exo.web = supports['web']
@ -87,7 +88,6 @@ def update_exo_db(old_exo: Exercice, new_exo: ExerciceEdit,supports: Supports, e
db.refresh(old_exo) db.refresh(old_exo)
return old_exo return old_exo
def delete_exo_db(exo: Exercice, db: Session): def delete_exo_db(exo: Exercice, db: Session):
db.delete(exo) db.delete(exo)
db.commit() db.commit()
@ -98,24 +98,27 @@ 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_db = db.exec(select(Tag).where(Tag.author_id == user.id).where(or_(
Tag.id_code == tag.id_code, Tag.label == tag.label))).first() Tag.id_code == tag.id_code, Tag.label == tag.label))).first()
if tag_db is not None: if tag_db is not None:
return tag_db return tag_db, False
id_code = generate_unique_code(Tag, db) id_code = generate_unique_code(Tag, db)
tag_db = Tag(**{**tag.dict(exclude_unset=True), tag_db = Tag(**{**tag.dict(exclude_unset=True),
'id_code': id_code, 'author_id': user.id}) 'id_code': id_code, 'author_id': user.id})
db.add(tag_db) db.add(tag_db)
db.commit() db.commit()
db.refresh(tag_db) db.refresh(tag_db)
return tag_db return tag_db, True
def add_tags_db(exo: Exercice, tags: List[TagCreate], user: User, db: Session): def add_tags_db(exo: Exercice, tags: List[TagCreate], user: User, db: Session):
new = []
for tag in tags: for tag in tags:
tag_db = get_or_create_tag(tag, user, db) tag_db, created = get_or_create_tag(tag, user, db)
if created:
new.append(tag_db)
exo.tags.append(tag_db) exo.tags.append(tag_db)
db.add(exo) db.add(exo)
db.commit() db.commit()
db.refresh(exo) db.refresh(exo)
return exo return exo, new
def remove_tag_db(exo: Exercice, tag: Tag, db: Session): def remove_tag_db(exo: Exercice, tag: Tag, db: Session):
@ -171,13 +174,11 @@ def check_tag_author(tag_id: str, user: User = Depends(get_current_user), db: Se
def get_tags_dependency(tags: List[str] | None = Query(None), db: Session = Depends(get_session)): def get_tags_dependency(tags: List[str] | None = Query(None), db: Session = Depends(get_session)):
if tags is None: if tags is None:
return None return []
validated_tags = [] validated_tags = db.exec(
for t in tags: select(Tag.id_code, Tag.id).where(col(Tag.id_code).in_(tags))).all()
tag = db.exec(select(Tag.id).where(Tag.id_code == t)).first()
if tag is not None: return [t.id for t in validated_tags]
validated_tags.append(tag)
return validated_tags
#Serialize #Serialize
@ -185,9 +186,17 @@ def get_tags_dependency(tags: List[str] | None = Query(None), db: Session = Depe
def check_author(exo: Exercice, user_id:int): def check_author(exo: Exercice, user_id:int):
return exo.author_id == user_id return exo.author_id == user_id
def serialize_exo(*, exo: ExerciceRead, user_id: User = None, db: Session): def serialize_exo(*, exo: Exercice, user_id: User = None, db: Session):
tags = parse_exo_tags(exo_id=exo.id, user_id=user_id, tags = parse_exo_tags(exo_id=exo.id, user_id=user_id,
db=db) if user_id is not None else [] 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) is_author = user_id is not None and check_author(exo=exo, user_id=user_id)
return ExerciceReadFull(**exo.dict(), author=exo.author, original=exo.original, tags=tags, is_author=is_author, supports={**exo.dict()}) print('USER', exo.dict(), exo)
if exo.original is not None:
print('TEST', db.exec(select(User).where(User.id == exo.original.author_id)).all())
author = db.exec(select(User).where(
User.id == exo.original.author_id)).all()[0]
original = {**exo.original.dict(), 'author': author.username}
else:
original = None
return ExerciceReadFull(**{**exo.dict(), "author":exo.author, "original":original, "tags":tags, "is_author":is_author, "supports":{**exo.dict()}})

View File

@ -125,11 +125,6 @@ class ExerciceEdit(ExerciceCreate):
pass pass
class ExerciceOrigin(SQLModel):
#id: int
id_code: str
name: str
@ -138,6 +133,13 @@ class Author(SQLModel):
username: str username: str
class ExerciceOrigin(SQLModel):
#id: int
id_code: str
name: str
author: str
@ -164,12 +166,15 @@ class ExerciceReadBase(ExerciceBase):
return get_filename_from_path(value) return get_filename_from_path(value)
return value return value
class ExerciceRead(ExerciceBase): class ExerciceRead(ExerciceBase, Supports):
id_code: str
id: int id: int
author_id:int author_id:int
exo_source: str
author: User author: User
original: Optional[Exercice] original: Optional[Exercice]
tags: List[Tag] tags: List[Tag]
examples: Example
class ExerciceReadFull(ExerciceReadBase): class ExerciceReadFull(ExerciceReadBase):
supports: Supports supports: Supports

12
backend/api/exo.py Normal file
View File

@ -0,0 +1,12 @@
import requests
def exos(nb, username, password):
rr = requests.post('http://localhost:8002/register', data={"username": username,
'password': password, 'password_confirm': password})
token = rr.json()['access_token']
for i in range(nb):
r = requests.post('http://localhost:8002/exercices', data={"name": "FakingTest" + str(i), "consigne": "consigne", "private": False}, files={
'file': ('test.py', open('./tests/testing_exo_source/exo_source.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
print('DONE')
exos(100, "lilianTest", "Pomme937342")

View File

@ -37,6 +37,7 @@ origins = [
"https://localhost:8001", "https://localhost:8001",
"http://localhost", "http://localhost",
"http://localhost:8080", "http://localhost:8080",
"http://localhost:5173"
] ]
app.add_middleware( app.add_middleware(
@ -45,6 +46,7 @@ app.add_middleware(
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
expose_headers=['*']
) )
admin = Admin(app, engine) admin = Admin(app, engine)

View File

@ -1,3 +1,4 @@
from pydantic import BaseModel
from enum import Enum from enum import Enum
from typing import List from typing import List
from fastapi import APIRouter, Depends, Path, Query, UploadFile, HTTPException, status from fastapi import APIRouter, Depends, Path, Query, UploadFile, HTTPException, status
@ -11,8 +12,10 @@ from services.exoValidation import validate_file, validate_file_optionnal
from services.io import add_fast_api_root, get_filename_from_path from services.io import add_fast_api_root, get_filename_from_path
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from sqlmodel import func from sqlmodel import func
from fastapi_pagination import Page, paginate from fastapi_pagination import paginate ,Page
from services.models import Page
from fastapi_pagination.ext.sqlalchemy_future import paginate as p from fastapi_pagination.ext.sqlalchemy_future import paginate as p
router = APIRouter(tags=['exercices']) router = APIRouter(tags=['exercices'])
class ExoType(str, Enum): class ExoType(str, Enum):
@ -27,7 +30,7 @@ def filter_exo_by_tags(exos: List[tuple[Exercice, str]], tags: List[Tag]):
return valid_exos return valid_exos
def queryFilters_dependency(search: str = "", tags: List[int] | None = Depends(get_tags_dependency), type: ExoType | None = Query(default = None)): def queryFilters_dependency(search: str = "", tags: List[str] | None = Depends(get_tags_dependency), type: ExoType | None = Query(default = None)):
return search, tags, type return search, tags, type
@ -53,15 +56,10 @@ def clone_exo(exercice: Exercice | None = Depends(check_private), user: User = D
@router.get('/exercices/user', response_model=Page[ExerciceRead|ExerciceReadFull]) @router.get('/exercices/user', response_model=Page[ExerciceRead|ExerciceReadFull])
def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tuple[str, List[int] | None] = Depends(queryFilters_dependency), db: Session = Depends(get_session)): def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tuple[str, List[int] | None, ExoType | None] = Depends(queryFilters_dependency), db: Session = Depends(get_session)):
search, tags, type = queryFilters search, tags, type = queryFilters
if tags is not None and len(tags) != 0:
statement = select(Exercice, func.group_concat(
ExercicesTagLink.tag_id))
else:
statement = select(Exercice) statement = select(Exercice)
statement = statement.where(Exercice.author_id == user.id) statement = statement.where(Exercice.author_id == user.id)
statement = statement.where(Exercice.name.startswith(search)) statement = statement.where(Exercice.name.startswith(search))
@ -72,18 +70,14 @@ def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tup
if type == ExoType.web: if type == ExoType.web:
statement = statement.where(Exercice.web == True) statement = statement.where(Exercice.web == True)
if tags is not None and len(tags) != 0: for t in tags:
statement = statement.join(ExercicesTagLink).where( sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id==Exercice.id).where(
col(ExercicesTagLink.tag_id).in_(tags)).group_by(ExercicesTagLink.exercice_id) ExercicesTagLink.tag_id == t).exists()
statement = statement.where(sub)
#exercices = db.exec(statement).all()
page = p(db, statement) page = p(db, statement)
exercices = page.items exercices = page.items
if tags is not None and len(tags) != 0:
exercices = filter_exo_by_tags(exercices, tags)
page.items = [ page.items = [
serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices] serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices]
return page return page
@ -92,12 +86,7 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional),
search, tags, type = queryFilters search, tags, type = queryFilters
if user is not None: 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 = select(Exercice)
statement = statement.where(Exercice.author_id != user.id) statement = statement.where(Exercice.author_id != user.id)
statement = statement.where(Exercice.private == False) statement = statement.where(Exercice.private == False)
statement = statement.where(Exercice.name.startswith(search)) statement = statement.where(Exercice.name.startswith(search))
@ -109,18 +98,18 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional),
if type == ExoType.web: if type == ExoType.web:
statement = statement.where(Exercice.web == True) statement = statement.where(Exercice.web == True)
if tags is not None and len(tags) != 0: for t in tags:
statement = statement.join(ExercicesTagLink).where( sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id==Exercice.id).where(
col(ExercicesTagLink.tag_id).in_(tags)).group_by(ExercicesTagLink.exercice_id) ExercicesTagLink.tag_id == t).exists()
statement = statement.where(sub)
exercices = db.exec(statement).all() page = p(db, statement)
print('¨PAGE', page)
if tags is not None and len(tags) != 0: exercices = page.items
exercices = filter_exo_by_tags(exercices, tags) page.items = [
exercices = [
serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices] serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices]
return exercices return page
else: else:
statement = select(Exercice) statement = select(Exercice)
statement = statement.where(Exercice.private == False) statement = statement.where(Exercice.private == False)
@ -131,8 +120,12 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional),
statement = statement.where(Exercice.pdf == True) statement = statement.where(Exercice.pdf == True)
if type == ExoType.web: if type == ExoType.web:
statement = statement.where(Exercice.web == True) statement = statement.where(Exercice.web == True)
exercices = db.exec(statement).all()
return paginate([serialize_exo(exo=e, user_id=None, db=db) for e in exercices]) page = p(db, statement)
exercices = page.items
page.items = [
serialize_exo(exo=e, user_id=None, db=db) for e in exercices]
return page
@router.get('/exercice/{id_code}', response_model=ExerciceReadFull) @router.get('/exercice/{id_code}', response_model=ExerciceReadFull)
@ -153,7 +146,7 @@ def update_exo(file: UploadFile = Depends(validate_file_optionnal), exo: Exercic
if file: if file:
file_obj = file["file"].file._file file_obj = file["file"].file._file
file_obj.name = file['file'].filename file_obj.name = file['file'].filename
exo_obj = update_exo_db(exo, exercice,file['supports'] if file is not None else None, file_obj, db) exo_obj = update_exo_db(exo, exercice, file['supports'] if file is not None else None, file_obj, db)
return serialize_exo(exo=exo_obj, user_id=exo_obj.author_id, db=db) return serialize_exo(exo=exo_obj, user_id=exo_obj.author_id, db=db)
@ -169,13 +162,19 @@ def delete_exercice(exercice: Exercice | bool | None = Depends(check_exercice_au
return {'detail': 'Exercice supprimé avec succès'} return {'detail': 'Exercice supprimé avec succès'}
@router.post('/exercice/{id_code}/tags', response_model=ExerciceReadFull, tags=['tags'])
class NewTags(BaseModel):
exo: ExerciceReadFull
tags: list[TagRead]
@router.post('/exercice/{id_code}/tags', response_model=NewTags, tags=['tags'])
def add_tags(tags: List[TagCreate], exo: Exercice | None = Depends(get_exo_dependency), db: Session = Depends(get_session), user: User = Depends(get_current_user)): def add_tags(tags: List[TagCreate], exo: Exercice | None = Depends(get_exo_dependency), db: Session = Depends(get_session), user: User = Depends(get_current_user)):
if exo is None: if exo is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable') status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable')
exo_obj = add_tags_db(exo, tags, user, db) exo_obj, new = add_tags_db(exo, tags, user, db)
return serialize_exo(exo=exo_obj, user_id=user.id, db=db) return {"exo":serialize_exo(exo=exo_obj, user_id=user.id, db=db), "tags": new}
@router.delete('/exercice/{id_code}/tags/{tag_id}', response_model=ExerciceReadFull, tags=['tags']) @router.delete('/exercice/{id_code}/tags/{tag_id}', response_model=ExerciceReadFull, tags=['tags'])

View File

@ -0,0 +1,42 @@
from __future__ import annotations
from typing import TypeVar, Generic, Sequence
from fastapi_pagination import Params
from fastapi_pagination.bases import AbstractPage, AbstractParams, BasePage
from math import ceil
from pydantic import conint
T = TypeVar("T")
class Page(BasePage[T], Generic[T]):
page: conint(ge=1) # type: ignore
size: conint(ge=1)
totalPage: conint(ge=0)
hasMore: bool
__params_type__ = Params # Set params related to Page
@classmethod
def create(
cls,
items: Sequence[T],
total: int,
params: AbstractParams,
) -> Page[T]:
print("PARAMS", params)
totalPage = ceil(total/params.size)
return cls(
total=total,
items=items,
page=params.page,
size=params.size,
totalPage = totalPage,
hasMore= params.page < totalPage
)
return {
"items": items,
"total": total,
"size": params,
}
return cls(results=items)

View File

@ -6,7 +6,6 @@ from tests.test_auth import test_register
def test_create(client: TestClient, name="test_exo", consigne="consigne", private=False, user=None): def test_create(client: TestClient, name="test_exo", consigne="consigne", private=False, user=None):
if user == None: if user == None:
token = test_register(client, username="lilian")['access'] token = test_register(client, username="lilian")['access']
username = 'lilian' username = 'lilian'
@ -145,6 +144,18 @@ def test_update(client: TestClient):
'pdf': False, 'csv': True, 'web': True}, 'examples': {'type': 'csv', 'data': [{'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}]}, 'is_author': True} 'pdf': False, 'csv': True, 'web': True}, 'examples': {'type': 'csv', 'data': [{'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}]}, 'is_author': True}
return r.json() return r.json()
def test_update_no_file(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}, 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': 'test.py', 'supports': {
'pdf': False, 'csv': True, 'web': True}, 'examples': {'type': 'csv', 'data': [{'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}]}, 'is_author': True}
return r.json()
def test_update_missing_name(client: TestClient): def test_update_missing_name(client: TestClient):
token = test_register(client, username="lilian")['access'] token = test_register(client, username="lilian")['access']
@ -346,19 +357,30 @@ def test_get_users_exos_page(client: TestClient):
token1 = test_register(client, username="lilian")['access'] token1 = test_register(client, username="lilian")['access']
token2 = test_register(client, username="lilian2")['access'] token2 = test_register(client, username="lilian2")['access']
prv = test_create(client, private=True, user={'token': token1, 'username': "lilian"}) prv = test_create(client, private=True, user={
prv2 = test_create(client, private=True, user={'token': token1, 'username': "lilian"}) 'token': token1, 'username': "lilian"})
prv3= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) prv2 = test_create(client, private=True, user={
prv4= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) 'token': token1, 'username': "lilian"})
prv5= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) prv3 = test_create(client, private=True, user={
prv6= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) 'token': token1, 'username': "lilian"})
prv7= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) prv4 = test_create(client, private=True, user={
prv8= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) 'token': token1, 'username': "lilian"})
prv9= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) prv5 = test_create(client, private=True, user={
prv10= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) 'token': token1, 'username': "lilian"})
prv11 = test_create(client, private=True, user={'token': token1, 'username': "lilian"}) prv6 = test_create(client, private=True, user={
prv12 = test_create(client, private=True, user={'token': token1, 'username': "lilian"}) 'token': token1, 'username': "lilian"})
prv7 = test_create(client, private=True, user={
'token': token1, 'username': "lilian"})
prv8 = test_create(client, private=True, user={
'token': token1, 'username': "lilian"})
prv9 = test_create(client, private=True, user={
'token': token1, 'username': "lilian"})
prv10 = test_create(client, private=True, user={
'token': token1, 'username': "lilian"})
prv11 = test_create(client, private=True, user={
'token': token1, 'username': "lilian"})
prv12 = test_create(client, private=True, user={
'token': token1, 'username': "lilian"})
r = client.get('/exercices/user', r = client.get('/exercices/user',
headers={'Authorization': 'Bearer ' + token1}, params={"page": 2, "size": 10}) headers={'Authorization': 'Bearer ' + token1}, params={"page": 2, "size": 10})
@ -367,23 +389,35 @@ def test_get_users_exos_page(client: TestClient):
assert r.json()['size'] == 10 assert r.json()['size'] == 10
assert r.json()["items"] == [prv11, prv12] assert r.json()["items"] == [prv11, prv12]
def test_get_users_exos_page_up(client: TestClient): def test_get_users_exos_page_up(client: TestClient):
token1 = test_register(client, username="lilian")['access'] token1 = test_register(client, username="lilian")['access']
token2 = test_register(client, username="lilian2")['access'] token2 = test_register(client, username="lilian2")['access']
prv = test_create(client, private=True, user={'token': token1, 'username': "lilian"}) prv = test_create(client, private=True, user={
prv2 = test_create(client, private=True, user={'token': token1, 'username': "lilian"}) 'token': token1, 'username': "lilian"})
prv3= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) prv2 = test_create(client, private=True, user={
prv4= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) 'token': token1, 'username': "lilian"})
prv5= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) prv3 = test_create(client, private=True, user={
prv6= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) 'token': token1, 'username': "lilian"})
prv7= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) prv4 = test_create(client, private=True, user={
prv8= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) 'token': token1, 'username': "lilian"})
prv9= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) prv5 = test_create(client, private=True, user={
prv10= test_create(client, private=True, user={'token': token1, 'username': "lilian"}) 'token': token1, 'username': "lilian"})
prv11 = test_create(client, private=True, user={'token': token1, 'username': "lilian"}) prv6 = test_create(client, private=True, user={
prv12 = test_create(client, private=True, user={'token': token1, 'username': "lilian"}) 'token': token1, 'username': "lilian"})
prv7 = test_create(client, private=True, user={
'token': token1, 'username': "lilian"})
prv8 = test_create(client, private=True, user={
'token': token1, 'username': "lilian"})
prv9 = test_create(client, private=True, user={
'token': token1, 'username': "lilian"})
prv10 = test_create(client, private=True, user={
'token': token1, 'username': "lilian"})
prv11 = test_create(client, private=True, user={
'token': token2, 'username': "lilian2"})
prv12 = test_create(client, private=True, user={
'token': token2, 'username': "lilian2"})
r = client.get('/exercices/user', r = client.get('/exercices/user',
headers={'Authorization': 'Bearer ' + token1}, params={"page": 2, "size": 10}) headers={'Authorization': 'Bearer ' + token1}, params={"page": 2, "size": 10})
@ -393,6 +427,7 @@ def test_get_users_exos_page_up(client: TestClient):
assert r.json()["items"] == [] assert r.json()["items"] == []
def test_get_user_with_search(client: TestClient): def test_get_user_with_search(client: TestClient):
token1 = test_register(client, username="lilian")['access'] token1 = test_register(client, username="lilian")['access']
token2 = test_register(client, username="lilian2")['access'] token2 = test_register(client, username="lilian2")['access']
@ -561,7 +596,7 @@ def test_get_exo_no_auth(client: TestClient):
exo = test_add_tags(client, user={'token': token, 'username': "lilian"}) exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
r = client.get('/exercice/' + exo['id_code']) r = client.get('/exercice/' + exo['id_code'])
assert r.json()['items'] == {**exo, "tags": [], 'is_author': False} assert r.json() == {**exo, "tags": [], 'is_author': False}
def test_get_exo_no_auth_private(client: TestClient): def test_get_exo_no_auth_private(client: TestClient):
@ -630,7 +665,7 @@ def test_get_csv(client: TestClient):
r = client.get('/exercices/public', params={"type": "csv"}) r = client.get('/exercices/public', params={"type": "csv"})
assert r.json() == [{**exoCsv.json(), 'is_author': False}] assert r.json()['items'] == [{**exoCsv.json(), 'is_author': False}]
def test_get_pdf(client: TestClient): def test_get_pdf(client: TestClient):
@ -644,7 +679,7 @@ def test_get_pdf(client: TestClient):
r = client.get('/exercices/public', params={"type": "pdf"}) r = client.get('/exercices/public', params={"type": "pdf"})
assert r.json() == [{**exoPdf.json(), 'is_author': False}] assert r.json()['items'] == [{**exoPdf.json(), 'is_author': False}]
def test_get_web(client: TestClient): def test_get_web(client: TestClient):
@ -665,4 +700,5 @@ def test_get_invalid_type(client: TestClient):
r = client.get('/exercices/public', params={"type": "lol"}) r = client.get('/exercices/public', params={"type": "lol"})
assert r.json() == {"detail": {'type_error': "value is not a valid enumeration member; permitted: 'csv', 'pdf', 'web'"}} assert r.json() == {"detail": {
'type_error': "value is not a valid enumeration member; permitted: 'csv', 'pdf', 'web'"}}

View File

@ -14,6 +14,7 @@
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^1.0.0", "@sveltejs/adapter-auto": "^1.0.0",
"@sveltejs/kit": "^1.0.0", "@sveltejs/kit": "^1.0.0",
"@types/chroma-js": "^2.1.4",
"@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0", "@typescript-eslint/parser": "^5.45.0",
"eslint": "^8.28.0", "eslint": "^8.28.0",
@ -21,13 +22,27 @@
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.8.0", "prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1", "prettier-plugin-svelte": "^2.8.1",
"sass": "^1.53.0",
"svelte": "^3.54.0", "svelte": "^3.54.0",
"svelte-check": "^2.9.2", "svelte-check": "^2.9.2",
"svelte-preprocess": "^4.10.7",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^4.9.3", "typescript": "^4.9.3",
"vite": "^4.0.0", "vite": "^4.0.0"
"sass": "^1.53.0",
"svelte-preprocess": "^4.10.7"
}, },
"type": "module" "type": "module",
"dependencies": {
"@sveltestack/svelte-query": "^1.6.0",
"@types/qs": "^6.9.7",
"axios": "^1.2.2",
"chroma-js": "^2.4.2",
"jwt-decode": "^3.1.2",
"qs": "^6.11.0",
"svelecte": "^3.13.0",
"svelte-forms": "^2.3.1",
"svelte-icons": "^2.1.0",
"svelte-multiselect": "^8.2.3",
"svelte-navigator": "^3.2.2",
"svelte-routing": "^1.6.0"
}
} }

244
frontend/pnpm-lock.yaml generated
View File

@ -3,24 +3,52 @@ lockfileVersion: 5.4
specifiers: specifiers:
'@sveltejs/adapter-auto': ^1.0.0 '@sveltejs/adapter-auto': ^1.0.0
'@sveltejs/kit': ^1.0.0 '@sveltejs/kit': ^1.0.0
'@sveltestack/svelte-query': ^1.6.0
'@types/chroma-js': ^2.1.4
'@types/qs': ^6.9.7
'@typescript-eslint/eslint-plugin': ^5.45.0 '@typescript-eslint/eslint-plugin': ^5.45.0
'@typescript-eslint/parser': ^5.45.0 '@typescript-eslint/parser': ^5.45.0
axios: ^1.2.2
chroma-js: ^2.4.2
eslint: ^8.28.0 eslint: ^8.28.0
eslint-config-prettier: ^8.5.0 eslint-config-prettier: ^8.5.0
eslint-plugin-svelte3: ^4.0.0 eslint-plugin-svelte3: ^4.0.0
jwt-decode: ^3.1.2
prettier: ^2.8.0 prettier: ^2.8.0
prettier-plugin-svelte: ^2.8.1 prettier-plugin-svelte: ^2.8.1
qs: ^6.11.0
sass: ^1.53.0 sass: ^1.53.0
svelecte: ^3.13.0
svelte: ^3.54.0 svelte: ^3.54.0
svelte-check: ^2.9.2 svelte-check: ^2.9.2
svelte-forms: ^2.3.1
svelte-icons: ^2.1.0
svelte-multiselect: ^8.2.3
svelte-navigator: ^3.2.2
svelte-preprocess: ^4.10.7 svelte-preprocess: ^4.10.7
svelte-routing: ^1.6.0
tslib: ^2.4.1 tslib: ^2.4.1
typescript: ^4.9.3 typescript: ^4.9.3
vite: ^4.0.0 vite: ^4.0.0
dependencies:
'@sveltestack/svelte-query': 1.6.0
'@types/qs': 6.9.7
axios: 1.2.2
chroma-js: 2.4.2
jwt-decode: 3.1.2
qs: 6.11.0
svelecte: 3.13.0
svelte-forms: 2.3.1
svelte-icons: 2.1.0
svelte-multiselect: 8.2.3
svelte-navigator: 3.2.2_niwyv7xychq2ag6arq5eqxbomm
svelte-routing: 1.6.0_niwyv7xychq2ag6arq5eqxbomm
devDependencies: devDependencies:
'@sveltejs/adapter-auto': 1.0.0_@sveltejs+kit@1.0.1 '@sveltejs/adapter-auto': 1.0.0_@sveltejs+kit@1.0.1
'@sveltejs/kit': 1.0.1_svelte@3.55.0+vite@4.0.3 '@sveltejs/kit': 1.0.1_svelte@3.55.0+vite@4.0.3
'@types/chroma-js': 2.1.4
'@typescript-eslint/eslint-plugin': 5.47.1_txmweb6yn7coi7nfrp22gpyqmy '@typescript-eslint/eslint-plugin': 5.47.1_txmweb6yn7coi7nfrp22gpyqmy
'@typescript-eslint/parser': 5.47.1_lzzuuodtsqwxnvqeq4g4likcqa '@typescript-eslint/parser': 5.47.1_lzzuuodtsqwxnvqeq4g4likcqa
eslint: 8.30.0 eslint: 8.30.0
@ -370,6 +398,19 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@sveltestack/svelte-query/1.6.0:
resolution: {integrity: sha512-C0wWuh6av1zu3Pzwrg6EQmX3BhDZQ4gMAdYu6Tfv4bjbEZTB00uEDz52z92IZdONh+iUKuyo0xRZ2e16k2Xifg==}
peerDependencies:
broadcast-channel: ^4.5.0
peerDependenciesMeta:
broadcast-channel:
optional: true
dev: false
/@types/chroma-js/2.1.4:
resolution: {integrity: sha512-l9hWzP7cp7yleJUI7P2acmpllTJNYf5uU6wh50JzSIZt3fFHe+w2FM6w9oZGBTYzjjm2qHdnQvI+fF/JF/E5jQ==}
dev: true
/@types/cookie/0.5.1: /@types/cookie/0.5.1:
resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==} resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==}
dev: true dev: true
@ -386,6 +427,10 @@ packages:
resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==} resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
dev: true dev: true
/@types/qs/6.9.7:
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
dev: false
/@types/sass/1.43.1: /@types/sass/1.43.1:
resolution: {integrity: sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==} resolution: {integrity: sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==}
dependencies: dependencies:
@ -577,6 +622,20 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/asynckit/0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
dev: false
/axios/1.2.2:
resolution: {integrity: sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==}
dependencies:
follow-redirects: 1.15.2
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
dev: false
/balanced-match/1.0.2: /balanced-match/1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true dev: true
@ -611,6 +670,13 @@ packages:
streamsearch: 1.1.0 streamsearch: 1.1.0
dev: true dev: true
/call-bind/1.0.2:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
dependencies:
function-bind: 1.1.1
get-intrinsic: 1.1.3
dev: false
/callsites/3.1.0: /callsites/3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -639,6 +705,10 @@ packages:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true dev: true
/chroma-js/2.4.2:
resolution: {integrity: sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==}
dev: false
/color-convert/2.0.1: /color-convert/2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@ -650,6 +720,13 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true dev: true
/combined-stream/1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
dependencies:
delayed-stream: 1.0.0
dev: false
/concat-map/0.0.1: /concat-map/0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true dev: true
@ -680,6 +757,10 @@ packages:
ms: 2.1.2 ms: 2.1.2
dev: true dev: true
/dedent-js/1.0.1:
resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==}
dev: false
/deep-is/0.1.4: /deep-is/0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true dev: true
@ -689,6 +770,11 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/delayed-stream/1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dev: false
/detect-indent/6.1.0: /detect-indent/6.1.0:
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -959,6 +1045,25 @@ packages:
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
dev: true dev: true
/follow-redirects/1.15.2:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dev: false
/form-data/4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
dev: false
/fs.realpath/1.0.0: /fs.realpath/1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: true dev: true
@ -973,7 +1078,14 @@ packages:
/function-bind/1.1.1: /function-bind/1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
dev: true
/get-intrinsic/1.1.3:
resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==}
dependencies:
function-bind: 1.1.1
has: 1.0.3
has-symbols: 1.0.3
dev: false
/glob-parent/5.1.2: /glob-parent/5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
@ -1040,12 +1152,16 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/has-symbols/1.0.3:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
engines: {node: '>= 0.4'}
dev: false
/has/1.0.3: /has/1.0.3:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
engines: {node: '>= 0.4.0'} engines: {node: '>= 0.4.0'}
dependencies: dependencies:
function-bind: 1.1.1 function-bind: 1.1.1
dev: true
/ignore/5.2.4: /ignore/5.2.4:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
@ -1119,6 +1235,10 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/is-promise/4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
dev: false
/isexe/2.0.0: /isexe/2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true dev: true
@ -1142,6 +1262,10 @@ packages:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
dev: true dev: true
/jwt-decode/3.1.2:
resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==}
dev: false
/kleur/4.1.5: /kleur/4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -1166,6 +1290,12 @@ packages:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true dev: true
/lower-case/2.0.2:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
dependencies:
tslib: 2.4.1
dev: false
/lru-cache/6.0.0: /lru-cache/6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1199,6 +1329,18 @@ packages:
picomatch: 2.3.1 picomatch: 2.3.1
dev: true dev: true
/mime-db/1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: false
/mime-types/2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: false
/mime/3.0.0: /mime/3.0.0:
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@ -1255,11 +1397,22 @@ packages:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true dev: true
/no-case/3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
dependencies:
lower-case: 2.0.2
tslib: 2.4.1
dev: false
/normalize-path/3.0.0: /normalize-path/3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/object-inspect/1.12.2:
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
dev: false
/once/1.4.0: /once/1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies: dependencies:
@ -1299,6 +1452,13 @@ packages:
callsites: 3.1.0 callsites: 3.1.0
dev: true dev: true
/pascal-case/3.1.2:
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
dependencies:
no-case: 3.0.4
tslib: 2.4.1
dev: false
/path-exists/4.0.0: /path-exists/4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1362,11 +1522,22 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/proxy-from-env/1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
dev: false
/punycode/2.1.1: /punycode/2.1.1:
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
/qs/6.11.0:
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
engines: {node: '>=0.6'}
dependencies:
side-channel: 1.0.4
dev: false
/queue-microtask/1.2.3: /queue-microtask/1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true dev: true
@ -1480,6 +1651,14 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/side-channel/1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
call-bind: 1.0.2
get-intrinsic: 1.1.3
object-inspect: 1.12.2
dev: false
/sirv/2.0.2: /sirv/2.0.2:
resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==} resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
@ -1550,6 +1729,12 @@ packages:
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dev: true dev: true
/svelecte/3.13.0:
resolution: {integrity: sha512-PwAV9+45+fVJsWFiM+xX+82qKs+GuL1hSUIajnEMMjbomLgoT6b0Z4dcyIRSjtCJrUtbBVQF6UG2Ekx4HFldNA==}
dependencies:
svelte-tiny-virtual-list: 2.0.5
dev: false
/svelte-check/2.10.3_sass@1.57.1+svelte@3.55.0: /svelte-check/2.10.3_sass@1.57.1+svelte@3.55.0:
resolution: {integrity: sha512-Nt1aWHTOKFReBpmJ1vPug0aGysqPwJh2seM1OvICfM2oeyaA62mOiy5EvkXhltGfhCcIQcq2LoE0l1CwcWPjlw==} resolution: {integrity: sha512-Nt1aWHTOKFReBpmJ1vPug0aGysqPwJh2seM1OvICfM2oeyaA62mOiy5EvkXhltGfhCcIQcq2LoE0l1CwcWPjlw==}
hasBin: true hasBin: true
@ -1578,6 +1763,12 @@ packages:
- sugarss - sugarss
dev: true dev: true
/svelte-forms/2.3.1:
resolution: {integrity: sha512-ExX9PM0JgvdOWlHl2ztD7XzLNPOPt9U5hBKV8sUAisMfcYWpPRnyz+6EFmh35BOBGJJmuhTDBGm5/7seLjOTIA==}
dependencies:
is-promise: 4.0.0
dev: false
/svelte-hmr/0.15.1_svelte@3.55.0: /svelte-hmr/0.15.1_svelte@3.55.0:
resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==} resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==}
engines: {node: ^12.20 || ^14.13.1 || >= 16} engines: {node: ^12.20 || ^14.13.1 || >= 16}
@ -1587,6 +1778,25 @@ packages:
svelte: 3.55.0 svelte: 3.55.0
dev: true dev: true
/svelte-icons/2.1.0:
resolution: {integrity: sha512-rHPQjweEc9fGSnvM0/4gA3pDHwyZyYsC5KhttCZRhSMJfLttJST5Uq0B16Czhw+HQ+HbSOk8kLigMlPs7gZtfg==}
dev: false
/svelte-multiselect/8.2.3:
resolution: {integrity: sha512-cCnPFkG+0i2eBDaYUOgmQVa2TaJ6Xdjly0/tpch0XCfu4Rs0whbnEXP4QfKVloaAxEDUXwiIq/FHEYZ61xAklg==}
dev: false
/svelte-navigator/3.2.2_niwyv7xychq2ag6arq5eqxbomm:
resolution: {integrity: sha512-Xio4ohLUG1nQJ+ENNbLphXXu9L189fnI1WGg+2Q3CIMPe8Jm2ipytKQthdBs8t0mN7p3Eb03SE9hq0xZAqwQNQ==}
peerDependencies:
svelte: 3.x
dependencies:
svelte: 3.55.0
svelte2tsx: 0.1.193_niwyv7xychq2ag6arq5eqxbomm
transitivePeerDependencies:
- typescript
dev: false
/svelte-preprocess/4.10.7_cfliyikhlimajcn5n7qvd3jsli: /svelte-preprocess/4.10.7_cfliyikhlimajcn5n7qvd3jsli:
resolution: {integrity: sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==} resolution: {integrity: sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==}
engines: {node: '>= 9.11.2'} engines: {node: '>= 9.11.2'}
@ -1639,10 +1849,36 @@ packages:
typescript: 4.9.4 typescript: 4.9.4
dev: true dev: true
/svelte-routing/1.6.0_niwyv7xychq2ag6arq5eqxbomm:
resolution: {integrity: sha512-+DbrSGttLA6lan7oWFz1MjyGabdn3tPRqn8Osyc471ut2UgCrzM5x1qViNMc2gahOP6fKbKK1aNtZMJEQP2vHQ==}
peerDependencies:
svelte: ^3.20.x
dependencies:
svelte: 3.55.0
svelte2tsx: 0.1.193_niwyv7xychq2ag6arq5eqxbomm
transitivePeerDependencies:
- typescript
dev: false
/svelte-tiny-virtual-list/2.0.5:
resolution: {integrity: sha512-xg9ckb8UeeIme4/5qlwCrl2QNmUZ8SCQYZn3Ji83cUsoASqRNy3KWjpmNmzYvPDqCHSZjruBBsoB7t5hwuzw5g==}
dev: false
/svelte/3.55.0: /svelte/3.55.0:
resolution: {integrity: sha512-uGu2FVMlOuey4JoKHKrpZFkoYyj0VLjJdz47zX5+gVK5odxHM40RVhar9/iK2YFRVxvfg9FkhfVlR0sjeIrOiA==} resolution: {integrity: sha512-uGu2FVMlOuey4JoKHKrpZFkoYyj0VLjJdz47zX5+gVK5odxHM40RVhar9/iK2YFRVxvfg9FkhfVlR0sjeIrOiA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
dev: true
/svelte2tsx/0.1.193_niwyv7xychq2ag6arq5eqxbomm:
resolution: {integrity: sha512-vzy4YQNYDnoqp2iZPnJy7kpPAY6y121L0HKrSBjU/IWW7DQ6T7RMJed2VVHFmVYm0zAGYMDl9urPc6R4DDUyhg==}
peerDependencies:
svelte: ^3.24
typescript: ^4.1.2
dependencies:
dedent-js: 1.0.1
pascal-case: 3.1.2
svelte: 3.55.0
typescript: 4.9.4
dev: false
/text-table/0.2.0: /text-table/0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@ -1673,7 +1909,6 @@ packages:
/tslib/2.4.1: /tslib/2.4.1:
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
dev: true
/tsutils/3.21.0_typescript@4.9.4: /tsutils/3.21.0_typescript@4.9.4:
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
@ -1701,7 +1936,6 @@ packages:
resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==} resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==}
engines: {node: '>=4.2.0'} engines: {node: '>=4.2.0'}
hasBin: true hasBin: true
dev: true
/undici/5.14.0: /undici/5.14.0:
resolution: {integrity: sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==} resolution: {integrity: sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==}

View File

@ -0,0 +1,15 @@
import { browser } from '$app/environment';
import axios from 'axios';
import { parse, stringify } from 'qs'
export const exoInstance = axios.create({
paramsSerializer:{encode:(params)=> {return parse(params, {arrayFormat:"brackets"})}, serialize: (p)=>{return stringify(p, {arrayFormat: "repeat"})}},
baseURL: `http://127.0.0.1:8002`,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'Access-Control-Allow-Origin': '*',
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
...(browser &&
localStorage.getItem('token') && { Authorization: `Bearer ${localStorage.getItem('token')}` })
}
});

View File

@ -1,4 +1,82 @@
/* Write your global styles here, in SCSS syntax. Variables and mixins from the src/variables.scss file are available here without importing */ /* Write your global styles here, in SCSS syntax. Variables and mixins from the src/variables.scss file are available here without importing */
.input{
background-color: red; * {
box-sizing: border-box;
margin: 0;
}
.btn {
border: none;
border-radius: 5px;
height: 38px;
font-weight: 700;
transition: 0.3s;
margin-bottom: 10px;
margin-right: 7px;
padding: 0 10%;
width: max-content;
cursor: pointer;
}
.primary-btn {
@extend .btn;
background-color: #fcbf49;
&:hover {
background-color: #ac7b19;
}
}
.danger-btn {
@extend .btn;
background-color: #fc5e49;
&:hover {
background-color: #ac1919;
}
}
.border-primary-btn {
@extend .btn;
background-color: transparent;
border: 1px solid #fcbf49;
color: #fcbf49;
&:hover {
background-color: #fcbf49;
color: black;
}
}
.input {
background-color: inherit;
color: inherit;
padding: 5px 10px;
width: 100%;
font-size: 16px;
font-weight: 450;
margin: 10px 0;
float: left;
border: none;
border-bottom: 1px solid #181553;
transition: 0.3s;
border-radius: 0;
margin: 0;
&:focus {
outline: none;
border-bottom-color: red;
}
}
.flex-row-center {
display: flex;
justify-content: center;
}
@for $f from 0 through 100 {
.wp-#{$f} {
width: 1% * $f;
}
}
.sv-dropdown{
z-index: 10!important;
} }

View File

@ -1,49 +1,143 @@
<script lang="ts"> <script lang="ts">
import type { Exercice } from '../../types/exo.type';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import ModalCard from './ModalCard.svelte'; import ModalCard from './ModalCard.svelte';
import { goto } from '$app/navigation';
import { cloneExo } from '../../requests/exo.request';
import TagContainer from './TagContainer.svelte';
import PrivacyIndicator from './PrivacyIndicator.svelte';
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte';
export let exo: Exercice;
export let title: string; const { show } = getContext<{ show: Function }>('modal');
export let examples: Array<string>; const { navigate } = getContext<{ navigate: Function }>('navigation');
export let tags: Array<string>; const { isAuth } = getContext<{ isAuth: boolean }>('auth');
const exerciceStore = getContext('exos');
const { show } = getContext<{show: Function}>('modal'); const tagsStore = getContext('tags');
let opened = false;
const handleClick = () => { const handleClick = () => {
console.log('OOOPPP'); opened = true;
show(ModalCard, {exo: {title, examples, tags}}) navigate(`/exercices/${exo.id_code}`);
}; };
let tg = false;
$: !!opened &&
show(
ModalCard,
{
exo,
exos: exerciceStore,
tags: tagsStore
},
() => {
navigate(-1);
opened = false;
}
);
</script> </script>
<div class="card" on:click={handleClick}> <div
<h1>{title}</h1> class="card"
class:tagMode={tg}
on:click={handleClick}
on:dblclick={() => {}}
on:keypress={() => {}}
>
<h1>{exo.name}</h1>
<div class="examples"> <div class="examples">
<h2>Exemples</h2> <h2>Exemples</h2>
{#each examples.slice(0, 3) as ex} {#if !!exo.consigne}<p>{exo.consigne}</p>{/if}
<p>{ex}</p> {#each exo.examples.data.slice(0, 3) as ex}
<p>{ex.calcul}</p>
{/each} {/each}
</div> </div>
{#if !!isAuth}
{#if exo.is_author && exo.original == null}
<div class="status">
<PrivacyIndicator color={exo.private == true ? 'red' : 'green'}>
{exo.private == true ? 'Privé' : 'Public'}</PrivacyIndicator
>
</div>
{:else if !exo.is_author}
<div class="status">
<PrivacyIndicator color={'blue'}>
Par <strong>{exo.author.username}</strong>
</PrivacyIndicator>
<div <div
class="tags" class="icon"
on:click={() => { on:keydown={() => {}}
alert('test'); on:click|stopPropagation={() => {
cloneExo(exo.id_code).then((r) => {
goto('/exercices/' + r.id_code);
show(ModalCard, { exo: r }, () => {
goto('/exercices/user');
});
});
}} }}
/> >
<MdContentCopy />
</div>
</div>
{:else if exo.is_author && exo.original != null}
<div class="status">
<PrivacyIndicator color="blue">Par <strong>{exo.original?.author}</strong></PrivacyIndicator
>
</div>
{/if}{/if}
<div class="card-hover" /> <div class="card-hover" />
{#if !!isAuth}
<TagContainer bind:exo />
{/if}
<!-- TagContainer Must be directly after card-hover for the hover effect -->
</div> </div>
<style lang="scss"> <style lang="scss">
@import '../../variables';
* { * {
transition: 0.45s; transition: 0.45s;
} }
.icon {
width: 18px;
height: 18px;
transition: 0.3s;
cursor: pointer;
opacity: 0.7;
transform: scale(0.9);
&:hover {
transform: scale(1);
opacity: 1;
}
}
.status {
position: absolute;
top: 0;
right: 0;
margin: 10px;
z-index: 3;
display: flex;
align-items: center;
gap: 10px;
}
.examples { .examples {
color: gray; color: gray;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 20px;
p { p {
margin: 10px;
margin-left: 18px; margin-left: 18px;
font-size: 0.95em; font-size: 0.95em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
h2 { h2 {
font-size: 0.95em; font-size: 0.95em;
margin: 10px;
margin-left: 0;
} }
} }
@ -54,9 +148,16 @@
left: 0; left: 0;
bottom: 0; bottom: 0;
z-index: 1; z-index: 1;
border: 1px solid green; border: 1px solid $border;
+ :global(div) {
transition: 0.45s;
}
&:hover { &:hover {
border: 1px solid red; border: 1px solid $primary;
+ :global(div) {
border: 1px solid $primary;
border-top: none;
}
} }
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.75); box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.75);
} }
@ -66,19 +167,17 @@
margin: 0; margin: 0;
position: relative; position: relative;
z-index: 2; z-index: 2;
&:hover { max-width: 88%;
color: red;
}
}
.tags { overflow: hidden;
height: 20px; text-overflow: ellipsis;
position: absolute; display: -webkit-box;
bottom: 1px; -webkit-line-clamp: 2;
background-color: blue; -webkit-box-orient: vertical;
right: 1px; word-wrap: break-word;
left: 1px; &:hover {
z-index: 3; color: $primary;
}
} }
.card { .card {
@ -86,9 +185,10 @@
padding: 20px; padding: 20px;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
background-color: violet; background-color: $background;
min-height: 250px; min-height: 250px;
&:hover { max-height: 300px;
&:not(.tagMode):hover {
transform: translateX(10px) translateY(-10px); transform: translateX(10px) translateY(-10px);
} }
} }

View File

@ -0,0 +1,32 @@
<script lang="ts">
import type { Exercice, Page } from '../../types/exo.type';
import type { Writable } from 'svelte/store';
import EditForm from './EditForm.svelte';
export let cancel: Function;
export let exos: Writable<{ isLoading: boolean; isFetching: boolean; data: Page }>;
const updateExo = (e: Exercice) => {
exos.update((o) => {
return { ...o, data: { ...o.data, items: [e, ...o.data.items] } };
});
};
</script>
<div>
<h1>Nouvel exercice</h1>
<EditForm editing={false} {cancel} {updateExo} />
</div>
<style>
div {
background-color: blue;
padding: 50px;
display: flex;
flex-direction: column;
gap: 20px;
align-items: flex-start;
}
h1 {
font-size: 1.5em;
}
</style>

View File

@ -0,0 +1,199 @@
<script lang="ts">
import MdClose from 'svelte-icons/md/MdClose.svelte';
import MdEdit from 'svelte-icons/md/MdEdit.svelte';
import MdDelete from 'svelte-icons/md/MdDelete.svelte';
import { getContext } from 'svelte';
import InputWithLabel from '../forms/InputWithLabel.svelte';
import { useMutation, useQueryClient } from '@sveltestack/svelte-query';
import { cloneExo, delExo, getExo } from '../../requests/exo.request';
import type { Exercice } from 'src/types/exo.type';
import TagContainer from './TagContainer.svelte';
import PrivacyIndicator from './PrivacyIndicator.svelte';
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte';
import ModalCard from './ModalCard.svelte';
import { goto } from '$app/navigation';
export let exo: Exercice;
export let edit: Function;
export let delete_: Function;
const { close, show } = getContext<{ close: Function; show: Function }>('modal');
const { alert } = getContext<{ alert: Function }>('alert');
const { isAuth } = getContext<{ isAuth: boolean }>('auth');
let name = '';
</script>
{#if exo != null}
<h1>
<span class="name">{exo.name}</span>
{#if exo.is_author && exo.original == null}
<span
><PrivacyIndicator color={exo.private == true ? 'red' : 'green'}
>{exo.private == true ? 'Privé' : 'Public'}</PrivacyIndicator
></span
>
{:else if exo.is_author && exo.original != null}
<span
on:click={() => {
if (exo.original == null) return;
getExo(exo.original?.id_code).then((r) => {
goto(`/exercices/${exo.original?.id_code}`);
show(
ModalCard,
{ exo: r },
() => {
goto('/exercices/user');
},
true
);
});
}}
on:keyup={() => {}}
>
<PrivacyIndicator color="blue"
>Exercice original de <strong>{exo.original?.author}</strong></PrivacyIndicator
>
</span>
{:else if !exo.is_author && exo.original == null}
<span>
<PrivacyIndicator color="blue">Par <strong>{exo.author.username}</strong></PrivacyIndicator>
</span>
{/if}
</h1>
<InputWithLabel type="text" value={name} label="Nom" />
<div class="examples">
<h2>Exemples</h2>
{#if !!exo.consigne}<p>{exo.consigne}</p>{/if}
{#each exo.examples.data as e}
<p>{e.calcul}</p>
{/each}
</div>
<div class="flex-row-center wp-100">
<button class="primary-btn">Télécharger</button>
</div>
<div class="tags" />
{#if !!isAuth}
<TagContainer {exo} />
{/if}
<div class="icons">
<div>
<div class="icon" style:color="black" on:click={() => close()} on:keypress={() => {}}>
<MdClose />
</div>
</div>
<div>
{#if !!isAuth}
{#if exo.is_author}
<div
class="icon"
style:color="red"
on:click={() => {
alert({
title: 'Sur ?',
description: 'Voulez vous supprimer ? ',
validate: () => {
close();
delete_();
}
});
}}
on:keypress={() => {}}
>
<MdDelete />
</div>
<div class="icon" style:color="green" on:click={() => edit()} on:keypress={() => {}}>
<MdEdit />
</div>
{:else}
<div
class="icon"
style:color="#f0f0f0"
on:click={() => {
cloneExo(exo.id_code).then((r) => {
goto('/exercices/' + r.id_code);
show(
ModalCard,
{ exo: r },
() => {
goto('/exercices/user');
},
true
);
});
}}
on:keypress={() => {}}
title="Copier l'exercice pour pouvoir le modifier"
>
<MdContentCopy />
</div>
{/if}
{/if}
</div>
</div>
{/if}
<style lang="scss">
.icon {
width: 25px;
height: 25px;
transition: 0.3s;
cursor: pointer;
opacity: 0.7;
transform: scale(0.9);
&:hover {
transform: scale(1);
opacity: 1;
}
}
.icons {
position: absolute;
top: 0;
margin: 30px;
gap: 10px;
display: flex;
justify-content: space-between;
right: 0;
left: 0;
div {
display: flex;
}
}
h1 {
font-size: 1.5em;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
span.name {
overflow: hidden;
word-wrap: break-word;
}
span:not(.name) {
position: relative;
height: min-content;
font-size: 18px;
cursor: pointer;
}
}
.examples {
h2 {
font-size: 1em;
font-weight: 800;
margin-bottom: 5px;
}
p {
margin: 5px 0;
margin-left: 30px;
color: gray;
font-weight: 400;
}
}
</style>

View File

@ -0,0 +1,118 @@
<script lang="ts">
import { form, field } from 'svelte-forms';
import { required, max, min } from 'svelte-forms/validators';
import FileInput from '../forms/FileInput.svelte';
import InputWithLabel from '../forms/InputWithLabel.svelte';
import { getContext } from 'svelte';
import { createExo, editExo } from '../../requests/exo.request';
import type { Exercice } from '../../types/exo.type';
import { checkFile, errorMsg } from '../../utils/forms';
import { compareObject } from '../../utils/utils';
import { goto } from '$app/navigation';
export let editing = true;
export let updateExo: Function = (e: Exercice) => {};
export let exo: Exercice | null = null;
export let cancel: Function;
const { alert } = getContext<{ alert: Function }>('alert');
// "Legally" initiate empty FileList for model field (simple list raises warning)
let list = new DataTransfer();
let file = new File(['content'], !editing || exo == null ? 'filename.py' : exo.exo_source);
list.items.add(file);
!editing && list.items.remove(0);
// Initiate fields and form
const name = field('name', !!exo ? exo.name : '', [required(), max(50), min(5)], {
checkOnInit: true
});
const consigne = field('consigne', !!exo && exo.consigne != null ? exo.consigne : '', [max(200)], { checkOnInit: true });
const prv = field('private', !!exo ? exo.private : false);
const model = field('model', list.files, [checkFile(), required()], {
checkOnInit: !editing
});
const myForm = form(name, consigne, prv, model);
</script>
<form
action=""
on:submit|preventDefault={() => {
if (editing && exo != null) {
editExo(exo.id_code, {
name: $name.value,
consigne: $consigne.value,
private: $prv.value,
...($model.dirty == true && { file: $model.value[0] })
}).then((r) => {
exo=r.data
updateExo(r.data);
cancel()
});
} else {
createExo({
name: $name.value,
consigne: $consigne.value,
private: $prv.value,
file: $model.value[0]
}).then((r) => {
updateExo(r.data);
cancel()
});
}
}}
>
<InputWithLabel
type="text"
bind:value={$name.value}
maxlength="50"
minlength="5"
required
label="Nom"
errors={errorMsg($myForm, 'name')}
/>
<InputWithLabel
type="text"
bind:value={$consigne.value}
maxlength="200"
label="Consigne"
errors={errorMsg($myForm, 'consigne')}
/>
<div>
<input type="checkbox" bind:checked={$prv.value} name="private" id="private" />
<label for="private">Privé</label>
</div>
<FileInput bind:value={$model.value} accept=".py" id_code={exo?.id_code} />
<div class="wp-100">
<button class="primary-btn" disabled={!$myForm.valid}>Valider</button>
<button
class="danger-btn"
on:click|preventDefault={() => {
if (exo != null && ($model.dirty || !compareObject({...exo, consigne: exo.consigne == null ? "": exo.consigne}, myForm.summary()))) {
alert({
title: 'test',
description:
'Aliquip in cupidatat anim tempor quis est sint qui sunt. Magna consequat excepteur deserunt ullamco quis.',
validate: cancel
});
} else {
cancel();
}
}}>Annuler</button
>
</div>
</form>
<style>
form {
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
</style>

View File

@ -1,31 +1,89 @@
<script> <script lang="ts">
import { each } from 'svelte/internal'; import { getContext } from 'svelte/internal';
import Card from './Card.svelte'; import Card from './Card.svelte';
import Head from './Head.svelte';
import ModalCard from './ModalCard.svelte'; import ModalCard from './ModalCard.svelte';
let exos = [ import { Query, useQueryClient, type QueryOptions } from '@sveltestack/svelte-query';
{ import { getExo, getExos, getTags } from '../../requests/exo.request';
title: 'test1', import { page } from '$app/stores';
examples: ['an example', 'an example', 'an example', 'an example', 'an example'], import { onMount } from 'svelte';
tags: [] import Pagination from './Pagination.svelte';
}, import { goto } from '$app/navigation';
{ import { writable } from 'svelte/store';
title: 'test2', import { setContext } from 'svelte';
examples: ['an example', 'an example', 'an example', 'an example', 'an example'], import type { Page, Tag } from '../../types/exo.type';
tags: [] import type { Store } from '../../types/api.type';
},
{ const { show } = getContext<{ show: Function }>('modal');
title: 'test3', const { navigate } = getContext<{ navigate: Function }>('navigation');
examples: ['an example', 'an example', 'an example', 'an example', 'an example'],
tags: [] let filter = 'user';
},
{ const exerciceStore = writable<Store<Page|undefined>>({
title: 'test1', isLoading: false,
examples: ['an example', 'an example', 'an example', 'an example', 'an example'], isFetching: false,
tags: [] isSuccess: false,
data: undefined
});
const tagStore = writable<Store<Tag[]|undefined>>({
isLoading: false,
isFetching: false,
isSuccess: false,
data: undefined
});
setContext('exos', exerciceStore);
setContext('tags', tagStore);
onMount(() => {
if ($page.params.slug != undefined && !['user', 'public'].includes($page.params.slug)) {
getExo($page.params.slug).then((r) => {
show(ModalCard, { exo: r, exos: exerciceStore, tags: tagStore }, () => navigate('/exercices/' + filter));
});
} else if ($page.params.slug == undefined) {
goto('/exercices/public');
} }
]; });
$: filter = ['user', 'public'].includes($page.params.slug) ? $page.params.slug : filter;
$: {
exerciceStore.update((s) => {
return { ...s, isFetching: true };
});
getExos(filter as 'public' | 'user', {
page: activePage,
search,
tags: [...selected.map((t) => t.id_code)]
}).then((r) => {
exerciceStore.update((e) => {
return { ...e, isSuccess: true, isFetching: false, data: r };
});
});
}
$: {
tagStore.update((s)=>{return {...s, isFetching: true}});
getTags().then(r=>{
tagStore.update((e) => {
return { ...e, isSuccess: true, isFetching: false, data: r };
});
})
}
let activePage = parseInt($page.url.searchParams.get('page')!) || 1;
let search = '';
let selected: Tag[] = [];
</script> </script>
{#if $tagStore.isSuccess == true && $tagStore.data != undefined}
<Head location={filter} bind:search bind:selected />
{/if}
{#if $tagStore.isFetching == true}
Fetching
{/if}
<div class="feed"> <div class="feed">
<div class="title"> <div class="title">
<h1> <h1>
@ -36,9 +94,14 @@
publics publics
</p> </p>
</div> </div>
{#each exos as e} {#if $exerciceStore.data != undefined}
<Card examples={e.examples} title={e.title} tags={e.tags} /> {#each $exerciceStore.data.items.filter((e) => e != null && selected.every((t) => e.tags
.map((s) => s.id_code)
.includes(t.id_code))) as e}
<Card bind:exo={e} />
{/each} {/each}
<Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} />
{/if}
</div> </div>
<style lang="scss"> <style lang="scss">
@ -48,6 +111,7 @@
grid-auto-flow: dense; grid-auto-flow: dense;
grid-gap: 32px; grid-gap: 32px;
margin: 0 auto; margin: 0 auto;
margin-top: 20px;
} }
.title { .title {

View File

@ -0,0 +1,68 @@
<script lang="ts">
import TagSelector from '../forms/TagSelector.svelte';
import { getContext } from 'svelte';
import CreateCard from './CreateCard.svelte';
import { useQueryClient } from '@sveltestack/svelte-query';
import type { Page, Tag } from '../../types/exo.type';
import type { Writable } from 'svelte/store';
import type { Store } from '../../types/api.type';
const { navigate } = getContext<{navigate: Function}>('navigation');
const { show, close } = getContext<{show: Function, close: Function}>('modal');
export let location = 'public';
export let search = '';
export let selected: Tag[] = [];
const { isAuth } = getContext<{ isAuth: boolean }>('auth');
const tags: Writable<Store<Tag[]>> = getContext('tags')
const exerciceStore: Writable<Store<Page>> = getContext('exos');
</script>
<div class="head">
<div class="new">
{#if !!isAuth}
<button
class="border-primary-btn"
on:click={() => {
show(CreateCard, {
cancel: () => {
close();
},
exos: exerciceStore
});
}}>Nouveau</button
>
{/if}
</div>
<div class="search">
<input type="text" placeholder="Rechercher" class="input" bind:value={search} />
{#if !!isAuth}
<TagSelector options={$tags.data} bind:selected />
<select
name="ee"
id="e"
class="input"
bind:value={location}
on:change={(e) => navigate(`/exercices/${e.currentTarget.value}`)}
>
<option value="user">Vos exos</option>
<option value="public">Tous les exos</option>
</select>{/if}
</div>
</div>
<style lang="scss">
.head {
display: flex;
justify-content: space-between;
align-items: center;
div {
width: 50%;
}
}
.search {
display: flex;
flex-direction: column;
gap: 5px;
}
</style>

View File

@ -1,44 +1,77 @@
<script> <script lang="ts">
export let exo; import MdClose from 'svelte-icons/md/MdClose.svelte';
let examples = ['an example', 'an example', 'an example', 'an example', 'an example']; import MdEdit from 'svelte-icons/md/MdEdit.svelte';
import MdDelete from 'svelte-icons/md/MdDelete.svelte';
import { getContext, setContext } from 'svelte';
import DownloadForm from './DownloadForm.svelte';
import EditForm from './EditForm.svelte';
import type { Exercice, Page, Tag } from '../../types/exo.type';
import type { Writable } from 'svelte/store';
import type { Store } from '../../types/api.type';
export let exo: Exercice;
export let exos: Writable<Store<Page>>;
export let tags: Writable<Store<Tag[]>>;
setContext("tags", tags)
let editing = false;
const updateExo = (e: Exercice) => {
exos.update((o) => {
return {
...o,
data: {
...o.data,
items: [
...o.data.items.map((ex) => {
if (ex.id_code == exo.id_code) {
return e;
} return ex;
})
]
}
};
});
};
</script> </script>
<div class="modal"> <div class="modal">
<h1>Titre</h1> {#if editing === false}
<input type="text" class="input" /> <DownloadForm
<div class="examples"> {exo}
<h2>Exemples</h2> delete_={() => {
{#each examples as e} exos.update((o) => {
<p>{e}</p> return {
{/each} ...o,
</div> data: { ...o.data, items: [...o.data.items.filter((e) => e.id_code != exo.id_code)] }
<button>Télécharger</button> };
<div class="tags" /> });
}}
edit={() => {
editing = true;
}}
/>
{:else if editing === true}
<EditForm
bind:exo
cancel={() => {
editing = false;
}}
{updateExo}
/>
{/if}
</div> </div>
<style lang="scss"> <style lang="scss">
@import '../../variables';
.modal { .modal {
min-width: 820px; min-width: 820px;
display: grid; background: $background;
grid-template-columns: 1fr; padding: 70px;
grid-template-rows: repeat(5, auto); grid-gap: 10px;
background: blue; position: relative;
padding:70px; display: flex;
grid-gap:10px; flex-direction: column;
} align-items: flex-start;
h1 { gap: 20px;
font-size: 1.5em;
}
.examples {
h2 {
font-size: 1em;
font-weight: 800;
margin-bottom: 5px;
}
p {
margin: 5px 0;
margin-left: 30px;
}
} }
</style> </style>

View File

@ -0,0 +1,35 @@
<script lang="ts">
import {page as p} from '$app/stores'
import { goto } from "$app/navigation";
export let page: number;
export let total: number;
</script>
<div class="pagination">
{#each Array(total) as _, i}
<p
class:active={page == i + 1}
on:click={() => {
page = i + 1;
$p.url.searchParams.set('page', String(i+1))
goto(`?${$p.url.searchParams.toString()}`);
}}
on:keydown = {()=>{}}
>
{i + 1}
</p>
{/each}
</div>
<style lang="scss">
.active {
color: red;
}
.pagination {
display: flex;
margin: 30px;
p {
margin: 10px;
}
}
</style>

View File

@ -0,0 +1,45 @@
<script lang="ts">
export let color: 'red' | "green" | 'blue'= "red";
</script>
<p class={color}><slot /></p>
<style lang="scss">
@import '../../variables';
.red {
color: $red;
}
.green{
color: $green;
}
.blue {
color: $contrast;
}
p {
font-size: 0.8em;
height: 18px;
padding: 2px 5px;
font-weight: 400;
line-height: 18px;
border-radius: 3px;
box-sizing: content-box;
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: currentColor;
opacity: 0.2;
transition: opacity 0.2s;
z-index: 0;
border-radius: 3px;
}
&:hover::after{
opacity: .3;
}
}
</style>

View File

@ -0,0 +1,85 @@
<script lang="ts">
import chroma from 'chroma-js';
export let label: string;
export let color: string;
export let remove: Function;
let removed = false;
</script>
<div class:removed class="selected" style={`--item-color:${chroma(color).rgb().join(',')};`}>
<div class="label">{label}</div>
<div
class="unselect"
on:click={() => {
removed = true;
remove()
/* setTimeout(() => {
if(!remove()){
removed=false
}
}, 300); */
}}
on:keypress={() => {}}
>
<svg
height="14"
width="14"
viewBox="0 0 20 20"
aria-hidden="true"
focusable="false"
class="css-8mmkcg"
><path
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
/></svg
>
</div>
</div>
<style lang="scss">
.unselect {
color: rgb(var(--item-color));
-moz-box-align: center;
align-items: center;
border-radius: 2px;
display: flex;
padding-left: 4px;
padding-right: 4px;
box-sizing: border-box;
cursor: pointer;
svg {
fill: currentColor;
}
&:hover {
background-color: currentColor;
svg {
fill: white;
}
}
}
.selected {
background-color: rgba(var(--item-color), 0.1);
border-radius: 2px;
display: flex;
margin: 2px;
min-width: 0px;
box-sizing: border-box;
transition: 0.5s;
max-width: 100px;
}
.removed {
max-width: 0;
}
.label {
color: rgb(var(--item-color));
border-radius: 2px;
font-size: 85%;
overflow: hidden;
padding: 3px 3px 3px 6px;
text-overflow: ellipsis;
white-space: nowrap;
box-sizing: border-box;
}
</style>

View File

@ -0,0 +1,165 @@
<script lang="ts">
import { useMutation, useQueryClient } from '@sveltestack/svelte-query';
import { addTags, delTags } from '../../requests/exo.request';
import type { Exercice, Tag as TagType } from '../../types/exo.type';
import TagSelector from '../forms/TagSelector.svelte';
import Tag from './Tag.svelte';
import { page } from '$app/stores';
import { getContext } from 'svelte';
import type { Writable } from 'svelte/store';
import type { Store } from '../../types/api.type';
export let exo: Exercice;
let tg = false;
let tagMode = false;
let selected: { label: string; id_code: string; color: string, created?: boolean }[] = [];
export let tags: Writable<Store<TagType[]>> = getContext('tags');
console.log('TAGS +', tags, getContext('test'))
</script>
<div
class="tags-container"
class:tg
class:tagMode
on:click|stopPropagation={() => {}}
on:keypress={() => {}}
>
{#if tg === false}
<div class="tags">
{#each exo.tags as t}
<Tag
color={t.color}
remove={() => {
delTags(exo.id_code, t.id_code).then((r) => {
exo.tags = r.tags;
});
return true;
}}
label={t.label}
/>
{/each}
</div>
<div
class="expand"
on:click={() => {
tg = true;
console.log('TAGGGG', $tags)
setTimeout(() => {
tagMode = true;
}, 200);
}}
on:keydown={() => {}}
>
<p>+</p>
</div>
{:else}
<TagSelector disabled={exo.tags} bind:selected options={$tags.data} creatable allowEditing />
<button
class="primary-btn"
on:click|preventDefault={() => {
addTags(
exo.id_code,
selected.map((t) => {
delete t.created;
return t;
})
).then((r) => {
exo.tags = r.exo.tags;
if (r.tags.length != 0) {
tags.update((tt) => {
return { ...tt, data: [...tt.data, ...r.tags] };
});
}
tg = false;
tagMode = false;
});
}}>Valider !</button
>
<button
class="danger-btn"
on:click={() => {
tagMode = false;
tg = false;
}}>Annuler</button
>
{/if}
</div>
<style lang="scss">
* {
transition: 0.45s;
}
.tags-container {
min-height: 30px;
position: absolute;
padding: 1% 2%;
padding-right: 0;
bottom: 0;
background-color: darken($background, $amount: 7);
right: 0;
left: 0;
z-index: 3;
border: 1px solid darken($background, $amount: 7);
border-top: none;
display: flex;
justify-content: space-between;
align-items: center;
display: grid;
grid-template-columns: 85% auto;
grid-gap: 8px;
}
.tg {
transition: 0.3s;
min-height: 100%;
:global(> *) {
opacity: 0;
position: absolute;
}
}
.tags {
display: flex;
overflow: auto;
:global(> div) {
flex-shrink: 0;
}
}
.expand {
padding: 0 15px;
font-weight: 900;
font-size: 1.1em;
cursor: pointer;
&:hover p {
transform: rotate(180deg);
}
position: relative;
&::before {
content: '';
background-color: black;
width: 1px;
height: 100%;
display: inline-block;
position: absolute;
left: 0%;
}
}
.tagMode {
transition: 0.5s;
min-height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
padding: 10px;
gap: 5px;
:global(> *) {
opacity: 1;
display: block;
position: static;
}
:global(> div),
:global(> button) {
flex-grow: 0;
width: 100%;
margin: 0;
}
}
</style>

View File

@ -0,0 +1,20 @@
<script lang="ts">
import chroma from 'chroma-js';
export let option: { label: string; color: string };
export let idx;
</script>
<div style={`--color: ${chroma(option.color).rgb().join(',')}`}>
{option.label}
</div>
<style lang="scss">
div {
color: rgb(var(--color));
padding: 10px;
&:hover {
background-color: rgba(var(--color), 0.2);
}
}
</style>

View File

@ -0,0 +1,96 @@
<script lang="ts">
import { getExoSource } from '../../requests/exo.request';
import MdFileDownload from 'svelte-icons/md/MdFileDownload.svelte';
export let label = 'Choisir un fichier';
export let value: FileList;
export let id_code: string | null = null;
const id = String(Math.random());
</script>
<div class="fileinput">
<input type="file" {id} {...$$restProps} bind:files={value} />
<label for={id}>{label}</label>
<div class="filename">
{#if value.length !== 0}
<p>{value[0].name}</p>
{#if id_code != null}
<div
class="icon"
on:click={() => {
if (id_code == null) return;
getExoSource(id_code);
}}
on:keydown={() => {}}
>
<MdFileDownload />
</div>{/if}
{:else}
...
{/if}
</div>
</div>
<style lang="scss">
.icon {
width: 22px;
height: 22px;
display: flex;
cursor: pointer;
transition: 0.2s;
&:hover {
transform: scale(1.1);
}
}
.fileinput {
display: flex;
border: yellow 1px solid;
border-radius: 5px;
align-items: center;
width: max-content;
}
input {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
& + label {
font-size: 1;
font-weight: 700;
color: black;
background-color: yellow;
display: inline-block;
padding: 10px;
border-radius: 5px;
white-space: nowrap;
transition: 0.3s;
&:hover {
background-color: orange;
}
}
}
.filename {
max-width: 500px;
text-align: center;
min-width: 100px;
color: yellow;
font-weight: 700;
text-overflow: ellipsis;
overflow: hidden;
padding: 0 10px;
white-space: nowrap;
cursor: default;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
p {
max-width: 200px;
margin: 0;
text-overflow: ellipsis;
overflow: hidden;
}
}
</style>

View File

@ -0,0 +1,138 @@
<script lang="ts">
import IoMdEye from 'svelte-icons/io/IoMdEye.svelte';
import IoMdEyeOff from 'svelte-icons/io/IoMdEyeOff.svelte';
export let type = 'text';
export let value = '';
export let label = '';
export let errors: string[] = [];
function typeAction(node: HTMLInputElement) {
node.type = type;
}
let show = type != 'password';
const id = String(Math.random());
const toggle = () => {
const element = document.getElementById(id) as HTMLInputElement;
if (element === null) return;
element.type = show === true ? 'password' : 'text';
show = !show;
};
</script>
<span class="inputLabel" class:error={errors.length !== 0}>
<input use:typeAction {id} bind:value {...$$restProps} placeholder="" />
<!-- placeholder = "" pour que le label se place bien avec :placeholder-shown -->
<label for={id}>{label}</label>
{#if type == 'password'}
<div class="toggle" on:click={toggle} on:keypress={() => {}}>
{#if show == false}
<IoMdEyeOff />
{:else if show == true}
<IoMdEye />
{/if}
</div>
{/if}
<span class="bar" />
{#if errors.length !== 0}
<p class="error-msg">{errors[0]}</p>
{/if}
</span>
<style lang="scss">
@import '../../variables';
.error-msg {
color: $red;
font-weight: 800;
margin-top: 5px;
}
.toggle {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
right: 0;
width: 20px;
height: 20px;
cursor: pointer;
}
.inputLabel {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
margin: 0;
margin-top: 10px;
}
input {
background-color: transparent;
border: none;
padding: 10px 10px 10px 5px;
border-bottom: 1px solid $input-border;
width: 100%;
font-size: 0.9em;
font-weight: 500;
color: #f8f8f8;
& ~ label {
font-size: 1em;
font-weight: normal;
position: absolute;
pointer-events: none;
left: 5px;
top: 10px;
transition: 0.3s ease all;
font-weight: 400;
color: #8e8e8e;
opacity: 0.4;
}
&:focus {
outline: none;
}
&:focus ~ label,
&:not(:placeholder-shown) ~ label {
top: -0.8em;
font-size: 12px;
color: $contrast;
opacity: 1;
font-weight: 600;
}
&:disabled ~ label {
color: grey;
}
&:focus ~ .bar:before {
width: 100%;
}
}
.error {
color: $red;
& input {
color: $red;
border-bottom: 1px solid $red;
&:focus ~ label,
&:not(:placeholder-shown):valid ~ label {
color: $red;
}
&:focus ~ .bar::before {
background-color: $red;
}
}
}
.bar {
position: relative;
display: block;
width: 100%;
&:before {
content: '';
height: 2px;
width: 0;
bottom: 0px;
position: absolute;
background: $contrast;
transition: 0.3s ease all;
left: 0%;
}
}
</style>

View File

@ -0,0 +1,130 @@
<script lang="ts">
import { itemActions, highlightSearch, CloseButton } from 'svelecte/item';
import chroma from 'chroma-js';
// these properties can be used
//export let inputValue;
export let index = -1;
export let item: { label: string; id_code: string; color: string };
export let isSelected = false;
export let isDisabled = false;
//export let isMultiple = false;
const color = chroma(item.color);
console.log(color.rgb(), color);
</script>
<!-- you need to use itemActions and pass given events -->
<div
class="sv-item"
use:itemActions={{ item, index }}
class:is-selected={isSelected}
on:select
on:deselect
on:hover
>
<div
class={`sv-item-content`}
style={`--item-color:${chroma(item.color).rgb().join(',')};`}
class:disabled={isDisabled}
>
{#if !isSelected}
{item.label}
{:else}
<div class="selected">
<div class="label">{item.label}</div>
<div class="unselect" data-action="deselect">
<svg
height="14"
width="14"
viewBox="0 0 20 20"
aria-hidden="true"
focusable="false"
class="css-8mmkcg"
><path
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
/></svg
>
</div>
</div>
{/if}
</div>
</div>
<style lang="scss">
.unselect {
color: rgb(var(--item-color));
-moz-box-align: center;
align-items: center;
border-radius: 2px;
display: flex;
padding-left: 4px;
padding-right: 4px;
box-sizing: border-box;
cursor: pointer;
svg {
fill: currentColor;
}
&:hover {
background-color: currentColor;
svg {
fill: white;
}
}
}
.selected {
background-color: rgba(var(--item-color), 0.1);
border-radius: 2px;
display: flex;
margin: 2px;
min-width: 0px;
box-sizing: border-box;
}
.label {
color: rgb(var(--item-color));
border-radius: 2px;
font-size: 85%;
overflow: hidden;
padding: 3px 3px 3px 6px;
text-overflow: ellipsis;
white-space: nowrap;
box-sizing: border-box;
}
/** some style override */
.sv-item {
background-color: $background!important;
padding: 0;
}
.sv-item-content {
color: rgb(var(--item-color));
}
:global(.sv-dropdown-content) .sv-item-content {
padding: 13px;
}
:global(.sv-dropdown-content) .sv-item {
padding: 0;
}
:global(.sv-dd-item-active) .sv-item-content:not(.disabled) {
background-color: rgba(var(--item-color), 0.1);
color: rgb(var(--item-color)) !important;
}
:global(.sv-dd-item-active) .sv-dd-item{
background-color: var(--sv-bg);
}
:global(.sv-content) .sv-item-content {
padding: 0 !important;
background-color: transparent;
}
:global(.sv-content) .sv-item {
background-color: transparent;
}
.disabled,
:global(.sv-dd-item-active) .sv-item-content.disabled {
cursor: not-allowed;
color: grey;
background-color: var(--sv-bg);
}
</style>

View File

@ -0,0 +1,15 @@
<script lang='ts'>
import chroma from 'chroma-js'
export let option: {label: string, color: string};
export let idx;
</script>
{option.label} {option.color}
<button>test</button>
<style>
</style>

View File

@ -0,0 +1,58 @@
<script lang="ts">
import Svelecte, { addFormatter } from 'svelecte';
import { writable, type Writable } from 'svelte/store';
import Item from './Item.svelte';
export let options: { label: string; id_code: string; color: string }[];
export let disabled: { label: string; id_code: string; color: string }[] = [];
export let selected: { label: string; id_code: string; color: string }[] =[];
//$: options = options.filter(o=>!selected.map(s=>s.id_code).includes(o.id_code))
</script>
<Svelecte
options={options.map((o) => {
if (disabled.map((s) => s.id_code).includes(o.id_code)) {
return { ...o, $disabled: true };
}
return {...o};
}).sort(o=> o.$disabled == true ? 1:0)}
controlItem={Item}
dropdownItem={Item}
valueField={'id_code'}
createTransform={(inputValue, creatablePrefix, valueField, labelField) => {
return {
label: inputValue,
id_code: Math.random().toString(36).substring(2, 9),
color: '#00ff00',
created: true
};
}}
on:change={
(s)=>{
console.log('CHANGES', s)
selected = s.detail
}
}
on:createoption={(opt) => {
console.log('NEW OPT', opt);
selected.push({ ...opt.detail });
}}
multiple
{...$$restProps}
/>
<style lang="scss">
@import "../../variables";
:global(.sv-dropdown-scroll) {
padding: 0 !important;
}
:global(.svelecte-control) {
--sv-bg: $background!important;
--sv-border-color: green !important;
--sv-item-active-bg: $background!important;
--sv-border: none!important;
--sv-active-border:(1px solid $contrast)!important;
}
</style>

View File

@ -0,0 +1,150 @@
<script lang="ts">
import { setContext } from 'svelte';
let visible: boolean = false;
let title: string = '';
let description: string = '';
let validateButton: string = '';
let validate: Function = () => {};
const alert = (newAlert: {
title: string;
description: string;
validate: Function;
validateButton?: string;
}) => {
title = newAlert.title;
description = newAlert.description;
validateButton = newAlert.validateButton || 'Oui';
validate = newAlert.validate;
visible = true;
};
setContext('alert', { alert });
const close = () => {
visible = false;
};
const keyPress = () => {};
</script>
<slot />
<div class="overlay" on:click={() => close()} class:visible on:keypress={keyPress} />
<div id="modal" class:visible on:keypress={keyPress}>
<div class="head">
<div>
<p>{title}</p>
</div>
</div>
<div class="desc">
<p>{description}</p>
</div>
<div class="buttons">
<div>
<button
on:click={() => {
validate();
close();
}}
class="valid">Oui</button
>
<button on:click={close} class="cancel">Non</button>
</div>
</div>
</div>
<style lang="scss">
* {
margin: 0;
}
.valid {
background-color: rgba(255, 79, 100, 0.2);
color: rgb(255, 79, 100);
&:hover {
background-color: rgba(199, 38, 57, 0.2);
}
}
.cancel {
background-color: rgba(13, 2, 33, 0.3);
color: white;
&:hover {
background-color: rgba(32, 5, 82, 0.3);
}
}
.head {
width: 100%;
padding: 26px 24px;
display: flex;
justify-content: space-between;
font-size: 1.1em;
border-radius: 5px 5px 0 0;
background-color: #ff4f64;
}
.desc {
width: 100%;
padding: 24px;
background-color: #1d1a5a;
font-size: 0.9em;
}
.buttons {
background-color: #1d1a5a;
width: 100%;
padding: 0 24px 30px 24px;
border-radius: 0 0 5px 5px;
text-align: right;
box-sizing: border-box;
button {
min-height: 36px;
padding-inline: 16px;
margin-left: 2%;
border: none;
border-radius: 8px;
transition: 0.3s ease-in-out;
cursor: pointer;
}
div {
border-top: 1px solid gray;
padding-top: 20px;
}
}
#modal {
position: fixed;
top: 50%;
left: 50%;
//width: 50%;
max-width: 600px;
min-width: 500px;
transform: translateX(-50%) translateY(-50%) scale(0.7);
visibility: hidden;
transition: 0.4s;
opacity: 0;
z-index: 2000;
&.visible {
visibility: visible !important;
transform: translateX(-50%) translateY(-50%) scale(1) !important;
opacity: 1 !important;
}
}
.overlay {
background-color: black;
opacity: 0;
z-index: 1999;
width: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
visibility: hidden;
&.visible {
opacity: 0.7;
visibility: visible;
}
}
</style>

View File

@ -0,0 +1,40 @@
<script lang="ts">
import { onMount, setContext } from 'svelte';
import { writable } from 'svelte/store';
import { loginRequest, refreshRequest, registerRequest } from '../requests/auth.request';
import jwt_decode from 'jwt-decode';
const user = writable(null);
const isAuth = writable(false);
const login = (login: string, password: string) => {
loginRequest({ login, password }).then((r) => {
localStorage.setItem('token', `${r.access_token}`);
localStorage.setItem('refresh', `${r.refresh_token}`);
isAuth.set(true);
});
};
const register = (username: string, password: string, confirm: string) => {
registerRequest({ username, password, password_confirm: confirm }).then((r) => {
localStorage.setItem('token', `${r.access_token}`);
localStorage.setItem('refresh', `${r.refresh_token}`);
isAuth.set(true);
});
};
const logout = () => {};
setContext('auth', { user, isAuth, login, register, logout });
onMount(() => {
if (localStorage.getItem('token') != null) {
const { exp } = jwt_decode(localStorage.getItem('token')!);
console.log(Date.now(), exp, Date.now() >= exp * 1000)
if (Date.now() >= exp * 1000) {
refreshRequest(localStorage.getItem('refresh')!).then(r=>{localStorage.setItem('token', r.access_token)})
isAuth.set(true)
return
}
isAuth.set(true)
}
});
</script>
<p>{$isAuth ? 'Connecté' : 'Non connecté'}</p>
<slot />

View File

@ -2,68 +2,104 @@
import { onDestroy, setContext, SvelteComponent } from 'svelte'; import { onDestroy, setContext, SvelteComponent } from 'svelte';
let visible = false; let visible = false;
let onClose: Function;
let props = {}; let props = {};
let component: ConstructorOfATypedSvelteComponent | undefined; let component: ConstructorOfATypedSvelteComponent | undefined;
function show(c : ConstructorOfATypedSvelteComponent, p: Object) { let closed = true;
visible = true let onClose: Function = () => {};
component = c function show(
props = p c: ConstructorOfATypedSvelteComponent,
p: Object,
newOnClose: Function = () => {},
editing: boolean = false
) {
if (editing == false && closed === false) return;
visible = true;
closed = false;
component = c;
console.log('edi', editing, c, p)
if (editing) {
console.log('EDITINGF', props)
props = { ...props, ...p };
console.log(props)
} else {
props = p;
} }
function close(){ onClose = newOnClose;
visible=false
component = undefined;
props = {}
onClose()
} }
setContext('modal', {show, close})
function addContext (key: string, value: any) {
setContext(key, value)
}
function close() {
visible = false;
onClose();
setTimeout(() => {
component = undefined;
props = {};
closed = true;
}, 500);
}
setContext('modal', { show, close, addContext });
function keyPress(e: KeyboardEvent) { function keyPress(e: KeyboardEvent) {
console.log('HOP');
if (e.key == 'Escape' && visible == true) { if (e.key == 'Escape' && visible == true) {
visible = false; visible = false;
} }
} }
</script> </script>
<slot /> <slot />
<div id="topModal" class:visible on:click={() => close()} on:keypress={keyPress}>
<div id="modal" on:click|stopPropagation={()=>{}} on:keypress={()=>{}}> <div
<svelte:component this={component} {...props}/> class="overlay"
</div> on:click={() => close()}
class:visible={!closed}
class:closing={!visible && !closed}
on:keypress={keyPress}
/>
<div id="modal" class:visible on:keypress={keyPress}>
<svelte:component this={component} {...props} />
</div> </div>
<style lang="scss">
<style> #modal {
#topModal {
visibility: hidden;
z-index: 9999;
position: fixed; position: fixed;
top: 50%;
left: 50%;
width: 50%;
transform: translateX(-50%) translateY(-50%) scale(0.7);
visibility: hidden;
transition: 0.4s;
opacity: 0;
z-index: 1000;
&.visible {
visibility: visible !important;
transform: translateX(-50%) translateY(-50%) scale(1) !important;
opacity: 1 !important;
}
}
.overlay {
background-color: black;
opacity: 0;
z-index: 999;
width: 100%;
position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: #4448; visibility: hidden;
display: flex; transition: 0.3s;
align-items: center; &.visible {
justify-content: center; opacity: 0.7;
visibility: visible;
} }
#modal {
position: relative;
border-radius: 6px;
background: white;
border: 2px solid #000;
filter: drop-shadow(5px 5px 5px #555);
padding: 1em;
} }
.closing {
.visible { opacity: 0 !important;
visibility: visible !important;
} }
/* #modal-content {
max-width: calc(100vw - 20px);
max-height: calc(100vh - 20px);
overflow: auto;
} */
</style> </style>

View File

@ -0,0 +1,42 @@
<script lang="ts">
import { browser } from '$app/environment';
import { goto, afterNavigate } from '$app/navigation';
import { setContext } from 'svelte';
import { page } from '$app/stores';
import { base } from '$app/paths';
let previous: string | null = base;
let first = true;
const navigate = (
url: string | number,
params: object | undefined,
options:
| {
replaceState?: boolean | undefined;
noScroll?: boolean | undefined;
keepFocus?: boolean | undefined;
state?: any;
invalidateAll?: boolean | undefined;
}
| undefined
) => {
if (browser) {
console.log('PREVIOUS', previous, typeof url == 'number', previous);
if (typeof url == 'number' && previous != null) {
goto(previous);
} else {
const parsedParams = new URLSearchParams(params as Record<string, string>);
goto(`${url}?${parsedParams.toString()}`, { ...options });
}
first = false;
}
};
afterNavigate(({ from }) => {
previous = from?.url.toString() || previous;
});
setContext('navigation', { navigate });
</script>
<slot />

View File

@ -0,0 +1,42 @@
import axios from 'axios';
export const loginRequest = (data: { login: string; password: string }) => {
return axios({
url: 'http://localhost:8002/login',
method: 'POST',
data
})
.then((r) => r.data as {access_token: string, refresh_token: string, token_type: string })
.catch((e) => {
throw e;
});
};
export const registerRequest = (data: { username: string; password: string, password_confirm: string }) => {
return axios({
url: 'http://localhost:8002/register',
method: 'POST',
data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
.then((r) => r.data as { access_token: string; refresh_token: string; token_type: string })
.catch((e) => {
throw e;
});
};
export const refreshRequest = (token: string) => {
return axios({
url: 'http://localhost:8002/refresh',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
"Authorization": `Bearer ${token}`
}
})
.then((r) => r.data as {access_token:string})
.catch((e) => {
throw e;
});
}

View File

@ -0,0 +1,134 @@
import type { Tag } from '..//types/exo.type';
import { exoInstance } from '../apis/exo.api';
import { stringify } from 'qs';
const formDataFromObj = (obj: object) => {
const form_data = new FormData();
for (const key in obj) {
form_data.append(key, obj[key as keyof typeof obj]);
}
return form_data;
};
export const createExo = (data: {
name: string;
consigne: string;
private: boolean;
file: File;
}) => {
return exoInstance({
url: '/exercices',
method: 'POST',
data: formDataFromObj(data),
headers: {
'Content-type': 'multipart/form-data'
}
});
};
export const delExo = (id_code: string) => {
return exoInstance({
url: '/exercice/' + id_code,
method: 'DELETE'
});
};
export const editExo = (
id_code: string,
data: {
name: string;
consigne: string;
private: boolean;
file?: File;
}
) => {
return exoInstance({
url: '/exercice/' + id_code,
method: 'PUT',
data: formDataFromObj(data),
headers: {
'Content-type': 'multipart/form-data'
}
});
};
export const getExos = (
category: 'public' | 'user',
data: {
search?: string;
type?: 'csv' | 'pdf' | 'web';
tags?: string[];
page?: number;
size?: number;
}
) => {
console.log('SENDINF', data, stringify(data, { arrayFormat: 'brackets' }));
return exoInstance({
url: '/exercices/' + category,
method: 'GET',
params: data
})
.then((r) => r.data)
.catch(console.log);
};
export const getExo = (id_code: string) => {
return exoInstance({
url: '/exercice/' + id_code,
method: 'GET'
})
.then((r) => r.data)
.catch(console.log);
};
export const addTags = (id_code: string, data: Tag[]) => {
return exoInstance({
url: `/exercice/${id_code}/tags`,
data,
method: 'POST'
}).then((r) => r.data);
};
export const delTags = (id_code: string, tag_id: string) => {
return exoInstance({
url: `/exercice/${id_code}/tags/${tag_id}`,
method: 'DELETE'
}).then((r) => r.data);
};
export const getTags = () => {
return exoInstance({
url: `/tags`,
method: 'Get'
}).then((r) => r.data).catch(console.log);
};
export const cloneExo = (id_code: string) => {
return exoInstance({
url: `/clone/${id_code}`,
method: 'POST'
}).then((r) => r.data);
};
export const getExoSource = (id_code: string,) => {
return exoInstance({
url: `/exercice/${id_code}/exo_source`,
method: 'Get'
}).then((r) => {
const contentDisposition = r.headers['content-disposition'] || "filename=untitled.py";
const splitted = contentDisposition.split('filename=')
let filename = "untitled.py"
if(splitted.length >= 1) {
filename = splitted[1]
}
const blob = new Blob([r.data], {
type: 'text/x-python'
});
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
link.remove();
});
};

View File

@ -2,9 +2,19 @@
import Modal from '../context/Modal.svelte'; import Modal from '../context/Modal.svelte';
import NavLink from '../components/NavLink.svelte'; import NavLink from '../components/NavLink.svelte';
import '../app.scss'; import '../app.scss';
import Alert from '../context/Alert.svelte';
import Auth from '../context/Auth.svelte';
import { QueryClient, QueryClientProvider } from '@sveltestack/svelte-query';
import { Router } from 'svelte-navigator';
import Navigation from '../context/Navigation.svelte';
const queryClient = new QueryClient();
</script> </script>
<Modal> <Navigation>
<QueryClientProvider client={queryClient}>
<Auth>
<Alert>
<Modal>
<main> <main>
<nav data-sveltekit-preload-data="hover"> <nav data-sveltekit-preload-data="hover">
<NavLink href="/" exact>Home</NavLink> <NavLink href="/" exact>Home</NavLink>
@ -13,9 +23,14 @@
</nav> </nav>
<slot /> <slot />
</main> </main>
</Modal> </Modal>
</Alert>
</Auth>
</QueryClientProvider>
</Navigation>
<style lang="scss"> <style lang="scss">
@import '../variables';
.links { .links {
display: flex; display: flex;
align-items: center; align-items: center;
@ -41,9 +56,9 @@
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
Fira Sans, Droid Sans, Helvetica Neue, sans-serif; Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
overflow: hidden; overflow: hidden;
background-color: #1d1a5a; background-color: $background;
color: #d9d9d9; color: #d9d9d9;
background: linear-gradient(to bottom left, #0d0221 30%, #1a0f7a); background: linear-gradient(to bottom left, $background-dark 30%, $background-light);
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
} }
@ -54,6 +69,7 @@
padding-right: calc(50% - var(--container-width) / 2); padding-right: calc(50% - var(--container-width) / 2);
height: calc(100vh - var(--navbar-height) - 10px); height: calc(100vh - var(--navbar-height) - 10px);
overflow: auto; overflow: auto;
height: 100%;
a { a {
color: red; color: red;
} }
@ -65,7 +81,7 @@
align-items: center; align-items: center;
margin-bottom: 10px; margin-bottom: 10px;
padding: 30px 0; padding: 30px 0;
border-bottom: 1px solid #181553; border-bottom: 1px solid $border;
width: 100%; width: 100%;
gap: 10px; gap: 10px;
height: 30px; height: 30px;

View File

@ -1,16 +1,15 @@
<script> <script>
import Card from '../components/exos/Card.svelte'; import { getExos } from '../requests/exo.request';
import { getContext } from 'svelte'; import { useQuery } from '@sveltestack/svelte-query';
let count = 1; import { browser } from '$app/environment'
const { show } = getContext('modal'); const t = useQuery('tst',async ()=>{
return getExos('public' , {})
const add = () => { });
show(Card, { title: 'test', examples: ['test'], tags: [], click: () => {} }); $: console.log("DATA", $t.data)
};
</script> </script>
<h1>Welcome to SvelteKit</h1> <button
<div> on:click={() => {
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation console.log(getExos('public', {}));
<button on:click={add}>test {count}</button> }}>test</button
</div> >

View File

@ -0,0 +1,20 @@
<script lang='ts'>
import { getContext } from "svelte";
let u = '';
let p = '';
let p2 = "";
const {register} = getContext('auth')
</script>
<input type="text" bind:value={u}>
<input type="text" bind:value={p}>
<input type="text" bind:value={p2}>
<button on:click={()=>{
register(u, p, p2)
}}>
register
</button>
<style lang='scss'>
</style>

View File

@ -0,0 +1,7 @@
export type Store<T> = {
isLoading: boolean,
isFetching: boolean,
isSuccess: boolean,
data: T
}

View File

@ -0,0 +1,31 @@
export type Tag = {
label: string,
color: string,
id_code: string
}
export type Exercice = {
csv: boolean,
pdf: boolean,
web: boolean,
name: string,
consigne: string,
private: boolean,
id_code: string,
exo_source: string,
author: { username: string },
original: {name: string, id_code: string, author: string} | null
tags: Tag[],
examples: { type: string, data: { calcul: string, correction: string }[] },
is_author: boolean
}
export type Page ={
items: Exercice[],
total: number,
page: number,
size: number,
totalPage: number,
hasMore: number
}

View File

@ -0,0 +1,31 @@
export const errorMsg = (
form: {
dirty: boolean;
valid: boolean;
errors: string[];
hasError: (s: string) => boolean;
},
name: string
): string[] => {
return [
form.hasError(`${name}.required`) && 'Champ requis',
form.hasError(`${name}.min`) && 'Trop court',
form.hasError(`${name}.max`) && 'Trop long',
form.hasError(`${name}.url`) && 'Pas bonne url',
form.hasError(`${name}.between`) && 'Pas valide',
form.hasError(`${name}.matchField`) && 'Ca matche pas',
form.hasError(`${name}.not`) && 'Valeur impossible',
].filter((r) => typeof r === 'string') as string[];
};
export const checkFile = () => {
return async (value: Array<File>) => {
console.log('VALIDATION', value)
if (value.length == 0) {
return { valid:false, name: 'required' };
}
const name = value[0].name.split('.');
const ext = name[name.length - 1];
return { valid: value[0].type == 'text/x-python' && ext == 'py', name: 'extension' };
};
};

View File

@ -0,0 +1,16 @@
export const compareObject = (obj1: object, obj2: object) => {
const keys = Object.keys(obj1);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const v1 = obj1[key as keyof typeof obj1];
const v2 = obj2[key as keyof typeof obj2];
console.log(obj1, obj2, v1, v2, key)
if (v1 != undefined && v2 != undefined) {
if (v1 != v2) {
return false
}
}
}
return true;
};

View File

@ -1 +1,23 @@
/* Variables and mixins declared here will be available in all other SCSS files */ /* Variables and mixins declared here will be available in all other SCSS files */
$primary: #FCBF49;
$primary-dark: #f08336;
$on-primary: #080808;
$secondary: #DF2935;
$secondary-dark: #c40e21;
$on-secondary: #080808;
$contrast: #5396e7;
$input-border: #64619f;
$border: #181553;
$background: #1d1a5a;
$background-light: #1a0f7a;
$background-dark: #0D0221;
$red: rgb(255, 79, 100);
$green: #41cf7c;
$rouge: #a6333f;
$vert: #41cf7c;
$bleu: #045aff;
$blanc: white;
$orange: orange;
$marron: brown;
$dark-green: #00712c;

View File

@ -7,10 +7,12 @@ const config = {
css: { css: {
preprocessorOptions: { preprocessorOptions: {
scss: { scss: {
additionalData: "@use \"src/variables.scss\" as *;" additionalData: '@use "src/variables.scss" as *;'
}
} }
} }
},
optimizeDeps: { exclude: ['svelte-navigator'] }
}; };
export default config; export default config;