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
|
||||
from services.password import get_password_hash
|
||||
from database.auth.models import User, UserEdit
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from jose import jwt, exceptions
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from config import SECRET_KEY, ALGORITHM
|
||||
from database.auth.models import User, UserEdit
|
||||
from database.room.models import Member
|
||||
from services.password import get_password_hash
|
||||
|
||||
|
||||
def create_user_db(username:str , password: str, db: Session):
|
||||
user = User(username=username, hashed_password=password, clientId=uuid.uuid4())
|
||||
db.add(user)
|
||||
@ -84,3 +89,7 @@ def change_user_uuid(id: int, db: Session):
|
||||
db.refresh(user)
|
||||
return user.clientId
|
||||
|
||||
|
||||
def parse_user_rooms(user: User, db: Session):
|
||||
members = db.exec(select(Member).where(Member.user_id == user.id)).all()
|
||||
return [{"name": m.room.name, "id_code": m.room.id_code, "admin": m.is_admin} for m in members]
|
||||
|
@ -1,9 +1,11 @@
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
from typing import List
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from uuid import UUID
|
||||
import uuid
|
||||
from sqlmodel import Field, SQLModel, Relationship
|
||||
|
||||
from pydantic import validator, BaseModel
|
||||
from sqlmodel import Field, SQLModel, Relationship
|
||||
|
||||
from services.password import validate_password
|
||||
from services.schema import as_form
|
||||
|
||||
@ -29,9 +31,16 @@ class User(UserBase, table=True):
|
||||
@as_form
|
||||
class UserEdit(UserBase):
|
||||
pass
|
||||
|
||||
class UsersRoom(BaseModel):
|
||||
name: str
|
||||
id_code: str
|
||||
admin: bool = False
|
||||
class UserRead(UserBase):
|
||||
id: int
|
||||
rooms: List[UsersRoom] = []
|
||||
class UserEditRead(UserBase):
|
||||
id: int
|
||||
#rooms: List[UsersRoom] = []
|
||||
|
||||
|
||||
@as_form
|
||||
|
@ -2,7 +2,7 @@ import pydantic.json
|
||||
import json
|
||||
from sqlmodel import SQLModel, create_engine, Session, select
|
||||
|
||||
sqlite_file_name = "database.db"
|
||||
sqlite_file_name = "database7.db"
|
||||
sqlite_url = f"sqlite:///{sqlite_file_name}"
|
||||
|
||||
|
||||
@ -19,6 +19,6 @@ def create_db_and_tables():
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
def get_session():
|
||||
with Session(engine) as s:
|
||||
with Session(engine, expire_on_commit=False) as s:
|
||||
yield s
|
||||
|
||||
|
@ -18,7 +18,7 @@ if TYPE_CHECKING:
|
||||
from database.auth.models import User
|
||||
|
||||
|
||||
class ExampleEnum(Enum):
|
||||
class ExampleEnum(str, Enum):
|
||||
csv = 'csv'
|
||||
pdf = 'pdf'
|
||||
web = 'web'
|
||||
|
@ -1,17 +1,21 @@
|
||||
from services.auth import get_current_user_optional
|
||||
from services.misc import noteOn20
|
||||
from copy import deepcopy
|
||||
from typing import Dict, List
|
||||
import uuid
|
||||
from fastapi import Body, Depends, HTTPException, status
|
||||
from copy import deepcopy
|
||||
from typing import List
|
||||
|
||||
from fastapi import Depends, HTTPException, status, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import Session, delete, select, col, table
|
||||
from database.db import get_session
|
||||
from database.room.models import Anonymous, Challenge, Challenges, CorrigedGeneratorOut, Exercices, ExercicesCreate, Member, Note, Parcours, ParcoursCreate, ParcoursReadShort, ParsedGeneratorOut, Room, RoomCreate, RoomInfo, RoomRead, TmpCorrection, Waiter, MemberRead
|
||||
from database.auth.models import User
|
||||
from services.database import generate_unique_code
|
||||
from sqlalchemy import func
|
||||
from sqlmodel import Session, delete, select, col
|
||||
|
||||
from database.auth.crud import get_user_from_token
|
||||
from database.auth.models import User
|
||||
from database.db import get_session
|
||||
from database.exercices.models import Exercice
|
||||
from database.room.models import Anonymous, Challenge, Challenges, CorrigedGeneratorOut, Exercices, ExercicesCreate, \
|
||||
Member, Note, Parcours, ParcoursCreate, ParcoursReadShort, ParsedGeneratorOut, Room, RoomCreate, RoomInfo, \
|
||||
TmpCorrection, Waiter, MemberRead, CorrigedData, CorrectionData, Challenger
|
||||
from services.auth import get_current_user_optional
|
||||
from services.database import generate_unique_code
|
||||
|
||||
|
||||
def create_room_db(*, room: RoomCreate, user: User | None = None, username: str | None = None, db: Session):
|
||||
@ -38,12 +42,15 @@ def create_room_db(*, room: RoomCreate, user: User | None = None, username: str
|
||||
|
||||
return {"room": room_obj, "member": member}
|
||||
|
||||
|
||||
def change_room_name(room: Room, name: str, db: Session):
|
||||
room.name = name
|
||||
db.add(room)
|
||||
db.commit()
|
||||
db.refresh(room)
|
||||
return room
|
||||
|
||||
|
||||
def change_room_status(room: Room, public: bool, db: Session):
|
||||
room.public = public
|
||||
db.add(room)
|
||||
@ -51,6 +58,7 @@ def change_room_status(room: Room, public: bool, db: Session):
|
||||
db.refresh(room)
|
||||
return room
|
||||
|
||||
|
||||
def get_member_from_user(user_id: int, room_id: int, db: Session):
|
||||
member = db.exec(select(Member).where(Member.room_id ==
|
||||
room_id, Member.user_id == user_id)).first()
|
||||
@ -86,6 +94,7 @@ def get_anonymous_from_code(reconnect_code: str, db: Session):
|
||||
Anonymous.reconnect_code == reconnect_code)).first()
|
||||
return anonymous
|
||||
|
||||
|
||||
def get_anonymous_from_clientId(clientId: str, db: Session):
|
||||
anonymous = db.exec(select(Anonymous).where(
|
||||
Anonymous.clientId == clientId)).first()
|
||||
@ -102,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):
|
||||
member_id = generate_unique_code(Member, s=db)
|
||||
member = Member(room=room, user=user, anonymous=anonymous, waiting=waiting,
|
||||
member = Member(room=room, user=user, anonymous=anonymous, waiting=waiting,
|
||||
id_code=member_id)
|
||||
member.online = True
|
||||
db.add(member)
|
||||
db.commit()
|
||||
db.refresh(member)
|
||||
return member
|
||||
|
||||
|
||||
def get_or_create_member(*, room: Room, user: User | None = None, anonymous: Anonymous | None = None, waiting: bool = False, db: Session):
|
||||
member = user is not None and get_member_from_user(user.id, room.id, db)
|
||||
def get_or_create_member(*, room: Room, user: User | None = None, anonymous: Anonymous | None = None,
|
||||
waiting: bool = False, db: Session):
|
||||
member = user is not None and get_member_from_user(user.id, room.id, db)
|
||||
if member is not None and member is not False:
|
||||
return member
|
||||
member= create_member(room=room, user=user, anonymous=anonymous, waiting=waiting, db=db)
|
||||
|
||||
member = create_member(room=room, user=user,
|
||||
anonymous=anonymous, waiting=waiting, db=db)
|
||||
|
||||
|
||||
def connect_member(member: Member, db: Session):
|
||||
member.online = True
|
||||
@ -128,10 +140,10 @@ def connect_member(member: Member, db: Session):
|
||||
def disconnect_member(member: Member, db: Session):
|
||||
if member.waiting == False:
|
||||
member.online = False
|
||||
|
||||
|
||||
if member.anonymous is not None:
|
||||
change_anonymous_clientId(member.anonymous,db)
|
||||
|
||||
change_anonymous_clientId(member.anonymous, db)
|
||||
|
||||
db.add(member)
|
||||
db.commit()
|
||||
db.refresh(member)
|
||||
@ -167,6 +179,7 @@ def create_anonymous_member(username: str, room: Room, db: Session):
|
||||
db.refresh(member)
|
||||
return member
|
||||
|
||||
|
||||
def create_anonymous(username: str, room: Room, db: Session):
|
||||
username = validate_username(username, room, db)
|
||||
if username is None:
|
||||
@ -179,10 +192,13 @@ def create_anonymous(username: str, room: Room, db: Session):
|
||||
db.refresh(anonymous)
|
||||
return anonymous
|
||||
|
||||
|
||||
def check_user_in_room(user_id: int, room_id: int, db: Session):
|
||||
user = db.exec(select(Member).where(Member.user_id==user_id, Member.room_id == room_id)).first()
|
||||
user = db.exec(select(Member).where(Member.user_id ==
|
||||
user_id, Member.room_id == room_id)).first()
|
||||
return user
|
||||
|
||||
|
||||
def create_user_member(user: User, room: Room, db: Session):
|
||||
member = get_member_from_user(user.id, room.id, db)
|
||||
if member is not None:
|
||||
@ -194,6 +210,7 @@ def create_user_member(user: User, room: Room, db: Session):
|
||||
db.refresh(member)
|
||||
return member
|
||||
|
||||
|
||||
def create_anonymous_waiter(username: str, room: Room, db: Session):
|
||||
username = validate_username(username, room, db)
|
||||
if username is None:
|
||||
@ -210,6 +227,7 @@ def create_anonymous_waiter(username: str, room: Room, db: Session):
|
||||
db.refresh(member)
|
||||
return member
|
||||
|
||||
|
||||
def create_user_waiter(user: User, room: Room, db: Session):
|
||||
member = get_member_from_user(user.id, room.id, db)
|
||||
if member is not None:
|
||||
@ -219,6 +237,7 @@ def create_user_waiter(user: User, room: Room, db: Session):
|
||||
db=db)
|
||||
return member
|
||||
|
||||
|
||||
def get_waiter(waiter_code: str, db: Session):
|
||||
return db.exec(select(Member).where(Member.id_code == waiter_code, Member.waiting == True)).first()
|
||||
|
||||
@ -236,6 +255,7 @@ def delete_member(member: Member, db: Session):
|
||||
def accept_waiter(member: Member, db: Session):
|
||||
member.waiting = False
|
||||
member.waiter_code = None
|
||||
member.online = True
|
||||
db.add(member)
|
||||
db.commit()
|
||||
db.refresh(member)
|
||||
@ -254,39 +274,173 @@ def leave_room(member: Member, db: Session):
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def serialize_member(member: Member) -> MemberRead | Waiter:
|
||||
def serialize_member(member: Member, private: bool = False, admin: bool = False,
|
||||
m2: Member | None = None) -> MemberRead | Waiter:
|
||||
member_obj = member.user or member.anonymous
|
||||
if member.waiting == False:
|
||||
return MemberRead(username=member_obj.username, reconnect_code=getattr(member_obj, "reconnect_code", ""), isUser=member.user_id != None, isAdmin=member.is_admin, id_code=member.id_code).dict()
|
||||
if member.waiting == True:
|
||||
print("OHLA", member_obj, private, member.user_id == None)
|
||||
if not member.waiting:
|
||||
return MemberRead(username=member_obj.username, online=member.online,
|
||||
clientId=str(member_obj.clientId) if (private == True and member.user_id == None) else "",
|
||||
reconnect_code=getattr(member_obj, "reconnect_code", "") if (admin or m2 == member) else "",
|
||||
isUser=member.user_id != None, isAdmin=member.is_admin, id_code=member.id_code).dict()
|
||||
if member.waiting:
|
||||
return Waiter(username=member_obj.username, waiter_id=member.id_code).dict()
|
||||
|
||||
|
||||
def serialize_parcours_short(parcours: Parcours, member: Member, db: Session):
|
||||
best_note = db.exec(select(Challenge.note, Challenge.time).where(Challenge.parcours_id == parcours.id, Challenge.challenger_id == member.id).order_by(col(Challenge.note).desc()).limit(1)).first()
|
||||
note = None
|
||||
if best_note is not None:
|
||||
best_note=best_note[0]
|
||||
note = Note(note=best_note[0], time=best_note[1])
|
||||
return ParcoursReadShort(**parcours.dict(exclude_unset=True), best_note=note)
|
||||
challenger = getChallenger(parcours, member, db)
|
||||
|
||||
return ParcoursReadShort(name=parcours.name, id_code=parcours.id_code, best_note=challenger.best,
|
||||
validated=challenger.validated)
|
||||
|
||||
|
||||
def serialize_challenge(challenge: Challenge):
|
||||
return Challenges(name=challenge.challenger.user.username if challenge.challenger.user is not None else challenge.challenger.anonymous.username, value=Note(note=challenge.note, time=challenge.time), isCorriged=challenge.isCorriged, canCorrige=challenge.data is not None)
|
||||
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):
|
||||
return RoomInfo(**room.dict(), parcours=[serialize_parcours_short(p, member, db) for p in room.parcours], members=[serialize_member(m) for m in room.members])
|
||||
return RoomInfo(**room.dict(), parcours=[serialize_parcours_short(p, member, db) for p in room.parcours],
|
||||
members=[serialize_member(m, admin=member.is_admin, m2=member) for m in room.members])
|
||||
|
||||
|
||||
def getUsername(m: Member):
|
||||
return m.user.username if m.user is not None else m.anonymous.username
|
||||
|
||||
|
||||
def getChallengerInfo(c: Challenge, db: Session):
|
||||
challenger = db.exec(select(Challenger).where(Challenger.member_id ==
|
||||
c.challenger_mid, Challenger.parcours_id == c.challenger_pid)).first()
|
||||
if challenger is not None:
|
||||
member = challenger.member
|
||||
return {"name": getUsername(member), "id_code": member.id_code}
|
||||
|
||||
|
||||
def getChallenges(c: Challenger, db: Session):
|
||||
challenges = db.exec(select(Challenge).where(Challenge.challenger_mid == c.member_id,
|
||||
Challenge.challenger_pid == c.parcours_id)).all()
|
||||
return challenges
|
||||
|
||||
|
||||
def getTops(p: Parcours, db: Session):
|
||||
tops = db.exec(select(Challenge).where(Challenge.parcours_id == p.id_code).order_by(
|
||||
col(Challenge.mistakes), col(Challenge.time)).limit(3)).all()
|
||||
|
||||
tops = [{"challenger": getChallengerInfo(
|
||||
t, db), "mistakes": t.mistakes, "time": t.time} for t in tops]
|
||||
return tops
|
||||
|
||||
|
||||
def getAvgTops(p: Parcours, db: Session):
|
||||
avgTop = db.exec(select(Challenger).where(Challenger.parcours_id ==
|
||||
p.id).order_by(col(Challenger.avg)).limit(3)).all()
|
||||
|
||||
avgTop = [{"id_code": t.member.id_code, "avg": t.avg,
|
||||
"name": getUsername(t.member)} for t in avgTop]
|
||||
return avgTop
|
||||
|
||||
|
||||
def getRank(c: Challenger, p: Parcours, db: Session):
|
||||
noteRank = db.exec(select([func.count(Challenge.id)]).where(Challenge.parcours_id == p.id_code).order_by(
|
||||
col(Challenge.mistakes), col(Challenge.time)).where(Challenge.mistakes <= c.best,
|
||||
Challenge.time < c.best_time)).one()
|
||||
return noteRank + 1
|
||||
|
||||
|
||||
def getAvgRank(c: Challenger, p: Parcours, db: Session):
|
||||
avgRank = db.exec(select([func.count(Challenger.member_id)]).where(
|
||||
Challenger.parcours_id == p.id).order_by(col(Challenger.avg)).where(Challenger.avg < c.avg)).one()
|
||||
return avgRank + 1
|
||||
|
||||
|
||||
def getMemberRank(m: Member, p: Parcours, db: Session):
|
||||
challenger = db.exec(select(Challenger).where(Challenger.member_id == m.id)).first()
|
||||
if challenger is None or challenger.best is None:
|
||||
return None
|
||||
return getRank(challenger, p, db)
|
||||
|
||||
|
||||
def getMemberAvgRank(m: Member, p: Parcours, db: Session):
|
||||
challenger = db.exec(select(Challenger).where(Challenger.member_id == m.id)).first()
|
||||
print('CHALLE', challenger)
|
||||
if challenger is None or challenger.avg is None:
|
||||
return None
|
||||
return getAvgRank(challenger, p, db)
|
||||
|
||||
|
||||
def serialize_parcours(parcours: Parcours, member: Member, db: Session):
|
||||
tops = getTops(parcours, db)
|
||||
avgTop = getAvgTops(parcours, db)
|
||||
|
||||
challenger = db.exec(select(Challenger).where(
|
||||
Challenger.member_id == member.id, Challenger.parcours_id == parcours.id)).first()
|
||||
|
||||
noteRank = None
|
||||
avgRank = None
|
||||
pb = None
|
||||
if challenger is not None and challenger.avg is not None and challenger.best is not None:
|
||||
noteRank = getRank(challenger, parcours, db)
|
||||
avgRank = getAvgRank(challenger, parcours, db)
|
||||
pb = {"mistakes": challenger.best, "time": challenger.best_time}
|
||||
|
||||
statement = select(Challenger).where(Challenger.parcours_id == parcours.id)
|
||||
if not member.is_admin:
|
||||
statement = statement.where(Challenger.member_id == member.id)
|
||||
|
||||
challengers = db.exec(statement).all()
|
||||
|
||||
challs = {c.member.id_code: {
|
||||
"challenger": {"id_code": c.member.id_code, "name": getUsername(c.member)},
|
||||
# 'validated': chall.mistakes <= parcours.max_mistakes
|
||||
"challenges": [Challenges(**{**chall.dict(), "canCorrige": chall.data != []}) for chall in getChallenges(c, db)]
|
||||
} for c in challengers}
|
||||
|
||||
return {**parcours.dict(), "pb": pb, "tops": tops, "challenges": challs, "rank": noteRank, "memberRank": avgRank,
|
||||
"validated": challenger.validated if challenger != None else False, "ranking": avgTop}
|
||||
tops = []
|
||||
challs = {}
|
||||
challenges = sorted(parcours.challenges, key=lambda x: (
|
||||
x.note['value'], x.time), reverse=True)
|
||||
memberRank = None
|
||||
rank = None
|
||||
pb = None
|
||||
validated = False
|
||||
|
||||
total = 0
|
||||
|
||||
for i, chall in enumerate(challenges):
|
||||
total += chall.note['value']
|
||||
id = chall.challenger.id_code
|
||||
name = chall.challenger.user.username if chall.challenger.user_id != None else chall.challenger.anonymous.username
|
||||
if i <= 2:
|
||||
tops.append({"challenger": {"id_code": id, "name": name},
|
||||
"note": chall.note, "time": chall.time})
|
||||
|
||||
if id == member.id_code:
|
||||
if challs.get(id) is None:
|
||||
rank = i + 1
|
||||
memberRank = len(challs) + 1
|
||||
pb = {"note": chall.note, "time": chall.time}
|
||||
if validated is False and chall.validated:
|
||||
validated = True
|
||||
|
||||
if member.is_admin or chall.challenger.id_code == member.id_code:
|
||||
t = challs.get(id, {"total": 0})['total']
|
||||
challs[id] = {"challenger": {"id_code": id, "name": name
|
||||
}, "challenges": [*challs.get(id, {'challenges': []})['challenges'],
|
||||
Challenges(
|
||||
**{**chall.dict(), "canCorrige": chall.data != []})],
|
||||
"total": t + chall.note['value']}
|
||||
|
||||
topMembers = [{**c['challenger'], "avg": c['total'] /
|
||||
len(c['challenges'])} for id, c in challs.items()]
|
||||
topMembers.sort(key=lambda x: x['avg'], reverse=True)
|
||||
return {**parcours.dict(), "tops": tops, "challenges": challs, "rank": rank, "memberRank": memberRank, "pb": pb,
|
||||
"validated": validated,
|
||||
'avg': None if len(parcours.challenges) == 0 else round(total / len(parcours.challenges), 2),
|
||||
"ranking": topMembers}
|
||||
|
||||
|
||||
def change_anonymous_clientId(anonymous: Anonymous, db: Session):
|
||||
@ -298,20 +452,49 @@ def change_anonymous_clientId(anonymous: Anonymous, db: Session):
|
||||
return anonymous
|
||||
|
||||
|
||||
#Parcours
|
||||
def validate_exercices(exos: List[ExercicesCreate], db: Session ):
|
||||
exercices = db.exec(select(Exercice).where(Exercice.web == True).where(col(Exercice.id_code).in_([e.exercice_id for e in exos]))).all()
|
||||
exos_id_list = [e.exercice_id for e in exos]
|
||||
exercices.sort(key=lambda e: exos_id_list.index(e.id_code))
|
||||
return [Exercices(exercice_id=e.id_code, name=e.name, quantity=[ex for ex in exos if ex.exercice_id == e.id_code][0].quantity).dict() for e in exercices]
|
||||
# Parcours
|
||||
from services.io import add_fast_api_root
|
||||
from generateur.generateur_main import generate_from_path, parseGeneratorOut
|
||||
|
||||
def create_parcours_db(parcours: ParcoursCreate,room_id: int, db: Session):
|
||||
|
||||
def countInput(ex: Exercice, q: int):
|
||||
exo = parseGeneratorOut(generate_from_path(add_fast_api_root(
|
||||
ex.exo_source), 1, "web"))
|
||||
return len(exo.inputs) * q
|
||||
|
||||
|
||||
class ExoToCount(BaseModel):
|
||||
ex: Exercice
|
||||
q: int
|
||||
|
||||
|
||||
def getTotal(exs: list[ExoToCount]):
|
||||
total = 0
|
||||
for e in exs:
|
||||
total += countInput(e.ex, e.q)
|
||||
return total
|
||||
|
||||
|
||||
def validate_exercices(exos: List[ExercicesCreate], db: Session):
|
||||
exercices = db.exec(select(Exercice).where(Exercice.web == True).where(
|
||||
col(Exercice.id_code).in_([e.exercice_id for e in exos]))).all()
|
||||
exos_id_list = [e.exercice_id for e in exos]
|
||||
# exoToCountList = [ExoToCount(ex=e, q=q) for e, q in zip(exercices, [c.quantity for c in exos])]
|
||||
exercices.sort(key=lambda e: exos_id_list.index(e.id_code))
|
||||
return [Exercices(exercice_id=e.id_code, name=e.name,
|
||||
quantity=[ex for ex in exos if ex.exercice_id == e.id_code][0].quantity,
|
||||
examples=e.examples).dict() for e in exercices]
|
||||
|
||||
|
||||
def create_parcours_db(parcours: ParcoursCreate, room_id: int, db: Session):
|
||||
exercices = validate_exercices(parcours.exercices, db)
|
||||
if len(exercices) == 0:
|
||||
return "Veuillez entrer au moins un exercice valide"
|
||||
id_code = generate_unique_code(Parcours, s=db)
|
||||
parcours_obj = Parcours(**{**parcours.dict(), "exercices": exercices}, room_id=room_id, id_code=id_code)
|
||||
print(parcours_obj)
|
||||
|
||||
parcours_obj = Parcours(
|
||||
**{**parcours.dict(), "exercices": exercices}, room_id=room_id, id_code=id_code)
|
||||
|
||||
db.add(parcours_obj)
|
||||
db.commit()
|
||||
db.refresh(parcours_obj)
|
||||
@ -320,24 +503,80 @@ def create_parcours_db(parcours: ParcoursCreate,room_id: int, db: Session):
|
||||
|
||||
def deleteParcoursRelated(parcours: Parcours, db: Session):
|
||||
db.exec(delete(Challenge).where(Challenge.parcours_id == parcours.id_code))
|
||||
db.exec(delete(TmpCorrection).where(TmpCorrection.parcours_id == parcours.id_code))
|
||||
db.exec(delete(TmpCorrection).where(
|
||||
TmpCorrection.parcours_id == parcours.id_code))
|
||||
db.exec(delete(Challenger).where(Challenger.parcours_id == parcours.id))
|
||||
|
||||
db.commit()
|
||||
|
||||
|
||||
|
||||
def change_challengers_validation(p: Parcours, validation: int, db: Session):
|
||||
challengers = db.exec(select(Challenger).where(
|
||||
Challenger.parcours_id == p.id)).all()
|
||||
challs = []
|
||||
for c in challengers:
|
||||
validated = c.best <= validation
|
||||
if validated != c.validated:
|
||||
c.validated = validated
|
||||
challs.append(c)
|
||||
|
||||
db.bulk_save_objects(challs)
|
||||
db.commit()
|
||||
|
||||
|
||||
def change_challenges_validation(p: Parcours, validation: int, db: Session):
|
||||
print('cHANGE')
|
||||
challenges = db.exec(select(Challenge).where(
|
||||
Challenge.parcours_id == p.id_code)).all()
|
||||
print('CHALLS', challenges)
|
||||
challs = []
|
||||
for c in challenges:
|
||||
validated = c.mistakes <= validation
|
||||
print('CHAL', validated, c.validated, c)
|
||||
if validated != c.validated:
|
||||
c.validated = validated
|
||||
challs.append(c)
|
||||
|
||||
db.bulk_save_objects(challs)
|
||||
db.commit()
|
||||
|
||||
|
||||
def changeValidation(p: Parcours, validation: int, db: Session):
|
||||
change_challengers_validation(p, validation, db)
|
||||
change_challenges_validation(p, validation, db)
|
||||
|
||||
|
||||
def compareExercices(old: list[Exercices], new: list[ExercicesCreate]):
|
||||
old = [{"id": o['exercice_id'], "q": o['quantity']} for o in old]
|
||||
new = [{"id": n.exercice_id, "q": n.quantity} for n in new]
|
||||
return old == new
|
||||
|
||||
|
||||
def update_parcours_db(parcours: ParcoursCreate, parcours_obj: Parcours, db: Session):
|
||||
exercices = validate_exercices(parcours.exercices, db)
|
||||
if len(exercices) == 0:
|
||||
return "Veuillez entrer au moins un exercice valide"
|
||||
|
||||
parcours_data = parcours.dict(exclude_unset=True)
|
||||
for key, value in parcours_data.items():
|
||||
setattr(parcours_obj, key, value)
|
||||
parcours_obj.exercices = exercices
|
||||
update_challenges = False
|
||||
|
||||
if not compareExercices(parcours_obj.exercices, parcours.exercices):
|
||||
exercices = validate_exercices(parcours.exercices, db)
|
||||
if len(exercices) == 0:
|
||||
return "Veuillez entrer au moins un exercice valide"
|
||||
deleteParcoursRelated(parcours_obj, db)
|
||||
update_challenges = True
|
||||
parcours_obj.exercices = exercices
|
||||
|
||||
if parcours_obj.max_mistakes != parcours.max_mistakes:
|
||||
changeValidation(parcours_obj, parcours.max_mistakes, db)
|
||||
|
||||
parcours_obj.name = parcours.name
|
||||
parcours_obj.time = parcours.time
|
||||
parcours_obj.max_mistakes = parcours.max_mistakes
|
||||
|
||||
db.add(parcours_obj)
|
||||
db.commit()
|
||||
deleteParcoursRelated(parcours_obj, db)
|
||||
|
||||
db.refresh(parcours_obj)
|
||||
|
||||
return parcours_obj
|
||||
|
||||
return parcours_obj, update_challenges
|
||||
|
||||
|
||||
def delete_parcours_db(parcours: Parcours, db: Session):
|
||||
db.delete(parcours)
|
||||
@ -347,138 +586,210 @@ def delete_parcours_db(parcours: Parcours, db: Session):
|
||||
|
||||
class CorrigedChallenge(BaseModel):
|
||||
data: List[List[CorrigedGeneratorOut]]
|
||||
note: Note
|
||||
mistakes: int
|
||||
isCorriged: bool
|
||||
|
||||
|
||||
def create_tmp_correction(data: List[List[CorrigedGeneratorOut]], parcours_id: str, member: Member, db: Session):
|
||||
def create_tmp_correction(data: List[CorrigedData], parcours_id: str, member: Member, db: Session):
|
||||
code = generate_unique_code(TmpCorrection, s=db)
|
||||
tmpCorr = TmpCorrection(data=data, id_code=code,
|
||||
member=member, parcours_id=parcours_id)
|
||||
db.add(tmpCorr)
|
||||
db.commit()
|
||||
db.refresh(tmpCorr)
|
||||
|
||||
return tmpCorr
|
||||
|
||||
def change_challenge(challenge: Challenge, corriged: CorrigedChallenge, db: Session):
|
||||
challenge.data = corriged['data']
|
||||
challenge.note = corriged['note']
|
||||
challenge.isCorriged = corriged['isCorriged']
|
||||
challenge.validated = noteOn20(
|
||||
corriged['note']['value'], corriged['note']['total']) > challenge.parcours.validate_condition
|
||||
|
||||
db.add(challenge)
|
||||
db.commit()
|
||||
db.refresh(challenge)
|
||||
|
||||
return challenge
|
||||
|
||||
|
||||
def validate_challenge_input(obj: List[List[ParsedGeneratorOut]], corr: TmpCorrection):
|
||||
def validate_challenge_input(obj: List[CorrectionData], corr: TmpCorrection):
|
||||
data = corr.data
|
||||
if len(obj) != len(data):
|
||||
return False
|
||||
for i in range(len(data)):
|
||||
exo_corr = data[i]
|
||||
exo = obj[i]
|
||||
if len(exo) != len(exo_corr):
|
||||
print('EXO', exo)
|
||||
print('EXO', exo.data)
|
||||
if len(exo.data) != len(exo_corr['data']):
|
||||
return
|
||||
zipped = zip(exo_corr, exo)
|
||||
same = all([e['calcul'] == f.calcul and len(e['inputs']) == len(f.inputs) for e,f in zipped])
|
||||
zipped = zip(exo_corr['data'], exo.data)
|
||||
same = all([e['calcul'] == f.calcul and len(e['inputs'])
|
||||
== len(f.inputs) for e, f in zipped])
|
||||
if not same:
|
||||
return False
|
||||
return True
|
||||
|
||||
def validate_challenge_correction(obj: List[List[CorrigedGeneratorOut]], chall: Challenge):
|
||||
|
||||
def validate_challenge_correction(obj: List[CorrigedData], chall: Challenge):
|
||||
data = chall.data
|
||||
if len(obj) != len(data):
|
||||
return False
|
||||
for i in range(len(data)):
|
||||
exo_corr = data[i]
|
||||
exo = obj[i]
|
||||
if len(exo) != len(exo_corr):
|
||||
if len(exo.data) != len(exo_corr['data']):
|
||||
return
|
||||
zipped = zip(exo_corr, exo)
|
||||
same = all([e['calcul'] == f.calcul and len(e['inputs']) == len(f.inputs) for e,f in zipped])
|
||||
zipped = zip(exo_corr['data'], exo.data)
|
||||
same = all([e['calcul'] == f.calcul and len(e['inputs'])
|
||||
== len(f.inputs) for e, f in zipped])
|
||||
if not same:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def corrige_challenge(obj: List[List[ParsedGeneratorOut]], corr: TmpCorrection) -> CorrigedChallenge:
|
||||
if validate_challenge_input(obj , corr) is False:
|
||||
if validate_challenge_input(obj, corr) is False:
|
||||
return None
|
||||
|
||||
data = corr.data
|
||||
note = 0
|
||||
total = 0
|
||||
isCorriged = True
|
||||
mistakes = 0
|
||||
for i in range(len(data)):
|
||||
exo_corr = data[i]
|
||||
exo = obj[i]
|
||||
exo_corr = data[i]["data"]
|
||||
exo = obj[i].data
|
||||
if len(exo) != len(exo_corr):
|
||||
return
|
||||
zipped = zip(exo_corr, exo)
|
||||
for e, f in zipped:
|
||||
print("HO\n\n")
|
||||
for k, l in zip(e['inputs'], f.inputs):
|
||||
k["value"] = str(l.value)
|
||||
total += 1
|
||||
if k['correction'] is None:
|
||||
isCorriged = False
|
||||
if str(k["correction"]) == str(l.value):
|
||||
k['valid'] = None
|
||||
|
||||
elif str(k["correction"]) == str(l.value):
|
||||
k['valid'] = True
|
||||
note += 1
|
||||
|
||||
return {"data": data, "note": {"value": 1, "total": 3}, "isCorriged": isCorriged}
|
||||
|
||||
|
||||
|
||||
def change_correction(obj: List[List[CorrigedGeneratorOut]], chall: Challenge) -> CorrigedChallenge:
|
||||
else:
|
||||
k['valid'] = False
|
||||
mistakes += 1
|
||||
|
||||
return {"data": data, "mistakes": mistakes, "isCorriged": isCorriged}
|
||||
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:
|
||||
return None
|
||||
data = deepcopy(chall.data)
|
||||
note = 0
|
||||
total = 0
|
||||
isCorriged = True
|
||||
mistakes = 0
|
||||
for i in range(len(data)):
|
||||
exo_corr = data[i]
|
||||
exo = obj[i]
|
||||
exo_corr = data[i]['data']
|
||||
exo = obj[i].data
|
||||
if len(exo) != len(exo_corr):
|
||||
return
|
||||
zipped = zip(exo_corr, exo)
|
||||
for e, f in zipped:
|
||||
for k, l in zip(e['inputs'], f.inputs):
|
||||
k["correction"] = str(l.correction)
|
||||
k["correction"] = l.correction
|
||||
k["valid"] = l.valid
|
||||
total += 1
|
||||
if k['correction'] is None:
|
||||
if k['correction'] is None and l.valid is None:
|
||||
isCorriged = False
|
||||
if str(k["correction"]) == str(l.value):
|
||||
if l.valid is True:
|
||||
note += 1
|
||||
|
||||
else:
|
||||
mistakes += 1
|
||||
|
||||
return {"data": data, "mistakes": mistakes, "isCorriged": isCorriged}
|
||||
return {"data": data, "note": {"value": note, "total": total}, "isCorriged": isCorriged}
|
||||
|
||||
|
||||
|
||||
def getChallenger(parcours: Parcours, member: Member, db: Session):
|
||||
challenger = db.exec(select(Challenger).where(
|
||||
Challenger.member_id == member.id, Challenger.parcours_id == parcours.id)).first()
|
||||
if challenger is None:
|
||||
return Challenger(parcours_id=parcours.id, member_id=member.id)
|
||||
return challenger
|
||||
|
||||
def create_challenge(data: List[List[CorrigedGeneratorOut]], challenger: Member, parcours: Parcours, time: int, note: Note, isCorriged: bool, db: Session):
|
||||
challenge = Challenge(data=data, challenger=challenger, parcours=parcours, time=time, note=note, validated=noteOn20(note["value"], note['total']) >=
|
||||
parcours.validate_condition, isCorriged=isCorriged, id_code=generate_unique_code(Challenge, s=db))
|
||||
|
||||
def ChallengerFromChallenge(c: Challenge, db: Session):
|
||||
challenger = db.exec(select(Challenger).where(
|
||||
Challenger.member_id == c.challenger_mid, Challenger.parcours_id == c.challenger_pid)).first()
|
||||
return challenger
|
||||
|
||||
|
||||
def checkValidated(challenger: Challenger, db: Session, challenge: Challenge | None = None):
|
||||
challenges = db.exec(select(Challenge).where(Challenge.challenger_mid == challenger.member_id,
|
||||
Challenge.challenger_pid == challenger.parcours_id,
|
||||
Challenge.validated == True)).all()
|
||||
if challenge is not None:
|
||||
challenges = [c for c in challenges if c.id != challenge.id]
|
||||
return len(challenges) != 0
|
||||
|
||||
|
||||
def create_challenge(data: List[CorrigedData], challenger: Member, parcours: Parcours, time: int, mistakes: int,
|
||||
isCorriged: bool, db: Session):
|
||||
challenger_obj: Challenger = getChallenger(parcours, challenger, db)
|
||||
validated = mistakes <= parcours.max_mistakes
|
||||
challenge = Challenge(data=data, challenger_pid=challenger_obj.parcours_id, challenger_mid=challenger_obj.member_id,
|
||||
parcours=parcours, time=time, mistakes=mistakes, isCorriged=isCorriged,
|
||||
id_code=generate_unique_code(Challenge, s=db), validated=validated)
|
||||
|
||||
if (challenger_obj.best is not None and challenger_obj.best > mistakes) or challenger_obj.best is None:
|
||||
challenger_obj.best = mistakes
|
||||
challenger_obj.best_time = time
|
||||
|
||||
challenges = db.exec(select([func.count(Challenge.id)]).where(
|
||||
Challenge.challenger_mid == challenger_obj.member_id, Challenge.challenger_pid == parcours.id)).one()
|
||||
|
||||
if validated and challenger_obj.validated is False:
|
||||
challenger_obj.validated = True
|
||||
|
||||
avg = challenger_obj.avg
|
||||
if avg is None:
|
||||
avg = 0
|
||||
challenger_obj.avg = (avg *
|
||||
(challenges - 1) + mistakes) / (challenges)
|
||||
db.add(challenge)
|
||||
db.add(challenger_obj)
|
||||
db.commit()
|
||||
db.refresh(challenge)
|
||||
return challenge
|
||||
db.refresh(challenger_obj)
|
||||
print('RETURN,', challenge, challenger_obj)
|
||||
return challenge, challenger_obj
|
||||
|
||||
|
||||
def change_challenge(challenge: Challenge, corriged: CorrigedChallenge, db: Session):
|
||||
challenger = ChallengerFromChallenge(challenge, db)
|
||||
|
||||
challengesCount = len(getChallenges(challenger, db))
|
||||
avg = challenger.avg * challengesCount - challenge.mistakes
|
||||
parcours = challenge.parcours
|
||||
if challenger.best > corriged['mistakes']:
|
||||
challenger.best = corriged['mistakes']
|
||||
challenger.best_time = challenge.time
|
||||
|
||||
validated = corriged['mistakes'] <= parcours.max_mistakes
|
||||
challenge.validated = validated
|
||||
|
||||
if challenger.validated == False and validated:
|
||||
challenger.validated = True
|
||||
|
||||
elif challenger.validated == True and not validated:
|
||||
challenger.validated = checkValidated(challenger, db, challenge)
|
||||
|
||||
challenger.avg = (avg + corriged['mistakes']) / challengesCount
|
||||
|
||||
challenge.data = corriged['data']
|
||||
challenge.note = corriged['note']
|
||||
challenge.mistakes = corriged['mistakes']
|
||||
challenge.isCorriged = corriged['isCorriged']
|
||||
challenge.validated = noteOn20(
|
||||
corriged['note']['value'], corriged['note']['total']) > challenge.parcours.validate_condition
|
||||
# challenge.validated = corriged['mistakes'] <= parcours.max_mistakes
|
||||
|
||||
db.add(challenge)
|
||||
db.add(challenger)
|
||||
db.commit()
|
||||
db.refresh(challenge)
|
||||
db.refresh(challenger)
|
||||
|
||||
return challenge
|
||||
return challenge, challenger
|
||||
|
||||
|
||||
# Dependencies
|
||||
@ -489,8 +800,6 @@ def check_room(room_id: str, db: Session = Depends(get_session)):
|
||||
return room
|
||||
|
||||
|
||||
|
||||
|
||||
def get_room(room_id, db: Session = Depends(get_session)):
|
||||
room = db.exec(select(Room).where(Room.id_code == room_id)).first()
|
||||
if room is None:
|
||||
@ -499,13 +808,14 @@ def get_room(room_id, db: Session = Depends(get_session)):
|
||||
return room
|
||||
|
||||
|
||||
def get_member_dep(room: Room = Depends(get_room), user: User = Depends(get_current_user_optional), clientId: str | None = Body(default=None), db: Session = Depends(get_session)):
|
||||
def get_member_dep(room: Room = Depends(get_room), user: User = Depends(get_current_user_optional),
|
||||
clientId: str | None = Query(default=None), db: Session = Depends(get_session)):
|
||||
if user is None and clientId is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
|
||||
if user is not None:
|
||||
member = get_member_from_user(user.id, room.id, db)
|
||||
if clientId is not None:
|
||||
elif clientId is not None:
|
||||
member = get_member_from_clientId(clientId, room.id, db)
|
||||
|
||||
if member is None:
|
||||
@ -550,11 +860,9 @@ def get_correction(correction_id: str, parcours_id: str, member: Member = Depend
|
||||
return tmpCorr
|
||||
|
||||
|
||||
|
||||
def get_challenge(challenge_id: str, parcours_id: str, db: Session = Depends(get_session)):
|
||||
|
||||
def get_challenge(challenge_id: str, db: Session = Depends(get_session)):
|
||||
challenge = db.exec(select(Challenge).where(
|
||||
Challenge.id_code == challenge_id, Challenge.parcours_id == parcours_id)).first()
|
||||
Challenge.id_code == challenge_id)).first()
|
||||
if challenge is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Challenge introuvable")
|
||||
@ -564,4 +872,3 @@ def get_challenge(challenge_id: str, parcours_id: str, db: Session = Depends(get
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Impossible de corriger ce challenge")
|
||||
|
||||
return challenge
|
||||
|
||||
|
@ -2,214 +2,325 @@ from uuid import UUID, uuid4
|
||||
from pydantic import root_validator, BaseModel
|
||||
import pydantic.json
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
from sqlmodel import SQLModel, Field, Relationship, JSON, Column
|
||||
|
||||
from sqlmodel import SQLModel, Field, Relationship, JSON, Column, ForeignKeyConstraint
|
||||
from database.exercices.models import Example
|
||||
from database.auth.models import UserRead
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from database.auth.models import User
|
||||
|
||||
|
||||
|
||||
|
||||
class RoomBase(SQLModel):
|
||||
name: str = Field(max_length=20)
|
||||
public: bool = Field(default=False)
|
||||
global_results: bool = Field(default=False)
|
||||
name: str = Field(max_length=20)
|
||||
public: bool = Field(default=False)
|
||||
global_results: bool = Field(default=False)
|
||||
|
||||
|
||||
class RoomCreate(RoomBase):
|
||||
pass
|
||||
pass
|
||||
|
||||
|
||||
class Room(RoomBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code: str = Field(index=True)
|
||||
|
||||
members: List['Member'] = Relationship(back_populates="room")
|
||||
parcours: List['Parcours'] = Relationship(back_populates="room")
|
||||
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code: str = Field(index=True)
|
||||
|
||||
members: List['Member'] = Relationship(back_populates="room")
|
||||
parcours: List['Parcours'] = Relationship(back_populates="room")
|
||||
|
||||
|
||||
class AnonymousBase(SQLModel):
|
||||
username: str = Field(max_length=20)
|
||||
|
||||
username: str = Field(max_length=20)
|
||||
|
||||
|
||||
class AnonymousCreate(AnonymousBase):
|
||||
pass
|
||||
pass
|
||||
|
||||
|
||||
class Anonymous(AnonymousBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
reconnect_code: str = Field(index=True)
|
||||
|
||||
clientId: Optional[UUID] = Field(default=uuid4(), index=True)
|
||||
member: 'Member' = Relationship(back_populates="anonymous")
|
||||
|
||||
|
||||
class Member(SQLModel, table = True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code: str = Field(index=True)
|
||||
|
||||
user_id: Optional[int] = Field(foreign_key="user.id", default=None)
|
||||
user: Optional["User"] = Relationship(back_populates='members')
|
||||
|
||||
anonymous_id: Optional[int] = Field(foreign_key="anonymous.id", default=None)
|
||||
anonymous: Optional[Anonymous] = Relationship(back_populates="member")
|
||||
|
||||
room_id: int = Field(foreign_key="room.id")
|
||||
room: Room = Relationship(back_populates='members')
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
reconnect_code: str = Field(index=True)
|
||||
|
||||
clientId: Optional[UUID] = Field(default=uuid4(), index=True)
|
||||
member: 'Member' = Relationship(back_populates="anonymous")
|
||||
|
||||
|
||||
class Member(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code: str = Field(index=True)
|
||||
|
||||
user_id: Optional[int] = Field(foreign_key="user.id", default=None)
|
||||
user: Optional["User"] = Relationship(back_populates='members')
|
||||
|
||||
anonymous_id: Optional[int] = Field(
|
||||
foreign_key="anonymous.id", default=None)
|
||||
anonymous: Optional[Anonymous] = Relationship(back_populates="member")
|
||||
|
||||
room_id: int = Field(foreign_key="room.id")
|
||||
room: Room = Relationship(back_populates='members')
|
||||
|
||||
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):
|
||||
exercice_id: str
|
||||
quantity: int = 10
|
||||
|
||||
class Exercices(ExercicesCreate):
|
||||
name: str
|
||||
exercice_id: str
|
||||
quantity: int = 10
|
||||
|
||||
|
||||
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):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code: str = Field(index=True, unique=True)
|
||||
|
||||
room_id: int = Field(foreign_key="room.id")
|
||||
room: Room = Relationship(back_populates='parcours')
|
||||
|
||||
name: str
|
||||
time: int
|
||||
validate_condition: int
|
||||
|
||||
exercices: List[Exercices] = Field(sa_column=Column(JSON))
|
||||
challenges: List["Challenge"] = Relationship(back_populates="parcours")
|
||||
corrections: List["TmpCorrection"] = Relationship(back_populates="parcours")
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code: str = Field(index=True, unique=True)
|
||||
|
||||
room_id: int = Field(foreign_key="room.id")
|
||||
room: Room = Relationship(back_populates='parcours')
|
||||
|
||||
challengers: list[Challenger] = Relationship(back_populates="parcours")
|
||||
|
||||
name: str
|
||||
time: int
|
||||
|
||||
|
||||
max_mistakes: int
|
||||
|
||||
exercices: List[Exercices] = Field(sa_column=Column(JSON))
|
||||
challenges: List["Challenge"] = Relationship(back_populates="parcours")
|
||||
corrections: List["TmpCorrection"] = Relationship(
|
||||
back_populates="parcours")
|
||||
|
||||
|
||||
|
||||
|
||||
class Note(BaseModel):
|
||||
value: int
|
||||
total: int
|
||||
|
||||
|
||||
value: int
|
||||
total: int
|
||||
|
||||
|
||||
class TimedNote(Note):
|
||||
time: int
|
||||
|
||||
time: int
|
||||
|
||||
|
||||
class ParcoursReadShort(SQLModel):
|
||||
name: str
|
||||
best_note: str | None = None
|
||||
id_code: str
|
||||
|
||||
name: str
|
||||
best_note: str | None = None
|
||||
validated: bool = False
|
||||
id_code: str
|
||||
|
||||
class ParcoursReadUpdate(SQLModel):
|
||||
id_code: str
|
||||
name: str
|
||||
time: int
|
||||
max_mistakes: int
|
||||
exercices: List[Exercices]
|
||||
update_challenges: bool = False
|
||||
|
||||
class ChallengerInfo(BaseModel):
|
||||
name: str
|
||||
id_code: str
|
||||
|
||||
|
||||
class ChallengerAverage(ChallengerInfo):
|
||||
avg: float
|
||||
|
||||
|
||||
class Challenges(SQLModel):
|
||||
id_code: str
|
||||
challenger: str
|
||||
note: Note
|
||||
time: int
|
||||
isCorriged: bool
|
||||
canCorrige: bool
|
||||
validated: bool
|
||||
id_code: str
|
||||
mistakes: int
|
||||
time: int
|
||||
isCorriged: bool
|
||||
canCorrige: bool = True
|
||||
validated: bool = False
|
||||
|
||||
|
||||
class ChallengeInfo(BaseModel):
|
||||
challenger: ChallengerInfo
|
||||
challenges: list[Challenges]
|
||||
#total: int
|
||||
|
||||
class Tops(BaseModel):
|
||||
challenger: ChallengerInfo
|
||||
mistakes: int
|
||||
time: int
|
||||
|
||||
class ParcoursRead(SQLModel):
|
||||
name: str
|
||||
time: int
|
||||
validate_condition: int
|
||||
id_code: str
|
||||
exercices: List[Exercices]
|
||||
challenges: List[Challenges]
|
||||
|
||||
tops: list[Tops]
|
||||
name: str
|
||||
time: int
|
||||
max_mistakes: int
|
||||
id_code: str
|
||||
exercices: List[Exercices]
|
||||
challenges: dict[str, ChallengeInfo]
|
||||
rank: int | None
|
||||
pb: dict[str, int] | None
|
||||
memberRank: int | None
|
||||
validated: bool
|
||||
ranking: list[ChallengerAverage]
|
||||
#avg: float | None
|
||||
|
||||
|
||||
class ParcoursCreate(SQLModel):
|
||||
name: str
|
||||
time: int
|
||||
validate_condition: int
|
||||
exercices: List[ExercicesCreate]
|
||||
name: str
|
||||
time: int
|
||||
max_mistakes: int
|
||||
exercices: List[ExercicesCreate]
|
||||
|
||||
|
||||
class NotCorrigedInput(BaseModel):
|
||||
index: int
|
||||
value: str
|
||||
|
||||
index: int
|
||||
value: str
|
||||
|
||||
|
||||
class CorrigedInput(NotCorrigedInput):
|
||||
correction: str
|
||||
correction: str | None
|
||||
valid: bool | None
|
||||
|
||||
|
||||
class ParsedGeneratorOut(BaseModel):
|
||||
calcul: str
|
||||
inputs: List[NotCorrigedInput]
|
||||
|
||||
calcul: str
|
||||
inputs: List[NotCorrigedInput]
|
||||
|
||||
|
||||
class CorrigedGeneratorOut(BaseModel):
|
||||
calcul: str
|
||||
inputs: List[CorrigedInput]
|
||||
calcul: str
|
||||
inputs: List[CorrigedInput]
|
||||
|
||||
|
||||
class Challenge(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code: str = Field(index=True, unique=True)
|
||||
|
||||
challenger_id: int = Field(foreign_key="member.id")
|
||||
challenger: Member = Relationship(back_populates="challenges")
|
||||
|
||||
parcours_id: int = Field(foreign_key="parcours.id_code")
|
||||
parcours: Parcours = Relationship(back_populates="challenges")
|
||||
|
||||
data: Optional[List[List[CorrigedGeneratorOut]]] = Field(sa_column=Column(JSON), default=[])
|
||||
|
||||
time: int
|
||||
note: Note = Field(
|
||||
sa_column=Column(JSON))
|
||||
validated: bool
|
||||
isCorriged: bool
|
||||
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code: str = Field(index=True, unique=True)
|
||||
|
||||
''' challenger_id: int = Field(foreign_key="member.id")
|
||||
challenger: Member = Relationship(back_populates="challenges") '''
|
||||
|
||||
parcours_id: int = Field(foreign_key="parcours.id_code")
|
||||
parcours: Parcours = Relationship(back_populates="challenges")
|
||||
|
||||
challenger_pid: int
|
||||
challenger_mid: int
|
||||
__table_args__ = (ForeignKeyConstraint(["challenger_pid", "challenger_mid"],
|
||||
["challenger.parcours_id","challenger.member_id"]),
|
||||
{})
|
||||
|
||||
#challenger: "Challenger" = Relationship(back_populates="challenges")
|
||||
|
||||
data: Optional[List] = Field(
|
||||
sa_column=Column(JSON), default=[])
|
||||
|
||||
time: int
|
||||
''' note: Note = Field(
|
||||
sa_column=Column(JSON)) '''
|
||||
|
||||
mistakes: int
|
||||
#note_value: int
|
||||
validated: bool
|
||||
|
||||
isCorriged: bool
|
||||
|
||||
|
||||
class ExoInfo(BaseModel):
|
||||
id_code: str
|
||||
name: str
|
||||
consigne: str | None
|
||||
|
||||
|
||||
class CorrigedData(BaseModel):
|
||||
exo: ExoInfo
|
||||
data: List[CorrigedGeneratorOut]
|
||||
|
||||
|
||||
class CorrectionData(BaseModel):
|
||||
exo: ExoInfo
|
||||
data: List[ParsedGeneratorOut]
|
||||
|
||||
|
||||
class ChallengeRead(SQLModel):
|
||||
id_code: str
|
||||
data: Optional[List[List[CorrigedGeneratorOut]]] = []
|
||||
data: Optional[List[CorrigedData]] = []
|
||||
time: int
|
||||
note: Note
|
||||
mistakes: int
|
||||
validated: bool
|
||||
isCorriged: bool
|
||||
|
||||
|
||||
class TmpCorrection(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code: str = Field(index=True)
|
||||
|
||||
parcours_id: str = Field(foreign_key="parcours.id_code")
|
||||
parcours: Parcours = Relationship(back_populates="corrections")
|
||||
|
||||
member_id: int = Field(foreign_key="member.id")
|
||||
member: Member = Relationship(back_populates="corrections")
|
||||
|
||||
data: List[List[CorrigedGeneratorOut]] = Field(sa_column=Column(JSON))
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code: str = Field(index=True)
|
||||
|
||||
parcours_id: str = Field(foreign_key="parcours.id_code")
|
||||
parcours: Parcours = Relationship(back_populates="corrections")
|
||||
|
||||
member_id: int = Field(foreign_key="member.id")
|
||||
member: Member = Relationship(back_populates="corrections")
|
||||
|
||||
data: List = Field(sa_column=Column(JSON))
|
||||
|
||||
|
||||
class AnonymousRead(AnonymousBase):
|
||||
reconnect_code: str
|
||||
|
||||
reconnect_code: str
|
||||
|
||||
|
||||
class Username(SQLModel):
|
||||
username: str
|
||||
|
||||
|
||||
username: str
|
||||
|
||||
|
||||
class MemberRead(SQLModel):
|
||||
username: str
|
||||
reconnect_code: str = ''
|
||||
isUser: bool
|
||||
isAdmin: bool
|
||||
id_code: str
|
||||
username: str
|
||||
reconnect_code: str = ''
|
||||
isUser: bool
|
||||
isAdmin: bool
|
||||
id_code: str
|
||||
clientId: str = ""
|
||||
online: bool
|
||||
|
||||
|
||||
class RoomRead(RoomBase):
|
||||
id_code: str
|
||||
|
||||
id_code: str
|
||||
|
||||
|
||||
class RoomAndMember(BaseModel):
|
||||
room: RoomRead
|
||||
member: MemberRead
|
||||
|
||||
class RoomInfo(RoomRead):
|
||||
public: bool
|
||||
name: str
|
||||
members: List[MemberRead]
|
||||
parcours: List[ParcoursReadShort]
|
||||
|
||||
|
||||
class Waiter(BaseModel):
|
||||
username: str
|
||||
waiter_id: str
|
||||
|
||||
|
||||
class RoomInfo(RoomRead):
|
||||
public: bool
|
||||
name: str
|
||||
members: List[MemberRead | Waiter]
|
||||
parcours: List[ParcoursReadShort]
|
||||
|
||||
|
||||
class RoomConnectionInfos(BaseModel):
|
||||
room: str
|
||||
member: str | None = None
|
||||
room: str
|
||||
member: str | None = None
|
||||
|
BIN
backend/api/database2.db
Normal file
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):
|
||||
writer.writerow([''])
|
||||
|
||||
|
@ -1,41 +1,45 @@
|
||||
import random
|
||||
import re
|
||||
import importlib.util
|
||||
import string
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
|
||||
import sympy
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class GeneratorOut(BaseModel):
|
||||
calcul: str
|
||||
correction: str | None = None
|
||||
|
||||
def parseOut(calcul):
|
||||
"""Fait en sorte de séparer la correction présente dans le calcul"""
|
||||
regex = r"\[(.*?)\]"
|
||||
calculEx = calcul['calcul'].replace('[', ' [').replace(']', '] ')
|
||||
splitted = calculEx.split()
|
||||
|
||||
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]):
|
||||
return [parseOut(c) for c in out]
|
||||
return [parseOut(c) for c in out]
|
||||
|
||||
|
||||
def getObjectKey(obj, key):
|
||||
if obj[key] == None:
|
||||
@ -44,7 +48,9 @@ def getObjectKey(obj, key):
|
||||
|
||||
|
||||
def getCorrectionKey(obj, key):
|
||||
return key if (obj[key] != False and obj['correction'] == False) else 'calcul' if(obj['calcul'] != False and obj['correction'] == False) else 'correction' if obj['correction'] != False else None
|
||||
return key if (obj[key] != False and obj['correction'] == False) else 'calcul' if (
|
||||
obj['calcul'] != False and obj['correction'] == False) else 'correction' if obj[
|
||||
'correction'] != False else None
|
||||
|
||||
|
||||
def parseCorrection(calc, replacer='...'):
|
||||
@ -54,35 +60,37 @@ def parseCorrection(calc, replacer='...'):
|
||||
return calc
|
||||
|
||||
|
||||
def generate_from_data(data, quantity, key, forcedCorrection=False):
|
||||
def generate_from_data(data, quantity, key, forced_correction=False):
|
||||
locs = {}
|
||||
exec(data, {"random": random, "string": string, "sympy": sympy}, locs)
|
||||
try:
|
||||
main_func = locs['main']
|
||||
except:
|
||||
except KeyError:
|
||||
return None
|
||||
main_result = main_func()
|
||||
default_object = {"calcul": False, 'pdf': False, 'csv': False,
|
||||
'web': False, 'correction': False} # les valeurs par défaut
|
||||
# Si l'utilisateur n'a pas entré une valeur, elle est définie à False
|
||||
|
||||
|
||||
result_object = {**default_object, **main_result}
|
||||
object_key = getObjectKey(result_object, key)
|
||||
correction_key = getCorrectionKey(result_object, key)
|
||||
op_list = []
|
||||
try:
|
||||
replacer = locs["CORRECTION_REPLACER"]
|
||||
except:
|
||||
except KeyError:
|
||||
replacer = '...'
|
||||
|
||||
|
||||
for i in range(quantity):
|
||||
main_result = main_func()
|
||||
main = {**default_object, **main_result}
|
||||
op_list.append({'calcul': parseCorrection(main[
|
||||
object_key], replacer) if (forcedCorrection or (key != 'web' and main['correction'] == False)) else main[object_key], "correction": main[correction_key]})
|
||||
object_key], replacer) if (
|
||||
forced_correction or (key != 'web' and main['correction'] == False)) else main[object_key],
|
||||
"correction": main[correction_key]})
|
||||
return op_list
|
||||
|
||||
|
||||
def generate_from_path(path, quantity, key, forcedCorrection=False):
|
||||
def generate_from_path(path, quantity, key, forced_correction=False):
|
||||
data = open(path, "r").read()
|
||||
return generate_from_data(data, quantity, key, forcedCorrection)
|
||||
return generate_from_data(data, quantity, key, forced_correction)
|
||||
|
@ -1,36 +1,29 @@
|
||||
#import schemas.base
|
||||
from services.database import generate_unique_code
|
||||
from sqlmodel import SQLModel, Field, select
|
||||
from services.password import get_password_hash
|
||||
from sqlmodel import Session, select
|
||||
from database.auth.crud import create_user_db
|
||||
from services.auth import get_current_user_optional, jwt_required
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from database.auth.models import User, UserRead
|
||||
from database.exercices.models import Exercice, ExerciceReadFull
|
||||
from fastapi_pagination import add_pagination
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import Depends, Request, status
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError, ValidationError
|
||||
from fastapi import FastAPI, HTTPException, Depends, Request, status, Header
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi_jwt_auth import AuthJWT
|
||||
from fastapi_jwt_auth.exceptions import AuthJWTException
|
||||
from fastapi.responses import JSONResponse
|
||||
from typing import List, Optional, Sequence
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
from fastapi import FastAPI, HTTPException, params
|
||||
from tortoise import Tortoise
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from tortoise.contrib.fastapi import register_tortoise
|
||||
from pydantic import BaseModel, validator
|
||||
from database.db import create_db_and_tables, get_session
|
||||
from services.jwt import revoke_access, revoke_refresh
|
||||
import routes.base
|
||||
from redis import Redis
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
import config
|
||||
from fastapi_pagination import add_pagination
|
||||
from sqladmin import Admin, ModelView
|
||||
from sqlmodel import SQLModel, Field
|
||||
from sqlmodel import Session, select
|
||||
|
||||
import config
|
||||
import routes.base
|
||||
from database.auth.crud import create_user_db
|
||||
from database.auth.models import User, UserRead
|
||||
from database.db import create_db_and_tables, get_session
|
||||
from database.db import engine
|
||||
from fastapi.security import OAuth2PasswordBearer, HTTPBearer
|
||||
from database.exercices.models import Exercice, ExerciceReadFull
|
||||
from services.jwt import revoke_access, revoke_refresh
|
||||
from services.password import get_password_hash
|
||||
|
||||
app = FastAPI(title="API Generateur d'exercices")
|
||||
origins = [
|
||||
"http://localhost:8000",
|
||||
@ -102,7 +95,7 @@ def test(test_1: str, test_2: str, test_3: str = Depends(t), test_4: str = Depen
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
@app.exception_handler(ValidationError)
|
||||
async def validation_exception_handler(request, exc: RequestValidationError|ValidationError):
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError|ValidationError):
|
||||
errors = {}
|
||||
print(exc.errors())
|
||||
for e in exc.errors():
|
||||
|
@ -1,15 +1,18 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from services.jwt import revoke_access
|
||||
from services.password import get_password_hash, verify_password
|
||||
from services.auth import get_current_clientId, get_current_user, get_current_user_optional, jwt_refresh_required
|
||||
from database.auth.crud import change_user_uuid, check_unique_username, create_user_db, delete_user_db, update_password_db, update_user_db
|
||||
from services.auth import authenticate_user
|
||||
from database.auth.models import PasswordSet, User, UserEdit, UserRead, UserRegister
|
||||
from pydantic import BaseModel
|
||||
from fastapi_jwt_auth import AuthJWT
|
||||
from sqlmodel import Session,select
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from database.auth.crud import change_user_uuid, check_unique_username, create_user_db, delete_user_db, \
|
||||
update_password_db, update_user_db, parse_user_rooms
|
||||
from database.auth.models import PasswordSet, User, UserEdit, UserRead, UserRegister, UserEditRead
|
||||
from database.db import get_session
|
||||
from services.auth import authenticate_user
|
||||
from services.auth import get_current_clientId, get_current_user, get_current_user_optional, jwt_refresh_required
|
||||
from services.password import get_password_hash, verify_password
|
||||
|
||||
router = APIRouter(tags=['Authentification'])
|
||||
|
||||
|
||||
@ -22,8 +25,9 @@ class Token(BaseModel):
|
||||
def login_for_access_token(user: User = Depends(authenticate_user)):
|
||||
Authorize = AuthJWT()
|
||||
access_token = Authorize.create_access_token(
|
||||
subject=str(user.clientId), fresh=True)
|
||||
refresh_token = Authorize.create_refresh_token(subject=str(user.clientId))
|
||||
subject=str(user.clientId), fresh=True, user_claims={"username": user.username})
|
||||
refresh_token = Authorize.create_refresh_token(subject=str(
|
||||
user.clientId), user_claims={"username": user.username})
|
||||
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
|
||||
|
||||
@router.post('/register', response_model=Token)
|
||||
@ -33,8 +37,9 @@ def register(user: UserRegister = Depends(UserRegister.as_form), Authorize: Auth
|
||||
raise HTTPException(status_code = status.HTTP_400_BAD_REQUEST,detail={'username_error': "Nom d'utilisateur indisponible"})
|
||||
user = create_user_db(username, get_password_hash(user.password), db)
|
||||
access_token = Authorize.create_access_token(
|
||||
subject=str(user.clientId))
|
||||
refresh_token = Authorize.create_refresh_token(subject=str(user.clientId))
|
||||
subject=str(user.clientId), user_claims={"username": user.username})
|
||||
refresh_token = Authorize.create_refresh_token(subject=str(
|
||||
user.clientId), user_claims={"username": user.username})
|
||||
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
|
||||
|
||||
@router.get('/users', response_model=List[UserRead])
|
||||
@ -42,7 +47,11 @@ def get_users(db: Session = Depends(get_session)):
|
||||
users = db.exec(select(User)).all()
|
||||
return users
|
||||
|
||||
@router.put('/user' , response_model=UserRead,)
|
||||
|
||||
@router.get('/user', response_model=UserRead)
|
||||
def get_user(user: User = Depends(get_current_user), db: Session = Depends(get_session)):
|
||||
return {**user.dict(), "rooms": parse_user_rooms(user, db)}
|
||||
@router.put('/user' , response_model=UserEditRead,)
|
||||
def update_user(user: UserEdit = Depends(UserEdit.as_form), clientId: str = Depends(get_current_clientId), db: Session = Depends(get_session)):
|
||||
user_obj = update_user_db(clientId, user, db)
|
||||
return user_obj
|
||||
@ -58,8 +67,9 @@ def update_password(password: PasswordSet = Depends(PasswordSet.as_form), user:
|
||||
user_obj = change_user_uuid(user.id, db)
|
||||
|
||||
access_token = Authorize.create_access_token(
|
||||
subject=str(user_obj))
|
||||
refresh_token = Authorize.create_refresh_token(subject=str(user_obj))
|
||||
subject=str(user_obj), user_claims={"username": user.username})
|
||||
refresh_token = Authorize.create_refresh_token(
|
||||
subject=str(user_obj), user_claims={"username": user.username})
|
||||
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@ -82,5 +92,6 @@ def check_token(user: User = Depends(get_current_user_optional)):
|
||||
@router.post('/refresh')
|
||||
def refresh(Authorize: AuthJWT = Depends(jwt_refresh_required)):
|
||||
current_user = Authorize.get_jwt_subject()
|
||||
new_access_token = Authorize.create_access_token(subject=current_user)
|
||||
username = Authorize.get_raw_jwt()['username']
|
||||
new_access_token = Authorize.create_access_token(subject=current_user, user_claims={"username":username})
|
||||
return {"access_token": new_access_token}
|
||||
|
@ -1,28 +1,34 @@
|
||||
from pydantic import BaseModel
|
||||
import csv
|
||||
import io
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, Path, Query, UploadFile, HTTPException, status
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, UploadFile, HTTPException, status
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from fastapi_pagination.ext.sqlalchemy_future import paginate as p
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from database.auth.models import User
|
||||
from database.db import get_session
|
||||
from database.exercices.models import Exercice, ExerciceCreate, ExerciceEdit, ExerciceReadFull, ExercicesTagLink, Tag, TagCreate, TagRead, ExerciceRead
|
||||
from database.exercices.crud import add_tags_db, check_exercice_author, check_private, check_tag_author, create_exo_db, \
|
||||
delete_exo_db, get_exo_dependency, clone_exo_db, remove_tag_db, serialize_exo, update_exo_db, get_tags_dependency
|
||||
from database.exercices.models import Exercice, ExerciceCreate, ExerciceEdit, ExerciceReadFull, ExercicesTagLink, Tag, \
|
||||
TagCreate, TagRead, ExerciceRead
|
||||
from generateur.generateur_csv import Csv_generator
|
||||
from services.auth import get_current_user, get_current_user_optional
|
||||
from sqlmodel import Session, select, col
|
||||
from database.exercices.crud import add_tags_db, check_exercice_author, check_private, check_tag_author, create_exo_db, delete_exo_db, get_exo_dependency, clone_exo_db, parse_exo_tags, remove_tag_db, serialize_exo, update_exo_db, get_tags_dependency
|
||||
from services.exoValidation import validate_file, validate_file_optionnal
|
||||
from services.io import add_fast_api_root, get_filename_from_path
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlmodel import func
|
||||
from fastapi_pagination import paginate ,Page
|
||||
from services.models import Page
|
||||
from fastapi_pagination.ext.sqlalchemy_future import paginate as p
|
||||
|
||||
router = APIRouter(tags=['exercices'])
|
||||
|
||||
|
||||
class ExoType(str, Enum):
|
||||
csv="csv"
|
||||
pdf="pdf"
|
||||
web="web"
|
||||
|
||||
csv = "csv"
|
||||
pdf = "pdf"
|
||||
web = "web"
|
||||
|
||||
|
||||
def filter_exo_by_tags(exos: List[tuple[Exercice, str]], tags: List[Tag]):
|
||||
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
|
||||
|
||||
|
||||
def queryFilters_dependency(search: str = "", tags: List[str] | None = Depends(get_tags_dependency), type: ExoType | None = Query(default = None)):
|
||||
def queryFilters_dependency(search: str = "", tags: List[str] | None = Depends(get_tags_dependency),
|
||||
type: ExoType | None = Query(default=None)):
|
||||
return search, tags, type
|
||||
|
||||
|
||||
@router.post('/exercices', response_model=ExerciceReadFull, status_code=status.HTTP_201_CREATED)
|
||||
def create_exo(exercice: ExerciceCreate = Depends(ExerciceCreate.as_form), file: UploadFile = Depends(validate_file), user: User = Depends(get_current_user), db: Session = Depends(get_session)):
|
||||
def create_exo(exercice: ExerciceCreate = Depends(ExerciceCreate.as_form), file: UploadFile = Depends(validate_file),
|
||||
user: User = Depends(get_current_user), db: Session = Depends(get_session)):
|
||||
file_obj = file['file'].file._file
|
||||
file_obj.name = file['file'].filename
|
||||
exo_obj = create_exo_db(exercice=exercice, user=user,
|
||||
exo_source=file_obj, supports=file['supports'],db=db)
|
||||
exo_source=file_obj, supports=file['supports'], db=db)
|
||||
return serialize_exo(exo=exo_obj, user_id=user.id, db=db)
|
||||
|
||||
|
||||
@router.post('/clone/{id_code}', response_model=ExerciceReadFull)
|
||||
def clone_exo(exercice: Exercice | None = Depends(check_private), user: User = Depends(get_current_user), db: Session = Depends(get_session)):
|
||||
def clone_exo(exercice: Exercice | None = Depends(check_private), user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_session)):
|
||||
if not exercice:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail={
|
||||
"Exercice introuvable"})
|
||||
"Exercice introuvable"})
|
||||
exo_obj = clone_exo_db(exercice, user, db)
|
||||
if type(exo_obj) == str:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail=exo_obj)
|
||||
return serialize_exo(exo=exo_obj, user_id=user.id, db=db)
|
||||
|
||||
|
||||
@router.get('/exercices/user', response_model=Page[ExerciceRead|ExerciceReadFull])
|
||||
def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tuple[str, List[int] | None, ExoType | None] = Depends(queryFilters_dependency), db: Session = Depends(get_session)):
|
||||
|
||||
@router.get('/exercices/user', response_model=Page[ExerciceRead | ExerciceReadFull])
|
||||
def get_user_exercices(user: User = Depends(get_current_user),
|
||||
queryFilters: tuple[str, List[int] | None, ExoType | None] = Depends(queryFilters_dependency),
|
||||
db: Session = Depends(get_session)):
|
||||
search, tags, type = queryFilters
|
||||
|
||||
|
||||
statement = select(Exercice)
|
||||
statement = statement.where(Exercice.author_id == user.id)
|
||||
statement = statement.where(Exercice.name.startswith(search))
|
||||
|
||||
|
||||
if type == ExoType.csv:
|
||||
statement = statement.where(Exercice.csv == True)
|
||||
if type == ExoType.pdf:
|
||||
statement = statement.where(Exercice.pdf == True)
|
||||
if type == ExoType.web:
|
||||
statement = statement.where(Exercice.web == True)
|
||||
|
||||
|
||||
for t in tags:
|
||||
sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id==Exercice.id).where(
|
||||
sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id == Exercice.id).where(
|
||||
ExercicesTagLink.tag_id == t).exists()
|
||||
statement = statement.where(sub)
|
||||
page = p(db, statement)
|
||||
@ -81,14 +92,17 @@ def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tup
|
||||
return page
|
||||
|
||||
|
||||
@router.get('/exercices/public', response_model=Page[ExerciceRead|ExerciceReadFull])
|
||||
def get_public_exercices(user: User | None = Depends(get_current_user_optional), queryFilters: tuple[str, List[int] | None] = Depends(queryFilters_dependency), db: Session = Depends(get_session)):
|
||||
@router.get('/exercices/public', response_model=Page[ExerciceRead | ExerciceReadFull])
|
||||
def get_public_exercices(user: User | None = Depends(get_current_user_optional),
|
||||
queryFilters: tuple[str, List[int] | None] = Depends(queryFilters_dependency),
|
||||
db: Session = Depends(get_session)):
|
||||
search, tags, type = queryFilters
|
||||
|
||||
if user is not None:
|
||||
statement = select(Exercice)
|
||||
statement = statement.where(Exercice.author_id != user.id)
|
||||
statement = statement.where(Exercice.private == False)
|
||||
statement = statement.where(Exercice.origin_id == None)
|
||||
statement = statement.where(Exercice.name.startswith(search))
|
||||
|
||||
if type == ExoType.csv:
|
||||
@ -97,19 +111,19 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional),
|
||||
statement = statement.where(Exercice.pdf == True)
|
||||
if type == ExoType.web:
|
||||
statement = statement.where(Exercice.web == True)
|
||||
|
||||
|
||||
for t in tags:
|
||||
sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id==Exercice.id).where(
|
||||
ExercicesTagLink.tag_id == t).exists()
|
||||
sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id == Exercice.id).where(
|
||||
ExercicesTagLink.tag_id == t).exists()
|
||||
statement = statement.where(sub)
|
||||
|
||||
|
||||
page = p(db, statement)
|
||||
print('¨PAGE', page)
|
||||
exercices = page.items
|
||||
page.items = [
|
||||
serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices]
|
||||
return page
|
||||
|
||||
|
||||
else:
|
||||
statement = select(Exercice)
|
||||
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)
|
||||
if type == ExoType.web:
|
||||
statement = statement.where(Exercice.web == True)
|
||||
|
||||
|
||||
page = p(db, statement)
|
||||
exercices = 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)
|
||||
def get_exercice(exo: Exercice = Depends(check_private), user: User | None = Depends(get_current_user_optional), db: Session = Depends(get_session)):
|
||||
def get_exercice(exo: Exercice = Depends(check_private), user: User | None = Depends(get_current_user_optional),
|
||||
db: Session = Depends(get_session)):
|
||||
return serialize_exo(exo=exo, user_id=getattr(user, 'id', None), db=db)
|
||||
|
||||
|
||||
@router.put('/exercice/{id_code}', response_model=ExerciceReadFull)
|
||||
def update_exo(file: UploadFile = Depends(validate_file_optionnal), exo: Exercice = Depends(check_exercice_author), exercice: ExerciceEdit = Depends(ExerciceEdit.as_form), db: Session = Depends(get_session)):
|
||||
def update_exo(file: UploadFile = Depends(validate_file_optionnal), exo: Exercice = Depends(check_exercice_author),
|
||||
exercice: ExerciceEdit = Depends(ExerciceEdit.as_form), db: Session = Depends(get_session)):
|
||||
if exo is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable')
|
||||
@ -151,7 +167,8 @@ def update_exo(file: UploadFile = Depends(validate_file_optionnal), exo: Exercic
|
||||
|
||||
|
||||
@router.delete('/exercice/{id_code}')
|
||||
def delete_exercice(exercice: Exercice | bool | None = Depends(check_exercice_author), db: Session = Depends(get_session)):
|
||||
def delete_exercice(exercice: Exercice | bool | None = Depends(check_exercice_author),
|
||||
db: Session = Depends(get_session)):
|
||||
if exercice is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Exercice introuvable")
|
||||
@ -162,23 +179,24 @@ def delete_exercice(exercice: Exercice | bool | None = Depends(check_exercice_au
|
||||
return {'detail': 'Exercice supprimé avec succès'}
|
||||
|
||||
|
||||
|
||||
class NewTags(BaseModel):
|
||||
exo: ExerciceReadFull
|
||||
tags: list[TagRead]
|
||||
|
||||
|
||||
|
||||
@router.post('/exercice/{id_code}/tags', response_model=NewTags, tags=['tags'])
|
||||
def add_tags(tags: List[TagCreate], exo: Exercice | None = Depends(get_exo_dependency), db: Session = Depends(get_session), user: User = Depends(get_current_user)):
|
||||
def add_tags(tags: List[TagCreate], exo: Exercice | None = Depends(get_exo_dependency),
|
||||
db: Session = Depends(get_session), user: User = Depends(get_current_user)):
|
||||
if exo is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable')
|
||||
exo_obj, new = add_tags_db(exo, tags, user, db)
|
||||
return {"exo":serialize_exo(exo=exo_obj, user_id=user.id, db=db), "tags": new}
|
||||
return {"exo": serialize_exo(exo=exo_obj, user_id=user.id, db=db), "tags": new}
|
||||
|
||||
|
||||
@router.delete('/exercice/{id_code}/tags/{tag_id}', response_model=ExerciceReadFull, tags=['tags'])
|
||||
def remove_tag(exo: Exercice | None = Depends(get_exo_dependency), tag: Tag | None | bool = Depends(check_tag_author), db: Session = Depends(get_session)):
|
||||
def remove_tag(exo: Exercice | None = Depends(get_exo_dependency), tag: Tag | None | bool = Depends(check_tag_author),
|
||||
db: Session = Depends(get_session)):
|
||||
if exo is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail='Exercice introuvable')
|
||||
@ -194,7 +212,7 @@ def remove_tag(exo: Exercice | None = Depends(get_exo_dependency), tag: Tag | No
|
||||
|
||||
|
||||
@router.get('/exercice/{id_code}/exo_source')
|
||||
async def get_exo_source(exo: Exercice = Depends(check_exercice_author), db: Session = Depends(get_session)):
|
||||
async def get_exo_source(exo: Exercice = Depends(check_exercice_author)):
|
||||
if exo is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail='Exercice introuvable')
|
||||
@ -203,9 +221,30 @@ async def get_exo_source(exo: Exercice = Depends(check_exercice_author), db: Ses
|
||||
detail='Cet exercice ne vous appartient pas')
|
||||
path = add_fast_api_root(exo.exo_source)
|
||||
filename = get_filename_from_path(path)
|
||||
return FileResponse(path, headers={'content-disposition': 'attachment;filename='+filename})
|
||||
return FileResponse(path, headers={'content-disposition': 'attachment;filename=' + filename})
|
||||
|
||||
|
||||
@router.get('/tags', response_model=List[TagRead], tags=['tags'])
|
||||
def get_tags(user: User = Depends(get_current_user), db: Session = Depends(get_session)):
|
||||
return user.tags
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@router.get('/generator/csv/{id_code}')
|
||||
async def generate_csv(*, exo: Exercice | None = Depends(get_exo_dependency), filename: str):
|
||||
if exo.csv is False:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail='Impossible de générer cet exercice sur dans ce format')
|
||||
|
||||
source_path = add_fast_api_root(exo.exo_source)
|
||||
consigne = exo.consigne
|
||||
|
||||
buffer = io.StringIO()
|
||||
writer = csv.writer(buffer, delimiter=',',
|
||||
quotechar=',', quoting=csv.QUOTE_MINIMAL, dialect='excel') # mettre | comme sep un jour
|
||||
Csv_generator(source_path, 10, 10, 12, consigne, writer)
|
||||
|
||||
return StreamingResponse(iter([buffer.getvalue()]),
|
||||
headers={"Content-Disposition": f'attachment;filename={filename}'}, media_type='text/csv')
|
||||
|
@ -1,29 +1,42 @@
|
||||
from typing import Any, TYPE_CHECKING, Callable
|
||||
|
||||
from fastapi.websockets import WebSocket
|
||||
from services.websocket import Consumer
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from database.room.models import Room, Member, MemberRead, Waiter
|
||||
from sqlmodel import Session
|
||||
from database.room.crud import change_room_name, change_room_status, serialize_member,check_user_in_room, create_anonymous, create_member, get_member, get_member_from_token, get_member_from_reconnect_code, connect_member, disconnect_member, create_anonymous_member, create_anonymous_waiter, create_user_member, create_user_waiter, get_or_create_member, get_waiter, accept_waiter, leave_room, refuse_waiter, check_room
|
||||
|
||||
from database.auth.crud import get_user_from_token
|
||||
from database.room.crud import change_room_name, change_room_status, serialize_member, check_user_in_room, \
|
||||
create_anonymous, create_member, get_member, get_member_from_token, get_member_from_reconnect_code, connect_member, \
|
||||
disconnect_member, get_waiter, accept_waiter, leave_room, refuse_waiter
|
||||
from database.room.models import Room, Member, MemberRead, Waiter
|
||||
from services.websocket import Consumer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from routes.room.routes import RoomManager
|
||||
|
||||
from routes.room.routes import RoomManager
|
||||
|
||||
|
||||
class RoomConsumer(Consumer):
|
||||
|
||||
|
||||
def __init__(self, ws: WebSocket, room: Room, manager: "RoomManager", db: Session):
|
||||
self.room = room
|
||||
self.ws = ws
|
||||
self.manager = manager
|
||||
self.db = db
|
||||
self.member = None
|
||||
|
||||
|
||||
# WS Utilities
|
||||
async def send(self, payload: Any | Callable):
|
||||
if callable(payload):
|
||||
payload = payload(self.member)
|
||||
return await super().send(payload)
|
||||
|
||||
async def connect(self):
|
||||
await self.ws.accept()
|
||||
|
||||
async def direct_send(self, type: str, payload: Any):
|
||||
await self.ws.send_json({'type': type, "data": payload})
|
||||
async def direct_send(self, type: str, payload: Any, code: int | None = None):
|
||||
sending = {'type': type, "data": payload, }
|
||||
if code != None:
|
||||
sending["code"] = code
|
||||
await self.ws.send_json({'type': type, "data": payload, })
|
||||
|
||||
async def send_to_admin(self, type: str, payload: Any, exclude: bool = False):
|
||||
await self.manager.send_to_admin(self.room.id_code, {'type': type, "data": payload})
|
||||
@ -32,7 +45,8 @@ class RoomConsumer(Consumer):
|
||||
await self.manager.send_to(self.room.id_code, member_id, {'type': type, "data": payload})
|
||||
|
||||
async def broadcast(self, type, payload, exclude=False):
|
||||
await self.manager.broadcast({"type": type, "data": payload}, self.room.id_code, exclude=[exclude == True and self])
|
||||
await self.manager.broadcast({"type": type, "data": payload}, self.room.id_code,
|
||||
exclude=[exclude == True and self])
|
||||
|
||||
def add_to_group(self):
|
||||
self.manager.add(self.room.id_code, self)
|
||||
@ -40,13 +54,20 @@ class RoomConsumer(Consumer):
|
||||
async def connect_self(self):
|
||||
if isinstance(self.member, Member):
|
||||
connect_member(self.member, self.db)
|
||||
await self.broadcast(type="connect", payload={"member": serialize_member(self.member)}, exclude=True)
|
||||
await self.manager.broadcast(lambda m: {"type": "connect", "data": {
|
||||
"member": serialize_member(self.member, admin=m.is_admin, m2=m)}}, self.room.id_code, exclude=[self])
|
||||
# await self.broadcast(type="connect", payload={"member": serialize_member(self.member)}, exclude=True)
|
||||
|
||||
async def disconnect_self(self):
|
||||
if isinstance(self.member, Member):
|
||||
''' self.db.expire(self.member)
|
||||
self.db.refresh(self.member) '''
|
||||
disconnect_member(self.member, self.db)
|
||||
if self.member.waiting is False:
|
||||
await self.broadcast(type="disconnect", payload={"member": serialize_member(self.member)})
|
||||
await self.manager.broadcast(lambda m: {"type": "disconnect", "data": {
|
||||
"member": serialize_member(self.member, admin=m.is_admin, m2=m)}}, self.room.id_code,
|
||||
exclude=[self])
|
||||
# await self.broadcast(type="disconnect", payload={"member": serialize_member(self.member)})
|
||||
else:
|
||||
await self.send_to_admin(type="disconnect_waiter", payload={"waiter": serialize_member(self.member)})
|
||||
|
||||
@ -55,11 +76,12 @@ class RoomConsumer(Consumer):
|
||||
self.member = member
|
||||
await self.connect_self()
|
||||
self.add_to_group()
|
||||
clientId = self.member.anonymous.clientId if self.member.anonymous is not None else ""
|
||||
await self.direct_send(type="loggedIn", payload={"member": {**serialize_member(self.member), 'clientId': str(clientId)}})
|
||||
await self.direct_send(type="loggedIn",
|
||||
payload={"member": {**serialize_member(self.member, private=True, m2=self.member)}})
|
||||
|
||||
async def send_error(self, msg, code: int = 400):
|
||||
await self.direct_send(type="error", payload={"msg": msg, "code": code})
|
||||
|
||||
async def send_error(self, msg):
|
||||
await self.direct_send(type="error", payload={"msg": msg})
|
||||
# Conditions
|
||||
|
||||
async def isAdminReceive(self):
|
||||
@ -71,38 +93,40 @@ class RoomConsumer(Consumer):
|
||||
|
||||
def isAdmin(self):
|
||||
return self.member is not None and self.member.is_admin == True
|
||||
|
||||
|
||||
async def isMember(self):
|
||||
print('S', self.member, self.ws, self.ws.state, self.ws.application_state.__str__())
|
||||
if self.member is None:
|
||||
await self.send_error("Vous n'êtes connecté à aucune salle")
|
||||
return self.member is not None and self.member.waiting == False
|
||||
|
||||
|
||||
def isWaiter(self):
|
||||
return self.member is not None and self.member.waiting == True
|
||||
|
||||
# Received Events
|
||||
|
||||
|
||||
@Consumer.event('login')
|
||||
async def login(self, token: str | None = None, reconnect_code: str | None = 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"})
|
||||
return
|
||||
|
||||
print("login", token)
|
||||
if token is not None:
|
||||
member = get_member_from_token(token, self.room.id, self.db)
|
||||
print('MEMBER', member)
|
||||
if member == False:
|
||||
await self.send_error("Token expired")
|
||||
await self.send_error("Token expired", code=422)
|
||||
return
|
||||
if member is None:
|
||||
await self.send_error("Utilisateur introuvable dans cette salle")
|
||||
await self.send_error("Utilisateur introuvable dans cette salle", code=401)
|
||||
return
|
||||
|
||||
elif reconnect_code is not None:
|
||||
member = get_member_from_reconnect_code(
|
||||
reconnect_code, self.room.id, db=self.db)
|
||||
if member is None:
|
||||
await self.send_error("Utilisateur introuvable dans cette salle")
|
||||
await self.send_error("Utilisateur introuvable dans cette salle", code=401)
|
||||
return
|
||||
|
||||
await self.loginMember(member)
|
||||
|
||||
@Consumer.event('join')
|
||||
@ -116,9 +140,9 @@ class RoomConsumer(Consumer):
|
||||
if user is False:
|
||||
await self.send_error("Token expired")
|
||||
return
|
||||
|
||||
|
||||
userInRoom = check_user_in_room(user.id, self.room.id, self.db)
|
||||
|
||||
|
||||
if userInRoom is not None:
|
||||
await self.loginMember(userInRoom)
|
||||
return
|
||||
@ -127,6 +151,9 @@ class RoomConsumer(Consumer):
|
||||
user=user, room=self.room, waiting=self.room.public is False, db=self.db)
|
||||
|
||||
elif username is not None:
|
||||
if len(username) < 4 or len(username) > 15:
|
||||
await self.send_error("Nom d'utilisateur invalide ou indisponible")
|
||||
return
|
||||
anonymous = create_anonymous(username, self.room, self.db)
|
||||
if anonymous is None:
|
||||
await self.send_error("Nom d'utilisateur invalide ou indisponible")
|
||||
@ -134,61 +161,69 @@ class RoomConsumer(Consumer):
|
||||
|
||||
waiter = create_member(
|
||||
anonymous=anonymous, room=self.room, waiting=self.room.public is False, db=self.db)
|
||||
|
||||
|
||||
self.member = waiter
|
||||
self.add_to_group()
|
||||
|
||||
|
||||
if self.room.public is False:
|
||||
await self.direct_send(type="waiting", payload={"waiter": serialize_member(self.member)})
|
||||
await self.send_to_admin(type="waiter", payload={"waiter": serialize_member(self.member)})
|
||||
else:
|
||||
await self.broadcast(type="joined", payload={"member": serialize_member(self.member)}, exclude=True)
|
||||
await self.direct_send(type="accepted", payload={"member": serialize_member(self.member)})
|
||||
|
||||
await self.manager.broadcast(
|
||||
lambda m: {"type": "joined", "data": {"member": serialize_member(self.member, admin=m.is_admin, m2=m)}},
|
||||
self.room.id_code)
|
||||
# await self.broadcast(type="joined", payload={"member": serialize_member(self.member)}, exclude=True)
|
||||
await self.direct_send(type="accepted",
|
||||
payload={"member": serialize_member(self.member, private=True, m2=self.member)})
|
||||
|
||||
@Consumer.event('accept', conditions=[isAdminReceive])
|
||||
async def accept(self, waiter_id: str):
|
||||
waiter = get_waiter(waiter_id, self.db)
|
||||
if waiter is None:
|
||||
await self.send_error("Utilisateur en list d'attente introuvable")
|
||||
await self.send_error("Utilisateur en liste d'attente introuvable")
|
||||
return
|
||||
member = accept_waiter(waiter, self.db)
|
||||
await self.send_to(type="accepted", payload={"member": serialize_member(member)}, member_id=waiter_id)
|
||||
await self.broadcast(type="joined", payload={"member": serialize_member(member)})
|
||||
await self.send_to(type="accepted", payload={"member": serialize_member(member, private=True, m2=member)},
|
||||
member_id=waiter_id)
|
||||
await self.manager.broadcast(
|
||||
lambda m: {"type": "joined",
|
||||
"data": {"member": serialize_member(member, admin=m.is_admin, m2=m)}},
|
||||
self.room.id_code)
|
||||
# await self.broadcast(type="joined", payload={"member": serialize_member(member)})
|
||||
|
||||
@Consumer.event('refuse', conditions=[isAdminReceive])
|
||||
async def accept(self, waiter_id: str):
|
||||
async def refuse(self, waiter_id: str):
|
||||
waiter = get_waiter(waiter_id, self.db)
|
||||
member = refuse_waiter(waiter, self.db)
|
||||
refuse_waiter(waiter, self.db)
|
||||
await self.send_to(type="refused", payload={'waiter_id': waiter_id}, member_id=waiter_id)
|
||||
await self.direct_send(type="successfullyRefused", payload= {"waiter_id": waiter_id})
|
||||
|
||||
await self.direct_send(type="successfullyRefused", payload={"waiter_id": waiter_id})
|
||||
|
||||
@Consumer.event('ping_room')
|
||||
async def proom(self):
|
||||
await self.broadcast(type='ping', payload={}, exclude=True)
|
||||
|
||||
|
||||
@Consumer.event('sub_parcours')
|
||||
async def sub_parcours(self, parcours_id: str):
|
||||
if isinstance(self.member, Member) and self.member.waiting == False:
|
||||
self.manager.add(parcours_id, self)
|
||||
|
||||
|
||||
@Consumer.event('unsub_parcours')
|
||||
async def unsub_parcours(self, parcours_id: str):
|
||||
if isinstance(self.member, Member) and self.member.waiting == False:
|
||||
self.manager.remove(parcours_id, self)
|
||||
|
||||
|
||||
@Consumer.event('set_name', conditions=[isAdminReceive])
|
||||
async def change_name(self, name: str):
|
||||
if len(name) < 20:
|
||||
self.room = change_room_name(self.room,name, self.db)
|
||||
self.room = change_room_name(self.room, name, self.db)
|
||||
print('SENDING')
|
||||
await self.broadcast(type="new_name", payload={"name": name})
|
||||
|
||||
|
||||
return
|
||||
await self.send_error('Nom trop long (max 20 character)')
|
||||
|
||||
|
||||
@Consumer.event('set_visibility', conditions=[isAdminReceive])
|
||||
async def change_name(self, public: bool):
|
||||
async def change_visibility(self, public: bool):
|
||||
self.room = change_room_status(self.room, public, self.db)
|
||||
await self.broadcast(type="new_visibility", payload={"public": public})
|
||||
|
||||
@ -197,7 +232,6 @@ class RoomConsumer(Consumer):
|
||||
await self.direct_send(type="error", payload={"msg": "Vous n'êtes connecté à aucune salle"})
|
||||
return self.member is not None
|
||||
|
||||
|
||||
@Consumer.event('leave', conditions=[isMember])
|
||||
async def leave(self):
|
||||
if self.member.is_admin is True:
|
||||
@ -205,7 +239,7 @@ class RoomConsumer(Consumer):
|
||||
return
|
||||
member_obj = serialize_member(self.member)
|
||||
leave_room(self.member, self.db)
|
||||
|
||||
|
||||
await self.direct_send(type="successfully_leaved", payload={})
|
||||
await self.broadcast(type='leaved', payload={"member": member_obj})
|
||||
self.member = None
|
||||
@ -219,7 +253,7 @@ class RoomConsumer(Consumer):
|
||||
if member.is_admin is True:
|
||||
await self.send_error("Vous ne pouvez pas bannir un administrateur")
|
||||
return
|
||||
|
||||
|
||||
member_serialized = serialize_member(member)
|
||||
leave_room(member, self.db)
|
||||
await self.send_to(type="banned", payload={}, member_id=member.id_code)
|
||||
@ -227,10 +261,11 @@ class RoomConsumer(Consumer):
|
||||
|
||||
# Sending Events
|
||||
|
||||
@Consumer.sending(['connect', "disconnect", "joined"], conditions=[isMember])
|
||||
@Consumer.sending(["joined"], conditions=[isMember])
|
||||
def joined(self, member: MemberRead):
|
||||
|
||||
if self.member.id_code == member.id_code:
|
||||
raise ValueError("") # Prevent from sending event
|
||||
raise ValueError("") # Prevent from sending event
|
||||
if self.member.is_admin == False:
|
||||
member.reconnect_code = ""
|
||||
return {"member": member}
|
||||
@ -239,6 +274,11 @@ class RoomConsumer(Consumer):
|
||||
def waiter(self, waiter: Waiter):
|
||||
return {"waiter": waiter}
|
||||
|
||||
@Consumer.sending('accepted')
|
||||
def accepted(self, member: MemberRead):
|
||||
self.db.refresh(self.member)
|
||||
return {"member": member}
|
||||
|
||||
@Consumer.sending("refused", conditions=[isWaiter])
|
||||
def refused(self, waiter_id: str):
|
||||
self.member = None
|
||||
@ -249,6 +289,7 @@ class RoomConsumer(Consumer):
|
||||
def banned(self):
|
||||
self.member = None
|
||||
self.manager.remove(self.room.id, self)
|
||||
self.ws.close()
|
||||
return {}
|
||||
|
||||
@Consumer.sending('ping', conditions=[isMember])
|
||||
@ -256,6 +297,6 @@ class RoomConsumer(Consumer):
|
||||
return {}
|
||||
|
||||
async def disconnect(self):
|
||||
print('DISCONNECTED', self.member)
|
||||
self.manager.remove(self.room.id, self)
|
||||
await self.disconnect_self()
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
from typing import TYPE_CHECKING, Dict, List, Callable, Any
|
||||
from starlette.websockets import WebSocketState
|
||||
if TYPE_CHECKING:
|
||||
from routes.room.consumer import RoomConsumer
|
||||
@ -16,7 +16,8 @@ class RoomManager:
|
||||
self.active_connections[group].append(member)
|
||||
|
||||
async def _send(self, connection: "RoomConsumer", message, group: str):
|
||||
if connection.ws.application_state == WebSocketState.DISCONNECTED:
|
||||
print("STATE", connection.ws.client_state.__str__())
|
||||
if connection.ws.application_state == WebSocketState.DISCONNECTED or connection.ws.client_state == WebSocketState.DISCONNECTED:
|
||||
self.remove(group, connection)
|
||||
elif connection.ws.application_state == WebSocketState.CONNECTED:
|
||||
await connection.send(message)
|
||||
@ -24,14 +25,17 @@ class RoomManager:
|
||||
def remove(self, group: str, member: "RoomConsumer"):
|
||||
if group in self.active_connections:
|
||||
if member in self.active_connections[group]:
|
||||
print("remoied")
|
||||
self.active_connections[group].remove(member)
|
||||
|
||||
async def broadcast(self, message, group: str, exclude: list["RoomConsumer"] = []):
|
||||
print('BROADCaST', message)
|
||||
async def broadcast(self, message: Any | Callable, group: str, conditions: list[Callable] = [], exclude: list["RoomConsumer"] = [], ):
|
||||
print('BROADCaST', message, self.active_connections)
|
||||
|
||||
|
||||
if group in self.active_connections:
|
||||
for connection in list(set(self.active_connections[group])):
|
||||
# print(connection)
|
||||
if connection not in exclude:
|
||||
print(connection, connection.ws.state, connection.ws.client_state, connection.ws.application_state)
|
||||
if connection not in exclude and all(f(connection) for f in conditions ):
|
||||
await self._send(connection, message, group)
|
||||
|
||||
async def send_to(self, group, id_code, msg):
|
||||
@ -44,6 +48,6 @@ class RoomManager:
|
||||
async def send_to_admin(self, group, msg):
|
||||
if group in self.active_connections:
|
||||
members = [c for c in self.active_connections[group]
|
||||
if c.member.is_admin == True]
|
||||
if c.member is not None and c.member.is_admin == True]
|
||||
for m in members:
|
||||
await self._send(m, msg, group)
|
||||
|
@ -1,26 +1,52 @@
|
||||
from services.database import generate_unique_code
|
||||
from services.io import add_fast_api_root
|
||||
from generateur.generateur_main import generate_from_path, parseGeneratorOut, parseOut
|
||||
from database.exercices.models import Exercice
|
||||
from database.room.crud import CorrigedChallenge, change_correction, corrige_challenge, create_parcours_db, delete_parcours_db, create_room_db, get_member_dep, check_room, serialize_room, update_parcours_db, get_parcours, get_room, check_admin, get_exercices, get_challenge, get_correction, create_tmp_correction, create_challenge, change_challenge
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
from fastapi import APIRouter, Depends, WebSocket, status, Query, Body
|
||||
from config import ALGORITHM, SECRET_KEY
|
||||
from database.auth.crud import get_user_from_clientId_db
|
||||
from fastapi.exceptions import HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from database.auth.models import User
|
||||
from database.db import get_session
|
||||
|
||||
from sqlmodel import Session, col, select
|
||||
from database.room.models import Challenge, ChallengeRead, Challenges, CorrigedGeneratorOut, Member, Note, Parcours, ParcoursCreate, ParcoursRead, ParcoursReadShort, ParsedGeneratorOut, Room, RoomConnectionInfos, RoomCreate, RoomAndMember, RoomInfo, TmpCorrection
|
||||
from database.exercices.models import Exercice
|
||||
from database.room.crud import serialize_parcours_short, change_correction, corrige_challenge, \
|
||||
create_parcours_db, delete_parcours_db, create_room_db, get_member_dep, check_room, serialize_room, \
|
||||
update_parcours_db, get_parcours, get_room, check_admin, get_exercices, get_challenge, get_correction, \
|
||||
create_tmp_correction, create_challenge, change_challenge, serialize_parcours, getTops, getAvgRank, getRank, \
|
||||
getAvgTops, ChallengerFromChallenge, getMemberAvgRank, getMemberRank
|
||||
from database.room.models import Challenge, ChallengeRead, Challenges, ParcoursReadUpdate, ChallengeInfo, Member, \
|
||||
Parcours, ParcoursCreate, ParcoursRead, ParcoursReadShort, Room, RoomConnectionInfos, \
|
||||
RoomCreate, RoomInfo, TmpCorrection, CorrigedData, CorrectionData
|
||||
from generateur.generateur_main import generate_from_path, parseGeneratorOut
|
||||
from routes.room.consumer import RoomConsumer
|
||||
from routes.room.manager import RoomManager
|
||||
from services.auth import get_current_user_optional
|
||||
from services.io import add_fast_api_root
|
||||
from services.misc import stripKeyDict
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, WebSocket, status, Query, Body
|
||||
from fastapi.exceptions import HTTPException
|
||||
from database.auth.crud import get_user_from_token
|
||||
from services.websocket import Consumer
|
||||
from services.misc import noteOn20, stripKeyDict
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from database.auth.models import User
|
||||
from database.db import get_session
|
||||
from database.exercices.models import Exercice
|
||||
from database.room.crud import serialize_parcours_short, change_correction, corrige_challenge, \
|
||||
create_parcours_db, delete_parcours_db, create_room_db, get_member_dep, check_room, serialize_room, \
|
||||
update_parcours_db, get_parcours, get_room, check_admin, get_exercices, get_challenge, get_correction, \
|
||||
create_tmp_correction, create_challenge, change_challenge, serialize_parcours, getTops, getAvgRank, getRank, \
|
||||
getAvgTops, ChallengerFromChallenge, getMemberAvgRank, getMemberRank
|
||||
from database.room.models import Challenge, ChallengeRead, Challenges, ParcoursReadUpdate, ChallengeInfo, Member, \
|
||||
Parcours, ParcoursCreate, ParcoursRead, ParcoursReadShort, Room, RoomConnectionInfos, \
|
||||
RoomCreate, RoomInfo, TmpCorrection, CorrigedData, CorrectionData
|
||||
from generateur.generateur_main import generate_from_path, parseGeneratorOut
|
||||
from routes.room.consumer import RoomConsumer
|
||||
from routes.room.manager import RoomManager
|
||||
from services.auth import get_current_user_optional
|
||||
from services.io import add_fast_api_root
|
||||
from services.misc import stripKeyDict
|
||||
|
||||
router = APIRouter(tags=["room"])
|
||||
manager = RoomManager()
|
||||
|
||||
@ -28,6 +54,7 @@ manager = RoomManager()
|
||||
def get_manager():
|
||||
return manager
|
||||
|
||||
|
||||
@router.post('/room', response_model=RoomConnectionInfos)
|
||||
def create_room(room: RoomCreate, username: Optional[str] = Query(default=None, max_length=20), user: User | None = Depends(get_current_user_optional), db: Session = Depends(get_session)):
|
||||
room_obj = create_room_db(room=room, user=user, username=username, db=db)
|
||||
@ -39,36 +66,42 @@ def get_room_route(room: Room = Depends(get_room), member: Member = Depends(get_
|
||||
return serialize_room(room, member, db)
|
||||
|
||||
|
||||
|
||||
@router.post('/room/{room_id}/parcours', response_model=ParcoursRead)
|
||||
async def create_parcours(*, parcours: ParcoursCreate, room_id: str, member: Member = Depends(check_admin), m: RoomManager = Depends(get_manager), db: Session = Depends(get_session)):
|
||||
parcours_obj = create_parcours_db(parcours, member.room_id, db)
|
||||
if type(parcours_obj) == str:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail=parcours_obj)
|
||||
await m.broadcast({"type": "add_parcours", "data": {"parcours": ParcoursReadShort(**parcours_obj.dict(exclude_unset=True)).dict()}}, room_id)
|
||||
return parcours_obj
|
||||
await m.broadcast({"type": "add_parcours",
|
||||
"data": {"parcours": ParcoursReadShort(**parcours_obj.dict(exclude_unset=True)).dict()}},
|
||||
room_id)
|
||||
|
||||
return serialize_parcours(parcours_obj, member, db)
|
||||
|
||||
|
||||
@router.get('/room/{room_id}/parcours/{parcours_id}', response_model=ParcoursRead)
|
||||
async def get_parcours_route(*, parcours: Parcours = Depends(get_parcours), member: Member = Depends(get_member_dep), db: Session = Depends(get_session)):
|
||||
if member.is_admin == False:
|
||||
return {**parcours.dict(), "challenges": [Challenges(**{**chall.dict(), "challenger": member.id_code, "canCorrige": chall.data != []}) for chall in parcours.challenges if chall.challenger.id_code == member.id_code]}
|
||||
|
||||
if member.is_admin == True:
|
||||
return {**parcours.dict(), "challenges": [Challenges(**{**chall.dict(), "challenger": member.id_code, "canCorrige": chall.data != []}) for chall in parcours.challenges]}
|
||||
return serialize_parcours(parcours, member, db)
|
||||
|
||||
|
||||
@router.put('/room/{room_id}/parcours/{parcours_id}', response_model=ParcoursRead, dependencies=[Depends(check_admin)])
|
||||
async def update_parcours(*, room_id: str, parcours: ParcoursCreate, parcours_old: Parcours = Depends(get_parcours), m: RoomManager = Depends(get_manager), db: Session = Depends(get_session)):
|
||||
parcours_obj = update_parcours_db(parcours, parcours_old, db)
|
||||
@router.put('/room/{room_id}/parcours/{parcours_id}', response_model=ParcoursRead)
|
||||
async def update_parcours(*, room_id: str, parcours: ParcoursCreate, member: Member = Depends(check_admin),
|
||||
parcours_old: Parcours = Depends(get_parcours), m: RoomManager = Depends(get_manager),
|
||||
db: Session = Depends(get_session)):
|
||||
parcours_obj, update_challenges = update_parcours_db(
|
||||
parcours, parcours_old, db)
|
||||
if type(parcours_obj) == str:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail=parcours_obj)
|
||||
short = ParcoursReadShort(
|
||||
**parcours_obj.dict(exclude_unset=True), id_code=parcours_obj.id_code)
|
||||
await m.broadcast({"type": "update_parcours", "data": {"parcours": short.dict()}}, room_id)
|
||||
return parcours_obj
|
||||
|
||||
await m.broadcast(lambda m: {"type": "update_parcours",
|
||||
"data": {"parcours": serialize_parcours_short(parcours_obj, m, db).dict()}}, room_id)
|
||||
await m.broadcast({"type": "edit_parcours", "data": {
|
||||
"parcours": ParcoursReadUpdate(**parcours_obj.dict(), update_challenges=update_challenges).dict()}},
|
||||
parcours_old.id_code)
|
||||
|
||||
return serialize_parcours(parcours_obj, member, db)
|
||||
return {**parcours_obj.dict()}
|
||||
|
||||
|
||||
@router.delete('/room/{room_id}/parcours/{parcours_id}', dependencies=[Depends(check_admin)])
|
||||
@ -82,51 +115,133 @@ class Exos(BaseModel):
|
||||
exercice: Exercice
|
||||
quantity: int
|
||||
|
||||
|
||||
@router.get('/room/{room_id}/challenge/{parcours_id}')
|
||||
def challenge_route(parcours_id: str, exercices: List[Exos] = Depends(get_exercices), member: Member = Depends(get_member_dep), db: Session = Depends(get_session)):
|
||||
def challenge_route(parcours: Parcours = Depends(get_parcours), exercices: List[Exos] = Depends(get_exercices),
|
||||
member: Member = Depends(get_member_dep), db: Session = Depends(get_session)):
|
||||
print('GENERATE', exercices)
|
||||
correction = [parseGeneratorOut(generate_from_path(add_fast_api_root(
|
||||
e['exercice'].exo_source), e['quantity'], "web")) for e in exercices]
|
||||
sending = [[{**c, 'inputs': [stripKeyDict(i, "correction")
|
||||
for i in c['inputs']]} for c in e] for e in correction]
|
||||
tmpCorr = create_tmp_correction(correction, parcours_id, member, db)
|
||||
|
||||
return {'challenge': sending, "id_code": tmpCorr.id_code}
|
||||
infos = [{"name": e["exercice"].name, "consigne": e['exercice'].consigne, "id_code": e['exercice'].id_code}
|
||||
for e in exercices]
|
||||
|
||||
sending = [{"exo": ei, "data": [{**c, 'inputs': [stripKeyDict(i, "correction")
|
||||
for i in c['inputs']]} for c in e]} for e, ei in
|
||||
zip(correction, infos)]
|
||||
|
||||
corriged = [{"exo": ei, "data": e} for e, ei in zip(correction, infos)]
|
||||
tmpCorr = create_tmp_correction(corriged, parcours.id_code, member, db)
|
||||
|
||||
return {'challenge': sending, "id_code": tmpCorr.id_code,
|
||||
'parcours': {"name": parcours.name, 'time': parcours.time, "max_mistakes": parcours.max_mistakes,
|
||||
'id_code': parcours.id_code}}
|
||||
|
||||
|
||||
@router.post('/room/{room_id}/challenge/{parcours_id}/{correction_id}', response_model=ChallengeRead)
|
||||
async def send_challenge(*, challenge: List[List[ParsedGeneratorOut]], correction: TmpCorrection = Depends(get_correction), time: int = Body(), db: Session = Depends(get_session), m: RoomManager = Depends(get_manager),):
|
||||
async def send_challenge(*, challenge: List[CorrectionData], correction: TmpCorrection = Depends(get_correction),
|
||||
time: int = Body(), db: Session = Depends(get_session),
|
||||
m: RoomManager = Depends(get_manager), ):
|
||||
parcours = correction.parcours
|
||||
member = correction.member
|
||||
data = corrige_challenge(challenge, correction)
|
||||
|
||||
if data is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail={"challenge_error":"Object does not correspond to correction"})
|
||||
chall = create_challenge(**data, challenger=member,
|
||||
parcours=parcours, time=time, db=db)
|
||||
|
||||
await m.broadcast({"type": "challenge", "data": Challenges(**{**chall.dict(), "challenger": member.id_code, "canCorrige": chall.data != []}).dict()}, parcours.id_code)
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={"challenge_error": "Object does not correspond to correction"})
|
||||
chall, challenger = create_challenge(**data, challenger=member,
|
||||
parcours=parcours, time=time, db=db)
|
||||
print('CHALLENGE', chall)
|
||||
await m.broadcast({"type": "challenge", "data": ChallengeInfo(
|
||||
challenger={"name": member.user.username if member.user_id != None else member.anonymous.username,
|
||||
"id_code": member.id_code},
|
||||
challenges=[Challenges(**{**chall.dict(), "canCorrige": chall.data != [], })]).dict()}, parcours.id_code,
|
||||
conditions=[lambda c: c.member.is_admin or c.member.id_code == member.id_code])
|
||||
#TODO : Envoyer que à ceux d'après
|
||||
await m.broadcast(lambda m: {"type": "newRanks", "data": {"rank": getMemberRank(m, correction.parcours, db),
|
||||
"avgRank": getMemberAvgRank(m, correction.parcours, db)}},
|
||||
parcours.id_code)
|
||||
print('CHALLENGE', chall)
|
||||
rank, avgRank = getRank(
|
||||
challenger, parcours, db), getAvgRank(challenger, parcours, db)
|
||||
print('RANKS', rank, avgRank)
|
||||
if rank <= 3 or avgRank <= 3:
|
||||
await m.broadcast({"type": "newTops", "data": {
|
||||
"tops": getTops(correction.parcours, db),
|
||||
"avgTops": getAvgTops(correction.parcours, db),
|
||||
}}, parcours.id_code)
|
||||
print('CHALLENGE', chall)
|
||||
db.delete(correction)
|
||||
returnValue = {**chall.dict(), 'validated': chall.mistakes <= correction.parcours.max_mistakes}
|
||||
db.commit()
|
||||
return chall
|
||||
return returnValue
|
||||
# return {**chall.dict(), 'validated': chall.mistakes <= correction.parcours.max_mistakes}
|
||||
|
||||
|
||||
@router.get('/room/{room_id}/challenge/{parcours_id}/{challenge_id}', response_model=ChallengeRead, dependencies=[Depends(get_member_dep)])
|
||||
async def challenge_read(*, challenge: Challenge = Depends(get_challenge)):
|
||||
return challenge
|
||||
class ParcoursInfo(BaseModel):
|
||||
name: str
|
||||
time: int
|
||||
# validate: int
|
||||
id_code: str
|
||||
|
||||
|
||||
@router.put('/room/{room_id}/challenge/{parcours_id}/{challenge_id}', response_model=ChallengeRead, dependencies=[Depends(check_admin)])
|
||||
async def corrige(*, correction: List[List[CorrigedGeneratorOut]], challenge: Challenge = Depends(get_challenge), db: Session = Depends(get_session), m: RoomManager = Depends(get_manager),):
|
||||
class Chall(BaseModel):
|
||||
challenge: Challenge
|
||||
parcours: ParcoursInfo
|
||||
|
||||
|
||||
# response_model=ChallengeRead
|
||||
@router.get('/room/{room_id}/correction/{challenge_id}', dependencies=[Depends(get_member_dep)])
|
||||
async def challenge_read(*, challenge: Challenge = Depends(get_challenge), db: Session = Depends(get_session)):
|
||||
parcours = challenge.parcours
|
||||
member = challenge.challenger
|
||||
|
||||
member = db.exec(select(Member).where(
|
||||
Member.id == challenge.challenger_mid)).first()
|
||||
challenger = ChallengerFromChallenge(challenge, db)
|
||||
obj = member.user if member.user_id is not None else member.anonymous
|
||||
return {**ChallengeRead(**challenge.dict()).dict(), "challenger": {"name": obj.username},
|
||||
'parcours': {"name": parcours.name, 'time': parcours.time, "max_mistakes": parcours.max_mistakes,
|
||||
'id_code': parcours.id_code}}
|
||||
|
||||
|
||||
# response_model=ChallengeRead
|
||||
|
||||
|
||||
@router.put('/room/{room_id}/correction/{challenge_id}', dependencies=[Depends(check_admin)])
|
||||
async def corrige(*, correction: List[CorrigedData] = Body(), challenge: Challenge = Depends(get_challenge),
|
||||
db: Session = Depends(get_session), m: RoomManager = Depends(get_manager), ):
|
||||
data = change_correction(correction, challenge)
|
||||
if data is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail={"correction_error": "Object does not correspond to challenge"})
|
||||
challenge = change_challenge(challenge, data, db)
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={"correction_error": "Object does not correspond to challenge"})
|
||||
|
||||
await m.broadcast({"type": "challenge_change", "data": Challenges(**{**challenge.dict(), "challenger": member.id_code, "canCorrige": challenge.data != []}).dict()}, parcours.id_code)
|
||||
return challenge
|
||||
parcours = challenge.parcours
|
||||
member = db.exec(select(Member).where(
|
||||
Member.id == challenge.challenger_mid)).first()
|
||||
challenge, challenger = change_challenge(challenge, data, db)
|
||||
obj = member.user if member.user_id is not None else member.anonymous
|
||||
|
||||
await m.broadcast(lambda m: {"type": "newRanks", "data": {"rank": getMemberRank(m, parcours, db),
|
||||
"avgRank": getMemberAvgRank(m, parcours, db)}},
|
||||
parcours.id_code)
|
||||
|
||||
rank, avgRank = getRank(
|
||||
challenger, parcours, db), getAvgRank(challenger, parcours, db)
|
||||
print('Rank', rank, avgRank)
|
||||
if rank <= 3 or avgRank <= 3:
|
||||
await m.broadcast({"type": "newTops", "data": {
|
||||
"tops": getTops(parcours, db),
|
||||
"avgTops": getAvgTops(parcours, db),
|
||||
}}, parcours.id_code)
|
||||
|
||||
await m.broadcast({"type": "challenge_change",
|
||||
"data": {"challenge": Challenges(**challenge.dict()).dict(), "member": member.id_code}},
|
||||
parcours.id_code, conditions=[lambda m: m.member.is_admin or m.member.id_code == member.id_code])
|
||||
|
||||
return {**ChallengeRead(**challenge.dict()).dict(), "challenger": {"name": obj.username}}
|
||||
return {**ChallengeRead.from_orm(challenge).dict(), "challenger": {"name": obj.username, }}
|
||||
|
||||
|
||||
@router.websocket('/ws/room/{room_id}')
|
||||
|
@ -3,6 +3,7 @@ from pydantic import validate_arguments, BaseModel
|
||||
from fastapi.websockets import WebSocketDisconnect, WebSocket
|
||||
from pydantic.error_wrappers import ValidationError
|
||||
import inspect
|
||||
from starlette.websockets import WebSocketState
|
||||
|
||||
def make_event_decorator(eventsDict):
|
||||
def _(name: str | List, conditions: List[Callable | bool] = []):
|
||||
@ -68,8 +69,11 @@ class Consumer:
|
||||
await self.ws.send_json({"type": "error", "data": {"detail": [{ers['loc'][-1]: ers['msg']} for ers in errors]}})
|
||||
|
||||
async def send(self, payload):
|
||||
''' if self.ws.state == WebSocketState.DISCONNECTED:
|
||||
return '''
|
||||
type = payload.get('type', None)
|
||||
#print('TYPE', type, self.member)
|
||||
print('TYPE', type, self.member)
|
||||
|
||||
if type is not None:
|
||||
event_wrapper = self.sendings.get(type, None)
|
||||
if event_wrapper is not None:
|
||||
@ -85,6 +89,7 @@ class Consumer:
|
||||
try:
|
||||
validated_payload = model(self=self, **data)
|
||||
except ValidationError as e:
|
||||
print("ERROR", e)
|
||||
await self.ws.send_json({"type": "error", "data": {"msg": "Oops there was an error"}})
|
||||
return
|
||||
|
||||
@ -133,4 +138,5 @@ class Consumer:
|
||||
data = await self.ws.receive_json()
|
||||
await self.receive(data)
|
||||
except WebSocketDisconnect:
|
||||
print('DISCONNECTION')
|
||||
await self.disconnect()
|
||||
|
@ -116,7 +116,7 @@ def test_clone(client: TestClient):
|
||||
print(rr.json())
|
||||
assert rr.status_code == 200
|
||||
assert 'id_code' in rr.json()
|
||||
assert {**rr.json(), 'id_code': None} == {'name': 'test_exo', 'consigne': 'consigne', 'private': False, 'id_code': None, 'author': {'username': 'lilian2'}, 'original': {"id_code": id_code, "name": create['name']}, 'tags': [], 'exo_source': 'test.py', 'supports': {
|
||||
assert {**rr.json(), 'id_code': None} == {'name': 'test_exo', 'consigne': 'consigne', 'private': False, 'id_code': None, 'author': {'username': 'lilian2'}, 'original': {"id_code": id_code, "name": create['name'], 'author': 'lilian'}, 'tags': [], 'exo_source': 'test.py', 'supports': {
|
||||
'pdf': False, 'csv': True, 'web': True}, 'examples': {'type': 'csv', 'data': [{'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}]}, 'is_author': True}
|
||||
|
||||
|
||||
@ -233,22 +233,25 @@ class Tags(BaseModel):
|
||||
|
||||
|
||||
def test_add_tags(client: TestClient, name='name', tags: List[Tags] = [{'label': "name", 'color': "#ff0000",
|
||||
'id_code': "tag_id"}], user=None):
|
||||
'id_code': None}], user=None, exo =None):
|
||||
if user == None:
|
||||
token = test_register(client, username="lilian")['access']
|
||||
user = {"token": token, 'username': "lilian"}
|
||||
else:
|
||||
token = user['token']
|
||||
|
||||
exo = test_create(client, name=name, user=user)
|
||||
id_code = exo['id_code']
|
||||
if exo == None:
|
||||
exo = test_create(client, name=name, user=user)
|
||||
id_code = exo['id_code']
|
||||
else:
|
||||
id_code = exo['id_code']
|
||||
r = client.post(f'/exercice/{id_code}/tags', json=tags,
|
||||
headers={'Authorization': 'Bearer ' + token})
|
||||
print(r.json())
|
||||
print("DATA", tags, "\n\n",r.json())
|
||||
data = r.json()
|
||||
labels = [l['label'] for l in tags]
|
||||
assert r.status_code == 200
|
||||
assert {**data, "tags": [{**t, "id_code": None}
|
||||
for t in data['tags']]} == {**exo, 'tags': [*exo['tags'], *[{**t, 'id_code': None} for t in tags]]}
|
||||
assert {'exo': {**data['exo'], 'tags': [{**t, "id_code": "test"} for t in data['exo']['tags'] if t['id_code'] != None]}, "tags": [{**t, "id_code": "test"}
|
||||
for t in data['tags'] if t['id_code'] != None]} == {"exo": {**exo, 'tags': [{**t, "id_code": "test"} for t in tags]}, 'tags': [*exo['tags'], *[{**t, 'id_code': "test"} for t in tags if t['id_code'] == None]]}
|
||||
return r.json()
|
||||
|
||||
|
||||
@ -281,19 +284,19 @@ def test_add_tags_too_long(client: TestClient):
|
||||
def test_remove_tag(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
exo = test_add_tags(client, user={"token": token, 'username': "lilian"})
|
||||
id_code = exo['id_code']
|
||||
id_code = exo['exo']['id_code']
|
||||
tag_id = exo["tags"][0]["id_code"]
|
||||
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
|
||||
headers={'Authorization': 'Bearer ' + token})
|
||||
print(r.json())
|
||||
assert r.json() == {
|
||||
**exo, 'tags': exo['tags'][1:]}
|
||||
**exo['exo'], 'tags': exo['tags'][1:]}
|
||||
|
||||
|
||||
def test_remove_tag_not_found(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
exo = test_add_tags(client, user={"token": token, 'username': "lilian"})
|
||||
id_code = exo['id_code']
|
||||
id_code = exo['exo']['id_code']
|
||||
tag_id = "none"
|
||||
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
|
||||
headers={'Authorization': 'Bearer ' + token})
|
||||
@ -316,7 +319,7 @@ def test_remove_tag_not_owner(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
token2 = test_register(client, username="lilian2")['access']
|
||||
exo = test_add_tags(client, user={"token": token, 'username': "lilian"})
|
||||
id_code = exo['id_code']
|
||||
id_code = exo['exo']['id_code']
|
||||
tag_id = exo['tags'][0]['id_code']
|
||||
r = client.delete(f'/exercice/{id_code}/tags/{tag_id}',
|
||||
headers={'Authorization': 'Bearer ' + token2})
|
||||
@ -481,29 +484,31 @@ def test_get_user_with_tags(client: TestClient):
|
||||
token2 = test_register(client, username="lilian2")['access']
|
||||
|
||||
tags1 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None}]
|
||||
tags2 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None},
|
||||
tags2 = [
|
||||
{'label': "tag2", 'color': "#ff0000", 'id_code': None}]
|
||||
tags3 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None},
|
||||
{'label': "tag2", 'color': "#ff0000", 'id_code': None}, {'label': "tag3", 'color': "#ff0000", 'id_code': None}]
|
||||
|
||||
tags3 = [{'label': "tag3", 'color': "#ff0000", 'id_code': None}]
|
||||
|
||||
exo_other_user = test_create(
|
||||
client, user={'token': token2, 'username': "lilian2"})
|
||||
|
||||
exo1 = test_add_tags(client, user={
|
||||
'token': token1, 'username': "lilian"}, tags=tags1)
|
||||
|
||||
tags2 = [*exo1['tags'], *tags2]
|
||||
exo2 = test_add_tags(client, user={
|
||||
'token': token1, 'username': "lilian"}, tags=tags2)
|
||||
|
||||
tags3 = [*exo2['tags'], *tags3]
|
||||
exo3 = test_add_tags(client, user={
|
||||
'token': token1, 'username': "lilian"}, tags=tags3)
|
||||
|
||||
tags1 = exo1['tags']
|
||||
tags2 = exo2['tags']
|
||||
tags3 = exo3['tags']
|
||||
r = client.get('/exercices/user', params={'tags': [*[t['id_code'] for t in tags2], 'notexist']},
|
||||
r = client.get('/exercices/user', params={'tags': [*[t['id_code'] for t in tags2], 'notexisting']},
|
||||
headers={'Authorization': 'Bearer ' + token1})
|
||||
print(r.json())
|
||||
assert r.json()['items'] == [exo2, exo3]
|
||||
print("DATA", r.json())
|
||||
assert r.json()['items'] == [exo2['exo'], exo3['exo']]
|
||||
|
||||
|
||||
def test_get_user_with_tags_and_search(client: TestClient):
|
||||
@ -511,20 +516,18 @@ def test_get_user_with_tags_and_search(client: TestClient):
|
||||
token2 = test_register(client, username="lilian2")['access']
|
||||
|
||||
tags1 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None}]
|
||||
tags2 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None},
|
||||
{'label': "tag2", 'color': "#ff0000", 'id_code': None}]
|
||||
tags3 = [{'label': "tag1", 'color': "#ff0000", 'id_code': None},
|
||||
{'label': "tag2", 'color': "#ff0000", 'id_code': None}, {'label': "tag3", 'color': "#ff0000", 'id_code': None}]
|
||||
tags2 = [{'label': "tag2", 'color': "#ff0000", 'id_code': None}]
|
||||
tags3 = [{'label': "tag3", 'color': "#ff0000", 'id_code': None}]
|
||||
|
||||
exo_other_user = test_create(
|
||||
client, user={'token': token2, 'username': "lilian2"})
|
||||
|
||||
exo1 = test_add_tags(client, user={
|
||||
'token': token1, 'username': "lilian"}, tags=tags1, name="yes")
|
||||
|
||||
tags2 = [*exo1['tags'], *tags2]
|
||||
exo2 = test_add_tags(client, user={
|
||||
'token': token1, 'username': "lilian"}, tags=tags2, name="no")
|
||||
|
||||
tags3 = [*exo2['tags'], *tags3]
|
||||
exo3 = test_add_tags(client, user={
|
||||
'token': token1, 'username': "lilian"}, tags=tags3, name="yes")
|
||||
|
||||
@ -534,7 +537,7 @@ def test_get_user_with_tags_and_search(client: TestClient):
|
||||
r = client.get('/exercices/user', params={"search": "yes", 'tags': [t['id_code'] for t in tags2]},
|
||||
headers={'Authorization': 'Bearer ' + token1})
|
||||
print(r.json())
|
||||
assert r.json()['items'] == [exo3]
|
||||
assert r.json()['items'] == [exo3['exo']]
|
||||
|
||||
|
||||
def test_get_public_auth(client: TestClient):
|
||||
@ -595,8 +598,8 @@ def test_get_exo_no_auth(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
|
||||
|
||||
r = client.get('/exercice/' + exo['id_code'])
|
||||
assert r.json() == {**exo, "tags": [], 'is_author': False}
|
||||
r = client.get('/exercice/' + exo['exo']['id_code'])
|
||||
assert r.json() == {**exo['exo'], "tags": [], 'is_author': False}
|
||||
|
||||
|
||||
def test_get_exo_no_auth_private(client: TestClient):
|
||||
@ -613,20 +616,24 @@ def test_get_exo_auth(client: TestClient):
|
||||
token2 = test_register(client, username="lilian2")['access']
|
||||
exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
|
||||
|
||||
r = client.get('/exercice/' + exo['id_code'],
|
||||
r = client.get('/exercice/' + exo['exo']['id_code'],
|
||||
headers={'Authorization': 'Bearer ' + token2})
|
||||
print(r.json(), exo)
|
||||
assert r.json() == {**exo, "tags": [], 'is_author': False}
|
||||
assert r.json() == {**exo['exo'], "tags": [], 'is_author': False}
|
||||
|
||||
|
||||
def test_get_exo_auth_with_tags(client: TestClient):
|
||||
token = test_register(client, username="lilian")['access']
|
||||
token2 = test_register(client, username="lilian2")['access']
|
||||
|
||||
exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
|
||||
|
||||
r = client.get('/exercice/' + exo['id_code'],
|
||||
test_add_tags(client, user={'token': token2, 'username': "lilian2"}, exo={**exo['exo'], "is_author": False, "tags": []})
|
||||
|
||||
r = client.get('/exercice/' + exo['exo']['id_code'],
|
||||
headers={'Authorization': 'Bearer ' + token})
|
||||
|
||||
print(r.json(), exo)
|
||||
assert r.json() == {**exo}
|
||||
assert r.json() == {**exo['exo']}
|
||||
|
||||
|
||||
def test_get_exo_auth_private(client: TestClient):
|
||||
@ -668,7 +675,7 @@ def test_get_csv(client: TestClient):
|
||||
assert r.json()['items'] == [{**exoCsv.json(), 'is_author': False}]
|
||||
|
||||
|
||||
def test_get_pdf(client: TestClient):
|
||||
''' def test_get_pdf(client: TestClient):
|
||||
token = test_register(client)['access']
|
||||
exoCsv = client.post('/exercices', data={"name": "name", "consigne": "consigne", "private": False}, files={
|
||||
'file': ('test.py', open('tests/testing_exo_source/exo_source_csv_only.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
||||
@ -680,7 +687,7 @@ def test_get_pdf(client: TestClient):
|
||||
r = client.get('/exercices/public', params={"type": "pdf"})
|
||||
|
||||
assert r.json()['items'] == [{**exoPdf.json(), 'is_author': False}]
|
||||
|
||||
'''
|
||||
|
||||
def test_get_web(client: TestClient):
|
||||
token = test_register(client)['access']
|
||||
@ -693,7 +700,7 @@ def test_get_web(client: TestClient):
|
||||
|
||||
r = client.get('/exercices/public', params={"type": "web"})
|
||||
|
||||
assert r.json() == [{**exoWeb.json(), 'is_author': False}]
|
||||
assert r.json()['items'] == [{**exoWeb.json(), 'is_author': False}]
|
||||
|
||||
|
||||
def test_get_invalid_type(client: TestClient):
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -8,4 +8,4 @@ Fonction main() qui doit renvoyer un objet avec:
|
||||
|
||||
def main():
|
||||
t = random.randint(1, 10)
|
||||
return {"csv": None, 'web': "None","pdf": None, "calcul": "1+1=2"}
|
||||
return {"csv": None, 'web': "1 + [] = 2","pdf": None, "calcul": "1+1=2"}
|
||||
|
8
frontend/.idea/.gitignore
generated
vendored
Normal file
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",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "npx vitest run --reporter verbose",
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
@ -14,12 +15,17 @@
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^1.0.0",
|
||||
"@sveltejs/kit": "^1.0.0",
|
||||
"@testing-library/svelte": "^3.2.2",
|
||||
"@types/chroma-js": "^2.1.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"@vitest/browser": "^0.28.4",
|
||||
"@vitest/ui": "^0.28.4",
|
||||
"cypress": "^12.6.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"jsdom": "^21.1.0",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.8.1",
|
||||
"sass": "^1.53.0",
|
||||
@ -28,21 +34,27 @@
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.0.0"
|
||||
"vite": "^4.0.0",
|
||||
"vitest": "^0.28.4"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@sveltestack/svelte-query": "^1.6.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@types/qs": "^6.9.7",
|
||||
"axios": "^1.2.2",
|
||||
"chroma-js": "^2.4.2",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"qs": "^6.11.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"svelecte": "^3.13.0",
|
||||
"svelte-forms": "^2.3.1",
|
||||
"svelte-htm": "^1.2.0",
|
||||
"svelte-icons": "^2.1.0",
|
||||
"svelte-markdown": "^0.2.3",
|
||||
"svelte-multiselect": "^8.2.3",
|
||||
"svelte-navigator": "^3.2.2",
|
||||
"svelte-routing": "^1.6.0"
|
||||
"svelte-routing": "^1.6.0",
|
||||
"svelte-websocket-store": "^1.1.33"
|
||||
}
|
||||
}
|
2534
frontend/pnpm-lock.yaml
generated
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 { parse, stringify } from 'qs'
|
||||
import { autoRefresh } from '../utils/utils';
|
||||
|
||||
export const exoInstance = axios.create({
|
||||
paramsSerializer:{encode:(params)=> {return parse(params, {arrayFormat:"brackets"})}, serialize: (p)=>{return stringify(p, {arrayFormat: "repeat"})}},
|
||||
baseURL: `http://127.0.0.1:8002`,
|
||||
@ -9,7 +10,9 @@ export const exoInstance = axios.create({
|
||||
Accept: 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
|
||||
...(browser &&
|
||||
localStorage.getItem('token') && { Authorization: `Bearer ${localStorage.getItem('token')}` })
|
||||
}
|
||||
});
|
||||
|
||||
exoInstance.interceptors.request.use(autoRefresh, (error) => {
|
||||
Promise.reject(error);
|
||||
});
|
||||
|
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;
|
||||
margin: 0;
|
||||
}
|
||||
.spinner {
|
||||
width: 30px;
|
||||
height:30px;
|
||||
border: 3px solid $contrast;
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: rotation 1s infinite linear;
|
||||
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
height: calc(100vh - 100px); // 100% - nav
|
||||
}
|
||||
|
||||
|
||||
*{
|
||||
scrollbar-width: auto!important;
|
||||
scrollbar-color: $contrast transparent;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
@ -13,9 +51,12 @@
|
||||
transition: 0.3s;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 7px;
|
||||
padding: 0 10%;
|
||||
padding: 0 50px;
|
||||
width: max-content;
|
||||
cursor: pointer;
|
||||
&:disabled{
|
||||
cursor: not-allowed
|
||||
}
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
@ -61,7 +102,7 @@
|
||||
margin: 0;
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-bottom-color: red;
|
||||
border-bottom-color: $contrast;
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,4 +120,9 @@
|
||||
|
||||
.sv-dropdown{
|
||||
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';
|
||||
export let href = "/";
|
||||
export let exact = false;
|
||||
export let no_hover = false;
|
||||
</script>
|
||||
|
||||
<a href={href} class:selected={exact ? $page.url.pathname === href: $page.url.pathname.includes(href)}><slot/></a>
|
||||
<a href={href} class:no_hover class:selected={exact ? $page.url.pathname === href: $page.url.pathname.includes(href)}><slot/></a>
|
||||
|
||||
<style lang="scss">
|
||||
a {
|
||||
cursor: pointer;
|
||||
margin: 0 10px;
|
||||
color: yellow;
|
||||
color: $primary-dark;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
font-weight: 600;
|
||||
@ -22,7 +23,7 @@
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: currentColor;
|
||||
top: 100%;
|
||||
bottom: 5px;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
transform-origin: 100% 50%;
|
||||
@ -30,9 +31,9 @@
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
&:hover {
|
||||
color: red;
|
||||
color: $primary;
|
||||
transform: scale(1.05);
|
||||
&::before {
|
||||
&:not(.no_hover)::before {
|
||||
transform-origin: 0% 50%;
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
@ -40,7 +41,7 @@
|
||||
}
|
||||
.selected {
|
||||
font-weight: bolder;
|
||||
color: red;
|
||||
color: $primary;
|
||||
transform: scale(1.05);
|
||||
&::before {
|
||||
content: none;
|
||||
|
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 PrivacyIndicator from './PrivacyIndicator.svelte';
|
||||
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let exo: Exercice;
|
||||
|
||||
const { show } = getContext<{ show: Function }>('modal');
|
||||
const { navigate } = getContext<{ navigate: Function }>('navigation');
|
||||
const { isAuth } = getContext<{ isAuth: boolean }>('auth');
|
||||
const { isAuth } = getContext<{ isAuth: Writable<boolean> }>('auth');
|
||||
const exerciceStore = getContext('exos');
|
||||
const tagsStore = getContext('tags');
|
||||
|
||||
@ -19,10 +21,6 @@
|
||||
const handleClick = () => {
|
||||
opened = true;
|
||||
navigate(`/exercices/${exo.id_code}`);
|
||||
};
|
||||
|
||||
let tg = false;
|
||||
$: !!opened &&
|
||||
show(
|
||||
ModalCard,
|
||||
{
|
||||
@ -35,36 +33,36 @@
|
||||
opened = false;
|
||||
}
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="card"
|
||||
class:tagMode={tg}
|
||||
on:click={handleClick}
|
||||
on:dblclick={() => {}}
|
||||
on:keypress={() => {}}
|
||||
>
|
||||
<div class="card" on:click={handleClick} on:dblclick={() => {}} on:keypress={() => {}}>
|
||||
<h1>{exo.name}</h1>
|
||||
<div class="examples">
|
||||
<h2>Exemples</h2>
|
||||
{#if !!exo.consigne}<p>{exo.consigne}</p>{/if}
|
||||
{#each exo.examples.data.slice(0, 3) as ex}
|
||||
<p>{ex.calcul}</p>
|
||||
{/each}
|
||||
{#if exo.examples != null}
|
||||
<h2>Exemples</h2>
|
||||
{#if !!exo.consigne}<p data-testid="consigne">{exo.consigne}</p>{/if}
|
||||
{#each exo.examples.data.slice(0, 3) as ex}
|
||||
<p>{ex.calcul}</p>
|
||||
{/each}
|
||||
{:else}
|
||||
<p>Aucun exemple disponible</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !!isAuth}
|
||||
{#if exo.is_author && exo.original == null}
|
||||
<div class="status">
|
||||
<PrivacyIndicator color={exo.private == true ? 'red' : 'green'}>
|
||||
{exo.private == true ? 'Privé' : 'Public'}</PrivacyIndicator
|
||||
>
|
||||
</div>
|
||||
{:else if !exo.is_author}
|
||||
<div class="status">
|
||||
<PrivacyIndicator color={'blue'}>
|
||||
Par <strong>{exo.author.username}</strong>
|
||||
</PrivacyIndicator>
|
||||
{#if !!$isAuth && exo.is_author && exo.original == null }
|
||||
<div class="status">
|
||||
<PrivacyIndicator color={exo.private == true ? 'red' : 'green'}>
|
||||
{exo.private == true ? 'Privé' : 'Public'}</PrivacyIndicator
|
||||
>
|
||||
</div>
|
||||
{:else if !exo.is_author}
|
||||
<div class="status">
|
||||
<PrivacyIndicator color={'blue'}>
|
||||
Par <strong>{exo.author.username}</strong>
|
||||
</PrivacyIndicator>
|
||||
{#if !!$isAuth}
|
||||
<div
|
||||
data-testid="copy"
|
||||
class="icon"
|
||||
on:keydown={() => {}}
|
||||
on:click|stopPropagation={() => {
|
||||
@ -78,15 +76,15 @@
|
||||
>
|
||||
<MdContentCopy />
|
||||
</div>
|
||||
</div>
|
||||
{:else if exo.is_author && exo.original != null}
|
||||
<div class="status">
|
||||
<PrivacyIndicator color="blue">Par <strong>{exo.original?.author}</strong></PrivacyIndicator
|
||||
>
|
||||
</div>
|
||||
{/if}{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{:else if exo.is_author && exo.original != null}
|
||||
<div class="status">
|
||||
<PrivacyIndicator color="blue">Par <strong>{exo.original?.author}</strong></PrivacyIndicator>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="card-hover" />
|
||||
{#if !!isAuth}
|
||||
{#if !!$isAuth}
|
||||
<TagContainer bind:exo />
|
||||
{/if}
|
||||
<!-- TagContainer Must be directly after card-hover for the hover effect -->
|
||||
@ -188,7 +186,7 @@
|
||||
background-color: $background;
|
||||
min-height: 250px;
|
||||
max-height: 300px;
|
||||
&:not(.tagMode):hover {
|
||||
&:hover {
|
||||
transform: translateX(10px) translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
@ -17,9 +17,10 @@
|
||||
<EditForm editing={false} {cancel} {updateExo} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
@import '../../variables';
|
||||
div {
|
||||
background-color: blue;
|
||||
background: $background;
|
||||
padding: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -12,13 +12,16 @@
|
||||
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte';
|
||||
import ModalCard from './ModalCard.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import {generateRequest} from "../../requests/exo.request.js";
|
||||
export let exo: Exercice;
|
||||
export let edit: Function;
|
||||
export let delete_: Function;
|
||||
export let edit: Function=()=>{};
|
||||
export let delete_: Function=()=>{};
|
||||
|
||||
const { close, show } = getContext<{ close: Function; show: Function }>('modal');
|
||||
const { alert } = getContext<{ alert: Function }>('alert');
|
||||
const { isAuth } = getContext<{ isAuth: boolean }>('auth');
|
||||
const { isAuth } = getContext<{ isAuth: Writable<boolean> }>('auth');
|
||||
const { navigate } = getContext<{ navigate: Function }>('navigation');
|
||||
|
||||
let name = '';
|
||||
</script>
|
||||
@ -61,7 +64,7 @@
|
||||
</span>
|
||||
{/if}
|
||||
</h1>
|
||||
<InputWithLabel type="text" value={name} label="Nom" />
|
||||
<InputWithLabel type="text" bind:value={name} label="Nom" disabled={!exo.supports.csv}/>
|
||||
|
||||
<div class="examples">
|
||||
<h2>Exemples</h2>
|
||||
@ -72,11 +75,13 @@
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex-row-center wp-100">
|
||||
<button class="primary-btn">Télécharger</button>
|
||||
</div>
|
||||
<div class="tags" />
|
||||
<button class="primary-btn" disabled= {!exo.supports.csv} on:click={()=>{
|
||||
generateRequest(exo.id_code, name)
|
||||
|
||||
{#if !!isAuth}
|
||||
}}>Télécharger</button>
|
||||
</div>
|
||||
|
||||
{#if !!$isAuth}
|
||||
<TagContainer {exo} />
|
||||
{/if}
|
||||
<div class="icons">
|
||||
@ -87,7 +92,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if !!isAuth}
|
||||
{#if !!$isAuth}
|
||||
{#if exo.is_author}
|
||||
<div
|
||||
class="icon"
|
||||
@ -97,16 +102,19 @@
|
||||
title: 'Sur ?',
|
||||
description: 'Voulez vous supprimer ? ',
|
||||
validate: () => {
|
||||
close();
|
||||
delExo(exo.id_code).then((r)=>{
|
||||
close();
|
||||
delete_();
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
on:keypress={() => {}}
|
||||
data-testid="delete"
|
||||
>
|
||||
<MdDelete />
|
||||
</div>
|
||||
<div class="icon" style:color="green" on:click={() => edit()} on:keypress={() => {}}>
|
||||
<div class="icon" style:color="green" on:click={() => edit()} on:keypress={() => {}} data-testid="edit">
|
||||
<MdEdit />
|
||||
</div>
|
||||
{:else}
|
||||
@ -120,7 +128,8 @@
|
||||
ModalCard,
|
||||
{ exo: r },
|
||||
() => {
|
||||
goto('/exercices/user');
|
||||
//goto('/exercices/user');
|
||||
navigate(-2)
|
||||
},
|
||||
true
|
||||
);
|
||||
@ -128,6 +137,7 @@
|
||||
}}
|
||||
on:keypress={() => {}}
|
||||
title="Copier l'exercice pour pouvoir le modifier"
|
||||
data-testid="copy"
|
||||
>
|
||||
<MdContentCopy />
|
||||
</div>
|
||||
@ -173,7 +183,7 @@
|
||||
span.name {
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
|
||||
|
||||
}
|
||||
span:not(.name) {
|
||||
position: relative;
|
||||
|
@ -8,7 +8,7 @@
|
||||
import type { Exercice } from '../../types/exo.type';
|
||||
import { checkFile, errorMsg } from '../../utils/forms';
|
||||
import { compareObject } from '../../utils/utils';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let editing = true;
|
||||
export let updateExo: Function = (e: Exercice) => {};
|
||||
|
||||
@ -18,10 +18,10 @@
|
||||
const { alert } = getContext<{ alert: Function }>('alert');
|
||||
|
||||
// "Legally" initiate empty FileList for model field (simple list raises warning)
|
||||
let list = new DataTransfer();
|
||||
/* let list = new DataTransfer();
|
||||
let file = new File(['content'], !editing || exo == null ? 'filename.py' : exo.exo_source);
|
||||
list.items.add(file);
|
||||
!editing && list.items.remove(0);
|
||||
!editing && list.items.remove(0); */
|
||||
|
||||
// Initiate fields and form
|
||||
const name = field('name', !!exo ? exo.name : '', [required(), max(50), min(5)], {
|
||||
@ -29,7 +29,7 @@
|
||||
});
|
||||
const consigne = field('consigne', !!exo && exo.consigne != null ? exo.consigne : '', [max(200)], { checkOnInit: true });
|
||||
const prv = field('private', !!exo ? exo.private : false);
|
||||
const model = field('model', list.files, [checkFile(), required()], {
|
||||
const model = field('model', [], [checkFile(), required()], {
|
||||
checkOnInit: !editing
|
||||
});
|
||||
const myForm = form(name, consigne, prv, model);
|
||||
@ -70,6 +70,7 @@
|
||||
required
|
||||
label="Nom"
|
||||
errors={errorMsg($myForm, 'name')}
|
||||
name="name"
|
||||
/>
|
||||
|
||||
<InputWithLabel
|
||||
@ -78,25 +79,26 @@
|
||||
maxlength="200"
|
||||
label="Consigne"
|
||||
errors={errorMsg($myForm, 'consigne')}
|
||||
name="consigne"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<input type="checkbox" bind:checked={$prv.value} name="private" id="private" />
|
||||
<label for="private">Privé</label>
|
||||
</div>
|
||||
<FileInput bind:value={$model.value} accept=".py" id_code={exo?.id_code} />
|
||||
<FileInput bind:value={$model.value} accept=".py" id_code={exo?.id_code} defaultValue={editing &&exo!= null? exo.exo_source: null}/>
|
||||
|
||||
<div class="wp-100">
|
||||
<button class="primary-btn" disabled={!$myForm.valid}>Valider</button>
|
||||
<button class="primary-btn" disabled={!$myForm.valid}>Modifier</button>
|
||||
<button
|
||||
class="danger-btn"
|
||||
on:click|preventDefault={() => {
|
||||
|
||||
if (exo != null && ($model.dirty || !compareObject({...exo, consigne: exo.consigne == null ? "": exo.consigne}, myForm.summary()))) {
|
||||
alert({
|
||||
title: 'test',
|
||||
title: 'Voulez-vous annuler ?',
|
||||
description:
|
||||
'Aliquip in cupidatat anim tempor quis est sint qui sunt. Magna consequat excepteur deserunt ullamco quis.',
|
||||
'Vous avez des modifications non enregistrées, êtes vous sur de vouloir annuler ?',
|
||||
validate: cancel
|
||||
});
|
||||
} else {
|
||||
|
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 Head from './Head.svelte';
|
||||
import ModalCard from './ModalCard.svelte';
|
||||
import { Query, useQueryClient, type QueryOptions } from '@sveltestack/svelte-query';
|
||||
import { getExo, getExos, getTags } from '../../requests/exo.request';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import Pagination from './Pagination.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { writable } from 'svelte/store';
|
||||
import {type Writable, writable} from 'svelte/store';
|
||||
import { setContext } from 'svelte';
|
||||
import type { Page, Tag } from '../../types/exo.type';
|
||||
import type { Store } from '../../types/api.type';
|
||||
import { page as p } from '$app/stores';
|
||||
|
||||
const { show } = getContext<{ show: Function }>('modal');
|
||||
const { navigate } = getContext<{ navigate: Function }>('navigation');
|
||||
const { navigate, insertUrl } = getContext<{ navigate: Function; insertUrl: Function }>(
|
||||
'navigation'
|
||||
);
|
||||
const { isAuth } = getContext<{ isAuth: Writable<boolean> }>('auth');
|
||||
let filter = $isAuth ? 'user' : 'public';
|
||||
|
||||
let filter = 'user';
|
||||
|
||||
const exerciceStore = writable<Store<Page|undefined>>({
|
||||
const exerciceStore = writable<Store<Page | undefined>>({
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
isSuccess: false,
|
||||
data: undefined
|
||||
});
|
||||
const tagStore = writable<Store<Tag[]|undefined>>({
|
||||
const tagStore = writable<Store<Tag[] | undefined>>({
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
isSuccess: false,
|
||||
data: undefined
|
||||
data: []
|
||||
});
|
||||
|
||||
setContext('exos', exerciceStore);
|
||||
setContext('tags', tagStore);
|
||||
|
||||
|
||||
onMount(() => {
|
||||
let page = $page.url.searchParams.get('page')
|
||||
if(page == null){
|
||||
page = '1'
|
||||
}
|
||||
if ($page.params.slug != undefined && !['user', 'public'].includes($page.params.slug)) {
|
||||
getExo($page.params.slug).then((r) => {
|
||||
show(ModalCard, { exo: r, exos: exerciceStore, tags: tagStore }, () => navigate('/exercices/' + filter));
|
||||
insertUrl('exercices/' + filter);
|
||||
show(ModalCard, { exo: r, exos: exerciceStore, tags: tagStore }, () => navigate(-1));
|
||||
});
|
||||
} else if ($page.params.slug == undefined) {
|
||||
goto('/exercices/public');
|
||||
} else if ($page.params.slug == undefined || $page.params.slug == "user") {
|
||||
filter = $isAuth ? 'user' : 'public';
|
||||
goto(`/exercices/${filter}?${new URLSearchParams({page}).toString()}`)
|
||||
} else if($page.params.slug == "public"){
|
||||
filter = 'public';
|
||||
goto(`/exercices/${filter}?${new URLSearchParams({page}).toString()}`)
|
||||
}
|
||||
});
|
||||
|
||||
$: filter = ['user', 'public'].includes($page.params.slug) ? $page.params.slug : filter;
|
||||
|
||||
/*$: if(['user', 'public'].includes($page.params.slug) && !$isAuth){
|
||||
filter = "public"
|
||||
//goto('/exercices/' + filter)
|
||||
}*/
|
||||
const size = 15;
|
||||
$: activePage = parseInt($page.url.searchParams.get('page')!) || 1;
|
||||
let search = '';
|
||||
let selected: Tag[] = [];
|
||||
|
||||
$: {
|
||||
if(!$isAuth){
|
||||
filter = 'public'
|
||||
}
|
||||
exerciceStore.update((s) => {
|
||||
return { ...s, isFetching: true };
|
||||
});
|
||||
getExos(filter as 'public' | 'user', {
|
||||
page: activePage,
|
||||
page: activePage == 0 ? 1: activePage,
|
||||
search,
|
||||
size,
|
||||
tags: [...selected.map((t) => t.id_code)]
|
||||
}).then((r) => {
|
||||
exerciceStore.update((e) => {
|
||||
return { ...e, isSuccess: true, isFetching: false, data: r };
|
||||
});
|
||||
});
|
||||
})
|
||||
.then((r) => {
|
||||
console.log('R', r);
|
||||
if (activePage > r.totalPage && r.total != 0 && r.totalPage != 0) {
|
||||
activePage = r.totalPage;
|
||||
//$p.url.searchParams.set('page', String(activePage));
|
||||
goto(`?${new URLSearchParams({page: activePage}).toString()}`);
|
||||
return;
|
||||
}
|
||||
exerciceStore.update((e) => {
|
||||
return { ...e, isSuccess: true, isFetching: false, data: r };
|
||||
});
|
||||
})
|
||||
.catch(console.log);
|
||||
}
|
||||
|
||||
$: {
|
||||
tagStore.update((s)=>{return {...s, isFetching: true}});
|
||||
getTags().then(r=>{
|
||||
tagStore.update((e) => {
|
||||
return { ...e, isSuccess: true, isFetching: false, data: r };
|
||||
if($isAuth) {
|
||||
tagStore.update((s) => {
|
||||
return {...s, isFetching: true};
|
||||
});
|
||||
})
|
||||
getTags().then((r) => {
|
||||
tagStore.update((e) => {
|
||||
return {...e, isSuccess: true, isFetching: false, data: r};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let activePage = parseInt($page.url.searchParams.get('page')!) || 1;
|
||||
let search = '';
|
||||
let selected: Tag[] = [];
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{#if $tagStore.isSuccess == true && $tagStore.data != undefined}
|
||||
{#if $tagStore.data != undefined}
|
||||
<Head location={filter} bind:search bind:selected />
|
||||
{/if}
|
||||
{#if $tagStore.isFetching == true}
|
||||
Fetching
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="feed">
|
||||
<div class="title">
|
||||
<h1>
|
||||
Tous les <span>exercices</span>
|
||||
</h1>
|
||||
<p>
|
||||
Vous retrouverez ici tous les exercices que vous avez créé ou copié depuis les exercices
|
||||
publics
|
||||
</p>
|
||||
{#if filter == 'user'}
|
||||
<h1>
|
||||
Vos <span>exercices</span>
|
||||
</h1>
|
||||
<p>
|
||||
Vous retrouverez ici tous les exercices que vous avez créé ou copié depuis les exercices
|
||||
publics
|
||||
</p>
|
||||
{:else}
|
||||
<h1>
|
||||
Tous les <span>exercices</span>
|
||||
</h1>
|
||||
<p>Vous retrouverez ici tous les exercices créés par les autres utilisateurs</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $exerciceStore.data != undefined}
|
||||
{#each $exerciceStore.data.items.filter((e) => e != null && selected.every((t) => e.tags
|
||||
@ -100,11 +144,63 @@
|
||||
.includes(t.id_code))) as e}
|
||||
<Card bind:exo={e} />
|
||||
{/each}
|
||||
<Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} />
|
||||
{:else}
|
||||
{#each Array(10) as i}
|
||||
<div class="skeleton"><span /></div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{#if $exerciceStore.data != undefined}
|
||||
<Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} />
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../variables';
|
||||
|
||||
.skeleton {
|
||||
width: 330px;
|
||||
height: 250px;
|
||||
opacity: .8;
|
||||
span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: $skeleton;
|
||||
display: block;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
background-color: $skeleton;
|
||||
overflow: hidden;
|
||||
&::after {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
position: absolute;
|
||||
animation: waves 1.6s linear 0.5s infinite;
|
||||
transform: translateX(-100%);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
(lighten($color: $skeleton, $amount: 3), rgba(0, 0, 0, 0.04)),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@keyframes waves {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
60% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.feed {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
@ -129,7 +225,7 @@
|
||||
font-size: 1.1em;
|
||||
}
|
||||
span {
|
||||
color: red;
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -7,20 +7,31 @@
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { Store } from '../../types/api.type';
|
||||
|
||||
const { navigate } = getContext<{navigate: Function}>('navigation');
|
||||
const { show, close } = getContext<{show: Function, close: Function}>('modal');
|
||||
const { navigate } = getContext<{ navigate: Function }>('navigation');
|
||||
const { show, close } = getContext<{ show: Function; close: Function }>('modal');
|
||||
|
||||
export let location = 'public';
|
||||
export let search = '';
|
||||
export let selected: Tag[] = [];
|
||||
const { isAuth } = getContext<{ isAuth: boolean }>('auth');
|
||||
const { isAuth } = getContext<{ isAuth: Writable<boolean> }>('auth');
|
||||
|
||||
const tags: Writable<Store<Tag[]>> = getContext('tags')
|
||||
const tags: Writable<Store<Tag[]>> = getContext('tags');
|
||||
const exerciceStore: Writable<Store<Page>> = getContext('exos');
|
||||
let updateText = null;
|
||||
let tmp =""
|
||||
$: {
|
||||
if(updateText != null){
|
||||
clearTimeout(updateText);
|
||||
}
|
||||
updateText = window.setTimeout(() => {
|
||||
search = tmp;
|
||||
}, 500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="head">
|
||||
<div class="new">
|
||||
{#if !!isAuth}
|
||||
{#if !!$isAuth}
|
||||
<button
|
||||
class="border-primary-btn"
|
||||
on:click={() => {
|
||||
@ -35,15 +46,22 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="search">
|
||||
<input type="text" placeholder="Rechercher" class="input" bind:value={search} />
|
||||
{#if !!isAuth}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher"
|
||||
class="input"
|
||||
bind:value={tmp}
|
||||
|
||||
/>
|
||||
{#if !!$isAuth}
|
||||
<TagSelector options={$tags.data} bind:selected />
|
||||
<select
|
||||
name="ee"
|
||||
id="e"
|
||||
name="tagsSelect"
|
||||
id="tagsSelect"
|
||||
class="input"
|
||||
bind:value={location}
|
||||
on:change={(e) => navigate(`/exercices/${e.currentTarget.value}`)}
|
||||
on:change={(e) =>
|
||||
navigate(`/exercices/${e.currentTarget.value}?${new URLSearchParams({ page: 1 })}`)}
|
||||
>
|
||||
<option value="user">Vos exos</option>
|
||||
<option value="public">Tous les exos</option>
|
||||
|
@ -1,35 +1,137 @@
|
||||
<script lang="ts">
|
||||
import {page as p} from '$app/stores'
|
||||
import { goto } from "$app/navigation";
|
||||
import { page as p } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
export let page: number;
|
||||
export let total: number;
|
||||
|
||||
const changePage = (p: number) => {
|
||||
goto(`?${new URLSearchParams({page: p}).toString()}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="pagination">
|
||||
{#each Array(total) as _, i}
|
||||
<p
|
||||
class:active={page == i + 1}
|
||||
on:click={() => {
|
||||
page = i + 1;
|
||||
$p.url.searchParams.set('page', String(i+1))
|
||||
goto(`?${$p.url.searchParams.toString()}`);
|
||||
}}
|
||||
on:keydown = {()=>{}}
|
||||
>
|
||||
{i + 1}
|
||||
</p>
|
||||
{/each}
|
||||
<button
|
||||
on:click={() => {
|
||||
changePage(page - 1);
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
disabled={page <= 1}
|
||||
>
|
||||
{'<'}
|
||||
</button>
|
||||
|
||||
{#if total >= 7}
|
||||
<!-- First two -->
|
||||
{#each Array.from({ length: 2 }, (v, k) => k + 1) as i}
|
||||
<button
|
||||
class:active={page == i}
|
||||
on:click={() => {
|
||||
changePage(i);
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
{i}
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
<!-- Middle : active with a padding of 1 -->
|
||||
{#if page >= 2 && page <= total - 1}
|
||||
{#if page - 1 > 3}
|
||||
<p>...</p>
|
||||
{/if}
|
||||
{#each Array.from({ length: 3 }, (v, k) => page - 1 + k) as i}
|
||||
{#if i > 2 && i <= total - 2}
|
||||
<button
|
||||
class:active={page == i}
|
||||
on:click={() => {
|
||||
changePage(i);
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
{i}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if page + 1 < total - 2}
|
||||
<p>...</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p>...</p>
|
||||
{/if}
|
||||
|
||||
<!-- Last two -->
|
||||
{#each Array.from({ length: 2 }, (v, k) => total - 2 + k + 1) as i}
|
||||
<button
|
||||
class:active={page == i}
|
||||
on:click={() => {
|
||||
changePage(i);
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
{i}
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
{:else}
|
||||
{#each Array.from({ length: total }, (v, k) => k + 1) as i}
|
||||
<button
|
||||
class:active={page == i}
|
||||
on:click={() => {
|
||||
changePage(i);
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
{i}
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
<button
|
||||
on:click={() => {
|
||||
changePage(page+1)
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
disabled={page >= total}
|
||||
>
|
||||
{'>'}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.active {
|
||||
color: red;
|
||||
}
|
||||
@import '../../variables';
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
margin: 30px;
|
||||
p {
|
||||
height: max-content;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
button {
|
||||
margin: 10px;
|
||||
border: 1px solid $border;
|
||||
border-radius: 4px;
|
||||
padding: 7px 9px;
|
||||
cursor: pointer;
|
||||
background-color: rgba($background, 0.3);
|
||||
color: #f8f8f8;
|
||||
transition: .3s;
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
&:not(:disabled):hover{
|
||||
color: $primary;
|
||||
border-color: $primary;
|
||||
}
|
||||
}
|
||||
p{
|
||||
font-size: 1.2em;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
}
|
||||
.active {
|
||||
color: #080808 !important;
|
||||
background-color: $primary !important;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,38 +1,37 @@
|
||||
<script lang="ts">
|
||||
import chroma from 'chroma-js';
|
||||
|
||||
export let label: string;
|
||||
export let color: string;
|
||||
export let remove: Function;
|
||||
console.log(color)
|
||||
export let remove: Function | null = null;
|
||||
let removed = false;
|
||||
|
||||
</script>
|
||||
|
||||
<div class:removed class="selected" style={`--item-color:${chroma(color).rgb().join(',')};`}>
|
||||
<div class:removed class="selected" style={`--item-color:${chroma(color).rgb().join(',')};`} class:removable={!!remove} {...$$restProps}>
|
||||
<div class="label">{label}</div>
|
||||
<div
|
||||
class="unselect"
|
||||
on:click={() => {
|
||||
removed = true;
|
||||
remove()
|
||||
/* setTimeout(() => {
|
||||
if(!remove()){
|
||||
removed=false
|
||||
}
|
||||
}, 300); */
|
||||
}}
|
||||
on:keypress={() => {}}
|
||||
>
|
||||
<svg
|
||||
height="14"
|
||||
width="14"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
class="css-8mmkcg"
|
||||
><path
|
||||
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
|
||||
/></svg
|
||||
{#if !!remove}
|
||||
<button
|
||||
class="unselect"
|
||||
on:click={() => {
|
||||
removed = true;
|
||||
remove && remove();
|
||||
}}
|
||||
on:keypress={() => {}}
|
||||
>
|
||||
</div>
|
||||
<svg
|
||||
height="14"
|
||||
width="14"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
><path
|
||||
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@ -65,7 +64,7 @@
|
||||
min-width: 0px;
|
||||
box-sizing: border-box;
|
||||
transition: 0.5s;
|
||||
max-width: 100px;
|
||||
//max-width: 100px;
|
||||
}
|
||||
|
||||
.removed {
|
||||
@ -82,4 +81,8 @@
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:not(.removable) > .label{
|
||||
padding: 4px 6px;
|
||||
}
|
||||
</style>
|
||||
|
@ -13,11 +13,12 @@
|
||||
let tagMode = false;
|
||||
let selected: { label: string; id_code: string; color: string, created?: boolean }[] = [];
|
||||
export let tags: Writable<Store<TagType[]>> = getContext('tags');
|
||||
console.log('TAGS +', tags, getContext('test'))
|
||||
const {alert, info, success, error} = getContext<{alert: Function, info: Function, success: Function, error: Function}>("notif")
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="tags-container"
|
||||
data-testid="tags"
|
||||
class:tg
|
||||
class:tagMode
|
||||
on:click|stopPropagation={() => {}}
|
||||
@ -31,6 +32,9 @@
|
||||
remove={() => {
|
||||
delTags(exo.id_code, t.id_code).then((r) => {
|
||||
exo.tags = r.tags;
|
||||
success('Tag', `Tag *${t.label}* supprimé à *${exo.name}* avec succès`)
|
||||
}).catch((r)=>{
|
||||
error('Tag', `Erreur lors de la suppression du tag *${t.label}* à *${exo.name}*`)
|
||||
});
|
||||
return true;
|
||||
}}
|
||||
@ -42,7 +46,6 @@
|
||||
class="expand"
|
||||
on:click={() => {
|
||||
tg = true;
|
||||
console.log('TAGGGG', $tags)
|
||||
setTimeout(() => {
|
||||
tagMode = true;
|
||||
}, 200);
|
||||
@ -71,6 +74,9 @@
|
||||
}
|
||||
tg = false;
|
||||
tagMode = false;
|
||||
success('Tags', `Tags ajouté(s) avec succès à *${exo.name}*`)
|
||||
}).catch((r)=>{
|
||||
error('Tags', `Erreur lors de l'ajout de tags à *${exo.name}*`)
|
||||
});
|
||||
}}>Valider !</button
|
||||
>
|
||||
@ -161,5 +167,8 @@
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
:global(> button){
|
||||
width: 99%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
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';
|
||||
export let label = 'Choisir un fichier';
|
||||
export let value: FileList;
|
||||
export let defaultValue: string|null;
|
||||
export let id_code: string | null = null;
|
||||
|
||||
const id = String(Math.random());
|
||||
@ -12,8 +13,8 @@
|
||||
<input type="file" {id} {...$$restProps} bind:files={value} />
|
||||
<label for={id}>{label}</label>
|
||||
<div class="filename">
|
||||
{#if value.length !== 0}
|
||||
<p>{value[0].name}</p>
|
||||
{#if value.length !== 0 || defaultValue != null}
|
||||
<p>{value.length !== 0 && value[0] != undefined? value[0].name : defaultValue!=null?defaultValue:"..."}</p>
|
||||
{#if id_code != null}
|
||||
<div
|
||||
class="icon"
|
||||
|
@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import IoMdEye from 'svelte-icons/io/IoMdEye.svelte';
|
||||
import IoMdEyeOff from 'svelte-icons/io/IoMdEyeOff.svelte';
|
||||
|
||||
export let type = 'text';
|
||||
export let value = '';
|
||||
export let value: string | null = null;
|
||||
export let label = '';
|
||||
export let errors: string[] = [];
|
||||
export let change: Function = (e: Event) => {};
|
||||
function typeAction(node: HTMLInputElement) {
|
||||
node.type = type;
|
||||
}
|
||||
@ -17,10 +19,24 @@
|
||||
element.type = show === true ? 'password' : 'text';
|
||||
show = !show;
|
||||
};
|
||||
let test: HTMLInputElement;
|
||||
export const focus = () => {
|
||||
console.log('FOCUS', test)
|
||||
test.focus();
|
||||
};
|
||||
</script>
|
||||
|
||||
<span class="inputLabel" class:error={errors.length !== 0}>
|
||||
<input use:typeAction {id} bind:value {...$$restProps} placeholder="" />
|
||||
|
||||
<input
|
||||
use:typeAction
|
||||
on:input={(e)=>{change(e)}}
|
||||
{id}
|
||||
bind:value
|
||||
{...$$restProps}
|
||||
placeholder=""
|
||||
bind:this={test}
|
||||
/>
|
||||
<!-- placeholder = "" pour que le label se place bien avec :placeholder-shown -->
|
||||
<label for={id}>{label}</label>
|
||||
{#if type == 'password'}
|
||||
@ -58,7 +74,7 @@
|
||||
}
|
||||
.inputLabel {
|
||||
position: relative;
|
||||
|
||||
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -8,8 +8,8 @@
|
||||
export let isSelected = false;
|
||||
export let isDisabled = false;
|
||||
//export let isMultiple = false;
|
||||
|
||||
const color = chroma(item.color);
|
||||
|
||||
const color = chroma("rgb(255,0,0)");
|
||||
console.log(color.rgb(), color);
|
||||
</script>
|
||||
|
||||
@ -51,6 +51,7 @@
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../variables';
|
||||
.unselect {
|
||||
color: rgb(var(--item-color));
|
||||
-moz-box-align: center;
|
||||
@ -125,6 +126,9 @@
|
||||
cursor: not-allowed;
|
||||
color: grey;
|
||||
background-color: var(--sv-bg);
|
||||
|
||||
}
|
||||
|
||||
:global(.creatable-row-wrap){
|
||||
background-color: red;
|
||||
}
|
||||
</style>
|
||||
|
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-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>
|
||||
|
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">
|
||||
import { onMount, setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { loginRequest, refreshRequest, registerRequest } from '../requests/auth.request';
|
||||
import jwt_decode from 'jwt-decode';
|
||||
const user = writable(null);
|
||||
const isAuth = writable(false);
|
||||
const login = (login: string, password: string) => {
|
||||
loginRequest({ login, password }).then((r) => {
|
||||
localStorage.setItem('token', `${r.access_token}`);
|
||||
localStorage.setItem('refresh', `${r.refresh_token}`);
|
||||
isAuth.set(true);
|
||||
});
|
||||
};
|
||||
const register = (username: string, password: string, confirm: string) => {
|
||||
registerRequest({ username, password, password_confirm: confirm }).then((r) => {
|
||||
localStorage.setItem('token', `${r.access_token}`);
|
||||
localStorage.setItem('refresh', `${r.refresh_token}`);
|
||||
isAuth.set(true);
|
||||
});
|
||||
};
|
||||
const logout = () => {};
|
||||
setContext('auth', { user, isAuth, login, register, logout });
|
||||
import {getContext, onMount, setContext} from 'svelte';
|
||||
import {writable} from 'svelte/store';
|
||||
import {
|
||||
loginRequest,
|
||||
refreshRequest,
|
||||
registerRequest,
|
||||
logoutRequest
|
||||
} from '../requests/auth.request';
|
||||
import jwt_decode from 'jwt-decode';
|
||||
import {checkExpire} from '../utils/utils';
|
||||
import {goto} from '$app/navigation';
|
||||
import {page} from "$app/stores";
|
||||
|
||||
onMount(() => {
|
||||
if (localStorage.getItem('token') != null) {
|
||||
const { exp } = jwt_decode(localStorage.getItem('token')!);
|
||||
console.log(Date.now(), exp, Date.now() >= exp * 1000)
|
||||
if (Date.now() >= exp * 1000) {
|
||||
refreshRequest(localStorage.getItem('refresh')!).then(r=>{localStorage.setItem('token', r.access_token)})
|
||||
isAuth.set(true)
|
||||
return
|
||||
}
|
||||
isAuth.set(true)
|
||||
}
|
||||
});
|
||||
const username = writable<string | null>(null);
|
||||
const isAuth = writable(false);
|
||||
const initialLoading = writable(true);
|
||||
|
||||
const getTokens = () => {
|
||||
return {access: localStorage.getItem('token'), refresh: localStorage.getItem('refresh')};
|
||||
};
|
||||
|
||||
const {
|
||||
alert,
|
||||
info,
|
||||
success,
|
||||
error
|
||||
} = getContext<{ alert: Function, info: Function, success: Function, error: Function }>('notif');
|
||||
|
||||
const login = (username: string, password: string) => {
|
||||
return loginRequest({username, password})
|
||||
.then((r) => {
|
||||
localStorage.setItem('token', `${r.access_token}`);
|
||||
localStorage.setItem('refresh', `${r.refresh_token}`);
|
||||
const {username: name} = jwt_decode<{ username: string }>(r.access_token);
|
||||
$username = name;
|
||||
$isAuth = true;
|
||||
success('Connexion', `Connecté en tant que **${username}** !`);
|
||||
goto('/dashboard');
|
||||
})
|
||||
.catch((r) => {
|
||||
error('Connexion', 'Erreur lors de la connexion !');
|
||||
throw r.response;
|
||||
});
|
||||
};
|
||||
|
||||
const register = (username: string, password: string, confirm: string) => {
|
||||
return registerRequest({username, password, password_confirm: confirm})
|
||||
.then((r) => {
|
||||
localStorage.setItem('token', `${r.access_token}`);
|
||||
localStorage.setItem('refresh', `${r.refresh_token}`);
|
||||
const {username: name} = jwt_decode<{ username: string }>(r.access_token);
|
||||
$isAuth = true;
|
||||
$username = name;
|
||||
success('Inscription', `Connecté en tant que **${username}** !`);
|
||||
goto('/dashboard');
|
||||
})
|
||||
.catch((r) => {
|
||||
error('Deconnexion', "Erreur lors de l'inscription !");
|
||||
throw r.response;
|
||||
});
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
const {access, refresh} = getTokens();
|
||||
if (isAuth && access != null) {
|
||||
logoutRequest()
|
||||
.then(() => {
|
||||
$isAuth = false;
|
||||
$username = null;
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refresh');
|
||||
localStorage.removeItem('reconnect');
|
||||
|
||||
success('Déconnexion', 'Déconnecté !');
|
||||
|
||||
if($page.url.href.indexOf('room')> -1){
|
||||
goto('/room')
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
error('Déconnexion', 'Erreur lors de la déconnexion !');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setContext('auth', {username, isAuth, login, register, logout, initialLoading});
|
||||
|
||||
onMount(() => {
|
||||
const {access, refresh} = getTokens();
|
||||
if (access != null) {
|
||||
const {exp, username: name} = jwt_decode<{ username: string, exp: number }>(access);
|
||||
|
||||
if (checkExpire(exp) && refresh != null) {
|
||||
refreshRequest(refresh).then((r) => {
|
||||
localStorage.setItem('token', r.access_token);
|
||||
$username = username;
|
||||
$isAuth = true;
|
||||
});
|
||||
|
||||
$initialLoading = false;
|
||||
return;
|
||||
}
|
||||
$isAuth = true;
|
||||
$username = name;
|
||||
}
|
||||
$initialLoading = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<p>{$isAuth ? 'Connecté' : 'Non connecté'}</p>
|
||||
<slot />
|
||||
<p>{$isAuth ? $username : 'Non connecté'}</p>
|
||||
{#if !$initialLoading}
|
||||
<slot/>
|
||||
{/if}
|
||||
|
@ -27,9 +27,6 @@
|
||||
onClose = newOnClose;
|
||||
}
|
||||
|
||||
function addContext (key: string, value: any) {
|
||||
setContext(key, value)
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false;
|
||||
@ -39,9 +36,10 @@
|
||||
component = undefined;
|
||||
props = {};
|
||||
closed = true;
|
||||
|
||||
}, 500);
|
||||
}
|
||||
setContext('modal', { show, close, addContext });
|
||||
setContext('modal', { show, close });
|
||||
function keyPress(e: KeyboardEvent) {
|
||||
console.log('HOP');
|
||||
if (e.key == 'Escape' && visible == true) {
|
||||
|
@ -5,8 +5,15 @@
|
||||
import { page } from '$app/stores';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
let previous: string | null = base;
|
||||
let previous: string[] = [base];
|
||||
let first = true;
|
||||
const insertUrl = (url: string, id: number = -1)=>{
|
||||
if(id >= 0){
|
||||
previous.splice(id, 0, `${base}/${url}`)}
|
||||
else if(id == -1){
|
||||
previous.push(`${base}/${url}`)
|
||||
}
|
||||
}
|
||||
const navigate = (
|
||||
url: string | number,
|
||||
params: object | undefined,
|
||||
@ -23,20 +30,27 @@
|
||||
if (browser) {
|
||||
console.log('PREVIOUS', previous, typeof url == 'number', previous);
|
||||
if (typeof url == 'number' && previous != null) {
|
||||
goto(previous);
|
||||
const id = Math.abs(url);
|
||||
if (previous.length > id) {
|
||||
goto(previous.reverse()[id-1]);
|
||||
} else{
|
||||
goto(previous.reverse()[0])
|
||||
}
|
||||
} else {
|
||||
const parsedParams = new URLSearchParams(params as Record<string, string>);
|
||||
goto(`${url}?${parsedParams.toString()}`, { ...options });
|
||||
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
};
|
||||
afterNavigate(({ from }) => {
|
||||
previous = from?.url.toString() || previous;
|
||||
if (from ) {
|
||||
previous.push(from?.url.toString());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
setContext('navigation', { navigate });
|
||||
setContext('navigation', { navigate, insertUrl });
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
|
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 {authInstance} from '../apis/auth.api';
|
||||
|
||||
export const loginRequest = (data: { login: string; password: string }) => {
|
||||
return axios({
|
||||
url: 'http://localhost:8002/login',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
.then((r) => r.data as {access_token: string, refresh_token: string, token_type: string })
|
||||
.catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
export const loginRequest = (data: { username: string; password: string }) => {
|
||||
return authInstance
|
||||
.request({
|
||||
url: 'http://localhost:8002/login',
|
||||
method: 'POST',
|
||||
data,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
.then((r) => r.data as { access_token: string; refresh_token: string; token_type: string })
|
||||
.catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
export const registerRequest = (data: { username: string; password: string, password_confirm: string }) => {
|
||||
return axios({
|
||||
url: 'http://localhost:8002/register',
|
||||
method: 'POST',
|
||||
data,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
.then((r) => r.data as { access_token: string; refresh_token: string; token_type: string })
|
||||
.catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
export const registerRequest = (data: {
|
||||
username: string;
|
||||
password: string;
|
||||
password_confirm: string;
|
||||
}) => {
|
||||
return authInstance
|
||||
.request({
|
||||
url: 'http://localhost:8002/register',
|
||||
method: 'POST',
|
||||
data,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
.then((r) => r.data as { access_token: string; refresh_token: string; token_type: string })
|
||||
.catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
|
||||
export const refreshRequest = (token: string) => {
|
||||
return axios({
|
||||
url: 'http://localhost:8002/refresh',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
return authInstance
|
||||
.request({
|
||||
url: 'http://localhost:8002/refresh',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
"Authorization": `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then((r) => r.data as {access_token:string})
|
||||
.catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then((r) => r.data as { access_token: string })
|
||||
.catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
|
||||
export const logoutRequest = () => {
|
||||
return authInstance
|
||||
.request({
|
||||
url: '/logout',
|
||||
method: 'POST'
|
||||
})
|
||||
.then((r) => r.data as { access_token: string })
|
||||
.catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
|
||||
export const dashBoardRequest = () => {
|
||||
return authInstance
|
||||
.request({
|
||||
url: '/user',
|
||||
method: 'GET'
|
||||
})
|
||||
.then((r) => r.data)
|
||||
.catch(console.log);
|
||||
}
|
||||
|
||||
export const updateUserRequest = (data: { username: string, email: string | null, firstname: string | null, name: string | null }) => {
|
||||
return authInstance
|
||||
.request({
|
||||
url: '/user',
|
||||
method: 'PUT',
|
||||
data,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
.then((r) => r.data)
|
||||
.catch((r)=>{throw r});
|
||||
}
|
||||
|
||||
export const updatePassword = (data: {
|
||||
old_password: string, password: string,
|
||||
password_confirm: string
|
||||
}) => {
|
||||
return authInstance
|
||||
.request({
|
||||
url: '/user/password',
|
||||
method: 'PUT',
|
||||
data,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
.then((r) => r.data)
|
||||
.catch((r)=>{throw r});
|
||||
}
|
@ -131,4 +131,29 @@ export const getExoSource = (id_code: string,) => {
|
||||
link.click();
|
||||
link.remove();
|
||||
});
|
||||
};export const generateRequest = (id_code: string,filename: string) => {
|
||||
return exoInstance({
|
||||
url: `/generator/csv/${id_code}/`,
|
||||
method: 'Get',
|
||||
params: {filename}
|
||||
}).then((r) => {
|
||||
const contentDisposition = r.headers['content-disposition'] || "filename=untitled.csv";
|
||||
const splitted = contentDisposition.split('filename=')
|
||||
let filename = "untitled.csv"
|
||||
if(splitted.length >= 1) {
|
||||
filename = splitted[1]
|
||||
}
|
||||
const blob = new Blob([r.data], {
|
||||
type: 'text/csv'
|
||||
});
|
||||
const downloadUrl = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl;
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
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>
|
||||
import Modal from '../context/Modal.svelte';
|
||||
import NavLink from '../components/NavLink.svelte';
|
||||
import '../app.scss';
|
||||
import Alert from '../context/Alert.svelte';
|
||||
import Auth from '../context/Auth.svelte';
|
||||
import { QueryClient, QueryClientProvider } from '@sveltestack/svelte-query';
|
||||
import { Router } from 'svelte-navigator';
|
||||
import Navigation from '../context/Navigation.svelte';
|
||||
const queryClient = new QueryClient();
|
||||
<script lang="ts">
|
||||
import Modal from '../context/Modal.svelte';
|
||||
import NavLink from '../components/NavLink.svelte';
|
||||
import '../app.scss';
|
||||
import Alert from '../context/Alert.svelte';
|
||||
import Auth from '../context/Auth.svelte';
|
||||
import {QueryClient, QueryClientProvider} from '@sveltestack/svelte-query';
|
||||
import {Router} from 'svelte-navigator';
|
||||
import Navigation from '../context/Navigation.svelte';
|
||||
import Notification from '../context/Notification.svelte';
|
||||
import {getContext} from "svelte";
|
||||
import type {Writable} from "svelte/store";
|
||||
import NavBar from "../components/NavBar.svelte";
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<Navigation>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Auth>
|
||||
<Alert>
|
||||
<Modal>
|
||||
<main>
|
||||
<nav data-sveltekit-preload-data="hover">
|
||||
<NavLink href="/" exact>Home</NavLink>
|
||||
<NavLink href="/exercices" exact>Exercices</NavLink>
|
||||
<NavLink href="/settings" exact>Settings</NavLink>
|
||||
</nav>
|
||||
<slot />
|
||||
</main>
|
||||
</Modal>
|
||||
</Alert>
|
||||
</Auth>
|
||||
</QueryClientProvider>
|
||||
</Navigation>
|
||||
|
||||
<Notification>
|
||||
<Navigation>
|
||||
<Auth>
|
||||
<Alert>
|
||||
<Modal>
|
||||
<main>
|
||||
<NavBar/>
|
||||
<slot/>
|
||||
</main>
|
||||
</Modal>
|
||||
</Alert>
|
||||
</Auth>
|
||||
</Navigation>
|
||||
</Notification>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
@import '../variables';
|
||||
.links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
height: 30px;
|
||||
& li {
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
@import '../variables';
|
||||
@import "../mixins";
|
||||
|
||||
:root {
|
||||
--container-padding: 20px;
|
||||
--container-width: 1330px;
|
||||
}
|
||||
:global(body) {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
|
||||
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
overflow: hidden;
|
||||
background-color: $background;
|
||||
color: #d9d9d9;
|
||||
background: linear-gradient(to bottom left, $background-dark 30%, $background-light);
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
main {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding-left: calc(50% - var(--container-width) / 2);
|
||||
padding-right: calc(50% - var(--container-width) / 2);
|
||||
height: calc(100vh - var(--navbar-height) - 10px);
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
a {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
.links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
height: 30px;
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 30px 0;
|
||||
border-bottom: 1px solid $border;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
height: 30px;
|
||||
}
|
||||
& li {
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--container-padding: 20px;
|
||||
--container-width: 1330px;
|
||||
}
|
||||
|
||||
:global(body) {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
|
||||
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
overflow: hidden;
|
||||
background-color: $background;
|
||||
color: #d9d9d9;
|
||||
background: linear-gradient(to bottom left, $background-dark 30%, $background-light);
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding-left: calc(50% - var(--container-width) / 2);
|
||||
padding-right: calc(50% - var(--container-width) / 2);
|
||||
height: calc(100vh - var(--navbar-height) - 10px);
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
|
||||
a {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 30px 0;
|
||||
border-bottom: 1px solid $border;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
height: 30px;
|
||||
}
|
||||
</style>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user