This commit is contained in:
Kilton937342 2023-02-22 12:43:39 +01:00
parent 43031d22fb
commit 4941b5a154
120 changed files with 12235 additions and 1229 deletions

9
.idea/fastapi_gen.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/fastapi_gen.iml" filepath="$PROJECT_DIR$/.idea/fastapi_gen.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

84
.idea/workspace.xml generated Normal file
View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="a539d37a-9d57-43b3-91cf-30110de7e657" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/backend/api/database/db.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/database/db.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/api/database/exercices/models.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/database/exercices/models.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/api/database/room/crud.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/database/room/crud.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/api/database/room/models.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/database/room/models.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/api/generateur/generateur_main.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/generateur/generateur_main.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/api/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/main.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/api/routes/auth/routes.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/routes/auth/routes.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/api/routes/exercices/routes.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/routes/exercices/routes.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/api/routes/room/consumer.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/routes/room/consumer.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/api/routes/room/manager.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/routes/room/manager.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/api/routes/room/routes.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/routes/room/routes.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/api/services/websocket.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/services/websocket.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/api/tests/test_exos.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/tests/test_exos.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/api/tests/test_room.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/tests/test_room.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/api/tests/testing_exo_source/exo_source_web_only.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/api/tests/testing_exo_source/exo_source_web_only.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/pnpm-lock.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pnpm-lock.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/apis/exo.api.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/apis/exo.api.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/app.scss" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/app.scss" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/components/exos/Card.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/components/exos/Card.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/components/exos/CreateCard.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/components/exos/CreateCard.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/components/exos/DownloadForm.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/components/exos/DownloadForm.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/components/exos/EditForm.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/components/exos/EditForm.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/components/exos/Feed.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/components/exos/Feed.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/components/exos/Head.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/components/exos/Head.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/components/exos/Pagination.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/components/exos/Pagination.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/components/exos/Tag.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/components/exos/Tag.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/components/exos/TagContainer.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/components/exos/TagContainer.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/components/forms/FileInput.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/components/forms/FileInput.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/components/forms/InputWithLabel.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/components/forms/InputWithLabel.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/components/forms/Item.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/components/forms/Item.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/components/forms/TagSelector.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/components/forms/TagSelector.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/context/Auth.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/context/Auth.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/context/Modal.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/context/Modal.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/context/Navigation.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/context/Navigation.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/requests/auth.request.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/requests/auth.request.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/routes/+layout.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/routes/+layout.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/routes/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/routes/+page.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/routes/exercices/[[slug]]/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/routes/exercices/[[slug]]/+page.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/routes/signup/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/routes/signup/+page.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/types/exo.type.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/types/exo.type.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/utils/forms.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/utils/forms.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/utils/utils.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/utils/utils.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/variables.scss" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/variables.scss" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/vite.config.js" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/vite.config.js" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectId" id="2LnGvp3nb4X9GdPKgjfvgSHhKvw" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"WebServerToolWindowFactoryState": "false",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="a539d37a-9d57-43b3-91cf-30110de7e657" name="Changes" comment="" />
<created>1676499669960</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1676499669960</updated>
<workItem from="1676499673827" duration="6000" />
</task>
<servers />
</component>
</project>

8
backend/api/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

11
backend/api/.idea/api.iml generated Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

24
backend/api/.idea/dataSources.xml generated Normal file
View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="database" uuid="384abd64-7cbf-4b69-a4ff-253dfdd48393">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/database.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="database7" uuid="a2cbf379-d945-498f-a48c-23b53a5d1d5f">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/database7.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.39.2/sqlite-jdbc-3.39.2.jar</url>
</library>
</libraries>
</data-source>
</component>
</project>

423
backend/api/.idea/dbnavigator.xml generated Normal file
View File

@ -0,0 +1,423 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DBNavigator.Project.DataEditorManager">
<record-view-column-sorting-type value="BY_INDEX" />
<value-preview-text-wrapping value="false" />
<value-preview-pinned value="false" />
</component>
<component name="DBNavigator.Project.DatabaseEditorStateManager">
<last-used-providers />
</component>
<component name="DBNavigator.Project.DatabaseFileManager">
<open-files />
</component>
<component name="DBNavigator.Project.ExecutionManager">
<retain-sticky-names value="false" />
</component>
<component name="DBNavigator.Project.Settings">
<connections />
<browser-settings>
<general>
<display-mode value="TABBED" />
<navigation-history-size value="100" />
<show-object-details value="false" />
</general>
<filters>
<object-type-filter>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="true" />
<object-type name="ROLE" enabled="true" />
<object-type name="PRIVILEGE" enabled="true" />
<object-type name="CHARSET" enabled="true" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED_VIEW" enabled="true" />
<object-type name="NESTED_TABLE" enabled="true" />
<object-type name="COLUMN" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET_TRIGGER" enabled="true" />
<object-type name="DATABASE_TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="true" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
<object-type name="ARGUMENT" enabled="true" />
<object-type name="DIMENSION" enabled="true" />
<object-type name="CLUSTER" enabled="true" />
<object-type name="DBLINK" enabled="true" />
</object-type-filter>
</filters>
<sorting>
<object-type name="COLUMN" sorting-type="NAME" />
<object-type name="FUNCTION" sorting-type="NAME" />
<object-type name="PROCEDURE" sorting-type="NAME" />
<object-type name="ARGUMENT" sorting-type="POSITION" />
<object-type name="TYPE ATTRIBUTE" sorting-type="POSITION" />
</sorting>
<default-editors>
<object-type name="VIEW" editor-type="SELECTION" />
<object-type name="PACKAGE" editor-type="SELECTION" />
<object-type name="TYPE" editor-type="SELECTION" />
</default-editors>
</browser-settings>
<navigation-settings>
<lookup-filters>
<lookup-objects>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="false" />
<object-type name="ROLE" enabled="false" />
<object-type name="PRIVILEGE" enabled="false" />
<object-type name="CHARSET" enabled="false" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED VIEW" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET TRIGGER" enabled="true" />
<object-type name="DATABASE TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="false" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="DIMENSION" enabled="false" />
<object-type name="CLUSTER" enabled="false" />
<object-type name="DBLINK" enabled="true" />
</lookup-objects>
<force-database-load value="false" />
<prompt-connection-selection value="true" />
<prompt-schema-selection value="true" />
</lookup-filters>
</navigation-settings>
<dataset-grid-settings>
<general>
<enable-zooming value="true" />
<enable-column-tooltip value="true" />
</general>
<sorting>
<nulls-first value="true" />
<max-sorting-columns value="4" />
</sorting>
<audit-columns>
<column-names value="" />
<visible value="true" />
<editable value="false" />
</audit-columns>
</dataset-grid-settings>
<dataset-editor-settings>
<text-editor-popup>
<active value="false" />
<active-if-empty value="false" />
<data-length-threshold value="100" />
<popup-delay value="1000" />
</text-editor-popup>
<values-actions-popup>
<show-popup-button value="true" />
<element-count-threshold value="1000" />
<data-length-threshold value="250" />
</values-actions-popup>
<general>
<fetch-block-size value="100" />
<fetch-timeout value="30" />
<trim-whitespaces value="true" />
<convert-empty-strings-to-null value="true" />
<select-content-on-cell-edit value="true" />
<large-value-preview-active value="true" />
</general>
<filters>
<prompt-filter-dialog value="true" />
<default-filter-type value="BASIC" />
</filters>
<qualified-text-editor text-length-threshold="300">
<content-types>
<content-type name="Text" enabled="true" />
<content-type name="Properties" enabled="true" />
<content-type name="XML" enabled="true" />
<content-type name="DTD" enabled="true" />
<content-type name="HTML" enabled="true" />
<content-type name="XHTML" enabled="true" />
<content-type name="CSS" enabled="true" />
<content-type name="Java" enabled="true" />
<content-type name="SQL" enabled="true" />
<content-type name="PL/SQL" enabled="true" />
<content-type name="JavaScript" enabled="true" />
<content-type name="JSON" enabled="true" />
<content-type name="JSON5" enabled="true" />
<content-type name="JSP" enabled="true" />
<content-type name="JSPx" enabled="true" />
<content-type name="Groovy" enabled="true" />
<content-type name="FTL" enabled="true" />
<content-type name="VTL" enabled="true" />
<content-type name="AIDL" enabled="true" />
<content-type name="YAML" enabled="true" />
<content-type name="Manifest" enabled="true" />
</content-types>
</qualified-text-editor>
<record-navigation>
<navigation-target value="VIEWER" />
</record-navigation>
</dataset-editor-settings>
<code-editor-settings>
<general>
<show-object-navigation-gutter value="false" />
<show-spec-declaration-navigation-gutter value="true" />
<enable-spellchecking value="true" />
<enable-reference-spellchecking value="false" />
</general>
<confirmations>
<save-changes value="false" />
<revert-changes value="true" />
</confirmations>
</code-editor-settings>
<code-completion-settings>
<filters>
<basic-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="false" />
<filter-element type="OBJECT" id="view" selected="false" />
<filter-element type="OBJECT" id="materialized view" selected="false" />
<filter-element type="OBJECT" id="index" selected="false" />
<filter-element type="OBJECT" id="constraint" selected="false" />
<filter-element type="OBJECT" id="trigger" selected="false" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="false" />
<filter-element type="OBJECT" id="procedure" selected="false" />
<filter-element type="OBJECT" id="function" selected="false" />
<filter-element type="OBJECT" id="package" selected="false" />
<filter-element type="OBJECT" id="type" selected="false" />
<filter-element type="OBJECT" id="dimension" selected="false" />
<filter-element type="OBJECT" id="cluster" selected="false" />
<filter-element type="OBJECT" id="dblink" selected="false" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</basic-filter>
<extended-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</extended-filter>
</filters>
<sorting enabled="true">
<sorting-element type="RESERVED_WORD" id="keyword" />
<sorting-element type="RESERVED_WORD" id="datatype" />
<sorting-element type="OBJECT" id="column" />
<sorting-element type="OBJECT" id="table" />
<sorting-element type="OBJECT" id="view" />
<sorting-element type="OBJECT" id="materialized view" />
<sorting-element type="OBJECT" id="index" />
<sorting-element type="OBJECT" id="constraint" />
<sorting-element type="OBJECT" id="trigger" />
<sorting-element type="OBJECT" id="synonym" />
<sorting-element type="OBJECT" id="sequence" />
<sorting-element type="OBJECT" id="procedure" />
<sorting-element type="OBJECT" id="function" />
<sorting-element type="OBJECT" id="package" />
<sorting-element type="OBJECT" id="type" />
<sorting-element type="OBJECT" id="dimension" />
<sorting-element type="OBJECT" id="cluster" />
<sorting-element type="OBJECT" id="dblink" />
<sorting-element type="OBJECT" id="schema" />
<sorting-element type="OBJECT" id="role" />
<sorting-element type="OBJECT" id="user" />
<sorting-element type="RESERVED_WORD" id="function" />
<sorting-element type="RESERVED_WORD" id="parameter" />
</sorting>
<format>
<enforce-code-style-case value="true" />
</format>
</code-completion-settings>
<execution-engine-settings>
<statement-execution>
<fetch-block-size value="100" />
<execution-timeout value="20" />
<debug-execution-timeout value="600" />
<focus-result value="false" />
<prompt-execution value="false" />
</statement-execution>
<script-execution>
<command-line-interfaces />
<execution-timeout value="300" />
</script-execution>
<method-execution>
<execution-timeout value="30" />
<debug-execution-timeout value="600" />
<parameter-history-size value="10" />
</method-execution>
</execution-engine-settings>
<operation-settings>
<transactions>
<uncommitted-changes>
<on-project-close value="ASK" />
<on-disconnect value="ASK" />
<on-autocommit-toggle value="ASK" />
</uncommitted-changes>
<multiple-uncommitted-changes>
<on-commit value="ASK" />
<on-rollback value="ASK" />
</multiple-uncommitted-changes>
</transactions>
<session-browser>
<disconnect-session value="ASK" />
<kill-session value="ASK" />
<reload-on-filter-change value="false" />
</session-browser>
<compiler>
<compile-type value="KEEP" />
<compile-dependencies value="ASK" />
<always-show-controls value="false" />
</compiler>
<debugger>
<debugger-type value="ASK" />
<use-generic-runners value="true" />
</debugger>
</operation-settings>
<ddl-file-settings>
<extensions>
<mapping file-type-id="VIEW" extensions="vw" />
<mapping file-type-id="TRIGGER" extensions="trg" />
<mapping file-type-id="PROCEDURE" extensions="prc" />
<mapping file-type-id="FUNCTION" extensions="fnc" />
<mapping file-type-id="PACKAGE" extensions="pkg" />
<mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
<mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
<mapping file-type-id="TYPE" extensions="tpe" />
<mapping file-type-id="TYPE_SPEC" extensions="tps" />
<mapping file-type-id="TYPE_BODY" extensions="tpb" />
</extensions>
<general>
<lookup-ddl-files value="true" />
<create-ddl-files value="false" />
<synchronize-ddl-files value="true" />
<use-qualified-names value="false" />
<make-scripts-rerunnable value="true" />
</general>
</ddl-file-settings>
<general-settings>
<regional-settings>
<date-format value="MEDIUM" />
<number-format value="UNGROUPED" />
<locale value="SYSTEM_DEFAULT" />
<use-custom-formats value="false" />
</regional-settings>
<environment>
<environment-types>
<environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
<environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
<environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
<environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
</environment-types>
<visibility-settings>
<connection-tabs value="true" />
<dialog-headers value="true" />
<object-editor-tabs value="true" />
<script-editor-tabs value="false" />
<execution-result-tabs value="true" />
</visibility-settings>
</environment>
</general-settings>
</component>
</project>

6
backend/api/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_19" project-jdk-name="Python 3.10 (env)" project-jdk-type="Python SDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
backend/api/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/api.iml" filepath="$PROJECT_DIR$/.idea/api.iml" />
</modules>
</component>
</project>

6
backend/api/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>

View File

@ -1,9 +1,14 @@
import uuid
from services.password import get_password_hash
from database.auth.models import User, UserEdit
from sqlmodel import Session, select
from jose import jwt, exceptions
from sqlmodel import Session, select
from config import SECRET_KEY, ALGORITHM
from database.auth.models import User, UserEdit
from database.room.models import Member
from services.password import get_password_hash
def create_user_db(username:str , password: str, db: Session):
user = User(username=username, hashed_password=password, clientId=uuid.uuid4())
db.add(user)
@ -84,3 +89,7 @@ def change_user_uuid(id: int, db: Session):
db.refresh(user)
return user.clientId
def parse_user_rooms(user: User, db: Session):
members = db.exec(select(Member).where(Member.user_id == user.id)).all()
return [{"name": m.room.name, "id_code": m.room.id_code, "admin": m.is_admin} for m in members]

View File

@ -1,9 +1,11 @@
from typing import List, Optional
import uuid
from typing import List
from typing import TYPE_CHECKING, Optional
from uuid import UUID
import uuid
from sqlmodel import Field, SQLModel, Relationship
from pydantic import validator, BaseModel
from sqlmodel import Field, SQLModel, Relationship
from services.password import validate_password
from services.schema import as_form
@ -29,9 +31,16 @@ class User(UserBase, table=True):
@as_form
class UserEdit(UserBase):
pass
class UsersRoom(BaseModel):
name: str
id_code: str
admin: bool = False
class UserRead(UserBase):
id: int
rooms: List[UsersRoom] = []
class UserEditRead(UserBase):
id: int
#rooms: List[UsersRoom] = []
@as_form

View File

@ -2,7 +2,7 @@ import pydantic.json
import json
from sqlmodel import SQLModel, create_engine, Session, select
sqlite_file_name = "database.db"
sqlite_file_name = "database7.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
@ -19,6 +19,6 @@ def create_db_and_tables():
SQLModel.metadata.create_all(engine)
def get_session():
with Session(engine) as s:
with Session(engine, expire_on_commit=False) as s:
yield s

View File

@ -18,7 +18,7 @@ if TYPE_CHECKING:
from database.auth.models import User
class ExampleEnum(Enum):
class ExampleEnum(str, Enum):
csv = 'csv'
pdf = 'pdf'
web = 'web'

View File

