tests
This commit is contained in:
parent
43031d22fb
commit
4941b5a154
9
.idea/fastapi_gen.iml
generated
Normal file
9
.idea/fastapi_gen.iml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
84
.idea/workspace.xml
generated
Normal 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
8
backend/api/.idea/.gitignore
generated
vendored
Normal 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
11
backend/api/.idea/api.iml
generated
Normal 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>
|
5
backend/api/.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
backend/api/.idea/codeStyles/codeStyleConfig.xml
generated
Normal 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
24
backend/api/.idea/dataSources.xml
generated
Normal 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
423
backend/api/.idea/dbnavigator.xml
generated
Normal 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
6
backend/api/.idea/misc.xml
generated
Normal 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
8
backend/api/.idea/modules.xml
generated
Normal 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
6
backend/api/.idea/vcs.xml
generated
Normal 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>
|
@ -1,9 +1,14 @@
|
|||||||
import uuid
|
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 jose import jwt, exceptions
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
from config import SECRET_KEY, ALGORITHM
|
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):
|
def create_user_db(username:str , password: str, db: Session):
|
||||||
user = User(username=username, hashed_password=password, clientId=uuid.uuid4())
|
user = User(username=username, hashed_password=password, clientId=uuid.uuid4())
|
||||||
db.add(user)
|
db.add(user)
|
||||||
@ -84,3 +89,7 @@ def change_user_uuid(id: int, db: Session):
|
|||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
return user.clientId
|
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]
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
from typing import List, Optional
|
import uuid
|
||||||
|
from typing import List
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
import uuid
|
|
||||||
from sqlmodel import Field, SQLModel, Relationship
|
|
||||||
from pydantic import validator, BaseModel
|
from pydantic import validator, BaseModel
|
||||||
|
from sqlmodel import Field, SQLModel, Relationship
|
||||||
|
|
||||||
from services.password import validate_password
|
from services.password import validate_password
|
||||||
from services.schema import as_form
|
from services.schema import as_form
|
||||||
|
|
||||||
@ -29,9 +31,16 @@ class User(UserBase, table=True):
|
|||||||
@as_form
|
@as_form
|
||||||
class UserEdit(UserBase):
|
class UserEdit(UserBase):
|
||||||
pass
|
pass
|
||||||
|
class UsersRoom(BaseModel):
|
||||||
|
name: str
|
||||||
|
id_code: str
|
||||||
|
admin: bool = False
|
||||||
class UserRead(UserBase):
|
class UserRead(UserBase):
|
||||||
id: int
|
id: int
|
||||||
|
rooms: List[UsersRoom] = []
|
||||||
|
class UserEditRead(UserBase):
|
||||||
|
id: int
|
||||||
|
#rooms: List[UsersRoom] = []
|
||||||
|
|
||||||
|
|
||||||
@as_form
|
@as_form
|
||||||
|
@ -2,7 +2,7 @@ import pydantic.json
|
|||||||
import json
|
import json
|
||||||
from sqlmodel import SQLModel, create_engine, Session, select
|
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}"
|
sqlite_url = f"sqlite:///{sqlite_file_name}"
|
||||||
|
|
||||||
|
|
||||||
@ -19,6 +19,6 @@ def create_db_and_tables():
|
|||||||
SQLModel.metadata.create_all(engine)
|
SQLModel.metadata.create_all(engine)
|
||||||
|
|
||||||
def get_session():
|
def get_session():
|
||||||
with Session(engine) as s:
|
with Session(engine, expire_on_commit=False) as s:
|
||||||
yield s
|
yield s
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ if TYPE_CHECKING:
|
|||||||
from database.auth.models import User
|
from database.auth.models import User
|
||||||
|
|
||||||
|
|
||||||
class ExampleEnum(Enum):
|
class ExampleEnum(str, Enum):
|
||||||
csv = 'csv'
|
csv = 'csv'
|
||||||
pdf = 'pdf'
|
pdf = 'pdf'
|
||||||
web = 'web'
|
web = 'web'
|
||||||
|
@ -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
|
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 pydantic import BaseModel
|
||||||
from sqlmodel import Session, delete, select, col, table
|
from sqlalchemy import func
|
||||||
from database.db import get_session
|
from sqlmodel import Session, delete, select, col
|
||||||
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 database.auth.crud import get_user_from_token
|
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.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):
|
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}
|
return {"room": room_obj, "member": member}
|
||||||
|
|
||||||
|
|
||||||
def change_room_name(room: Room, name: str, db: Session):
|
def change_room_name(room: Room, name: str, db: Session):
|
||||||
room.name = name
|
room.name = name
|
||||||
db.add(room)
|
db.add(room)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(room)
|
db.refresh(room)
|
||||||
return room
|
return room
|
||||||
|
|
||||||
|
|
||||||
def change_room_status(room: Room, public: bool, db: Session):
|
def change_room_status(room: Room, public: bool, db: Session):
|
||||||
room.public = public
|
room.public = public
|
||||||
db.add(room)
|
db.add(room)
|
||||||
@ -51,6 +58,7 @@ def change_room_status(room: Room, public: bool, db: Session):
|
|||||||
db.refresh(room)
|
db.refresh(room)
|
||||||
return room
|
return room
|
||||||
|
|
||||||
|
|
||||||
def get_member_from_user(user_id: int, room_id: int, db: Session):
|
def get_member_from_user(user_id: int, room_id: int, db: Session):
|
||||||
member = db.exec(select(Member).where(Member.room_id ==
|
member = db.exec(select(Member).where(Member.room_id ==
|
||||||
room_id, Member.user_id == user_id)).first()
|
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()
|
Anonymous.reconnect_code == reconnect_code)).first()
|
||||||
return anonymous
|
return anonymous
|
||||||
|
|
||||||
|
|
||||||
def get_anonymous_from_clientId(clientId: str, db: Session):
|
def get_anonymous_from_clientId(clientId: str, db: Session):
|
||||||
anonymous = db.exec(select(Anonymous).where(
|
anonymous = db.exec(select(Anonymous).where(
|
||||||
Anonymous.clientId == clientId)).first()
|
Anonymous.clientId == clientId)).first()
|
||||||
@ -102,20 +111,23 @@ def get_member_from_clientId(clientId: str, room_id: int, db: Session):
|
|||||||
|
|
||||||
def create_member(*, room: Room, user: User | None = None, anonymous: Anonymous | None = None, waiting: bool = False, db: Session):
|
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_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)
|
id_code=member_id)
|
||||||
|
member.online = True
|
||||||
db.add(member)
|
db.add(member)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(member)
|
db.refresh(member)
|
||||||
return 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,
|
||||||
member = user is not None and get_member_from_user(user.id, room.id, db)
|
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:
|
if member is not None and member is not False:
|
||||||
return member
|
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):
|
def connect_member(member: Member, db: Session):
|
||||||
member.online = True
|
member.online = True
|
||||||
@ -128,10 +140,10 @@ def connect_member(member: Member, db: Session):
|
|||||||
def disconnect_member(member: Member, db: Session):
|
def disconnect_member(member: Member, db: Session):
|
||||||
if member.waiting == False:
|
if member.waiting == False:
|
||||||
member.online = False
|
member.online = False
|
||||||
|
|
||||||
if member.anonymous is not None:
|
if member.anonymous is not None:
|
||||||
change_anonymous_clientId(member.anonymous,db)
|
change_anonymous_clientId(member.anonymous, db)
|
||||||
|
|
||||||
db.add(member)
|
db.add(member)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(member)
|
db.refresh(member)
|
||||||
@ -167,6 +179,7 @@ def create_anonymous_member(username: str, room: Room, db: Session):
|
|||||||
db.refresh(member)
|
db.refresh(member)
|
||||||
return member
|
return member
|
||||||
|
|
||||||
|
|
||||||
def create_anonymous(username: str, room: Room, db: Session):
|
def create_anonymous(username: str, room: Room, db: Session):
|
||||||
username = validate_username(username, room, db)
|
username = validate_username(username, room, db)
|
||||||
if username is None:
|
if username is None:
|
||||||
@ -179,10 +192,13 @@ def create_anonymous(username: str, room: Room, db: Session):
|
|||||||
db.refresh(anonymous)
|
db.refresh(anonymous)
|
||||||
return anonymous
|
return anonymous
|
||||||
|
|
||||||
|
|
||||||
def check_user_in_room(user_id: int, room_id: int, db: Session):
|
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
|
return user
|
||||||
|
|
||||||
|
|
||||||
def create_user_member(user: User, room: Room, db: Session):
|
def create_user_member(user: User, room: Room, db: Session):
|
||||||
member = get_member_from_user(user.id, room.id, db)
|
member = get_member_from_user(user.id, room.id, db)
|
||||||
if member is not None:
|
if member is not None:
|
||||||
@ -194,6 +210,7 @@ def create_user_member(user: User, room: Room, db: Session):
|
|||||||
db.refresh(member)
|
db.refresh(member)
|
||||||
return member
|
return member
|
||||||
|
|
||||||
|
|
||||||
def create_anonymous_waiter(username: str, room: Room, db: Session):
|
def create_anonymous_waiter(username: str, room: Room, db: Session):
|
||||||
username = validate_username(username, room, db)
|
username = validate_username(username, room, db)
|
||||||
if username is None:
|
if username is None:
|
||||||
@ -210,6 +227,7 @@ def create_anonymous_waiter(username: str, room: Room, db: Session):
|
|||||||
db.refresh(member)
|
db.refresh(member)
|
||||||
return member
|
return member
|
||||||
|
|
||||||
|
|
||||||
def create_user_waiter(user: User, room: Room, db: Session):
|
def create_user_waiter(user: User, room: Room, db: Session):
|
||||||
member = get_member_from_user(user.id, room.id, db)
|
member = get_member_from_user(user.id, room.id, db)
|
||||||
if member is not None:
|
if member is not None:
|
||||||
@ -219,6 +237,7 @@ def create_user_waiter(user: User, room: Room, db: Session):
|
|||||||
db=db)
|
db=db)
|
||||||
return member
|
return member
|
||||||
|
|
||||||
|
|
||||||
def get_waiter(waiter_code: str, db: Session):
|
def get_waiter(waiter_code: str, db: Session):
|
||||||
return db.exec(select(Member).where(Member.id_code == waiter_code, Member.waiting == True)).first()
|
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):
|
def accept_waiter(member: Member, db: Session):
|
||||||
member.waiting = False
|
member.waiting = False
|
||||||
member.waiter_code = None
|
member.waiter_code = None
|
||||||
|
member.online = True
|
||||||
db.add(member)
|
db.add(member)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(member)
|
db.refresh(member)
|
||||||
@ -254,39 +274,173 @@ def leave_room(member: Member, db: Session):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_member(member: Member, private: bool = False, admin: bool = False,
|
||||||
def serialize_member(member: Member) -> MemberRead | Waiter:
|
m2: Member | None = None) -> MemberRead | Waiter:
|
||||||
member_obj = member.user or member.anonymous
|
member_obj = member.user or member.anonymous
|
||||||
if member.waiting == False:
|
print("OHLA", member_obj, private, member.user_id == None)
|
||||||
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 not member.waiting:
|
||||||
if member.waiting == True:
|
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()
|
return Waiter(username=member_obj.username, waiter_id=member.id_code).dict()
|
||||||
|
|
||||||
|
|
||||||
def serialize_parcours_short(parcours: Parcours, member: Member, db: Session):
|
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()
|
challenger = getChallenger(parcours, member, db)
|
||||||
note = None
|
|
||||||
if best_note is not None:
|
return ParcoursReadShort(name=parcours.name, id_code=parcours.id_code, best_note=challenger.best,
|
||||||
best_note=best_note[0]
|
validated=challenger.validated)
|
||||||
note = Note(note=best_note[0], time=best_note[1])
|
|
||||||
return ParcoursReadShort(**parcours.dict(exclude_unset=True), best_note=note)
|
|
||||||
|
|
||||||
def serialize_challenge(challenge: Challenge):
|
def serialize_challenge(challenge: Challenge):
|
||||||
return Challenges(name=challenge.challenger.user.username if challenge.challenger.user is not None else challenge.challenger.anonymous.username, value=Note(note=challenge.note, time=challenge.time), isCorriged=challenge.isCorriged, canCorrige=challenge.data is not None)
|
return Challenges(
|
||||||
|
name=challenge.challenger.user.username if challenge.challenger.user is not None else challenge.challenger.anonymous.username,
|
||||||
|
value=Note(note=challenge.note, time=challenge.time), isCorriged=challenge.isCorriged,
|
||||||
|
canCorrige=challenge.data is not None)
|
||||||
|
|
||||||
def serialize_parcours_short(parcours: Parcours, member: Member, db: Session):
|
|
||||||
if member.is_member == False:
|
|
||||||
challenges = db.exec(select(Challenge).where(Challenge.parcours_id == parcours.id, Challenge.challenger_id == member.id)).all()
|
|
||||||
else:
|
|
||||||
challenges = db.exec(select(Challenge).where(
|
|
||||||
Challenge.parcours_id == parcours.id)).all()
|
|
||||||
|
|
||||||
challenges = [serialize_challenge(c) for c in challenges]
|
|
||||||
return Parcours(**parcours.dict(), challenges=challenges)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_room(room: Room, member: Member, db: Session):
|
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):
|
def change_anonymous_clientId(anonymous: Anonymous, db: Session):
|
||||||
@ -298,20 +452,49 @@ def change_anonymous_clientId(anonymous: Anonymous, db: Session):
|
|||||||
return anonymous
|
return anonymous
|
||||||
|
|
||||||
|
|
||||||
#Parcours
|
# Parcours
|
||||||
def validate_exercices(exos: List[ExercicesCreate], db: Session ):
|
from services.io import add_fast_api_root
|
||||||
exercices = db.exec(select(Exercice).where(Exercice.web == True).where(col(Exercice.id_code).in_([e.exercice_id for e in exos]))).all()
|
from generateur.generateur_main import generate_from_path, parseGeneratorOut
|
||||||
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]
|
|
||||||
|
|
||||||
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)
|
exercices = validate_exercices(parcours.exercices, db)
|
||||||
if len(exercices) == 0:
|
if len(exercices) == 0:
|
||||||
return "Veuillez entrer au moins un exercice valide"
|
return "Veuillez entrer au moins un exercice valide"
|
||||||
id_code = generate_unique_code(Parcours, s=db)
|
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.add(parcours_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(parcours_obj)
|
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):
|
def deleteParcoursRelated(parcours: Parcours, db: Session):
|
||||||
db.exec(delete(Challenge).where(Challenge.parcours_id == parcours.id_code))
|
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()
|
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):
|
def update_parcours_db(parcours: ParcoursCreate, parcours_obj: Parcours, db: Session):
|
||||||
exercices = validate_exercices(parcours.exercices, db)
|
update_challenges = False
|
||||||
if len(exercices) == 0:
|
|
||||||
return "Veuillez entrer au moins un exercice valide"
|
if not compareExercices(parcours_obj.exercices, parcours.exercices):
|
||||||
|
exercices = validate_exercices(parcours.exercices, db)
|
||||||
parcours_data = parcours.dict(exclude_unset=True)
|
if len(exercices) == 0:
|
||||||
for key, value in parcours_data.items():
|
return "Veuillez entrer au moins un exercice valide"
|
||||||
setattr(parcours_obj, key, value)
|
deleteParcoursRelated(parcours_obj, db)
|
||||||
parcours_obj.exercices = exercices
|
update_challenges = True
|
||||||
|
parcours_obj.exercices = exercices
|
||||||
|
|
||||||
|
if parcours_obj.max_mistakes != parcours.max_mistakes:
|
||||||
|
changeValidation(parcours_obj, parcours.max_mistakes, db)
|
||||||
|
|
||||||
|
parcours_obj.name = parcours.name
|
||||||
|
parcours_obj.time = parcours.time
|
||||||
|
parcours_obj.max_mistakes = parcours.max_mistakes
|
||||||
|
|
||||||
db.add(parcours_obj)
|
db.add(parcours_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
deleteParcoursRelated(parcours_obj, db)
|
|
||||||
db.refresh(parcours_obj)
|
db.refresh(parcours_obj)
|
||||||
|
|
||||||
return parcours_obj
|
return parcours_obj, update_challenges
|
||||||
|
|
||||||
|
|
||||||
def delete_parcours_db(parcours: Parcours, db: Session):
|
def delete_parcours_db(parcours: Parcours, db: Session):
|
||||||
db.delete(parcours)
|
db.delete(parcours)
|
||||||
@ -347,138 +586,210 @@ def delete_parcours_db(parcours: Parcours, db: Session):
|
|||||||
|
|
||||||
class CorrigedChallenge(BaseModel):
|
class CorrigedChallenge(BaseModel):
|
||||||
data: List[List[CorrigedGeneratorOut]]
|
data: List[List[CorrigedGeneratorOut]]
|
||||||
note: Note
|
mistakes: int
|
||||||
isCorriged: bool
|
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)
|
code = generate_unique_code(TmpCorrection, s=db)
|
||||||
tmpCorr = TmpCorrection(data=data, id_code=code,
|
tmpCorr = TmpCorrection(data=data, id_code=code,
|
||||||
member=member, parcours_id=parcours_id)
|
member=member, parcours_id=parcours_id)
|
||||||
db.add(tmpCorr)
|
db.add(tmpCorr)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(tmpCorr)
|
db.refresh(tmpCorr)
|
||||||
|
|
||||||
return 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)
|
def validate_challenge_input(obj: List[CorrectionData], corr: TmpCorrection):
|
||||||
db.commit()
|
|
||||||
db.refresh(challenge)
|
|
||||||
|
|
||||||
return challenge
|
|
||||||
|
|
||||||
|
|
||||||
def validate_challenge_input(obj: List[List[ParsedGeneratorOut]], corr: TmpCorrection):
|
|
||||||
data = corr.data
|
data = corr.data
|
||||||
if len(obj) != len(data):
|
if len(obj) != len(data):
|
||||||
return False
|
return False
|
||||||
for i in range(len(data)):
|
for i in range(len(data)):
|
||||||
exo_corr = data[i]
|
exo_corr = data[i]
|
||||||
exo = obj[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
|
return
|
||||||
zipped = zip(exo_corr, exo)
|
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])
|
same = all([e['calcul'] == f.calcul and len(e['inputs'])
|
||||||
|
== len(f.inputs) for e, f in zipped])
|
||||||
if not same:
|
if not same:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def validate_challenge_correction(obj: List[List[CorrigedGeneratorOut]], chall: Challenge):
|
|
||||||
|
def validate_challenge_correction(obj: List[CorrigedData], chall: Challenge):
|
||||||
data = chall.data
|
data = chall.data
|
||||||
if len(obj) != len(data):
|
if len(obj) != len(data):
|
||||||
return False
|
return False
|
||||||
for i in range(len(data)):
|
for i in range(len(data)):
|
||||||
exo_corr = data[i]
|
exo_corr = data[i]
|
||||||
exo = obj[i]
|
exo = obj[i]
|
||||||
if len(exo) != len(exo_corr):
|
if len(exo.data) != len(exo_corr['data']):
|
||||||
return
|
return
|
||||||
zipped = zip(exo_corr, exo)
|
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])
|
same = all([e['calcul'] == f.calcul and len(e['inputs'])
|
||||||
|
== len(f.inputs) for e, f in zipped])
|
||||||
if not same:
|
if not same:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def corrige_challenge(obj: List[List[ParsedGeneratorOut]], corr: TmpCorrection) -> CorrigedChallenge:
|
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
|
return None
|
||||||
|
|
||||||
data = corr.data
|
data = corr.data
|
||||||
note = 0
|
note = 0
|
||||||
total = 0
|
total = 0
|
||||||
isCorriged = True
|
isCorriged = True
|
||||||
|
mistakes = 0
|
||||||
for i in range(len(data)):
|
for i in range(len(data)):
|
||||||
exo_corr = data[i]
|
exo_corr = data[i]["data"]
|
||||||
exo = obj[i]
|
exo = obj[i].data
|
||||||
if len(exo) != len(exo_corr):
|
if len(exo) != len(exo_corr):
|
||||||
return
|
return
|
||||||
zipped = zip(exo_corr, exo)
|
zipped = zip(exo_corr, exo)
|
||||||
for e, f in zipped:
|
for e, f in zipped:
|
||||||
|
print("HO\n\n")
|
||||||
for k, l in zip(e['inputs'], f.inputs):
|
for k, l in zip(e['inputs'], f.inputs):
|
||||||
k["value"] = str(l.value)
|
k["value"] = str(l.value)
|
||||||
total += 1
|
total += 1
|
||||||
if k['correction'] is None:
|
if k['correction'] is None:
|
||||||
isCorriged = False
|
isCorriged = False
|
||||||
if str(k["correction"]) == str(l.value):
|
k['valid'] = None
|
||||||
|
|
||||||
|
elif str(k["correction"]) == str(l.value):
|
||||||
|
k['valid'] = True
|
||||||
note += 1
|
note += 1
|
||||||
|
else:
|
||||||
return {"data": data, "note": {"value": 1, "total": 3}, "isCorriged": isCorriged}
|
k['valid'] = False
|
||||||
|
mistakes += 1
|
||||||
|
|
||||||
|
return {"data": data, "mistakes": mistakes, "isCorriged": isCorriged}
|
||||||
def change_correction(obj: List[List[CorrigedGeneratorOut]], chall: Challenge) -> CorrigedChallenge:
|
return {"data": data, "mistakes": mistakes, "note": {"value": note, "total": total}, "isCorriged": isCorriged}
|
||||||
|
|
||||||
|
|
||||||
|
def change_correction(obj: List[CorrigedData], chall: Challenge) -> CorrigedChallenge:
|
||||||
if validate_challenge_correction(obj, chall) is False:
|
if validate_challenge_correction(obj, chall) is False:
|
||||||
return None
|
return None
|
||||||
data = deepcopy(chall.data)
|
data = deepcopy(chall.data)
|
||||||
note = 0
|
note = 0
|
||||||
total = 0
|
total = 0
|
||||||
isCorriged = True
|
isCorriged = True
|
||||||
|
mistakes = 0
|
||||||
for i in range(len(data)):
|
for i in range(len(data)):
|
||||||
exo_corr = data[i]
|
exo_corr = data[i]['data']
|
||||||
exo = obj[i]
|
exo = obj[i].data
|
||||||
if len(exo) != len(exo_corr):
|
if len(exo) != len(exo_corr):
|
||||||
return
|
return
|
||||||
zipped = zip(exo_corr, exo)
|
zipped = zip(exo_corr, exo)
|
||||||
for e, f in zipped:
|
for e, f in zipped:
|
||||||
for k, l in zip(e['inputs'], f.inputs):
|
for k, l in zip(e['inputs'], f.inputs):
|
||||||
k["correction"] = str(l.correction)
|
k["correction"] = l.correction
|
||||||
|
k["valid"] = l.valid
|
||||||
total += 1
|
total += 1
|
||||||
if k['correction'] is None:
|
if k['correction'] is None and l.valid is None:
|
||||||
isCorriged = False
|
isCorriged = False
|
||||||
if str(k["correction"]) == str(l.value):
|
if l.valid is True:
|
||||||
note += 1
|
note += 1
|
||||||
|
else:
|
||||||
|
mistakes += 1
|
||||||
|
|
||||||
|
return {"data": data, "mistakes": mistakes, "isCorriged": isCorriged}
|
||||||
return {"data": data, "note": {"value": note, "total": total}, "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']) >=
|
def ChallengerFromChallenge(c: Challenge, db: Session):
|
||||||
parcours.validate_condition, isCorriged=isCorriged, id_code=generate_unique_code(Challenge, s=db))
|
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(challenge)
|
||||||
|
db.add(challenger_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(challenge)
|
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):
|
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.data = corriged['data']
|
||||||
challenge.note = corriged['note']
|
challenge.mistakes = corriged['mistakes']
|
||||||
challenge.isCorriged = corriged['isCorriged']
|
challenge.isCorriged = corriged['isCorriged']
|
||||||
challenge.validated = noteOn20(
|
# challenge.validated = corriged['mistakes'] <= parcours.max_mistakes
|
||||||
corriged['note']['value'], corriged['note']['total']) > challenge.parcours.validate_condition
|
|
||||||
|
|
||||||
db.add(challenge)
|
db.add(challenge)
|
||||||
|
db.add(challenger)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(challenge)
|
db.refresh(challenge)
|
||||||
|
db.refresh(challenger)
|
||||||
|
|
||||||
return challenge
|
return challenge, challenger
|
||||||
|
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
@ -489,8 +800,6 @@ def check_room(room_id: str, db: Session = Depends(get_session)):
|
|||||||
return room
|
return room
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_room(room_id, db: Session = Depends(get_session)):
|
def get_room(room_id, db: Session = Depends(get_session)):
|
||||||
room = db.exec(select(Room).where(Room.id_code == room_id)).first()
|
room = db.exec(select(Room).where(Room.id_code == room_id)).first()
|
||||||
if room is None:
|
if room is None:
|
||||||
@ -499,13 +808,14 @@ def get_room(room_id, db: Session = Depends(get_session)):
|
|||||||
return room
|
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:
|
if user is None and clientId is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
|
||||||
if user is not None:
|
if user is not None:
|
||||||
member = get_member_from_user(user.id, room.id, db)
|
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)
|
member = get_member_from_clientId(clientId, room.id, db)
|
||||||
|
|
||||||
if member is None:
|
if member is None:
|
||||||
@ -550,11 +860,9 @@ def get_correction(correction_id: str, parcours_id: str, member: Member = Depend
|
|||||||
return tmpCorr
|
return tmpCorr
|
||||||
|
|
||||||
|
|
||||||
|
def get_challenge(challenge_id: str, db: Session = Depends(get_session)):
|
||||||
def get_challenge(challenge_id: str, parcours_id: str, db: Session = Depends(get_session)):
|
|
||||||
|
|
||||||
challenge = db.exec(select(Challenge).where(
|
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:
|
if challenge is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Challenge introuvable")
|
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")
|
status_code=status.HTTP_400_BAD_REQUEST, detail="Impossible de corriger ce challenge")
|
||||||
|
|
||||||
return challenge
|
return challenge
|
||||||
|
|
||||||
|
@ -2,214 +2,325 @@ from uuid import UUID, uuid4
|
|||||||
from pydantic import root_validator, BaseModel
|
from pydantic import root_validator, BaseModel
|
||||||
import pydantic.json
|
import pydantic.json
|
||||||
from typing import List, Optional, TYPE_CHECKING
|
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
|
from database.auth.models import UserRead
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from database.auth.models import User
|
from database.auth.models import User
|
||||||
|
|
||||||
|
|
||||||
class RoomBase(SQLModel):
|
class RoomBase(SQLModel):
|
||||||
name: str = Field(max_length=20)
|
name: str = Field(max_length=20)
|
||||||
public: bool = Field(default=False)
|
public: bool = Field(default=False)
|
||||||
global_results: bool = Field(default=False)
|
global_results: bool = Field(default=False)
|
||||||
|
|
||||||
|
|
||||||
class RoomCreate(RoomBase):
|
class RoomCreate(RoomBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Room(RoomBase, table=True):
|
class Room(RoomBase, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
id_code: str = Field(index=True)
|
id_code: str = Field(index=True)
|
||||||
|
|
||||||
members: List['Member'] = Relationship(back_populates="room")
|
members: List['Member'] = Relationship(back_populates="room")
|
||||||
parcours: List['Parcours'] = Relationship(back_populates="room")
|
parcours: List['Parcours'] = Relationship(back_populates="room")
|
||||||
|
|
||||||
|
|
||||||
class AnonymousBase(SQLModel):
|
class AnonymousBase(SQLModel):
|
||||||
username: str = Field(max_length=20)
|
username: str = Field(max_length=20)
|
||||||
|
|
||||||
|
|
||||||
class AnonymousCreate(AnonymousBase):
|
class AnonymousCreate(AnonymousBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Anonymous(AnonymousBase, table=True):
|
class Anonymous(AnonymousBase, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
reconnect_code: str = Field(index=True)
|
reconnect_code: str = Field(index=True)
|
||||||
|
|
||||||
clientId: Optional[UUID] = Field(default=uuid4(), index=True)
|
clientId: Optional[UUID] = Field(default=uuid4(), index=True)
|
||||||
member: 'Member' = Relationship(back_populates="anonymous")
|
member: 'Member' = Relationship(back_populates="anonymous")
|
||||||
|
|
||||||
|
|
||||||
class Member(SQLModel, table = True):
|
class Member(SQLModel, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
id_code: str = Field(index=True)
|
id_code: str = Field(index=True)
|
||||||
|
|
||||||
user_id: Optional[int] = Field(foreign_key="user.id", default=None)
|
user_id: Optional[int] = Field(foreign_key="user.id", default=None)
|
||||||
user: Optional["User"] = Relationship(back_populates='members')
|
user: Optional["User"] = Relationship(back_populates='members')
|
||||||
|
|
||||||
anonymous_id: Optional[int] = Field(foreign_key="anonymous.id", default=None)
|
anonymous_id: Optional[int] = Field(
|
||||||
anonymous: Optional[Anonymous] = Relationship(back_populates="member")
|
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')
|
||||||
|
|
||||||
|
challengers: List["Challenger"] = Relationship(back_populates="member")
|
||||||
|
|
||||||
|
is_admin: bool = False
|
||||||
|
|
||||||
|
waiting: bool = False
|
||||||
|
|
||||||
|
online: bool = False
|
||||||
|
|
||||||
|
waiter_code: Optional[str] = Field(default=None)
|
||||||
|
|
||||||
|
corrections: List['TmpCorrection'] = Relationship(back_populates="member")
|
||||||
|
|
||||||
challenges: List["Challenge"] = Relationship(back_populates="challenger")
|
|
||||||
|
|
||||||
is_admin: bool = False
|
|
||||||
|
|
||||||
waiting: bool = False
|
|
||||||
|
|
||||||
online: bool = False
|
|
||||||
|
|
||||||
waiter_code: Optional[str] = Field(default= None)
|
|
||||||
|
|
||||||
corrections: List['TmpCorrection'] = Relationship(back_populates="member")
|
|
||||||
|
|
||||||
class ExercicesCreate(SQLModel):
|
class ExercicesCreate(SQLModel):
|
||||||
exercice_id: str
|
exercice_id: str
|
||||||
quantity: int = 10
|
quantity: int = 10
|
||||||
|
|
||||||
class Exercices(ExercicesCreate):
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
|
class Exercices(ExercicesCreate):
|
||||||
|
name: str
|
||||||
|
examples: Example
|
||||||
|
|
||||||
|
class Challenger(SQLModel, table=True):
|
||||||
|
member_id: int = Field(foreign_key="member.id", primary_key=True)
|
||||||
|
parcours_id: int = Field(foreign_key="parcours.id", primary_key=True)
|
||||||
|
|
||||||
|
parcours: "Parcours" = Relationship(back_populates="challengers")
|
||||||
|
member: "Member" = Relationship(back_populates="challengers")
|
||||||
|
|
||||||
|
#challenges: list["Challenge"] = Relationship(back_populates="challenger")
|
||||||
|
|
||||||
|
avg: Optional[float] = Field(default=None)
|
||||||
|
best: Optional[int] = Field(default=None)
|
||||||
|
best_time: Optional[int] = Field(default=None)
|
||||||
|
validated: bool = Field(default=False)
|
||||||
|
|
||||||
class Parcours(SQLModel, table=True):
|
class Parcours(SQLModel, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
id_code: str = Field(index=True, unique=True)
|
id_code: str = Field(index=True, unique=True)
|
||||||
|
|
||||||
room_id: int = Field(foreign_key="room.id")
|
room_id: int = Field(foreign_key="room.id")
|
||||||
room: Room = Relationship(back_populates='parcours')
|
room: Room = Relationship(back_populates='parcours')
|
||||||
|
|
||||||
name: str
|
challengers: list[Challenger] = Relationship(back_populates="parcours")
|
||||||
time: int
|
|
||||||
validate_condition: int
|
name: str
|
||||||
|
time: int
|
||||||
exercices: List[Exercices] = Field(sa_column=Column(JSON))
|
|
||||||
challenges: List["Challenge"] = Relationship(back_populates="parcours")
|
|
||||||
corrections: List["TmpCorrection"] = Relationship(back_populates="parcours")
|
max_mistakes: int
|
||||||
|
|
||||||
|
exercices: List[Exercices] = Field(sa_column=Column(JSON))
|
||||||
|
challenges: List["Challenge"] = Relationship(back_populates="parcours")
|
||||||
|
corrections: List["TmpCorrection"] = Relationship(
|
||||||
|
back_populates="parcours")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Note(BaseModel):
|
class Note(BaseModel):
|
||||||
value: int
|
value: int
|
||||||
total: int
|
total: int
|
||||||
|
|
||||||
|
|
||||||
class TimedNote(Note):
|
class TimedNote(Note):
|
||||||
time: int
|
time: int
|
||||||
|
|
||||||
|
|
||||||
class ParcoursReadShort(SQLModel):
|
class ParcoursReadShort(SQLModel):
|
||||||
name: str
|
name: str
|
||||||
best_note: str | None = None
|
best_note: str | None = None
|
||||||
id_code: str
|
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):
|
class Challenges(SQLModel):
|
||||||
id_code: str
|
id_code: str
|
||||||
challenger: str
|
mistakes: int
|
||||||
note: Note
|
time: int
|
||||||
time: int
|
isCorriged: bool
|
||||||
isCorriged: bool
|
canCorrige: bool = True
|
||||||
canCorrige: bool
|
validated: bool = False
|
||||||
validated: bool
|
|
||||||
|
|
||||||
|
class ChallengeInfo(BaseModel):
|
||||||
|
challenger: ChallengerInfo
|
||||||
|
challenges: list[Challenges]
|
||||||
|
#total: int
|
||||||
|
|
||||||
|
class Tops(BaseModel):
|
||||||
|
challenger: ChallengerInfo
|
||||||
|
mistakes: int
|
||||||
|
time: int
|
||||||
|
|
||||||
class ParcoursRead(SQLModel):
|
class ParcoursRead(SQLModel):
|
||||||
name: str
|
tops: list[Tops]
|
||||||
time: int
|
name: str
|
||||||
validate_condition: int
|
time: int
|
||||||
id_code: str
|
max_mistakes: int
|
||||||
exercices: List[Exercices]
|
id_code: str
|
||||||
challenges: List[Challenges]
|
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):
|
class ParcoursCreate(SQLModel):
|
||||||
name: str
|
name: str
|
||||||
time: int
|
time: int
|
||||||
validate_condition: int
|
max_mistakes: int
|
||||||
exercices: List[ExercicesCreate]
|
exercices: List[ExercicesCreate]
|
||||||
|
|
||||||
|
|
||||||
class NotCorrigedInput(BaseModel):
|
class NotCorrigedInput(BaseModel):
|
||||||
index: int
|
index: int
|
||||||
value: str
|
value: str
|
||||||
|
|
||||||
|
|
||||||
class CorrigedInput(NotCorrigedInput):
|
class CorrigedInput(NotCorrigedInput):
|
||||||
correction: str
|
correction: str | None
|
||||||
|
valid: bool | None
|
||||||
|
|
||||||
|
|
||||||
class ParsedGeneratorOut(BaseModel):
|
class ParsedGeneratorOut(BaseModel):
|
||||||
calcul: str
|
calcul: str
|
||||||
inputs: List[NotCorrigedInput]
|
inputs: List[NotCorrigedInput]
|
||||||
|
|
||||||
|
|
||||||
class CorrigedGeneratorOut(BaseModel):
|
class CorrigedGeneratorOut(BaseModel):
|
||||||
calcul: str
|
calcul: str
|
||||||
inputs: List[CorrigedInput]
|
inputs: List[CorrigedInput]
|
||||||
|
|
||||||
|
|
||||||
class Challenge(SQLModel, table=True):
|
class Challenge(SQLModel, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
id_code: str = Field(index=True, unique=True)
|
id_code: str = Field(index=True, unique=True)
|
||||||
|
|
||||||
challenger_id: int = Field(foreign_key="member.id")
|
''' challenger_id: int = Field(foreign_key="member.id")
|
||||||
challenger: Member = Relationship(back_populates="challenges")
|
challenger: Member = Relationship(back_populates="challenges") '''
|
||||||
|
|
||||||
parcours_id: int = Field(foreign_key="parcours.id_code")
|
parcours_id: int = Field(foreign_key="parcours.id_code")
|
||||||
parcours: Parcours = Relationship(back_populates="challenges")
|
parcours: Parcours = Relationship(back_populates="challenges")
|
||||||
|
|
||||||
data: Optional[List[List[CorrigedGeneratorOut]]] = Field(sa_column=Column(JSON), default=[])
|
challenger_pid: int
|
||||||
|
challenger_mid: int
|
||||||
time: int
|
__table_args__ = (ForeignKeyConstraint(["challenger_pid", "challenger_mid"],
|
||||||
note: Note = Field(
|
["challenger.parcours_id","challenger.member_id"]),
|
||||||
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):
|
class ChallengeRead(SQLModel):
|
||||||
id_code: str
|
id_code: str
|
||||||
data: Optional[List[List[CorrigedGeneratorOut]]] = []
|
data: Optional[List[CorrigedData]] = []
|
||||||
time: int
|
time: int
|
||||||
note: Note
|
mistakes: int
|
||||||
validated: bool
|
validated: bool
|
||||||
isCorriged: bool
|
isCorriged: bool
|
||||||
|
|
||||||
|
|
||||||
class TmpCorrection(SQLModel, table=True):
|
class TmpCorrection(SQLModel, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
id_code: str = Field(index=True)
|
id_code: str = Field(index=True)
|
||||||
|
|
||||||
parcours_id: str = Field(foreign_key="parcours.id_code")
|
parcours_id: str = Field(foreign_key="parcours.id_code")
|
||||||
parcours: Parcours = Relationship(back_populates="corrections")
|
parcours: Parcours = Relationship(back_populates="corrections")
|
||||||
|
|
||||||
member_id: int = Field(foreign_key="member.id")
|
member_id: int = Field(foreign_key="member.id")
|
||||||
member: Member = Relationship(back_populates="corrections")
|
member: Member = Relationship(back_populates="corrections")
|
||||||
|
|
||||||
data: List[List[CorrigedGeneratorOut]] = Field(sa_column=Column(JSON))
|
data: List = Field(sa_column=Column(JSON))
|
||||||
|
|
||||||
|
|
||||||
class AnonymousRead(AnonymousBase):
|
class AnonymousRead(AnonymousBase):
|
||||||
reconnect_code: str
|
reconnect_code: str
|
||||||
|
|
||||||
|
|
||||||
class Username(SQLModel):
|
class Username(SQLModel):
|
||||||
username: str
|
username: str
|
||||||
|
|
||||||
|
|
||||||
class MemberRead(SQLModel):
|
class MemberRead(SQLModel):
|
||||||
username: str
|
username: str
|
||||||
reconnect_code: str = ''
|
reconnect_code: str = ''
|
||||||
isUser: bool
|
isUser: bool
|
||||||
isAdmin: bool
|
isAdmin: bool
|
||||||
id_code: str
|
id_code: str
|
||||||
|
clientId: str = ""
|
||||||
|
online: bool
|
||||||
|
|
||||||
|
|
||||||
class RoomRead(RoomBase):
|
class RoomRead(RoomBase):
|
||||||
id_code: str
|
id_code: str
|
||||||
|
|
||||||
|
|
||||||
class RoomAndMember(BaseModel):
|
class RoomAndMember(BaseModel):
|
||||||
room: RoomRead
|
room: RoomRead
|
||||||
member: MemberRead
|
member: MemberRead
|
||||||
|
|
||||||
class RoomInfo(RoomRead):
|
|
||||||
public: bool
|
|
||||||
name: str
|
|
||||||
members: List[MemberRead]
|
|
||||||
parcours: List[ParcoursReadShort]
|
|
||||||
|
|
||||||
class Waiter(BaseModel):
|
class Waiter(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
waiter_id: str
|
waiter_id: str
|
||||||
|
|
||||||
|
|
||||||
|
class RoomInfo(RoomRead):
|
||||||
|
public: bool
|
||||||
|
name: str
|
||||||
|
members: List[MemberRead | Waiter]
|
||||||
|
parcours: List[ParcoursReadShort]
|
||||||
|
|
||||||
|
|
||||||
class RoomConnectionInfos(BaseModel):
|
class RoomConnectionInfos(BaseModel):
|
||||||
room: str
|
room: str
|
||||||
member: str | None = None
|
member: str | None = None
|
||||||
|
BIN
backend/api/database2.db
Normal file
BIN
backend/api/database2.db
Normal file
Binary file not shown.
BIN
backend/api/database3.db
Normal file
BIN
backend/api/database3.db
Normal file
Binary file not shown.
BIN
backend/api/database4.db
Normal file
BIN
backend/api/database4.db
Normal file
Binary file not shown.
BIN
backend/api/database5.db
Normal file
BIN
backend/api/database5.db
Normal file
Binary file not shown.
BIN
backend/api/database6.db
Normal file
BIN
backend/api/database6.db
Normal file
Binary file not shown.
BIN
backend/api/database7.db
Normal file
BIN
backend/api/database7.db
Normal file
Binary file not shown.
0
backend/api/faking/room.py
Normal file
0
backend/api/faking/room.py
Normal file
@ -70,3 +70,4 @@ def Csv_generator(path, nb_in_serie, nb_page, police, consigne, writer):
|
|||||||
|
|
||||||
for r in range(rest_line):
|
for r in range(rest_line):
|
||||||
writer.writerow([''])
|
writer.writerow([''])
|
||||||
|
|
||||||
|
@ -1,41 +1,45 @@
|
|||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import importlib.util
|
|
||||||
import string
|
import string
|
||||||
from typing import List
|
from typing import List
|
||||||
from pydantic import BaseModel
|
|
||||||
import sympy
|
import sympy
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class GeneratorOut(BaseModel):
|
class GeneratorOut(BaseModel):
|
||||||
calcul: str
|
calcul: str
|
||||||
correction: str | None = None
|
correction: str | None = None
|
||||||
|
|
||||||
def parseOut(calcul):
|
|
||||||
"""Fait en sorte de séparer la correction présente dans le calcul"""
|
|
||||||
regex = r"\[(.*?)\]"
|
|
||||||
calculEx = calcul['calcul'].replace('[', ' [').replace(']', '] ')
|
|
||||||
splitted = calculEx.split()
|
|
||||||
|
|
||||||
if len(list(filter(lambda e: e.startswith("[") and e.endswith(']'), splitted))) == 0:
|
|
||||||
splitted.append('[]')
|
|
||||||
|
|
||||||
inputs = []
|
|
||||||
for i in range(len(splitted)):
|
|
||||||
c = splitted[i]
|
|
||||||
match = re.findall(regex, c)
|
|
||||||
if len(match) != 0:
|
|
||||||
correction = c[1:-1]
|
|
||||||
splitted[i] = f'[{len(inputs)}]'
|
|
||||||
inputs.append(
|
|
||||||
{'index': len(inputs), 'correction': correction, 'value': ""})
|
|
||||||
|
|
||||||
calculEx = ' '.join(splitted)
|
|
||||||
return {'calcul': calculEx, 'inputs': inputs}
|
|
||||||
|
|
||||||
|
def parseOut(calcul):
|
||||||
|
"""Fait en sorte de séparer la correction présente dans le calcul"""
|
||||||
|
regex = r"\[(.*?)\]"
|
||||||
|
calculEx = calcul['calcul'].replace('[', ' [').replace(']', '] ')
|
||||||
|
splitted = calculEx.split()
|
||||||
|
|
||||||
|
if len(list(filter(lambda e: e.startswith("[") and e.endswith(']'), splitted))) == 0:
|
||||||
|
splitted.append('[]')
|
||||||
|
|
||||||
|
inputs = []
|
||||||
|
for i in range(len(splitted)):
|
||||||
|
c = splitted[i]
|
||||||
|
match = re.findall(regex, c)
|
||||||
|
if len(match) != 0:
|
||||||
|
correction = c[1:-1]
|
||||||
|
if correction == "":
|
||||||
|
correction = None
|
||||||
|
splitted[i] = f'[{len(inputs)}]'
|
||||||
|
inputs.append(
|
||||||
|
{'index': len(inputs), 'correction': correction, 'value': ""})
|
||||||
|
|
||||||
|
calculEx = ' '.join(splitted)
|
||||||
|
return {'calcul': calculEx, 'inputs': inputs}
|
||||||
|
|
||||||
|
|
||||||
def parseGeneratorOut(out: List[GeneratorOut]):
|
def parseGeneratorOut(out: List[GeneratorOut]):
|
||||||
return [parseOut(c) for c in out]
|
return [parseOut(c) for c in out]
|
||||||
|
|
||||||
|
|
||||||
def getObjectKey(obj, key):
|
def getObjectKey(obj, key):
|
||||||
if obj[key] == None:
|
if obj[key] == None:
|
||||||
@ -44,7 +48,9 @@ def getObjectKey(obj, key):
|
|||||||
|
|
||||||
|
|
||||||
def getCorrectionKey(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='...'):
|
def parseCorrection(calc, replacer='...'):
|
||||||
@ -54,35 +60,37 @@ def parseCorrection(calc, replacer='...'):
|
|||||||
return calc
|
return calc
|
||||||
|
|
||||||
|
|
||||||
def generate_from_data(data, quantity, key, forcedCorrection=False):
|
def generate_from_data(data, quantity, key, forced_correction=False):
|
||||||
locs = {}
|
locs = {}
|
||||||
exec(data, {"random": random, "string": string, "sympy": sympy}, locs)
|
exec(data, {"random": random, "string": string, "sympy": sympy}, locs)
|
||||||
try:
|
try:
|
||||||
main_func = locs['main']
|
main_func = locs['main']
|
||||||
except:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
main_result = main_func()
|
main_result = main_func()
|
||||||
default_object = {"calcul": False, 'pdf': False, 'csv': False,
|
default_object = {"calcul": False, 'pdf': False, 'csv': False,
|
||||||
'web': False, 'correction': False} # les valeurs par défaut
|
'web': False, 'correction': False} # les valeurs par défaut
|
||||||
# Si l'utilisateur n'a pas entré une valeur, elle est définie à False
|
# Si l'utilisateur n'a pas entré une valeur, elle est définie à False
|
||||||
|
|
||||||
result_object = {**default_object, **main_result}
|
result_object = {**default_object, **main_result}
|
||||||
object_key = getObjectKey(result_object, key)
|
object_key = getObjectKey(result_object, key)
|
||||||
correction_key = getCorrectionKey(result_object, key)
|
correction_key = getCorrectionKey(result_object, key)
|
||||||
op_list = []
|
op_list = []
|
||||||
try:
|
try:
|
||||||
replacer = locs["CORRECTION_REPLACER"]
|
replacer = locs["CORRECTION_REPLACER"]
|
||||||
except:
|
except KeyError:
|
||||||
replacer = '...'
|
replacer = '...'
|
||||||
|
|
||||||
for i in range(quantity):
|
for i in range(quantity):
|
||||||
main_result = main_func()
|
main_result = main_func()
|
||||||
main = {**default_object, **main_result}
|
main = {**default_object, **main_result}
|
||||||
op_list.append({'calcul': parseCorrection(main[
|
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
|
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()
|
data = open(path, "r").read()
|
||||||
return generate_from_data(data, quantity, key, forcedCorrection)
|
return generate_from_data(data, quantity, key, forced_correction)
|
||||||
|
@ -1,36 +1,29 @@
|
|||||||
#import schemas.base
|
#import schemas.base
|
||||||
from services.database import generate_unique_code
|
from typing import List, Optional
|
||||||
from sqlmodel import SQLModel, Field, select
|
|
||||||
from services.password import get_password_hash
|
from fastapi import Depends, Request, status
|
||||||
from sqlmodel import Session, select
|
from fastapi import FastAPI, HTTPException
|
||||||
from database.auth.crud import create_user_db
|
from fastapi.encoders import jsonable_encoder
|
||||||
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 fastapi.exceptions import RequestValidationError, ValidationError
|
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 import AuthJWT
|
||||||
from fastapi_jwt_auth.exceptions import AuthJWTException
|
from fastapi_jwt_auth.exceptions import AuthJWTException
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi_pagination import add_pagination
|
||||||
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 sqladmin import Admin, ModelView
|
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 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")
|
app = FastAPI(title="API Generateur d'exercices")
|
||||||
origins = [
|
origins = [
|
||||||
"http://localhost:8000",
|
"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(RequestValidationError)
|
||||||
@app.exception_handler(ValidationError)
|
@app.exception_handler(ValidationError)
|
||||||
async def validation_exception_handler(request, exc: RequestValidationError|ValidationError):
|
async def validation_exception_handler(request: Request, exc: RequestValidationError|ValidationError):
|
||||||
errors = {}
|
errors = {}
|
||||||
print(exc.errors())
|
print(exc.errors())
|
||||||
for e in exc.errors():
|
for e in exc.errors():
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
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 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 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'])
|
router = APIRouter(tags=['Authentification'])
|
||||||
|
|
||||||
|
|
||||||
@ -22,8 +25,9 @@ class Token(BaseModel):
|
|||||||
def login_for_access_token(user: User = Depends(authenticate_user)):
|
def login_for_access_token(user: User = Depends(authenticate_user)):
|
||||||
Authorize = AuthJWT()
|
Authorize = AuthJWT()
|
||||||
access_token = Authorize.create_access_token(
|
access_token = Authorize.create_access_token(
|
||||||
subject=str(user.clientId), fresh=True)
|
subject=str(user.clientId), fresh=True, user_claims={"username": user.username})
|
||||||
refresh_token = Authorize.create_refresh_token(subject=str(user.clientId))
|
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"}
|
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
|
||||||
|
|
||||||
@router.post('/register', response_model=Token)
|
@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"})
|
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)
|
user = create_user_db(username, get_password_hash(user.password), db)
|
||||||
access_token = Authorize.create_access_token(
|
access_token = Authorize.create_access_token(
|
||||||
subject=str(user.clientId))
|
subject=str(user.clientId), user_claims={"username": user.username})
|
||||||
refresh_token = Authorize.create_refresh_token(subject=str(user.clientId))
|
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"}
|
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
|
||||||
|
|
||||||
@router.get('/users', response_model=List[UserRead])
|
@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()
|
users = db.exec(select(User)).all()
|
||||||
return users
|
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)):
|
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)
|
user_obj = update_user_db(clientId, user, db)
|
||||||
return user_obj
|
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)
|
user_obj = change_user_uuid(user.id, db)
|
||||||
|
|
||||||
access_token = Authorize.create_access_token(
|
access_token = Authorize.create_access_token(
|
||||||
subject=str(user_obj))
|
subject=str(user_obj), user_claims={"username": user.username})
|
||||||
refresh_token = Authorize.create_refresh_token(subject=str(user_obj))
|
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"}
|
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')
|
@router.post('/refresh')
|
||||||
def refresh(Authorize: AuthJWT = Depends(jwt_refresh_required)):
|
def refresh(Authorize: AuthJWT = Depends(jwt_refresh_required)):
|
||||||
current_user = Authorize.get_jwt_subject()
|
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}
|
return {"access_token": new_access_token}
|
||||||
|
@ -1,28 +1,34 @@
|
|||||||
from pydantic import BaseModel
|
import csv
|
||||||
|
import io
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import List
|
from typing import List
|
||||||
from fastapi import APIRouter, Depends, Path, Query, UploadFile, HTTPException, status
|
|
||||||
|
from fastapi import APIRouter, Depends, 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.auth.models import User
|
||||||
from database.db import get_session
|
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 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.exoValidation import validate_file, validate_file_optionnal
|
||||||
from services.io import add_fast_api_root, get_filename_from_path
|
from services.io import add_fast_api_root, get_filename_from_path
|
||||||
from fastapi.responses import FileResponse
|
|
||||||
from sqlmodel import func
|
|
||||||
from fastapi_pagination import paginate ,Page
|
|
||||||
from services.models import Page
|
from services.models import Page
|
||||||
from fastapi_pagination.ext.sqlalchemy_future import paginate as p
|
|
||||||
|
|
||||||
router = APIRouter(tags=['exercices'])
|
router = APIRouter(tags=['exercices'])
|
||||||
|
|
||||||
|
|
||||||
class ExoType(str, Enum):
|
class ExoType(str, Enum):
|
||||||
csv="csv"
|
csv = "csv"
|
||||||
pdf="pdf"
|
pdf = "pdf"
|
||||||
web="web"
|
web = "web"
|
||||||
|
|
||||||
|
|
||||||
def filter_exo_by_tags(exos: List[tuple[Exercice, str]], tags: List[Tag]):
|
def filter_exo_by_tags(exos: List[tuple[Exercice, str]], tags: List[Tag]):
|
||||||
valid_exos = [exo for exo, tag in exos if all(
|
valid_exos = [exo for exo, tag in exos if all(
|
||||||
@ -30,48 +36,53 @@ def filter_exo_by_tags(exos: List[tuple[Exercice, str]], tags: List[Tag]):
|
|||||||
return valid_exos
|
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
|
return search, tags, type
|
||||||
|
|
||||||
|
|
||||||
@router.post('/exercices', response_model=ExerciceReadFull, status_code=status.HTTP_201_CREATED)
|
@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 = file['file'].file._file
|
||||||
file_obj.name = file['file'].filename
|
file_obj.name = file['file'].filename
|
||||||
exo_obj = create_exo_db(exercice=exercice, user=user,
|
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)
|
return serialize_exo(exo=exo_obj, user_id=user.id, db=db)
|
||||||
|
|
||||||
|
|
||||||
@router.post('/clone/{id_code}', response_model=ExerciceReadFull)
|
@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:
|
if not exercice:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail={
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail={
|
||||||
"Exercice introuvable"})
|
"Exercice introuvable"})
|
||||||
exo_obj = clone_exo_db(exercice, user, db)
|
exo_obj = clone_exo_db(exercice, user, db)
|
||||||
if type(exo_obj) == str:
|
if type(exo_obj) == str:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail=exo_obj)
|
status_code=status.HTTP_400_BAD_REQUEST, detail=exo_obj)
|
||||||
return serialize_exo(exo=exo_obj, user_id=user.id, db=db)
|
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
|
search, tags, type = queryFilters
|
||||||
|
|
||||||
statement = select(Exercice)
|
statement = select(Exercice)
|
||||||
statement = statement.where(Exercice.author_id == user.id)
|
statement = statement.where(Exercice.author_id == user.id)
|
||||||
statement = statement.where(Exercice.name.startswith(search))
|
statement = statement.where(Exercice.name.startswith(search))
|
||||||
|
|
||||||
if type == ExoType.csv:
|
if type == ExoType.csv:
|
||||||
statement = statement.where(Exercice.csv == True)
|
statement = statement.where(Exercice.csv == True)
|
||||||
if type == ExoType.pdf:
|
if type == ExoType.pdf:
|
||||||
statement = statement.where(Exercice.pdf == True)
|
statement = statement.where(Exercice.pdf == True)
|
||||||
if type == ExoType.web:
|
if type == ExoType.web:
|
||||||
statement = statement.where(Exercice.web == True)
|
statement = statement.where(Exercice.web == True)
|
||||||
|
|
||||||
for t in tags:
|
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()
|
ExercicesTagLink.tag_id == t).exists()
|
||||||
statement = statement.where(sub)
|
statement = statement.where(sub)
|
||||||
page = p(db, statement)
|
page = p(db, statement)
|
||||||
@ -81,14 +92,17 @@ def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tup
|
|||||||
return page
|
return page
|
||||||
|
|
||||||
|
|
||||||
@router.get('/exercices/public', response_model=Page[ExerciceRead|ExerciceReadFull])
|
@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)):
|
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
|
search, tags, type = queryFilters
|
||||||
|
|
||||||
if user is not None:
|
if user is not None:
|
||||||
statement = select(Exercice)
|
statement = select(Exercice)
|
||||||
statement = statement.where(Exercice.author_id != user.id)
|
statement = statement.where(Exercice.author_id != user.id)
|
||||||
statement = statement.where(Exercice.private == False)
|
statement = statement.where(Exercice.private == False)
|
||||||
|
statement = statement.where(Exercice.origin_id == None)
|
||||||
statement = statement.where(Exercice.name.startswith(search))
|
statement = statement.where(Exercice.name.startswith(search))
|
||||||
|
|
||||||
if type == ExoType.csv:
|
if type == ExoType.csv:
|
||||||
@ -97,19 +111,19 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional),
|
|||||||
statement = statement.where(Exercice.pdf == True)
|
statement = statement.where(Exercice.pdf == True)
|
||||||
if type == ExoType.web:
|
if type == ExoType.web:
|
||||||
statement = statement.where(Exercice.web == True)
|
statement = statement.where(Exercice.web == True)
|
||||||
|
|
||||||
for t in tags:
|
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()
|
ExercicesTagLink.tag_id == t).exists()
|
||||||
statement = statement.where(sub)
|
statement = statement.where(sub)
|
||||||
|
|
||||||
page = p(db, statement)
|
page = p(db, statement)
|
||||||
print('¨PAGE', page)
|
print('¨PAGE', page)
|
||||||
exercices = page.items
|
exercices = page.items
|
||||||
page.items = [
|
page.items = [
|
||||||
serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices]
|
serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices]
|
||||||
return page
|
return page
|
||||||
|
|
||||||
else:
|
else:
|
||||||
statement = select(Exercice)
|
statement = select(Exercice)
|
||||||
statement = statement.where(Exercice.private == False)
|
statement = statement.where(Exercice.private == False)
|
||||||
@ -120,7 +134,7 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional),
|
|||||||
statement = statement.where(Exercice.pdf == True)
|
statement = statement.where(Exercice.pdf == True)
|
||||||
if type == ExoType.web:
|
if type == ExoType.web:
|
||||||
statement = statement.where(Exercice.web == True)
|
statement = statement.where(Exercice.web == True)
|
||||||
|
|
||||||
page = p(db, statement)
|
page = p(db, statement)
|
||||||
exercices = page.items
|
exercices = page.items
|
||||||
page.items = [
|
page.items = [
|
||||||
@ -129,12 +143,14 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional),
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/exercice/{id_code}', response_model=ExerciceReadFull)
|
@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)
|
return serialize_exo(exo=exo, user_id=getattr(user, 'id', None), db=db)
|
||||||
|
|
||||||
|
|
||||||
@router.put('/exercice/{id_code}', response_model=ExerciceReadFull)
|
@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:
|
if exo is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable')
|
status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable')
|
||||||
@ -151,7 +167,8 @@ def update_exo(file: UploadFile = Depends(validate_file_optionnal), exo: Exercic
|
|||||||
|
|
||||||
|
|
||||||
@router.delete('/exercice/{id_code}')
|
@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:
|
if exercice is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Exercice introuvable")
|
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'}
|
return {'detail': 'Exercice supprimé avec succès'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class NewTags(BaseModel):
|
class NewTags(BaseModel):
|
||||||
exo: ExerciceReadFull
|
exo: ExerciceReadFull
|
||||||
tags: list[TagRead]
|
tags: list[TagRead]
|
||||||
|
|
||||||
|
|
||||||
@router.post('/exercice/{id_code}/tags', response_model=NewTags, tags=['tags'])
|
@router.post('/exercice/{id_code}/tags', response_model=NewTags, tags=['tags'])
|
||||||
def add_tags(tags: List[TagCreate], exo: Exercice | None = Depends(get_exo_dependency), db: Session = Depends(get_session), user: User = Depends(get_current_user)):
|
def add_tags(tags: List[TagCreate], exo: Exercice | None = Depends(get_exo_dependency),
|
||||||
|
db: Session = Depends(get_session), user: User = Depends(get_current_user)):
|
||||||
if exo is None:
|
if exo is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable')
|
status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable')
|
||||||
exo_obj, new = add_tags_db(exo, tags, user, db)
|
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'])
|
@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:
|
if exo is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail='Exercice introuvable')
|
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')
|
@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:
|
if exo is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail='Exercice introuvable')
|
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')
|
detail='Cet exercice ne vous appartient pas')
|
||||||
path = add_fast_api_root(exo.exo_source)
|
path = add_fast_api_root(exo.exo_source)
|
||||||
filename = get_filename_from_path(path)
|
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'])
|
@router.get('/tags', response_model=List[TagRead], tags=['tags'])
|
||||||
def get_tags(user: User = Depends(get_current_user), db: Session = Depends(get_session)):
|
def get_tags(user: User = Depends(get_current_user), db: Session = Depends(get_session)):
|
||||||
return user.tags
|
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')
|
||||||
|
@ -1,29 +1,42 @@
|
|||||||
|
from typing import Any, TYPE_CHECKING, Callable
|
||||||
|
|
||||||
from fastapi.websockets import WebSocket
|
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 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.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:
|
if TYPE_CHECKING:
|
||||||
from routes.room.routes import RoomManager
|
from routes.room.routes import RoomManager
|
||||||
|
|
||||||
|
|
||||||
class RoomConsumer(Consumer):
|
class RoomConsumer(Consumer):
|
||||||
|
|
||||||
def __init__(self, ws: WebSocket, room: Room, manager: "RoomManager", db: Session):
|
def __init__(self, ws: WebSocket, room: Room, manager: "RoomManager", db: Session):
|
||||||
self.room = room
|
self.room = room
|
||||||
self.ws = ws
|
self.ws = ws
|
||||||
self.manager = manager
|
self.manager = manager
|
||||||
self.db = db
|
self.db = db
|
||||||
self.member = None
|
self.member = None
|
||||||
|
|
||||||
# WS Utilities
|
# 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):
|
async def connect(self):
|
||||||
await self.ws.accept()
|
await self.ws.accept()
|
||||||
|
|
||||||
async def direct_send(self, type: str, payload: Any):
|
async def direct_send(self, type: str, payload: Any, code: int | None = None):
|
||||||
await self.ws.send_json({'type': type, "data": payload})
|
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):
|
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})
|
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})
|
await self.manager.send_to(self.room.id_code, member_id, {'type': type, "data": payload})
|
||||||
|
|
||||||
async def broadcast(self, type, payload, exclude=False):
|
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):
|
def add_to_group(self):
|
||||||
self.manager.add(self.room.id_code, self)
|
self.manager.add(self.room.id_code, self)
|
||||||
@ -40,13 +54,20 @@ class RoomConsumer(Consumer):
|
|||||||
async def connect_self(self):
|
async def connect_self(self):
|
||||||
if isinstance(self.member, Member):
|
if isinstance(self.member, Member):
|
||||||
connect_member(self.member, self.db)
|
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):
|
async def disconnect_self(self):
|
||||||
if isinstance(self.member, Member):
|
if isinstance(self.member, Member):
|
||||||
|
''' self.db.expire(self.member)
|
||||||
|
self.db.refresh(self.member) '''
|
||||||
disconnect_member(self.member, self.db)
|
disconnect_member(self.member, self.db)
|
||||||
if self.member.waiting is False:
|
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:
|
else:
|
||||||
await self.send_to_admin(type="disconnect_waiter", payload={"waiter": serialize_member(self.member)})
|
await self.send_to_admin(type="disconnect_waiter", payload={"waiter": serialize_member(self.member)})
|
||||||
|
|
||||||
@ -55,11 +76,12 @@ class RoomConsumer(Consumer):
|
|||||||
self.member = member
|
self.member = member
|
||||||
await self.connect_self()
|
await self.connect_self()
|
||||||
self.add_to_group()
|
self.add_to_group()
|
||||||
clientId = self.member.anonymous.clientId if self.member.anonymous is not None else ""
|
await self.direct_send(type="loggedIn",
|
||||||
await self.direct_send(type="loggedIn", payload={"member": {**serialize_member(self.member), 'clientId': str(clientId)}})
|
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
|
# Conditions
|
||||||
|
|
||||||
async def isAdminReceive(self):
|
async def isAdminReceive(self):
|
||||||
@ -71,38 +93,40 @@ class RoomConsumer(Consumer):
|
|||||||
|
|
||||||
def isAdmin(self):
|
def isAdmin(self):
|
||||||
return self.member is not None and self.member.is_admin == True
|
return self.member is not None and self.member.is_admin == True
|
||||||
|
|
||||||
async def isMember(self):
|
async def isMember(self):
|
||||||
|
print('S', self.member, self.ws, self.ws.state, self.ws.application_state.__str__())
|
||||||
if self.member is None:
|
if self.member is None:
|
||||||
await self.send_error("Vous n'êtes connecté à aucune salle")
|
await self.send_error("Vous n'êtes connecté à aucune salle")
|
||||||
return self.member is not None and self.member.waiting == False
|
return self.member is not None and self.member.waiting == False
|
||||||
|
|
||||||
def isWaiter(self):
|
def isWaiter(self):
|
||||||
return self.member is not None and self.member.waiting == True
|
return self.member is not None and self.member.waiting == True
|
||||||
|
|
||||||
# Received Events
|
# Received Events
|
||||||
|
|
||||||
@Consumer.event('login')
|
@Consumer.event('login')
|
||||||
async def login(self, token: str | None = None, reconnect_code: str | None = None):
|
async def login(self, token: str | None = None, reconnect_code: str | None = None):
|
||||||
if reconnect_code is None and token is None:
|
if reconnect_code is None and token is None:
|
||||||
await self.direct_send(type="error", payload={"msg": "Veuillez spécifier une méthode de connection"})
|
await self.direct_send(type="error", payload={"msg": "Veuillez spécifier une méthode de connection"})
|
||||||
return
|
return
|
||||||
|
print("login", token)
|
||||||
if token is not None:
|
if token is not None:
|
||||||
member = get_member_from_token(token, self.room.id, self.db)
|
member = get_member_from_token(token, self.room.id, self.db)
|
||||||
|
print('MEMBER', member)
|
||||||
if member == False:
|
if member == False:
|
||||||
await self.send_error("Token expired")
|
await self.send_error("Token expired", code=422)
|
||||||
return
|
return
|
||||||
if member is None:
|
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
|
return
|
||||||
|
|
||||||
elif reconnect_code is not None:
|
elif reconnect_code is not None:
|
||||||
member = get_member_from_reconnect_code(
|
member = get_member_from_reconnect_code(
|
||||||
reconnect_code, self.room.id, db=self.db)
|
reconnect_code, self.room.id, db=self.db)
|
||||||
if member is None:
|
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
|
return
|
||||||
|
|
||||||
await self.loginMember(member)
|
await self.loginMember(member)
|
||||||
|
|
||||||
@Consumer.event('join')
|
@Consumer.event('join')
|
||||||
@ -116,9 +140,9 @@ class RoomConsumer(Consumer):
|
|||||||
if user is False:
|
if user is False:
|
||||||
await self.send_error("Token expired")
|
await self.send_error("Token expired")
|
||||||
return
|
return
|
||||||
|
|
||||||
userInRoom = check_user_in_room(user.id, self.room.id, self.db)
|
userInRoom = check_user_in_room(user.id, self.room.id, self.db)
|
||||||
|
|
||||||
if userInRoom is not None:
|
if userInRoom is not None:
|
||||||
await self.loginMember(userInRoom)
|
await self.loginMember(userInRoom)
|
||||||
return
|
return
|
||||||
@ -127,6 +151,9 @@ class RoomConsumer(Consumer):
|
|||||||
user=user, room=self.room, waiting=self.room.public is False, db=self.db)
|
user=user, room=self.room, waiting=self.room.public is False, db=self.db)
|
||||||
|
|
||||||
elif username is not None:
|
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)
|
anonymous = create_anonymous(username, self.room, self.db)
|
||||||
if anonymous is None:
|
if anonymous is None:
|
||||||
await self.send_error("Nom d'utilisateur invalide ou indisponible")
|
await self.send_error("Nom d'utilisateur invalide ou indisponible")
|
||||||
@ -134,61 +161,69 @@ class RoomConsumer(Consumer):
|
|||||||
|
|
||||||
waiter = create_member(
|
waiter = create_member(
|
||||||
anonymous=anonymous, room=self.room, waiting=self.room.public is False, db=self.db)
|
anonymous=anonymous, room=self.room, waiting=self.room.public is False, db=self.db)
|
||||||
|
|
||||||
self.member = waiter
|
self.member = waiter
|
||||||
self.add_to_group()
|
self.add_to_group()
|
||||||
|
|
||||||
if self.room.public is False:
|
if self.room.public is False:
|
||||||
await self.direct_send(type="waiting", payload={"waiter": serialize_member(self.member)})
|
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)})
|
await self.send_to_admin(type="waiter", payload={"waiter": serialize_member(self.member)})
|
||||||
else:
|
else:
|
||||||
await self.broadcast(type="joined", payload={"member": serialize_member(self.member)}, exclude=True)
|
await self.manager.broadcast(
|
||||||
await self.direct_send(type="accepted", payload={"member": serialize_member(self.member)})
|
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])
|
@Consumer.event('accept', conditions=[isAdminReceive])
|
||||||
async def accept(self, waiter_id: str):
|
async def accept(self, waiter_id: str):
|
||||||
waiter = get_waiter(waiter_id, self.db)
|
waiter = get_waiter(waiter_id, self.db)
|
||||||
if waiter is None:
|
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
|
return
|
||||||
member = accept_waiter(waiter, self.db)
|
member = accept_waiter(waiter, self.db)
|
||||||
await self.send_to(type="accepted", payload={"member": serialize_member(member)}, member_id=waiter_id)
|
await self.send_to(type="accepted", payload={"member": serialize_member(member, private=True, m2=member)},
|
||||||
await self.broadcast(type="joined", payload={"member": serialize_member(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])
|
@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)
|
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.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')
|
@Consumer.event('ping_room')
|
||||||
async def proom(self):
|
async def proom(self):
|
||||||
await self.broadcast(type='ping', payload={}, exclude=True)
|
await self.broadcast(type='ping', payload={}, exclude=True)
|
||||||
|
|
||||||
@Consumer.event('sub_parcours')
|
@Consumer.event('sub_parcours')
|
||||||
async def sub_parcours(self, parcours_id: str):
|
async def sub_parcours(self, parcours_id: str):
|
||||||
if isinstance(self.member, Member) and self.member.waiting == False:
|
if isinstance(self.member, Member) and self.member.waiting == False:
|
||||||
self.manager.add(parcours_id, self)
|
self.manager.add(parcours_id, self)
|
||||||
|
|
||||||
@Consumer.event('unsub_parcours')
|
@Consumer.event('unsub_parcours')
|
||||||
async def unsub_parcours(self, parcours_id: str):
|
async def unsub_parcours(self, parcours_id: str):
|
||||||
if isinstance(self.member, Member) and self.member.waiting == False:
|
if isinstance(self.member, Member) and self.member.waiting == False:
|
||||||
self.manager.remove(parcours_id, self)
|
self.manager.remove(parcours_id, self)
|
||||||
|
|
||||||
@Consumer.event('set_name', conditions=[isAdminReceive])
|
@Consumer.event('set_name', conditions=[isAdminReceive])
|
||||||
async def change_name(self, name: str):
|
async def change_name(self, name: str):
|
||||||
if len(name) < 20:
|
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')
|
print('SENDING')
|
||||||
await self.broadcast(type="new_name", payload={"name": name})
|
await self.broadcast(type="new_name", payload={"name": name})
|
||||||
|
|
||||||
return
|
return
|
||||||
await self.send_error('Nom trop long (max 20 character)')
|
await self.send_error('Nom trop long (max 20 character)')
|
||||||
|
|
||||||
@Consumer.event('set_visibility', conditions=[isAdminReceive])
|
@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)
|
self.room = change_room_status(self.room, public, self.db)
|
||||||
await self.broadcast(type="new_visibility", payload={"public": public})
|
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"})
|
await self.direct_send(type="error", payload={"msg": "Vous n'êtes connecté à aucune salle"})
|
||||||
return self.member is not None
|
return self.member is not None
|
||||||
|
|
||||||
|
|
||||||
@Consumer.event('leave', conditions=[isMember])
|
@Consumer.event('leave', conditions=[isMember])
|
||||||
async def leave(self):
|
async def leave(self):
|
||||||
if self.member.is_admin is True:
|
if self.member.is_admin is True:
|
||||||
@ -205,7 +239,7 @@ class RoomConsumer(Consumer):
|
|||||||
return
|
return
|
||||||
member_obj = serialize_member(self.member)
|
member_obj = serialize_member(self.member)
|
||||||
leave_room(self.member, self.db)
|
leave_room(self.member, self.db)
|
||||||
|
|
||||||
await self.direct_send(type="successfully_leaved", payload={})
|
await self.direct_send(type="successfully_leaved", payload={})
|
||||||
await self.broadcast(type='leaved', payload={"member": member_obj})
|
await self.broadcast(type='leaved', payload={"member": member_obj})
|
||||||
self.member = None
|
self.member = None
|
||||||
@ -219,7 +253,7 @@ class RoomConsumer(Consumer):
|
|||||||
if member.is_admin is True:
|
if member.is_admin is True:
|
||||||
await self.send_error("Vous ne pouvez pas bannir un administrateur")
|
await self.send_error("Vous ne pouvez pas bannir un administrateur")
|
||||||
return
|
return
|
||||||
|
|
||||||
member_serialized = serialize_member(member)
|
member_serialized = serialize_member(member)
|
||||||
leave_room(member, self.db)
|
leave_room(member, self.db)
|
||||||
await self.send_to(type="banned", payload={}, member_id=member.id_code)
|
await self.send_to(type="banned", payload={}, member_id=member.id_code)
|
||||||
@ -227,10 +261,11 @@ class RoomConsumer(Consumer):
|
|||||||
|
|
||||||
# Sending Events
|
# Sending Events
|
||||||
|
|
||||||
@Consumer.sending(['connect', "disconnect", "joined"], conditions=[isMember])
|
@Consumer.sending(["joined"], conditions=[isMember])
|
||||||
def joined(self, member: MemberRead):
|
def joined(self, member: MemberRead):
|
||||||
|
|
||||||
if self.member.id_code == member.id_code:
|
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:
|
if self.member.is_admin == False:
|
||||||
member.reconnect_code = ""
|
member.reconnect_code = ""
|
||||||
return {"member": member}
|
return {"member": member}
|
||||||
@ -239,6 +274,11 @@ class RoomConsumer(Consumer):
|
|||||||
def waiter(self, waiter: Waiter):
|
def waiter(self, waiter: Waiter):
|
||||||
return {"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])
|
@Consumer.sending("refused", conditions=[isWaiter])
|
||||||
def refused(self, waiter_id: str):
|
def refused(self, waiter_id: str):
|
||||||
self.member = None
|
self.member = None
|
||||||
@ -249,6 +289,7 @@ class RoomConsumer(Consumer):
|
|||||||
def banned(self):
|
def banned(self):
|
||||||
self.member = None
|
self.member = None
|
||||||
self.manager.remove(self.room.id, self)
|
self.manager.remove(self.room.id, self)
|
||||||
|
self.ws.close()
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@Consumer.sending('ping', conditions=[isMember])
|
@Consumer.sending('ping', conditions=[isMember])
|
||||||
@ -256,6 +297,6 @@ class RoomConsumer(Consumer):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
async def disconnect(self):
|
async def disconnect(self):
|
||||||
|
print('DISCONNECTED', self.member)
|
||||||
self.manager.remove(self.room.id, self)
|
self.manager.remove(self.room.id, self)
|
||||||
await self.disconnect_self()
|
await self.disconnect_self()
|
||||||
|
|
||||||
|
@ -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
|
from starlette.websockets import WebSocketState
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from routes.room.consumer import RoomConsumer
|
from routes.room.consumer import RoomConsumer
|
||||||
@ -16,7 +16,8 @@ class RoomManager:
|
|||||||
self.active_connections[group].append(member)
|
self.active_connections[group].append(member)
|
||||||
|
|
||||||
async def _send(self, connection: "RoomConsumer", message, group: str):
|
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)
|
self.remove(group, connection)
|
||||||
elif connection.ws.application_state == WebSocketState.CONNECTED:
|
elif connection.ws.application_state == WebSocketState.CONNECTED:
|
||||||
await connection.send(message)
|
await connection.send(message)
|
||||||
@ -24,14 +25,17 @@ class RoomManager:
|
|||||||
def remove(self, group: str, member: "RoomConsumer"):
|
def remove(self, group: str, member: "RoomConsumer"):
|
||||||
if group in self.active_connections:
|
if group in self.active_connections:
|
||||||
if member in self.active_connections[group]:
|
if member in self.active_connections[group]:
|
||||||
|
print("remoied")
|
||||||
self.active_connections[group].remove(member)
|
self.active_connections[group].remove(member)
|
||||||
|
|
||||||
async def broadcast(self, message, group: str, exclude: list["RoomConsumer"] = []):
|
async def broadcast(self, message: Any | Callable, group: str, conditions: list[Callable] = [], exclude: list["RoomConsumer"] = [], ):
|
||||||
print('BROADCaST', message)
|
print('BROADCaST', message, self.active_connections)
|
||||||
|
|
||||||
|
|
||||||
if group in self.active_connections:
|
if group in self.active_connections:
|
||||||
for connection in list(set(self.active_connections[group])):
|
for connection in list(set(self.active_connections[group])):
|
||||||
# print(connection)
|
print(connection, connection.ws.state, connection.ws.client_state, connection.ws.application_state)
|
||||||
if connection not in exclude:
|
if connection not in exclude and all(f(connection) for f in conditions ):
|
||||||
await self._send(connection, message, group)
|
await self._send(connection, message, group)
|
||||||
|
|
||||||
async def send_to(self, group, id_code, msg):
|
async def send_to(self, group, id_code, msg):
|
||||||
@ -44,6 +48,6 @@ class RoomManager:
|
|||||||
async def send_to_admin(self, group, msg):
|
async def send_to_admin(self, group, msg):
|
||||||
if group in self.active_connections:
|
if group in self.active_connections:
|
||||||
members = [c for c in self.active_connections[group]
|
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:
|
for m in members:
|
||||||
await self._send(m, msg, group)
|
await self._send(m, msg, group)
|
||||||
|
@ -1,26 +1,52 @@
|
|||||||
from services.database import generate_unique_code
|
from typing import List, Optional
|
||||||
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 pydantic import BaseModel
|
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
|
||||||
from fastapi import APIRouter, Depends, WebSocket, status, Query, Body
|
from fastapi import APIRouter, Depends, WebSocket, status, Query, Body
|
||||||
from config import ALGORITHM, SECRET_KEY
|
from fastapi.exceptions import HTTPException
|
||||||
from database.auth.crud import get_user_from_clientId_db
|
from pydantic import BaseModel
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
from database.auth.models import User
|
from database.auth.models import User
|
||||||
from database.db import get_session
|
from database.db import get_session
|
||||||
|
from database.exercices.models import Exercice
|
||||||
from sqlmodel import Session, col, select
|
from database.room.crud import serialize_parcours_short, change_correction, corrige_challenge, \
|
||||||
from database.room.models import Challenge, ChallengeRead, Challenges, CorrigedGeneratorOut, Member, Note, Parcours, ParcoursCreate, ParcoursRead, ParcoursReadShort, ParsedGeneratorOut, Room, RoomConnectionInfos, RoomCreate, RoomAndMember, RoomInfo, TmpCorrection
|
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.consumer import RoomConsumer
|
||||||
from routes.room.manager import RoomManager
|
from routes.room.manager import RoomManager
|
||||||
from services.auth import get_current_user_optional
|
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 fastapi.exceptions import HTTPException
|
||||||
from database.auth.crud import get_user_from_token
|
from pydantic import BaseModel
|
||||||
from services.websocket import Consumer
|
from sqlmodel import Session, select
|
||||||
from services.misc import noteOn20, stripKeyDict
|
|
||||||
|
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"])
|
router = APIRouter(tags=["room"])
|
||||||
manager = RoomManager()
|
manager = RoomManager()
|
||||||
|
|
||||||
@ -28,6 +54,7 @@ manager = RoomManager()
|
|||||||
def get_manager():
|
def get_manager():
|
||||||
return manager
|
return manager
|
||||||
|
|
||||||
|
|
||||||
@router.post('/room', response_model=RoomConnectionInfos)
|
@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)):
|
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)
|
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)
|
return serialize_room(room, member, db)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.post('/room/{room_id}/parcours', response_model=ParcoursRead)
|
@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)):
|
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)
|
parcours_obj = create_parcours_db(parcours, member.room_id, db)
|
||||||
if type(parcours_obj) == str:
|
if type(parcours_obj) == str:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail=parcours_obj)
|
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)
|
await m.broadcast({"type": "add_parcours",
|
||||||
return parcours_obj
|
"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)
|
@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)):
|
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 serialize_parcours(parcours, member, db)
|
||||||
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]}
|
|
||||||
|
|
||||||
|
|
||||||
@router.put('/room/{room_id}/parcours/{parcours_id}', response_model=ParcoursRead, dependencies=[Depends(check_admin)])
|
@router.put('/room/{room_id}/parcours/{parcours_id}', response_model=ParcoursRead)
|
||||||
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)):
|
async def update_parcours(*, room_id: str, parcours: ParcoursCreate, member: Member = Depends(check_admin),
|
||||||
parcours_obj = update_parcours_db(parcours, parcours_old, db)
|
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:
|
if type(parcours_obj) == str:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail=parcours_obj)
|
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(lambda m: {"type": "update_parcours",
|
||||||
await m.broadcast({"type": "update_parcours", "data": {"parcours": short.dict()}}, room_id)
|
"data": {"parcours": serialize_parcours_short(parcours_obj, m, db).dict()}}, room_id)
|
||||||
return parcours_obj
|
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)])
|
@router.delete('/room/{room_id}/parcours/{parcours_id}', dependencies=[Depends(check_admin)])
|
||||||
@ -82,51 +115,133 @@ class Exos(BaseModel):
|
|||||||
exercice: Exercice
|
exercice: Exercice
|
||||||
quantity: int
|
quantity: int
|
||||||
|
|
||||||
|
|
||||||
@router.get('/room/{room_id}/challenge/{parcours_id}')
|
@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(
|
correction = [parseGeneratorOut(generate_from_path(add_fast_api_root(
|
||||||
e['exercice'].exo_source), e['quantity'], "web")) for e in exercices]
|
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)
|
@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
|
parcours = correction.parcours
|
||||||
member = correction.member
|
member = correction.member
|
||||||
data = corrige_challenge(challenge, correction)
|
data = corrige_challenge(challenge, correction)
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail={"challenge_error":"Object does not correspond to correction"})
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
chall = create_challenge(**data, challenger=member,
|
detail={"challenge_error": "Object does not correspond to correction"})
|
||||||
parcours=parcours, time=time, db=db)
|
chall, challenger = 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)
|
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)
|
db.delete(correction)
|
||||||
|
returnValue = {**chall.dict(), 'validated': chall.mistakes <= correction.parcours.max_mistakes}
|
||||||
db.commit()
|
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)])
|
class ParcoursInfo(BaseModel):
|
||||||
async def challenge_read(*, challenge: Challenge = Depends(get_challenge)):
|
name: str
|
||||||
return challenge
|
time: int
|
||||||
|
# validate: int
|
||||||
|
id_code: str
|
||||||
|
|
||||||
|
|
||||||
@router.put('/room/{room_id}/challenge/{parcours_id}/{challenge_id}', response_model=ChallengeRead, dependencies=[Depends(check_admin)])
|
class Chall(BaseModel):
|
||||||
async def corrige(*, correction: List[List[CorrigedGeneratorOut]], challenge: Challenge = Depends(get_challenge), db: Session = Depends(get_session), m: RoomManager = Depends(get_manager),):
|
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
|
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)
|
data = change_correction(correction, challenge)
|
||||||
if data is None:
|
if data is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail={"correction_error": "Object does not correspond to challenge"})
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
challenge = change_challenge(challenge, data, db)
|
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)
|
parcours = challenge.parcours
|
||||||
return challenge
|
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}')
|
@router.websocket('/ws/room/{room_id}')
|
||||||
|
@ -3,6 +3,7 @@ from pydantic import validate_arguments, BaseModel
|
|||||||
from fastapi.websockets import WebSocketDisconnect, WebSocket
|
from fastapi.websockets import WebSocketDisconnect, WebSocket
|
||||||
from pydantic.error_wrappers import ValidationError
|
from pydantic.error_wrappers import ValidationError
|
||||||
import inspect
|
import inspect
|
||||||
|
from starlette.websockets import WebSocketState
|
||||||
|
|
||||||
def make_event_decorator(eventsDict):
|
def make_event_decorator(eventsDict):
|
||||||
def _(name: str | List, conditions: List[Callable | bool] = []):
|
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]}})
|
await self.ws.send_json({"type": "error", "data": {"detail": [{ers['loc'][-1]: ers['msg']} for ers in errors]}})
|
||||||
|
|
||||||
async def send(self, payload):
|
async def send(self, payload):
|
||||||
|
''' if self.ws.state == WebSocketState.DISCONNECTED:
|
||||||
|
return '''
|
||||||
type = payload.get('type', None)
|
type = payload.get('type', None)
|
||||||
#print('TYPE', type, self.member)
|
print('TYPE', type, self.member)
|
||||||
|
|
||||||
if type is not None:
|
if type is not None:
|
||||||
event_wrapper = self.sendings.get(type, None)
|
event_wrapper = self.sendings.get(type, None)
|
||||||
if event_wrapper is not None:
|
if event_wrapper is not None:
|
||||||
@ -85,6 +89,7 @@ class Consumer:
|
|||||||
try:
|
try:
|
||||||
validated_payload = model(self=self, **data)
|
validated_payload = model(self=self, **data)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
|
print("ERROR", e)
|
||||||
await self.ws.send_json({"type": "error", "data": {"msg": "Oops there was an error"}})
|
await self.ws.send_json({"type": "error", "data": {"msg": "Oops there was an error"}})
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -133,4 +138,5 @@ class Consumer:
|
|||||||
data = await self.ws.receive_json()
|
data = await self.ws.receive_json()
|
||||||
await self.receive(data)
|
await self.receive(data)
|
||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
|
print('DISCONNECTION')
|
||||||
await self.disconnect()
|
await self.disconnect()
|
||||||
|
@ -116,7 +116,7 @@ def test_clone(client: TestClient):
|
|||||||
print(rr.json())
|
print(rr.json())
|
||||||
assert rr.status_code == 200
|
assert rr.status_code == 200
|
||||||
assert 'id_code' in rr.json()
|
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}
|
'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",
|
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:
|
if user == None:
|
||||||
token = test_register(client, username="lilian")['access']
|
token = test_register(client, username="lilian")['access']
|
||||||
user = {"token": token, 'username': "lilian"}
|
user = {"token": token, 'username': "lilian"}
|
||||||
else:
|
else:
|
||||||
token = user['token']
|
token = user['token']
|
||||||
|
if exo == None:
|
||||||
exo = test_create(client, name=name, user=user)
|
exo = test_create(client, name=name, user=user)
|
||||||
id_code = exo['id_code']
|
id_code = exo['id_code']
|
||||||
|
else:
|
||||||
|
id_code = exo['id_code']
|
||||||
r = client.post(f'/exercice/{id_code}/tags', json=tags,
|
r = client.post(f'/exercice/{id_code}/tags', json=tags,
|
||||||
headers={'Authorization': 'Bearer ' + token})
|
headers={'Authorization': 'Bearer ' + token})
|
||||||
print(r.json())
|
print("DATA", tags, "\n\n",r.json())
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
labels = [l['label'] for l in tags]
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert {**data, "tags": [{**t, "id_code": None}
|
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']]} == {**exo, 'tags': [*exo['tags'], *[{**t, 'id_code': None} for t in tags]]}
|
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()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
@ -281,19 +284,19 @@ def test_add_tags_too_long(client: TestClient):
|
|||||||
def test_remove_tag(client: TestClient):
|
def test_remove_tag(client: TestClient):
|
||||||
token = test_register(client, username="lilian")['access']
|
token = test_register(client, username="lilian")['access']
|
||||||
exo = test_add_tags(client, user={"token": token, 'username': "lilian"})
|
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"]
|
tag_id = exo["tags"][0]["id_code"]
|
||||||
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
|
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
|
||||||
headers={'Authorization': 'Bearer ' + token})
|
headers={'Authorization': 'Bearer ' + token})
|
||||||
print(r.json())
|
print(r.json())
|
||||||
assert r.json() == {
|
assert r.json() == {
|
||||||
**exo, 'tags': exo['tags'][1:]}
|
**exo['exo'], 'tags': exo['tags'][1:]}
|
||||||
|
|
||||||
|
|
||||||
def test_remove_tag_not_found(client: TestClient):
|
def test_remove_tag_not_found(client: TestClient):
|
||||||
token = test_register(client, username="lilian")['access']
|
token = test_register(client, username="lilian")['access']
|
||||||
exo = test_add_tags(client, user={"token": token, 'username': "lilian"})
|
exo = test_add_tags(client, user={"token": token, 'username': "lilian"})
|
||||||
id_code = exo['id_code']
|
id_code = exo['exo']['id_code']
|
||||||
tag_id = "none"
|
tag_id = "none"
|
||||||
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
|
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
|
||||||
headers={'Authorization': 'Bearer ' + token})
|
headers={'Authorization': 'Bearer ' + token})
|
||||||
@ -316,7 +319,7 @@ def test_remove_tag_not_owner(client: TestClient):
|
|||||||
token = test_register(client, username="lilian")['access']
|
token = test_register(client, username="lilian")['access']
|
||||||
token2 = test_register(client, username="lilian2")['access']
|
token2 = test_register(client, username="lilian2")['access']
|
||||||
exo = test_add_tags(client, user={"token": token, 'username': "lilian"})
|
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']
|
tag_id = exo['tags'][0]['id_code']
|
||||||
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
|
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
|
||||||
headers={'Authorization': 'Bearer ' + token2})
|
headers={'Authorization': 'Bearer ' + token2})
|
||||||
@ -481,29 +484,31 @@ def test_get_user_with_tags(client: TestClient):
|
|||||||
token2 = test_register(client, username="lilian2")['access']
|
token2 = test_register(client, username="lilian2")['access']
|
||||||
|
|
||||||
tags1 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None}]
|
tags1 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None}]
|
||||||
tags2 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None},
|
tags2 = [
|
||||||
{'label': "tag2", '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}]
|
tags3 = [{'label': "tag3", 'color': "#ff0000", 'id_code': None}]
|
||||||
|
|
||||||
exo_other_user = test_create(
|
exo_other_user = test_create(
|
||||||
client, user={'token': token2, 'username': "lilian2"})
|
client, user={'token': token2, 'username': "lilian2"})
|
||||||
|
|
||||||
exo1 = test_add_tags(client, user={
|
exo1 = test_add_tags(client, user={
|
||||||
'token': token1, 'username': "lilian"}, tags=tags1)
|
'token': token1, 'username': "lilian"}, tags=tags1)
|
||||||
|
tags2 = [*exo1['tags'], *tags2]
|
||||||
exo2 = test_add_tags(client, user={
|
exo2 = test_add_tags(client, user={
|
||||||
'token': token1, 'username': "lilian"}, tags=tags2)
|
'token': token1, 'username': "lilian"}, tags=tags2)
|
||||||
|
|
||||||
|
tags3 = [*exo2['tags'], *tags3]
|
||||||
exo3 = test_add_tags(client, user={
|
exo3 = test_add_tags(client, user={
|
||||||
'token': token1, 'username': "lilian"}, tags=tags3)
|
'token': token1, 'username': "lilian"}, tags=tags3)
|
||||||
|
|
||||||
tags1 = exo1['tags']
|
tags1 = exo1['tags']
|
||||||
tags2 = exo2['tags']
|
tags2 = exo2['tags']
|
||||||
tags3 = exo3['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})
|
headers={'Authorization': 'Bearer ' + token1})
|
||||||
print(r.json())
|
print("DATA", r.json())
|
||||||
assert r.json()['items'] == [exo2, exo3]
|
assert r.json()['items'] == [exo2['exo'], exo3['exo']]
|
||||||
|
|
||||||
|
|
||||||
def test_get_user_with_tags_and_search(client: TestClient):
|
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']
|
token2 = test_register(client, username="lilian2")['access']
|
||||||
|
|
||||||
tags1 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None}]
|
tags1 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None}]
|
||||||
tags2 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None},
|
tags2 = [{'label': "tag2", 'color': "#ff0000", 'id_code': None}]
|
||||||
{'label': "tag2", 'color': "#ff0000", 'id_code': None}]
|
tags3 = [{'label': "tag3", 'color': "#ff0000", 'id_code': None}]
|
||||||
tags3 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None},
|
|
||||||
{'label': "tag2", 'color': "#ff0000", 'id_code': None}, {'label': "tag3", 'color': "#ff0000", 'id_code': None}]
|
|
||||||
|
|
||||||
exo_other_user = test_create(
|
exo_other_user = test_create(
|
||||||
client, user={'token': token2, 'username': "lilian2"})
|
client, user={'token': token2, 'username': "lilian2"})
|
||||||
|
|
||||||
exo1 = test_add_tags(client, user={
|
exo1 = test_add_tags(client, user={
|
||||||
'token': token1, 'username': "lilian"}, tags=tags1, name="yes")
|
'token': token1, 'username': "lilian"}, tags=tags1, name="yes")
|
||||||
|
tags2 = [*exo1['tags'], *tags2]
|
||||||
exo2 = test_add_tags(client, user={
|
exo2 = test_add_tags(client, user={
|
||||||
'token': token1, 'username': "lilian"}, tags=tags2, name="no")
|
'token': token1, 'username': "lilian"}, tags=tags2, name="no")
|
||||||
|
tags3 = [*exo2['tags'], *tags3]
|
||||||
exo3 = test_add_tags(client, user={
|
exo3 = test_add_tags(client, user={
|
||||||
'token': token1, 'username': "lilian"}, tags=tags3, name="yes")
|
'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]},
|
r = client.get('/exercices/user', params={"search": "yes", 'tags': [t['id_code'] for t in tags2]},
|
||||||
headers={'Authorization': 'Bearer ' + token1})
|
headers={'Authorization': 'Bearer ' + token1})
|
||||||
print(r.json())
|
print(r.json())
|
||||||
assert r.json()['items'] == [exo3]
|
assert r.json()['items'] == [exo3['exo']]
|
||||||
|
|
||||||
|
|
||||||
def test_get_public_auth(client: TestClient):
|
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']
|
token = test_register(client, username="lilian")['access']
|
||||||
exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
|
exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
|
||||||
|
|
||||||
r = client.get('/exercice/' + exo['id_code'])
|
r = client.get('/exercice/' + exo['exo']['id_code'])
|
||||||
assert r.json() == {**exo, "tags": [], 'is_author': False}
|
assert r.json() == {**exo['exo'], "tags": [], 'is_author': False}
|
||||||
|
|
||||||
|
|
||||||
def test_get_exo_no_auth_private(client: TestClient):
|
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']
|
token2 = test_register(client, username="lilian2")['access']
|
||||||
exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
|
exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
|
||||||
|
|
||||||
r = client.get('/exercice/' + exo['id_code'],
|
r = client.get('/exercice/' + exo['exo']['id_code'],
|
||||||
headers={'Authorization': 'Bearer ' + token2})
|
headers={'Authorization': 'Bearer ' + token2})
|
||||||
print(r.json(), exo)
|
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):
|
def test_get_exo_auth_with_tags(client: TestClient):
|
||||||
token = test_register(client, username="lilian")['access']
|
token = test_register(client, username="lilian")['access']
|
||||||
|
token2 = test_register(client, username="lilian2")['access']
|
||||||
|
|
||||||
exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
|
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['id_code'],
|
|
||||||
|
r = client.get('/exercice/' + exo['exo']['id_code'],
|
||||||
headers={'Authorization': 'Bearer ' + token})
|
headers={'Authorization': 'Bearer ' + token})
|
||||||
|
|
||||||
print(r.json(), exo)
|
print(r.json(), exo)
|
||||||
assert r.json() == {**exo}
|
assert r.json() == {**exo['exo']}
|
||||||
|
|
||||||
|
|
||||||
def test_get_exo_auth_private(client: TestClient):
|
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}]
|
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']
|
token = test_register(client)['access']
|
||||||
exoCsv = client.post('/exercices', data={"name": "name", "consigne": "consigne", "private": False}, files={
|
exoCsv = client.post('/exercices', data={"name": "name", "consigne": "consigne", "private": False}, files={
|
||||||
'file': ('test.py', open('tests/testing_exo_source/exo_source_csv_only.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
'file': ('test.py', open('tests/testing_exo_source/exo_source_csv_only.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
||||||
@ -680,7 +687,7 @@ def test_get_pdf(client: TestClient):
|
|||||||
r = client.get('/exercices/public', params={"type": "pdf"})
|
r = client.get('/exercices/public', params={"type": "pdf"})
|
||||||
|
|
||||||
assert r.json()['items'] == [{**exoPdf.json(), 'is_author': False}]
|
assert r.json()['items'] == [{**exoPdf.json(), 'is_author': False}]
|
||||||
|
'''
|
||||||
|
|
||||||
def test_get_web(client: TestClient):
|
def test_get_web(client: TestClient):
|
||||||
token = test_register(client)['access']
|
token = test_register(client)['access']
|
||||||
@ -693,7 +700,7 @@ def test_get_web(client: TestClient):
|
|||||||
|
|
||||||
r = client.get('/exercices/public', params={"type": "web"})
|
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):
|
def test_get_invalid_type(client: TestClient):
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -8,4 +8,4 @@ Fonction main() qui doit renvoyer un objet avec:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
t = random.randint(1, 10)
|
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
8
frontend/.idea/.gitignore
generated
vendored
Normal 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
420
frontend/.idea/dbnavigator.xml
generated
Normal 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
9
frontend/.idea/frontend.iml
generated
Normal 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>
|
6
frontend/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
frontend/.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
6
frontend/.idea/misc.xml
generated
Normal 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
8
frontend/.idea/modules.xml
generated
Normal 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
6
frontend/.idea/vcs.xml
generated
Normal 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>
|
9
frontend/cypress.config.ts
Normal file
9
frontend/cypress.config.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from "cypress";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
// implement node event listeners here
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
7
frontend/cypress/e2e/exos.cy.ts
Normal file
7
frontend/cypress/e2e/exos.cy.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// type definitions for Cypress object "cy"
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
describe('test exo', ()=>{
|
||||||
|
it('test exo', ()=>{
|
||||||
|
cy.visit('localhost:5173')
|
||||||
|
})
|
||||||
|
})
|
5
frontend/cypress/fixtures/example.json
Normal file
5
frontend/cypress/fixtures/example.json
Normal 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"
|
||||||
|
}
|
37
frontend/cypress/support/commands.ts
Normal file
37
frontend/cypress/support/commands.ts
Normal 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>
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
20
frontend/cypress/support/e2e.ts
Normal file
20
frontend/cypress/support/e2e.ts
Normal 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')
|
@ -3,6 +3,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"test": "npx vitest run --reporter verbose",
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
@ -14,12 +15,17 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^1.0.0",
|
"@sveltejs/adapter-auto": "^1.0.0",
|
||||||
"@sveltejs/kit": "^1.0.0",
|
"@sveltejs/kit": "^1.0.0",
|
||||||
|
"@testing-library/svelte": "^3.2.2",
|
||||||
"@types/chroma-js": "^2.1.4",
|
"@types/chroma-js": "^2.1.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
|
"@vitest/browser": "^0.28.4",
|
||||||
|
"@vitest/ui": "^0.28.4",
|
||||||
|
"cypress": "^12.6.0",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-svelte3": "^4.0.0",
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
|
"jsdom": "^21.1.0",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"prettier-plugin-svelte": "^2.8.1",
|
"prettier-plugin-svelte": "^2.8.1",
|
||||||
"sass": "^1.53.0",
|
"sass": "^1.53.0",
|
||||||
@ -28,21 +34,27 @@
|
|||||||
"svelte-preprocess": "^4.10.7",
|
"svelte-preprocess": "^4.10.7",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^4.9.3",
|
"typescript": "^4.9.3",
|
||||||
"vite": "^4.0.0"
|
"vite": "^4.0.0",
|
||||||
|
"vitest": "^0.28.4"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltestack/svelte-query": "^1.6.0",
|
"@sveltestack/svelte-query": "^1.6.0",
|
||||||
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"axios": "^1.2.2",
|
"axios": "^1.2.2",
|
||||||
"chroma-js": "^2.4.2",
|
"chroma-js": "^2.4.2",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"qs": "^6.11.0",
|
"qs": "^6.11.0",
|
||||||
|
"reconnecting-websocket": "^4.4.0",
|
||||||
"svelecte": "^3.13.0",
|
"svelecte": "^3.13.0",
|
||||||
"svelte-forms": "^2.3.1",
|
"svelte-forms": "^2.3.1",
|
||||||
|
"svelte-htm": "^1.2.0",
|
||||||
"svelte-icons": "^2.1.0",
|
"svelte-icons": "^2.1.0",
|
||||||
|
"svelte-markdown": "^0.2.3",
|
||||||
"svelte-multiselect": "^8.2.3",
|
"svelte-multiselect": "^8.2.3",
|
||||||
"svelte-navigator": "^3.2.2",
|
"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
2534
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
15
frontend/src/apis/auth.api.ts
Normal file
15
frontend/src/apis/auth.api.ts
Normal 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);
|
||||||
|
});
|
@ -1,6 +1,7 @@
|
|||||||
import { browser } from '$app/environment';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { parse, stringify } from 'qs'
|
import { parse, stringify } from 'qs'
|
||||||
|
import { autoRefresh } from '../utils/utils';
|
||||||
|
|
||||||
export const exoInstance = axios.create({
|
export const exoInstance = axios.create({
|
||||||
paramsSerializer:{encode:(params)=> {return parse(params, {arrayFormat:"brackets"})}, serialize: (p)=>{return stringify(p, {arrayFormat: "repeat"})}},
|
paramsSerializer:{encode:(params)=> {return parse(params, {arrayFormat:"brackets"})}, serialize: (p)=>{return stringify(p, {arrayFormat: "repeat"})}},
|
||||||
baseURL: `http://127.0.0.1:8002`,
|
baseURL: `http://127.0.0.1:8002`,
|
||||||
@ -9,7 +10,9 @@ export const exoInstance = axios.create({
|
|||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
|
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
|
||||||
...(browser &&
|
|
||||||
localStorage.getItem('token') && { Authorization: `Bearer ${localStorage.getItem('token')}` })
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
exoInstance.interceptors.request.use(autoRefresh, (error) => {
|
||||||
|
Promise.reject(error);
|
||||||
|
});
|
||||||
|
19
frontend/src/apis/room.api.ts
Normal file
19
frontend/src/apis/room.api.ts
Normal 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);
|
||||||
|
}
|
||||||
|
);
|
@ -4,6 +4,44 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
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 {
|
.btn {
|
||||||
border: none;
|
border: none;
|
||||||
@ -13,9 +51,12 @@
|
|||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
margin-right: 7px;
|
margin-right: 7px;
|
||||||
padding: 0 10%;
|
padding: 0 50px;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
&:disabled{
|
||||||
|
cursor: not-allowed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-btn {
|
.primary-btn {
|
||||||
@ -61,7 +102,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-bottom-color: red;
|
border-bottom-color: $contrast;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,4 +120,9 @@
|
|||||||
|
|
||||||
.sv-dropdown{
|
.sv-dropdown{
|
||||||
z-index: 10!important;
|
z-index: 10!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.strong{
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
207
frontend/src/components/NavBar.svelte
Normal file
207
frontend/src/components/NavBar.svelte
Normal 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>
|
@ -2,15 +2,16 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
export let href = "/";
|
export let href = "/";
|
||||||
export let exact = false;
|
export let exact = false;
|
||||||
|
export let no_hover = false;
|
||||||
</script>
|
</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">
|
<style lang="scss">
|
||||||
a {
|
a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
color: yellow;
|
color: $primary-dark;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -22,7 +23,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: currentColor;
|
background: currentColor;
|
||||||
top: 100%;
|
bottom: 5px;
|
||||||
left: 0;
|
left: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transform-origin: 100% 50%;
|
transform-origin: 100% 50%;
|
||||||
@ -30,9 +31,9 @@
|
|||||||
transition: transform 0.3s;
|
transition: transform 0.3s;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
color: red;
|
color: $primary;
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
&::before {
|
&:not(.no_hover)::before {
|
||||||
transform-origin: 0% 50%;
|
transform-origin: 0% 50%;
|
||||||
transform: scale3d(1, 1, 1);
|
transform: scale3d(1, 1, 1);
|
||||||
}
|
}
|
||||||
@ -40,7 +41,7 @@
|
|||||||
}
|
}
|
||||||
.selected {
|
.selected {
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
color: red;
|
color: $primary;
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
&::before {
|
&::before {
|
||||||
content: none;
|
content: none;
|
||||||
|
46
frontend/src/components/auth/InfoForm.svelte
Normal file
46
frontend/src/components/auth/InfoForm.svelte
Normal 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>
|
31
frontend/src/components/auth/PasswordForm.svelte
Normal file
31
frontend/src/components/auth/PasswordForm.svelte
Normal 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>
|
40
frontend/src/components/auth/RoomList.svelte
Normal file
40
frontend/src/components/auth/RoomList.svelte
Normal 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>
|
53
frontend/src/components/auth/Section.svelte
Normal file
53
frontend/src/components/auth/Section.svelte
Normal 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>
|
35
frontend/src/components/auth/UserConfirm.svelte
Normal file
35
frontend/src/components/auth/UserConfirm.svelte
Normal 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>
|
@ -7,11 +7,13 @@
|
|||||||
import TagContainer from './TagContainer.svelte';
|
import TagContainer from './TagContainer.svelte';
|
||||||
import PrivacyIndicator from './PrivacyIndicator.svelte';
|
import PrivacyIndicator from './PrivacyIndicator.svelte';
|
||||||
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte';
|
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
|
||||||
export let exo: Exercice;
|
export let exo: Exercice;
|
||||||
|
|
||||||
const { show } = getContext<{ show: Function }>('modal');
|
const { show } = getContext<{ show: Function }>('modal');
|
||||||
const { navigate } = getContext<{ navigate: Function }>('navigation');
|
const { navigate } = getContext<{ navigate: Function }>('navigation');
|
||||||
const { isAuth } = getContext<{ isAuth: boolean }>('auth');
|
const { isAuth } = getContext<{ isAuth: Writable<boolean> }>('auth');
|
||||||
const exerciceStore = getContext('exos');
|
const exerciceStore = getContext('exos');
|
||||||
const tagsStore = getContext('tags');
|
const tagsStore = getContext('tags');
|
||||||
|
|
||||||
@ -19,10 +21,6 @@
|
|||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
opened = true;
|
opened = true;
|
||||||
navigate(`/exercices/${exo.id_code}`);
|
navigate(`/exercices/${exo.id_code}`);
|
||||||
};
|
|
||||||
|
|
||||||
let tg = false;
|
|
||||||
$: !!opened &&
|
|
||||||
show(
|
show(
|
||||||
ModalCard,
|
ModalCard,
|
||||||
{
|
{
|
||||||
@ -35,36 +33,36 @@
|
|||||||
opened = false;
|
opened = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class="card" on:click={handleClick} on:dblclick={() => {}} on:keypress={() => {}}>
|
||||||
class="card"
|
|
||||||
class:tagMode={tg}
|
|
||||||
on:click={handleClick}
|
|
||||||
on:dblclick={() => {}}
|
|
||||||
on:keypress={() => {}}
|
|
||||||
>
|
|
||||||
<h1>{exo.name}</h1>
|
<h1>{exo.name}</h1>
|
||||||
<div class="examples">
|
<div class="examples">
|
||||||
<h2>Exemples</h2>
|
{#if exo.examples != null}
|
||||||
{#if !!exo.consigne}<p>{exo.consigne}</p>{/if}
|
<h2>Exemples</h2>
|
||||||
{#each exo.examples.data.slice(0, 3) as ex}
|
{#if !!exo.consigne}<p data-testid="consigne">{exo.consigne}</p>{/if}
|
||||||
<p>{ex.calcul}</p>
|
{#each exo.examples.data.slice(0, 3) as ex}
|
||||||
{/each}
|
<p>{ex.calcul}</p>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<p>Aucun exemple disponible</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if !!isAuth}
|
{#if !!$isAuth && exo.is_author && exo.original == null }
|
||||||
{#if exo.is_author && exo.original == null}
|
<div class="status">
|
||||||
<div class="status">
|
<PrivacyIndicator color={exo.private == true ? 'red' : 'green'}>
|
||||||
<PrivacyIndicator color={exo.private == true ? 'red' : 'green'}>
|
{exo.private == true ? 'Privé' : 'Public'}</PrivacyIndicator
|
||||||
{exo.private == true ? 'Privé' : 'Public'}</PrivacyIndicator
|
>
|
||||||
>
|
</div>
|
||||||
</div>
|
{:else if !exo.is_author}
|
||||||
{:else if !exo.is_author}
|
<div class="status">
|
||||||
<div class="status">
|
<PrivacyIndicator color={'blue'}>
|
||||||
<PrivacyIndicator color={'blue'}>
|
Par <strong>{exo.author.username}</strong>
|
||||||
Par <strong>{exo.author.username}</strong>
|
</PrivacyIndicator>
|
||||||
</PrivacyIndicator>
|
{#if !!$isAuth}
|
||||||
<div
|
<div
|
||||||
|
data-testid="copy"
|
||||||
class="icon"
|
class="icon"
|
||||||
on:keydown={() => {}}
|
on:keydown={() => {}}
|
||||||
on:click|stopPropagation={() => {
|
on:click|stopPropagation={() => {
|
||||||
@ -78,15 +76,15 @@
|
|||||||
>
|
>
|
||||||
<MdContentCopy />
|
<MdContentCopy />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{:else if exo.is_author && exo.original != null}
|
</div>
|
||||||
<div class="status">
|
{:else if exo.is_author && exo.original != null}
|
||||||
<PrivacyIndicator color="blue">Par <strong>{exo.original?.author}</strong></PrivacyIndicator
|
<div class="status">
|
||||||
>
|
<PrivacyIndicator color="blue">Par <strong>{exo.original?.author}</strong></PrivacyIndicator>
|
||||||
</div>
|
</div>
|
||||||
{/if}{/if}
|
{/if}
|
||||||
<div class="card-hover" />
|
<div class="card-hover" />
|
||||||
{#if !!isAuth}
|
{#if !!$isAuth}
|
||||||
<TagContainer bind:exo />
|
<TagContainer bind:exo />
|
||||||
{/if}
|
{/if}
|
||||||
<!-- TagContainer Must be directly after card-hover for the hover effect -->
|
<!-- TagContainer Must be directly after card-hover for the hover effect -->
|
||||||
@ -188,7 +186,7 @@
|
|||||||
background-color: $background;
|
background-color: $background;
|
||||||
min-height: 250px;
|
min-height: 250px;
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
&:not(.tagMode):hover {
|
&:hover {
|
||||||
transform: translateX(10px) translateY(-10px);
|
transform: translateX(10px) translateY(-10px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,10 @@
|
|||||||
<EditForm editing={false} {cancel} {updateExo} />
|
<EditForm editing={false} {cancel} {updateExo} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
|
@import '../../variables';
|
||||||
div {
|
div {
|
||||||
background-color: blue;
|
background: $background;
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -12,13 +12,16 @@
|
|||||||
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte';
|
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte';
|
||||||
import ModalCard from './ModalCard.svelte';
|
import ModalCard from './ModalCard.svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
import {generateRequest} from "../../requests/exo.request.js";
|
||||||
export let exo: Exercice;
|
export let exo: Exercice;
|
||||||
export let edit: Function;
|
export let edit: Function=()=>{};
|
||||||
export let delete_: Function;
|
export let delete_: Function=()=>{};
|
||||||
|
|
||||||
const { close, show } = getContext<{ close: Function; show: Function }>('modal');
|
const { close, show } = getContext<{ close: Function; show: Function }>('modal');
|
||||||
const { alert } = getContext<{ alert: Function }>('alert');
|
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 = '';
|
let name = '';
|
||||||
</script>
|
</script>
|
||||||
@ -61,7 +64,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</h1>
|
</h1>
|
||||||
<InputWithLabel type="text" value={name} label="Nom" />
|
<InputWithLabel type="text" bind:value={name} label="Nom" disabled={!exo.supports.csv}/>
|
||||||
|
|
||||||
<div class="examples">
|
<div class="examples">
|
||||||
<h2>Exemples</h2>
|
<h2>Exemples</h2>
|
||||||
@ -72,11 +75,13 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-row-center wp-100">
|
<div class="flex-row-center wp-100">
|
||||||
<button class="primary-btn">Télécharger</button>
|
<button class="primary-btn" disabled= {!exo.supports.csv} on:click={()=>{
|
||||||
</div>
|
generateRequest(exo.id_code, name)
|
||||||
<div class="tags" />
|
|
||||||
|
|
||||||
{#if !!isAuth}
|
}}>Télécharger</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !!$isAuth}
|
||||||
<TagContainer {exo} />
|
<TagContainer {exo} />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="icons">
|
<div class="icons">
|
||||||
@ -87,7 +92,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#if !!isAuth}
|
{#if !!$isAuth}
|
||||||
{#if exo.is_author}
|
{#if exo.is_author}
|
||||||
<div
|
<div
|
||||||
class="icon"
|
class="icon"
|
||||||
@ -97,16 +102,19 @@
|
|||||||
title: 'Sur ?',
|
title: 'Sur ?',
|
||||||
description: 'Voulez vous supprimer ? ',
|
description: 'Voulez vous supprimer ? ',
|
||||||
validate: () => {
|
validate: () => {
|
||||||
close();
|
delExo(exo.id_code).then((r)=>{
|
||||||
|
close();
|
||||||
delete_();
|
delete_();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
on:keypress={() => {}}
|
on:keypress={() => {}}
|
||||||
|
data-testid="delete"
|
||||||
>
|
>
|
||||||
<MdDelete />
|
<MdDelete />
|
||||||
</div>
|
</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 />
|
<MdEdit />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@ -120,7 +128,8 @@
|
|||||||
ModalCard,
|
ModalCard,
|
||||||
{ exo: r },
|
{ exo: r },
|
||||||
() => {
|
() => {
|
||||||
goto('/exercices/user');
|
//goto('/exercices/user');
|
||||||
|
navigate(-2)
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@ -128,6 +137,7 @@
|
|||||||
}}
|
}}
|
||||||
on:keypress={() => {}}
|
on:keypress={() => {}}
|
||||||
title="Copier l'exercice pour pouvoir le modifier"
|
title="Copier l'exercice pour pouvoir le modifier"
|
||||||
|
data-testid="copy"
|
||||||
>
|
>
|
||||||
<MdContentCopy />
|
<MdContentCopy />
|
||||||
</div>
|
</div>
|
||||||
@ -173,7 +183,7 @@
|
|||||||
span.name {
|
span.name {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
|
||||||
}
|
}
|
||||||
span:not(.name) {
|
span:not(.name) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
import type { Exercice } from '../../types/exo.type';
|
import type { Exercice } from '../../types/exo.type';
|
||||||
import { checkFile, errorMsg } from '../../utils/forms';
|
import { checkFile, errorMsg } from '../../utils/forms';
|
||||||
import { compareObject } from '../../utils/utils';
|
import { compareObject } from '../../utils/utils';
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
export let editing = true;
|
export let editing = true;
|
||||||
export let updateExo: Function = (e: Exercice) => {};
|
export let updateExo: Function = (e: Exercice) => {};
|
||||||
|
|
||||||
@ -18,10 +18,10 @@
|
|||||||
const { alert } = getContext<{ alert: Function }>('alert');
|
const { alert } = getContext<{ alert: Function }>('alert');
|
||||||
|
|
||||||
// "Legally" initiate empty FileList for model field (simple list raises warning)
|
// "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);
|
let file = new File(['content'], !editing || exo == null ? 'filename.py' : exo.exo_source);
|
||||||
list.items.add(file);
|
list.items.add(file);
|
||||||
!editing && list.items.remove(0);
|
!editing && list.items.remove(0); */
|
||||||
|
|
||||||
// Initiate fields and form
|
// Initiate fields and form
|
||||||
const name = field('name', !!exo ? exo.name : '', [required(), max(50), min(5)], {
|
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 consigne = field('consigne', !!exo && exo.consigne != null ? exo.consigne : '', [max(200)], { checkOnInit: true });
|
||||||
const prv = field('private', !!exo ? exo.private : false);
|
const prv = field('private', !!exo ? exo.private : false);
|
||||||
const model = field('model', list.files, [checkFile(), required()], {
|
const model = field('model', [], [checkFile(), required()], {
|
||||||
checkOnInit: !editing
|
checkOnInit: !editing
|
||||||
});
|
});
|
||||||
const myForm = form(name, consigne, prv, model);
|
const myForm = form(name, consigne, prv, model);
|
||||||
@ -70,6 +70,7 @@
|
|||||||
required
|
required
|
||||||
label="Nom"
|
label="Nom"
|
||||||
errors={errorMsg($myForm, 'name')}
|
errors={errorMsg($myForm, 'name')}
|
||||||
|
name="name"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputWithLabel
|
<InputWithLabel
|
||||||
@ -78,25 +79,26 @@
|
|||||||
maxlength="200"
|
maxlength="200"
|
||||||
label="Consigne"
|
label="Consigne"
|
||||||
errors={errorMsg($myForm, 'consigne')}
|
errors={errorMsg($myForm, 'consigne')}
|
||||||
|
name="consigne"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<input type="checkbox" bind:checked={$prv.value} name="private" id="private" />
|
<input type="checkbox" bind:checked={$prv.value} name="private" id="private" />
|
||||||
<label for="private">Privé</label>
|
<label for="private">Privé</label>
|
||||||
</div>
|
</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">
|
<div class="wp-100">
|
||||||
<button class="primary-btn" disabled={!$myForm.valid}>Valider</button>
|
<button class="primary-btn" disabled={!$myForm.valid}>Modifier</button>
|
||||||
<button
|
<button
|
||||||
class="danger-btn"
|
class="danger-btn"
|
||||||
on:click|preventDefault={() => {
|
on:click|preventDefault={() => {
|
||||||
|
|
||||||
if (exo != null && ($model.dirty || !compareObject({...exo, consigne: exo.consigne == null ? "": exo.consigne}, myForm.summary()))) {
|
if (exo != null && ($model.dirty || !compareObject({...exo, consigne: exo.consigne == null ? "": exo.consigne}, myForm.summary()))) {
|
||||||
alert({
|
alert({
|
||||||
title: 'test',
|
title: 'Voulez-vous annuler ?',
|
||||||
description:
|
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
|
validate: cancel
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
132
frontend/src/components/exos/ExerciceSelector.svelte
Normal file
132
frontend/src/components/exos/ExerciceSelector.svelte
Normal 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>
|
165
frontend/src/components/exos/ExoList.svelte
Normal file
165
frontend/src/components/exos/ExoList.svelte
Normal 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>
|
@ -3,96 +3,140 @@
|
|||||||
import Card from './Card.svelte';
|
import Card from './Card.svelte';
|
||||||
import Head from './Head.svelte';
|
import Head from './Head.svelte';
|
||||||
import ModalCard from './ModalCard.svelte';
|
import ModalCard from './ModalCard.svelte';
|
||||||
import { Query, useQueryClient, type QueryOptions } from '@sveltestack/svelte-query';
|
|
||||||
import { getExo, getExos, getTags } from '../../requests/exo.request';
|
import { getExo, getExos, getTags } from '../../requests/exo.request';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Pagination from './Pagination.svelte';
|
import Pagination from './Pagination.svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { writable } from 'svelte/store';
|
import {type Writable, writable} from 'svelte/store';
|
||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
import type { Page, Tag } from '../../types/exo.type';
|
import type { Page, Tag } from '../../types/exo.type';
|
||||||
import type { Store } from '../../types/api.type';
|
import type { Store } from '../../types/api.type';
|
||||||
|
import { page as p } from '$app/stores';
|
||||||
|
|
||||||
const { show } = getContext<{ show: Function }>('modal');
|
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,
|
isLoading: false,
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isSuccess: false,
|
isSuccess: false,
|
||||||
data: undefined
|
data: undefined
|
||||||
});
|
});
|
||||||
const tagStore = writable<Store<Tag[]|undefined>>({
|
const tagStore = writable<Store<Tag[] | undefined>>({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isSuccess: false,
|
isSuccess: false,
|
||||||
data: undefined
|
data: []
|
||||||
});
|
});
|
||||||
|
|
||||||
setContext('exos', exerciceStore);
|
setContext('exos', exerciceStore);
|
||||||
setContext('tags', tagStore);
|
setContext('tags', tagStore);
|
||||||
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
let page = $page.url.searchParams.get('page')
|
||||||
|
if(page == null){
|
||||||
|
page = '1'
|
||||||
|
}
|
||||||
if ($page.params.slug != undefined && !['user', 'public'].includes($page.params.slug)) {
|
if ($page.params.slug != undefined && !['user', 'public'].includes($page.params.slug)) {
|
||||||
getExo($page.params.slug).then((r) => {
|
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) {
|
} else if ($page.params.slug == undefined || $page.params.slug == "user") {
|
||||||
goto('/exercices/public');
|
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;
|
$: 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) => {
|
exerciceStore.update((s) => {
|
||||||
return { ...s, isFetching: true };
|
return { ...s, isFetching: true };
|
||||||
});
|
});
|
||||||
getExos(filter as 'public' | 'user', {
|
getExos(filter as 'public' | 'user', {
|
||||||
page: activePage,
|
page: activePage == 0 ? 1: activePage,
|
||||||
search,
|
search,
|
||||||
|
size,
|
||||||
tags: [...selected.map((t) => t.id_code)]
|
tags: [...selected.map((t) => t.id_code)]
|
||||||
}).then((r) => {
|
})
|
||||||
exerciceStore.update((e) => {
|
.then((r) => {
|
||||||
return { ...e, isSuccess: true, isFetching: false, data: 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}});
|
if($isAuth) {
|
||||||
getTags().then(r=>{
|
tagStore.update((s) => {
|
||||||
tagStore.update((e) => {
|
return {...s, isFetching: true};
|
||||||
return { ...e, isSuccess: true, isFetching: false, data: r };
|
|
||||||
});
|
});
|
||||||
})
|
getTags().then((r) => {
|
||||||
|
tagStore.update((e) => {
|
||||||
|
return {...e, isSuccess: true, isFetching: false, data: r};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let activePage = parseInt($page.url.searchParams.get('page')!) || 1;
|
|
||||||
let search = '';
|
|
||||||
let selected: Tag[] = [];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if $tagStore.data != undefined}
|
||||||
{#if $tagStore.isSuccess == true && $tagStore.data != undefined}
|
|
||||||
<Head location={filter} bind:search bind:selected />
|
<Head location={filter} bind:search bind:selected />
|
||||||
{/if}
|
{/if}
|
||||||
{#if $tagStore.isFetching == true}
|
{#if $tagStore.isFetching == true}
|
||||||
Fetching
|
Fetching
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="feed">
|
<div class="feed">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h1>
|
{#if filter == 'user'}
|
||||||
Tous les <span>exercices</span>
|
<h1>
|
||||||
</h1>
|
Vos <span>exercices</span>
|
||||||
<p>
|
</h1>
|
||||||
Vous retrouverez ici tous les exercices que vous avez créé ou copié depuis les exercices
|
<p>
|
||||||
publics
|
Vous retrouverez ici tous les exercices que vous avez créé ou copié depuis les exercices
|
||||||
</p>
|
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>
|
</div>
|
||||||
{#if $exerciceStore.data != undefined}
|
{#if $exerciceStore.data != undefined}
|
||||||
{#each $exerciceStore.data.items.filter((e) => e != null && selected.every((t) => e.tags
|
{#each $exerciceStore.data.items.filter((e) => e != null && selected.every((t) => e.tags
|
||||||
@ -100,11 +144,63 @@
|
|||||||
.includes(t.id_code))) as e}
|
.includes(t.id_code))) as e}
|
||||||
<Card bind:exo={e} />
|
<Card bind:exo={e} />
|
||||||
{/each}
|
{/each}
|
||||||
<Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} />
|
{:else}
|
||||||
|
{#each Array(10) as i}
|
||||||
|
<div class="skeleton"><span /></div>
|
||||||
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{#if $exerciceStore.data != undefined}
|
||||||
|
<Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<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 {
|
.feed {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
@ -129,7 +225,7 @@
|
|||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
color: red;
|
color: $primary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -7,20 +7,31 @@
|
|||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
import type { Store } from '../../types/api.type';
|
import type { Store } from '../../types/api.type';
|
||||||
|
|
||||||
const { navigate } = getContext<{navigate: Function}>('navigation');
|
const { navigate } = getContext<{ navigate: Function }>('navigation');
|
||||||
const { show, close } = getContext<{show: Function, close: Function}>('modal');
|
const { show, close } = getContext<{ show: Function; close: Function }>('modal');
|
||||||
|
|
||||||
export let location = 'public';
|
export let location = 'public';
|
||||||
export let search = '';
|
export let search = '';
|
||||||
export let selected: Tag[] = [];
|
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');
|
const exerciceStore: Writable<Store<Page>> = getContext('exos');
|
||||||
|
let updateText = null;
|
||||||
|
let tmp =""
|
||||||
|
$: {
|
||||||
|
if(updateText != null){
|
||||||
|
clearTimeout(updateText);
|
||||||
|
}
|
||||||
|
updateText = window.setTimeout(() => {
|
||||||
|
search = tmp;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<div class="new">
|
<div class="new">
|
||||||
{#if !!isAuth}
|
{#if !!$isAuth}
|
||||||
<button
|
<button
|
||||||
class="border-primary-btn"
|
class="border-primary-btn"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@ -35,15 +46,22 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<input type="text" placeholder="Rechercher" class="input" bind:value={search} />
|
<input
|
||||||
{#if !!isAuth}
|
type="text"
|
||||||
|
placeholder="Rechercher"
|
||||||
|
class="input"
|
||||||
|
bind:value={tmp}
|
||||||
|
|
||||||
|
/>
|
||||||
|
{#if !!$isAuth}
|
||||||
<TagSelector options={$tags.data} bind:selected />
|
<TagSelector options={$tags.data} bind:selected />
|
||||||
<select
|
<select
|
||||||
name="ee"
|
name="tagsSelect"
|
||||||
id="e"
|
id="tagsSelect"
|
||||||
class="input"
|
class="input"
|
||||||
bind:value={location}
|
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="user">Vos exos</option>
|
||||||
<option value="public">Tous les exos</option>
|
<option value="public">Tous les exos</option>
|
||||||
|
@ -1,35 +1,137 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {page as p} from '$app/stores'
|
import { page as p } from '$app/stores';
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from '$app/navigation';
|
||||||
export let page: number;
|
export let page: number;
|
||||||
export let total: number;
|
export let total: number;
|
||||||
|
|
||||||
|
const changePage = (p: number) => {
|
||||||
|
goto(`?${new URLSearchParams({page: p}).toString()}`);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
{#each Array(total) as _, i}
|
<button
|
||||||
<p
|
on:click={() => {
|
||||||
class:active={page == i + 1}
|
changePage(page - 1);
|
||||||
on:click={() => {
|
}}
|
||||||
page = i + 1;
|
on:keydown={() => {}}
|
||||||
$p.url.searchParams.set('page', String(i+1))
|
disabled={page <= 1}
|
||||||
goto(`?${$p.url.searchParams.toString()}`);
|
>
|
||||||
}}
|
{'<'}
|
||||||
on:keydown = {()=>{}}
|
</button>
|
||||||
>
|
|
||||||
{i + 1}
|
{#if total >= 7}
|
||||||
</p>
|
<!-- First two -->
|
||||||
{/each}
|
{#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>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.active {
|
@import '../../variables';
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
.pagination {
|
.pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 30px;
|
margin: 30px;
|
||||||
p {
|
height: max-content;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
button {
|
||||||
margin: 10px;
|
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>
|
</style>
|
||||||
|
@ -1,38 +1,37 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import chroma from 'chroma-js';
|
import chroma from 'chroma-js';
|
||||||
|
|
||||||
export let label: string;
|
export let label: string;
|
||||||
export let color: string;
|
export let color: string;
|
||||||
export let remove: Function;
|
console.log(color)
|
||||||
|
export let remove: Function | null = null;
|
||||||
let removed = false;
|
let removed = false;
|
||||||
|
|
||||||
</script>
|
</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="label">{label}</div>
|
||||||
<div
|
{#if !!remove}
|
||||||
class="unselect"
|
<button
|
||||||
on:click={() => {
|
class="unselect"
|
||||||
removed = true;
|
on:click={() => {
|
||||||
remove()
|
removed = true;
|
||||||
/* setTimeout(() => {
|
remove && remove();
|
||||||
if(!remove()){
|
}}
|
||||||
removed=false
|
on:keypress={() => {}}
|
||||||
}
|
|
||||||
}, 300); */
|
|
||||||
}}
|
|
||||||
on:keypress={() => {}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
height="14"
|
|
||||||
width="14"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
aria-hidden="true"
|
|
||||||
focusable="false"
|
|
||||||
class="css-8mmkcg"
|
|
||||||
><path
|
|
||||||
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
|
|
||||||
/></svg
|
|
||||||
>
|
>
|
||||||
</div>
|
<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>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@ -65,7 +64,7 @@
|
|||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: 0.5s;
|
transition: 0.5s;
|
||||||
max-width: 100px;
|
//max-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.removed {
|
.removed {
|
||||||
@ -82,4 +81,8 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:not(.removable) > .label{
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -13,11 +13,12 @@
|
|||||||
let tagMode = false;
|
let tagMode = false;
|
||||||
let selected: { label: string; id_code: string; color: string, created?: boolean }[] = [];
|
let selected: { label: string; id_code: string; color: string, created?: boolean }[] = [];
|
||||||
export let tags: Writable<Store<TagType[]>> = getContext('tags');
|
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>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="tags-container"
|
class="tags-container"
|
||||||
|
data-testid="tags"
|
||||||
class:tg
|
class:tg
|
||||||
class:tagMode
|
class:tagMode
|
||||||
on:click|stopPropagation={() => {}}
|
on:click|stopPropagation={() => {}}
|
||||||
@ -31,6 +32,9 @@
|
|||||||
remove={() => {
|
remove={() => {
|
||||||
delTags(exo.id_code, t.id_code).then((r) => {
|
delTags(exo.id_code, t.id_code).then((r) => {
|
||||||
exo.tags = r.tags;
|
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;
|
return true;
|
||||||
}}
|
}}
|
||||||
@ -42,7 +46,6 @@
|
|||||||
class="expand"
|
class="expand"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
tg = true;
|
tg = true;
|
||||||
console.log('TAGGGG', $tags)
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
tagMode = true;
|
tagMode = true;
|
||||||
}, 200);
|
}, 200);
|
||||||
@ -71,6 +74,9 @@
|
|||||||
}
|
}
|
||||||
tg = false;
|
tg = false;
|
||||||
tagMode = 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
|
}}>Valider !</button
|
||||||
>
|
>
|
||||||
@ -161,5 +167,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
:global(> button){
|
||||||
|
width: 99%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
53
frontend/src/components/exos/TagViewer.svelte
Normal file
53
frontend/src/components/exos/TagViewer.svelte
Normal 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>
|
@ -3,6 +3,7 @@
|
|||||||
import MdFileDownload from 'svelte-icons/md/MdFileDownload.svelte';
|
import MdFileDownload from 'svelte-icons/md/MdFileDownload.svelte';
|
||||||
export let label = 'Choisir un fichier';
|
export let label = 'Choisir un fichier';
|
||||||
export let value: FileList;
|
export let value: FileList;
|
||||||
|
export let defaultValue: string|null;
|
||||||
export let id_code: string | null = null;
|
export let id_code: string | null = null;
|
||||||
|
|
||||||
const id = String(Math.random());
|
const id = String(Math.random());
|
||||||
@ -12,8 +13,8 @@
|
|||||||
<input type="file" {id} {...$$restProps} bind:files={value} />
|
<input type="file" {id} {...$$restProps} bind:files={value} />
|
||||||
<label for={id}>{label}</label>
|
<label for={id}>{label}</label>
|
||||||
<div class="filename">
|
<div class="filename">
|
||||||
{#if value.length !== 0}
|
{#if value.length !== 0 || defaultValue != null}
|
||||||
<p>{value[0].name}</p>
|
<p>{value.length !== 0 && value[0] != undefined? value[0].name : defaultValue!=null?defaultValue:"..."}</p>
|
||||||
{#if id_code != null}
|
{#if id_code != null}
|
||||||
<div
|
<div
|
||||||
class="icon"
|
class="icon"
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import IoMdEye from 'svelte-icons/io/IoMdEye.svelte';
|
import IoMdEye from 'svelte-icons/io/IoMdEye.svelte';
|
||||||
import IoMdEyeOff from 'svelte-icons/io/IoMdEyeOff.svelte';
|
import IoMdEyeOff from 'svelte-icons/io/IoMdEyeOff.svelte';
|
||||||
|
|
||||||
export let type = 'text';
|
export let type = 'text';
|
||||||
export let value = '';
|
export let value: string | null = null;
|
||||||
export let label = '';
|
export let label = '';
|
||||||
export let errors: string[] = [];
|
export let errors: string[] = [];
|
||||||
|
export let change: Function = (e: Event) => {};
|
||||||
function typeAction(node: HTMLInputElement) {
|
function typeAction(node: HTMLInputElement) {
|
||||||
node.type = type;
|
node.type = type;
|
||||||
}
|
}
|
||||||
@ -17,10 +19,24 @@
|
|||||||
element.type = show === true ? 'password' : 'text';
|
element.type = show === true ? 'password' : 'text';
|
||||||
show = !show;
|
show = !show;
|
||||||
};
|
};
|
||||||
|
let test: HTMLInputElement;
|
||||||
|
export const focus = () => {
|
||||||
|
console.log('FOCUS', test)
|
||||||
|
test.focus();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="inputLabel" class:error={errors.length !== 0}>
|
<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 -->
|
<!-- placeholder = "" pour que le label se place bien avec :placeholder-shown -->
|
||||||
<label for={id}>{label}</label>
|
<label for={id}>{label}</label>
|
||||||
{#if type == 'password'}
|
{#if type == 'password'}
|
||||||
@ -58,7 +74,7 @@
|
|||||||
}
|
}
|
||||||
.inputLabel {
|
.inputLabel {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -8,8 +8,8 @@
|
|||||||
export let isSelected = false;
|
export let isSelected = false;
|
||||||
export let isDisabled = false;
|
export let isDisabled = false;
|
||||||
//export let isMultiple = false;
|
//export let isMultiple = false;
|
||||||
|
|
||||||
const color = chroma(item.color);
|
const color = chroma("rgb(255,0,0)");
|
||||||
console.log(color.rgb(), color);
|
console.log(color.rgb(), color);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -51,6 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import '../../variables';
|
||||||
.unselect {
|
.unselect {
|
||||||
color: rgb(var(--item-color));
|
color: rgb(var(--item-color));
|
||||||
-moz-box-align: center;
|
-moz-box-align: center;
|
||||||
@ -125,6 +126,9 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
color: grey;
|
color: grey;
|
||||||
background-color: var(--sv-bg);
|
background-color: var(--sv-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.creatable-row-wrap){
|
||||||
|
background-color: red;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
96
frontend/src/components/forms/LabeledInput.svelte
Normal file
96
frontend/src/components/forms/LabeledInput.svelte
Normal 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>
|
@ -55,4 +55,11 @@
|
|||||||
--sv-border: none!important;
|
--sv-border: none!important;
|
||||||
--sv-active-border:(1px solid $contrast)!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>
|
</style>
|
||||||
|
37
frontend/src/components/rooms/AskPseudo.svelte
Normal file
37
frontend/src/components/rooms/AskPseudo.svelte
Normal 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>
|
290
frontend/src/components/rooms/Challenge.svelte
Normal file
290
frontend/src/components/rooms/Challenge.svelte
Normal 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>
|
113
frontend/src/components/rooms/ChallengesList.svelte
Normal file
113
frontend/src/components/rooms/ChallengesList.svelte
Normal 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>
|
41
frontend/src/components/rooms/Classement.svelte
Normal file
41
frontend/src/components/rooms/Classement.svelte
Normal 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>
|
184
frontend/src/components/rooms/InputChallenge.svelte
Normal file
184
frontend/src/components/rooms/InputChallenge.svelte
Normal 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>
|
206
frontend/src/components/rooms/Members.svelte
Normal file
206
frontend/src/components/rooms/Members.svelte
Normal 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>
|
41
frontend/src/components/rooms/Note.svelte
Normal file
41
frontend/src/components/rooms/Note.svelte
Normal 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>
|
143
frontend/src/components/rooms/ParcoursCreate.svelte
Normal file
143
frontend/src/components/rooms/ParcoursCreate.svelte
Normal 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>
|
261
frontend/src/components/rooms/ParcoursDetails.svelte
Normal file
261
frontend/src/components/rooms/ParcoursDetails.svelte
Normal 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>
|
128
frontend/src/components/rooms/ParcoursList.svelte
Normal file
128
frontend/src/components/rooms/ParcoursList.svelte
Normal 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>
|
149
frontend/src/components/rooms/RoomHead.svelte
Normal file
149
frontend/src/components/rooms/RoomHead.svelte
Normal 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>
|
118
frontend/src/components/rooms/Stats.svelte
Normal file
118
frontend/src/components/rooms/Stats.svelte
Normal 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>
|
@ -1,40 +1,114 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, setContext } from 'svelte';
|
import {getContext, onMount, setContext} from 'svelte';
|
||||||
import { writable } from 'svelte/store';
|
import {writable} from 'svelte/store';
|
||||||
import { loginRequest, refreshRequest, registerRequest } from '../requests/auth.request';
|
import {
|
||||||
import jwt_decode from 'jwt-decode';
|
loginRequest,
|
||||||
const user = writable(null);
|
refreshRequest,
|
||||||
const isAuth = writable(false);
|
registerRequest,
|
||||||
const login = (login: string, password: string) => {
|
logoutRequest
|
||||||
loginRequest({ login, password }).then((r) => {
|
} from '../requests/auth.request';
|
||||||
localStorage.setItem('token', `${r.access_token}`);
|
import jwt_decode from 'jwt-decode';
|
||||||
localStorage.setItem('refresh', `${r.refresh_token}`);
|
import {checkExpire} from '../utils/utils';
|
||||||
isAuth.set(true);
|
import {goto} from '$app/navigation';
|
||||||
});
|
import {page} from "$app/stores";
|
||||||
};
|
|
||||||
const register = (username: string, password: string, confirm: string) => {
|
|
||||||
registerRequest({ username, password, password_confirm: confirm }).then((r) => {
|
|
||||||
localStorage.setItem('token', `${r.access_token}`);
|
|
||||||
localStorage.setItem('refresh', `${r.refresh_token}`);
|
|
||||||
isAuth.set(true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const logout = () => {};
|
|
||||||
setContext('auth', { user, isAuth, login, register, logout });
|
|
||||||
|
|
||||||
onMount(() => {
|
const username = writable<string | null>(null);
|
||||||
if (localStorage.getItem('token') != null) {
|
const isAuth = writable(false);
|
||||||
const { exp } = jwt_decode(localStorage.getItem('token')!);
|
const initialLoading = writable(true);
|
||||||
console.log(Date.now(), exp, Date.now() >= exp * 1000)
|
|
||||||
if (Date.now() >= exp * 1000) {
|
const getTokens = () => {
|
||||||
refreshRequest(localStorage.getItem('refresh')!).then(r=>{localStorage.setItem('token', r.access_token)})
|
return {access: localStorage.getItem('token'), refresh: localStorage.getItem('refresh')};
|
||||||
isAuth.set(true)
|
};
|
||||||
return
|
|
||||||
}
|
const {
|
||||||
isAuth.set(true)
|
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>
|
</script>
|
||||||
|
|
||||||
<p>{$isAuth ? 'Connecté' : 'Non connecté'}</p>
|
<p>{$isAuth ? $username : 'Non connecté'}</p>
|
||||||
<slot />
|
{#if !$initialLoading}
|
||||||
|
<slot/>
|
||||||
|
{/if}
|
||||||
|
@ -27,9 +27,6 @@
|
|||||||
onClose = newOnClose;
|
onClose = newOnClose;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addContext (key: string, value: any) {
|
|
||||||
setContext(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
visible = false;
|
visible = false;
|
||||||
@ -39,9 +36,10 @@
|
|||||||
component = undefined;
|
component = undefined;
|
||||||
props = {};
|
props = {};
|
||||||
closed = true;
|
closed = true;
|
||||||
|
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
setContext('modal', { show, close, addContext });
|
setContext('modal', { show, close });
|
||||||
function keyPress(e: KeyboardEvent) {
|
function keyPress(e: KeyboardEvent) {
|
||||||
console.log('HOP');
|
console.log('HOP');
|
||||||
if (e.key == 'Escape' && visible == true) {
|
if (e.key == 'Escape' && visible == true) {
|
||||||
|
@ -5,8 +5,15 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
|
|
||||||
let previous: string | null = base;
|
let previous: string[] = [base];
|
||||||
let first = true;
|
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 = (
|
const navigate = (
|
||||||
url: string | number,
|
url: string | number,
|
||||||
params: object | undefined,
|
params: object | undefined,
|
||||||
@ -23,20 +30,27 @@
|
|||||||
if (browser) {
|
if (browser) {
|
||||||
console.log('PREVIOUS', previous, typeof url == 'number', previous);
|
console.log('PREVIOUS', previous, typeof url == 'number', previous);
|
||||||
if (typeof url == 'number' && previous != null) {
|
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 {
|
} else {
|
||||||
const parsedParams = new URLSearchParams(params as Record<string, string>);
|
const parsedParams = new URLSearchParams(params as Record<string, string>);
|
||||||
goto(`${url}?${parsedParams.toString()}`, { ...options });
|
goto(`${url}?${parsedParams.toString()}`, { ...options });
|
||||||
|
|
||||||
}
|
}
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
afterNavigate(({ from }) => {
|
afterNavigate(({ from }) => {
|
||||||
previous = from?.url.toString() || previous;
|
if (from ) {
|
||||||
|
previous.push(from?.url.toString());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
setContext('navigation', { navigate });
|
setContext('navigation', { navigate, insertUrl });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
160
frontend/src/context/Notification.svelte
Normal file
160
frontend/src/context/Notification.svelte
Normal 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
32
frontend/src/mixins.scss
Normal 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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -1,42 +1,106 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import {authInstance} from '../apis/auth.api';
|
||||||
|
|
||||||
export const loginRequest = (data: { login: string; password: string }) => {
|
export const loginRequest = (data: { username: string; password: string }) => {
|
||||||
return axios({
|
return authInstance
|
||||||
url: 'http://localhost:8002/login',
|
.request({
|
||||||
method: 'POST',
|
url: 'http://localhost:8002/login',
|
||||||
data
|
method: 'POST',
|
||||||
})
|
data,
|
||||||
.then((r) => r.data as {access_token: string, refresh_token: string, token_type: string })
|
headers: {
|
||||||
.catch((e) => {
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
throw e;
|
}
|
||||||
});
|
})
|
||||||
|
.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 }) => {
|
export const registerRequest = (data: {
|
||||||
return axios({
|
username: string;
|
||||||
url: 'http://localhost:8002/register',
|
password: string;
|
||||||
method: 'POST',
|
password_confirm: string;
|
||||||
data,
|
}) => {
|
||||||
headers: {
|
return authInstance
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
.request({
|
||||||
}
|
url: 'http://localhost:8002/register',
|
||||||
})
|
method: 'POST',
|
||||||
.then((r) => r.data as { access_token: string; refresh_token: string; token_type: string })
|
data,
|
||||||
.catch((e) => {
|
headers: {
|
||||||
throw e;
|
'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) => {
|
export const refreshRequest = (token: string) => {
|
||||||
return axios({
|
return authInstance
|
||||||
url: 'http://localhost:8002/refresh',
|
.request({
|
||||||
method: 'POST',
|
url: 'http://localhost:8002/refresh',
|
||||||
headers: {
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
"Authorization": `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((r) => r.data as {access_token:string})
|
.then((r) => r.data as { access_token: string })
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
throw 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});
|
||||||
}
|
}
|
@ -131,4 +131,29 @@ export const getExoSource = (id_code: string,) => {
|
|||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
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();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
158
frontend/src/requests/room.request.ts
Normal file
158
frontend/src/requests/room.request.ts
Normal 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);
|
||||||
|
};
|
@ -1,89 +1,98 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import Modal from '../context/Modal.svelte';
|
import Modal from '../context/Modal.svelte';
|
||||||
import NavLink from '../components/NavLink.svelte';
|
import NavLink from '../components/NavLink.svelte';
|
||||||
import '../app.scss';
|
import '../app.scss';
|
||||||
import Alert from '../context/Alert.svelte';
|
import Alert from '../context/Alert.svelte';
|
||||||
import Auth from '../context/Auth.svelte';
|
import Auth from '../context/Auth.svelte';
|
||||||
import { QueryClient, QueryClientProvider } from '@sveltestack/svelte-query';
|
import {QueryClient, QueryClientProvider} from '@sveltestack/svelte-query';
|
||||||
import { Router } from 'svelte-navigator';
|
import {Router} from 'svelte-navigator';
|
||||||
import Navigation from '../context/Navigation.svelte';
|
import Navigation from '../context/Navigation.svelte';
|
||||||
const queryClient = new QueryClient();
|
import Notification from '../context/Notification.svelte';
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
import type {Writable} from "svelte/store";
|
||||||
|
import NavBar from "../components/NavBar.svelte";
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Navigation>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
<Notification>
|
||||||
<Auth>
|
<Navigation>
|
||||||
<Alert>
|
<Auth>
|
||||||
<Modal>
|
<Alert>
|
||||||
<main>
|
<Modal>
|
||||||
<nav data-sveltekit-preload-data="hover">
|
<main>
|
||||||
<NavLink href="/" exact>Home</NavLink>
|
<NavBar/>
|
||||||
<NavLink href="/exercices" exact>Exercices</NavLink>
|
<slot/>
|
||||||
<NavLink href="/settings" exact>Settings</NavLink>
|
</main>
|
||||||
</nav>
|
</Modal>
|
||||||
<slot />
|
</Alert>
|
||||||
</main>
|
</Auth>
|
||||||
</Modal>
|
</Navigation>
|
||||||
</Alert>
|
</Notification>
|
||||||
</Auth>
|
|
||||||
</QueryClientProvider>
|
|
||||||
</Navigation>
|
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../variables';
|
@import '../variables';
|
||||||
.links {
|
@import "../mixins";
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
.links {
|
||||||
--container-padding: 20px;
|
display: flex;
|
||||||
--container-width: 1330px;
|
align-items: center;
|
||||||
}
|
gap: 14px;
|
||||||
:global(body) {
|
overflow: hidden;
|
||||||
padding: 0;
|
flex-wrap: wrap;
|
||||||
margin: 0;
|
height: 30px;
|
||||||
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 {
|
& li {
|
||||||
display: flex;
|
height: 30px;
|
||||||
justify-content: space-between;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
white-space: nowrap;
|
||||||
padding: 30px 0;
|
}
|
||||||
border-bottom: 1px solid $border;
|
}
|
||||||
width: 100%;
|
|
||||||
gap: 10px;
|
:root {
|
||||||
height: 30px;
|
--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>
|
</style>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user