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

9
.idea/fastapi_gen.iml Normal file
View File

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

8
.idea/modules.xml Normal file
View File

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

6
.idea/vcs.xml Normal file
View File

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

84
.idea/workspace.xml Normal file
View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

BIN
backend/api/database2.db Normal file

Binary file not shown.

BIN
backend/api/database3.db Normal file

Binary file not shown.

BIN
backend/api/database4.db Normal file

Binary file not shown.

BIN
backend/api/database5.db Normal file

Binary file not shown.

BIN
backend/api/database6.db Normal file

Binary file not shown.

BIN
backend/api/database7.db Normal file

Binary file not shown.

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
import type { Exercice } from '../../types/exo.type'; import type { Exercice } from '../../types/exo.type';
import { checkFile, errorMsg } from '../../utils/forms'; import { checkFile, errorMsg } from '../../utils/forms';
import { compareObject } from '../../utils/utils'; import { compareObject } from '../../utils/utils';
import { goto } from '$app/navigation';
export let editing = true; export let editing = true;
export let updateExo: Function = (e: Exercice) => {}; export let updateExo: Function = (e: Exercice) => {};
@ -18,10 +18,10 @@
const { alert } = getContext<{ alert: Function }>('alert'); const { alert } = getContext<{ alert: Function }>('alert');
// "Legally" initiate empty FileList for model field (simple list raises warning) // "Legally" initiate empty FileList for model field (simple list raises warning)
let list = new DataTransfer(); /* let list = new DataTransfer();
let file = new File(['content'], !editing || exo == null ? 'filename.py' : exo.exo_source); let file = new File(['content'], !editing || exo == null ? 'filename.py' : exo.exo_source);
list.items.add(file); list.items.add(file);
!editing && list.items.remove(0); !editing && list.items.remove(0); */
// Initiate fields and form // Initiate fields and form
const name = field('name', !!exo ? exo.name : '', [required(), max(50), min(5)], { const name = field('name', !!exo ? exo.name : '', [required(), max(50), min(5)], {
@ -29,7 +29,7 @@
}); });
const consigne = field('consigne', !!exo && exo.consigne != null ? exo.consigne : '', [max(200)], { checkOnInit: true }); const consigne = field('consigne', !!exo && exo.consigne != null ? exo.consigne : '', [max(200)], { checkOnInit: true });
const prv = field('private', !!exo ? exo.private : false); const prv = field('private', !!exo ? exo.private : false);
const model = field('model', list.files, [checkFile(), required()], { const model = field('model', [], [checkFile(), required()], {
checkOnInit: !editing checkOnInit: !editing
}); });
const myForm = form(name, consigne, prv, model); const myForm = form(name, consigne, prv, model);
@ -70,6 +70,7 @@
required required
label="Nom" label="Nom"
errors={errorMsg($myForm, 'name')} errors={errorMsg($myForm, 'name')}
name="name"
/> />
<InputWithLabel <InputWithLabel
@ -78,25 +79,26 @@
maxlength="200" maxlength="200"
label="Consigne" label="Consigne"
errors={errorMsg($myForm, 'consigne')} errors={errorMsg($myForm, 'consigne')}
name="consigne"
/> />
<div> <div>
<input type="checkbox" bind:checked={$prv.value} name="private" id="private" /> <input type="checkbox" bind:checked={$prv.value} name="private" id="private" />
<label for="private">Privé</label> <label for="private">Privé</label>
</div> </div>
<FileInput bind:value={$model.value} accept=".py" id_code={exo?.id_code} /> <FileInput bind:value={$model.value} accept=".py" id_code={exo?.id_code} defaultValue={editing &&exo!= null? exo.exo_source: null}/>
<div class="wp-100"> <div class="wp-100">
<button class="primary-btn" disabled={!$myForm.valid}>Valider</button> <button class="primary-btn" disabled={!$myForm.valid}>Modifier</button>
<button <button
class="danger-btn" class="danger-btn"
on:click|preventDefault={() => { on:click|preventDefault={() => {
if (exo != null && ($model.dirty || !compareObject({...exo, consigne: exo.consigne == null ? "": exo.consigne}, myForm.summary()))) { if (exo != null && ($model.dirty || !compareObject({...exo, consigne: exo.consigne == null ? "": exo.consigne}, myForm.summary()))) {
alert({ alert({
title: 'test', title: 'Voulez-vous annuler ?',
description: description:
'Aliquip in cupidatat anim tempor quis est sint qui sunt. Magna consequat excepteur deserunt ullamco quis.', 'Vous avez des modifications non enregistrées, êtes vous sur de vouloir annuler ?',
validate: cancel validate: cancel
}); });
} else { } else {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,38 +1,37 @@
<script lang="ts"> <script lang="ts">
import chroma from 'chroma-js'; import chroma from 'chroma-js';
export let label: string; export let label: string;
export let color: string; export let color: string;
export let remove: Function; console.log(color)
export let remove: Function | null = null;
let removed = false; let removed = false;
</script> </script>
<div class:removed class="selected" style={`--item-color:${chroma(color).rgb().join(',')};`}> <div class:removed class="selected" style={`--item-color:${chroma(color).rgb().join(',')};`} class:removable={!!remove} {...$$restProps}>
<div class="label">{label}</div> <div class="label">{label}</div>
<div {#if !!remove}
class="unselect" <button
on:click={() => { class="unselect"
removed = true; on:click={() => {
remove() removed = true;
/* setTimeout(() => { remove && remove();
if(!remove()){ }}
removed=false on:keypress={() => {}}
}
}, 300); */
}}
on:keypress={() => {}}
>
<svg
height="14"
width="14"
viewBox="0 0 20 20"
aria-hidden="true"
focusable="false"
class="css-8mmkcg"
><path
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
/></svg
> >
</div> <svg
height="14"
width="14"
viewBox="0 0 20 20"
aria-hidden="true"
focusable="false"
><path
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
/></svg
>
</button>
{/if}
</div> </div>
<style lang="scss"> <style lang="scss">
@ -65,7 +64,7 @@
min-width: 0px; min-width: 0px;
box-sizing: border-box; box-sizing: border-box;
transition: 0.5s; transition: 0.5s;
max-width: 100px; //max-width: 100px;
} }
.removed { .removed {
@ -82,4 +81,8 @@
white-space: nowrap; white-space: nowrap;
box-sizing: border-box; box-sizing: border-box;
} }
:not(.removable) > .label{
padding: 4px 6px;
}
</style> </style>

View File

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

View File

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

View File

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

View File

@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import IoMdEye from 'svelte-icons/io/IoMdEye.svelte'; import IoMdEye from 'svelte-icons/io/IoMdEye.svelte';
import IoMdEyeOff from 'svelte-icons/io/IoMdEyeOff.svelte'; import IoMdEyeOff from 'svelte-icons/io/IoMdEyeOff.svelte';
export let type = 'text'; export let type = 'text';
export let value = ''; export let value: string | null = null;
export let label = ''; export let label = '';
export let errors: string[] = []; export let errors: string[] = [];
export let change: Function = (e: Event) => {};
function typeAction(node: HTMLInputElement) { function typeAction(node: HTMLInputElement) {
node.type = type; node.type = type;
} }
@ -17,10 +19,24 @@
element.type = show === true ? 'password' : 'text'; element.type = show === true ? 'password' : 'text';
show = !show; show = !show;
}; };
let test: HTMLInputElement;
export const focus = () => {
console.log('FOCUS', test)
test.focus();
};
</script> </script>
<span class="inputLabel" class:error={errors.length !== 0}> <span class="inputLabel" class:error={errors.length !== 0}>
<input use:typeAction {id} bind:value {...$$restProps} placeholder="" />
<input
use:typeAction
on:input={(e)=>{change(e)}}
{id}
bind:value
{...$$restProps}
placeholder=""
bind:this={test}
/>
<!-- placeholder = "" pour que le label se place bien avec :placeholder-shown --> <!-- placeholder = "" pour que le label se place bien avec :placeholder-shown -->
<label for={id}>{label}</label> <label for={id}>{label}</label>
{#if type == 'password'} {#if type == 'password'}
@ -58,7 +74,7 @@
} }
.inputLabel { .inputLabel {
position: relative; position: relative;
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@ -131,4 +131,29 @@ export const getExoSource = (id_code: string,) => {
link.click(); link.click();
link.remove(); link.remove();
}); });
};export const generateRequest = (id_code: string,filename: string) => {
return exoInstance({
url: `/generator/csv/${id_code}/`,
method: 'Get',
params: {filename}
}).then((r) => {
const contentDisposition = r.headers['content-disposition'] || "filename=untitled.csv";
const splitted = contentDisposition.split('filename=')
let filename = "untitled.csv"
if(splitted.length >= 1) {
filename = splitted[1]
}
const blob = new Blob([r.data], {
type: 'text/csv'
});
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
link.remove();
});
}; };

View File

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

View File

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

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