@ -1,17 +1,21 @@
from services.auth import get_current_user_optional
from services.misc import noteOn20
from copy import deepcopy
from typing import Dict, List
import uuid
from fastapi import Body, Depends, HTTPException, status
from copy import deepcopy
from typing import List
from fastapi import Depends, HTTPException, status, Query
from pydantic import BaseModel
from sqlmodel import Session, delete, select, col, table
from database.db import get_session
from database.room.models import Anonymous, Challenge, Challenges, CorrigedGeneratorOut, Exercices, ExercicesCreate, Member, Note, Parcours, ParcoursCreate, ParcoursReadShort, ParsedGeneratorOut, Room, RoomCreate, RoomInfo, RoomRead, TmpCorrection, Waiter, MemberRead
from database.auth.models import User
from services.database import generate_unique_code
from sqlalchemy import func
from sqlmodel import Session, delete, select, col
from database.auth.crud import get_user_from_token
from database.auth.models import User
from database.db import get_session
from database.exercices.models import Exercice
from database.room.models import Anonymous, Challenge, Challenges, CorrigedGeneratorOut, Exercices, ExercicesCreate, \
Member, Note, Parcours, ParcoursCreate, ParcoursReadShort, ParsedGeneratorOut, Room, RoomCreate, RoomInfo, \
TmpCorrection, Waiter, MemberRead, CorrigedData, CorrectionData, Challenger
from services.auth import get_current_user_optional
from services.database import generate_unique_code
def create_room_db(*, room: RoomCreate, user: User | None = None, username: str | None = None, db: Session):
@ -38,12 +42,15 @@ def create_room_db(*, room: RoomCreate, user: User | None = None, username: str
return {"room": room_obj, "member": member}
def change_room_name(room: Room, name: str, db: Session):
room.name = name
db.add(room)
db.commit()
db.refresh(room)
return room
def change_room_status(room: Room, public: bool, db: Session):
room.public = public
db.add(room)
@ -51,6 +58,7 @@ def change_room_status(room: Room, public: bool, db: Session):
db.refresh(room)
return room
def get_member_from_user(user_id: int, room_id: int, db: Session):
member = db.exec(select(Member).where(Member.room_id ==
room_id, Member.user_id == user_id)).first()
@ -86,6 +94,7 @@ def get_anonymous_from_code(reconnect_code: str, db: Session):
Anonymous.reconnect_code == reconnect_code)).first()
return anonymous
def get_anonymous_from_clientId(clientId: str, db: Session):
anonymous = db.exec(select(Anonymous).where(
Anonymous.clientId == clientId)).first()
@ -102,19 +111,22 @@ def get_member_from_clientId(clientId: str, room_id: int, db: Session):
def create_member(*, room: Room, user: User | None = None, anonymous: Anonymous | None = None, waiting: bool = False, db: Session):
member_id = generate_unique_code(Member, s=db)
member = Member(room=room, user=user, anonymous=anonymous, waiting=waiting,
member = Member(room=room, user=user, anonymous=anonymous, waiting=waiting,
id_code=member_id)
member.online = True
db.add(member)
db.commit()
db.refresh(member)
return member
def get_or_create_member(*, room: Room, user: User | None = None, anonymous: Anonymous | None = None, waiting: bool = False, db: Session):
def get_or_create_member(*, room: Room, user: User | None = None, anonymous: Anonymous | None = None,
waiting: bool = False, db: Session):
member = user is not None and get_member_from_user(user.id, room.id, db)
if member is not None and member is not False:
return member
member= create_member(room=room, user=user, anonymous=anonymous, waiting=waiting, db=db)
member = create_member(room=room, user=user,
anonymous=anonymous, waiting=waiting, db=db)
def connect_member(member: Member, db: Session):
@ -130,7 +142,7 @@ def disconnect_member(member: Member, db: Session):
member.online = False
if member.anonymous is not None:
change_anonymous_clientId(member.anonymous,db)
change_anonymous_clientId(member.anonymous, db)
db.add(member)
db.commit()
@ -167,6 +179,7 @@ def create_anonymous_member(username: str, room: Room, db: Session):
db.refresh(member)
return member
def create_anonymous(username: str, room: Room, db: Session):
username = validate_username(username, room, db)
if username is None:
@ -179,10 +192,13 @@ def create_anonymous(username: str, room: Room, db: Session):
db.refresh(anonymous)
return anonymous
def check_user_in_room(user_id: int, room_id: int, db: Session):
user = db.exec(select(Member).where(Member.user_id==user_id, Member.room_id == room_id)).first()
user = db.exec(select(Member).where(Member.user_id ==
user_id, Member.room_id == room_id)).first()
return user
def create_user_member(user: User, room: Room, db: Session):
member = get_member_from_user(user.id, room.id, db)
if member is not None:
@ -194,6 +210,7 @@ def create_user_member(user: User, room: Room, db: Session):
db.refresh(member)
return member
def create_anonymous_waiter(username: str, room: Room, db: Session):
username = validate_username(username, room, db)
if username is None:
@ -210,6 +227,7 @@ def create_anonymous_waiter(username: str, room: Room, db: Session):
db.refresh(member)
return member
def create_user_waiter(user: User, room: Room, db: Session):
member = get_member_from_user(user.id, room.id, db)
if member is not None:
@ -219,6 +237,7 @@ def create_user_waiter(user: User, room: Room, db: Session):
db=db)
return member
def get_waiter(waiter_code: str, db: Session):
return db.exec(select(Member).where(Member.id_code == waiter_code, Member.waiting == True)).first()
@ -236,6 +255,7 @@ def delete_member(member: Member, db: Session):
def accept_waiter(member: Member, db: Session):
member.waiting = False
member.waiter_code = None
member.online = True
db.add(member)
db.commit()
db.refresh(member)
@ -254,39 +274,173 @@ def leave_room(member: Member, db: Session):
return None
def serialize_member(member: Member) -> MemberRead | Waiter:
def serialize_member(member: Member, private: bool = False, admin: bool = False,
m2: Member | None = None) -> MemberRead | Waiter:
member_obj = member.user or member.anonymous
if member.waiting == False:
return MemberRead(username=member_obj.username, reconnect_code=getattr(member_obj, "reconnect_code", ""), isUser=member.user_id != None, isAdmin=member.is_admin, id_code=member.id_code).dict()
if member.waiting == True:
print("OHLA", member_obj, private, member.user_id == None)
if not member.waiting:
return MemberRead(username=member_obj.username, online=member.online,
clientId=str(member_obj.clientId) if (private == True and member.user_id == None) else "",
reconnect_code=getattr(member_obj, "reconnect_code", "") if (admin or m2 == member) else "",
isUser=member.user_id != None, isAdmin=member.is_admin, id_code=member.id_code).dict()
if member.waiting:
return Waiter(username=member_obj.username, waiter_id=member.id_code).dict()
def serialize_parcours_short(parcours: Parcours, member: Member, db: Session):
best_note = db.exec(select(Challenge.note, Challenge.time).where(Challenge.parcours_id == parcours.id, Challenge.challenger_id == member.id).order_by(col(Challenge.note).desc()).limit(1)).first()
note = None
if best_note is not None:
best_note=best_note[0]
note = Note(note=best_note[0], time=best_note[1])
return ParcoursReadShort(**parcours.dict(exclude_unset=True), best_note=note)
challenger = getChallenger(parcours, member, db)
return ParcoursReadShort(name=parcours.name, id_code=parcours.id_code, best_note=challenger.best,
validated=challenger.validated)
def serialize_challenge(challenge: Challenge):
return Challenges(name=challenge.challenger.user.username if challenge.challenger.user is not None else challenge.challenger.anonymous.username, value=Note(note=challenge.note, time=challenge.time), isCorriged=challenge.isCorriged, canCorrige=challenge.data is not None)
def serialize_parcours_short(parcours: Parcours, member: Member, db: Session):
if member.is_member == False:
challenges = db.exec(select(Challenge).where(Challenge.parcours_id == parcours.id, Challenge.challenger_id == member.id)).all()
else:
challenges = db.exec(select(Challenge).where(
Challenge.parcours_id == parcours.id)).all()
challenges = [serialize_challenge(c) for c in challenges]
return Parcours(**parcours.dict(), challenges=challenges)
return Challenges(
name=challenge.challenger.user.username if challenge.challenger.user is not None else challenge.challenger.anonymous.username,
value=Note(note=challenge.note, time=challenge.time), isCorriged=challenge.isCorriged,
canCorrige=challenge.data is not None)
def serialize_room(room: Room, member: Member, db: Session):
return RoomInfo(**room.dict(), parcours=[serialize_parcours_short(p, member, db) for p in room.parcours], members=[serialize_member(m) for m in room.members])
return RoomInfo(**room.dict(), parcours=[serialize_parcours_short(p, member, db) for p in room.parcours],
members=[serialize_member(m, admin=member.is_admin, m2=member) for m in room.members])
def getUsername(m: Member):
return m.user.username if m.user is not None else m.anonymous.username
def getChallengerInfo(c: Challenge, db: Session):
challenger = db.exec(select(Challenger).where(Challenger.member_id ==
c.challenger_mid, Challenger.parcours_id == c.challenger_pid)).first()
if challenger is not None:
member = challenger.member
return {"name": getUsername(member), "id_code": member.id_code}
def getChallenges(c: Challenger, db: Session):
challenges = db.exec(select(Challenge).where(Challenge.challenger_mid == c.member_id,
Challenge.challenger_pid == c.parcours_id)).all()
return challenges
def getTops(p: Parcours, db: Session):
tops = db.exec(select(Challenge).where(Challenge.parcours_id == p.id_code).order_by(
col(Challenge.mistakes), col(Challenge.time)).limit(3)).all()
tops = [{"challenger": getChallengerInfo(
t, db), "mistakes": t.mistakes, "time": t.time} for t in tops]
return tops
def getAvgTops(p: Parcours, db: Session):
avgTop = db.exec(select(Challenger).where(Challenger.parcours_id ==
p.id).order_by(col(Challenger.avg)).limit(3)).all()
avgTop = [{"id_code": t.member.id_code, "avg": t.avg,
"name": getUsername(t.member)} for t in avgTop]
return avgTop
def getRank(c: Challenger, p: Parcours, db: Session):
noteRank = db.exec(select([func.count(Challenge.id)]).where(Challenge.parcours_id == p.id_code).order_by(
col(Challenge.mistakes), col(Challenge.time)).where(Challenge.mistakes <= c.best,
Challenge.time < c.best_time)).one()
return noteRank + 1
def getAvgRank(c: Challenger, p: Parcours, db: Session):
avgRank = db.exec(select([func.count(Challenger.member_id)]).where(
Challenger.parcours_id == p.id).order_by(col(Challenger.avg)).where(Challenger.avg < c.avg)).one()
return avgRank + 1
def getMemberRank(m: Member, p: Parcours, db: Session):
challenger = db.exec(select(Challenger).where(Challenger.member_id == m.id)).first()
if challenger is None or challenger.best is None:
return None
return getRank(challenger, p, db)
def getMemberAvgRank(m: Member, p: Parcours, db: Session):
challenger = db.exec(select(Challenger).where(Challenger.member_id == m.id)).first()
print('CHALLE', challenger)
if challenger is None or challenger.avg is None:
return None
return getAvgRank(challenger, p, db)
def serialize_parcours(parcours: Parcours, member: Member, db: Session):
tops = getTops(parcours, db)
avgTop = getAvgTops(parcours, db)
challenger = db.exec(select(Challenger).where(
Challenger.member_id == member.id, Challenger.parcours_id == parcours.id)).first()
noteRank = None
avgRank = None
pb = None
if challenger is not None and challenger.avg is not None and challenger.best is not None:
noteRank = getRank(challenger, parcours, db)
avgRank = getAvgRank(challenger, parcours, db)
pb = {"mistakes": challenger.best, "time": challenger.best_time}
statement = select(Challenger).where(Challenger.parcours_id == parcours.id)
if not member.is_admin:
statement = statement.where(Challenger.member_id == member.id)
challengers = db.exec(statement).all()
challs = {c.member.id_code: {
"challenger": {"id_code": c.member.id_code, "name": getUsername(c.member)},
# 'validated': chall.mistakes <= parcours.max_mistakes
"challenges": [Challenges(**{**chall.dict(), "canCorrige": chall.data != []}) for chall in getChallenges(c, db)]
} for c in challengers}
return {**parcours.dict(), "pb": pb, "tops": tops, "challenges": challs, "rank": noteRank, "memberRank": avgRank,
"validated": challenger.validated if challenger != None else False, "ranking": avgTop}
tops = []
challs = {}
challenges = sorted(parcours.challenges, key=lambda x: (
x.note['value'], x.time), reverse=True)
memberRank = None
rank = None
pb = None
validated = False
total = 0
for i, chall in enumerate(challenges):
total += chall.note['value']
id = chall.challenger.id_code
name = chall.challenger.user.username if chall.challenger.user_id != None else chall.challenger.anonymous.username
if i <= 2:
tops.append({"challenger": {"id_code": id, "name": name},
"note": chall.note, "time": chall.time})
if id == member.id_code:
if challs.get(id) is None:
rank = i + 1
memberRank = len(challs) + 1
pb = {"note": chall.note, "time": chall.time}
if validated is False and chall.validated:
validated = True
if member.is_admin or chall.challenger.id_code == member.id_code:
t = challs.get(id, {"total": 0})['total']
challs[id] = {"challenger": {"id_code": id, "name": name
}, "challenges": [*challs.get(id, {'challenges': []})['challenges'],
Challenges(
**{**chall.dict(), "canCorrige": chall.data != []})],
"total": t + chall.note['value']}
topMembers = [{**c['challenger'], "avg": c['total'] /
len(c['challenges'])} for id, c in challs.items()]
topMembers.sort(key=lambda x: x['avg'], reverse=True)
return {**parcours.dict(), "tops": tops, "challenges": challs, "rank": rank, "memberRank": memberRank, "pb": pb,
"validated": validated,
'avg': None if len(parcours.challenges) == 0 else round(total / len(parcours.challenges), 2),
"ranking": topMembers}
def change_anonymous_clientId(anonymous: Anonymous, db: Session):
@ -298,20 +452,49 @@ def change_anonymous_clientId(anonymous: Anonymous, db: Session):
return anonymous
#Parcours
def validate_exercices(exos: List[ExercicesCreate], db: Session ):
exercices = db.exec(select(Exercice).where(Exercice.web == True).where(col(Exercice.id_code).in_([e.exercice_id for e in exos]))).all()
exos_id_list = [e.exercice_id for e in exos]
exercices.sort(key=lambda e: exos_id_list.index(e.id_code))
return [Exercices(exercice_id=e.id_code, name=e.name, quantity=[ex for ex in exos if ex.exercice_id == e.id_code][0].quantity).dict() for e in exercices]
# Parcours
from services.io import add_fast_api_root
from generateur.generateur_main import generate_from_path, parseGeneratorOut
def create_parcours_db(parcours: ParcoursCreate,room_id: int, db: Session):
def countInput(ex: Exercice, q: int):
exo = parseGeneratorOut(generate_from_path(add_fast_api_root(
ex.exo_source), 1, "web"))
return len(exo.inputs) * q
class ExoToCount(BaseModel):
ex: Exercice
q: int
def getTotal(exs: list[ExoToCount]):
total = 0
for e in exs:
total += countInput(e.ex, e.q)
return total
def validate_exercices(exos: List[ExercicesCreate], db: Session):
exercices = db.exec(select(Exercice).where(Exercice.web == True).where(
col(Exercice.id_code).in_([e.exercice_id for e in exos]))).all()
exos_id_list = [e.exercice_id for e in exos]
# exoToCountList = [ExoToCount(ex=e, q=q) for e, q in zip(exercices, [c.quantity for c in exos])]
exercices.sort(key=lambda e: exos_id_list.index(e.id_code))
return [Exercices(exercice_id=e.id_code, name=e.name,
quantity=[ex for ex in exos if ex.exercice_id == e.id_code][0].quantity,
examples=e.examples).dict() for e in exercices]
def create_parcours_db(parcours: ParcoursCreate, room_id: int, db: Session):
exercices = validate_exercices(parcours.exercices, db)
if len(exercices) == 0:
return "Veuillez entrer au moins un exercice valide"
id_code = generate_unique_code(Parcours, s=db)
parcours_obj = Parcours(**{**parcours.dict(), "exercices": exercices}, room_id=room_id, id_code=id_code)
print(parcours_obj)
parcours_obj = Parcours(
**{**parcours.dict(), "exercices": exercices}, room_id=room_id, id_code=id_code)
db.add(parcours_obj)
db.commit()
db.refresh(parcours_obj)
@ -320,24 +503,80 @@ def create_parcours_db(parcours: ParcoursCreate,room_id: int, db: Session):
def deleteParcoursRelated(parcours: Parcours, db: Session):
db.exec(delete(Challenge).where(Challenge.parcours_id == parcours.id_code))
db.exec(delete(TmpCorrection).where(TmpCorrection.parcours_id == parcours.id_code))
db.exec(delete(TmpCorrection).where(
TmpCorrection.parcours_id == parcours.id_code))
db.exec(delete(Challenger).where(Challenger.parcours_id == parcours.id))
db.commit()
def change_challengers_validation(p: Parcours, validation: int, db: Session):
challengers = db.exec(select(Challenger).where(
Challenger.parcours_id == p.id)).all()
challs = []
for c in challengers:
validated = c.best <= validation
if validated != c.validated:
c.validated = validated
challs.append(c)
db.bulk_save_objects(challs)
db.commit()
def change_challenges_validation(p: Parcours, validation: int, db: Session):
print('cHANGE')
challenges = db.exec(select(Challenge).where(
Challenge.parcours_id == p.id_code)).all()
print('CHALLS', challenges)
challs = []
for c in challenges:
validated = c.mistakes <= validation
print('CHAL', validated, c.validated, c)
if validated != c.validated:
c.validated = validated
challs.append(c)
db.bulk_save_objects(challs)
db.commit()
def changeValidation(p: Parcours, validation: int, db: Session):
change_challengers_validation(p, validation, db)
change_challenges_validation(p, validation, db)
def compareExercices(old: list[Exercices], new: list[ExercicesCreate]):
old = [{"id": o['exercice_id'], "q": o['quantity']} for o in old]
new = [{"id": n.exercice_id, "q": n.quantity} for n in new]
return old == new
def update_parcours_db(parcours: ParcoursCreate, parcours_obj: Parcours, db: Session):
exercices = validate_exercices(parcours.exercices, db)
if len(exercices) == 0:
return "Veuillez entrer au moins un exercice valide"
update_challenges = False
if not compareExercices(parcours_obj.exercices, parcours.exercices):
exercices = validate_exercices(parcours.exercices, db)
if len(exercices) == 0:
return "Veuillez entrer au moins un exercice valide"
deleteParcoursRelated(parcours_obj, db)
update_challenges = True
parcours_obj.exercices = exercices
if parcours_obj.max_mistakes != parcours.max_mistakes:
changeValidation(parcours_obj, parcours.max_mistakes, db)
parcours_obj.name = parcours.name
parcours_obj.time = parcours.time
parcours_obj.max_mistakes = parcours.max_mistakes
parcours_data = parcours.dict(exclude_unset=True)
for key, value in parcours_data.items():
setattr(parcours_obj, key, value)
parcours_obj.exercices = exercices
db.add(parcours_obj)
db.commit()
deleteParcoursRelated(parcours_obj, db)
db.refresh(parcours_obj)
return parcours_obj
return parcours_obj, update_challenges
def delete_parcours_db(parcours: Parcours, db: Session):
db.delete(parcours)
@ -347,138 +586,210 @@ def delete_parcours_db(parcours: Parcours, db: Session):
class CorrigedChallenge(BaseModel):
data: List[List[CorrigedGeneratorOut]]
note: Note
mistakes: int
isCorriged: bool
def create_tmp_correction(data: List[List[CorrigedGeneratorOut]], parcours_id: str, member: Member, db: Session):
def create_tmp_correction(data: List[CorrigedData], parcours_id: str, member: Member, db: Session):
code = generate_unique_code(TmpCorrection, s=db)
tmpCorr = TmpCorrection(data=data, id_code=code,
member=member, parcours_id=parcours_id)
db.add(tmpCorr)
db.commit()
db.refresh(tmpCorr)
return tmpCorr
def change_challenge(challenge: Challenge, corriged: CorrigedChallenge, db: Session):
challenge.data = corriged['data']
challenge.note = corriged['note']
challenge.isCorriged = corriged['isCorriged']
challenge.validated = noteOn20(
corriged['note']['value'], corriged['note']['total']) > challenge.parcours.validate_condition
db.add(challenge)
db.commit()
db.refresh(challenge)
return challenge
def validate_challenge_input(obj: List[List[ParsedGeneratorOut]], corr: TmpCorrection):
def validate_challenge_input(obj: List[CorrectionData], corr: TmpCorrection):
data = corr.data
if len(obj) != len(data):
return False
for i in range(len(data)):
exo_corr = data[i]
exo = obj[i]
if len(exo) != len(exo_corr):
print('EXO', exo)
print('EXO', exo.data)
if len(exo.data) != len(exo_corr['data']):
return
zipped = zip(exo_corr, exo)
same = all([e['calcul'] == f.calcul and len(e['inputs']) == len(f.inputs) for e,f in zipped])
zipped = zip(exo_corr['data'], exo.data)
same = all([e['calcul'] == f.calcul and len(e['inputs'])
== len(f.inputs) for e, f in zipped])
if not same:
return False
return True
def validate_challenge_correction(obj: List[List[CorrigedGeneratorOut]], chall: Challenge):
def validate_challenge_correction(obj: List[CorrigedData], chall: Challenge):
data = chall.data
if len(obj) != len(data):
return False
for i in range(len(data)):
exo_corr = data[i]
exo = obj[i]
if len(exo) != len(exo_corr):
if len(exo.data) != len(exo_corr['data']):
return
zipped = zip(exo_corr, exo)
same = all([e['calcul'] == f.calcul and len(e['inputs']) == len(f.inputs) for e,f in zipped])
zipped = zip(exo_corr['data'], exo.data)
same = all([e['calcul'] == f.calcul and len(e['inputs'])
== len(f.inputs) for e, f in zipped])
if not same:
return False
return True
def corrige_challenge(obj: List[List[ParsedGeneratorOut]], corr: TmpCorrection) -> CorrigedChallenge:
if validate_challenge_input(obj , corr) is False:
if validate_challenge_input(obj, corr) is False:
return None
data = corr.data
note = 0
total = 0
isCorriged = True
mistakes = 0
for i in range(len(data)):
exo_corr = data[i]
exo = obj[i]
exo_corr = data[i]["data"]
exo = obj[i].data
if len(exo) != len(exo_corr):
return
zipped = zip(exo_corr, exo)
for e, f in zipped:
print("HO\n\n")
for k, l in zip(e['inputs'], f.inputs):
k["value"] = str(l.value)
total += 1
if k['correction'] is None:
isCorriged = False
if str(k["correction"]) == str(l.value):
k['valid'] = None
elif str(k["correction"]) == str(l.value):
k['valid'] = True
note += 1
else:
k['valid'] = False
mistakes += 1
return {"data": data, "note": {"value": 1, "total": 3}, "isCorriged": isCorriged}
return {"data": data, "mistakes": mistakes, "isCorriged": isCorriged}
return {"data": data, "mistakes": mistakes, "note": {"value": note, "total": total}, "isCorriged": isCorriged}
def change_correction(obj: List[List[CorrigedGeneratorOut]], chall: Challenge) -> CorrigedChallenge:
def change_correction(obj: List[CorrigedData], chall: Challenge) -> CorrigedChallenge:
if validate_challenge_correction(obj, chall) is False:
return None
data = deepcopy(chall.data)
note = 0
total = 0
isCorriged = True
mistakes = 0
for i in range(len(data)):
exo_corr = data[i]
exo = obj[i]
exo_corr = data[i]['data']
exo = obj[i].data
if len(exo) != len(exo_corr):
return
zipped = zip(exo_corr, exo)
for e, f in zipped:
for k, l in zip(e['inputs'], f.inputs):
k["correction"] = str(l.correction)
k["correction"] = l.correction
k["valid"] = l.valid
total += 1
if k['correction'] is None:
if k['correction'] is None and l.valid is None:
isCorriged = False
if str(k["correction"]) == str(l.value):
if l.valid is True:
note += 1
else:
mistakes += 1
return {"data": data, "mistakes": mistakes, "isCorriged": isCorriged}
return {"data": data, "note": {"value": note, "total": total}, "isCorriged": isCorriged}
def getChallenger(parcours: Parcours, member: Member, db: Session):
challenger = db.exec(select(Challenger).where(
Challenger.member_id == member.id, Challenger.parcours_id == parcours.id)).first()
if challenger is None:
return Challenger(parcours_id=parcours.id, member_id=member.id)
return challenger
def create_challenge(data: List[List[CorrigedGeneratorOut]], challenger: Member, parcours: Parcours, time: int, note: Note, isCorriged: bool, db: Session):
challenge = Challenge(data=data, challenger=challenger, parcours=parcours, time=time, note=note, validated=noteOn20(note["value"], note['total']) >=
parcours.validate_condition, isCorriged=isCorriged, id_code=generate_unique_code(Challenge, s=db))
def ChallengerFromChallenge(c: Challenge, db: Session):
challenger = db.exec(select(Challenger).where(
Challenger.member_id == c.challenger_mid, Challenger.parcours_id == c.challenger_pid)).first()
return challenger
def checkValidated(challenger: Challenger, db: Session, challenge: Challenge | None = None):
challenges = db.exec(select(Challenge).where(Challenge.challenger_mid == challenger.member_id,
Challenge.challenger_pid == challenger.parcours_id,
Challenge.validated == True)).all()
if challenge is not None:
challenges = [c for c in challenges if c.id != challenge.id]
return len(challenges) != 0
def create_challenge(data: List[CorrigedData], challenger: Member, parcours: Parcours, time: int, mistakes: int,
isCorriged: bool, db: Session):
challenger_obj: Challenger = getChallenger(parcours, challenger, db)
validated = mistakes <= parcours.max_mistakes
challenge = Challenge(data=data, challenger_pid=challenger_obj.parcours_id, challenger_mid=challenger_obj.member_id,
parcours=parcours, time=time, mistakes=mistakes, isCorriged=isCorriged,
id_code=generate_unique_code(Challenge, s=db), validated=validated)
if (challenger_obj.best is not None and challenger_obj.best > mistakes) or challenger_obj.best is None:
challenger_obj.best = mistakes
challenger_obj.best_time = time
challenges = db.exec(select([func.count(Challenge.id)]).where(
Challenge.challenger_mid == challenger_obj.member_id, Challenge.challenger_pid == parcours.id)).one()
if validated and challenger_obj.validated is False:
challenger_obj.validated = True
avg = challenger_obj.avg
if avg is None:
avg = 0
challenger_obj.avg = (avg *
(challenges - 1) + mistakes) / (challenges)
db.add(challenge)
db.add(challenger_obj)
db.commit()
db.refresh(challenge)
return challenge
db.refresh(challenger_obj)
print('RETURN,', challenge, challenger_obj)
return challenge, challenger_obj
def change_challenge(challenge: Challenge, corriged: CorrigedChallenge, db: Session):
challenger = ChallengerFromChallenge(challenge, db)
challengesCount = len(getChallenges(challenger, db))
avg = challenger.avg * challengesCount - challenge.mistakes
parcours = challenge.parcours
if challenger.best > corriged['mistakes']:
challenger.best = corriged['mistakes']
challenger.best_time = challenge.time
validated = corriged['mistakes'] <= parcours.max_mistakes
challenge.validated = validated
if challenger.validated == False and validated:
challenger.validated = True
elif challenger.validated == True and not validated:
challenger.validated = checkValidated(challenger, db, challenge)
challenger.avg = (avg + corriged['mistakes']) / challengesCount
challenge.data = corriged['data']
challenge.note = corriged['note']
challenge.mistakes = corriged['mistakes']
challenge.isCorriged = corriged['isCorriged']
challenge.validated = noteOn20(
corriged['note']['value'], corriged['note']['total']) > challenge.parcours.validate_condition
# challenge.validated = corriged['mistakes'] <= parcours.max_mistakes
db.add(challenge)
db.add(challenger)
db.commit()
db.refresh(challenge)
db.refresh(challenger)
return challenge
return challenge, challenger
# Dependencies
@ -489,8 +800,6 @@ def check_room(room_id: str, db: Session = Depends(get_session)):
return room
def get_room(room_id, db: Session = Depends(get_session)):
room = db.exec(select(Room).where(Room.id_code == room_id)).first()
if room is None:
@ -499,13 +808,14 @@ def get_room(room_id, db: Session = Depends(get_session)):
return room
def get_member_dep(room: Room = Depends(get_room), user: User = Depends(get_current_user_optional), clientId: str | None = Body(default=None), db: Session = Depends(get_session)):
def get_member_dep(room: Room = Depends(get_room), user: User = Depends(get_current_user_optional),
clientId: str | None = Query(default=None), db: Session = Depends(get_session)):
if user is None and clientId is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
if user is not None:
member = get_member_from_user(user.id, room.id, db)
if clientId is not None:
elif clientId is not None:
member = get_member_from_clientId(clientId, room.id, db)
if member is None:
@ -550,11 +860,9 @@ def get_correction(correction_id: str, parcours_id: str, member: Member = Depend
return tmpCorr
def get_challenge(challenge_id: str, parcours_id: str, db: Session = Depends(get_session)):
def get_challenge(challenge_id: str, db: Session = Depends(get_session)):
challenge = db.exec(select(Challenge).where(
Challenge.id_code == challenge_id, Challenge.parcours_id == parcours_id)).first()
Challenge.id_code == challenge_id)).first()
if challenge is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Challenge introuvable")
@ -564,4 +872,3 @@ def get_challenge(challenge_id: str, parcours_id: str, db: Session = Depends(get
status_code=status.HTTP_400_BAD_REQUEST, detail="Impossible de corriger ce challenge")
return challenge

View File

@ -2,214 +2,325 @@ from uuid import UUID, uuid4
from pydantic import root_validator, BaseModel
import pydantic.json
from typing import List, Optional, TYPE_CHECKING
from sqlmodel import SQLModel, Field, Relationship, JSON, Column
from sqlmodel import SQLModel, Field, Relationship, JSON, Column, ForeignKeyConstraint
from database.exercices.models import Example
from database.auth.models import UserRead
if TYPE_CHECKING:
from database.auth.models import User
class RoomBase(SQLModel):
name: str = Field(max_length=20)
public: bool = Field(default=False)
global_results: bool = Field(default=False)
name: str = Field(max_length=20)
public: bool = Field(default=False)
global_results: bool = Field(default=False)
class RoomCreate(RoomBase):
pass
pass
class Room(RoomBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
id_code: str = Field(index=True)
id: Optional[int] = Field(default=None, primary_key=True)
id_code: str = Field(index=True)
members: List['Member'] = Relationship(back_populates="room")
parcours: List['Parcours'] = Relationship(back_populates="room")
members: List['Member'] = Relationship(back_populates="room")
parcours: List['Parcours'] = Relationship(back_populates="room")
class AnonymousBase(SQLModel):
username: str = Field(max_length=20)
username: str = Field(max_length=20)
class AnonymousCreate(AnonymousBase):
pass
pass
class Anonymous(AnonymousBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
reconnect_code: str = Field(index=True)
id: Optional[int] = Field(default=None, primary_key=True)
reconnect_code: str = Field(index=True)
clientId: Optional[UUID] = Field(default=uuid4(), index=True)
member: 'Member' = Relationship(back_populates="anonymous")
clientId: Optional[UUID] = Field(default=uuid4(), index=True)
member: 'Member' = Relationship(back_populates="anonymous")
class Member(SQLModel, table = True):
id: Optional[int] = Field(default=None, primary_key=True)
id_code: str = Field(index=True)
class Member(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
id_code: str = Field(index=True)
user_id: Optional[int] = Field(foreign_key="user.id", default=None)
user: Optional["User"] = Relationship(back_populates='members')
user_id: Optional[int] = Field(foreign_key="user.id", default=None)
user: Optional["User"] = Relationship(back_populates='members')
anonymous_id: Optional[int] = Field(foreign_key="anonymous.id", default=None)
anonymous: Optional[Anonymous] = Relationship(back_populates="member")
anonymous_id: Optional[int] = Field(
foreign_key="anonymous.id", default=None)
anonymous: Optional[Anonymous] = Relationship(back_populates="member")
room_id: int = Field(foreign_key="room.id")
room: Room = Relationship(back_populates='members')
room_id: int = Field(foreign_key="room.id")
room: Room = Relationship(back_populates='members')
challenges: List["Challenge"] = Relationship(back_populates="challenger")
challengers: List["Challenger"] = Relationship(back_populates="member")
is_admin: bool = False
is_admin: bool = False
waiting: bool = False
waiting: bool = False
online: bool = False
online: bool = False
waiter_code: Optional[str] = Field(default= None)
waiter_code: Optional[str] = Field(default=None)
corrections: List['TmpCorrection'] = Relationship(back_populates="member")
corrections: List['TmpCorrection'] = Relationship(back_populates="member")
class ExercicesCreate(SQLModel):
exercice_id: str
quantity: int = 10
exercice_id: str
quantity: int = 10
class Exercices(ExercicesCreate):
name: str
name: str
examples: Example
class Challenger(SQLModel, table=True):
member_id: int = Field(foreign_key="member.id", primary_key=True)
parcours_id: int = Field(foreign_key="parcours.id", primary_key=True)
parcours: "Parcours" = Relationship(back_populates="challengers")
member: "Member" = Relationship(back_populates="challengers")
#challenges: list["Challenge"] = Relationship(back_populates="challenger")
avg: Optional[float] = Field(default=None)
best: Optional[int] = Field(default=None)
best_time: Optional[int] = Field(default=None)
validated: bool = Field(default=False)
class Parcours(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
id_code: str = Field(index=True, unique=True)
id: Optional[int] = Field(default=None, primary_key=True)
id_code: str = Field(index=True, unique=True)
room_id: int = Field(foreign_key="room.id")
room: Room = Relationship(back_populates='parcours')
challengers: list[Challenger] = Relationship(back_populates="parcours")
name: str
time: int
max_mistakes: int
exercices: List[Exercices] = Field(sa_column=Column(JSON))
challenges: List["Challenge"] = Relationship(back_populates="parcours")
corrections: List["TmpCorrection"] = Relationship(
back_populates="parcours")
room_id: int = Field(foreign_key="room.id")
room: Room = Relationship(back_populates='parcours')
name: str
time: int
validate_condition: int
exercices: List[Exercices] = Field(sa_column=Column(JSON))
challenges: List["Challenge"] = Relationship(back_populates="parcours")
corrections: List["TmpCorrection"] = Relationship(back_populates="parcours")
class Note(BaseModel):
value: int
total: int
value: int
total: int
class TimedNote(Note):
time: int
time: int
class ParcoursReadShort(SQLModel):
name: str
best_note: str | None = None
id_code: str
name: str
best_note: str | None = None
validated: bool = False
id_code: str
class ParcoursReadUpdate(SQLModel):
id_code: str
name: str
time: int
max_mistakes: int
exercices: List[Exercices]
update_challenges: bool = False
class ChallengerInfo(BaseModel):
name: str
id_code: str
class ChallengerAverage(ChallengerInfo):
avg: float
class Challenges(SQLModel):
id_code: str
challenger: str
note: Note
time: int
isCorriged: bool
canCorrige: bool
validated: bool
id_code: str
mistakes: int
time: int
isCorriged: bool
canCorrige: bool = True
validated: bool = False
class ChallengeInfo(BaseModel):
challenger: ChallengerInfo
challenges: list[Challenges]
#total: int
class Tops(BaseModel):
challenger: ChallengerInfo
mistakes: int
time: int
class ParcoursRead(SQLModel):
name: str
time: int
validate_condition: int
id_code: str
exercices: List[Exercices]
challenges: List[Challenges]
tops: list[Tops]
name: str
time: int
max_mistakes: int
id_code: str
exercices: List[Exercices]
challenges: dict[str, ChallengeInfo]
rank: int | None
pb: dict[str, int] | None
memberRank: int | None
validated: bool
ranking: list[ChallengerAverage]
#avg: float | None
class ParcoursCreate(SQLModel):
name: str
time: int
validate_condition: int
exercices: List[ExercicesCreate]
name: str
time: int
max_mistakes: int
exercices: List[ExercicesCreate]
class NotCorrigedInput(BaseModel):
index: int
value: str
index: int
value: str
class CorrigedInput(NotCorrigedInput):
correction: str
correction: str | None
valid: bool | None
class ParsedGeneratorOut(BaseModel):
calcul: str
inputs: List[NotCorrigedInput]
calcul: str
inputs: List[NotCorrigedInput]
class CorrigedGeneratorOut(BaseModel):
calcul: str
inputs: List[CorrigedInput]
calcul: str
inputs: List[CorrigedInput]
class Challenge(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
id_code: str = Field(index=True, unique=True)
id: Optional[int] = Field(default=None, primary_key=True)
id_code: str = Field(index=True, unique=True)
challenger_id: int = Field(foreign_key="member.id")
challenger: Member = Relationship(back_populates="challenges")
''' challenger_id: int = Field(foreign_key="member.id")
challenger: Member = Relationship(back_populates="challenges") '''
parcours_id: int = Field(foreign_key="parcours.id_code")
parcours: Parcours = Relationship(back_populates="challenges")
parcours_id: int = Field(foreign_key="parcours.id_code")
parcours: Parcours = Relationship(back_populates="challenges")
data: Optional[List[List[CorrigedGeneratorOut]]] = Field(sa_column=Column(JSON), default=[])
challenger_pid: int
challenger_mid: int
__table_args__ = (ForeignKeyConstraint(["challenger_pid", "challenger_mid"],
["challenger.parcours_id","challenger.member_id"]),
{})
time: int
note: Note = Field(
sa_column=Column(JSON))
validated: bool
isCorriged: bool
#challenger: "Challenger" = Relationship(back_populates="challenges")
data: Optional[List] = Field(
sa_column=Column(JSON), default=[])
time: int
''' note: Note = Field(
sa_column=Column(JSON)) '''
mistakes: int
#note_value: int
validated: bool
isCorriged: bool
class ExoInfo(BaseModel):
id_code: str
name: str
consigne: str | None
class CorrigedData(BaseModel):
exo: ExoInfo
data: List[CorrigedGeneratorOut]
class CorrectionData(BaseModel):
exo: ExoInfo
data: List[ParsedGeneratorOut]
class ChallengeRead(SQLModel):
id_code: str
data: Optional[List[List[CorrigedGeneratorOut]]] = []
data: Optional[List[CorrigedData]] = []
time: int
note: Note
mistakes: int
validated: bool
isCorriged: bool
class TmpCorrection(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
id_code: str = Field(index=True)
id: Optional[int] = Field(default=None, primary_key=True)
id_code: str = Field(index=True)
parcours_id: str = Field(foreign_key="parcours.id_code")
parcours: Parcours = Relationship(back_populates="corrections")
parcours_id: str = Field(foreign_key="parcours.id_code")
parcours: Parcours = Relationship(back_populates="corrections")
member_id: int = Field(foreign_key="member.id")
member: Member = Relationship(back_populates="corrections")
member_id: int = Field(foreign_key="member.id")
member: Member = Relationship(back_populates="corrections")
data: List = Field(sa_column=Column(JSON))
data: List[List[CorrigedGeneratorOut]] = Field(sa_column=Column(JSON))
class AnonymousRead(AnonymousBase):
reconnect_code: str
reconnect_code: str
class Username(SQLModel):
username: str
username: str
class MemberRead(SQLModel):
username: str
reconnect_code: str = ''
isUser: bool
isAdmin: bool
id_code: str
username: str
reconnect_code: str = ''
isUser: bool
isAdmin: bool
id_code: str
clientId: str = ""
online: bool
class RoomRead(RoomBase):
id_code: str
id_code: str
class RoomAndMember(BaseModel):
room: RoomRead
member: MemberRead
class RoomInfo(RoomRead):
public: bool
name: str
members: List[MemberRead]
parcours: List[ParcoursReadShort]
class Waiter(BaseModel):
username: str
waiter_id: str
class RoomInfo(RoomRead):
public: bool
name: str
members: List[MemberRead | Waiter]
parcours: List[ParcoursReadShort]
class RoomConnectionInfos(BaseModel):
room: str
member: str | None = None
room: str
member: str | None = None

BIN
backend/api/database2.db Normal file

Binary file not shown.

BIN
backend/api/database3.db Normal file

Binary file not shown.

BIN
backend/api/database4.db Normal file

Binary file not shown.

BIN
backend/api/database5.db Normal file

Binary file not shown.

BIN
backend/api/database6.db Normal file

Binary file not shown.

BIN
backend/api/database7.db Normal file

Binary file not shown.

View File

View File

@ -70,3 +70,4 @@ def Csv_generator(path, nb_in_serie, nb_page, police, consigne, writer):
for r in range(rest_line):
writer.writerow([''])

View File

@ -1,42 +1,46 @@
import random
import re
import importlib.util
import string
from typing import List
from pydantic import BaseModel
import sympy
from pydantic import BaseModel
class GeneratorOut(BaseModel):
calcul: str
correction: str | None = None
def parseOut(calcul):
"""Fait en sorte de séparer la correction présente dans le calcul"""
regex = r"\[(.*?)\]"
calculEx = calcul['calcul'].replace('[', ' [').replace(']', '] ')
splitted = calculEx.split()
"""Fait en sorte de séparer la correction présente dans le calcul"""
regex = r"\[(.*?)\]"
calculEx = calcul['calcul'].replace('[', ' [').replace(']', '] ')
splitted = calculEx.split()
if len(list(filter(lambda e: e.startswith("[") and e.endswith(']'), splitted))) == 0:
splitted.append('[]')
if len(list(filter(lambda e: e.startswith("[") and e.endswith(']'), splitted))) == 0:
splitted.append('[]')
inputs = []
for i in range(len(splitted)):
c = splitted[i]
match = re.findall(regex, c)
if len(match) != 0:
correction = c[1:-1]
splitted[i] = f'[{len(inputs)}]'
inputs.append(
{'index': len(inputs), 'correction': correction, 'value': ""})
calculEx = ' '.join(splitted)
return {'calcul': calculEx, 'inputs': inputs}
inputs = []
for i in range(len(splitted)):
c = splitted[i]
match = re.findall(regex, c)
if len(match) != 0:
correction = c[1:-1]
if correction == "":
correction = None
splitted[i] = f'[{len(inputs)}]'
inputs.append(
{'index': len(inputs), 'correction': correction, 'value': ""})
calculEx = ' '.join(splitted)
return {'calcul': calculEx, 'inputs': inputs}
def parseGeneratorOut(out: List[GeneratorOut]):
return [parseOut(c) for c in out]
def getObjectKey(obj, key):
if obj[key] == None:
return None
@ -44,7 +48,9 @@ def getObjectKey(obj, key):
def getCorrectionKey(obj, key):
return key if (obj[key] != False and obj['correction'] == False) else 'calcul' if(obj['calcul'] != False and obj['correction'] == False) else 'correction' if obj['correction'] != False else None
return key if (obj[key] != False and obj['correction'] == False) else 'calcul' if (
obj['calcul'] != False and obj['correction'] == False) else 'correction' if obj[
'correction'] != False else None
def parseCorrection(calc, replacer='...'):
@ -54,12 +60,12 @@ def parseCorrection(calc, replacer='...'):
return calc
def generate_from_data(data, quantity, key, forcedCorrection=False):
def generate_from_data(data, quantity, key, forced_correction=False):
locs = {}
exec(data, {"random": random, "string": string, "sympy": sympy}, locs)
try:
main_func = locs['main']
except:
except KeyError:
return None
main_result = main_func()
default_object = {"calcul": False, 'pdf': False, 'csv': False,
@ -72,17 +78,19 @@ def generate_from_data(data, quantity, key, forcedCorrection=False):
op_list = []
try:
replacer = locs["CORRECTION_REPLACER"]
except:
except KeyError:
replacer = '...'
for i in range(quantity):
main_result = main_func()
main = {**default_object, **main_result}
op_list.append({'calcul': parseCorrection(main[
object_key], replacer) if (forcedCorrection or (key != 'web' and main['correction'] == False)) else main[object_key], "correction": main[correction_key]})
object_key], replacer) if (
forced_correction or (key != 'web' and main['correction'] == False)) else main[object_key],
"correction": main[correction_key]})
return op_list
def generate_from_path(path, quantity, key, forcedCorrection=False):
def generate_from_path(path, quantity, key, forced_correction=False):
data = open(path, "r").read()
return generate_from_data(data, quantity, key, forcedCorrection)
return generate_from_data(data, quantity, key, forced_correction)

View File

@ -1,36 +1,29 @@
#import schemas.base
from services.database import generate_unique_code
from sqlmodel import SQLModel, Field, select
from services.password import get_password_hash
from sqlmodel import Session, select
from database.auth.crud import create_user_db
from services.auth import get_current_user_optional, jwt_required
from fastapi.openapi.utils import get_openapi
from database.auth.models import User, UserRead
from database.exercices.models import Exercice, ExerciceReadFull
from fastapi_pagination import add_pagination
from fastapi.responses import PlainTextResponse
from typing import List, Optional
from fastapi import Depends, Request, status
from fastapi import FastAPI, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError, ValidationError
from fastapi import FastAPI, HTTPException, Depends, Request, status, Header
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi_jwt_auth import AuthJWT
from fastapi_jwt_auth.exceptions import AuthJWTException
from fastapi.responses import JSONResponse
from typing import List, Optional, Sequence
from tortoise.contrib.pydantic import pydantic_model_creator
from fastapi import FastAPI, HTTPException, params
from tortoise import Tortoise
from fastapi.middleware.cors import CORSMiddleware
from tortoise.contrib.fastapi import register_tortoise
from pydantic import BaseModel, validator
from database.db import create_db_and_tables, get_session
from services.jwt import revoke_access, revoke_refresh
import routes.base
from redis import Redis
from fastapi.encoders import jsonable_encoder
import config
from fastapi_pagination import add_pagination
from sqladmin import Admin, ModelView
from sqlmodel import SQLModel, Field
from sqlmodel import Session, select
import config
import routes.base
from database.auth.crud import create_user_db
from database.auth.models import User, UserRead
from database.db import create_db_and_tables, get_session
from database.db import engine
from fastapi.security import OAuth2PasswordBearer, HTTPBearer
from database.exercices.models import Exercice, ExerciceReadFull
from services.jwt import revoke_access, revoke_refresh
from services.password import get_password_hash
app = FastAPI(title="API Generateur d'exercices")
origins = [
"http://localhost:8000",
@ -102,7 +95,7 @@ def test(test_1: str, test_2: str, test_3: str = Depends(t), test_4: str = Depen
@app.exception_handler(RequestValidationError)
@app.exception_handler(ValidationError)
async def validation_exception_handler(request, exc: RequestValidationError|ValidationError):
async def validation_exception_handler(request: Request, exc: RequestValidationError|ValidationError):
errors = {}
print(exc.errors())
for e in exc.errors():

View File

@ -1,15 +1,18 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from services.jwt import revoke_access
from services.password import get_password_hash, verify_password
from services.auth import get_current_clientId, get_current_user, get_current_user_optional, jwt_refresh_required
from database.auth.crud import change_user_uuid, check_unique_username, create_user_db, delete_user_db, update_password_db, update_user_db
from services.auth import authenticate_user
from database.auth.models import PasswordSet, User, UserEdit, UserRead, UserRegister
from pydantic import BaseModel
from fastapi_jwt_auth import AuthJWT
from sqlmodel import Session,select
from pydantic import BaseModel
from sqlmodel import Session, select
from database.auth.crud import change_user_uuid, check_unique_username, create_user_db, delete_user_db, \
update_password_db, update_user_db, parse_user_rooms
from database.auth.models import PasswordSet, User, UserEdit, UserRead, UserRegister, UserEditRead
from database.db import get_session
from services.auth import authenticate_user
from services.auth import get_current_clientId, get_current_user, get_current_user_optional, jwt_refresh_required
from services.password import get_password_hash, verify_password
router = APIRouter(tags=['Authentification'])
@ -22,8 +25,9 @@ class Token(BaseModel):
def login_for_access_token(user: User = Depends(authenticate_user)):
Authorize = AuthJWT()
access_token = Authorize.create_access_token(
subject=str(user.clientId), fresh=True)
refresh_token = Authorize.create_refresh_token(subject=str(user.clientId))
subject=str(user.clientId), fresh=True, user_claims={"username": user.username})
refresh_token = Authorize.create_refresh_token(subject=str(
user.clientId), user_claims={"username": user.username})
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
@router.post('/register', response_model=Token)
@ -33,8 +37,9 @@ def register(user: UserRegister = Depends(UserRegister.as_form), Authorize: Auth
raise HTTPException(status_code = status.HTTP_400_BAD_REQUEST,detail={'username_error': "Nom d'utilisateur indisponible"})
user = create_user_db(username, get_password_hash(user.password), db)
access_token = Authorize.create_access_token(
subject=str(user.clientId))
refresh_token = Authorize.create_refresh_token(subject=str(user.clientId))
subject=str(user.clientId), user_claims={"username": user.username})
refresh_token = Authorize.create_refresh_token(subject=str(
user.clientId), user_claims={"username": user.username})
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
@router.get('/users', response_model=List[UserRead])
@ -42,7 +47,11 @@ def get_users(db: Session = Depends(get_session)):
users = db.exec(select(User)).all()
return users
@router.put('/user' , response_model=UserRead,)
@router.get('/user', response_model=UserRead)
def get_user(user: User = Depends(get_current_user), db: Session = Depends(get_session)):
return {**user.dict(), "rooms": parse_user_rooms(user, db)}
@router.put('/user' , response_model=UserEditRead,)
def update_user(user: UserEdit = Depends(UserEdit.as_form), clientId: str = Depends(get_current_clientId), db: Session = Depends(get_session)):
user_obj = update_user_db(clientId, user, db)
return user_obj
@ -58,8 +67,9 @@ def update_password(password: PasswordSet = Depends(PasswordSet.as_form), user:
user_obj = change_user_uuid(user.id, db)
access_token = Authorize.create_access_token(
subject=str(user_obj))
refresh_token = Authorize.create_refresh_token(subject=str(user_obj))
subject=str(user_obj), user_claims={"username": user.username})
refresh_token = Authorize.create_refresh_token(
subject=str(user_obj), user_claims={"username": user.username})
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
@ -82,5 +92,6 @@ def check_token(user: User = Depends(get_current_user_optional)):
@router.post('/refresh')
def refresh(Authorize: AuthJWT = Depends(jwt_refresh_required)):
current_user = Authorize.get_jwt_subject()
new_access_token = Authorize.create_access_token(subject=current_user)
username = Authorize.get_raw_jwt()['username']
new_access_token = Authorize.create_access_token(subject=current_user, user_claims={"username":username})
return {"access_token": new_access_token}

View File

@ -1,27 +1,33 @@
from pydantic import BaseModel
import csv
import io
from enum import Enum
from typing import List
from fastapi import APIRouter, Depends, Path, Query, UploadFile, HTTPException, status
from fastapi import APIRouter, Depends, Query, UploadFile, HTTPException, status
from fastapi.responses import FileResponse, StreamingResponse
from fastapi_pagination.ext.sqlalchemy_future import paginate as p
from pydantic import BaseModel
from sqlmodel import Session, select
from database.auth.models import User
from database.db import get_session
from database.exercices.models import Exercice, ExerciceCreate, ExerciceEdit, ExerciceReadFull, ExercicesTagLink, Tag, TagCreate, TagRead, ExerciceRead
from database.exercices.crud import add_tags_db, check_exercice_author, check_private, check_tag_author, create_exo_db, \
delete_exo_db, get_exo_dependency, clone_exo_db, remove_tag_db, serialize_exo, update_exo_db, get_tags_dependency
from database.exercices.models import Exercice, ExerciceCreate, ExerciceEdit, ExerciceReadFull, ExercicesTagLink, Tag, \
TagCreate, TagRead, ExerciceRead
from generateur.generateur_csv import Csv_generator
from services.auth import get_current_user, get_current_user_optional
from sqlmodel import Session, select, col
from database.exercices.crud import add_tags_db, check_exercice_author, check_private, check_tag_author, create_exo_db, delete_exo_db, get_exo_dependency, clone_exo_db, parse_exo_tags, remove_tag_db, serialize_exo, update_exo_db, get_tags_dependency
from services.exoValidation import validate_file, validate_file_optionnal
from services.io import add_fast_api_root, get_filename_from_path
from fastapi.responses import FileResponse
from sqlmodel import func
from fastapi_pagination import paginate ,Page
from services.models import Page
from fastapi_pagination.ext.sqlalchemy_future import paginate as p
router = APIRouter(tags=['exercices'])
class ExoType(str, Enum):
csv="csv"
pdf="pdf"
web="web"
csv = "csv"
pdf = "pdf"
web = "web"
def filter_exo_by_tags(exos: List[tuple[Exercice, str]], tags: List[Tag]):
@ -30,24 +36,27 @@ def filter_exo_by_tags(exos: List[tuple[Exercice, str]], tags: List[Tag]):
return valid_exos
def queryFilters_dependency(search: str = "", tags: List[str] | None = Depends(get_tags_dependency), type: ExoType | None = Query(default = None)):
def queryFilters_dependency(search: str = "", tags: List[str] | None = Depends(get_tags_dependency),
type: ExoType | None = Query(default=None)):
return search, tags, type
@router.post('/exercices', response_model=ExerciceReadFull, status_code=status.HTTP_201_CREATED)
def create_exo(exercice: ExerciceCreate = Depends(ExerciceCreate.as_form), file: UploadFile = Depends(validate_file), user: User = Depends(get_current_user), db: Session = Depends(get_session)):
def create_exo(exercice: ExerciceCreate = Depends(ExerciceCreate.as_form), file: UploadFile = Depends(validate_file),
user: User = Depends(get_current_user), db: Session = Depends(get_session)):
file_obj = file['file'].file._file
file_obj.name = file['file'].filename
exo_obj = create_exo_db(exercice=exercice, user=user,
exo_source=file_obj, supports=file['supports'],db=db)
exo_source=file_obj, supports=file['supports'], db=db)
return serialize_exo(exo=exo_obj, user_id=user.id, db=db)
@router.post('/clone/{id_code}', response_model=ExerciceReadFull)
def clone_exo(exercice: Exercice | None = Depends(check_private), user: User = Depends(get_current_user), db: Session = Depends(get_session)):
def clone_exo(exercice: Exercice | None = Depends(check_private), user: User = Depends(get_current_user),
db: Session = Depends(get_session)):
if not exercice:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail={
"Exercice introuvable"})
"Exercice introuvable"})
exo_obj = clone_exo_db(exercice, user, db)
if type(exo_obj) == str:
raise HTTPException(
@ -55,8 +64,10 @@ def clone_exo(exercice: Exercice | None = Depends(check_private), user: User = D
return serialize_exo(exo=exo_obj, user_id=user.id, db=db)
@router.get('/exercices/user', response_model=Page[ExerciceRead|ExerciceReadFull])
def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tuple[str, List[int] | None, ExoType | None] = Depends(queryFilters_dependency), db: Session = Depends(get_session)):
@router.get('/exercices/user', response_model=Page[ExerciceRead | ExerciceReadFull])
def get_user_exercices(user: User = Depends(get_current_user),
queryFilters: tuple[str, List[int] | None, ExoType | None] = Depends(queryFilters_dependency),
db: Session = Depends(get_session)):
search, tags, type = queryFilters
statement = select(Exercice)
@ -71,7 +82,7 @@ def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tup
statement = statement.where(Exercice.web == True)
for t in tags:
sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id==Exercice.id).where(
sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id == Exercice.id).where(
ExercicesTagLink.tag_id == t).exists()
statement = statement.where(sub)
page = p(db, statement)
@ -81,14 +92,17 @@ def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tup
return page
@router.get('/exercices/public', response_model=Page[ExerciceRead|ExerciceReadFull])
def get_public_exercices(user: User | None = Depends(get_current_user_optional), queryFilters: tuple[str, List[int] | None] = Depends(queryFilters_dependency), db: Session = Depends(get_session)):
@router.get('/exercices/public', response_model=Page[ExerciceRead | ExerciceReadFull])
def get_public_exercices(user: User | None = Depends(get_current_user_optional),
queryFilters: tuple[str, List[int] | None] = Depends(queryFilters_dependency),
db: Session = Depends(get_session)):
search, tags, type = queryFilters
if user is not None:
statement = select(Exercice)
statement = statement.where(Exercice.author_id != user.id)
statement = statement.where(Exercice.private == False)
statement = statement.where(Exercice.origin_id == None)
statement = statement.where(Exercice.name.startswith(search))
if type == ExoType.csv:
@ -99,8 +113,8 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional),
statement = statement.where(Exercice.web == True)
for t in tags:
sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id==Exercice.id).where(
ExercicesTagLink.tag_id == t).exists()
sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id == Exercice.id).where(
ExercicesTagLink.tag_id == t).exists()
statement = statement.where(sub)
page = p(db, statement)
@ -129,12 +143,14 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional),
@router.get('/exercice/{id_code}', response_model=ExerciceReadFull)
def get_exercice(exo: Exercice = Depends(check_private), user: User | None = Depends(get_current_user_optional), db: Session = Depends(get_session)):
def get_exercice(exo: Exercice = Depends(check_private), user: User | None = Depends(get_current_user_optional),
db: Session = Depends(get_session)):
return serialize_exo(exo=exo, user_id=getattr(user, 'id', None), db=db)
@router.put('/exercice/{id_code}', response_model=ExerciceReadFull)
def update_exo(file: UploadFile = Depends(validate_file_optionnal), exo: Exercice = Depends(check_exercice_author), exercice: ExerciceEdit = Depends(ExerciceEdit.as_form), db: Session = Depends(get_session)):
def update_exo(file: UploadFile = Depends(validate_file_optionnal), exo: Exercice = Depends(check_exercice_author),
exercice: ExerciceEdit = Depends(ExerciceEdit.as_form), db: Session = Depends(get_session)):
if exo is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable')
@ -151,7 +167,8 @@ def update_exo(file: UploadFile = Depends(validate_file_optionnal), exo: Exercic
@router.delete('/exercice/{id_code}')
def delete_exercice(exercice: Exercice | bool | None = Depends(check_exercice_author), db: Session = Depends(get_session)):
def delete_exercice(exercice: Exercice | bool | None = Depends(check_exercice_author),
db: Session = Depends(get_session)):
if exercice is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail="Exercice introuvable")
@ -162,23 +179,24 @@ def delete_exercice(exercice: Exercice | bool | None = Depends(check_exercice_au
return {'detail': 'Exercice supprimé avec succès'}
class NewTags(BaseModel):
exo: ExerciceReadFull
tags: list[TagRead]
@router.post('/exercice/{id_code}/tags', response_model=NewTags, tags=['tags'])
def add_tags(tags: List[TagCreate], exo: Exercice | None = Depends(get_exo_dependency), db: Session = Depends(get_session), user: User = Depends(get_current_user)):
def add_tags(tags: List[TagCreate], exo: Exercice | None = Depends(get_exo_dependency),
db: Session = Depends(get_session), user: User = Depends(get_current_user)):
if exo is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable')
exo_obj, new = add_tags_db(exo, tags, user, db)
return {"exo":serialize_exo(exo=exo_obj, user_id=user.id, db=db), "tags": new}
return {"exo": serialize_exo(exo=exo_obj, user_id=user.id, db=db), "tags": new}
@router.delete('/exercice/{id_code}/tags/{tag_id}', response_model=ExerciceReadFull, tags=['tags'])
def remove_tag(exo: Exercice | None = Depends(get_exo_dependency), tag: Tag | None | bool = Depends(check_tag_author), db: Session = Depends(get_session)):
def remove_tag(exo: Exercice | None = Depends(get_exo_dependency), tag: Tag | None | bool = Depends(check_tag_author),
db: Session = Depends(get_session)):
if exo is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail='Exercice introuvable')
@ -194,7 +212,7 @@ def remove_tag(exo: Exercice | None = Depends(get_exo_dependency), tag: Tag | No
@router.get('/exercice/{id_code}/exo_source')
async def get_exo_source(exo: Exercice = Depends(check_exercice_author), db: Session = Depends(get_session)):
async def get_exo_source(exo: Exercice = Depends(check_exercice_author)):
if exo is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail='Exercice introuvable')
@ -203,9 +221,30 @@ async def get_exo_source(exo: Exercice = Depends(check_exercice_author), db: Ses
detail='Cet exercice ne vous appartient pas')
path = add_fast_api_root(exo.exo_source)
filename = get_filename_from_path(path)
return FileResponse(path, headers={'content-disposition': 'attachment;filename='+filename})
return FileResponse(path, headers={'content-disposition': 'attachment;filename=' + filename})
@router.get('/tags', response_model=List[TagRead], tags=['tags'])
def get_tags(user: User = Depends(get_current_user), db: Session = Depends(get_session)):
return user.tags
@router.get('/generator/csv/{id_code}')
async def generate_csv(*, exo: Exercice | None = Depends(get_exo_dependency), filename: str):
if exo.csv is False:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail='Impossible de générer cet exercice sur dans ce format')
source_path = add_fast_api_root(exo.exo_source)
consigne = exo.consigne
buffer = io.StringIO()
writer = csv.writer(buffer, delimiter=',',
quotechar=',', quoting=csv.QUOTE_MINIMAL, dialect='excel') # mettre | comme sep un jour
Csv_generator(source_path, 10, 10, 12, consigne, writer)
return StreamingResponse(iter([buffer.getvalue()]),
headers={"Content-Disposition": f'attachment;filename={filename}'}, media_type='text/csv')

