diff --git a/assets/controllers/challenge_executor_controller.ts b/assets/controllers/challenge_executor_controller.ts index c43d3ca..cd0fb9c 100644 --- a/assets/controllers/challenge_executor_controller.ts +++ b/assets/controllers/challenge_executor_controller.ts @@ -53,7 +53,7 @@ export default class extends Controller { const query = editorView.state.doc.toString(); console.debug("Executing query", { query }); - await component.action("execute", { + await component.action("createNewQuery", { query, }); diff --git a/composer.lock b/composer.lock index 3abe807..ac844e7 100644 --- a/composer.lock +++ b/composer.lock @@ -2305,12 +2305,12 @@ "source": { "type": "git", "url": "https://github.com/openai-php/client.git", - "reference": "cab6bbe852954ce8db319a982d7004bd31a46d04" + "reference": "4a565d145e0fb3ea1baba8fffe39d86c56b6dc2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/openai-php/client/zipball/cab6bbe852954ce8db319a982d7004bd31a46d04", - "reference": "cab6bbe852954ce8db319a982d7004bd31a46d04", + "url": "https://api.github.com/repos/openai-php/client/zipball/4a565d145e0fb3ea1baba8fffe39d86c56b6dc2c", + "reference": "4a565d145e0fb3ea1baba8fffe39d86c56b6dc2c", "shasum": "" }, "require": { @@ -2373,7 +2373,7 @@ ], "support": { "issues": "https://github.com/openai-php/client/issues", - "source": "https://github.com/openai-php/client/tree/main" + "source": "https://github.com/openai-php/client/tree/v0.10.3" }, "funding": [ { @@ -2389,7 +2389,7 @@ "type": "github" } ], - "time": "2024-10-27T12:45:10+00:00" + "time": "2024-11-12T20:51:16+00:00" }, { "name": "oro/doctrine-extensions", @@ -4066,12 +4066,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "653ab90f3b2c7fcd3a37b29c23552ab7680f54d2" + "reference": "0ef6f8a99eef66277245014c06a5b6f8c1c29392" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/653ab90f3b2c7fcd3a37b29c23552ab7680f54d2", - "reference": "653ab90f3b2c7fcd3a37b29c23552ab7680f54d2", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/0ef6f8a99eef66277245014c06a5b6f8c1c29392", + "reference": "0ef6f8a99eef66277245014c06a5b6f8c1c29392", "shasum": "" }, "require": { @@ -4120,7 +4120,7 @@ "symfony/security-core": "^6.4|^7.0", "symfony/stopwatch": "^6.4|^7.0", "symfony/translation": "^6.4|^7.0", - "symfony/type-info": "^7.1", + "symfony/type-info": "^7.2", "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0" @@ -4167,7 +4167,7 @@ "type": "tidelift" } ], - "time": "2024-10-23T06:56:12+00:00" + "time": "2024-11-13T14:27:59+00:00" }, { "name": "symfony/doctrine-messenger", @@ -4849,12 +4849,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "7a46f7de39c92cdc29fd86beeb6f16ad1a3e5dd8" + "reference": "e1fe0394f1db2b4a2306ed9dab43a3cfe601a0ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/7a46f7de39c92cdc29fd86beeb6f16ad1a3e5dd8", - "reference": "7a46f7de39c92cdc29fd86beeb6f16ad1a3e5dd8", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/e1fe0394f1db2b4a2306ed9dab43a3cfe601a0ba", + "reference": "e1fe0394f1db2b4a2306ed9dab43a3cfe601a0ba", "shasum": "" }, "require": { @@ -4940,7 +4940,7 @@ "symfony/string": "^6.4|^7.0", "symfony/translation": "^6.4|^7.0", "symfony/twig-bundle": "^6.4|^7.0", - "symfony/type-info": "^7.1", + "symfony/type-info": "^7.2", "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", "symfony/web-link": "^6.4|^7.0", @@ -4991,7 +4991,7 @@ "type": "tidelift" } ], - "time": "2024-11-05T09:05:33+00:00" + "time": "2024-11-13T17:08:38+00:00" }, { "name": "symfony/http-client", @@ -4999,19 +4999,19 @@ "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "730f1bb15938598419e92432e2156fc960dcf782" + "reference": "456c64c884cabb95ba46eaa8f31a93f04c393f46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/730f1bb15938598419e92432e2156fc960dcf782", - "reference": "730f1bb15938598419e92432e2156fc960dcf782", + "url": "https://api.github.com/repos/symfony/http-client/zipball/456c64c884cabb95ba46eaa8f31a93f04c393f46", + "reference": "456c64c884cabb95ba46eaa8f31a93f04c393f46", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3.4.1", + "symfony/http-client-contracts": "~3.4.3|^3.5.1", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -5086,7 +5086,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T08:44:59+00:00" + "time": "2024-11-13T21:20:02+00:00" }, { "name": "symfony/http-client-contracts", @@ -5094,12 +5094,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "075fadd18649068440dae4667a0ab98293535235" + "reference": "e34b200cdbcfe17b1047838bd68e74f045a797eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/075fadd18649068440dae4667a0ab98293535235", - "reference": "075fadd18649068440dae4667a0ab98293535235", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/e34b200cdbcfe17b1047838bd68e74f045a797eb", + "reference": "e34b200cdbcfe17b1047838bd68e74f045a797eb", "shasum": "" }, "require": { @@ -5165,7 +5165,7 @@ "type": "tidelift" } ], - "time": "2024-09-26T08:57:56+00:00" + "time": "2024-11-13T18:58:46+00:00" }, { "name": "symfony/http-foundation", @@ -5173,12 +5173,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "b77b5a8295ea945ae6f4f91adc5204a2405cc579" + "reference": "e88a66c3997859532bc2ddd6dd8f35aba2711744" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b77b5a8295ea945ae6f4f91adc5204a2405cc579", - "reference": "b77b5a8295ea945ae6f4f91adc5204a2405cc579", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e88a66c3997859532bc2ddd6dd8f35aba2711744", + "reference": "e88a66c3997859532bc2ddd6dd8f35aba2711744", "shasum": "" }, "require": { @@ -5243,7 +5243,7 @@ "type": "tidelift" } ], - "time": "2024-11-09T09:29:03+00:00" + "time": "2024-11-13T18:58:46+00:00" }, { "name": "symfony/http-kernel", @@ -5251,12 +5251,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "cd23537252813e8db3b22d0149e908a0ef473fcb" + "reference": "873703cd7998a920f0047354b5ba94982a7f307f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/cd23537252813e8db3b22d0149e908a0ef473fcb", - "reference": "cd23537252813e8db3b22d0149e908a0ef473fcb", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/873703cd7998a920f0047354b5ba94982a7f307f", + "reference": "873703cd7998a920f0047354b5ba94982a7f307f", "shasum": "" }, "require": { @@ -5357,7 +5357,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T10:04:42+00:00" + "time": "2024-11-13T15:19:04+00:00" }, { "name": "symfony/intl", @@ -6933,18 +6933,18 @@ "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "cad7bca32405b5c57c6331e7760df34fa0be23ea" + "reference": "c33697d80ef5efa8aa8e6a43e36a8a22fec44d3f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/cad7bca32405b5c57c6331e7760df34fa0be23ea", - "reference": "cad7bca32405b5c57c6331e7760df34fa0be23ea", + "url": "https://api.github.com/repos/symfony/property-info/zipball/c33697d80ef5efa8aa8e6a43e36a8a22fec44d3f", + "reference": "c33697d80ef5efa8aa8e6a43e36a8a22fec44d3f", "shasum": "" }, "require": { "php": ">=8.2", "symfony/string": "^6.4|^7.0", - "symfony/type-info": "^7.1" + "symfony/type-info": "^7.2" }, "conflict": { "phpdocumentor/reflection-docblock": "<5.2", @@ -7009,7 +7009,7 @@ "type": "tidelift" } ], - "time": "2024-11-09T09:29:03+00:00" + "time": "2024-11-13T14:30:29+00:00" }, { "name": "symfony/routing", @@ -7017,12 +7017,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "0782e32f411cf1c4a1ab3f5c074a9b61f3df8e5a" + "reference": "40f0d287bd9bcf61dbbc3d43d84f973c0d26c4cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/0782e32f411cf1c4a1ab3f5c074a9b61f3df8e5a", - "reference": "0782e32f411cf1c4a1ab3f5c074a9b61f3df8e5a", + "url": "https://api.github.com/repos/symfony/routing/zipball/40f0d287bd9bcf61dbbc3d43d84f973c0d26c4cf", + "reference": "40f0d287bd9bcf61dbbc3d43d84f973c0d26c4cf", "shasum": "" }, "require": { @@ -7090,7 +7090,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T08:41:34+00:00" + "time": "2024-11-13T16:15:23+00:00" }, { "name": "symfony/runtime", @@ -7440,12 +7440,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "dd89ea67fcf72c4728aa265d3b471e09e5b170f9" + "reference": "0d0ab4d491f22306c893b2d30ce73ea911201a61" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/dd89ea67fcf72c4728aa265d3b471e09e5b170f9", - "reference": "dd89ea67fcf72c4728aa265d3b471e09e5b170f9", + "url": "https://api.github.com/repos/symfony/security-http/zipball/0d0ab4d491f22306c893b2d30ce73ea911201a61", + "reference": "0d0ab4d491f22306c893b2d30ce73ea911201a61", "shasum": "" }, "require": { @@ -7520,7 +7520,7 @@ "type": "tidelift" } ], - "time": "2024-11-05T15:35:02+00:00" + "time": "2024-11-13T13:40:36+00:00" }, { "name": "symfony/serializer", @@ -7528,12 +7528,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "fa8b444a5ad9872d042e5acbf6828a6560ea985e" + "reference": "2127f4a5b547f90b117da612e85caa8ba79c79b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/fa8b444a5ad9872d042e5acbf6828a6560ea985e", - "reference": "fa8b444a5ad9872d042e5acbf6828a6560ea985e", + "url": "https://api.github.com/repos/symfony/serializer/zipball/2127f4a5b547f90b117da612e85caa8ba79c79b2", + "reference": "2127f4a5b547f90b117da612e85caa8ba79c79b2", "shasum": "" }, "require": { @@ -7547,7 +7547,7 @@ "symfony/dependency-injection": "<6.4", "symfony/property-access": "<6.4", "symfony/property-info": "<6.4", - "symfony/type-info": "<7.1.5", + "symfony/type-info": "<7.2", "symfony/uid": "<6.4", "symfony/validator": "<6.4", "symfony/yaml": "<6.4" @@ -7570,7 +7570,7 @@ "symfony/property-access": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/type-info": "^7.1.5", + "symfony/type-info": "^7.2", "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0", @@ -7619,7 +7619,7 @@ "type": "tidelift" } ], - "time": "2024-11-09T09:29:03+00:00" + "time": "2024-11-13T14:27:59+00:00" }, { "name": "symfony/service-contracts", @@ -7843,12 +7843,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "205580699b4d3e11f7b679faf2c0f57ffca6981c" + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/205580699b4d3e11f7b679faf2c0f57ffca6981c", - "reference": "205580699b4d3e11f7b679faf2c0f57ffca6981c", + "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", "shasum": "" }, "require": { @@ -7922,7 +7922,7 @@ "type": "tidelift" } ], - "time": "2024-10-23T06:56:12+00:00" + "time": "2024-11-13T13:31:26+00:00" }, { "name": "symfony/translation", @@ -8104,12 +8104,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "e34839ab413e1dc32bd2cb9f95186af1bacaee7e" + "reference": "4fc9a3d8b5a5b3639f392214d6c3d42ba7482085" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/e34839ab413e1dc32bd2cb9f95186af1bacaee7e", - "reference": "e34839ab413e1dc32bd2cb9f95186af1bacaee7e", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/4fc9a3d8b5a5b3639f392214d6c3d42ba7482085", + "reference": "4fc9a3d8b5a5b3639f392214d6c3d42ba7482085", "shasum": "" }, "require": { @@ -8206,7 +8206,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T08:15:21+00:00" + "time": "2024-11-13T13:31:26+00:00" }, { "name": "symfony/twig-bundle", @@ -8298,12 +8298,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "2bdf5f7c0a60da69c3444faf206c21fa4e028921" + "reference": "cb5105c42db7299d1e65c4cfd19dedf1ba86bcdf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/2bdf5f7c0a60da69c3444faf206c21fa4e028921", - "reference": "2bdf5f7c0a60da69c3444faf206c21fa4e028921", + "url": "https://api.github.com/repos/symfony/type-info/zipball/cb5105c42db7299d1e65c4cfd19dedf1ba86bcdf", + "reference": "cb5105c42db7299d1e65c4cfd19dedf1ba86bcdf", "shasum": "" }, "require": { @@ -8313,12 +8313,14 @@ "conflict": { "phpstan/phpdoc-parser": "<1.0", "symfony/dependency-injection": "<6.4", - "symfony/property-info": "<6.4" + "symfony/property-info": ">=7.1,<7.1.9", + "symfony/serializer": ">=7.1,<7.1.9", + "symfony/validator": ">=7.1,<7.1.9" }, "require-dev": { "phpstan/phpdoc-parser": "^1.0|^2.0", "symfony/dependency-injection": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0" + "symfony/property-info": "^7.2" }, "type": "library", "autoload": { @@ -8372,7 +8374,7 @@ "type": "tidelift" } ], - "time": "2024-11-08T21:15:15+00:00" + "time": "2024-11-14T14:28:25+00:00" }, { "name": "symfony/uid", @@ -8535,12 +8537,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/ux-live-component.git", - "reference": "eb6c228adeee3386a78212cfd69a45c184885431" + "reference": "0ddcd67b1fa1d795506c7302cb0347133cf8b3b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-live-component/zipball/eb6c228adeee3386a78212cfd69a45c184885431", - "reference": "eb6c228adeee3386a78212cfd69a45c184885431", + "url": "https://api.github.com/repos/symfony/ux-live-component/zipball/0ddcd67b1fa1d795506c7302cb0347133cf8b3b7", + "reference": "0ddcd67b1fa1d795506c7302cb0347133cf8b3b7", "shasum": "" }, "require": { @@ -8622,7 +8624,7 @@ "type": "tidelift" } ], - "time": "2024-11-10T15:15:51+00:00" + "time": "2024-11-14T17:38:27+00:00" }, { "name": "symfony/ux-turbo", @@ -8729,12 +8731,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/ux-twig-component.git", - "reference": "e2da79a8f1960c04841a8186bbbec5dc24c1621f" + "reference": "c473b98f85237417df5ea1500797cd95ba3330c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/e2da79a8f1960c04841a8186bbbec5dc24c1621f", - "reference": "e2da79a8f1960c04841a8186bbbec5dc24c1621f", + "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/c473b98f85237417df5ea1500797cd95ba3330c6", + "reference": "c473b98f85237417df5ea1500797cd95ba3330c6", "shasum": "" }, "require": { @@ -8805,7 +8807,7 @@ "type": "tidelift" } ], - "time": "2024-11-10T15:15:51+00:00" + "time": "2024-11-14T17:38:27+00:00" }, { "name": "symfony/validator", @@ -8813,12 +8815,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "456fe160df3240d3fe54242d3dbf00bf36a35954" + "reference": "b978128c07a6a9916b31a143b25752eec4c9db58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/456fe160df3240d3fe54242d3dbf00bf36a35954", - "reference": "456fe160df3240d3fe54242d3dbf00bf36a35954", + "url": "https://api.github.com/repos/symfony/validator/zipball/b978128c07a6a9916b31a143b25752eec4c9db58", + "reference": "b978128c07a6a9916b31a143b25752eec4c9db58", "shasum": "" }, "require": { @@ -8856,7 +8858,7 @@ "symfony/property-access": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", "symfony/translation": "^6.4.3|^7.0.3", - "symfony/type-info": "^7.1", + "symfony/type-info": "^7.2", "symfony/yaml": "^6.4|^7.0" }, "type": "library", @@ -8902,7 +8904,7 @@ "type": "tidelift" } ], - "time": "2024-11-09T06:49:38+00:00" + "time": "2024-11-14T08:39:12+00:00" }, { "name": "symfony/var-dumper", @@ -8910,12 +8912,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "73b22e287d3248af064785992dad0ec40e5904c0" + "reference": "c6a22929407dec8765d6e2b6ff85b800b245879c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/73b22e287d3248af064785992dad0ec40e5904c0", - "reference": "73b22e287d3248af064785992dad0ec40e5904c0", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c6a22929407dec8765d6e2b6ff85b800b245879c", + "reference": "c6a22929407dec8765d6e2b6ff85b800b245879c", "shasum": "" }, "require": { @@ -9187,7 +9189,7 @@ ], "support": { "issues": "https://github.com/SymfonyCasts/sass-bundle/issues", - "source": "https://github.com/SymfonyCasts/sass-bundle/tree/main" + "source": "https://github.com/SymfonyCasts/sass-bundle/tree/v0.8.2" }, "time": "2024-10-22T16:58:17+00:00" }, @@ -9197,12 +9199,12 @@ "source": { "type": "git", "url": "https://github.com/twbs/bootstrap.git", - "reference": "cbbb567b637e24882fd0fad62bca49e248afb9a4" + "reference": "cacbdc680ecdfee5f0c7fbb876ad15188eaf697d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twbs/bootstrap/zipball/cbbb567b637e24882fd0fad62bca49e248afb9a4", - "reference": "cbbb567b637e24882fd0fad62bca49e248afb9a4", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/cacbdc680ecdfee5f0c7fbb876ad15188eaf697d", + "reference": "cacbdc680ecdfee5f0c7fbb876ad15188eaf697d", "shasum": "" }, "replace": { @@ -9240,7 +9242,7 @@ "issues": "https://github.com/twbs/bootstrap/issues", "source": "https://github.com/twbs/bootstrap/tree/main" }, - "time": "2024-11-06T05:58:08+00:00" + "time": "2024-11-14T10:12:33+00:00" }, { "name": "twig/extra-bundle", @@ -9668,12 +9670,12 @@ "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "c98c49833f5ca8e4b37660439660e3278e7ad162" + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/c98c49833f5ca8e4b37660439660e3278e7ad162", - "reference": "c98c49833f5ca8e4b37660439660e3278e7ad162", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", "shasum": "" }, "require": { @@ -9683,8 +9685,8 @@ "phpstan/phpstan": "<1.11.10" }, "require-dev": { - "phpstan/phpstan": "^1.11.10", - "phpstan/phpstan-strict-rules": "^1.1", + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", "phpunit/phpunit": "^8 || ^9" }, "default-branch": true, @@ -9724,7 +9726,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/main" + "source": "https://github.com/composer/pcre/tree/3.3.2" }, "funding": [ { @@ -9740,7 +9742,7 @@ "type": "tidelift" } ], - "time": "2024-11-12T15:24:51+00:00" + "time": "2024-11-12T16:29:46+00:00" }, { "name": "composer/xdebug-handler", @@ -9922,12 +9924,12 @@ "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "454d184c156914ba0d1a930b23c458309374eee2" + "reference": "2686bde4ba0a126841dad0db7df29eb29e9daba4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/454d184c156914ba0d1a930b23c458309374eee2", - "reference": "454d184c156914ba0d1a930b23c458309374eee2", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2686bde4ba0a126841dad0db7df29eb29e9daba4", + "reference": "2686bde4ba0a126841dad0db7df29eb29e9daba4", "shasum": "" }, "require": { @@ -10018,7 +10020,7 @@ "type": "github" } ], - "time": "2024-11-12T15:33:22+00:00" + "time": "2024-11-13T18:09:44+00:00" }, { "name": "masterminds/html5", @@ -10380,12 +10382,12 @@ "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "5d69413e0b6d23af3e57d2714c062b99dfd1752d" + "reference": "1772fd353f21c3409dcebcfde60134dc7783d7ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/5d69413e0b6d23af3e57d2714c062b99dfd1752d", - "reference": "5d69413e0b6d23af3e57d2714c062b99dfd1752d", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1772fd353f21c3409dcebcfde60134dc7783d7ff", + "reference": "1772fd353f21c3409dcebcfde60134dc7783d7ff", "shasum": "" }, "require": { @@ -10431,7 +10433,7 @@ "type": "github" } ], - "time": "2024-11-12T13:47:29+00:00" + "time": "2024-11-14T14:24:47+00:00" }, { "name": "phpstan/phpstan-doctrine", @@ -12772,12 +12774,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "3e838f9095f53f2b98287a361c1cdb68bbd3aa7b" + "reference": "b176e1f1f550ef44c94eb971bf92488de08f7c6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/3e838f9095f53f2b98287a361c1cdb68bbd3aa7b", - "reference": "3e838f9095f53f2b98287a361c1cdb68bbd3aa7b", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/b176e1f1f550ef44c94eb971bf92488de08f7c6b", + "reference": "b176e1f1f550ef44c94eb971bf92488de08f7c6b", "shasum": "" }, "require": { @@ -12831,7 +12833,7 @@ "type": "tidelift" } ], - "time": "2024-10-25T15:15:23+00:00" + "time": "2024-11-13T16:15:23+00:00" }, { "name": "symfony/maker-bundle", @@ -12931,12 +12933,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "0228dd1180ee3ef6c85eeb51ddbe89e0eaef7d33" + "reference": "2bbde92ab25a0e2c88160857af7be9db5da0d145" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/0228dd1180ee3ef6c85eeb51ddbe89e0eaef7d33", - "reference": "0228dd1180ee3ef6c85eeb51ddbe89e0eaef7d33", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/2bbde92ab25a0e2c88160857af7be9db5da0d145", + "reference": "2bbde92ab25a0e2c88160857af7be9db5da0d145", "shasum": "" }, "require": { @@ -13005,7 +13007,7 @@ "type": "tidelift" } ], - "time": "2024-10-27T06:46:44+00:00" + "time": "2024-11-13T16:15:23+00:00" }, { "name": "symfony/web-profiler-bundle", diff --git a/devenv.lock b/devenv.lock index 8fa6b32..28df912 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,10 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1731407985, + "lastModified": 1731501663, "owner": "cachix", "repo": "devenv", - "rev": "9d3bf4d4c3fef89d2b6b29ebd64047a29ee932ea", + "rev": "b48b0d8018e0dc8c14d9dca47cd6e55add94a603", "type": "github" }, "original": { @@ -53,10 +53,10 @@ }, "nixpkgs": { "locked": { - "lastModified": 1731245184, + "lastModified": 1731531548, "owner": "nixos", "repo": "nixpkgs", - "rev": "aebe249544837ce42588aa4b2e7972222ba12e8f", + "rev": "24f0d4acd634792badd6470134c387a3b039dace", "type": "github" }, "original": { @@ -68,10 +68,10 @@ }, "nixpkgs-stable": { "locked": { - "lastModified": 1731239293, + "lastModified": 1731386116, "owner": "NixOS", "repo": "nixpkgs", - "rev": "9256f7c71a195ebe7a218043d9f93390d49e6884", + "rev": "689fed12a013f56d4c4d3f612489634267d86529", "type": "github" }, "original": { diff --git a/frankenphp/docker-entrypoint.sh b/frankenphp/docker-entrypoint.sh index a190310..7d56f4e 100644 --- a/frankenphp/docker-entrypoint.sh +++ b/frankenphp/docker-entrypoint.sh @@ -37,6 +37,7 @@ if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then php bin/console cache:pool:clear cache.dbrunner || true echo "Updating Meilisearch indexes..." + php bin/console meili:clear || true php bin/console meili:import --update-settings || true setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var diff --git a/importmap.php b/importmap.php index af1e335..6d38a42 100644 --- a/importmap.php +++ b/importmap.php @@ -65,7 +65,7 @@ 'version' => '6.5.7', ], '@codemirror/autocomplete' => [ - 'version' => '6.18.2', + 'version' => '6.18.3', ], '@codemirror/lint' => [ 'version' => '6.8.2', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfbc1f1..954236b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,9 +49,9 @@ importers: version: 8.14.0(eslint@9.14.0)(typescript@5.6.3) packages: - "@codemirror/autocomplete@6.18.2": + "@codemirror/autocomplete@6.18.3": resolution: { - integrity: sha512-wJGylKtMFR/Ds6Gh01+OovXE/pncPiKZNNBKuC39pKnH+XK5d9+WsNqcrdxPjFPFTigRBqse0rfxw9UxrfyhPg==, + integrity: sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==, } peerDependencies: "@codemirror/language": ^6.0.0 @@ -194,9 +194,9 @@ packages: } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - "@eslint/plugin-kit@0.2.2": + "@eslint/plugin-kit@0.2.3": resolution: { - integrity: sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==, + integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } @@ -1000,7 +1000,7 @@ packages: engines: { node: ">=10" } snapshots: - "@codemirror/autocomplete@6.18.2(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.2)(@lezer/common@1.2.3)": + "@codemirror/autocomplete@6.18.3(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.2)(@lezer/common@1.2.3)": dependencies: "@codemirror/language": 6.10.3 "@codemirror/state": 6.4.1 @@ -1016,7 +1016,7 @@ snapshots: "@codemirror/lang-sql@6.8.0(@codemirror/view@6.34.2)": dependencies: - "@codemirror/autocomplete": 6.18.2(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.2)(@lezer/common@1.2.3) + "@codemirror/autocomplete": 6.18.3(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.2)(@lezer/common@1.2.3) "@codemirror/language": 6.10.3 "@codemirror/state": 6.4.1 "@lezer/common": 1.2.3 @@ -1113,7 +1113,7 @@ snapshots: "@eslint/object-schema@2.1.4": {} - "@eslint/plugin-kit@0.2.2": + "@eslint/plugin-kit@0.2.3": dependencies: levn: 0.4.1 @@ -1313,7 +1313,7 @@ snapshots: codemirror@6.0.1(@lezer/common@1.2.3): dependencies: - "@codemirror/autocomplete": 6.18.2(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.2)(@lezer/common@1.2.3) + "@codemirror/autocomplete": 6.18.3(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.2)(@lezer/common@1.2.3) "@codemirror/commands": 6.7.1 "@codemirror/language": 6.10.3 "@codemirror/lint": 6.8.2 @@ -1377,7 +1377,7 @@ snapshots: "@eslint/core": 0.7.0 "@eslint/eslintrc": 3.1.0 "@eslint/js": 9.14.0 - "@eslint/plugin-kit": 0.2.2 + "@eslint/plugin-kit": 0.2.3 "@humanfs/node": 0.16.6 "@humanwhocodes/module-importer": 1.0.1 "@humanwhocodes/retry": 0.4.1 diff --git a/src/Entity/ChallengeDto/CompareResult/ColumnDifferent.php b/src/Entity/ChallengeDto/CompareResult/ColumnDifferent.php new file mode 100644 index 0000000..031544d --- /dev/null +++ b/src/Entity/ChallengeDto/CompareResult/ColumnDifferent.php @@ -0,0 +1,25 @@ + $this->row, + ]); + } +} diff --git a/src/Entity/ChallengeDto/CompareResult/RowUnmatched.php b/src/Entity/ChallengeDto/CompareResult/RowUnmatched.php new file mode 100644 index 0000000..1fa0b19 --- /dev/null +++ b/src/Entity/ChallengeDto/CompareResult/RowUnmatched.php @@ -0,0 +1,35 @@ + $this->expected, + '%actual%' => $this->actual, + ]); + } +} diff --git a/src/Entity/ChallengeDto/CompareResult/Same.php b/src/Entity/ChallengeDto/CompareResult/Same.php new file mode 100644 index 0000000..2632e3c --- /dev/null +++ b/src/Entity/ChallengeDto/CompareResult/Same.php @@ -0,0 +1,25 @@ +result; + } + + public function setResult(?QueryResultDto $result): self + { + $this->result = $result; + + return $this; + } + + public function getErrorMessage(): ?TranslatableMessage + { + return $this->errorMessage; + } + + public function setErrorMessage(?TranslatableMessage $errorMessage): self + { + $this->errorMessage = $errorMessage; + + return $this; + } +} diff --git a/src/Entity/ChallengeDto/QueryResultDto.php b/src/Entity/ChallengeDto/QueryResultDto.php new file mode 100644 index 0000000..69e3787 --- /dev/null +++ b/src/Entity/ChallengeDto/QueryResultDto.php @@ -0,0 +1,36 @@ +> the result of the user's query + */ + private array $result; + + /** + * @return array> the result of the user's query + */ + public function getResult(): array + { + return $this->result; + } + + /** + * @param array> $result the result of the user's query + */ + public function setResult(array $result): self + { + $this->result = $result; + + return $this; + } +} diff --git a/src/Entity/Feedback.php b/src/Entity/Feedback.php index 22a9901..aac04d1 100644 --- a/src/Entity/Feedback.php +++ b/src/Entity/Feedback.php @@ -34,7 +34,7 @@ class Feedback private string $description; #[ORM\Column(length: 255, enumType: FeedbackType::class)] - private FeedbackType $type; + private FeedbackType $type = FeedbackType::Others; /** * @var array $metadata the metadata for the feedback diff --git a/src/Entity/Question.php b/src/Entity/Question.php index 8a6b041..909215e 100644 --- a/src/Entity/Question.php +++ b/src/Entity/Question.php @@ -31,7 +31,7 @@ class Question private string $type; #[ORM\Column(enumType: QuestionDifficulty::class)] - private QuestionDifficulty $difficulty; + private QuestionDifficulty $difficulty = QuestionDifficulty::Unspecified; #[ORM\Column(length: 255)] private string $title; diff --git a/src/Entity/SolutionEvent.php b/src/Entity/SolutionEvent.php index ea39e90..eb9bad6 100644 --- a/src/Entity/SolutionEvent.php +++ b/src/Entity/SolutionEvent.php @@ -20,7 +20,7 @@ class SolutionEvent extends BaseEvent private Question $question; #[ORM\Column(enumType: SolutionEventStatus::class)] - private SolutionEventStatus $status; + private SolutionEventStatus $status = SolutionEventStatus::Unspecified; #[ORM\Column(type: Types::TEXT)] private string $query; diff --git a/src/Repository/SolutionEventRepository.php b/src/Repository/SolutionEventRepository.php index a3c1f90..4caab87 100644 --- a/src/Repository/SolutionEventRepository.php +++ b/src/Repository/SolutionEventRepository.php @@ -219,4 +219,20 @@ public function getTotalAttempts(Question $question, ?Group $group): array return $result; } + + /** + * Get the latest query of a user for a question. + * + * @param Question $question The question to query + * @param User $submitter The user to query + * + * @return SolutionEvent|null The latest query of the user for the question + */ + public function getLatestQuery(Question $question, User $submitter): ?SolutionEvent + { + return $this->findOneBy([ + 'question' => $question, + 'submitter' => $submitter, + ], orderBy: ['id' => 'DESC']); + } } diff --git a/src/Service/DbRunner.php b/src/Service/DbRunner.php index 689e0c1..dc0871c 100644 --- a/src/Service/DbRunner.php +++ b/src/Service/DbRunner.php @@ -4,12 +4,12 @@ namespace App\Service; +use App\Entity\ChallengeDto\QueryResultDto; use App\Exception\QueryExecuteException; use App\Exception\ResourceException; use App\Exception\SchemaExecuteException; use App\Exception\TimedOutException; use App\Service\Types\DbRunnerProcessPayload; -use App\Service\Types\DbRunnerProcessResponse; use App\Service\Types\ProcessError; use Doctrine\SqlFormatter\SqlFormatter; use Symfony\Component\Process\Exception\ProcessFailedException; @@ -63,14 +63,14 @@ public function hashStatement(string $sql): string * @param string $schema the schema to create the database * @param string $query the query to run * - * @return array> the result of the query + * @return QueryResultDto the result of the query * * @throws SchemaExecuteException if the schema could not be executed * @throws QueryExecuteException if the query could not be executed * @throws ResourceException if the resource is exhausted (exit code = 255) * @throws \Throwable if the unexpected error is received */ - public function runQuery(string $schema, string $query): array + public function runQuery(string $schema, string $query): QueryResultDto { // Use a process to prevent the SQLite3 extension from crashing the PHP process. // For example, CTE queries and randomblob can crash the PHP process. @@ -90,15 +90,15 @@ public function runQuery(string $schema, string $query): array $output = $process->getOutput(); $outputDeserialized = unserialize($output, [ 'allowed_classes' => [ - DbRunnerProcessResponse::class, + QueryResultDto::class, ], ]); - if (!$outputDeserialized instanceof DbRunnerProcessResponse) { + if (!$outputDeserialized instanceof QueryResultDto) { throw new \RuntimeException("unexpected output: $output"); } - return $outputDeserialized->getResult(); + return $outputDeserialized; } catch (ProcessFailedException) { $exitCode = $process->getExitCode(); diff --git a/src/Service/DbRunnerComparer.php b/src/Service/DbRunnerComparer.php new file mode 100644 index 0000000..0f95e02 --- /dev/null +++ b/src/Service/DbRunnerComparer.php @@ -0,0 +1,54 @@ +getResult())) { + return new CompareResult\EmptyAnswer(); + } + if (0 === \count($userResult->getResult())) { + return new CompareResult\EmptyResult(); + } + + $answerColumns = $answerResult->getResult()[0]; + $userColumns = $userResult->getResult()[0]; + if ($answerColumns !== $userColumns) { + return new CompareResult\ColumnDifferent(); + } + + $answerRows = \array_slice($answerResult->getResult(), 1); + $userRows = \array_slice($userResult->getResult(), 1); + if (\count($answerRows) !== \count($userRows)) { + return new CompareResult\RowUnmatched( + expected: \count($answerRows), + actual: \count($userRows), + ); + } + + for ($i = 0; $i < \count($answerRows); ++$i) { + if ($answerRows[$i] !== $userRows[$i]) { + return new CompareResult\RowDifferent( + row: $i + 1, + ); + } + } + + return new CompareResult\Same(); + } +} diff --git a/src/Service/DbRunnerService.php b/src/Service/DbRunnerService.php index 130d8ca..745cc13 100644 --- a/src/Service/DbRunnerService.php +++ b/src/Service/DbRunnerService.php @@ -4,6 +4,7 @@ namespace App\Service; +use App\Entity\ChallengeDto\QueryResultDto; use App\Exception\QueryExecuteException; use App\Exception\SchemaExecuteException; use Psr\Cache\InvalidArgumentException; @@ -21,13 +22,11 @@ public function __construct(protected CacheInterface $cacheDbrunner) /** * Run a query on the SQLite3 database, cached. * - * @return array> - * * @throws InvalidArgumentException * @throws SchemaExecuteException * @throws QueryExecuteException */ - public function runQuery(string $schema, string $query): array + public function runQuery(string $schema, string $query): QueryResultDto { $schemaHash = $this->dbRunner->hashStatement($schema); $queryHash = $this->dbRunner->hashStatement($query); diff --git a/src/Service/Processes/DbRunnerProcessService.php b/src/Service/Processes/DbRunnerProcessService.php index e61d057..529ec09 100644 --- a/src/Service/Processes/DbRunnerProcessService.php +++ b/src/Service/Processes/DbRunnerProcessService.php @@ -4,8 +4,8 @@ namespace App\Service\Processes; +use App\Entity\ChallengeDto\QueryResultDto; use App\Service\Types\DbRunnerProcessPayload; -use App\Service\Types\DbRunnerProcessResponse; use App\Service\Types\SchemaDatabase; class DbRunnerProcessService extends ProcessService @@ -16,28 +16,49 @@ public function main(object $input): object throw new \InvalidArgumentException('Invalid input type'); } - $db = SchemaDatabase::get($input->getSchema()); - $result = $db->query($input->getQuery()); + $db = SchemaDatabase::get($input->schema); + $sqliteResult = $db->query($input->query); + $queryResult = $this->transformResult($sqliteResult); + $sqliteResult->finalize(); + return $queryResult; + } + + private function transformResult(\SQLite3Result $result): QueryResultDto + { /** - * @var array> $resultArray + * @var array> $columnsRow */ - $resultArray = []; + $columnsRow = []; - try { - while ($row = $result->fetchArray(\SQLITE3_ASSOC)) { - $rowCasted = []; + for ($i = 0; $i < $result->numColumns(); ++$i) { + $columnsRow[] = $result->columnName($i); + } - foreach ($row as $key => $value) { - $rowCasted[(string) $key] = $value; - } + /** + * @var array> $rows + */ + $rows = []; - $resultArray[] = $rowCasted; + while ($rawRow = $result->fetchArray(\SQLITE3_ASSOC)) { + $row = []; + foreach ($rawRow as $value) { + $row[] = match (true) { + null === $value => 'NULL', + \is_string($value) => $value, + \is_bool($value) => $value ? 'TRUE' : 'FALSE', + is_numeric($value) => (string) $value, + default => '', + }; } - } finally { - $result->finalize(); + $rows[] = $row; } - return new DbRunnerProcessResponse($resultArray); + /** + * @var array> $merged + */ + $merged = array_merge([$columnsRow], $rows); + + return (new QueryResultDto())->setResult($merged); } } diff --git a/src/Service/QuestionDbRunnerService.php b/src/Service/QuestionDbRunnerService.php index b8fc176..336ca2a 100644 --- a/src/Service/QuestionDbRunnerService.php +++ b/src/Service/QuestionDbRunnerService.php @@ -4,6 +4,7 @@ namespace App\Service; +use App\Entity\ChallengeDto\QueryResultDto; use App\Entity\Question; use App\Exception\QueryExecuteException; use App\Exception\SchemaExecuteException; @@ -29,13 +30,13 @@ public function __construct( * @param Question $question the question to get the result from * @param string $query the query to execute * - * @return array> the result of the query + * @return QueryResultDto the result of the query * * @throws InvalidArgumentException * @throws SchemaExecuteException * @throws QueryExecuteException */ - protected function getResult(Question $question, string $query): array + protected function getResult(Question $question, string $query): QueryResultDto { $schema = $question->getSchema(); @@ -50,14 +51,14 @@ protected function getResult(Question $question, string $query): array * * @param Question $question the question to get the result from * - * @return array> the result of the query + * @return QueryResultDto the result of the query * * @throws NotFoundHttpException * @throws InvalidArgumentException * @throws SchemaExecuteException * @throws QueryExecuteException */ - public function getAnswerResult(Question $question): array + public function getAnswerResult(Question $question): QueryResultDto { $lock = $this->lockFactory->createLock("question_{$question->getId()}_answer"); @@ -77,14 +78,14 @@ public function getAnswerResult(Question $question): array * @param Question $question the question to get the result from * @param string $query the query to execute * - * @return array> the result of the query + * @return QueryResultDto the result of the query * * @throws NotFoundHttpException * @throws InvalidArgumentException * @throws SchemaExecuteException * @throws QueryExecuteException */ - public function getQueryResult(Question $question, string $query): array + public function getQueryResult(Question $question, string $query): QueryResultDto { return $this->getResult($question, $query); } diff --git a/src/Service/Types/DbRunnerProcessPayload.php b/src/Service/Types/DbRunnerProcessPayload.php index d66290c..7042736 100644 --- a/src/Service/Types/DbRunnerProcessPayload.php +++ b/src/Service/Types/DbRunnerProcessPayload.php @@ -5,23 +5,13 @@ namespace App\Service\Types; /** - * The error that occurs when a process fails. + * The payload to the DbRunner process. */ readonly class DbRunnerProcessPayload { public function __construct( - private string $schema, - private string $query, + public string $schema, + public string $query, ) { } - - public function getSchema(): string - { - return $this->schema; - } - - public function getQuery(): string - { - return $this->query; - } } diff --git a/src/Service/Types/DbRunnerProcessResponse.php b/src/Service/Types/DbRunnerProcessResponse.php deleted file mode 100644 index fe169cd..0000000 --- a/src/Service/Types/DbRunnerProcessResponse.php +++ /dev/null @@ -1,27 +0,0 @@ -> $result - */ - public function __construct( - private array $result, - ) { - } - - /** - * @return array> - */ - public function getResult(): array - { - return $this->result; - } -} diff --git a/src/Twig/Components/Challenge/ColumnsOfAnswer.php b/src/Twig/Components/Challenge/ColumnsOfAnswer.php new file mode 100644 index 0000000..e3fa720 --- /dev/null +++ b/src/Twig/Components/Challenge/ColumnsOfAnswer.php @@ -0,0 +1,46 @@ +questionDbRunnerService->getAnswerResult($this->question); + $answerResult = $answer->getResult(); + + if (0 === \count($answerResult)) { + return []; + } + + return $answer->getResult()[0]; + } catch (\Throwable $e) { + return ["⚠️ Invalid Question: {$e->getMessage()}"]; + } + } +} diff --git a/src/Twig/Components/Challenge/Comment.php b/src/Twig/Components/Challenge/Comments.php similarity index 87% rename from src/Twig/Components/Challenge/Comment.php rename to src/Twig/Components/Challenge/Comments.php index aee6b86..0c9947f 100644 --- a/src/Twig/Components/Challenge/Comment.php +++ b/src/Twig/Components/Challenge/Comments.php @@ -14,7 +14,7 @@ use Symfony\UX\LiveComponent\DefaultActionTrait; #[AsLiveComponent] -final class Comment +final class Comments { use DefaultActionTrait; @@ -42,8 +42,5 @@ public function getComments(): array #[LiveListener('app:comment-refresh')] public function refresh(): void { - // Refresh the comments. - // - // It calls "__invoke()" implicitly, so this method itself is no-op. } } diff --git a/src/Twig/Components/Challenge/CommentModule/Comment.php b/src/Twig/Components/Challenge/Comments/Comment.php similarity index 97% rename from src/Twig/Components/Challenge/CommentModule/Comment.php rename to src/Twig/Components/Challenge/Comments/Comment.php index 7880a79..340f57a 100644 --- a/src/Twig/Components/Challenge/CommentModule/Comment.php +++ b/src/Twig/Components/Challenge/Comments/Comment.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Twig\Components\Challenge\CommentModule; +namespace App\Twig\Components\Challenge\Comments; use App\Entity\Comment as CommentEntity; use App\Entity\User as UserEntity; diff --git a/src/Twig/Components/Challenge/CommentModule/CommentForm.php b/src/Twig/Components/Challenge/Comments/CommentForm.php similarity index 97% rename from src/Twig/Components/Challenge/CommentModule/CommentForm.php rename to src/Twig/Components/Challenge/Comments/CommentForm.php index ab3e2bf..10cd494 100644 --- a/src/Twig/Components/Challenge/CommentModule/CommentForm.php +++ b/src/Twig/Components/Challenge/Comments/CommentForm.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Twig\Components\Challenge\CommentModule; +namespace App\Twig\Components\Challenge\Comments; use App\Entity\Comment as CommentEntity; use App\Entity\Question as QuestionEntity; diff --git a/src/Twig/Components/Challenge/Description.php b/src/Twig/Components/Challenge/Description.php index 8238072..154bfa9 100644 --- a/src/Twig/Components/Challenge/Description.php +++ b/src/Twig/Components/Challenge/Description.php @@ -5,37 +5,10 @@ namespace App\Twig\Components\Challenge; use App\Entity\Question; -use App\Service\QuestionDbRunnerService; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; #[AsTwigComponent] final class Description { - public function __construct( - private readonly QuestionDbRunnerService $questionDbRunnerService, - ) { - } - public Question $question; - - /** - * Get the columns of the answer. - * - * @return string[] the columns of the answer - */ - public function getColumnsOfAnswer(): array - { - try { - $answer = $this->questionDbRunnerService->getAnswerResult($this->question); - - // check if we have at least one row - if (0 === \count($answer)) { - return []; - } - - return array_keys($answer[0]); - } catch (\Throwable $e) { - return ["⚠️ Invalid Question: {$e->getMessage()}"]; - } - } } diff --git a/src/Twig/Components/Challenge/Executor.php b/src/Twig/Components/Challenge/Executor.php index 71e2e99..41918a3 100644 --- a/src/Twig/Components/Challenge/Executor.php +++ b/src/Twig/Components/Challenge/Executor.php @@ -9,10 +9,9 @@ use App\Entity\SolutionEventStatus; use App\Entity\User; use App\Repository\SolutionEventRepository; +use App\Service\DbRunnerComparer; use App\Service\QuestionDbRunnerService; use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\HttpKernel\Exception\HttpException; -use Symfony\Component\Serializer\SerializerInterface; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveAction; use Symfony\UX\LiveComponent\Attribute\LiveArg; @@ -41,17 +40,14 @@ public function __construct( public function getPreviousQuery(): string { - $se = $this->solutionEventRepository->findOneBy([ - 'question' => $this->question, - 'submitter' => $this->user, - ], orderBy: ['id' => 'DESC']); + $latestQuery = $this->solutionEventRepository + ->getLatestQuery($this->question, $this->user); - return $se?->getQuery() ?? ''; + return $latestQuery?->getQuery() ?? ''; } #[LiveAction] - public function execute( - SerializerInterface $serializer, + public function createNewQuery( #[LiveArg] string $query, ): void { if ('' === $query) { @@ -64,40 +60,24 @@ public function execute( ->setQuery($query); try { - $result = $this->questionDbRunnerService->getQueryResult($this->question, $query); - $answer = $this->questionDbRunnerService->getAnswerResult($this->question); - $same = $result === $answer; - - $solutionEvent = $solutionEvent->setStatus($same ? SolutionEventStatus::Passed : SolutionEventStatus::Failed); - - $payload = Payload::fromResult($result, same: $same); - } catch (HttpException $e) { - $solutionEvent = $solutionEvent->setStatus(SolutionEventStatus::Failed); - - $payload = Payload::fromErrorWithCode($e->getStatusCode(), $e->getMessage()); - } catch (\Throwable $e) { - $solutionEvent = $solutionEvent->setStatus(SolutionEventStatus::Failed); - - $payload = Payload::fromErrorWithCode(500, $e->getMessage()); - } - - try { - $serializedPayload = $serializer->serialize($payload, 'json'); - } catch (\Throwable $e) { - $solutionEvent = $solutionEvent->setStatus(SolutionEventStatus::Failed); + $result = $this->questionDbRunnerService->getQueryResult($this->question, $query); - $serializedPayload = $serializer->serialize( - Payload::fromErrorWithCode(500, $e->getMessage()), - 'json' + $compareResult = DbRunnerComparer::compare($answer, $result); + $solutionEvent = $solutionEvent->setStatus( + $compareResult->correct() + ? SolutionEventStatus::Passed + : SolutionEventStatus::Failed ); + } catch (\Throwable) { + $solutionEvent = $solutionEvent->setStatus(SolutionEventStatus::Failed); } - $this->emitUp('app:challenge-payload', [ - 'payload' => $serializedPayload, - ]); - $this->entityManager->persist($solutionEvent); $this->entityManager->flush(); + + $this->emit('app:challenge-executor:query-created', [ + 'query' => $query, + ]); } } diff --git a/src/Twig/Components/Challenge/Instruction/Content.php b/src/Twig/Components/Challenge/Instruction/Content.php index 5697d42..c1c0ede 100644 --- a/src/Twig/Components/Challenge/Instruction/Content.php +++ b/src/Twig/Components/Challenge/Instruction/Content.php @@ -4,7 +4,10 @@ namespace App\Twig\Components\Challenge\Instruction; +use Symfony\Component\Serializer\SerializerInterface; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; +use Symfony\UX\LiveComponent\Attribute\LiveArg; +use Symfony\UX\LiveComponent\Attribute\LiveListener; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; @@ -13,6 +16,13 @@ final class Content { use DefaultActionTrait; - #[LiveProp(updateFromParent: true)] - public ?HintPayload $hint; + #[LiveProp(writable: true)] + public ?HintPayload $hint = null; + + #[LiveListener('app:challenge-hint')] + public function onHintReceived(SerializerInterface $serializer, #[LiveArg] string $hint): void + { + $deserializedHint = $serializer->deserialize($hint, HintPayload::class, 'json'); + $this->hint = $deserializedHint; + } } diff --git a/src/Twig/Components/Challenge/Instruction/Modal.php b/src/Twig/Components/Challenge/Instruction/Modal.php index b2c91a2..ba32bab 100644 --- a/src/Twig/Components/Challenge/Instruction/Modal.php +++ b/src/Twig/Components/Challenge/Instruction/Modal.php @@ -7,6 +7,7 @@ use App\Entity\HintOpenEvent; use App\Entity\Question; use App\Entity\User; +use App\Repository\SolutionEventRepository; use App\Service\DbRunnerService; use App\Service\PointCalculationService; use App\Service\PromptService; @@ -33,9 +34,6 @@ final class Modal #[LiveProp] public Question $question; - #[LiveProp(updateFromParent: true)] - public string $query = ''; - public function getCost(): int { return PointCalculationService::hintOpenEventPoint; @@ -48,6 +46,7 @@ public function getCost(): int */ #[LiveAction] public function instruct( + SolutionEventRepository $solutionEventRepository, DbRunnerService $dbRunnerService, PromptService $promptService, TranslatorInterface $translator, @@ -62,7 +61,8 @@ public function instruct( throw new BadRequestHttpException('Hint feature is disabled.'); } - if ('' === $this->query) { + $query = $solutionEventRepository->getLatestQuery($this->question, $this->currentUser); + if (null === $query) { return; } @@ -72,7 +72,7 @@ public function instruct( $hintOpenEvent = (new HintOpenEvent()) ->setOpener($this->currentUser) ->setQuestion($this->question) - ->setQuery($this->query); + ->setQuery($query->getQuery()); // run answer. if it failed, we should consider it an error try { @@ -87,13 +87,13 @@ public function instruct( try { // run query to get the error message (or compare the result) - $result = $dbRunnerService->runQuery($schema, $this->query); + $result = $dbRunnerService->runQuery($schema, $query->getQuery()); } catch (\Throwable $e) { - $hint = $promptService->hint($this->query, $e->getMessage(), $answer); + $hint = $promptService->hint($query->getQuery(), $e->getMessage(), $answer); } if (isset($result) && $result !== $answerResult) { - $hint = $promptService->hint($this->query, 'Different output', $answer); + $hint = $promptService->hint($query->getQuery(), 'Different output', $answer); } if (!isset($hint)) { diff --git a/src/Twig/Components/Challenge/Payload.php b/src/Twig/Components/Challenge/Payload.php deleted file mode 100644 index 54f7725..0000000 --- a/src/Twig/Components/Challenge/Payload.php +++ /dev/null @@ -1,88 +0,0 @@ -result; - } - - public function getError(): ?Payload\ErrorPayload - { - return $this->error; - } - - public function setResult(?Payload\ResultPayload $result): void - { - $this->result = $result; - } - - public function setError(?Payload\ErrorPayload $error): void - { - $this->error = $error; - } - - /** - * A convenient method to create a result payload. - * - * @param array> $queryResult the result of the query - * @param bool $same whether the result is the same as the answer - * @param bool $answer whether the result is the answer - * - * @return self the payload - */ - public static function fromResult(array $queryResult, bool $same = false, bool $answer = false): self - { - $payload = new self(); - $payload->setResult( - (new Payload\ResultPayload()) - ->setQueryResult($queryResult) - ->setSame($same) - ->setAnswer($answer) - ); - - return $payload; - } - - /** - * A convenient method to create an error payload. - * - * @param ErrorProperty $property the error property - * @param string $message the error message - * - * @return self the payload - */ - public static function fromError(ErrorProperty $property, string $message): self - { - $payload = new self(); - $payload->setError( - (new Payload\ErrorPayload()) - ->setProperty($property) - ->setMessage($message) - ); - - return $payload; - } - - /** - * A convenient method to create an error (with code). - * - * @param int $code the error code - * @param string $message the error message - * - * @return self the payload - */ - public static function fromErrorWithCode(int $code, string $message): self - { - return self::fromError(ErrorProperty::fromCode($code), $message); - } -} diff --git a/src/Twig/Components/Challenge/Payload/ErrorPayload.php b/src/Twig/Components/Challenge/Payload/ErrorPayload.php deleted file mode 100644 index 25568e5..0000000 --- a/src/Twig/Components/Challenge/Payload/ErrorPayload.php +++ /dev/null @@ -1,35 +0,0 @@ -property; - } - - public function getMessage(): string - { - return $this->message; - } - - public function setProperty(ErrorProperty $property): self - { - $this->property = $property; - - return $this; - } - - public function setMessage(string $message): self - { - $this->message = $message; - - return $this; - } -} diff --git a/src/Twig/Components/Challenge/Payload/ErrorProperty.php b/src/Twig/Components/Challenge/Payload/ErrorProperty.php deleted file mode 100644 index f0c66f0..0000000 --- a/src/Twig/Components/Challenge/Payload/ErrorProperty.php +++ /dev/null @@ -1,31 +0,0 @@ - self::USER_ERROR, - 500 => self::SERVER_ERROR, - default => throw new \InvalidArgumentException("Unknown error code: $code"), - }; - } - - public function trans(TranslatorInterface $translator, ?string $locale = null): string - { - return match ($this) { - self::USER_ERROR => $translator->trans('challenge.error-type.user', locale: $locale), - self::SERVER_ERROR => $translator->trans('challenge.error-type.server', locale: $locale), - }; - } -} diff --git a/src/Twig/Components/Challenge/Payload/ResultPayload.php b/src/Twig/Components/Challenge/Payload/ResultPayload.php deleted file mode 100644 index 2468171..0000000 --- a/src/Twig/Components/Challenge/Payload/ResultPayload.php +++ /dev/null @@ -1,73 +0,0 @@ -> - */ - private array $queryResult; - private bool $same; - private bool $answer; - - /** - * Get the result of the query. - * - * @return array> - */ - public function getQueryResult(): array - { - return $this->queryResult; - } - - /** - * Get if this is same as the answer. - */ - public function isSame(): bool - { - return $this->same; - } - - /** - * Get if this is the answer. - */ - public function isAnswer(): bool - { - return $this->answer; - } - - /** - * Set the result of the query. - * - * @param array> $queryResult - */ - public function setQueryResult(array $queryResult): self - { - $this->queryResult = $queryResult; - - return $this; - } - - /** - * Set if this is same as the answer. - */ - public function setSame(bool $same): self - { - $this->same = $same; - - return $this; - } - - /** - * Set if this is the answer. - */ - public function setAnswer(bool $answer): self - { - $this->answer = $answer; - - return $this; - } -} diff --git a/src/Twig/Components/Challenge/ResultPresenterModule/AnswerPresenter.php b/src/Twig/Components/Challenge/ResultPresenterModule/AnswerPresenter.php deleted file mode 100644 index 1b68064..0000000 --- a/src/Twig/Components/Challenge/ResultPresenterModule/AnswerPresenter.php +++ /dev/null @@ -1,26 +0,0 @@ -payload?->getResult(); - } - - public function getError(): ?ErrorPayload - { - return $this->payload?->getError(); - } -} diff --git a/src/Twig/Components/Challenge/ResultPresenterModule/DiffPresenter.php b/src/Twig/Components/Challenge/ResultPresenterModule/DiffPresenter.php deleted file mode 100644 index fc199c7..0000000 --- a/src/Twig/Components/Challenge/ResultPresenterModule/DiffPresenter.php +++ /dev/null @@ -1,64 +0,0 @@ -answerPayload->getResult()?->getQueryResult(); - $rightQueryResult = $this->userPayload?->getResult()?->getQueryResult(); - - if (null === $leftQueryResult || null === $rightQueryResult) { - return null; - } - - $left = $this->serializer->serialize($leftQueryResult, 'csv', [ - 'csv_delimiter' => "\t", - 'csv_enclosure' => ' ', - ]); - $right = $this->serializer->serialize($rightQueryResult, 'csv', [ - 'csv_delimiter' => "\t", - 'csv_enclosure' => ' ', - ]); - - $diff = new Diff(explode("\n", $left), explode("\n", $right)); - $renderer = new SideBySide([ - 'title1' => $this->translator->trans('diff.answer'), - 'title2' => $this->translator->trans('diff.yours'), - ]); - - $result = $diff->render($renderer); - if (null === $result || false === $result) { - return ''; - } - - \assert(\is_string($result)); - - return $result; - } -} diff --git a/src/Twig/Components/Challenge/ResultPresenterModule/Table.php b/src/Twig/Components/Challenge/ResultPresenterModule/Table.php deleted file mode 100644 index ef70ab1..0000000 --- a/src/Twig/Components/Challenge/ResultPresenterModule/Table.php +++ /dev/null @@ -1,57 +0,0 @@ -> - */ - #[LiveProp(updateFromParent: true)] - public array $result; - - /** - * @return array - */ - public function getHeader(): array - { - if (0 === \count($this->result)) { - return []; - } - - return array_keys($this->result[0]); - } - - /** - * Get the data that can be paginated. - * - * It includes `[0, self::$LIMIT+1]` elements, where the last - * element is used to determine if there are more pages. - * - * @return array> - */ - protected function getData(): array - { - return \array_slice($this->result, ($this->page - 1) * self::limit, self::limit + 1); - } - - /** - * Get the paginated data. - * - * @return array> - */ - public function getRows(): array - { - return \array_slice($this->getData(), 0, self::limit); - } -} diff --git a/src/Twig/Components/Challenge/ResultPresenter.php b/src/Twig/Components/Challenge/Tabs.php similarity index 53% rename from src/Twig/Components/Challenge/ResultPresenter.php rename to src/Twig/Components/Challenge/Tabs.php index 456857a..ffe2dce 100644 --- a/src/Twig/Components/Challenge/ResultPresenter.php +++ b/src/Twig/Components/Challenge/Tabs.php @@ -6,14 +6,12 @@ use App\Entity\Question; use App\Entity\User; -use App\Service\QuestionDbRunnerService; -use App\Twig\Components\Challenge\Payload\ErrorProperty; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; #[AsLiveComponent] -final class ResultPresenter +final class Tabs { use DefaultActionTrait; @@ -44,29 +42,4 @@ final class ResultPresenter */ #[LiveProp(writable: true)] public string $currentTab = 'result'; - - /** - * @var Payload|null $userResult the result of the user's query - */ - #[LiveProp(updateFromParent: true)] - public ?Payload $userResult; - - public function __construct( - private readonly QuestionDbRunnerService $questionDbRunnerService, - ) { - } - - /** - * Get the wrapped payload of the answer. - */ - public function getAnswerPayload(): Payload - { - try { - $answer = $this->questionDbRunnerService->getAnswerResult($this->question); - } catch (\Throwable $e) { - return Payload::fromError(ErrorProperty::fromCode(500), $e->getMessage()); - } - - return Payload::fromResult($answer, answer: true); - } } diff --git a/src/Twig/Components/Challenge/Tabs/AnswerQueryResult.php b/src/Twig/Components/Challenge/Tabs/AnswerQueryResult.php new file mode 100644 index 0000000..3934c6f --- /dev/null +++ b/src/Twig/Components/Challenge/Tabs/AnswerQueryResult.php @@ -0,0 +1,41 @@ +questionDbRunnerService->getAnswerResult($this->question); + + return (new FallableQueryResultDto())->setResult($resultDto); + } catch (\Throwable $e) { + $errorMessage = t('challenge.errors.answer-query-failure', [ + '%error%' => $e->getMessage(), + ]); + + return (new FallableQueryResultDto())->setErrorMessage($errorMessage); + } + } +} diff --git a/src/Twig/Components/Challenge/Tabs/DiffPresenter.php b/src/Twig/Components/Challenge/Tabs/DiffPresenter.php new file mode 100644 index 0000000..808375d --- /dev/null +++ b/src/Twig/Components/Challenge/Tabs/DiffPresenter.php @@ -0,0 +1,119 @@ +query = $this->solutionEventRepository->getLatestQuery($this->question, $this->user)?->getQuery(); + } + + public function getAnswerResult(): ?string + { + try { + $resultDto = $this->questionDbRunnerService->getAnswerResult($this->question); + + return $this->serializer->serialize($resultDto->getResult(), 'csv', [ + 'csv_delimiter' => "\t", + 'csv_enclosure' => ' ', + ]); + } catch (\Throwable $e) { + $this->logger->debug('Failed to get the answer result', [ + 'exception' => $e, + ]); + + return null; + } + } + + public function getUserResult(): ?string + { + if (null === $this->query) { + return null; + } + + try { + $resultDto = $this->questionDbRunnerService->getQueryResult($this->question, $this->query); + + return $this->serializer->serialize($resultDto->getResult(), 'csv', [ + 'csv_delimiter' => "\t", + 'csv_enclosure' => ' ', + ]); + } catch (\Throwable $e) { + $this->logger->debug('Failed to get the user result', [ + 'exception' => $e, + ]); + + return null; + } + } + + /** + * @return ?string The HTML string of the diff. + * "" if the diff is empty. + * Null if the diff cannot be calculated, for example, no results. + */ + public function getDiff(): ?string + { + $leftQueryResult = $this->getUserResult(); + $rightQueryResult = $this->getAnswerResult(); + + if (null === $leftQueryResult || null === $rightQueryResult) { + return null; + } + + $diff = new Diff(explode("\n", $leftQueryResult), explode("\n", $rightQueryResult)); + $renderer = new SideBySide([ + 'title1' => $this->translator->trans('diff.answer'), + 'title2' => $this->translator->trans('diff.yours'), + ]); + + $result = $diff->render($renderer); + if (null === $result || false === $result) { + return ''; + } + + \assert(\is_string($result)); + + return $result; + } +} diff --git a/src/Twig/Components/Challenge/ResultPresenterModule/EventPresenter.php b/src/Twig/Components/Challenge/Tabs/Events.php similarity index 83% rename from src/Twig/Components/Challenge/ResultPresenterModule/EventPresenter.php rename to src/Twig/Components/Challenge/Tabs/Events.php index 0f4fc53..8c0d084 100644 --- a/src/Twig/Components/Challenge/ResultPresenterModule/EventPresenter.php +++ b/src/Twig/Components/Challenge/Tabs/Events.php @@ -2,23 +2,24 @@ declare(strict_types=1); -namespace App\Twig\Components\Challenge\ResultPresenterModule; +namespace App\Twig\Components\Challenge\Tabs; use App\Entity\Question; use App\Entity\SolutionEvent; use App\Entity\User; use App\Repository\SolutionEventRepository; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; +use Symfony\UX\LiveComponent\Attribute\LiveListener; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; #[AsLiveComponent] -final class EventPresenter +final class Events { use DefaultActionTrait; use Pagination; - #[LiveProp(updateFromParent: true)] + #[LiveProp] public Question $question; #[LiveProp] @@ -54,4 +55,9 @@ protected function getData(): array offset: ($this->page - 1) * self::limit, ); } + + #[LiveListener('app:challenge-executor:query-created')] + public function onQueryUpdated(): void + { + } } diff --git a/src/Twig/Components/Challenge/ResultPresenterModule/Pagination.php b/src/Twig/Components/Challenge/Tabs/Pagination.php similarity index 95% rename from src/Twig/Components/Challenge/ResultPresenterModule/Pagination.php rename to src/Twig/Components/Challenge/Tabs/Pagination.php index 358dcb7..094c737 100644 --- a/src/Twig/Components/Challenge/ResultPresenterModule/Pagination.php +++ b/src/Twig/Components/Challenge/Tabs/Pagination.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Twig\Components\Challenge\ResultPresenterModule; +namespace App\Twig\Components\Challenge\Tabs; use Symfony\UX\LiveComponent\Attribute\LiveAction; use Symfony\UX\LiveComponent\Attribute\LiveProp; diff --git a/src/Twig/Components/Challenge/Tabs/QueryResultTable.php b/src/Twig/Components/Challenge/Tabs/QueryResultTable.php new file mode 100644 index 0000000..978972a --- /dev/null +++ b/src/Twig/Components/Challenge/Tabs/QueryResultTable.php @@ -0,0 +1,59 @@ + the header + */ + public function getHeader(): array + { + return $this->result->getResult()[0]; + } + + /** + * @return array> the rows + */ + public function getRows(): array + { + return \array_slice($this->result->getResult(), 1); + } + + /** + * Get the paginated rows and another row to determine if there are more pages. + * + * @return array> + */ + protected function getData(): array + { + return \array_slice($this->getRows(), ($this->page - 1) * self::limit, self::limit + 1); + } + + /** + * Get the paginated rows. + * + * @return array> + */ + public function getPaginatedRows(): array + { + return \array_slice($this->getData(), 0, self::limit); + } +} diff --git a/src/Twig/Components/Challenge/Tabs/UserQueryResult.php b/src/Twig/Components/Challenge/Tabs/UserQueryResult.php new file mode 100644 index 0000000..2153e1d --- /dev/null +++ b/src/Twig/Components/Challenge/Tabs/UserQueryResult.php @@ -0,0 +1,98 @@ +query = $this->solutionEventRepository->getLatestQuery($this->question, $this->user)?->getQuery(); + } + + public function getResult(): ?FallableQueryResultDto + { + if (null === $this->query) { + return null; + } + + try { + $answerResultDto = $this->questionDbRunnerService->getAnswerResult($this->question); + } catch (\Throwable $e) { + $errorMessage = t('challenge.errors.answer-query-failure', [ + '%error%' => $e->getMessage(), + ]); + + return (new FallableQueryResultDto())->setErrorMessage($errorMessage); + } + + try { + $resultDto = $this->questionDbRunnerService->getQueryResult($this->question, $this->query); + } catch (\Throwable $e) { + $errorMessage = t('challenge.errors.user-query-error', [ + '%error%' => $e->getMessage(), + ]); + + return (new FallableQueryResultDto())->setErrorMessage($errorMessage); + } + + // compare the result + $compareResult = DbRunnerComparer::compare($answerResultDto, $resultDto); + if ($compareResult->correct()) { + return (new FallableQueryResultDto())->setResult($resultDto); + } + + $errorMessage = t('challenge.errors.user-query-failure', [ + '%error%' => $compareResult->reason(), + ]); + + return (new FallableQueryResultDto())->setResult($resultDto)->setErrorMessage($errorMessage); + } + + #[LiveListener('app:challenge-executor:query-created')] + public function onQueryUpdated(#[LiveArg] string $query): void + { + $this->query = $query; + } +} diff --git a/src/Twig/Components/Challenge/Ui.php b/src/Twig/Components/Challenge/Ui.php index f8364d1..1a5f908 100644 --- a/src/Twig/Components/Challenge/Ui.php +++ b/src/Twig/Components/Challenge/Ui.php @@ -6,12 +6,7 @@ use App\Entity\Question; use App\Entity\User; -use App\Twig\Components\Challenge\Instruction\HintPayload; -use Psr\Log\LoggerInterface; -use Symfony\Component\Serializer\SerializerInterface; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; -use Symfony\UX\LiveComponent\Attribute\LiveArg; -use Symfony\UX\LiveComponent\Attribute\LiveListener; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; @@ -28,45 +23,4 @@ final class Ui #[LiveProp] public int $limit; - - /** - * @var Payload|null $result the result of the user's query - */ - #[LiveProp(writable: true)] - public ?Payload $result = null; - - /** - * @var string $query the user's query - */ - #[LiveProp(writable: true)] - public string $query = ''; - - /** - * @var HintPayload|null $hint the hint for the user - */ - #[LiveProp(writable: true)] - public ?HintPayload $hint = null; - - #[LiveListener('app:challenge-payload')] - public function updateResult( - SerializerInterface $serializer, - #[LiveArg('payload')] string $rawPayload, - ): void { - $payload = $serializer->deserialize($rawPayload, Payload::class, 'json'); - - $this->result = $payload; - } - - #[LiveListener('app:challenge-hint')] - public function updateHint( - LoggerInterface $logger, - SerializerInterface $serializer, - #[LiveArg('hint')] string $rawHint, - ): void { - $logger->debug('Received hint', ['hint' => $rawHint]); - - $hint = $serializer->deserialize($rawHint, HintPayload::class, 'json'); - - $this->hint = $hint; - } } diff --git a/templates/components/Challenge/ColumnsOfAnswer.html.twig b/templates/components/Challenge/ColumnsOfAnswer.html.twig new file mode 100644 index 0000000..9e2122f --- /dev/null +++ b/templates/components/Challenge/ColumnsOfAnswer.html.twig @@ -0,0 +1,5 @@ +

+ 輸出格式:欄位順序分別為:{{ + this.columnsOfAnswer|joinToQuoted('、') + }}。 +

diff --git a/templates/components/Challenge/Comment.html.twig b/templates/components/Challenge/Comments.html.twig similarity index 65% rename from templates/components/Challenge/Comment.html.twig rename to templates/components/Challenge/Comments.html.twig index 247b677..54f5b7f 100644 --- a/templates/components/Challenge/Comment.html.twig +++ b/templates/components/Challenge/Comments.html.twig @@ -1,12 +1,12 @@
{% if appfeatures.comment %}
- +
{% for comment in this.comments %} - + {% else %}
尚無留言。成為第一個留言者吧!
{% endfor %} diff --git a/templates/components/Challenge/CommentModule/Comment.html.twig b/templates/components/Challenge/Comments/Comment.html.twig similarity index 100% rename from templates/components/Challenge/CommentModule/Comment.html.twig rename to templates/components/Challenge/Comments/Comment.html.twig diff --git a/templates/components/Challenge/CommentModule/CommentForm.html.twig b/templates/components/Challenge/Comments/CommentForm.html.twig similarity index 100% rename from templates/components/Challenge/CommentModule/CommentForm.html.twig rename to templates/components/Challenge/Comments/CommentForm.html.twig diff --git a/templates/components/Challenge/Description.html.twig b/templates/components/Challenge/Description.html.twig index 0651985..5bcac97 100644 --- a/templates/components/Challenge/Description.html.twig +++ b/templates/components/Challenge/Description.html.twig @@ -3,11 +3,7 @@ {{ question.description|raw }}
-

- 輸出格式:欄位順序分別為:{{ - this.columnsOfAnswer|joinToQuoted('、') - }}。 -

+ {% if question.schema %}

Schema ({{ question.schema.id }}): diff --git a/templates/components/Challenge/ResultPresenterModule/AnswerPresenter.html.twig b/templates/components/Challenge/ResultPresenterModule/AnswerPresenter.html.twig deleted file mode 100644 index bbe3335..0000000 --- a/templates/components/Challenge/ResultPresenterModule/AnswerPresenter.html.twig +++ /dev/null @@ -1,25 +0,0 @@ - - {% if this.result %} - {% if not this.result.answer %} - {% if this.result.same %} -

- {% else %} - - {% endif %} - {% endif %} - - {% elseif this.error %} - - {% else %} - - {% endif %} -
diff --git a/templates/components/Challenge/ResultPresenter.html.twig b/templates/components/Challenge/Tabs.html.twig similarity index 51% rename from templates/components/Challenge/ResultPresenter.html.twig rename to templates/components/Challenge/Tabs.html.twig index 98abd2d..b069252 100644 --- a/templates/components/Challenge/ResultPresenter.html.twig +++ b/templates/components/Challenge/Tabs.html.twig @@ -1,4 +1,4 @@ - + -
+
{% if currentTab == 'answer' %} - - {% elseif currentTab == 'diff' %} - + {% elseif currentTab == 'events' %} - + + {% elseif currentTab == 'diff' %} + {% else %} - + {% endif %}
diff --git a/templates/components/Challenge/Tabs/AnswerQueryResult.html.twig b/templates/components/Challenge/Tabs/AnswerQueryResult.html.twig new file mode 100644 index 0000000..a886f3c --- /dev/null +++ b/templates/components/Challenge/Tabs/AnswerQueryResult.html.twig @@ -0,0 +1,13 @@ +
+ {% set answer = this.answer %} + + {% if answer.errorMessage %} + + {% endif %} + + {% if answer.result %} + + {% endif %} +
diff --git a/templates/components/Challenge/ResultPresenterModule/DiffPresenter.html.twig b/templates/components/Challenge/Tabs/DiffPresenter.html.twig similarity index 99% rename from templates/components/Challenge/ResultPresenterModule/DiffPresenter.html.twig rename to templates/components/Challenge/Tabs/DiffPresenter.html.twig index ee77b2c..effd043 100644 --- a/templates/components/Challenge/ResultPresenterModule/DiffPresenter.html.twig +++ b/templates/components/Challenge/Tabs/DiffPresenter.html.twig @@ -163,7 +163,7 @@ {% elseif this.diff is same as('') %} {% else %} diff --git a/templates/components/Challenge/ResultPresenterModule/Pagination.html.twig b/templates/components/Challenge/Tabs/Pagination.html.twig similarity index 100% rename from templates/components/Challenge/ResultPresenterModule/Pagination.html.twig rename to templates/components/Challenge/Tabs/Pagination.html.twig diff --git a/templates/components/Challenge/ResultPresenterModule/Table.html.twig b/templates/components/Challenge/Tabs/QueryResultTable.html.twig similarity index 82% rename from templates/components/Challenge/ResultPresenterModule/Table.html.twig rename to templates/components/Challenge/Tabs/QueryResultTable.html.twig index f04432e..854c3c7 100644 --- a/templates/components/Challenge/ResultPresenterModule/Table.html.twig +++ b/templates/components/Challenge/Tabs/QueryResultTable.html.twig @@ -9,7 +9,7 @@ - {% for column in this.rows %} + {% for column in this.paginatedRows %} {{ this.currentOffset + loop.index }} {% for cell in column %} @@ -20,5 +20,5 @@ - {{ include('components/Challenge/ResultPresenterModule/Pagination.html.twig') }} + {{ include('components/Challenge/Tabs/Pagination.html.twig') }} diff --git a/templates/components/Challenge/Tabs/UserQueryResult.html.twig b/templates/components/Challenge/Tabs/UserQueryResult.html.twig new file mode 100644 index 0000000..56bf83a --- /dev/null +++ b/templates/components/Challenge/Tabs/UserQueryResult.html.twig @@ -0,0 +1,21 @@ + + {% set result = this.result %} + + {% if result is null %} + + {% elseif result.errorMessage is not null %} + + {% else %} + + {% endif %} + + {% if result is not null and result.result %} + + {% endif %} + diff --git a/templates/components/Challenge/Ui.html.twig b/templates/components/Challenge/Ui.html.twig index bad595c..d3cb57a 100644 --- a/templates/components/Challenge/Ui.html.twig +++ b/templates/components/Challenge/Ui.html.twig @@ -4,7 +4,7 @@ {% endif %} {% if appfeatures.hint %} - + {% endif %}
@@ -12,7 +12,7 @@
- +
{% if appfeatures.hint %} @@ -30,15 +30,15 @@
- - + +
{% if appfeatures.comment %}

留言區

- +
{% endif %}
diff --git a/tests/Service/DbRunnerServiceTest.php b/tests/Service/DbRunnerServiceTest.php index 2c36750..5bf1fb3 100644 --- a/tests/Service/DbRunnerServiceTest.php +++ b/tests/Service/DbRunnerServiceTest.php @@ -26,7 +26,7 @@ public function testCache(): void $query = 'SELECT * FROM newsletter'; $result = $dbRunnerService->runQuery($schema, $query); - self::assertEquals([['id' => 1, 'content' => 'hello']], $result); + self::assertEquals([['id', 'content'], ['1', 'hello']], $result->getResult()); $hashedSchema = $dbRunnerService->getDbRunner()->hashStatement($schema); $hashedQuery = $dbRunnerService->getDbRunner()->hashStatement($query); @@ -39,7 +39,7 @@ public function testCache(): void INSERT INTO newsletter (content) VALUES ('hello');", 'SELECT * FROM newsletter' ); - self::assertEquals([['id' => 1, 'content' => 'hello']], $result); + self::assertEquals([['id', 'content'], ['1', 'hello']], $result->getResult()); self::assertCount(1, $cache->getValues(), 'cache hit'); $result = $dbRunnerService->runQuery( @@ -48,7 +48,7 @@ public function testCache(): void INSERT INTO newsletter (content) VALUES ('hello');", 'SELECT * FROM newsletter -- normalization test' ); - self::assertEquals([['id' => 1, 'content' => 'hello']], $result); + self::assertEquals([['id', 'content'], ['1', 'hello']], $result->getResult()); self::assertCount(1, $cache->getValues(), 'cache hit'); $result = $dbRunnerService->runQuery( @@ -57,7 +57,7 @@ public function testCache(): void INSERT INTO newsletter (content) VALUES ('hello');", "SELECT * FROM newsletter WHERE content == 'hello'" ); - self::assertEquals([['id' => 1, 'content' => 'hello']], $result); + self::assertEquals([['id', 'content'], ['1', 'hello']], $result->getResult()); self::assertCount(2, $cache->getValues(), 'cache not hit'); } diff --git a/tests/Service/DbRunnerTest.php b/tests/Service/DbRunnerTest.php index de52411..74d28de 100644 --- a/tests/Service/DbRunnerTest.php +++ b/tests/Service/DbRunnerTest.php @@ -87,8 +87,9 @@ public static function runQueryProvider(): array INSERT INTO test (name) VALUES ('Bob');", 'SELECT * FROM test;', [ - ['id' => 1, 'name' => 'Alice'], - ['id' => 2, 'name' => 'Bob'], + ['id', 'name'], + ['1', 'Alice'], + ['2', 'Bob'], ], /* result */ null, /* exception */ ], @@ -126,7 +127,8 @@ public static function runQueryProvider(): array INSERT INTO test (name) VALUES ('Bob');", "UPDATE test SET name = 'Charlie' WHERE id = 1 RETURNING *;", [ - ['id' => 1, 'name' => 'Charlie'], + ['id', 'name'], + ['1', 'Charlie'], ], /* result */ QueryExecuteException::class, /* exception */ ], @@ -186,7 +188,8 @@ public static function runQueryProvider(): array INSERT INTO test VALUES (1, NULL);', 'SELECT * FROM test;', [ - ['id' => 1, 'name' => null], + ['id', 'name'], + ['1', 'NULL'], ], /* result */ null, /* exception */ ], @@ -199,7 +202,8 @@ public static function runQueryProvider(): array INSERT INTO test VALUES (1, 1.23);', 'SELECT * FROM test;', [ - ['id' => 1, 'name' => 1.23], + ['id', 'name'], + ['1', '1.23'], ], /* result */ null, /* exception */ ], @@ -212,7 +216,8 @@ public static function runQueryProvider(): array INSERT INTO test VALUES (1, x'68656c6c6f');", 'SELECT * FROM test;', [ - ['id' => 1, 'name' => 'hello'], + ['id', 'name'], + ['1', 'hello'], ], /* result */ null, /* exception */ ], @@ -220,7 +225,8 @@ public static function runQueryProvider(): array '', 'SELECT 1;', [ - ['1' => 1], + ['1'], + ['1'], ], /* result */ null, /* exception */ ], @@ -260,27 +266,10 @@ public static function runQueryProvider(): array LEFT(records.ClassNo, 3) ', [ - [ - '班級' => '101', - '事假總計' => 3, - '病假總計' => 5, - '公假總計' => 2, - '曠課總計' => 3, - ], - [ - '班級' => '102', - '事假總計' => 4, - '病假總計' => 0, - '公假總計' => 3, - '曠課總計' => 0, - ], - [ - '班級' => '103', - '事假總計' => 0, - '病假總計' => 0, - '公假總計' => 3, - '曠課總計' => 1, - ], + ['班級', '事假總計', '病假總計', '公假總計', '曠課總計'], + ['101', '3', '5', '2', '3'], + ['102', '4', '0', '3', '0'], + ['103', '0', '0', '3', '1'], ], /* result */ null, /* exception */ ], @@ -331,13 +320,12 @@ public function testRunQuery(string $schema, string $query, ?array $expect, ?str $this->expectNotToPerformAssertions(); } - $generator = $dbrunner->runQuery($schema, $query); - - if (null !== $expect) { - foreach ($generator as $idx => $actual) { - self::assertEquals($expect[$idx], $actual); - } + $result = $dbrunner->runQuery($schema, $query); + if (null === $expect) { + return; } + + self::assertEquals($expect, $result->getResult()); } public function testRunQueryCte(): void @@ -377,48 +365,64 @@ public function testRunQueryYear(): void { $dbrunner = new DbRunner(); - self::assertEquals([['year("2021-01-01")' => 2021]], $dbrunner->runQuery('', 'SELECT year("2021-01-01")')); + $result = $dbrunner->runQuery('', 'SELECT year("2021-01-01")'); + self::assertEquals([['year("2021-01-01")'], ['2021']], $result->getResult()); } public function testRunQueryMonth(): void { $dbrunner = new DbRunner(); - self::assertEquals([['month("2021-01-01")' => 1]], $dbrunner->runQuery('', 'SELECT month("2021-01-01")')); + $result = $dbrunner->runQuery('', 'SELECT month("2021-01-01")'); + self::assertEquals([['month("2021-01-01")'], ['1']], $result->getResult()); } public function testRunQueryDay(): void { $dbrunner = new DbRunner(); - self::assertEquals([['day("2021-01-01")' => 1]], $dbrunner->runQuery('', 'SELECT day("2021-01-01")')); + $result = $dbrunner->runQuery('', 'SELECT day("2021-01-01")'); + self::assertEquals([['day("2021-01-01")'], ['1']], $result->getResult()); } public function testRunQueryIf(): void { $dbrunner = new DbRunner(); - self::assertEquals([['if(1, 2, 3)' => 2]], $dbrunner->runQuery('', 'SELECT if(1, 2, 3)')); - self::assertEquals([['if(0, 2, 3)' => 3]], $dbrunner->runQuery('', 'SELECT if(0, 2, 3)')); + $result = $dbrunner->runQuery('', 'SELECT if(1, 2, 3)'); + self::assertEquals([['if(1, 2, 3)'], ['2']], $result->getResult()); + + $result = $dbrunner->runQuery('', 'SELECT if(0, 2, 3)'); + self::assertEquals([['if(0, 2, 3)'], ['3']], $result->getResult()); } public function testRunQueryLeft(): void { $dbrunner = new DbRunner(); - self::assertEquals([['left("abcdef", 3)' => 'abc']], $dbrunner->runQuery('', 'SELECT left("abcdef", 3)')); - self::assertEquals([['left("1234567", 8)' => '1234567']], $dbrunner->runQuery('', 'SELECT left("1234567", 8)')); - self::assertEquals([['left("hello", 2)' => 'he']], $dbrunner->runQuery('', 'SELECT left("hello", 2)')); - self::assertEquals([['left("hello", 0)' => '']], $dbrunner->runQuery('', 'SELECT left("hello", 0)')); - self::assertEquals([['left("hello", 6)' => 'hello']], $dbrunner->runQuery('', 'SELECT left("hello", 6)')); - self::assertEquals([['left(c, 6)' => 'hello']], $dbrunner->runQuery('', 'SELECT left(c, 6) FROM (SELECT \'hello\' AS c)')); + $testcases = [ + 'left("abcdef", 3)' => 'abc', + 'left("1234567", 8)' => '1234567', + 'left("hello", 2)' => 'he', + 'left("hello", 0)' => '', + 'left("hello", 6)' => 'hello', + ]; + + foreach ($testcases as $query => $expected) { + $result = $dbrunner->runQuery('', 'SELECT '.$query); + self::assertEquals([[$query], [$expected]], $result->getResult()); + } + + $result = $dbrunner->runQuery('', 'SELECT left(c, 6) FROM (SELECT \'hello\' AS c)'); + self::assertEquals([['left(c, 6)'], ['hello']], $result->getResult()); } public function testRunQuerySum(): void { $dbrunner = new DbRunner(); - self::assertEquals([['sum(n)' => 6]], $dbrunner->runQuery('', 'SELECT sum(n) FROM (SELECT 1 AS n UNION SELECT 2 AS n UNION SELECT 3 AS n)')); + $result = $dbrunner->runQuery('', 'SELECT sum(1)'); + self::assertEquals([['sum(1)'], ['1']], $result->getResult()); } public function testSchemaCache(): void diff --git a/translations/messages.zh_TW.yaml b/translations/messages.zh_TW.yaml index d4d44aa..b22bc18 100644 --- a/translations/messages.zh_TW.yaml +++ b/translations/messages.zh_TW.yaml @@ -56,9 +56,6 @@ New & In Progress: 新問題 & 處理中 Resolved & Closed: 已解決 & 已關閉 Impersonate: 模擬使用者 -challenge.error-type.user: 輸入錯誤 -challenge.error-type.server: 伺服器錯誤 - result_presenter.tabs.result: 執行結果 result_presenter.tabs.answer: 正確答案 result_presenter.tabs.diff: 答案比較 @@ -146,3 +143,22 @@ notification: 閱讀意見回饋 → %link% anonymous: <匿名> + +challenge: + tabs: + result: 執行結果 + answer: 正確答案 + diff: 答案比較 + events: 查詢記錄 + compare-result: + same: 答案完全相同。 + empty-answer: 正確答案沒有欄位,通常代表出題者寫出的查詢語句有誤,請回報給我們。 + empty-result: 你的答案沒有任何欄位,通常代表查詢語句有誤。 + column-different: 欄位名稱有差異,請對照正確答案修改。 + row-different: 您回答的第 %row% 列與正確答案不同。 + row-unmatched: 回傳列數和正確答案不一致(正確答案有 %expected% 列,你回答了 %actual% 列)。 + errors: + no-query-yet: 寫完查詢後按下「提交」來查看執行結果。 + answer-query-failure: 正確答案也是個錯誤的 SQL 查詢:%error% + user-query-error: 你的 SQL 查詢執行失敗:%error% + user-query-failure: 你的 SQL 查詢不正確:%error%