From 9ed22ed6cddc3ecef9c150901493e13d536e2173 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Thu, 14 Nov 2024 19:02:23 +0800 Subject: [PATCH 01/13] fix(docker): Clear Meilisearch index before re-indexing --- frankenphp/docker-entrypoint.sh | 1 + 1 file changed, 1 insertion(+) 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 From 12a788a86a6c10886e3c550e44114e975a58f7bc Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 01:27:30 +0800 Subject: [PATCH 02/13] refactor: Rewrite DbRunner and Challenge page * Typed query result: We previously returned the raw "array" and type-hinted them everywhere. Now, the DbRunner returns a QueryResultDto that wraps this type. * Improved "Result Presenter": Instead of passing the result everywhere, retrieve it directly in the tabs rather than receiving it from the Executor. * The executor now creates a new SolutionEvent without comparing the results. * Added a new Comparer that provides more comprehensive difference information. * Removed "Module" from the component namespace. * Update DbRunner return style * All the cells are string now * The first row is header * The rows started from the second are the rows --- .../challenge_executor_controller.ts | 2 +- .../CompareResult/ColumnDifferent.php | 25 +++ .../CompareResult/CompareResult.php | 17 ++ .../CompareResult/EmptyAnswer.php | 22 +++ .../CompareResult/EmptyResult.php | 22 +++ .../CompareResult/RowDifferent.php | 31 ++++ .../CompareResult/RowUnmatched.php | 35 ++++ .../ChallengeDto/CompareResult/Same.php | 25 +++ .../ChallengeDto/FallableQueryResultDto.php | 42 +++++ src/Entity/ChallengeDto/QueryResultDto.php | 36 ++++ src/Repository/SolutionEventRepository.php | 16 ++ src/Service/DbRunner.php | 12 +- src/Service/DbRunnerComparer.php | 54 ++++++ src/Service/DbRunnerService.php | 5 +- .../Processes/DbRunnerProcessService.php | 51 ++++-- src/Service/QuestionDbRunnerService.php | 13 +- src/Service/Types/DbRunnerProcessPayload.php | 16 +- src/Service/Types/DbRunnerProcessResponse.php | 27 --- .../Challenge/{Comment.php => Comments.php} | 5 +- .../{CommentModule => Comments}/Comment.php | 2 +- .../CommentForm.php | 2 +- src/Twig/Components/Challenge/Description.php | 6 +- src/Twig/Components/Challenge/Executor.php | 54 ++---- .../Challenge/Instruction/Content.php | 15 +- src/Twig/Components/Challenge/Payload.php | 88 --------- .../Challenge/Payload/ErrorPayload.php | 35 ---- .../Challenge/Payload/ErrorProperty.php | 31 ---- .../Challenge/Payload/ResultPayload.php | 73 -------- .../ResultPresenterModule/AnswerPresenter.php | 26 --- .../ResultPresenterModule/DiffPresenter.php | 64 ------- .../Challenge/ResultPresenterModule/Table.php | 57 ------ .../{ResultPresenter.php => Tabs.php} | 31 +--- .../Challenge/Tabs/AnswerQueryResult.php | 46 +++++ .../EventPresenter.php => Tabs/Events.php} | 6 +- .../Pagination.php | 2 +- .../Challenge/Tabs/QueryResultTable.php | 59 ++++++ .../Challenge/Tabs/UserQueryResult.php | 98 ++++++++++ src/Twig/Components/Challenge/Ui.php | 40 ---- .../{Comment.html.twig => Comments.html.twig} | 4 +- .../Comment.html.twig | 0 .../CommentForm.html.twig | 0 .../Challenge/ResultPresenter.html.twig | 26 --- .../AnswerPresenter.html.twig | 25 --- .../DiffPresenter.html.twig | 173 ------------------ templates/components/Challenge/Tabs.html.twig | 24 +++ .../Tabs/AnswerQueryResult.html.twig | 11 ++ .../Events.html.twig} | 2 +- .../Pagination.html.twig | 0 .../QueryResultTable.html.twig} | 4 +- .../Challenge/Tabs/UserQueryResult.html.twig | 21 +++ templates/components/Challenge/Ui.html.twig | 6 +- tests/Service/DbRunnerTest.php | 96 +++++----- translations/messages.zh_TW.yaml | 19 ++ 53 files changed, 758 insertions(+), 844 deletions(-) create mode 100644 src/Entity/ChallengeDto/CompareResult/ColumnDifferent.php create mode 100644 src/Entity/ChallengeDto/CompareResult/CompareResult.php create mode 100644 src/Entity/ChallengeDto/CompareResult/EmptyAnswer.php create mode 100644 src/Entity/ChallengeDto/CompareResult/EmptyResult.php create mode 100644 src/Entity/ChallengeDto/CompareResult/RowDifferent.php create mode 100644 src/Entity/ChallengeDto/CompareResult/RowUnmatched.php create mode 100644 src/Entity/ChallengeDto/CompareResult/Same.php create mode 100644 src/Entity/ChallengeDto/FallableQueryResultDto.php create mode 100644 src/Entity/ChallengeDto/QueryResultDto.php create mode 100644 src/Service/DbRunnerComparer.php delete mode 100644 src/Service/Types/DbRunnerProcessResponse.php rename src/Twig/Components/Challenge/{Comment.php => Comments.php} (87%) rename src/Twig/Components/Challenge/{CommentModule => Comments}/Comment.php (97%) rename src/Twig/Components/Challenge/{CommentModule => Comments}/CommentForm.php (97%) delete mode 100644 src/Twig/Components/Challenge/Payload.php delete mode 100644 src/Twig/Components/Challenge/Payload/ErrorPayload.php delete mode 100644 src/Twig/Components/Challenge/Payload/ErrorProperty.php delete mode 100644 src/Twig/Components/Challenge/Payload/ResultPayload.php delete mode 100644 src/Twig/Components/Challenge/ResultPresenterModule/AnswerPresenter.php delete mode 100644 src/Twig/Components/Challenge/ResultPresenterModule/DiffPresenter.php delete mode 100644 src/Twig/Components/Challenge/ResultPresenterModule/Table.php rename src/Twig/Components/Challenge/{ResultPresenter.php => Tabs.php} (50%) create mode 100644 src/Twig/Components/Challenge/Tabs/AnswerQueryResult.php rename src/Twig/Components/Challenge/{ResultPresenterModule/EventPresenter.php => Tabs/Events.php} (90%) rename src/Twig/Components/Challenge/{ResultPresenterModule => Tabs}/Pagination.php (95%) create mode 100644 src/Twig/Components/Challenge/Tabs/QueryResultTable.php create mode 100644 src/Twig/Components/Challenge/Tabs/UserQueryResult.php rename templates/components/Challenge/{Comment.html.twig => Comments.html.twig} (65%) rename templates/components/Challenge/{CommentModule => Comments}/Comment.html.twig (100%) rename templates/components/Challenge/{CommentModule => Comments}/CommentForm.html.twig (100%) delete mode 100644 templates/components/Challenge/ResultPresenter.html.twig delete mode 100644 templates/components/Challenge/ResultPresenterModule/AnswerPresenter.html.twig delete mode 100644 templates/components/Challenge/ResultPresenterModule/DiffPresenter.html.twig create mode 100644 templates/components/Challenge/Tabs.html.twig create mode 100644 templates/components/Challenge/Tabs/AnswerQueryResult.html.twig rename templates/components/Challenge/{ResultPresenterModule/EventPresenter.html.twig => Tabs/Events.html.twig} (91%) rename templates/components/Challenge/{ResultPresenterModule => Tabs}/Pagination.html.twig (100%) rename templates/components/Challenge/{ResultPresenterModule/Table.html.twig => Tabs/QueryResultTable.html.twig} (82%) create mode 100644 templates/components/Challenge/Tabs/UserQueryResult.html.twig 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/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/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/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..aaa2d8e 100644 --- a/src/Twig/Components/Challenge/Description.php +++ b/src/Twig/Components/Challenge/Description.php @@ -27,13 +27,13 @@ public function getColumnsOfAnswer(): array { try { $answer = $this->questionDbRunnerService->getAnswerResult($this->question); + $answerResult = $answer->getResult(); - // check if we have at least one row - if (0 === \count($answer)) { + if (0 === \count($answerResult)) { return []; } - return array_keys($answer[0]); + return $answer->getResult()[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..070ecc1 100644 --- a/src/Twig/Components/Challenge/Instruction/Content.php +++ b/src/Twig/Components/Challenge/Instruction/Content.php @@ -4,7 +4,11 @@ namespace App\Twig\Components\Challenge\Instruction; +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; 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 +17,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(LoggerInterface $logger, #[LiveArg] #[MapRequestPayload] HintPayload $hint): void + { + $logger->debug('Received hint', ['hint' => $hint]); + $this->hint = $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 50% rename from src/Twig/Components/Challenge/ResultPresenter.php rename to src/Twig/Components/Challenge/Tabs.php index 456857a..01ee85d 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; @@ -23,7 +21,7 @@ final class ResultPresenter * @var string[] */ public array $tabs = [ - 'result', 'answer', 'diff', 'events', + 'result', 'answer', 'events', ]; /** @@ -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..edc5b97 --- /dev/null +++ b/src/Twig/Components/Challenge/Tabs/AnswerQueryResult.php @@ -0,0 +1,46 @@ +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/ResultPresenterModule/EventPresenter.php b/src/Twig/Components/Challenge/Tabs/Events.php similarity index 90% rename from src/Twig/Components/Challenge/ResultPresenterModule/EventPresenter.php rename to src/Twig/Components/Challenge/Tabs/Events.php index 0f4fc53..e446fea 100644 --- a/src/Twig/Components/Challenge/ResultPresenterModule/EventPresenter.php +++ b/src/Twig/Components/Challenge/Tabs/Events.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Twig\Components\Challenge\ResultPresenterModule; +namespace App\Twig\Components\Challenge\Tabs; use App\Entity\Question; use App\Entity\SolutionEvent; @@ -13,12 +13,12 @@ use Symfony\UX\LiveComponent\DefaultActionTrait; #[AsLiveComponent] -final class EventPresenter +final class Events { use DefaultActionTrait; use Pagination; - #[LiveProp(updateFromParent: true)] + #[LiveProp] public Question $question; #[LiveProp] 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..1ee9df0 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; @@ -29,44 +24,9 @@ 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/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/ResultPresenter.html.twig b/templates/components/Challenge/ResultPresenter.html.twig deleted file mode 100644 index 98abd2d..0000000 --- a/templates/components/Challenge/ResultPresenter.html.twig +++ /dev/null @@ -1,26 +0,0 @@ - - - -
- {% if currentTab == 'answer' %} - - {% elseif currentTab == 'diff' %} - - {% elseif currentTab == 'events' %} - - {% else %} - - {% endif %} -
-
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/ResultPresenterModule/DiffPresenter.html.twig b/templates/components/Challenge/ResultPresenterModule/DiffPresenter.html.twig deleted file mode 100644 index ee77b2c..0000000 --- a/templates/components/Challenge/ResultPresenterModule/DiffPresenter.html.twig +++ /dev/null @@ -1,173 +0,0 @@ - - {% set diff = this.diff %} - - {% if diff %} - {{ diff|raw }} - - - {% elseif this.diff is same as('') %} - - {% else %} - - {% endif %} - diff --git a/templates/components/Challenge/Tabs.html.twig b/templates/components/Challenge/Tabs.html.twig new file mode 100644 index 0000000..db4c361 --- /dev/null +++ b/templates/components/Challenge/Tabs.html.twig @@ -0,0 +1,24 @@ + + + +
+ {% if currentTab == 'answer' %} + + {% elseif currentTab == 'events' %} + + {% 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..a790f44 --- /dev/null +++ b/templates/components/Challenge/Tabs/AnswerQueryResult.html.twig @@ -0,0 +1,11 @@ +
+ {% set answer = this.answer %} + + {% if answer.errorMessage %} + + {% endif %} + + +
diff --git a/templates/components/Challenge/ResultPresenterModule/EventPresenter.html.twig b/templates/components/Challenge/Tabs/Events.html.twig similarity index 91% rename from templates/components/Challenge/ResultPresenterModule/EventPresenter.html.twig rename to templates/components/Challenge/Tabs/Events.html.twig index 15a9e53..81ba43d 100644 --- a/templates/components/Challenge/ResultPresenterModule/EventPresenter.html.twig +++ b/templates/components/Challenge/Tabs/Events.html.twig @@ -24,5 +24,5 @@ - {{ include('components/Challenge/ResultPresenterModule/Pagination.html.twig') }} + {{ include('components/Challenge/Tabs/Pagination.html.twig') }} 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..330d91b --- /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..41b1f4a 100644 --- a/templates/components/Challenge/Ui.html.twig +++ b/templates/components/Challenge/Ui.html.twig @@ -30,15 +30,15 @@
- - + +
{% if appfeatures.comment %}

留言區

- +
{% endif %} 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..c781ff3 100644 --- a/translations/messages.zh_TW.yaml +++ b/translations/messages.zh_TW.yaml @@ -146,3 +146,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% From f7bb7674c167913a650b376d938da9acc7c2f39a Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 01:42:37 +0800 Subject: [PATCH 03/13] refactor: Remove unused strings --- translations/messages.zh_TW.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/translations/messages.zh_TW.yaml b/translations/messages.zh_TW.yaml index c781ff3..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: 答案比較 From 19987b2825573e655209505771227c281bf96c5f Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 01:43:11 +0800 Subject: [PATCH 04/13] feat(challenge): Add "diff presenter" back --- src/Twig/Components/Challenge/Tabs.php | 2 +- .../Challenge/Tabs/DiffPresenter.php | 119 ++++++++++++ templates/components/Challenge/Tabs.html.twig | 2 + .../Challenge/Tabs/DiffPresenter.html.twig | 173 ++++++++++++++++++ 4 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 src/Twig/Components/Challenge/Tabs/DiffPresenter.php create mode 100644 templates/components/Challenge/Tabs/DiffPresenter.html.twig diff --git a/src/Twig/Components/Challenge/Tabs.php b/src/Twig/Components/Challenge/Tabs.php index 01ee85d..ffe2dce 100644 --- a/src/Twig/Components/Challenge/Tabs.php +++ b/src/Twig/Components/Challenge/Tabs.php @@ -21,7 +21,7 @@ final class Tabs * @var string[] */ public array $tabs = [ - 'result', 'answer', 'events', + 'result', 'answer', 'diff', 'events', ]; /** 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/templates/components/Challenge/Tabs.html.twig b/templates/components/Challenge/Tabs.html.twig index db4c361..b069252 100644 --- a/templates/components/Challenge/Tabs.html.twig +++ b/templates/components/Challenge/Tabs.html.twig @@ -17,6 +17,8 @@ {% elseif currentTab == 'events' %} + {% elseif currentTab == 'diff' %} + {% else %} {% endif %} diff --git a/templates/components/Challenge/Tabs/DiffPresenter.html.twig b/templates/components/Challenge/Tabs/DiffPresenter.html.twig new file mode 100644 index 0000000..effd043 --- /dev/null +++ b/templates/components/Challenge/Tabs/DiffPresenter.html.twig @@ -0,0 +1,173 @@ + + {% set diff = this.diff %} + + {% if diff %} + {{ diff|raw }} + + + {% elseif this.diff is same as('') %} + + {% else %} + + {% endif %} + From 893ecea8eb6c501bfe0236539651332d33f4df4f Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 01:44:25 +0800 Subject: [PATCH 05/13] refactor(challenge): Make AnswerQueryResult static It seldom changes. --- .../Components/Challenge/Tabs/AnswerQueryResult.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Twig/Components/Challenge/Tabs/AnswerQueryResult.php b/src/Twig/Components/Challenge/Tabs/AnswerQueryResult.php index edc5b97..3934c6f 100644 --- a/src/Twig/Components/Challenge/Tabs/AnswerQueryResult.php +++ b/src/Twig/Components/Challenge/Tabs/AnswerQueryResult.php @@ -7,26 +7,21 @@ use App\Entity\ChallengeDto\FallableQueryResultDto; use App\Entity\Question; use App\Service\QuestionDbRunnerService; -use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; -use Symfony\UX\LiveComponent\Attribute\LiveProp; -use Symfony\UX\LiveComponent\DefaultActionTrait; +use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; use function Symfony\Component\Translation\t; -#[AsLiveComponent] +#[AsTwigComponent] final class AnswerQueryResult { - use DefaultActionTrait; - public function __construct( private readonly QuestionDbRunnerService $questionDbRunnerService, ) { } /** - * @var Question $question the question to present the answer + * @var Question the question to present the answer */ - #[LiveProp] public Question $question; public function getAnswer(): FallableQueryResultDto From a9e386e17376ee78e4c533d5e31635daaa28760f Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 01:46:04 +0800 Subject: [PATCH 06/13] fix(challenge): Update events when the new query submitted --- src/Twig/Components/Challenge/Tabs/Events.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Twig/Components/Challenge/Tabs/Events.php b/src/Twig/Components/Challenge/Tabs/Events.php index e446fea..8c0d084 100644 --- a/src/Twig/Components/Challenge/Tabs/Events.php +++ b/src/Twig/Components/Challenge/Tabs/Events.php @@ -9,6 +9,7 @@ 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; @@ -54,4 +55,9 @@ protected function getData(): array offset: ($this->page - 1) * self::limit, ); } + + #[LiveListener('app:challenge-executor:query-created')] + public function onQueryUpdated(): void + { + } } From 907582a4fdda0cfd5d3ad1fa68e035fef27d3340 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 01:49:48 +0800 Subject: [PATCH 07/13] fix(challenge): Broken answer --- .../components/Challenge/Tabs/AnswerQueryResult.html.twig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/components/Challenge/Tabs/AnswerQueryResult.html.twig b/templates/components/Challenge/Tabs/AnswerQueryResult.html.twig index a790f44..a886f3c 100644 --- a/templates/components/Challenge/Tabs/AnswerQueryResult.html.twig +++ b/templates/components/Challenge/Tabs/AnswerQueryResult.html.twig @@ -7,5 +7,7 @@ {% endif %} - + {% if answer.result %} + + {% endif %} From 05fc142e7762536e306e61bed3e8e6802d76be48 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 01:53:30 +0800 Subject: [PATCH 08/13] fix(entity): Specify default value for enums --- src/Entity/Feedback.php | 2 +- src/Entity/Question.php | 2 +- src/Entity/SolutionEvent.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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; From 1a52c2b5a93a2e3a625eac8b040bafca0b35edae Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 01:56:18 +0800 Subject: [PATCH 09/13] test(dbrunner): Adapt for new format --- tests/Service/DbRunnerServiceTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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'); } From 35a326c3d5e1e11af003172f2dc54ba57a9f46fd Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 01:59:08 +0800 Subject: [PATCH 10/13] refactor(challenge): Use i18n string key for no query response --- templates/components/Challenge/Tabs/UserQueryResult.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/components/Challenge/Tabs/UserQueryResult.html.twig b/templates/components/Challenge/Tabs/UserQueryResult.html.twig index 330d91b..56bf83a 100644 --- a/templates/components/Challenge/Tabs/UserQueryResult.html.twig +++ b/templates/components/Challenge/Tabs/UserQueryResult.html.twig @@ -3,7 +3,7 @@ {% if result is null %} {% elseif result.errorMessage is not null %} -

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

+ {% if question.schema %}

Schema ({{ question.schema.id }}): From 3bb0b751d881c85dea9c7a5584863f887179702d Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 02:21:03 +0800 Subject: [PATCH 13/13] fix(challenge): Correct hint implementation The query will not update to Live Component model now, so we should get the query from the last query. Besides, I remove the writable query model as it is completely unused now. --- .../Components/Challenge/Instruction/Content.php | 9 ++++----- .../Components/Challenge/Instruction/Modal.php | 16 ++++++++-------- src/Twig/Components/Challenge/Ui.php | 6 ------ templates/components/Challenge/Ui.html.twig | 4 ++-- 4 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/Twig/Components/Challenge/Instruction/Content.php b/src/Twig/Components/Challenge/Instruction/Content.php index 070ecc1..c1c0ede 100644 --- a/src/Twig/Components/Challenge/Instruction/Content.php +++ b/src/Twig/Components/Challenge/Instruction/Content.php @@ -4,8 +4,7 @@ namespace App\Twig\Components\Challenge\Instruction; -use Psr\Log\LoggerInterface; -use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; +use Symfony\Component\Serializer\SerializerInterface; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveArg; use Symfony\UX\LiveComponent\Attribute\LiveListener; @@ -21,9 +20,9 @@ final class Content public ?HintPayload $hint = null; #[LiveListener('app:challenge-hint')] - public function onHintReceived(LoggerInterface $logger, #[LiveArg] #[MapRequestPayload] HintPayload $hint): void + public function onHintReceived(SerializerInterface $serializer, #[LiveArg] string $hint): void { - $logger->debug('Received hint', ['hint' => $hint]); - $this->hint = $hint; + $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/Ui.php b/src/Twig/Components/Challenge/Ui.php index 1ee9df0..1a5f908 100644 --- a/src/Twig/Components/Challenge/Ui.php +++ b/src/Twig/Components/Challenge/Ui.php @@ -23,10 +23,4 @@ final class Ui #[LiveProp] public int $limit; - - /** - * @var string $query the user's query - */ - #[LiveProp(writable: true)] - public string $query = ''; } diff --git a/templates/components/Challenge/Ui.html.twig b/templates/components/Challenge/Ui.html.twig index 41b1f4a..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 %}