View File

@ -1,12 +1,18 @@
from typing import Any, TYPE_CHECKING, Callable
from fastapi.websockets import WebSocket
from services.websocket import Consumer
from typing import Any, TYPE_CHECKING
from database.room.models import Room, Member, MemberRead, Waiter
from sqlmodel import Session
from database.room.crud import change_room_name, change_room_status, serialize_member,check_user_in_room, create_anonymous, create_member, get_member, get_member_from_token, get_member_from_reconnect_code, connect_member, disconnect_member, create_anonymous_member, create_anonymous_waiter, create_user_member, create_user_waiter, get_or_create_member, get_waiter, accept_waiter, leave_room, refuse_waiter, check_room
from database.auth.crud import get_user_from_token
from database.room.crud import change_room_name, change_room_status, serialize_member, check_user_in_room, \
create_anonymous, create_member, get_member, get_member_from_token, get_member_from_reconnect_code, connect_member, \
disconnect_member, get_waiter, accept_waiter, leave_room, refuse_waiter
from database.room.models import Room, Member, MemberRead, Waiter
from services.websocket import Consumer
if TYPE_CHECKING:
from routes.room.routes import RoomManager
from routes.room.routes import RoomManager
class RoomConsumer(Consumer):
@ -18,12 +24,19 @@ class RoomConsumer(Consumer):
self.member = None
# WS Utilities
async def send(self, payload: Any | Callable):
if callable(payload):
payload = payload(self.member)
return await super().send(payload)
async def connect(self):
await self.ws.accept()
async def direct_send(self, type: str, payload: Any):
await self.ws.send_json({'type': type, "data": payload})
async def direct_send(self, type: str, payload: Any, code: int | None = None):
sending = {'type': type, "data": payload, }
if code != None:
sending["code"] = code
await self.ws.send_json({'type': type, "data": payload, })
async def send_to_admin(self, type: str, payload: Any, exclude: bool = False):
await self.manager.send_to_admin(self.room.id_code, {'type': type, "data": payload})
@ -32,7 +45,8 @@ class RoomConsumer(Consumer):
await self.manager.send_to(self.room.id_code, member_id, {'type': type, "data": payload})
async def broadcast(self, type, payload, exclude=False):
await self.manager.broadcast({"type": type, "data": payload}, self.room.id_code, exclude=[exclude == True and self])
await self.manager.broadcast({"type": type, "data": payload}, self.room.id_code,
exclude=[exclude == True and self])
def add_to_group(self):
self.manager.add(self.room.id_code, self)
@ -40,13 +54,20 @@ class RoomConsumer(Consumer):
async def connect_self(self):
if isinstance(self.member, Member):
connect_member(self.member, self.db)
await self.broadcast(type="connect", payload={"member": serialize_member(self.member)}, exclude=True)
await self.manager.broadcast(lambda m: {"type": "connect", "data": {
"member": serialize_member(self.member, admin=m.is_admin, m2=m)}}, self.room.id_code, exclude=[self])
# await self.broadcast(type="connect", payload={"member": serialize_member(self.member)}, exclude=True)
async def disconnect_self(self):
if isinstance(self.member, Member):
''' self.db.expire(self.member)
self.db.refresh(self.member) '''
disconnect_member(self.member, self.db)
if self.member.waiting is False:
await self.broadcast(type="disconnect", payload={"member": serialize_member(self.member)})
await self.manager.broadcast(lambda m: {"type": "disconnect", "data": {
"member": serialize_member(self.member, admin=m.is_admin, m2=m)}}, self.room.id_code,
exclude=[self])
# await self.broadcast(type="disconnect", payload={"member": serialize_member(self.member)})
else:
await self.send_to_admin(type="disconnect_waiter", payload={"waiter": serialize_member(self.member)})
@ -55,11 +76,12 @@ class RoomConsumer(Consumer):
self.member = member
await self.connect_self()
self.add_to_group()
clientId = self.member.anonymous.clientId if self.member.anonymous is not None else ""
await self.direct_send(type="loggedIn", payload={"member": {**serialize_member(self.member), 'clientId': str(clientId)}})
await self.direct_send(type="loggedIn",
payload={"member": {**serialize_member(self.member, private=True, m2=self.member)}})
async def send_error(self, msg, code: int = 400):
await self.direct_send(type="error", payload={"msg": msg, "code": code})
async def send_error(self, msg):
await self.direct_send(type="error", payload={"msg": msg})
# Conditions
async def isAdminReceive(self):
@ -73,12 +95,14 @@ class RoomConsumer(Consumer):
return self.member is not None and self.member.is_admin == True
async def isMember(self):
print('S', self.member, self.ws, self.ws.state, self.ws.application_state.__str__())
if self.member is None:
await self.send_error("Vous n'êtes connecté à aucune salle")
return self.member is not None and self.member.waiting == False
def isWaiter(self):
return self.member is not None and self.member.waiting == True
# Received Events
@Consumer.event('login')
@ -86,23 +110,23 @@ class RoomConsumer(Consumer):
if reconnect_code is None and token is None:
await self.direct_send(type="error", payload={"msg": "Veuillez spécifier une méthode de connection"})
return
print("login", token)
if token is not None:
member = get_member_from_token(token, self.room.id, self.db)
print('MEMBER', member)
if member == False:
await self.send_error("Token expired")
await self.send_error("Token expired", code=422)
return
if member is None:
await self.send_error("Utilisateur introuvable dans cette salle")
await self.send_error("Utilisateur introuvable dans cette salle", code=401)
return
elif reconnect_code is not None:
member = get_member_from_reconnect_code(
reconnect_code, self.room.id, db=self.db)
if member is None:
await self.send_error("Utilisateur introuvable dans cette salle")
await self.send_error("Utilisateur introuvable dans cette salle", code=401)
return
await self.loginMember(member)
@Consumer.event('join')
@ -127,6 +151,9 @@ class RoomConsumer(Consumer):
user=user, room=self.room, waiting=self.room.public is False, db=self.db)
elif username is not None:
if len(username) < 4 or len(username) > 15:
await self.send_error("Nom d'utilisateur invalide ou indisponible")
return
anonymous = create_anonymous(username, self.room, self.db)
if anonymous is None:
await self.send_error("Nom d'utilisateur invalide ou indisponible")
@ -142,26 +169,34 @@ class RoomConsumer(Consumer):
await self.direct_send(type="waiting", payload={"waiter": serialize_member(self.member)})
await self.send_to_admin(type="waiter", payload={"waiter": serialize_member(self.member)})
else:
await self.broadcast(type="joined", payload={"member": serialize_member(self.member)}, exclude=True)
await self.direct_send(type="accepted", payload={"member": serialize_member(self.member)})
await self.manager.broadcast(
lambda m: {"type": "joined", "data": {"member": serialize_member(self.member, admin=m.is_admin, m2=m)}},
self.room.id_code)
# await self.broadcast(type="joined", payload={"member": serialize_member(self.member)}, exclude=True)
await self.direct_send(type="accepted",
payload={"member": serialize_member(self.member, private=True, m2=self.member)})
@Consumer.event('accept', conditions=[isAdminReceive])
async def accept(self, waiter_id: str):
waiter = get_waiter(waiter_id, self.db)
if waiter is None:
await self.send_error("Utilisateur en list d'attente introuvable")
await self.send_error("Utilisateur en liste d'attente introuvable")
return
member = accept_waiter(waiter, self.db)
await self.send_to(type="accepted", payload={"member": serialize_member(member)}, member_id=waiter_id)
await self.broadcast(type="joined", payload={"member": serialize_member(member)})
await self.send_to(type="accepted", payload={"member": serialize_member(member, private=True, m2=member)},
member_id=waiter_id)
await self.manager.broadcast(
lambda m: {"type": "joined",
"data": {"member": serialize_member(member, admin=m.is_admin, m2=m)}},
self.room.id_code)
# await self.broadcast(type="joined", payload={"member": serialize_member(member)})
@Consumer.event('refuse', conditions=[isAdminReceive])
async def accept(self, waiter_id: str):
async def refuse(self, waiter_id: str):
waiter = get_waiter(waiter_id, self.db)
member = refuse_waiter(waiter, self.db)
refuse_waiter(waiter, self.db)
await self.send_to(type="refused", payload={'waiter_id': waiter_id}, member_id=waiter_id)
await self.direct_send(type="successfullyRefused", payload= {"waiter_id": waiter_id})
await self.direct_send(type="successfullyRefused", payload={"waiter_id": waiter_id})
@Consumer.event('ping_room')
async def proom(self):
@ -180,7 +215,7 @@ class RoomConsumer(Consumer):
@Consumer.event('set_name', conditions=[isAdminReceive])
async def change_name(self, name: str):
if len(name) < 20:
self.room = change_room_name(self.room,name, self.db)
self.room = change_room_name(self.room, name, self.db)
print('SENDING')
await self.broadcast(type="new_name", payload={"name": name})
@ -188,7 +223,7 @@ class RoomConsumer(Consumer):
await self.send_error('Nom trop long (max 20 character)')
@Consumer.event('set_visibility', conditions=[isAdminReceive])
async def change_name(self, public: bool):
async def change_visibility(self, public: bool):
self.room = change_room_status(self.room, public, self.db)
await self.broadcast(type="new_visibility", payload={"public": public})
@ -197,7 +232,6 @@ class RoomConsumer(Consumer):
await self.direct_send(type="error", payload={"msg": "Vous n'êtes connecté à aucune salle"})
return self.member is not None
@Consumer.event('leave', conditions=[isMember])
async def leave(self):
if self.member.is_admin is True:
@ -227,10 +261,11 @@ class RoomConsumer(Consumer):
# Sending Events
@Consumer.sending(['connect', "disconnect", "joined"], conditions=[isMember])
@Consumer.sending(["joined"], conditions=[isMember])
def joined(self, member: MemberRead):
if self.member.id_code == member.id_code:
raise ValueError("") # Prevent from sending event
raise ValueError("") # Prevent from sending event
if self.member.is_admin == False:
member.reconnect_code = ""
return {"member": member}
@ -239,6 +274,11 @@ class RoomConsumer(Consumer):
def waiter(self, waiter: Waiter):
return {"waiter": waiter}
@Consumer.sending('accepted')
def accepted(self, member: MemberRead):
self.db.refresh(self.member)
return {"member": member}
@Consumer.sending("refused", conditions=[isWaiter])
def refused(self, waiter_id: str):
self.member = None
@ -249,6 +289,7 @@ class RoomConsumer(Consumer):
def banned(self):
self.member = None
self.manager.remove(self.room.id, self)
self.ws.close()
return {}
@Consumer.sending('ping', conditions=[isMember])
@ -256,6 +297,6 @@ class RoomConsumer(Consumer):
return {}
async def disconnect(self):
print('DISCONNECTED', self.member)
self.manager.remove(self.room.id, self)
await self.disconnect_self()

View File

@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Dict, List
from typing import TYPE_CHECKING, Dict, List, Callable, Any
from starlette.websockets import WebSocketState
if TYPE_CHECKING:
from routes.room.consumer import RoomConsumer
@ -16,7 +16,8 @@ class RoomManager:
self.active_connections[group].append(member)
async def _send(self, connection: "RoomConsumer", message, group: str):
if connection.ws.application_state == WebSocketState.DISCONNECTED:
print("STATE", connection.ws.client_state.__str__())
if connection.ws.application_state == WebSocketState.DISCONNECTED or connection.ws.client_state == WebSocketState.DISCONNECTED:
self.remove(group, connection)
elif connection.ws.application_state == WebSocketState.CONNECTED:
await connection.send(message)
@ -24,14 +25,17 @@ class RoomManager:
def remove(self, group: str, member: "RoomConsumer"):
if group in self.active_connections:
if member in self.active_connections[group]:
print("remoied")
self.active_connections[group].remove(member)
async def broadcast(self, message, group: str, exclude: list["RoomConsumer"] = []):
print('BROADCaST', message)
async def broadcast(self, message: Any | Callable, group: str, conditions: list[Callable] = [], exclude: list["RoomConsumer"] = [], ):
print('BROADCaST', message, self.active_connections)
if group in self.active_connections:
for connection in list(set(self.active_connections[group])):
# print(connection)
if connection not in exclude:
print(connection, connection.ws.state, connection.ws.client_state, connection.ws.application_state)
if connection not in exclude and all(f(connection) for f in conditions ):
await self._send(connection, message, group)
async def send_to(self, group, id_code, msg):
@ -44,6 +48,6 @@ class RoomManager:
async def send_to_admin(self, group, msg):
if group in self.active_connections:
members = [c for c in self.active_connections[group]
if c.member.is_admin == True]
if c.member is not None and c.member.is_admin == True]
for m in members:
await self._send(m, msg, group)

View File

@ -1,26 +1,52 @@
from services.database import generate_unique_code
from services.io import add_fast_api_root
from generateur.generateur_main import generate_from_path, parseGeneratorOut, parseOut
from database.exercices.models import Exercice
from database.room.crud import CorrigedChallenge, change_correction, corrige_challenge, create_parcours_db, delete_parcours_db, create_room_db, get_member_dep, check_room, serialize_room, update_parcours_db, get_parcours, get_room, check_admin, get_exercices, get_challenge, get_correction, create_tmp_correction, create_challenge, change_challenge
from typing import List, Optional
from pydantic import BaseModel
from typing import Any, Callable, Dict, List, Optional
from fastapi import APIRouter, Depends, WebSocket, status, Query, Body
from config import ALGORITHM, SECRET_KEY
from database.auth.crud import get_user_from_clientId_db
from fastapi.exceptions import HTTPException
from pydantic import BaseModel
from sqlmodel import Session, select
from database.auth.models import User
from database.db import get_session
from sqlmodel import Session, col, select
from database.room.models import Challenge, ChallengeRead, Challenges, CorrigedGeneratorOut, Member, Note, Parcours, ParcoursCreate, ParcoursRead, ParcoursReadShort, ParsedGeneratorOut, Room, RoomConnectionInfos, RoomCreate, RoomAndMember, RoomInfo, TmpCorrection
from database.exercices.models import Exercice
from database.room.crud import serialize_parcours_short, change_correction, corrige_challenge, \
create_parcours_db, delete_parcours_db, create_room_db, get_member_dep, check_room, serialize_room, \
update_parcours_db, get_parcours, get_room, check_admin, get_exercices, get_challenge, get_correction, \
create_tmp_correction, create_challenge, change_challenge, serialize_parcours, getTops, getAvgRank, getRank, \
getAvgTops, ChallengerFromChallenge, getMemberAvgRank, getMemberRank
from database.room.models import Challenge, ChallengeRead, Challenges, ParcoursReadUpdate, ChallengeInfo, Member, \
Parcours, ParcoursCreate, ParcoursRead, ParcoursReadShort, Room, RoomConnectionInfos, \
RoomCreate, RoomInfo, TmpCorrection, CorrigedData, CorrectionData
from generateur.generateur_main import generate_from_path, parseGeneratorOut
from routes.room.consumer import RoomConsumer
from routes.room.manager import RoomManager
from services.auth import get_current_user_optional
from services.io import add_fast_api_root
from services.misc import stripKeyDict
from typing import List, Optional
from fastapi import APIRouter, Depends, WebSocket, status, Query, Body
from fastapi.exceptions import HTTPException
from database.auth.crud import get_user_from_token
from services.websocket import Consumer
from services.misc import noteOn20, stripKeyDict
from pydantic import BaseModel
from sqlmodel import Session, select
from database.auth.models import User
from database.db import get_session
from database.exercices.models import Exercice
from database.room.crud import serialize_parcours_short, change_correction, corrige_challenge, \
create_parcours_db, delete_parcours_db, create_room_db, get_member_dep, check_room, serialize_room, \
update_parcours_db, get_parcours, get_room, check_admin, get_exercices, get_challenge, get_correction, \
create_tmp_correction, create_challenge, change_challenge, serialize_parcours, getTops, getAvgRank, getRank, \
getAvgTops, ChallengerFromChallenge, getMemberAvgRank, getMemberRank
from database.room.models import Challenge, ChallengeRead, Challenges, ParcoursReadUpdate, ChallengeInfo, Member, \
Parcours, ParcoursCreate, ParcoursRead, ParcoursReadShort, Room, RoomConnectionInfos, \
RoomCreate, RoomInfo, TmpCorrection, CorrigedData, CorrectionData
from generateur.generateur_main import generate_from_path, parseGeneratorOut
from routes.room.consumer import RoomConsumer
from routes.room.manager import RoomManager
from services.auth import get_current_user_optional
from services.io import add_fast_api_root
from services.misc import stripKeyDict
router = APIRouter(tags=["room"])
manager = RoomManager()
@ -28,6 +54,7 @@ manager = RoomManager()
def get_manager():
return manager
@router.post('/room', response_model=RoomConnectionInfos)
def create_room(room: RoomCreate, username: Optional[str] = Query(default=None, max_length=20), user: User | None = Depends(get_current_user_optional), db: Session = Depends(get_session)):
room_obj = create_room_db(room=room, user=user, username=username, db=db)
@ -39,36 +66,42 @@ def get_room_route(room: Room = Depends(get_room), member: Member = Depends(get_
return serialize_room(room, member, db)
@router.post('/room/{room_id}/parcours', response_model=ParcoursRead)
async def create_parcours(*, parcours: ParcoursCreate, room_id: str, member: Member = Depends(check_admin), m: RoomManager = Depends(get_manager), db: Session = Depends(get_session)):
parcours_obj = create_parcours_db(parcours, member.room_id, db)
if type(parcours_obj) == str:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=parcours_obj)
await m.broadcast({"type": "add_parcours", "data": {"parcours": ParcoursReadShort(**parcours_obj.dict(exclude_unset=True)).dict()}}, room_id)
return parcours_obj
await m.broadcast({"type": "add_parcours",
"data": {"parcours": ParcoursReadShort(**parcours_obj.dict(exclude_unset=True)).dict()}},
room_id)
return serialize_parcours(parcours_obj, member, db)
@router.get('/room/{room_id}/parcours/{parcours_id}', response_model=ParcoursRead)
async def get_parcours_route(*, parcours: Parcours = Depends(get_parcours), member: Member = Depends(get_member_dep), db: Session = Depends(get_session)):
if member.is_admin == False:
return {**parcours.dict(), "challenges": [Challenges(**{**chall.dict(), "challenger": member.id_code, "canCorrige": chall.data != []}) for chall in parcours.challenges if chall.challenger.id_code == member.id_code]}
if member.is_admin == True:
return {**parcours.dict(), "challenges": [Challenges(**{**chall.dict(), "challenger": member.id_code, "canCorrige": chall.data != []}) for chall in parcours.challenges]}
return serialize_parcours(parcours, member, db)
@router.put('/room/{room_id}/parcours/{parcours_id}', response_model=ParcoursRead, dependencies=[Depends(check_admin)])
async def update_parcours(*, room_id: str, parcours: ParcoursCreate, parcours_old: Parcours = Depends(get_parcours), m: RoomManager = Depends(get_manager), db: Session = Depends(get_session)):
parcours_obj = update_parcours_db(parcours, parcours_old, db)
@router.put('/room/{room_id}/parcours/{parcours_id}', response_model=ParcoursRead)
async def update_parcours(*, room_id: str, parcours: ParcoursCreate, member: Member = Depends(check_admin),
parcours_old: Parcours = Depends(get_parcours), m: RoomManager = Depends(get_manager),
db: Session = Depends(get_session)):
parcours_obj, update_challenges = update_parcours_db(
parcours, parcours_old, db)
if type(parcours_obj) == str:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=parcours_obj)
short = ParcoursReadShort(
**parcours_obj.dict(exclude_unset=True), id_code=parcours_obj.id_code)
await m.broadcast({"type": "update_parcours", "data": {"parcours": short.dict()}}, room_id)
return parcours_obj
await m.broadcast(lambda m: {"type": "update_parcours",
"data": {"parcours": serialize_parcours_short(parcours_obj, m, db).dict()}}, room_id)
await m.broadcast({"type": "edit_parcours", "data": {
"parcours": ParcoursReadUpdate(**parcours_obj.dict(), update_challenges=update_challenges).dict()}},
parcours_old.id_code)
return serialize_parcours(parcours_obj, member, db)
return {**parcours_obj.dict()}
@router.delete('/room/{room_id}/parcours/{parcours_id}', dependencies=[Depends(check_admin)])
@ -82,51 +115,133 @@ class Exos(BaseModel):
exercice: Exercice
quantity: int
@router.get('/room/{room_id}/challenge/{parcours_id}')
def challenge_route(parcours_id: str, exercices: List[Exos] = Depends(get_exercices), member: Member = Depends(get_member_dep), db: Session = Depends(get_session)):
def challenge_route(parcours: Parcours = Depends(get_parcours), exercices: List[Exos] = Depends(get_exercices),
member: Member = Depends(get_member_dep), db: Session = Depends(get_session)):
print('GENERATE', exercices)
correction = [parseGeneratorOut(generate_from_path(add_fast_api_root(
e['exercice'].exo_source), e['quantity'], "web")) for e in exercices]
sending = [[{**c, 'inputs': [stripKeyDict(i, "correction")
for i in c['inputs']]} for c in e] for e in correction]
tmpCorr = create_tmp_correction(correction, parcours_id, member, db)
return {'challenge': sending, "id_code": tmpCorr.id_code}
infos = [{"name": e["exercice"].name, "consigne": e['exercice'].consigne, "id_code": e['exercice'].id_code}
for e in exercices]
sending = [{"exo": ei, "data": [{**c, 'inputs': [stripKeyDict(i, "correction")
for i in c['inputs']]} for c in e]} for e, ei in
zip(correction, infos)]
corriged = [{"exo": ei, "data": e} for e, ei in zip(correction, infos)]
tmpCorr = create_tmp_correction(corriged, parcours.id_code, member, db)
return {'challenge': sending, "id_code": tmpCorr.id_code,
'parcours': {"name": parcours.name, 'time': parcours.time, "max_mistakes": parcours.max_mistakes,
'id_code': parcours.id_code}}
@router.post('/room/{room_id}/challenge/{parcours_id}/{correction_id}', response_model=ChallengeRead)
async def send_challenge(*, challenge: List[List[ParsedGeneratorOut]], correction: TmpCorrection = Depends(get_correction), time: int = Body(), db: Session = Depends(get_session), m: RoomManager = Depends(get_manager),):
async def send_challenge(*, challenge: List[CorrectionData], correction: TmpCorrection = Depends(get_correction),
time: int = Body(), db: Session = Depends(get_session),
m: RoomManager = Depends(get_manager), ):
parcours = correction.parcours
member = correction.member
data = corrige_challenge(challenge, correction)
if data is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail={"challenge_error":"Object does not correspond to correction"})
chall = create_challenge(**data, challenger=member,
parcours=parcours, time=time, db=db)
await m.broadcast({"type": "challenge", "data": Challenges(**{**chall.dict(), "challenger": member.id_code, "canCorrige": chall.data != []}).dict()}, parcours.id_code)
status_code=status.HTTP_400_BAD_REQUEST,
detail={"challenge_error": "Object does not correspond to correction"})
chall, challenger = create_challenge(**data, challenger=member,
parcours=parcours, time=time, db=db)
print('CHALLENGE', chall)
await m.broadcast({"type": "challenge", "data": ChallengeInfo(
challenger={"name": member.user.username if member.user_id != None else member.anonymous.username,
"id_code": member.id_code},
challenges=[Challenges(**{**chall.dict(), "canCorrige": chall.data != [], })]).dict()}, parcours.id_code,
conditions=[lambda c: c.member.is_admin or c.member.id_code == member.id_code])
#TODO : Envoyer que à ceux d'après
await m.broadcast(lambda m: {"type": "newRanks", "data": {"rank": getMemberRank(m, correction.parcours, db),
"avgRank": getMemberAvgRank(m, correction.parcours, db)}},
parcours.id_code)
print('CHALLENGE', chall)
rank, avgRank = getRank(
challenger, parcours, db), getAvgRank(challenger, parcours, db)
print('RANKS', rank, avgRank)
if rank <= 3 or avgRank <= 3:
await m.broadcast({"type": "newTops", "data": {
"tops": getTops(correction.parcours, db),
"avgTops": getAvgTops(correction.parcours, db),
}}, parcours.id_code)
print('CHALLENGE', chall)
db.delete(correction)
returnValue = {**chall.dict(), 'validated': chall.mistakes <= correction.parcours.max_mistakes}
db.commit()
return chall
return returnValue
# return {**chall.dict(), 'validated': chall.mistakes <= correction.parcours.max_mistakes}
@router.get('/room/{room_id}/challenge/{parcours_id}/{challenge_id}', response_model=ChallengeRead, dependencies=[Depends(get_member_dep)])
async def challenge_read(*, challenge: Challenge = Depends(get_challenge)):
return challenge
class ParcoursInfo(BaseModel):
name: str
time: int
# validate: int
id_code: str
@router.put('/room/{room_id}/challenge/{parcours_id}/{challenge_id}', response_model=ChallengeRead, dependencies=[Depends(check_admin)])
async def corrige(*, correction: List[List[CorrigedGeneratorOut]], challenge: Challenge = Depends(get_challenge), db: Session = Depends(get_session), m: RoomManager = Depends(get_manager),):
class Chall(BaseModel):
challenge: Challenge
parcours: ParcoursInfo
# response_model=ChallengeRead
@router.get('/room/{room_id}/correction/{challenge_id}', dependencies=[Depends(get_member_dep)])
async def challenge_read(*, challenge: Challenge = Depends(get_challenge), db: Session = Depends(get_session)):
parcours = challenge.parcours
member = challenge.challenger
member = db.exec(select(Member).where(
Member.id == challenge.challenger_mid)).first()
challenger = ChallengerFromChallenge(challenge, db)
obj = member.user if member.user_id is not None else member.anonymous
return {**ChallengeRead(**challenge.dict()).dict(), "challenger": {"name": obj.username},
'parcours': {"name": parcours.name, 'time': parcours.time, "max_mistakes": parcours.max_mistakes,
'id_code': parcours.id_code}}
# response_model=ChallengeRead
@router.put('/room/{room_id}/correction/{challenge_id}', dependencies=[Depends(check_admin)])
async def corrige(*, correction: List[CorrigedData] = Body(), challenge: Challenge = Depends(get_challenge),
db: Session = Depends(get_session), m: RoomManager = Depends(get_manager), ):
data = change_correction(correction, challenge)
if data is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail={"correction_error": "Object does not correspond to challenge"})
challenge = change_challenge(challenge, data, db)
status_code=status.HTTP_400_BAD_REQUEST,
detail={"correction_error": "Object does not correspond to challenge"})
await m.broadcast({"type": "challenge_change", "data": Challenges(**{**challenge.dict(), "challenger": member.id_code, "canCorrige": challenge.data != []}).dict()}, parcours.id_code)
return challenge
parcours = challenge.parcours
member = db.exec(select(Member).where(
Member.id == challenge.challenger_mid)).first()
challenge, challenger = change_challenge(challenge, data, db)
obj = member.user if member.user_id is not None else member.anonymous
await m.broadcast(lambda m: {"type": "newRanks", "data": {"rank": getMemberRank(m, parcours, db),
"avgRank": getMemberAvgRank(m, parcours, db)}},
parcours.id_code)
rank, avgRank = getRank(
challenger, parcours, db), getAvgRank(challenger, parcours, db)
print('Rank', rank, avgRank)
if rank <= 3 or avgRank <= 3:
await m.broadcast({"type": "newTops", "data": {
"tops": getTops(parcours, db),
"avgTops": getAvgTops(parcours, db),
}}, parcours.id_code)
await m.broadcast({"type": "challenge_change",
"data": {"challenge": Challenges(**challenge.dict()).dict(), "member": member.id_code}},
parcours.id_code, conditions=[lambda m: m.member.is_admin or m.member.id_code == member.id_code])
return {**ChallengeRead(**challenge.dict()).dict(), "challenger": {"name": obj.username}}
return {**ChallengeRead.from_orm(challenge).dict(), "challenger": {"name": obj.username, }}
@router.websocket('/ws/room/{room_id}')

View File

@ -3,6 +3,7 @@ from pydantic import validate_arguments, BaseModel
from fastapi.websockets import WebSocketDisconnect, WebSocket
from pydantic.error_wrappers import ValidationError
import inspect
from starlette.websockets import WebSocketState
def make_event_decorator(eventsDict):
def _(name: str | List, conditions: List[Callable | bool] = []):
@ -68,8 +69,11 @@ class Consumer:
await self.ws.send_json({"type": "error", "data": {"detail": [{ers['loc'][-1]: ers['msg']} for ers in errors]}})
async def send(self, payload):
''' if self.ws.state == WebSocketState.DISCONNECTED:
return '''
type = payload.get('type', None)
#print('TYPE', type, self.member)
print('TYPE', type, self.member)
if type is not None:
event_wrapper = self.sendings.get(type, None)
if event_wrapper is not None:
@ -85,6 +89,7 @@ class Consumer:
try:
validated_payload = model(self=self, **data)
except ValidationError as e:
print("ERROR", e)
await self.ws.send_json({"type": "error", "data": {"msg": "Oops there was an error"}})
return
@ -133,4 +138,5 @@ class Consumer:
data = await self.ws.receive_json()
await self.receive(data)
except WebSocketDisconnect:
print('DISCONNECTION')
await self.disconnect()

View File

@ -116,7 +116,7 @@ def test_clone(client: TestClient):
print(rr.json())
assert rr.status_code == 200
assert 'id_code' in rr.json()
assert {**rr.json(), 'id_code': None} == {'name': 'test_exo', 'consigne': 'consigne', 'private': False, 'id_code': None, 'author': {'username': 'lilian2'}, 'original': {"id_code": id_code, "name": create['name']}, 'tags': [], 'exo_source': 'test.py', 'supports': {
assert {**rr.json(), 'id_code': None} == {'name': 'test_exo', 'consigne': 'consigne', 'private': False, 'id_code': None, 'author': {'username': 'lilian2'}, 'original': {"id_code": id_code, "name": create['name'], 'author': 'lilian'}, 'tags': [], 'exo_source': 'test.py', 'supports': {
'pdf': False, 'csv': True, 'web': True}, 'examples': {'type': 'csv', 'data': [{'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}]}, 'is_author': True}
@ -233,22 +233,25 @@ class Tags(BaseModel):
def test_add_tags(client: TestClient, name='name', tags: List[Tags] = [{'label': "name", 'color': "#ff0000",
'id_code': "tag_id"}], user=None):
'id_code': None}], user=None, exo =None):
if user == None:
token = test_register(client, username="lilian")['access']
user = {"token": token, 'username': "lilian"}
else:
token = user['token']
exo = test_create(client, name=name, user=user)
id_code = exo['id_code']
if exo == None:
exo = test_create(client, name=name, user=user)
id_code = exo['id_code']
else:
id_code = exo['id_code']
r = client.post(f'/exercice/{id_code}/tags', json=tags,
headers={'Authorization': 'Bearer ' + token})
print(r.json())
print("DATA", tags, "\n\n",r.json())
data = r.json()
labels = [l['label'] for l in tags]
assert r.status_code == 200
assert {**data, "tags": [{**t, "id_code": None}
for t in data['tags']]} == {**exo, 'tags': [*exo['tags'], *[{**t, 'id_code': None} for t in tags]]}
assert {'exo': {**data['exo'], 'tags': [{**t, "id_code": "test"} for t in data['exo']['tags'] if t['id_code'] != None]}, "tags": [{**t, "id_code": "test"}
for t in data['tags'] if t['id_code'] != None]} == {"exo": {**exo, 'tags': [{**t, "id_code": "test"} for t in tags]}, 'tags': [*exo['tags'], *[{**t, 'id_code': "test"} for t in tags if t['id_code'] == None]]}
return r.json()
@ -281,19 +284,19 @@ def test_add_tags_too_long(client: TestClient):
def test_remove_tag(client: TestClient):
token = test_register(client, username="lilian")['access']
exo = test_add_tags(client, user={"token": token, 'username': "lilian"})
id_code = exo['id_code']
id_code = exo['exo']['id_code']
tag_id = exo["tags"][0]["id_code"]
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
headers={'Authorization': 'Bearer ' + token})
print(r.json())
assert r.json() == {
**exo, 'tags': exo['tags'][1:]}
**exo['exo'], 'tags': exo['tags'][1:]}
def test_remove_tag_not_found(client: TestClient):
token = test_register(client, username="lilian")['access']
exo = test_add_tags(client, user={"token": token, 'username': "lilian"})
id_code = exo['id_code']
id_code = exo['exo']['id_code']
tag_id = "none"
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
headers={'Authorization': 'Bearer ' + token})
@ -316,7 +319,7 @@ def test_remove_tag_not_owner(client: TestClient):
token = test_register(client, username="lilian")['access']
token2 = test_register(client, username="lilian2")['access']
exo = test_add_tags(client, user={"token": token, 'username': "lilian"})
id_code = exo['id_code']
id_code = exo['exo']['id_code']
tag_id = exo['tags'][0]['id_code']
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
headers={'Authorization': 'Bearer ' + token2})
@ -481,29 +484,31 @@ def test_get_user_with_tags(client: TestClient):
token2 = test_register(client, username="lilian2")['access']
tags1 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None}]
tags2 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None},
tags2 = [
{'label': "tag2", 'color': "#ff0000", 'id_code': None}]
tags3 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None},
{'label': "tag2", 'color': "#ff0000", 'id_code': None}, {'label': "tag3", 'color': "#ff0000", 'id_code': None}]
tags3 = [{'label': "tag3", 'color': "#ff0000", 'id_code': None}]
exo_other_user = test_create(
client, user={'token': token2, 'username': "lilian2"})
exo1 = test_add_tags(client, user={
'token': token1, 'username': "lilian"}, tags=tags1)
tags2 = [*exo1['tags'], *tags2]
exo2 = test_add_tags(client, user={
'token': token1, 'username': "lilian"}, tags=tags2)
tags3 = [*exo2['tags'], *tags3]
exo3 = test_add_tags(client, user={
'token': token1, 'username': "lilian"}, tags=tags3)
tags1 = exo1['tags']
tags2 = exo2['tags']
tags3 = exo3['tags']
r = client.get('/exercices/user', params={'tags': [*[t['id_code'] for t in tags2], 'notexist']},
r = client.get('/exercices/user', params={'tags': [*[t['id_code'] for t in tags2], 'notexisting']},
headers={'Authorization': 'Bearer ' + token1})
print(r.json())
assert r.json()['items'] == [exo2, exo3]
print("DATA", r.json())
assert r.json()['items'] == [exo2['exo'], exo3['exo']]
def test_get_user_with_tags_and_search(client: TestClient):
@ -511,20 +516,18 @@ def test_get_user_with_tags_and_search(client: TestClient):
token2 = test_register(client, username="lilian2")['access']
tags1 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None}]
tags2 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None},
{'label': "tag2", 'color': "#ff0000", 'id_code': None}]
tags3 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None},
{'label': "tag2", 'color': "#ff0000", 'id_code': None}, {'label': "tag3", 'color': "#ff0000", 'id_code': None}]
tags2 = [{'label': "tag2", 'color': "#ff0000", 'id_code': None}]
tags3 = [{'label': "tag3", 'color': "#ff0000", 'id_code': None}]
exo_other_user = test_create(
client, user={'token': token2, 'username': "lilian2"})
exo1 = test_add_tags(client, user={
'token': token1, 'username': "lilian"}, tags=tags1, name="yes")
tags2 = [*exo1['tags'], *tags2]
exo2 = test_add_tags(client, user={
'token': token1, 'username': "lilian"}, tags=tags2, name="no")
tags3 = [*exo2['tags'], *tags3]
exo3 = test_add_tags(client, user={
'token': token1, 'username': "lilian"}, tags=tags3, name="yes")
@ -534,7 +537,7 @@ def test_get_user_with_tags_and_search(client: TestClient):
r = client.get('/exercices/user', params={"search": "yes", 'tags': [t['id_code'] for t in tags2]},
headers={'Authorization': 'Bearer ' + token1})
print(r.json())
assert r.json()['items'] == [exo3]
assert r.json()['items'] == [exo3['exo']]
def test_get_public_auth(client: TestClient):
@ -595,8 +598,8 @@ def test_get_exo_no_auth(client: TestClient):
token = test_register(client, username="lilian")['access']
exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
r = client.get('/exercice/' + exo['id_code'])
assert r.json() == {**exo, "tags": [], 'is_author': False}
r = client.get('/exercice/' + exo['exo']['id_code'])
assert r.json() == {**exo['exo'], "tags": [], 'is_author': False}
def test_get_exo_no_auth_private(client: TestClient):
@ -613,20 +616,24 @@ def test_get_exo_auth(client: TestClient):
token2 = test_register(client, username="lilian2")['access']
exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
r = client.get('/exercice/' + exo['id_code'],
r = client.get('/exercice/' + exo['exo']['id_code'],
headers={'Authorization': 'Bearer ' + token2})
print(r.json(), exo)
assert r.json() == {**exo, "tags": [], 'is_author': False}
assert r.json() == {**exo['exo'], "tags": [], 'is_author': False}
def test_get_exo_auth_with_tags(client: TestClient):
token = test_register(client, username="lilian")['access']
exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
token2 = test_register(client, username="lilian2")['access']
r = client.get('/exercice/' + exo['id_code'],
exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
test_add_tags(client, user={'token': token2, 'username': "lilian2"}, exo={**exo['exo'], "is_author": False, "tags": []})
r = client.get('/exercice/' + exo['exo']['id_code'],
headers={'Authorization': 'Bearer ' + token})
print(r.json(), exo)
assert r.json() == {**exo}
assert r.json() == {**exo['exo']}
def test_get_exo_auth_private(client: TestClient):
@ -668,7 +675,7 @@ def test_get_csv(client: TestClient):
assert r.json()['items'] == [{**exoCsv.json(), 'is_author': False}]
def test_get_pdf(client: TestClient):
''' def test_get_pdf(client: TestClient):
token = test_register(client)['access']
exoCsv = client.post('/exercices', data={"name": "name", "consigne": "consigne", "private": False}, files={
'file': ('test.py', open('tests/testing_exo_source/exo_source_csv_only.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
@ -680,7 +687,7 @@ def test_get_pdf(client: TestClient):
r = client.get('/exercices/public', params={"type": "pdf"})
assert r.json()['items'] == [{**exoPdf.json(), 'is_author': False}]
'''
def test_get_web(client: TestClient):
token = test_register(client)['access']
@ -693,7 +700,7 @@ def test_get_web(client: TestClient):
r = client.get('/exercices/public', params={"type": "web"})
assert r.json() == [{**exoWeb.json(), 'is_author': False}]
assert r.json()['items'] == [{**exoWeb.json(), 'is_author': False}]
def test_get_invalid_type(client: TestClient):

File diff suppressed because it is too large Load Diff

View File

@ -8,4 +8,4 @@ Fonction main() qui doit renvoyer un objet avec:
def main():
t = random.randint(1, 10)
return {"csv": None, 'web': "None","pdf": None, "calcul": "1+1=2"}
return {"csv": None, 'web': "1 + [] = 2","pdf": None, "calcul": "1+1=2"}

8
frontend/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

420
frontend/.idea/dbnavigator.xml generated Normal file
View File

@ -0,0 +1,420 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DBNavigator.Project.DataEditorManager">
<record-view-column-sorting-type value="BY_INDEX" />
<value-preview-text-wrapping value="true" />
<value-preview-pinned value="false" />
</component>
<component name="DBNavigator.Project.DatabaseEditorStateManager">
<last-used-providers />
</component>
<component name="DBNavigator.Project.DatabaseFileManager">
<open-files />
</component>
<component name="DBNavigator.Project.Settings">
<connections />
<browser-settings>
<general>
<display-mode value="TABBED" />
<navigation-history-size value="100" />
<show-object-details value="false" />
</general>
<filters>
<object-type-filter>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="true" />
<object-type name="ROLE" enabled="true" />
<object-type name="PRIVILEGE" enabled="true" />
<object-type name="CHARSET" enabled="true" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED_VIEW" enabled="true" />
<object-type name="NESTED_TABLE" enabled="true" />
<object-type name="COLUMN" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET_TRIGGER" enabled="true" />
<object-type name="DATABASE_TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="true" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
<object-type name="ARGUMENT" enabled="true" />
<object-type name="DIMENSION" enabled="true" />
<object-type name="CLUSTER" enabled="true" />
<object-type name="DBLINK" enabled="true" />
</object-type-filter>
</filters>
<sorting>
<object-type name="COLUMN" sorting-type="NAME" />
<object-type name="FUNCTION" sorting-type="NAME" />
<object-type name="PROCEDURE" sorting-type="NAME" />
<object-type name="ARGUMENT" sorting-type="POSITION" />
<object-type name="TYPE ATTRIBUTE" sorting-type="POSITION" />
</sorting>
<default-editors>
<object-type name="VIEW" editor-type="SELECTION" />
<object-type name="PACKAGE" editor-type="SELECTION" />
<object-type name="TYPE" editor-type="SELECTION" />
</default-editors>
</browser-settings>
<navigation-settings>
<lookup-filters>
<lookup-objects>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="false" />
<object-type name="ROLE" enabled="false" />
<object-type name="PRIVILEGE" enabled="false" />
<object-type name="CHARSET" enabled="false" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED VIEW" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET TRIGGER" enabled="true" />
<object-type name="DATABASE TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="false" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="DIMENSION" enabled="false" />
<object-type name="CLUSTER" enabled="false" />
<object-type name="DBLINK" enabled="true" />
</lookup-objects>
<force-database-load value="false" />
<prompt-connection-selection value="true" />
<prompt-schema-selection value="true" />
</lookup-filters>
</navigation-settings>
<dataset-grid-settings>
<general>
<enable-zooming value="true" />
<enable-column-tooltip value="true" />
</general>
<sorting>
<nulls-first value="true" />
<max-sorting-columns value="4" />
</sorting>
<audit-columns>
<column-names value="" />
<visible value="true" />
<editable value="false" />
</audit-columns>
</dataset-grid-settings>
<dataset-editor-settings>
<text-editor-popup>
<active value="false" />
<active-if-empty value="false" />
<data-length-threshold value="100" />
<popup-delay value="1000" />
</text-editor-popup>
<values-actions-popup>
<show-popup-button value="true" />
<element-count-threshold value="1000" />
<data-length-threshold value="250" />
</values-actions-popup>
<general>
<fetch-block-size value="100" />
<fetch-timeout value="30" />
<trim-whitespaces value="true" />
<convert-empty-strings-to-null value="true" />
<select-content-on-cell-edit value="true" />
<large-value-preview-active value="true" />
</general>
<filters>
<prompt-filter-dialog value="true" />
<default-filter-type value="BASIC" />
</filters>
<qualified-text-editor text-length-threshold="300">
<content-types>
<content-type name="Text" enabled="true" />
<content-type name="Properties" enabled="true" />
<content-type name="XML" enabled="true" />
<content-type name="DTD" enabled="true" />
<content-type name="HTML" enabled="true" />
<content-type name="XHTML" enabled="true" />
<content-type name="CSS" enabled="true" />
<content-type name="Java" enabled="true" />
<content-type name="SQL" enabled="true" />
<content-type name="PL/SQL" enabled="true" />
<content-type name="JavaScript" enabled="true" />
<content-type name="JSON" enabled="true" />
<content-type name="JSON5" enabled="true" />
<content-type name="JSP" enabled="true" />
<content-type name="JSPx" enabled="true" />
<content-type name="Groovy" enabled="true" />
<content-type name="FTL" enabled="true" />
<content-type name="VTL" enabled="true" />
<content-type name="AIDL" enabled="true" />
<content-type name="YAML" enabled="true" />
<content-type name="Manifest" enabled="true" />
</content-types>
</qualified-text-editor>
<record-navigation>
<navigation-target value="VIEWER" />
</record-navigation>
</dataset-editor-settings>
<code-editor-settings>
<general>
<show-object-navigation-gutter value="false" />
<show-spec-declaration-navigation-gutter value="true" />
<enable-spellchecking value="true" />
<enable-reference-spellchecking value="false" />
</general>
<confirmations>
<save-changes value="false" />
<revert-changes value="true" />
</confirmations>
</code-editor-settings>
<code-completion-settings>
<filters>
<basic-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="false" />
<filter-element type="OBJECT" id="view" selected="false" />
<filter-element type="OBJECT" id="materialized view" selected="false" />
<filter-element type="OBJECT" id="index" selected="false" />
<filter-element type="OBJECT" id="constraint" selected="false" />
<filter-element type="OBJECT" id="trigger" selected="false" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="false" />
<filter-element type="OBJECT" id="procedure" selected="false" />
<filter-element type="OBJECT" id="function" selected="false" />
<filter-element type="OBJECT" id="package" selected="false" />
<filter-element type="OBJECT" id="type" selected="false" />
<filter-element type="OBJECT" id="dimension" selected="false" />
<filter-element type="OBJECT" id="cluster" selected="false" />
<filter-element type="OBJECT" id="dblink" selected="false" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</basic-filter>
<extended-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</extended-filter>
</filters>
<sorting enabled="true">
<sorting-element type="RESERVED_WORD" id="keyword" />
<sorting-element type="RESERVED_WORD" id="datatype" />
<sorting-element type="OBJECT" id="column" />
<sorting-element type="OBJECT" id="table" />
<sorting-element type="OBJECT" id="view" />
<sorting-element type="OBJECT" id="materialized view" />
<sorting-element type="OBJECT" id="index" />
<sorting-element type="OBJECT" id="constraint" />
<sorting-element type="OBJECT" id="trigger" />
<sorting-element type="OBJECT" id="synonym" />
<sorting-element type="OBJECT" id="sequence" />
<sorting-element type="OBJECT" id="procedure" />
<sorting-element type="OBJECT" id="function" />
<sorting-element type="OBJECT" id="package" />
<sorting-element type="OBJECT" id="type" />
<sorting-element type="OBJECT" id="dimension" />
<sorting-element type="OBJECT" id="cluster" />
<sorting-element type="OBJECT" id="dblink" />
<sorting-element type="OBJECT" id="schema" />
<sorting-element type="OBJECT" id="role" />
<sorting-element type="OBJECT" id="user" />
<sorting-element type="RESERVED_WORD" id="function" />
<sorting-element type="RESERVED_WORD" id="parameter" />
</sorting>
<format>
<enforce-code-style-case value="true" />
</format>
</code-completion-settings>
<execution-engine-settings>
<statement-execution>
<fetch-block-size value="100" />
<execution-timeout value="20" />
<debug-execution-timeout value="600" />
<focus-result value="false" />
<prompt-execution value="false" />
</statement-execution>
<script-execution>
<command-line-interfaces />
<execution-timeout value="300" />
</script-execution>
<method-execution>
<execution-timeout value="30" />
<debug-execution-timeout value="600" />
<parameter-history-size value="10" />
</method-execution>
</execution-engine-settings>
<operation-settings>
<transactions>
<uncommitted-changes>
<on-project-close value="ASK" />
<on-disconnect value="ASK" />
<on-autocommit-toggle value="ASK" />
</uncommitted-changes>
<multiple-uncommitted-changes>
<on-commit value="ASK" />
<on-rollback value="ASK" />
</multiple-uncommitted-changes>
</transactions>
<session-browser>
<disconnect-session value="ASK" />
<kill-session value="ASK" />
<reload-on-filter-change value="false" />
</session-browser>
<compiler>
<compile-type value="KEEP" />
<compile-dependencies value="ASK" />
<always-show-controls value="false" />
</compiler>
<debugger>
<debugger-type value="ASK" />
<use-generic-runners value="true" />
</debugger>
</operation-settings>
<ddl-file-settings>
<extensions>
<mapping file-type-id="VIEW" extensions="vw" />
<mapping file-type-id="TRIGGER" extensions="trg" />
<mapping file-type-id="PROCEDURE" extensions="prc" />
<mapping file-type-id="FUNCTION" extensions="fnc" />
<mapping file-type-id="PACKAGE" extensions="pkg" />
<mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
<mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
<mapping file-type-id="TYPE" extensions="tpe" />
<mapping file-type-id="TYPE_SPEC" extensions="tps" />
<mapping file-type-id="TYPE_BODY" extensions="tpb" />
</extensions>
<general>
<lookup-ddl-files value="true" />
<create-ddl-files value="false" />
<synchronize-ddl-files value="true" />
<use-qualified-names value="false" />
<make-scripts-rerunnable value="true" />
</general>
</ddl-file-settings>
<general-settings>
<regional-settings>
<date-format value="MEDIUM" />
<number-format value="UNGROUPED" />
<locale value="SYSTEM_DEFAULT" />
<use-custom-formats value="false" />
</regional-settings>
<environment>
<environment-types>
<environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
<environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
<environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
<environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
</environment-types>
<visibility-settings>
<connection-tabs value="true" />
<dialog-headers value="true" />
<object-editor-tabs value="true" />
<script-editor-tabs value="false" />
<execution-result-tabs value="true" />
</visibility-settings>
</environment>
</general-settings>
</component>
</project>

9
frontend/.idea/frontend.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
frontend/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
frontend/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/frontend.iml" filepath="$PROJECT_DIR$/.idea/frontend.iml" />
</modules>
</component>
</project>

6
frontend/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,9 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});

View File

@ -0,0 +1,7 @@
// type definitions for Cypress object "cy"
/// <reference types="cypress" />
describe('test exo', ()=>{
it('test exo', ()=>{
cy.visit('localhost:5173')
})
})

View File

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -0,0 +1,37 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }

View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -3,6 +3,7 @@
"version": "0.0.1",
"private": true,
"scripts": {
"test": "npx vitest run --reporter verbose",
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
@ -14,12 +15,17 @@
"devDependencies": {
"@sveltejs/adapter-auto": "^1.0.0",
"@sveltejs/kit": "^1.0.0",
"@testing-library/svelte": "^3.2.2",
"@types/chroma-js": "^2.1.4",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"@vitest/browser": "^0.28.4",
"@vitest/ui": "^0.28.4",
"cypress": "^12.6.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
"jsdom": "^21.1.0",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
"sass": "^1.53.0",
@ -28,21 +34,27 @@
"svelte-preprocess": "^4.10.7",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"vite": "^4.0.0"
"vite": "^4.0.0",
"vitest": "^0.28.4"
},
"type": "module",
"dependencies": {
"@sveltestack/svelte-query": "^1.6.0",
"@testing-library/jest-dom": "^5.16.5",
"@types/qs": "^6.9.7",
"axios": "^1.2.2",
"chroma-js": "^2.4.2",
"jwt-decode": "^3.1.2",
"qs": "^6.11.0",
"reconnecting-websocket": "^4.4.0",
"svelecte": "^3.13.0",
"svelte-forms": "^2.3.1",
"svelte-htm": "^1.2.0",
"svelte-icons": "^2.1.0",
"svelte-markdown": "^0.2.3",
"svelte-multiselect": "^8.2.3",
"svelte-navigator": "^3.2.2",
"svelte-routing": "^1.6.0"
"svelte-routing": "^1.6.0",
"svelte-websocket-store": "^1.1.33"
}
}

2534
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
import axios from 'axios';
import { autoRefresh } from '../utils/utils';
export const authInstance = axios.create({
baseURL: `http://127.0.0.1:8002`,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'Access-Control-Allow-Origin': '*',
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
}
});
authInstance.interceptors.request.use(autoRefresh, (error) => {
Promise.reject(error);
});

View File

@ -1,6 +1,7 @@
import { browser } from '$app/environment';
import axios from 'axios';
import { parse, stringify } from 'qs'
import { autoRefresh } from '../utils/utils';
export const exoInstance = axios.create({
paramsSerializer:{encode:(params)=> {return parse(params, {arrayFormat:"brackets"})}, serialize: (p)=>{return stringify(p, {arrayFormat: "repeat"})}},
baseURL: `http://127.0.0.1:8002`,
@ -9,7 +10,9 @@ export const exoInstance = axios.create({
Accept: 'application/json',
'Access-Control-Allow-Origin': '*',
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
...(browser &&
localStorage.getItem('token') && { Authorization: `Bearer ${localStorage.getItem('token')}` })
}
});
exoInstance.interceptors.request.use(autoRefresh, (error) => {
Promise.reject(error);
});

View File

@ -0,0 +1,19 @@
import axios from 'axios';
import { autoRefresh } from '../utils/utils';
export const roomInstance = axios.create({
baseURL: `http://127.0.0.1:8002/room`,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'Access-Control-Allow-Origin': '*',
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
}
});
roomInstance.interceptors.request.use(
autoRefresh,
(error) => {
Promise.reject(error);
}
);

View File

@ -4,6 +4,44 @@
box-sizing: border-box;
margin: 0;
}
.spinner {
width: 30px;
height:30px;
border: 3px solid $contrast;
border-bottom-color: transparent;
border-radius: 50%;
animation: rotation 1s infinite linear;
display: inline-block;
box-sizing: border-box;
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.container {
height: calc(100vh - 100px); // 100% - nav
}
*{
scrollbar-width: auto!important;
scrollbar-color: $contrast transparent;
}
.btn {
border: none;
@ -13,9 +51,12 @@
transition: 0.3s;
margin-bottom: 10px;
margin-right: 7px;
padding: 0 10%;
padding: 0 50px;
width: max-content;
cursor: pointer;
&:disabled{
cursor: not-allowed
}
}
.primary-btn {
@ -61,7 +102,7 @@
margin: 0;
&:focus {
outline: none;
border-bottom-color: red;
border-bottom-color: $contrast;
}
}
@ -80,3 +121,8 @@
.sv-dropdown{
z-index: 10!important;
}
.strong{
font-weight: 900;
}

View File

@ -0,0 +1,207 @@
<script lang="ts">
import NavLink from "./NavLink.svelte";
import {getContext} from "svelte";
import type {Writable} from "svelte/store";
import FaHome from 'svelte-icons/fa/FaHome.svelte'
import {afterNavigate} from "$app/navigation";
import FaUser from 'svelte-icons/fa/FaUser.svelte'
import FaSignOutAlt from 'svelte-icons/fa/FaSignOutAlt.svelte'
const {isAuth, username, logout} = getContext<{ isAuth: Writable<boolean>, username: Writable<string | null> }>('auth');
let open = false
afterNavigate(() => {
open = false
})
</script>
<nav data-sveltekit-preload-data="hover" class:open>
<div class="navigate">
<NavLink href="/" exact no_hover>
<div class="icon">
<FaHome/>
</div>
</NavLink>
<NavLink href="/exercices">Exercices</NavLink>
<NavLink href="/room">Salles</NavLink>
</div>
<div class="auth">
{#if $isAuth && $username != null}
<NavLink href="/dashboard">
<div class="dashboard">
<div class="icon">
<FaUser/>
</div>
{$username}</div>
</NavLink>
<div class="icon signout" title="Se déconnecter" on:click={()=>{
logout()
}}><FaSignOutAlt /></div>
{:else}
<NavLink href="/signup" exact>S'inscrire</NavLink>
<NavLink href="/signin" exact>Se connecter</NavLink>
{/if}
<div class="burger" on:click={()=>{open=!open}}><span> </span></div>
</div>
</nav>
<style lang="scss">
@import "../mixins";
nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding: 30px 15px;
border-bottom: 1px solid $border;
width: 100%;
gap: 10px;
height: 30px;
//transition: .3s;
> div {
display: flex;
gap: 20px;
align-items: stretch;
height: 30px;
}
.icon {
width: 23px;
height: 23px;
display: flex;
align-items: center;
justify-content: center;
}
}
.dashboard {
display: flex;
align-items: center;
gap: 10px;
.icon {
width: 15px;
height: 15px;
}
}
.signout {
cursor: pointer;
transition: .3s;
color: $primary-dark;
&:hover {
color: $primary;
}
}
.burger {
background: 0 0;
@include up(750px) {
display: none;
}
border: none;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
color: $primary-dark;
&:hover {
color: $primary;
}
& span {
font-size: 0;
transition: 0.2s ease-in-out;
width: 12px;
height: 2px;
background-color: currentColor;
display: block;
position: relative;
transition: 0.2s ease-in-out;
&::before,
&::after {
transition: 0.2s ease-in-out;
content: "";
display: block;
width: 12px;
height: 2px;
background-color: currentColor;
position: relative;
}
&::after {
top: -6px;
}
&::before {
bottom: -4px;
}
}
}
.navigate{
height: 30px;
overflow: hidden;
flex-wrap: wrap;
}
.open {
@include down(750px) {
.navigate {
*:first-child {
display: none
}
// Remove home icon
transition: .2s;
background: rgba($background-dark, 0.8);
height: 100%;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 42px;
z-index: 100;
}
.auth {
justify-content: end;
width: 100%;
z-index: 101;
}
& .burger {
& span::before {
bottom: 0;
transform: rotate(-90deg);
}
& span::after {
transform: rotate(0);
top: -2px;
}
& span {
transform: rotate(135deg);
}
}
}
}
</style>

View File

@ -2,15 +2,16 @@
import { page } from '$app/stores';
export let href = "/";
export let exact = false;
export let no_hover = false;
</script>
<a href={href} class:selected={exact ? $page.url.pathname === href: $page.url.pathname.includes(href)}><slot/></a>
<a href={href} class:no_hover class:selected={exact ? $page.url.pathname === href: $page.url.pathname.includes(href)}><slot/></a>
<style lang="scss">
a {
cursor: pointer;
margin: 0 10px;
color: yellow;
color: $primary-dark;
text-decoration: none;
position: relative;
font-weight: 600;
@ -22,7 +23,7 @@
width: 100%;
height: 2px;
background: currentColor;
top: 100%;
bottom: 5px;
left: 0;
pointer-events: none;
transform-origin: 100% 50%;
@ -30,9 +31,9 @@
transition: transform 0.3s;
}
&:hover {
color: red;
color: $primary;
transform: scale(1.05);
&::before {
&:not(.no_hover)::before {
transform-origin: 0% 50%;
transform: scale3d(1, 1, 1);
}
@ -40,7 +41,7 @@
}
.selected {
font-weight: bolder;
color: red;
color: $primary;
transform: scale(1.05);
&::before {
content: none;

View File

@ -0,0 +1,46 @@
<script lang="ts">
import {field, form} from "svelte-forms";
import {max, min, required, email} from "svelte-forms/validators";
import LabeledInput from "../forms/LabeledInput.svelte";
import type {User} from "../../types/auth.type";
import {onMount} from "svelte";
import {errorMsg} from "../../utils/forms.js";
export let user: User
export let myForm;
const username = field('username', user.username, [required(), max(20), min(2)], {
checkOnInit: true
});
const name = field('name', user.name || "", [max(50)], {
checkOnInit: true
});
const firstname = field('firstname', user.firstname || "", [max(50),], {
checkOnInit: true
});
const emailField = field('email', user.email || "", [ /*email()*/], {
checkOnInit: true
});
onMount(() => {
myForm = form(username, name, firstname, emailField);
})
</script>
{#if !!$myForm}
<div class="">
<LabeledInput bind:value={$username.value} label="Nom d'utilisateur" type="text" placeholder="Nom d'utilisateur..."
errors={errorMsg($myForm, 'username')}/>
<LabeledInput bind:value={$emailField.value} label="Email" type="email" placeholder="Email..."
errors={errorMsg($myForm, 'email')}/>
<LabeledInput bind:value={$name.value} label="Nom" type="text" placeholder="Nom..."
errors={errorMsg($myForm, 'name')}/>
<LabeledInput bind:value={$firstname.value} label="Prénom" type="text" placeholder="Prénom..."
errors={errorMsg($myForm, 'firstname')}/>
</div>
{/if}
<style lang="scss">
div {
display: grid;
grid-template-columns: 1fr 1fr;
}
</style>

View File

@ -0,0 +1,31 @@
<script lang="ts">
import {field, form} from "svelte-forms";
import {matchField, min, pattern, required} from "svelte-forms/validators";
import LabeledInput from "../forms/LabeledInput.svelte";
import {onMount} from "svelte";
import {errorMsg} from "../../utils/forms";
const password = field('password', '', [required(), min(8), pattern(/[0-9]/), pattern(/[A-Z]/)], {checkOnInit: true});
const confirm = field('password_confirm', '', [required(), matchField(password)],{checkOnInit: true});
export let myForm;
onMount(() => {
myForm = form(password, confirm)
})
</script>
{#if !!$myForm}
<div>
<LabeledInput bind:value={$password.value} type="password" placeholder="Mot de passe..."
errors={errorMsg($myForm, 'password')}/>
<LabeledInput bind:value={$confirm.value} type="password" placeholder="Confirmer..."
errors={errorMsg($myForm, 'password_confirm')}/>
</div>
{/if}
<style lang="scss">
div {
display: grid;
grid-template-columns: 1fr 1fr;
}
</style>

View File

@ -0,0 +1,40 @@
<script lang="ts">
import type {UsersRoom} from "../../types/auth.type";
export let rooms: UsersRoom[]
</script>
<div>
<ul>
{#each rooms as room}
<li>
<a href="/room/{room.id_code}">{room.name} ({room.admin ? "Administrateur" : "Member"})</a>
</li>
{/each}
</ul>
</div>
<style lang="scss">
ul {
list-style: none;
padding: 0;
display: flex;
gap: 10px;
padding: 10px;
align-items: center;
}
a {
text-decoration: none;
display: flex;
gap: 10px;
align-items: center;
color: #f8f8f8;
transition: 0.2s;
&:hover {
color: $primary;
}
}
</style>

View File

@ -0,0 +1,53 @@
<script lang="ts">
export let icon = null
export let title;
export let validate = "Valider !"
export let onValidate = null
export let canValid = false
</script>
<div>
<h2>
<div class="icon">
<svelte:component this={icon}/></div>
{title}</h2>
<div class="content">
<slot/>
</div>
{#if !!onValidate}
<div class="btn-container">
<button on:click={onValidate} class="primary-btn" disabled={!canValid}>{validate}</button>
</div>
{/if}
</div>
<style lang="scss">
h2 {
display: flex;
align-items: center;
}
.icon{
margin-right: 10px;
width: 25px;
height: 25px;
display: flex;
align-items: center;
}
.content {
background: rgba($background, 0.7);
padding: 12px 8px;
margin: 10px;
border: 1px solid $border;
border-radius: 5px;
margin-left: 0;
}
.btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
</style>

View File

@ -0,0 +1,35 @@
<script lang="ts">
import InputWithLabel from "../forms/InputWithLabel.svelte";
export let onValidate = (p) => {
}
let password = ""
export let validate = "Valider"
export let cancel = ()=>{}
export let cancelMsg = "Annuler"
</script>
<div class="confirm">
<h1>Veuillez confirmer votre identité</h1>
<InputWithLabel bind:value={password} type="password" label="Mot de passe"/>
<div>
<button class="primary-btn" on:click={()=>{
onValidate(password)
}}>{validate}
</button>
<button class="danger-btn" on:click={cancel}>{cancelMsg}</button>
</div>
</div>
<style lang="scss">
.confirm {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 25px;
height: 100%;
background: $background;
padding: 30px 18px;
border-radius: 5px;
}
</style>

View File

@ -7,11 +7,13 @@
import TagContainer from './TagContainer.svelte';
import PrivacyIndicator from './PrivacyIndicator.svelte';
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte';
import type { Writable } from 'svelte/store';
export let exo: Exercice;
const { show } = getContext<{ show: Function }>('modal');
const { navigate } = getContext<{ navigate: Function }>('navigation');
const { isAuth } = getContext<{ isAuth: boolean }>('auth');
const { isAuth } = getContext<{ isAuth: Writable<boolean> }>('auth');
const exerciceStore = getContext('exos');
const tagsStore = getContext('tags');
@ -19,10 +21,6 @@
const handleClick = () => {
opened = true;
navigate(`/exercices/${exo.id_code}`);
};
let tg = false;
$: !!opened &&
show(
ModalCard,
{
@ -35,36 +33,36 @@
opened = false;
}
);
};
</script>
<div
class="card"
class:tagMode={tg}
on:click={handleClick}
on:dblclick={() => {}}
on:keypress={() => {}}
>
<div class="card" on:click={handleClick} on:dblclick={() => {}} on:keypress={() => {}}>
<h1>{exo.name}</h1>
<div class="examples">
<h2>Exemples</h2>
{#if !!exo.consigne}<p>{exo.consigne}</p>{/if}
{#each exo.examples.data.slice(0, 3) as ex}
<p>{ex.calcul}</p>
{/each}
{#if exo.examples != null}
<h2>Exemples</h2>
{#if !!exo.consigne}<p data-testid="consigne">{exo.consigne}</p>{/if}
{#each exo.examples.data.slice(0, 3) as ex}
<p>{ex.calcul}</p>
{/each}
{:else}
<p>Aucun exemple disponible</p>
{/if}
</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>
{#if !!$isAuth && 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>
{#if !!$isAuth}
<div
data-testid="copy"
class="icon"
on:keydown={() => {}}
on:click|stopPropagation={() => {
@ -78,15 +76,15 @@
>
<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}
{/if}
</div>
{:else if exo.is_author && exo.original != null}
<div class="status">
<PrivacyIndicator color="blue">Par <strong>{exo.original?.author}</strong></PrivacyIndicator>
</div>
{/if}
<div class="card-hover" />
{#if !!isAuth}
{#if !!$isAuth}
<TagContainer bind:exo />
{/if}
<!-- TagContainer Must be directly after card-hover for the hover effect -->
@ -188,7 +186,7 @@
background-color: $background;
min-height: 250px;
max-height: 300px;
&:not(.tagMode):hover {
&:hover {
transform: translateX(10px) translateY(-10px);
}
}

View File

@ -17,9 +17,10 @@
<EditForm editing={false} {cancel} {updateExo} />
</div>
<style>
<style lang="scss">
@import '../../variables';
div {
background-color: blue;
background: $background;
padding: 50px;
display: flex;
flex-direction: column;

View File

@ -12,13 +12,16 @@
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte';
import ModalCard from './ModalCard.svelte';
import { goto } from '$app/navigation';
import type { Writable } from 'svelte/store';
import {generateRequest} from "../../requests/exo.request.js";
export let exo: Exercice;
export let edit: Function;
export let delete_: Function;
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');
const { isAuth } = getContext<{ isAuth: Writable<boolean> }>('auth');
const { navigate } = getContext<{ navigate: Function }>('navigation');
let name = '';
</script>
@ -61,7 +64,7 @@
</span>
{/if}
</h1>
<InputWithLabel type="text" value={name} label="Nom" />
<InputWithLabel type="text" bind:value={name} label="Nom" disabled={!exo.supports.csv}/>
<div class="examples">
<h2>Exemples</h2>
@ -72,11 +75,13 @@
{/each}
</div>
<div class="flex-row-center wp-100">
<button class="primary-btn">Télécharger</button>
</div>
<div class="tags" />
<button class="primary-btn" disabled= {!exo.supports.csv} on:click={()=>{
generateRequest(exo.id_code, name)
{#if !!isAuth}
}}>Télécharger</button>
</div>
{#if !!$isAuth}
<TagContainer {exo} />
{/if}
<div class="icons">
@ -87,7 +92,7 @@
</div>
<div>
{#if !!isAuth}
{#if !!$isAuth}
{#if exo.is_author}
<div
class="icon"
@ -97,16 +102,19 @@
title: 'Sur ?',
description: 'Voulez vous supprimer ? ',
validate: () => {
close();
delExo(exo.id_code).then((r)=>{
close();
delete_();
})
}
});
}}
on:keypress={() => {}}
data-testid="delete"
>
<MdDelete />
</div>
<div class="icon" style:color="green" on:click={() => edit()} on:keypress={() => {}}>
<div class="icon" style:color="green" on:click={() => edit()} on:keypress={() => {}} data-testid="edit">
<MdEdit />
</div>
{:else}
@ -120,7 +128,8 @@
ModalCard,
{ exo: r },
() => {
goto('/exercices/user');
//goto('/exercices/user');
navigate(-2)
},
true
);
@ -128,6 +137,7 @@
}}
on:keypress={() => {}}
title="Copier l'exercice pour pouvoir le modifier"
data-testid="copy"
>
<MdContentCopy />
</div>

View File

@ -8,7 +8,7 @@
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) => {};
@ -18,10 +18,10 @@
const { alert } = getContext<{ alert: Function }>('alert');
// "Legally" initiate empty FileList for model field (simple list raises warning)
let list = new DataTransfer();
/* 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);
!editing && list.items.remove(0); */
// Initiate fields and form
const name = field('name', !!exo ? exo.name : '', [required(), max(50), min(5)], {
@ -29,7 +29,7 @@
});
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()], {
const model = field('model', [], [checkFile(), required()], {
checkOnInit: !editing
});
const myForm = form(name, consigne, prv, model);
@ -70,6 +70,7 @@
required
label="Nom"
errors={errorMsg($myForm, 'name')}
name="name"
/>
<InputWithLabel
@ -78,25 +79,26 @@
maxlength="200"
label="Consigne"
errors={errorMsg($myForm, 'consigne')}
name="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} />
<FileInput bind:value={$model.value} accept=".py" id_code={exo?.id_code} defaultValue={editing &&exo!= null? exo.exo_source: null}/>
<div class="wp-100">
<button class="primary-btn" disabled={!$myForm.valid}>Valider</button>
<button class="primary-btn" disabled={!$myForm.valid}>Modifier</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',
title: 'Voulez-vous annuler ?',
description:
'Aliquip in cupidatat anim tempor quis est sint qui sunt. Magna consequat excepteur deserunt ullamco quis.',
'Vous avez des modifications non enregistrées, êtes vous sur de vouloir annuler ?',
validate: cancel
});
} else {

View File

@ -0,0 +1,132 @@
<script lang="ts">
import { getExo, getExos } from '../../requests/exo.request';
import { getContext, onMount } from 'svelte';
import ExoList from './ExoList.svelte';
import type {Writable} from 'svelte/store';
import FaTimes from 'svelte-icons/fa/FaTimes.svelte';
import type { ExoSelect } from '../../types/room.type';
const { show } = getContext<{ show: Function }>('modal');
export let exos: Writable<ExoSelect[]>;
let open = false;
</script>
<div class="selector">
<div class="head">
<h2>Ajouter exo</h2>
<button
on:click={() => {
open = true;
show(ExoList, { selectedExos: exos, fetchNextPage: () => {} }, () => {
open = false;
});
}}>+</button
>
</div>
<div class="exos">
{#if $exos.length == 0}
<p class="empty">Aucun exercice sélectionné</p>
{/if}
{#each $exos as e}
<div>
<div class="name">
<div
class="icon"
on:click={() => {
$exos = $exos.filter((ex) => ex != e);
}}
on:keydown={()=>{}}
>
<FaTimes />
</div>
<p>{e.name}</p>
</div>
<div class="options">
<label for="quantity">Nombre</label>
<input bind:value={e.quantity} name="quantity" id="quantity" type="number"/>
</div>
</div>
{/each}
</div>
</div>
<style lang="scss">
.selector {
background-color: rgba($background, 0.4);
width: 100%;
height: 100%;
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 15px;
padding: 15px 0;
border-bottom: 1px $border solid;
button {
border: none;
outline: none;
background-color: transparent;
color: white;
font-weight: 900;
font-size: 1.4em;
cursor: pointer;
transition: 0.3s;
&:hover {
transform: rotate(180deg);
}
}
h2 {
font-size: 1.05em;
}
}
.empty{
text-align: center;
font-style: italic;
}
.exos {
padding: 10px;
> div {
display: flex;
align-items: center;
justify-content: space-between;
height: 100px;
border-bottom: 1px solid $border ;
&:hover .icon {
opacity: 1;
}
.name {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
}
}
.options{
display: flex;
gap: 10px;
align-items: center;
input{
width: 50px;
max-width: 100px;
}
}
.icon {
width: 20px;
height: 20px;
opacity: 0;
transition: 0.3s;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,165 @@
<script lang="ts">
import { getExos, getTags } from '../../requests/exo.request';
import { writable, type Writable } from 'svelte/store';
import type { Exercice, Tag } from '../../types/exo.type';
import TagViewer from './TagViewer.svelte';
import TagSelector from '../forms/TagSelector.svelte';
import { getContext } from 'svelte';
import type { ExoSelect } from 'src/types/room.type';
export let selectedExos: Writable<ExoSelect[]>;
const exos = writable<{ hasMore: Boolean; data: Exercice[]; page: number }>({
hasMore: true,
data: [],
page: 1
});
const tags = writable<Tag[]>([]);
let filter: "user" | 'public' = 'public';
let search = '';
let selected: Tag[] = [];
const { isAuth } = getContext<{isAuth: Writable<boolean>}>('auth');
$: {
getExos(filter, { search, size: 20, tags: [...selected.map((t) => t.id_code)] }).then((r) => {
exos.set({
hasMore: r.totalPage > 1,
data: r.items,
page: 1
});
});
}
$: getTags().then((r) => {
tags.set(r);
});
const fetchNextPage = () => {
if ($exos.hasMore) {
getExos(filter, {
search,
size: 20,
tags: [...selected.map((t) => t.id_code)],
page: $exos.page + 1
}).then((r) => {
exos.update((o) => {
return {
hasMore: r.totalPage > r.page,
data: [...o.data, ...r.items],
page: r.page
};
});
});
}
};
let more: HTMLDivElement;
let updateText: number | null = null;
</script>
<div
class="exolist"
bind:this={more}
on:scroll={() => {
if (
more != undefined &&
$exos.hasMore &&
more.offsetHeight + more.scrollTop >= more.scrollHeight
) {
fetchNextPage();
}
}}
>
<div class="head">
<input
type="text"
name="search"
placeholder="Rechercher..."
class="input"
on:input={(e) => {
if (updateText != null) {
clearTimeout(updateText);
}
updateText = window.setTimeout(() => {
search = e.currentTarget.value;
}, 500);
}}
/>
{#if !!$isAuth}
<div class="auth-head">
<TagSelector options={$tags} bind:selected placeholder="Selectionner..."/>
<select class="input" bind:value={filter}>
<option value="user">Vos exercices</option>
<option value="public">Tous les exercices</option>
</select>
</div>{/if}
</div>
<div class="exos">
{#each $exos.data as e (e.id_code)}
<div
on:click={() => {
if ($selectedExos.map((s) => s.exercice_id).includes(e.id_code)) {
selectedExos.update((n) => {
return [...n.filter((s) => s.exercice_id != e.id_code)];
});
} else {
selectedExos.update((n) => {
return [...n, { quantity: 10, exercice_id: e.id_code, name: e.name }];
});
}
}}
class:selected={$selectedExos.map((s) => s.exercice_id).includes(e.id_code)}
on:keydown={() => {}}
>
<p>{e.name}</p>
<TagViewer tags={e.tags} id={e.id_code} />
</div>
{/each}
{#if $exos.hasMore}
<p class="more"><span class="spinner"/></p>
{/if}
</div>
</div>
<style lang="scss">
.auth-head{
display: flex;
select{
width: max-content;
padding: 10px 5px;
gap: 5px;
}
}
.more{
display: flex;
justify-content: center;
}
.exolist {
height: 50vh;
overflow: scroll;
background-color: $background;
padding: 20px;
}
.selected {
color: grey;
}
.head {
display: flex;
flex-direction: column;
gap: 10px;
}
.exos {
> div {
padding: 16px 10px;
cursor: pointer;
transition: 0.3s;
display: flex;
align-items: center;
gap: 20px;
&:hover {
background-color: darken($color: $background, $amount: 3);
}
}
}
</style>

View File

@ -3,81 +3,118 @@
import Card from './Card.svelte';
import Head from './Head.svelte';
import ModalCard from './ModalCard.svelte';
import { Query, useQueryClient, type QueryOptions } from '@sveltestack/svelte-query';
import { getExo, getExos, getTags } from '../../requests/exo.request';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import Pagination from './Pagination.svelte';
import { goto } from '$app/navigation';
import { writable } from 'svelte/store';
import {type Writable, writable} from 'svelte/store';
import { setContext } from 'svelte';
import type { Page, Tag } from '../../types/exo.type';
import type { Store } from '../../types/api.type';
import { page as p } from '$app/stores';
const { show } = getContext<{ show: Function }>('modal');
const { navigate } = getContext<{ navigate: Function }>('navigation');
const { navigate, insertUrl } = getContext<{ navigate: Function; insertUrl: Function }>(
'navigation'
);
const { isAuth } = getContext<{ isAuth: Writable<boolean> }>('auth');
let filter = $isAuth ? 'user' : 'public';
let filter = 'user';
const exerciceStore = writable<Store<Page|undefined>>({
const exerciceStore = writable<Store<Page | undefined>>({
isLoading: false,
isFetching: false,
isSuccess: false,
data: undefined
});
const tagStore = writable<Store<Tag[]|undefined>>({
const tagStore = writable<Store<Tag[] | undefined>>({
isLoading: false,
isFetching: false,
isSuccess: false,
data: undefined
data: []
});
setContext('exos', exerciceStore);
setContext('tags', tagStore);
onMount(() => {
let page = $page.url.searchParams.get('page')
if(page == null){
page = '1'
}
if ($page.params.slug != undefined && !['user', 'public'].includes($page.params.slug)) {
getExo($page.params.slug).then((r) => {
show(ModalCard, { exo: r, exos: exerciceStore, tags: tagStore }, () => navigate('/exercices/' + filter));
insertUrl('exercices/' + filter);
show(ModalCard, { exo: r, exos: exerciceStore, tags: tagStore }, () => navigate(-1));
});
} else if ($page.params.slug == undefined) {
goto('/exercices/public');
} else if ($page.params.slug == undefined || $page.params.slug == "user") {
filter = $isAuth ? 'user' : 'public';
goto(`/exercices/${filter}?${new URLSearchParams({page}).toString()}`)
} else if($page.params.slug == "public"){
filter = 'public';
goto(`/exercices/${filter}?${new URLSearchParams({page}).toString()}`)
}
});
$: filter = ['user', 'public'].includes($page.params.slug) ? $page.params.slug : filter;
/*$: if(['user', 'public'].includes($page.params.slug) && !$isAuth){
filter = "public"
//goto('/exercices/' + filter)
}*/
const size = 15;
$: activePage = parseInt($page.url.searchParams.get('page')!) || 1;
let search = '';
let selected: Tag[] = [];
$: {
if(!$isAuth){
filter = 'public'
}
exerciceStore.update((s) => {
return { ...s, isFetching: true };
});
getExos(filter as 'public' | 'user', {
page: activePage,
page: activePage == 0 ? 1: activePage,
search,
size,
tags: [...selected.map((t) => t.id_code)]
}).then((r) => {
exerciceStore.update((e) => {
return { ...e, isSuccess: true, isFetching: false, data: r };
});
});
})
.then((r) => {
console.log('R', r);
if (activePage > r.totalPage && r.total != 0 && r.totalPage != 0) {
activePage = r.totalPage;
//$p.url.searchParams.set('page', String(activePage));
goto(`?${new URLSearchParams({page: activePage}).toString()}`);
return;
}
exerciceStore.update((e) => {
return { ...e, isSuccess: true, isFetching: false, data: r };
});
})
.catch(console.log);
}
$: {
tagStore.update((s)=>{return {...s, isFetching: true}});
getTags().then(r=>{
tagStore.update((e) => {
return { ...e, isSuccess: true, isFetching: false, data: r };
if($isAuth) {
tagStore.update((s) => {
return {...s, isFetching: true};
});
})
getTags().then((r) => {
tagStore.update((e) => {
return {...e, isSuccess: true, isFetching: false, data: r};
});
});
}
}
let activePage = parseInt($page.url.searchParams.get('page')!) || 1;
let search = '';
let selected: Tag[] = [];
</script>
{#if $tagStore.isSuccess == true && $tagStore.data != undefined}
{#if $tagStore.data != undefined}
<Head location={filter} bind:search bind:selected />
{/if}
{#if $tagStore.isFetching == true}
@ -86,13 +123,20 @@
<div class="feed">
<div class="title">
<h1>
Tous les <span>exercices</span>
</h1>
<p>
Vous retrouverez ici tous les exercices que vous avez créé ou copié depuis les exercices
publics
</p>
{#if filter == 'user'}
<h1>
Vos <span>exercices</span>
</h1>
<p>
Vous retrouverez ici tous les exercices que vous avez créé ou copié depuis les exercices
publics
</p>
{:else}
<h1>
Tous les <span>exercices</span>
</h1>
<p>Vous retrouverez ici tous les exercices créés par les autres utilisateurs</p>
{/if}
</div>
{#if $exerciceStore.data != undefined}
{#each $exerciceStore.data.items.filter((e) => e != null && selected.every((t) => e.tags
@ -100,11 +144,63 @@
.includes(t.id_code))) as e}
<Card bind:exo={e} />
{/each}
<Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} />
{:else}
{#each Array(10) as i}
<div class="skeleton"><span /></div>
{/each}
{/if}
</div>
{#if $exerciceStore.data != undefined}
<Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} />
{/if}
<style lang="scss">
@import '../../variables';
.skeleton {
width: 330px;
height: 250px;
opacity: .8;
span {
display: block;
height: 100%;
width: 100%;
background-color: $skeleton;
display: block;
position: relative;
border-radius: 4px;
background-color: $skeleton;
overflow: hidden;
&::after {
top: 0;
left: 0;
right: 0;
bottom: 0;
content: '';
position: absolute;
animation: waves 1.6s linear 0.5s infinite;
transform: translateX(-100%);
background: linear-gradient(
90deg,
transparent,
(lighten($color: $skeleton, $amount: 3), rgba(0, 0, 0, 0.04)),
transparent
);
}
}
}
@keyframes waves {
0% {
transform: translateX(-100%);
}
60% {
transform: translateX(100%);
}
100% {
transform: translateX(100%);
}
}
.feed {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
@ -129,7 +225,7 @@
font-size: 1.1em;
}
span {
color: red;
color: $primary;
}
}
</style>

View File

@ -7,20 +7,31 @@
import type { Writable } from 'svelte/store';
import type { Store } from '../../types/api.type';
const { navigate } = getContext<{navigate: Function}>('navigation');
const { show, close } = getContext<{show: Function, close: Function}>('modal');
const { navigate } = getContext<{ navigate: Function }>('navigation');
const { show, close } = getContext<{ show: Function; close: Function }>('modal');
export let location = 'public';
export let search = '';
export let selected: Tag[] = [];
const { isAuth } = getContext<{ isAuth: boolean }>('auth');
const { isAuth } = getContext<{ isAuth: Writable<boolean> }>('auth');
const tags: Writable<Store<Tag[]>> = getContext('tags')
const tags: Writable<Store<Tag[]>> = getContext('tags');
const exerciceStore: Writable<Store<Page>> = getContext('exos');
let updateText = null;
let tmp =""
$: {
if(updateText != null){
clearTimeout(updateText);
}
updateText = window.setTimeout(() => {
search = tmp;
}, 500);
}
</script>
<div class="head">
<div class="new">
{#if !!isAuth}
{#if !!$isAuth}
<button
class="border-primary-btn"
on:click={() => {
@ -35,15 +46,22 @@
{/if}
</div>
<div class="search">
<input type="text" placeholder="Rechercher" class="input" bind:value={search} />
{#if !!isAuth}
<input
type="text"
placeholder="Rechercher"
class="input"
bind:value={tmp}
/>
{#if !!$isAuth}
<TagSelector options={$tags.data} bind:selected />
<select
name="ee"
id="e"
name="tagsSelect"
id="tagsSelect"
class="input"
bind:value={location}
on:change={(e) => navigate(`/exercices/${e.currentTarget.value}`)}
on:change={(e) =>
navigate(`/exercices/${e.currentTarget.value}?${new URLSearchParams({ page: 1 })}`)}
>
<option value="user">Vos exos</option>
<option value="public">Tous les exos</option>

View File

@ -1,35 +1,137 @@
<script lang="ts">
import {page as p} from '$app/stores'
import { goto } from "$app/navigation";
import { page as p } from '$app/stores';
import { goto } from '$app/navigation';
export let page: number;
export let total: number;
const changePage = (p: number) => {
goto(`?${new URLSearchParams({page: p}).toString()}`);
};
</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}
<button
on:click={() => {
changePage(page - 1);
}}
on:keydown={() => {}}
disabled={page <= 1}
>
{'<'}
</button>
{#if total >= 7}
<!-- First two -->
{#each Array.from({ length: 2 }, (v, k) => k + 1) as i}
<button
class:active={page == i}
on:click={() => {
changePage(i);
}}
on:keydown={() => {}}
>
{i}
</button>
{/each}
<!-- Middle : active with a padding of 1 -->
{#if page >= 2 && page <= total - 1}
{#if page - 1 > 3}
<p>...</p>
{/if}
{#each Array.from({ length: 3 }, (v, k) => page - 1 + k) as i}
{#if i > 2 && i <= total - 2}
<button
class:active={page == i}
on:click={() => {
changePage(i);
}}
on:keydown={() => {}}
>
{i}
</button>
{/if}
{/each}
{#if page + 1 < total - 2}
<p>...</p>
{/if}
{:else}
<p>...</p>
{/if}
<!-- Last two -->
{#each Array.from({ length: 2 }, (v, k) => total - 2 + k + 1) as i}
<button
class:active={page == i}
on:click={() => {
changePage(i);
}}
on:keydown={() => {}}
>
{i}
</button>
{/each}
{:else}
{#each Array.from({ length: total }, (v, k) => k + 1) as i}
<button
class:active={page == i}
on:click={() => {
changePage(i);
}}
on:keydown={() => {}}
>
{i}
</button>
{/each}
{/if}
<button
on:click={() => {
changePage(page+1)
}}
on:keydown={() => {}}
disabled={page >= total}
>
{'>'}
</button>
</div>
<style lang="scss">
.active {
color: red;
}
@import '../../variables';
.pagination {
display: flex;
margin: 30px;
p {
height: max-content;
width: 100%;
justify-content: center;
align-items: center;
button {
margin: 10px;
border: 1px solid $border;
border-radius: 4px;
padding: 7px 9px;
cursor: pointer;
background-color: rgba($background, 0.3);
color: #f8f8f8;
transition: .3s;
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&:not(:disabled):hover{
color: $primary;
border-color: $primary;
}
}
p{
font-size: 1.2em;
letter-spacing: 1.5px;
}
}
.active {
color: #080808 !important;
background-color: $primary !important;
}
</style>

View File

@ -1,38 +1,37 @@
<script lang="ts">
import chroma from 'chroma-js';
export let label: string;
export let color: string;
export let remove: Function;
console.log(color)
export let remove: Function | null = null;
let removed = false;
</script>
<div class:removed class="selected" style={`--item-color:${chroma(color).rgb().join(',')};`}>
<div class:removed class="selected" style={`--item-color:${chroma(color).rgb().join(',')};`} class:removable={!!remove} {...$$restProps}>
<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
{#if !!remove}
<button
class="unselect"
on:click={() => {
removed = true;
remove && remove();
}}
on:keypress={() => {}}
>
</div>
<svg
height="14"
width="14"
viewBox="0 0 20 20"
aria-hidden="true"
focusable="false"
><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
>
</button>
{/if}
</div>
<style lang="scss">
@ -65,7 +64,7 @@
min-width: 0px;
box-sizing: border-box;
transition: 0.5s;
max-width: 100px;
//max-width: 100px;
}
.removed {
@ -82,4 +81,8 @@
white-space: nowrap;
box-sizing: border-box;
}
:not(.removable) > .label{
padding: 4px 6px;
}
</style>

View File

@ -13,11 +13,12 @@
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'))
const {alert, info, success, error} = getContext<{alert: Function, info: Function, success: Function, error: Function}>("notif")
</script>
<div
class="tags-container"
data-testid="tags"
class:tg
class:tagMode
on:click|stopPropagation={() => {}}
@ -31,6 +32,9 @@
remove={() => {
delTags(exo.id_code, t.id_code).then((r) => {
exo.tags = r.tags;
success('Tag', `Tag *${t.label}* supprimé à *${exo.name}* avec succès`)
}).catch((r)=>{
error('Tag', `Erreur lors de la suppression du tag *${t.label}* à *${exo.name}*`)
});
return true;
}}
@ -42,7 +46,6 @@
class="expand"
on:click={() => {
tg = true;
console.log('TAGGGG', $tags)
setTimeout(() => {
tagMode = true;
}, 200);
@ -71,6 +74,9 @@
}
tg = false;
tagMode = false;
success('Tags', `Tags ajouté(s) avec succès à *${exo.name}*`)
}).catch((r)=>{
error('Tags', `Erreur lors de l'ajout de tags à *${exo.name}*`)
});
}}>Valider !</button
>
@ -161,5 +167,8 @@
width: 100%;
margin: 0;
}
:global(> button){
width: 99%;
}
}
</style>

View File

@ -0,0 +1,53 @@
<script lang="ts">
import { writable } from 'svelte/store';
import type { Tag as TagType } from '../../types/exo.type';
import Tag from './Tag.svelte';
export let tags: TagType[];
export let id: str;
let tg: HTMLDivElement;
let invi = writable([]);
$: {
if (tg != undefined && tags.length > 0 && tg.id == id) {
if (tg != null && tg.clientWidth < tg.scrollWidth) {
invi.set([]);
for (const e of tg.children) {
if (e.offsetLeft + e.offsetWidth > tg.offsetLeft + tg.offsetWidth - 150) {
e.style.display = 'none';
$invi = [...$invi, e.getAttribute('aria-label')];
}
}
}
}
}
$: console.log('ID', id, invi);
</script>
<div class="tags" bind:this={tg} {id}>
{#each tags as t}
<div aria-label={t.label}><Tag label={t.label} color={t.color} /></div>
{/each}
</div>
{#if $invi.length > 0}
<div class="more"><Tag label={`+ ${$invi.length}`} color="#888888" title={$invi.join(', ')} /></div>
{/if}
<style lang="scss">
.more{
min-width: none;
}
.tags {
display: flex;
max-width: 100%;
overflow: hidden;
flex-shrink: 0;
//justify-content: end;
flex-grow: 1;
gap: 5px;
p {
background-color: red;
margin: 0 10px;
}
}
</style>

View File

@ -3,6 +3,7 @@
import MdFileDownload from 'svelte-icons/md/MdFileDownload.svelte';
export let label = 'Choisir un fichier';
export let value: FileList;
export let defaultValue: string|null;
export let id_code: string | null = null;
const id = String(Math.random());
@ -12,8 +13,8 @@
<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 value.length !== 0 || defaultValue != null}
<p>{value.length !== 0 && value[0] != undefined? value[0].name : defaultValue!=null?defaultValue:"..."}</p>
{#if id_code != null}
<div
class="icon"

View File

@ -1,10 +1,12 @@
<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 value: string | null = null;
export let label = '';
export let errors: string[] = [];
export let change: Function = (e: Event) => {};
function typeAction(node: HTMLInputElement) {
node.type = type;
}
@ -17,10 +19,24 @@
element.type = show === true ? 'password' : 'text';
show = !show;
};
let test: HTMLInputElement;
export const focus = () => {
console.log('FOCUS', test)
test.focus();
};
</script>
<span class="inputLabel" class:error={errors.length !== 0}>
<input use:typeAction {id} bind:value {...$$restProps} placeholder="" />
<input
use:typeAction
on:input={(e)=>{change(e)}}
{id}
bind:value
{...$$restProps}
placeholder=""
bind:this={test}
/>
<!-- placeholder = "" pour que le label se place bien avec :placeholder-shown -->
<label for={id}>{label}</label>
{#if type == 'password'}

View File

@ -9,7 +9,7 @@
export let isDisabled = false;
//export let isMultiple = false;
const color = chroma(item.color);
const color = chroma("rgb(255,0,0)");
console.log(color.rgb(), color);
</script>
@ -51,6 +51,7 @@
</div>
<style lang="scss">
@import '../../variables';
.unselect {
color: rgb(var(--item-color));
-moz-box-align: center;
@ -125,6 +126,9 @@
cursor: not-allowed;
color: grey;
background-color: var(--sv-bg);
}
:global(.creatable-row-wrap){
background-color: red;
}
</style>

View File

@ -0,0 +1,96 @@
<script lang="ts">
import IoMdEye from 'svelte-icons/io/IoMdEye.svelte';
import IoMdEyeOff from 'svelte-icons/io/IoMdEyeOff.svelte';
export let label: string;
export let value = ""
export let type = "text"
let show = type != 'password';
const id = Math.random().toString(36).substr(2, 9);
const toggle = () => {
console.log('OOGLE ')
const element = document.getElementById(id) as HTMLInputElement;
if (element === null) return;
element.type = show === true ? 'password' : 'text';
show = !show;
};
let test: HTMLInputElement;
function typeAction(node: HTMLInputElement) {
node.type = type;
}
export let errors: string[] = []
</script>
<div class="input">
<div class="labeled">
{#if label != null}<label for={id}>{label}</label>
{/if}
<input use:typeAction {id} bind:value {...$$restProps}/>
{#if type == 'password'}
<div class="toggle" on:click={toggle} on:keypress={() => {}}>
{#if show == false}
<IoMdEyeOff/>
{:else if show == true}
<IoMdEye/>
{/if}
</div>
{/if}
</div>
{#if errors.length !== 0}
<p class="error-msg">{errors[0]}</p>
{/if}
</div>
<style lang="scss">
.input {
display: flex;
flex-direction: column;
margin: 0.5rem;
}
.labeled {
display: flex;
flex-direction: column;
position: relative;
}
.toggle {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
right: 0;
width: 20px;
height: 20px;
cursor: pointer;
}
.error-msg{
color: $red;
margin-top: 5px;
}
label {
font-size: 0.8rem;
margin-bottom: 0.2rem;
text-transform: uppercase;
}
input {
border-radius: 0.2rem;
background: #160339;
border: 1px solid $border;
color: #f8f8f8;
border-radius: 0;
padding: 12px 8px;
&:focus {
border: 1px solid $contrast;
outline: none;
box-shadow: 0 0 0 3px #1a0f7a;
}
}
</style>

View File

@ -55,4 +55,11 @@
--sv-border: none!important;
--sv-active-border:(1px solid $contrast)!important;
}
:global(.creatable-row-wrap), :global(.sv-dropdown-scroll){
background-color: $background!important ;
}
:global(.creatable-row){
padding: 7px 3px!important;
}
</style>

View File

@ -0,0 +1,37 @@
<script lang='ts'>
import InputWithLabel from "../forms/InputWithLabel.svelte";
export let validate: Function = (e: string) => {
};
let pseudo = ""
</script>
<div class="content">
<h1>Veuillez vous identifier</h1>
<p>Entrez un pseudo ou un code (commençant par #, demandez à l'administrateur de la salle si vous l'avez oublié)</p>
<InputWithLabel type="text" bind:value={pseudo} label="Pseudo ou code"/>
<button on:click={()=>{
validate(pseudo)
}} class="primary-btn">Valider !
</button>
</div>
<style lang='scss'>
.content {
padding: 30px 25px;
background: $background;
display: flex;
flex-direction: column;
gap: 30px;
align-items: center;
border-radius: 4px;
}
h1 {
font-size: 2.8em;
}
p {
font-style: italic;
}
</style>

View File

@ -0,0 +1,290 @@
<script lang="ts">
import type {
Challenge,
Member,
ParcoursInfos,
Room,
Note as NoteType
} from '../../types/room.type';
import { getContext, onDestroy } from 'svelte';
import { writable, type Writable } from 'svelte/store';
import { challenge, corrigeChallenge, getChallenge, getParcours, sendChallenge } from '../../requests/room.request';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import InputChallenge from './InputChallenge.svelte';
import { parseTimer } from '../../utils/utils';
import FaUndo from 'svelte-icons/fa/FaUndo.svelte';
const room: Writable<Room> = getContext('room');
const member: Writable<Member> = getContext('member');
const challengeStore: Writable<{
challenge: Challenge[];
id_code: string;
parcours: ParcoursInfos;
corriged: boolean;
mistakes?: number
validated?: boolean;
challenger?: { name: string };
isCorriged?: boolean,
} | null> = writable(null);
export let id_code: string;
export let corrige: boolean = false;
$: !corrige &&
challenge($room.id_code, id_code, $member.isUser ? $member.clientId : null).then((p) => {
challengeStore.set({ ...p, corriged: false });
});
$: corrige &&
getChallenge($room.id_code, id_code, $member.isUser ? $member.clientId : null).then((p) => {
challengeStore.set({ ...p, challenge: p.data, note: {...p.note, temporary: !p.isCorriged}, corriged: true });
remaining = p.time;
});
let timer: number | null = null;
let remaining: number | null = null;
$: {
if (!corrige && $challengeStore != null && remaining == null) {
remaining = $challengeStore.parcours.time * 60;
}
}
$: {
if (!corrige && $challengeStore != null && timer == null && remaining != null) {
timer = window.setInterval(() => {
remaining = remaining! - 1;
}, 1000);
}
}
onDestroy(() => {
if (timer != null) {
clearInterval(timer);
}
});
</script>
{#if $challengeStore != null}
<div class="head">
<h1>
{$challengeStore.parcours.name}
{#if corrige && !!$challengeStore.challenger && remaining != null}
<span class="correction-info">
- Correction de <span class="italic">{$challengeStore.parcours.name}</span> par
<span class="italic underline">{$challengeStore.challenger.name}</span>
en {parseTimer(remaining)}</span
>
{:else}
<span
class="icon"
on:click={() => {
challenge($room.id_code, id_code, $member.isUser ? $member.clientId : null).then(
(p) => {
challengeStore.set({ ...p, corriged: false });
remaining = null;
if (timer != null) {
clearInterval(timer);
timer = null;
}
}
);
}}
title={'Réessayer'}
on:keydown={() => {}}><FaUndo /></span
>
{/if}
</h1>
{#if $challengeStore.mistakes}
{$challengeStore.mistakes} fautes
{/if}
{#if !corrige}
<p
class="timer"
class:oneminute={remaining != null && remaining < 60}
class:late={(remaining != null && remaining < 0) ||
[9, 7, 5, 3, 1].includes(remaining != null ? remaining : 0)}
>
{remaining != null && parseTimer(remaining)}
</p>
{/if}
</div>
{#each $challengeStore.challenge as e, d (`${$challengeStore.id_code}_${d}`)}
<div class="exo">
<div class="infos">
<h2>Exercice {d + 1} : <span>{e.exo.name}</span></h2>
<p>
{e.exo.consigne}
</p>
</div>
<div class="data">
{#each e.data as c, a}
<div class="calcul">
{#each c.calcul.replace(']', '] ').replace('[', ' [').split(' ') as i, b}
{#if i.startsWith('[') && i.endsWith(']')}
<InputChallenge
bind:value={c.inputs[parseInt(i.replace('[', '').replace(']', ''))].value}
bind:correction={c.inputs[parseInt(i.replace('[', '').replace(']', ''))]
.correction}
corriged={$challengeStore.corriged}
bind:valid={c.inputs[parseInt(i.replace('[', '').replace(']', ''))].valid}
corrigeable={corrige}
/>
{:else}
{i}{' '}
{/if}
{/each}
</div>
{/each}
</div>
</div>
{/each}
<div>
{#if !corrige}
<button
hidden={$challengeStore.corriged}
class="primary-btn"
on:click={() => {
if ($challengeStore == null || remaining == null) return;
sendChallenge(
$room.id_code,
id_code,
$challengeStore.id_code,
{
challenge: $challengeStore.challenge,
time: $challengeStore.parcours.time * 60 - remaining
},
$member.isUser ? $member.clientId : null
).then((r) => {
if ($challengeStore != null) {
$challengeStore.challenge = r.data;
$challengeStore.corriged = true;
$challengeStore.mistakes = r.mistakes
$challengeStore.validated = r.validated;
}
if (timer != null) {
clearInterval(timer);
}
});
}}>Valider !</button
>
<button
hidden={!$challengeStore.corriged}
class="primary-btn"
on:click={() => {
challenge($room.id_code, id_code, $member.isUser ? $member.clientId : null).then((p) => {
challengeStore.set({ ...p, corriged: false });
remaining = null;
if (timer != null) {
clearInterval(timer);
timer = null;
}
});
}}>Réessayer !</button
>
{:else}
<button
hidden={!$challengeStore.corriged}
class="primary-btn"
on:click={() => {
corrigeChallenge($room.id_code, id_code,$challengeStore?.challenge, $member.isUser ? $member.clientId : null).then((p) => {
if($challengeStore == null) return
$challengeStore.challenge = p.data
$challengeStore.mistakes = p.mistakes
$challengeStore.validated = p.validated
});
}}>Valider !</button
>
{/if}
<button
class="danger-btn"
on:click={() => {
if ($challengeStore == null) return;
goto(`?${new URLSearchParams({p: $challengeStore.parcours.id_code}).toString()}`);
}}>{!$challengeStore.corriged?"Annuler !":"Retour"}</button
>
</div>
{/if}
<style lang="scss">
.timer {
font-size: 2em;
color: $green;
font-weight: 800;
}
.oneminute {
color: $orange;
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
margin: 40px 0;
min-height: 70px;
}
.late {
color: $red;
}
.calcul {
display: flex;
align-items: center;
gap: 10px;
position: relative;
}
.data {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 10px;
}
.infos {
h2 {
font-size: 1.2em;
span {
font-style: italic;
}
}
p {
font-size: 1em;
font-style: italic;
text-decoration: underline;
}
margin-bottom: 10px;
}
.exo {
margin-bottom: 30px;
margin-top: 20px;
}
.icon {
height: 20px;
width: 20px;
cursor: pointer;
display: flex;
transition: 0.2s;
&:hover {
transform: rotate(360deg);
}
}
h1 {
display: flex;
align-items: center;
gap: 20px;
font-size: 2.3em;
}
.correction-info {
font-size: 0.6em;
color: grey;
font-weight: 600;
}
</style>

View File

@ -0,0 +1,113 @@
<script lang="ts">
import type { Member, ParcoursRead } from '../../types/room.type';
import { getContext } from 'svelte';
import type { Writable } from 'svelte/store';
import IoIosArrowDown from 'svelte-icons/io/IoIosArrowDown.svelte';
import { goto } from '$app/navigation';
import { parseTimer } from '../../utils/utils';
import IoMdOpen from 'svelte-icons/io/IoMdOpen.svelte';
const parcours: Writable<ParcoursRead | null> = getContext('parcours');
const member: Writable<Member | null> = getContext('member');
let selected = '';
</script>
{#if $parcours != null && $member != null}
<div class="trylist">
{#if Object.keys($parcours.challenges).length == 0}
<p class="italic">Aucun essai effectué :(</p>
{/if}
{#each Object.entries($parcours.challenges) as [id, chall]}
<p
on:click={() => {
selected = selected == chall.challenger.id_code ? '' : chall.challenger.id_code;
}}
class:selected={selected == chall.challenger.id_code}
class="tries"
on:keydown={() => {}}
>
<span class="icon"><IoIosArrowDown /></span>
{chall.challenger.id_code == $member.id_code ? 'Vos essais' : chall.challenger.name}
</p>
{#if selected == chall.challenger.id_code}
{#each chall.challenges as c}
<div
class="try"
on:click={() => {
goto(`?${new URLSearchParams({corr: c.id_code}).toString()}`);
}}
on:keydown={() => {}}
title="Voir la correction"
>
<p><span class:validated={c.validated} class:uncorriged={!c.isCorriged} class="note"
>{c.mistakes} faute{c.mistakes > 1 ?"s": ""} </span> en <strong >{parseTimer(c.time)}</strong>
</p>
<span class="corrige-link icon"><IoMdOpen /></span>
</div>
{/each}
{/if}
{/each}
</div>
{/if}
<style lang="scss">
.tries {
cursor: pointer;
display: flex;
gap: 5px;
align-items: center;
transition: 0.3s;
.icon {
width: 20px;
display: flex;
align-items: center;
transform: rotate(-90deg);
transition: 0.2s;
}
}
.selected {
font-weight: 700;
.icon {
transform: rotate(0);
}
}
.try {
display: flex;
gap: 10px;
cursor: pointer;
width: max-content;
margin-left: 30px;
p span{
color: $red;
font-weight: 600;
}
}
.icon {
display: flex;
align-items: center;
width: 20px;
}
.corrige-link {
color: $primary;
}
.uncorriged {
color: grey;
font-weight: 900;
}
.trylist {
display: flex;
flex-direction: column;
gap: 10px;
}
.validated {
color: $green !important;
}
</style>

View File

@ -0,0 +1,41 @@
<script lang="ts">
export let tops: { name: string; value: string }[];
export let rank: { rank: number; name: string; value: string } | null = null;
$: tops = [...tops, ...new Array(3).fill(null)].slice(0, 3);
</script>
<div class="classement">
{#each tops as t, i}
{#if t != null}
<p class={`top-${i+1}`}><strong>#{i + 1} - {t.name}</strong> - <span>{t.value}</span></p>
{:else}
<p class={`top-${i+1}`}><strong >#{i + 1}{" - "}</strong></p>
{/if}
{/each}
{#if rank != null && rank.rank > 3}
<p class="top-x"><span>#{rank.rank} - {rank.name}</span> - <span>{rank.value}</span></p>
{/if}
</div>
<style lang="scss">
.classement {
display: flex;
flex-direction: column;
gap: 5px;
}
.top-1 {
color: gold;
}
.top-2 {
color: silver;
}
.top-3 {
color: brown;
}
.top-x{
margin-top: 10px;
}
</style>

View File

@ -0,0 +1,184 @@
<script lang="ts">
import MdCheck from 'svelte-icons/md/MdCheck.svelte';
import MdClose from 'svelte-icons/md/MdClose.svelte';
import InputWithLabel from '../forms/InputWithLabel.svelte';
export let value = '';
export let correction: string | null = null;
export let corriged: boolean = false;
export let valid: boolean | null = null;
export let corrigeable = false;
let hidden = false;
let test: InputWithLabel;
$: hidden == true && test.focus();
let close = true;
let mainDiv: HTMLDivElement;
const toggleFocus = (e: FocusEvent) => {
console.log('TOOGLE', e.relatedTarget, 'ho', mainDiv.contains(e.relatedTarget as Node), e);
if (hidden == true && !mainDiv.contains(e.relatedTarget as Node)) {
hidden = false
console.log('PASEED')
setTimeout(() => {
console.log('HOPIDOP')
close = true;
}, 300);
}
};
const changeValid = (e: Event & { currentTarget: HTMLInputElement & EventTarget }) => {
valid = corriged && e.currentTarget.value == value;
};
</script>
<div style:position="relative" bind:this={mainDiv}>
<div
class:hidden={!hidden}
class:close
class="correction"
on:focusout={toggleFocus}
data-testid="hiddenInput"
>
<InputWithLabel
bind:this={test}
type="text"
class="input"
label="Correction"
bind:value={correction}
change={changeValid}
/>
<div
data-testid="valid"
class="icon check"
class:valid={valid != null && valid}
on:mousedown|preventDefault={() => {
valid = true;
}}
>
<MdCheck />
</div>
<div
class="icon invalid-mark"
data-testid="invalid"
class:invalid={valid != null && !valid}
on:mousedown|preventDefault={() => {
valid = false;
//test.focus()
}}
>
<MdClose />
</div>
</div>
<span
class="input"
role="textbox"
contenteditable={!corriged}
class:corriged
class:valid={corriged && valid != null && valid}
class:invalid={corriged && valid != null && !valid}
class:notcorriged={corriged && valid == null}
on:input={(e) => {
value = e.currentTarget.outerText;
}}
on:click|stopPropagation={() => {
if (!corrigeable || !corriged || !close) return;
close = false;
hidden = true;
setTimeout(() => {
if (test != null) {
test.focus();
}
}, 200);
}}
on:keydown={() => {}}
title={correction}
>
{value}
</span>
</div>
<style lang="scss">
.corriged {
cursor: pointer;
}
.input {
width: min-content;
display: inline;
max-width: 200px;
overflow: scroll;
scrollbar-width: none !important;
max-height: 30px;
height: 30px;
white-space: pre;
overflow: hidden;
}
.valid {
color: $green !important;
border-bottom: 2px solid $green;
font-weight: 800;
}
.invalid {
color: $red !important;
border-bottom: 2px solid $red;
font-weight: 800;
}
.notcorriged {
color: grey;
border-bottom: 2px solid grey;
font-weight: 800;
}
.correction {
transition: 0.3s;
transition: display 0;
position: absolute;
background-color: $background;
padding: 10px;
z-index: 10;
display: flex;
gap: 10px;
align-items: center;
top: -100%;
transform: translateY(-75%) translateX(-50%);
width: 232px;
&::after {
content: '';
position: absolute;
top: 100%;
width: 0;
height: 0;
border-left: 15px solid transparent;
border-right: 15px solid transparent;
border-top: 15px solid rgba(29, 26, 90, 0.9);
clear: both;
left: 50%;
}
}
.icon {
width: 25px;
cursor: pointer;
color: grey;
flex-shrink: 0;
&.valid,
&.invalid {
border: none;
}
&:hover {
transform: scale(1.05);
&.check {
color: $green;
}
&.invalid-mark {
color: $red;
}
}
}
.hidden {
z-index: -1;
opacity: 0;
}
.close :global(*) {
display: none;
}
</style>

View File

@ -0,0 +1,206 @@
<script lang="ts">
import type {Member, Room, Waiter} from '../../types/room.type';
import FaUnlock from 'svelte-icons/fa/FaUnlock.svelte';
import FaLock from 'svelte-icons/fa/FaLock.svelte';
import {getContext} from 'svelte';
import type {Writable} from 'svelte/store';
const room: Writable<Room> = getContext('room');
const member: Writable<Member> = getContext('member');
const {send} = getContext<{ send: Function }>('ws');
$: online =
$room != null
? $room.members.filter((r): r is Member => 'online' in r && r.online == true)
: [];
$: offline =
$room != null
? $room.members.filter((r): r is Member => 'online' in r && r.online == false)
: [];
$: waiters =
$room != null
? $room.members.filter((r): r is Waiter => 'waiter_id' in r && !!r.waiter_id)
: [];
</script>
<div class="members">
<div class="head">
<h2>Participants</h2>
{#if !!$member.isAdmin}
<div
data-testid="visibilityChange"
class:public={$room.public}
class:private={!$room.public}
class="icon"
on:click={() => {
send('set_visibility', { public: !$room.public });
}}
on:keydown={() => {}}
>
{#if $room.public}
<div data-testid="public">
<FaUnlock
title="Rendre la salle privée (vous devrez accepter chaque personne voulant entrer)"
/>
</div>
{:else}
<div data-testid="private">
<FaLock title="Rendre la salle ouverte (tout personne ayant le code pourra entrer librement)"/>
</div>
{/if}
</div>
{/if}
</div>
<div>
<h3>En ligne :</h3>
{#each online as m}
<p
class:admin={m.isAdmin}
class:bannable={m.id_code != $member.id_code && $member.isAdmin && !m.isAdmin}
class:member={m.id_code == $member.id_code}
class="online"
title={$member.isAdmin && !m.isAdmin ? 'Bannir' : ''}
on:click={() => {
$member.isAdmin && send('ban', { member_id: m.id_code });
}}
on:keydown={() => {}}
>
{m.username}
{#if m.reconnect_code != '' &&( $member.isAdmin || m.id_code == $member.id_code)}
<span>#{m.reconnect_code}</span>
{/if}
{#if m.isAdmin}
<span class="admin-mark">Administrateur</span>
{/if}
</p>
{/each}
{#if offline.length > 0}
<h3>Hors-ligne :</h3>
{#each offline as m}
<p
class:admin={m.isAdmin}
class="offline"
class:bannable={m.id_code != $member.id_code && $member.isAdmin && !m.isAdmin}
class:member={m.id_code == $member.id_code}
title={$member.isAdmin && !m.isAdmin ? 'Bannir' : ''}
on:click={() => {
$member.isAdmin && send('ban', { member_id: m.id_code });
}}
on:keydown={() => {}}
>
{m.username}
{#if (m.reconnect_code != '' && $member.isAdmin) || m.id_code == $member.id_code}
<span>#{m.reconnect_code}</span>
{/if}
{#if m.isAdmin}
<span class="admin-mark">Administrateur</span>
{/if}
</p>
{/each}
{/if}
{#if $member.isAdmin && waiters.length > 0}
<h3>Liste d'attente :</h3>
{#each waiters as m}
<p>
{m.username}<span>#{m.waiter_id}</span>
<button
class="accept"
on:click={() => {
send('accept', { waiter_id: m.waiter_id });
}}>Accept
</button
>
<button
class="refuse"
on:click={() => {
send('refuse', { waiter_id: m.waiter_id });
}}>Refuse
</button
>
</p>
{/each}
{/if}
</div>
</div>
<style lang="scss">
.head {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
border-bottom: 1px solid $border;
.icon {
width: 20px;
height: 20px;
cursor: pointer;
}
h2 {
font-size: 1.2em;
font-weight: 900;
padding: 10px;
padding-top: 0;
padding-left: 0;
}
}
.members {
background-color: rgba($background, 0.4);
border: 1px solid $border;
border-radius: 6px;
padding: 20px 15px;
width: 100%;
min-height: 70%;
max-height: 100%;
h3 {
font-size: 1em;
font-weight: 700;
margin: 10px 0;
}
p {
cursor: pointer;
margin-left: 15px;
font-size: 1em;
width: max-content;
span {
color: grey;
font-size: 0.9em;
margin-left: 3px;
}
margin-bottom: 4px;
}
.bannable:hover {
text-decoration: line-through;
color: $red;
font-weight: 700;
}
.offline {
color: rgb(178, 176, 176);
}
.member {
font-weight: 900;
}
}
.admin-mark {
border-radius: 3px;
background-color: $contrast;
padding: 3px;
color: #f8f8f8 !important;
font-weight: 500;
font-size: 0.8em !important;
}
</style>

View File

@ -0,0 +1,41 @@
<script lang="ts">
export let note: number;
export let total: number;
export let valid: Boolean;
export let temporary: boolean;
$: on20 = (note * 20) / total;
</script>
<div class="note">
<p class:valid class:temporary={!valid && temporary}>{note} / {total} = {parseFloat(on20.toFixed(2))} / 20</p>
{#if !temporary && !valid}
<p class='parc' class:valid>Parcours non validé !</p>
{:else if valid}
<p class='parc' class:valid>Parcours validé !</p>
{:else if temporary}
<p class='parc' class:temporary>Parcours en attente de correction</p>
{/if}
</div>
<style lang="scss">
.note {
display: flex;
flex-direction: column;
align-items: center;
p {
font-size: 2em;
font-weight: 800;
color: $red;
}
}
.valid {
color: $green!important;
}
.temporary {
color: grey;
}
.parc{
font-size: 1.3em;
}
</style>

View File

@ -0,0 +1,143 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { createParcours, getParcours, updateParcours } from '../../requests/room.request';
import { getContext, onMount } from 'svelte';
import { writable, type Writable } from 'svelte/store';
import ExerciceSelector from '../exos/ExerciceSelector.svelte';
import InputWithLabel from '../forms/InputWithLabel.svelte';
import type { ExoSelect, Member, ParcoursRead, Room } from 'src/types/room.type';
export let id_code: string | null = null;
const room = getContext<Writable<Room | null>>('room');
const member = getContext<Writable<Member | null>>('member');
const parcours = getContext<Writable<ParcoursRead | null>>('parcours');
const { error, success } = getContext<{ error: Function; success: Function }>('notif');
const { isAuth } = getContext<{ isAuth: Writable<boolean> }>('auth');
const exos: Writable<ExoSelect[]> = writable(
id_code != null &&$parcours != null ? ($parcours.exercices as ExoSelect[]) : []
);
let name = id_code != null &&$parcours != null ? $parcours.name : '';
let time = id_code != null &&$parcours != null ? String($parcours.time) : '10';
let max_mistakes =id_code != null && $parcours != null ? String($parcours.max_mistakes): '10';
onMount(() => {
if ($parcours == null && id_code != null && $room != null && $member != null) {
getParcours($room.id_code, id_code, $member.isUser ? $member.clientId : null).then((p) => {
parcours.set(p);
exos.set(p.exercices as ExoSelect[]);
name = p.name;
time = p.time;
max_mistakes = p.validate_condition;
});
}
});
</script>
<div class="create">
<div class="exos">
<ExerciceSelector {exos} />
</div>
<form
class="options"
on:submit|preventDefault={() => {
if (!$room) return;
if ($parcours == null || id_code== null) {
createParcours(
$room?.id_code,
{
time: parseInt(time),
name,
max_mistakes: parseInt(max_mistakes),
exercices: [
...$exos.map((e) => {
return {
exercice_id: e.exercice_id,
quantity: typeof e.quantity != 'number' ? parseInt(e.quantity) : e.quantity
};
})
]
},
!$isAuth ? $member?.clientId : null
).then((r)=>{
parcours.set(r)
$page.url.searchParams.set('p', r.id_code);
goto(`?${$page.url.searchParams.toString()}`);
}).catch((r) => {
error('Echec lors de la création du parcours', `Raison: ${r.detail}`);
});
} else if($parcours != null && $parcours.id_code == id_code){
updateParcours(
$room?.id_code,
$parcours.id_code,
{
time: parseInt(time),
name,
max_mistakes: parseInt(max_mistakes),
exercices: [
...$exos.map((e) => {
return {
exercice_id: e.exercice_id,
quantity: typeof e.quantity != 'number' ? parseInt(e.quantity) : e.quantity
};
})
]
},
!$isAuth ? $member?.clientId : null
)
.then((r) => {
console.log(r);
parcours.set(r);
$page.url.searchParams.delete('edit');
goto(`?${$page.url.searchParams.toString()}`);
})
.catch((r) => {
console.log(r);
error('Echec lors de la modification du parcours', `Raison: ${r.detail}`);
});
}
}}
>
<h1>Nouveau parcours</h1>
<InputWithLabel label="Nom" bind:value={name} required min="5" autofocus/>
<InputWithLabel label="Temps (min)" bind:value={time} type="number" />
<InputWithLabel label="Nombre maximum de fautes" bind:value={max_mistakes} type="number" />
<div class="btns">
<button class="primary-btn">Valider</button>
<button
class="danger-btn"
on:click|preventDefault={() => {
if ($parcours != null && id_code !=null) {
$page.url.searchParams.delete('edit');
goto(`?${$page.url.searchParams.toString()}`);
return
}
console.log('CLICKED');
$page.url.searchParams.delete('p');
goto(`?${$page.url.searchParams.toString()}`);
}}>Annuler</button
>
</div>
</form>
</div>
<style lang="scss">
.create {
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 30px;
height: 100%;
}
.options {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
margin-top: 30px;
}
</style>

View File

@ -0,0 +1,261 @@
<script lang="ts">
import type { Member, ParcoursRead, Room } from '../../types/room.type';
import { getContext, onDestroy } from 'svelte';
import type { Writable } from 'svelte/store';
import { getParcours } from '../../requests/room.request';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { parseTimer } from '../../utils/utils';
import IoIosArrowDown from 'svelte-icons/io/IoIosArrowDown.svelte';
import IoMdOpen from 'svelte-icons/io/IoMdOpen.svelte';
import FaTimes from 'svelte-icons/fa/FaTimes.svelte';
import FaEdit from 'svelte-icons/fa/FaEdit.svelte';
import { messages, handlers } from '../../store/ws';
import Stats from './Stats.svelte';
import ChallengesList from './ChallengesList.svelte';
export let id_code: string;
const room: Writable<Room> = getContext('room');
const member: Writable<Member> = getContext('member');
const { send } = getContext<{ send: Function }>('ws');
const parcours: Writable<ParcoursRead | null> = getContext('parcours');
let open = '';
$: ($parcours == null || (parcours != null && $parcours.id_code != id_code)) &&
getParcours($room.id_code, id_code, $member.isUser ? $member.clientId : null).then((p) => {
console.log('SETPARC');
parcours.set(p);
send('sub_parcours', { parcours_id: p.id_code });
});
let tab = 'stats';
</script>
<div class="parcours">
{#if $parcours != null}
<div class="title">
<h1>{$parcours.name}</h1>
<div>
{#if !!$member.isAdmin}
<div
class="icon edit"
on:click={() => {
goto(`?${new URLSearchParams({p: id_code, edit: "1"}).toString()}`);
}}
on:keydown={()=>{}}
>
<FaEdit />
</div>
{/if}
<div
class="icon back"
on:click={() => {
//$page.url.searchParams.delete('p');
goto(`?${new URLSearchParams().toString()}`);
//parcours.set(null)
}}
on:keydown={()=>{}}
>
<FaTimes />
</div>
</div>
</div>
<div class="main">
<div class="tabs">
<div
class="tab"
class:active={tab == 'stats'}
on:click={() => {
tab = 'stats';
}}
on:keydown={() => {}}
>
Statistiques
</div>
<div
class="tab"
class:active={tab == 'infos'}
on:click={() => {
tab = 'infos';
}}
on:keydown={() => {}}
>
Informations
</div>
</div>
{#if tab == 'infos'}
<div class="infos">
<div>
<h2>Informations</h2>
<p><span class="strong">Temps imparti :</span> {parseTimer($parcours.time)}</p>
<p>
<span class="strong">Nombre de fautes maximal :</span>
{$parcours.max_mistakes}
</p>
</div>
<div class="exos">
<h2>Exercices</h2>
{#each $parcours.exercices as e}
<div class="exo">
<p>{e.name} <span class="quantity">x {e.quantity}</span></p>
<div class="examples">
{#each e.examples.data as ex}
<p>{ex.calcul}</p>
{/each}
</div>
</div>
{/each}
</div>
</div>
{/if}
{#if tab == 'stats'}
<div class="stats">
<Stats />
</div>
<div class="trylist">
<h2>Résumé des essais</h2>
<ChallengesList />
</div>
{/if}
</div>
<div class="btn">
<button
on:click={() => {
if ($parcours == null) return;
$page.url.searchParams.set('c', $parcours.id_code);
$page.url.searchParams.delete('p');
$page.url.searchParams.delete('corr');
goto(`?${$page.url.searchParams.toString()}`);
}}
class="primary-btn">Essayer</button
>
</div>
{/if}
</div>
<style lang="scss">
.main {
background-color: $background;
padding: 50px;
border-radius: 5px;
border: 1px solid $border;
display: flex;
flex-direction: column;
gap: 20px;
position: relative;
padding-top: 100px;
}
.tabs {
position: absolute;
display: flex;
top: 0;
left: 0;
width: 100%;
border-bottom: 1px solid $border;
.tab {
flex-grow: 1;
text-align: center;
padding: 20px 0;
cursor: pointer;
}
}
.active {
color: $primary;
border-bottom: 1px solid $primary;
font-weight: 600;
}
.icon {
display: flex;
align-items: center;
width: 20px;
}
.trylist {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 40px;
}
.exo {
margin-bottom: 10px;
margin-left: 10px;
> p {
font-weight: 600;
font-size: 1.1em;
span.quantity {
font-weight: 400;
font-size: 0.9em;
color: grey;
}
}
}
.examples {
margin-left: 10px;
display: flex;
flex-direction: column;
gap: 5px;
p {
color: grey;
}
}
.infos {
display: flex;
flex-direction: column;
gap: 10px;
}
.parcours {
display: flex;
flex-direction: column;
gap: 20px;
align-items: stretch;
}
.btn {
text-align: center;
width: 100%;
}
.title {
display: flex;
align-items: center;
justify-content: space-between;
> div {
display: flex;
align-items: center;
gap: 10px;
}
.icon {
cursor: pointer;
&:hover {
transform: scale(1.1);
}
}
}
.back {
color: $red;
}
.edit {
color: $green;
}
</style>

View File

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

View File

@ -0,0 +1,149 @@
<script lang="ts">
import { getContext } from 'svelte';
import FaRegTrashAlt from 'svelte-icons/fa/FaRegTrashAlt.svelte';
import FaUndo from 'svelte-icons/fa/FaUndo.svelte';
import FaTimes from 'svelte-icons/fa/FaTimes.svelte';
import { goto } from '$app/navigation';
import type { Writable } from 'svelte/store';
import type { Room, Member } from '../../types/room.type';
import type ReconnectingWebSocket from 'reconnecting-websocket';
const room: Writable<Room> = getContext('room');
const member: Writable<Member>= getContext('member');
const { send, ws } = getContext<{send: Function, ws: ReconnectingWebSocket}>('ws');
let editing = false;
let name = $room.name;
let r: HTMLInputElement;
</script>
<div class="head">
<div class="title">
{#if !editing}
<h1
on:dblclick={() => {
if(!$member.isAdmin) return
editing = true;
name = $room.name;
r.focus();
console.log("OPENED")
}}
>
{$room.name}<span on:dblclick|stopPropagation>#{$room.id_code}</span>
</h1>
{/if}
<input
type="text"
class="input"
class:hide={!editing}
on:focusout={() => {
editing = false;
}}
bind:value={name}
bind:this={r}
on:keydown={(e) => {
if (e.key == 'Escape') {
editing = false;
} else if (e.key == 'Enter') {
send('set_name', { name });
editing = false;
}
}}
/>
</div>
<div class="icons">
{#if $member.isAdmin}
<div class="icon trash" data-testid="delete"><FaRegTrashAlt /></div>
{/if}
<div
class="icon refresh"
on:click={() => {
console.log(ws)
ws.reconnect();
}}
on:keydown={() => {}}
data-testid="refresh"
>
<FaUndo />
</div>
<div
data-testid="leave"
class="icon trash"
on:click={() => {
ws.close();
goto('/room/join')
}}
on:keydown={() => {}}
>
<FaTimes />
</div>
</div>
</div>
<style lang="scss">
.hide {
width: 0;
height: 0;
padding: 0;
border: none;
opacity: 0;
}
.title {
max-width: 50%;
margin: 10px 0;
font-size: 2em;
// height: 50px;
input {
font-size: inherit;
font-weight: 700;
transition: all 0s;
transition: border 0.3s;
padding-left: 0;
padding-top: 0;
}
h1 {
margin: 0;
font-weight: 700;
font-size: inherit;
span {
font-size: 0.5em;
color: grey;
}
}
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
}
.icon {
width: 20px;
height: 20px;
transition: 0.2s;
cursor: pointer;
display: flex;
&:hover {
transform: scale(1.1);
}
}
.trash {
color: $red;
}
.refresh {
color: $contrast;
&:hover {
transform: rotate(-360deg);
}
}
.icons {
display: flex;
gap: 20px;
align-items: center;
}
</style>

View File

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

View File

@ -1,40 +1,114 @@
<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 });
import {getContext, onMount, setContext} from 'svelte';
import {writable} from 'svelte/store';
import {
loginRequest,
refreshRequest,
registerRequest,
logoutRequest
} from '../requests/auth.request';
import jwt_decode from 'jwt-decode';
import {checkExpire} from '../utils/utils';
import {goto} from '$app/navigation';
import {page} from "$app/stores";
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)
}
});
const username = writable<string | null>(null);
const isAuth = writable(false);
const initialLoading = writable(true);
const getTokens = () => {
return {access: localStorage.getItem('token'), refresh: localStorage.getItem('refresh')};
};
const {
alert,
info,
success,
error
} = getContext<{ alert: Function, info: Function, success: Function, error: Function }>('notif');
const login = (username: string, password: string) => {
return loginRequest({username, password})
.then((r) => {
localStorage.setItem('token', `${r.access_token}`);
localStorage.setItem('refresh', `${r.refresh_token}`);
const {username: name} = jwt_decode<{ username: string }>(r.access_token);
$username = name;
$isAuth = true;
success('Connexion', `Connecté en tant que **${username}** !`);
goto('/dashboard');
})
.catch((r) => {
error('Connexion', 'Erreur lors de la connexion !');
throw r.response;
});
};
const register = (username: string, password: string, confirm: string) => {
return registerRequest({username, password, password_confirm: confirm})
.then((r) => {
localStorage.setItem('token', `${r.access_token}`);
localStorage.setItem('refresh', `${r.refresh_token}`);
const {username: name} = jwt_decode<{ username: string }>(r.access_token);
$isAuth = true;
$username = name;
success('Inscription', `Connecté en tant que **${username}** !`);
goto('/dashboard');
})
.catch((r) => {
error('Deconnexion', "Erreur lors de l'inscription !");
throw r.response;
});
};
const logout = () => {
const {access, refresh} = getTokens();
if (isAuth && access != null) {
logoutRequest()
.then(() => {
$isAuth = false;
$username = null;
localStorage.removeItem('token');
localStorage.removeItem('refresh');
localStorage.removeItem('reconnect');
success('Déconnexion', 'Déconnecté !');
if($page.url.href.indexOf('room')> -1){
goto('/room')
}
})
.catch(() => {
error('Déconnexion', 'Erreur lors de la déconnexion !');
});
}
};
setContext('auth', {username, isAuth, login, register, logout, initialLoading});
onMount(() => {
const {access, refresh} = getTokens();
if (access != null) {
const {exp, username: name} = jwt_decode<{ username: string, exp: number }>(access);
if (checkExpire(exp) && refresh != null) {
refreshRequest(refresh).then((r) => {
localStorage.setItem('token', r.access_token);
$username = username;
$isAuth = true;
});
$initialLoading = false;
return;
}
$isAuth = true;
$username = name;
}
$initialLoading = false;
});
</script>
<p>{$isAuth ? 'Connecté' : 'Non connecté'}</p>
<slot />
<p>{$isAuth ? $username : 'Non connecté'}</p>
{#if !$initialLoading}
<slot/>
{/if}

View File

@ -27,9 +27,6 @@
onClose = newOnClose;
}
function addContext (key: string, value: any) {
setContext(key, value)
}
function close() {
visible = false;
@ -39,9 +36,10 @@
component = undefined;
props = {};
closed = true;
}, 500);
}
setContext('modal', { show, close, addContext });
setContext('modal', { show, close });
function keyPress(e: KeyboardEvent) {
console.log('HOP');
if (e.key == 'Escape' && visible == true) {

View File

@ -5,8 +5,15 @@
import { page } from '$app/stores';
import { base } from '$app/paths';
let previous: string | null = base;
let previous: string[] = [base];
let first = true;
const insertUrl = (url: string, id: number = -1)=>{
if(id >= 0){
previous.splice(id, 0, `${base}/${url}`)}
else if(id == -1){
previous.push(`${base}/${url}`)
}
}
const navigate = (
url: string | number,
params: object | undefined,
@ -23,20 +30,27 @@
if (browser) {
console.log('PREVIOUS', previous, typeof url == 'number', previous);
if (typeof url == 'number' && previous != null) {
goto(previous);
const id = Math.abs(url);
if (previous.length > id) {
goto(previous.reverse()[id-1]);
} else{
goto(previous.reverse()[0])
}
} else {
const parsedParams = new URLSearchParams(params as Record<string, string>);
goto(`${url}?${parsedParams.toString()}`, { ...options });
}
first = false;
}
};
afterNavigate(({ from }) => {
previous = from?.url.toString() || previous;
if (from ) {
previous.push(from?.url.toString());
}
});
setContext('navigation', { navigate });
setContext('navigation', { navigate, insertUrl });
</script>
<slot />

View File

@ -0,0 +1,160 @@
<script lang="ts">
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
import SvelteMarkdown from 'svelte-markdown'
type Notif = {
title: string;
description: string;
type: 'alert' | 'info' | 'success' | 'error';
};
type Notification = {
title: string;
description: string;
type: 'alert' | 'info' | 'success' | 'error';
id: number;
deleted: boolean;
};
const notifications = writable<Notification[]>([]);
const getId = () => {
return Math.round(Date.now() * Math.random());
};
const toast = (notif: Notif) => {
let id = getId();
notifications.update((n) => {
return [...n, { ...notif, id, deleted: false }];
});
setTimeout(() => {
notifications.update((n) => {
return n.map((o) => {
if (o.id == id) {
return { ...o, deleted: true };
}
return o;
});
});
setTimeout(() => {
notifications.update((n) => {
return n.filter((o) => o.id != id);
});
}, 500);
}, 3000);
};
const alert = (title: string, description: string) => {
toast({ title, description, type: 'alert' });
};
const info = (title: string, description: string) => {
toast({ title, description, type: 'info' });
};
const success = (title: string, description: string) => {
toast({ title, description, type: 'success' });
};
const error = (title: string, description: string) => {
toast({ title, description, type: 'error' });
};
setContext('notif', { toast, alert, info, success, error });
</script>
<slot />
<div class="notifs" class:empty ={$notifications.length == 0}>
{#each $notifications as n}
<div
on:click={() => {
n.deleted = true;
setTimeout(() => {
notifications.update((o) => o.filter((i) => i.id != n.id));
}, 500);
}}
class={n.type}
on:keydown={() => {}}
class:deleted={n.deleted}
>
<h1>{n.title}</h1>
<p><SvelteMarkdown source={n.description} /></p>
</div>
{/each}
</div>
<style lang="scss">
.notifs {
position: absolute;
right: 0;
top: 0;
padding: 30px;
gap: 10px;
display: flex;
flex-direction: column;
width: 500px;
z-index: 1000;
div {
background-color: $background;
z-index: 1000;
padding: 10px;
cursor: pointer;
position: relative;
overflow: hidden;
word-wrap: break-word;
h1 {
font-size: 1.1em;
}
p {
font-size: 0.9em;
}
&::before,
&::after {
content: '';
display: block;
height: 3px;
position: absolute;
bottom: 0;
right: 0;
left: 0;
}
&::before {
z-index: 3;
background-color: $background-light;
transition: 3s;
width: 0%;
animation: slide 3s forwards ease-in-out;
}
&::after {
width: 100%;
z-index: 2;
}
&.deleted {
transition: 0.5s;
opacity: 0;
}
}
}
.empty{
z-index: -20;
}
.alert::after{
background-color: $orange;
}
.info::after{
background-color: $bleu;
}
.error::after{
background-color: $red;
}
.success::after{
background-color: $green;
}
@keyframes slide {
from {
width: 0%;
}
to {
width: 100%;
}
}
</style>

32
frontend/src/mixins.scss Normal file
View File

@ -0,0 +1,32 @@
@function strip-unit($number) {
@if type-of($number) == 'number' and not unitless($number) {
@return $number / ($number * 0 + 1);
}
@return $number;
}
@mixin up($size) {
$size: strip-unit($size);
@media (min-width: $size * 1px) {
@content;
}
}
@mixin down($size) {
$size: strip-unit($size);
@media (max-width: $size * 1px) {
@content;
}
}
@mixin between($down, $up) {
$down: strip-unit($down);
$up: strip-unit($up);
@media (min-width: $down * 1px) and (max-width: $up * 1px) {
@content;
}
}

View File

@ -1,42 +1,106 @@
import axios from 'axios';
import {authInstance} from '../apis/auth.api';
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 loginRequest = (data: { username: string; password: string }) => {
return authInstance
.request({
url: 'http://localhost:8002/login',
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 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 registerRequest = (data: {
username: string;
password: string;
password_confirm: string;
}) => {
return authInstance
.request({
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: {
return authInstance
.request({
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;
});
Authorization: `Bearer ${token}`
}
})
.then((r) => r.data as { access_token: string })
.catch((e) => {
throw e;
});
};
export const logoutRequest = () => {
return authInstance
.request({
url: '/logout',
method: 'POST'
})
.then((r) => r.data as { access_token: string })
.catch((e) => {
throw e;
});
};
export const dashBoardRequest = () => {
return authInstance
.request({
url: '/user',
method: 'GET'
})
.then((r) => r.data)
.catch(console.log);
}
export const updateUserRequest = (data: { username: string, email: string | null, firstname: string | null, name: string | null }) => {
return authInstance
.request({
url: '/user',
method: 'PUT',
data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
.then((r) => r.data)
.catch((r)=>{throw r});
}
export const updatePassword = (data: {
old_password: string, password: string,
password_confirm: string
}) => {
return authInstance
.request({
url: '/user/password',
method: 'PUT',
data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
.then((r) => r.data)
.catch((r)=>{throw r});
}

View File

@ -131,4 +131,29 @@ export const getExoSource = (id_code: string,) => {
link.click();
link.remove();
});
};export const generateRequest = (id_code: string,filename: string) => {
return exoInstance({
url: `/generator/csv/${id_code}/`,
method: 'Get',
params: {filename}
}).then((r) => {
const contentDisposition = r.headers['content-disposition'] || "filename=untitled.csv";
const splitted = contentDisposition.split('filename=')
let filename = "untitled.csv"
if(splitted.length >= 1) {
filename = splitted[1]
}
const blob = new Blob([r.data], {
type: 'text/csv'
});
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

@ -0,0 +1,158 @@
import type { ExoSelect } from '../types/room.type';
import { roomInstance } from '../apis/room.api';
export const createRoom = (data: { name: string }, username: string | null = null) => {
return roomInstance
.request({
url: '/',
method: 'POST',
params: { username },
data: { ...data, public: false, global_results: false }
})
.then((r) => r.data);
};
export const getRoom = (id_code: string, clientId: string | null = null) => {
console.log('GETROOM', clientId, { ...(clientId != null && { clientId }) });
return roomInstance
.request({
url: '/' + id_code,
method: 'GET',
params: { ...(clientId != null && { clientId }) }
})
.then((r) => r.data);
};
export const createParcours = (
id_code: string,
parcours: { time: number; name: string; max_mistakes: number; exercices: {exercice_id: string, quantity:number}[] },
clientId: string | null = null
) => {
return roomInstance
.request({
url: `/${id_code}/parcours`,
method: 'POST',
params: { ...(clientId != null && { clientId }) },
data: parcours
})
.catch((r) => {
throw r.response.data;
})
.then((r) => r.data);
};
export const updateParcours = (
id_code: string,
parcours_id: string,
parcours: { time: number; name: string; max_mistakes: number; exercices: {exercice_id: string, quantity:number}[] },
clientId: string | null = null
) => {
return roomInstance
.request({
url: `/${id_code}/parcours/${parcours_id}`,
method: 'PUT',
params: { ...(clientId != null && { clientId }) },
data: parcours
})
.catch((r) => {
throw r.response.data;
})
.then((r) => r.data);
};
export const getParcours = (
id_code: string,
parcours_id: string,
clientId: string | null = null
) => {
return roomInstance
.request({
url: `/${id_code}/parcours/${parcours_id}`,
method: 'GET',
params: { ...(clientId != null && { clientId }) }
})
.catch((r) => {
throw r.response.data;
})
.then((r) => r.data);
};
export const challenge = (id_code: string, parcours_id: string, clientId: string | null = null) => {
return roomInstance
.request({
url: `/${id_code}/challenge/${parcours_id}`,
method: 'GET',
params: { ...(clientId != null && { clientId }) }
})
.catch((r) => {
throw r.response.data;
})
.then((r) => r.data);
};
export const sendChallenge = (
id_code: string,
parcours_id: string,
challenge_id: string,
data,
clientId: string | null = null
) => {
return roomInstance
.request({
url: `/${id_code}/challenge/${parcours_id}/${challenge_id}`,
method: 'POST',
params: { ...(clientId != null && { clientId }) },
data
})
.catch((r) => {
throw r.response.data;
})
.then((r) => r.data);
};
export const getChallenge = (
id_code: string,
challenge_id: string,
clientId: string | null = null
) => {
return roomInstance
.request({
url: `/${id_code}/correction/${challenge_id}`,
method: 'GET',
params: { ...(clientId != null && { clientId }) },
})
.catch((r) => {
throw r.response.data;
})
.then((r) => r.data);
};
export const corrigeChallenge = (
id_code: string,
challenge_id: string,
data,
clientId: string | null = null
) => {
return roomInstance
.request({
url: `/${id_code}/correction/${challenge_id}`,
method: 'PUT',
params: { ...(clientId != null && { clientId }) },
data
})
.catch((r) => {
throw r.response.data;
})
.then((r) => r.data);
};
export const delParcours = (
id_code: string,
parcours_id: string,
clientId: string | null = null
) => {
return roomInstance
.request({
url: `/${id_code}/parcours/${parcours_id}`,
method: 'DELETE',
params: { ...(clientId != null && { clientId }) }
})
.then((r) => r.data);
};

View File

@ -1,89 +1,98 @@
<script>
import Modal from '../context/Modal.svelte';
import NavLink from '../components/NavLink.svelte';
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 lang="ts">
import Modal from '../context/Modal.svelte';
import NavLink from '../components/NavLink.svelte';
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';
import Notification from '../context/Notification.svelte';
import {getContext} from "svelte";
import type {Writable} from "svelte/store";
import NavBar from "../components/NavBar.svelte";
</script>
<Navigation>
<QueryClientProvider client={queryClient}>
<Auth>
<Alert>
<Modal>
<main>
<nav data-sveltekit-preload-data="hover">
<NavLink href="/" exact>Home</NavLink>
<NavLink href="/exercices" exact>Exercices</NavLink>
<NavLink href="/settings" exact>Settings</NavLink>
</nav>
<slot />
</main>
</Modal>
</Alert>
</Auth>
</QueryClientProvider>
</Navigation>
<Notification>
<Navigation>
<Auth>
<Alert>
<Modal>
<main>
<NavBar/>
<slot/>
</main>
</Modal>
</Alert>
</Auth>
</Navigation>
</Notification>
<style lang="scss">
@import '../variables';
.links {
display: flex;
align-items: center;
gap: 14px;
overflow: hidden;
flex-wrap: wrap;
height: 30px;
& li {
height: 30px;
display: flex;
align-items: center;
white-space: nowrap;
}
}
@import '../variables';
@import "../mixins";
:root {
--container-padding: 20px;
--container-width: 1330px;
}
:global(body) {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
overflow: hidden;
background-color: $background;
color: #d9d9d9;
background: linear-gradient(to bottom left, $background-dark 30%, $background-light);
width: 100vw;
height: 100vh;
}
main {
box-sizing: border-box;
width: 100%;
padding-left: calc(50% - var(--container-width) / 2);
padding-right: calc(50% - var(--container-width) / 2);
height: calc(100vh - var(--navbar-height) - 10px);
overflow: auto;
height: 100%;
a {
color: red;
}
}
.links {
display: flex;
align-items: center;
gap: 14px;
overflow: hidden;
flex-wrap: wrap;
height: 30px;
nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding: 30px 0;
border-bottom: 1px solid $border;
width: 100%;
gap: 10px;
height: 30px;
}
& li {
height: 30px;
display: flex;
align-items: center;
white-space: nowrap;
}
}
:root {
--container-padding: 20px;
--container-width: 1330px;
}
:global(body) {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
overflow: hidden;
background-color: $background;
color: #d9d9d9;
background: linear-gradient(to bottom left, $background-dark 30%, $background-light);
width: 100vw;
height: 100vh;
}
main {
box-sizing: border-box;
width: 100%;
padding-left: calc(50% - var(--container-width) / 2);
padding-right: calc(50% - var(--container-width) / 2);
height: calc(100vh - var(--navbar-height) - 10px);
overflow: auto;
height: 100%;
a {
color: red;
}
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding: 30px 0;
border-bottom: 1px solid $border;
width: 100%;
gap: 10px;
height: 30px;
}
</style>

Some files were not shown because too many files have changed in this diff Show More